← all branches

feat/discovery

needs attentionviewing older commit
ab7c103 · incrementalPR #305reviewed 2026-07-20 15:52 UTC0H · 1M · 6L · 4I
The branch
Purpose
Batu platform feature discovery system — spotlight coachmarks, Descubre hub checklist, and first-run guided tour for provider orgs
Goal
Shipping Feature Discovery v1: contextual capability coachmarks (server-decided), a Descubre hub with a capability checklist, and a first-run product tour; fixing the hub's pull→pull jam by replacing the floating pull card with a navigate+pulse cue
Sub-goals
  • SG-1: Core FCIS domain (discovery-state entity, decisions, shells, queries)
  • SG-2: API contract + handler + mapper for GET /spotlight and POST /spotlight/:key/record
  • SG-3: FeatureSpotlight ambient coachmark (flag-gated, desktop, server-decided)
  • SG-4: DescubreMenu hub (checklist, progress badge, 'Volver al tour' entry)
  • SG-5: GuidedTour + RpuSubTour (first-run linear tour, flag-gated)
  • SG-6: FeatureHighlight pulse cue (hub navigate+pulse, replaces floating pull-card)
The changes (whole branch)
What
Replaced the hub's ?descubre-driven floating pull-card (server query + mounted coachmark) with a lighter FeatureHighlight component (DOM querySelector + ring animation + param strip). Removed ?revisit mode. Unified tutorialHref so both done and available rows use ?descubre. Simplified FeatureSpotlight to ambient-only (no requested/revisit params). Added two new analytics events (highlight_shown, highlight_anchor_missing).
Why
The floating pull-card pattern kept jamming on pull→pull: clicking one hub row then another left both cards vanishing and neither reopening. A pulse is a pure function of the URL param — no mounted identity to strand, no round-trip, self-clearing — so switching between hub items is a clean param change.
Areas
apps/platform/src/components/discovery+120050apps/platform/src/app+43035apps/platform/src/api+28010domains/core/src/discovery+3100domains/cross-domain/src+8350packages/analytics/src+1150packages/api/src/schemas+990packages/database/drizzle+133700e2e/platform+610
Blast
66 files, +18723/-65 whole-branch (dominated by drizzle meta snapshot); incremental window is 9 files, +273/-124 — limited to discovery UI components and analytics types
feature-discovery-spotlights (PostHog, flag-gated entry point for spotlights and hub)
CI· Token does not have statusCheckRollup accessCodeRabbit· No .coderabbit.yaml in repo

Findings · 13

correctness3

medium

FeatureHighlight fires without the discovery feature flag

apps/platform/src/components/discovery/FeatureHighlight.tsx:34

FeatureSpotlight gates on `(flagEnabled || isPreviewDeployment()) && isDesktop && !!page && !!orgId`. FeatureHighlight has no such guard — it processes any `?descubre=<key>` URL unconditionally. DescubreMenu is flag-gated so ordinary users won't see the link, but a stale bookmark or hand-crafted URL will pulse the control even when `feature-discovery-spotlights` is disabled. Failure mode is benign (visual only, param is stripped), but the flag doesn't fully suppress the surface. Also: DescubreMenu is md-gated (768px) while FeatureSpotlight is lg-gated (1024px), so on tablets (768–1024px) the hub can generate ?descubre links whose visual cue has no breakpoint guard.

low

Stale ring position during rapid key A→B switch

apps/platform/src/components/discovery/FeatureHighlight.tsx:60

When the user clicks a second hub item before the first pulse expires, the locate effect cleanup clears A's timers but does not call `setRect(null)`. Because `key` is now B but `rect` still holds A's DOMRect, the ring briefly renders at A's position during the `setTimeout(0)` + retry window (up to 300ms if B's element is async). Adding `setRect(null)` as the first statement in the locate effect for truthy `key` eliminates the artefact.

low

Dead `typeof document === 'undefined'` guard after hooks

apps/platform/src/components/discovery/FeatureHighlight.tsx:116

