← all branches

feat/discovery

needs attentionviewing older commit
c1f337b · incrementalpre-PRreviewed 2026-07-13 13:30 UTC3H · 7M · 14L · 8I
The branch
Purpose
Feature discovery system for Batu providers — guided tour + spotlight coachmarks to help new users adopt CFE credentials, RPU registration, and monitoring on first run.
Goal
Ship first-run linear product tour (Diego's v1): fixed 5-step sequence anchored to Contratos page, server-backed first-run detection, re-launchable from Descubre hub; plus two prod bug fixes (VERCEL_ENV preview gating, spotlight self-destruct).
Sub-goals
  • SG-1: VERCEL_ENV fix — replace insecure Host-header preview check with VERCEL_ENV=preview (build-time, not client-controllable)
  • SG-2: Spotlight latch — keep shown card stable across the seen-POST self-invalidating refetch that made cards vanish sub-second in prod
  • SG-3: Atomic upsert — replace fetch-decide-insert-or-update (race + stale lock) with INSERT ON CONFLICT DO UPDATE using SQL deltas
  • SG-4: Gate keys never retire — cfe_setup and add_contract exempt from retirement (cross-org exposure would kill onboarding)
  • SG-5: product_tour budget exclusion — completing the tour must not silence the ambient daily spotlight budget
  • SG-6: GuidedTour component — 5-step overlay with spotlight cutout, Hazlo ahora dispatch, first-run + forced re-launch
The changes (whole branch)
What
Added GuidedTour.tsx (315 lines), tour-catalog.ts, spotlight latch (computeSpotlightLatch); refactored discovery upsert from insert/updateWithVersion to upsertOutcome; added tourSeen to gap response; fixed isPreviewHost to isPreviewDeployment; added gate-key never-retire + product_tour budget exclusion; new analytics events; new tests for latch, gate exemption, and createSecret idempotency.
Why
Diego's v1 product tour direction; concurrent with fixing two prod-blocking bugs found in pre-merge review.
Areas
apps/platform/src/components/discovery+70030domains/core/src/discovery+320120domains/cross-domain/src+94050packages/api/src/schemas+100packages/analytics/src+700packages/database/src/schema+1105packages/secrets+15010apps/platform/next.config.ts+110
Blast
50 files, +3630/−31 across discovery domain, cross-domain coordinator, API schemas, analytics, DB schema, and secrets. Touches dashboard layout (GuidedTour mount), gap/spotlight API response (tourSeen field), and the discovery_state upsert path used by every spotlight event.
openedRef blocks hub re-launch in same session (confirmed medium bug) 3 high test gaps: isBudgetSpent exclusion, tourSeen shell, GuidedTour state machine outbox event omission in recordSpotlightOutcomeShell (ADR-016 deviation, v1 OK if documented)
CI· No CI checks — no open PR for this branch yetcoderabbit· No .coderabbit.yaml present

Findings · 27

correctness3

medium

openedRef blocks hub re-launch after first open in same session

apps/platform/src/components/discovery/GuidedTour.tsx:92

GuidedTour mounts in the layout and never unmounts. openedRef.current is set to true on first open and never reset. A second hub re-launch (?tour=1) in the same session silently does nothing. Fix: if (!forced && openedRef.current) return; and reset openedRef.current = false on forced re-launch.

low

Anchor-locate and close() can double-invoke outcome report

apps/platform/src/components/discovery/GuidedTour.tsx:163

If locate() fires its last retry concurrently with a user clicking Skip, both paths call report(). Upsert is idempotent (COALESCE on clickedAt, additive deltas of 0), so no data corruption, but two requests fire.

info

tourSeen is user-scoped, not org-scoped

domains/cross-domain/src/feature-discovery.shells.ts:154

A user who saw the tour in org A has tourSeen=true in org B. Consistent with acknowledged cross-org staleness trade-off. Full fix: orgId on discovery_state (deferred).

security3

low

?descubre URL param forwarded to API without client-side enum validation

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

Forwarded without safeParse. Server-side Zod rejects invalid values, so no data leakage. A client-side guard eliminates unnecessary round-trips.

info

NEXT_PUBLIC_VERCEL_ENV intentionally discloses deployment tier

apps/platform/next.config.ts

By design. No secret material. Comment in next.config.ts explains the tradeoff.

info

In-memory rate limit not shared across Lambda instances (pre-existing)

apps/platform/src/middleware

Pre-existing. recordOutcome endpoint callable in a tight loop across warm instances. Blast radius: inflated seenCount/dismissCount counters only.

conventions4

medium

console.error used instead of createShellLogger in cross-domain shells

domains/cross-domain/src/feature-discovery.shells.ts:113

getDiscoverySpotlightShell and getDiscoveryGapShell use console.error. The project's structured logger (createShellLogger) is the canonical pattern for domain shells.

medium

recordSpotlightOutcomeShell omits outbox event emission

domains/core/src/discovery/discovery.shells.ts:42

ADR-016: every state-mutating shell writes entity + outbox event atomically. This shell mutates discovery_state but emits no outbox event. If intentional (no subscribers yet), document as an ADR exception.

medium

upsertOutcome deviates from canonical query naming convention

domains/core/src/discovery/discovery.queries.ts:46

Canonical naming: insert, updateWithVersion, findBy{Field}. The new upsertOutcome name is semantically accurate but does not follow the prescribed pattern.

low

Shell JSDoc still describes stale FCIS fetch-then-decide flow

domains/core/src/discovery/discovery.shells.ts:38

JSDoc comment on recordSpotlightOutcomeShell describes the removed fetch step (1. FETCH, 2. DECIDE, 3. EXECUTE). The implementation skips the fetch entirely.

tests7

high

isBudgetSpent product_tour exclusion untested

domains/cross-domain/src/__tests__/feature-discovery.decisions.test.ts

The isBudgetSpent filter that excludes product_tour rows has no test. A regression would block every spotlight for every user who has taken the tour.

high

getDiscoveryGapShell tourSeen logic has zero test coverage

domains/cross-domain/src/__tests__/feature-discovery.integration.test.ts

Both new behaviors in getDiscoveryGapShell (computing tourSeen from product_tour row, returning tourSeen:true for consumer orgs) are untested. A wrong value re-shows the tour every session or never shows it on first run.

high

GuidedTour state machine has no tests

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

315-line component with non-trivial logic (openedRef guard, anchor retry loop, step navigation, forced re-launch, hazloAhora dispatch) has zero tests.

medium

createSecret pending-deletion + tags combination path untested

packages/secrets/__tests__/unit/secrets-manager.createSecret.test.ts:77

Pending-deletion test has no tags variant. Source runs TagResourceCommand after Update when tags supplied. Compare: ResourceExists path has a tags variant.

medium

resolveSpotlightPage and pathForPage have no unit tests

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

Pure functions with non-trivial edge cases (startsWith match, fallback path) called on every route change. The spotlight-latch test file only tests computeSpotlightLatch.

medium

upsertOutcome dismissed path: lastSeenAt immutability not asserted in integration test

domains/cross-domain/src/__tests__/feature-discovery.integration.test.ts:161

The core pacing invariant (a dismiss must not spend the daily budget) — lastSeenAt unchanged after dismiss — is not asserted at the DB level in the integration test.

low

spotlight-latch forceAdopt idempotent case not pinned

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

forceAdopt=true with the SAME key as the shown key is a silent no-op (hits prev.key guard first). Correct behavior but not pinned — a guard reordering breaks it without a test failing.

improvement10

low

product_tour key hardcoded as string literal in cross-domain shell

domains/cross-domain/src/feature-discovery.shells.ts:154

TOUR_KEY is defined in tour-catalog.ts (frontend). Backend shell can't import it; string is duplicated. Should live in discovery-state.type.ts so renaming is caught at compile time.

low

DiscoveryTourSkippedEvent.properties typed as Record<string, never>

packages/analytics/src/events.ts

Rejects any property at compile time. Inconsistent with other tour events (started, completed, step_viewed) which carry contextual properties. Use properties?: { step?: string; stepIndex?: number }.

low

hazloAhora click-anchor path silently no-ops if anchor absent

apps/platform/src/components/discovery/GuidedTour.tsx:130

?.click() silently does nothing if the anchor isn't found. The anchor-retry loop emits discovery.spotlight_anchor_missing on miss; this path has no tripwire.

low

GuidedTour flagEnabled unsubscribe pattern inconsistent with siblings

apps/platform/src/components/discovery/GuidedTour.tsx:46

Returns posthog?.onFeatureFlags?.(cb) directly. DescubreMenu and FeatureSpotlight wrap it: () => unsubscribe?.(). Fragile if PostHog changes the return type.

low

Double-report when hazloAhora triggers

apps/platform/src/components/discovery/GuidedTour.tsx:124

onHazloAhora duplicates the report+track+setOpen sequence from close(). If close() gains cleanup logic, the hazloAhora path will silently miss it. Call close('completed') then apply the action.

low

cardPos useMemo reads window dimensions without resize dependency

apps/platform/src/components/discovery/GuidedTour.tsx:209

Resize listener updates rect but not window.innerWidth/innerHeight. On a vertical-only resize, placeBelow may be stale. Pre-existing in FeatureSpotlight; now in two card components.

low

Magic card-height estimate (190) inconsistent with FeatureSpotlight (180)

apps/platform/src/components/discovery/GuidedTour.tsx:214

190 may be too small for GuidedTour's taller layout (Hazlo ahora row + nav + skip). Should be a named constant; inconsistency with 180 should be documented.

info

tourSeen defaults to true on gap query failure

apps/platform/src/components/discovery/GuidedTour.tsx:70

Fail-safe and intentional (transient 500 suppresses first-run rather than re-showing on every error). Worth documenting in inline comment.

info

DescubreMenu navigates to TOUR_PAGE_PREFIX even when already there

apps/platform/src/components/discovery/DescubreMenu.tsx:155

Adds unnecessary history entry when already on /bills/contratos. router.replace or pathname check would avoid this.

info

CARD_WIDTH constants duplicated in GuidedTour (340) and FeatureSpotlight (320)

apps/platform/src/components/discovery/GuidedTour.tsx:31

Intentionally different values but no naming to signal the difference. Named constants (TOUR_CARD_WIDTH / SPOTLIGHT_CARD_WIDTH) would make the distinction explicit.

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: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:30current
  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