This guard is dead code in a `'use client'` component — document is always defined. If ever reached in SSR, all the hooks above would have thrown first. Remove it or move the check before any hooks.

security2

low

querySelector key not CSS.escape()d — safe today, fragile by convention

apps/platform/src/components/discovery/FeatureHighlight.tsx:68

`[data-tour="${key}"]` is safe today because `key` is catalog-validated. But if a catalog key ever contains a CSS metacharacter (dot, bracket, quote), the selector silently misbehaves or throws. `CSS.escape(key)` would make the contract independent of catalog authors remembering to avoid those characters.

info

router.replace strip path is safe — no open redirect

apps/platform/src/components/discovery/FeatureHighlight.tsx:53

The strip path derives the query string from the browser's own URLSearchParams (not the raw param value) and calls next-intl's internal router.replace. No open-redirect surface.

conventions3

low

Multi-paragraph docstrings prohibited by CLAUDE.md

apps/platform/src/components/discovery/FeatureHighlight.tsx:3

The file-level JSDoc (lines 3–19) is an 18-line, 3-paragraph rationale block ('Why a pulse and not a floating card...'). CLAUDE.md prohibits multi-paragraph docstrings. Also: HIGHLIGHT_PARAM in spotlight-catalog.ts has a 10-line multi-paragraph JSDoc (lines 72–82). Architectural rationale belongs in ADRs or commit messages.

low

eslint-disable masking non-idiomatic deps expression

apps/platform/src/components/discovery/FeatureHighlight.tsx:113

The scroll/resize effect uses `[rect !== null, key]` with `// eslint-disable-line react-hooks/exhaustive-deps`. Passing a derived boolean into deps is non-idiomatic. Extracting `const pulsing = rect !== null` and using `[pulsing, key]` expresses the intent clearly with no suppression needed.

info

Redundant SSR guard in render return of 'use client' component

apps/platform/src/components/discovery/FeatureHighlight.tsx:116

`typeof document === 'undefined'` in the render return of a `'use client'` component is always false — client components never run server-side in App Router. The guard is dead code.

tests2

low

resolveSpotlightPage and pathForPage have no direct unit tests

apps/platform/src/components/discovery/spotlight-catalog.ts:34

Both are pure functions with branching logic (sub-path matching, fallback path) exercised only indirectly via tutorialHref tests. A handful of direct assertions would give complete coverage cheaply.

info

resolveHighlightKey coverage is complete

apps/platform/src/components/discovery/__tests__/highlight-key.test.ts

Full catalog enumeration, prototype-chain guards (constructor/toString), and absent/empty inputs are all covered. No gaps.

improvement3

low

Locate retry constants duplicated between FeatureHighlight and FeatureSpotlight

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

FeatureHighlight.tsx defines LOCATE_RETRY_MS=300 and LOCATE_MAX_ATTEMPTS=10 as named constants; FeatureSpotlight.tsx still uses bare magic numbers 300 and 10 in its locate loop. If the retry budget needs tuning, both files must be updated in sync. Centralise in spotlight-catalog.ts or a shared constants file.

low

Effect 3 re-queries DOM per scroll event instead of caching element ref

apps/platform/src/components/discovery/FeatureHighlight.tsx:103

Effect 3 runs `document.querySelector(...)` on every scroll/resize event. FeatureSpotlight avoids this by caching the element in `anchorRef.current` (with `.isConnected` guard). Caching the found element in a ref in Effect 2 and reading it in Effect 3 saves a DOM query per event and makes the element-gone case explicit.

info

Effect 1 no-dep-array pattern benefits from an inline clarifying comment

apps/platform/src/components/discovery/FeatureHighlight.tsx:48

The no-dep-array effect that keeps stripRef and pageRef current is correct and intentional, but maintainers may add a dep array as a 'fix'. An inline 'intentional: no dep array — kept current every render' note would make it unmissable.

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:52current
  9. 7eff611needs attentionincremental0H · 1M · 2L2026-07-18 00:41
  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