Skip to content

feat: add context-aware getting-started onboarding checklist - #3084

Merged
kodiakhq[bot] merged 14 commits into
mainfrom
brandon/brandon-onboarding-steps
Sep 14, 2026
Merged

kodiakhq[bot] merged 14 commits into
mainfrom
brandon/brandon-onboarding-steps

Conversation

@brandon-pereira

@brandon-pereira brandon-pereira commented Sep 4, 2026

Copy link
Copy Markdown
Member

What

Adds a second phase to the sidebar onboarding checklist. After the existing setup steps (connect ClickHouse, create sources, add data) complete, a product-usage phase tracks four "getting started" milestones per user:

  • Explore your data — run a search with a filter or query condition
  • Build a dashboard — add a chart tile to a dashboard
  • Set up an alert — create or edit an alert
  • Connect the MCP server — make a successful MCP tool call

Each task links to where to do it, the card can be dismissed, and a brief celebration shows when the final task is completed in-session.

Why

The existing checklist stops once a team is technically set up. New users still need a nudge toward the actions that deliver value (searching, charting, alerting, querying via an agent). This extends the same surface to cover first real usage without adding a separate onboarding UI.

How it works

  • Single source of truth: the task registry (ONBOARDING_TASK_IDS) lives in common-utils. It's typed so adding a task is a compile error until both the API validation enum and the frontend UI (copy + link) are updated — a new task can't be silently untracked.
  • Surface-agnostic completion: alert and dashboard tasks complete whether the action came from the UI, external REST API v2, or an MCP tool (all authenticate with the user's personal access key). Recorded server-side and fire-and-forget, so onboarding bookkeeping never blocks or fails the triggering write.
  • "Dashboard" means a chart, not a shell: the dashboard task completes only once a dashboard has at least one tile — including tiles added to a temporary (unsaved, URL-state) dashboard, which the frontend records directly since it never touches the backend.
  • Explore data: completes on any non-trivial search (a non-empty where clause in Lucene or SQL, or an applied filter); a blank default search doesn't count.
  • MCP: recorded in the tool-tracing chokepoint — a successful tool call is the only reliable signal the user exercised the server.
  • Cache hygiene: after a UI action the frontend patches only onboardingData in the cached me object — no me refetch — so the many useMe consumers (metadata, ClickHouse settings, nav) are untouched.
  • Derived "done": the checklist's completed state is derived from whether every current task is complete rather than a persisted flag, so adding or changing a task later automatically reopens the checklist for users who finished the old set. The persisted isDismissed is only for the manual X (opting out early).
  • Read-tolerant / write-strict schema: the read path drops persisted task ids no longer in ONBOARDING_TASK_IDS (so removing a task can't 500 GET /me), while the write boundary stays a strict enum. onboardingData is defaulted for users created before the field existed.

Demo

demo.mp4

Persistence

New optional user.onboardingData subdocument (completedTasks, isDismissed). Two new routes: POST /me/onboarding/task and PATCH /me/onboarding/dismiss.

Testing

  • make ci-lint — lint + tsc + styles + escape-hatch ratchet: green
  • make ci-unit scope — app (onboarding suites + DBSearchPage + dashboard, 274 tests), common-utils (2424), api (825): all pass
  • Integration tests updated (me.int.test.ts, external dashboards.int.test.ts); they require the Docker stack (make dev-int) and were not run in this session.

Add a second onboarding phase to the sidebar checklist that tracks
product-usage milestones per user (explore data, build a dashboard, set
up an alert, use the MCP server), persisted on user.onboardingData.

Completion is recorded server-side so it is surface-agnostic: alert and
dashboard tasks complete from the UI, external REST API v2, or MCP tools.
The dashboard task requires at least one tile (a chart), not an empty
shell, including tiles added to temporary URL-state dashboards. Exploring
data completes on any non-trivial search; MCP usage is recorded in the
tool-tracing chokepoint. All recording is fire-and-forget.

The task registry (ONBOARDING_TASK_IDS) is a single source of truth in
common-utils, typed so adding a task is a compile error until both the API
validation enum and the frontend UI are updated. The checklist's done
state is derived from whether every current task is complete, so adding a
task later reopens the checklist for previously-finished users.
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9dabd7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Minor
@hyperdx/api Minor
@hyperdx/app Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 14, 2026 5:49pm UTC
hyperdx-storybook Ready Ready Preview Sep 14, 2026 5:49pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a context-aware second onboarding phase that records per-user product milestones across frontend, API, external API, and MCP workflows.

  • Persists completion and dismissal state in the user document through authenticated /me/onboarding routes.
  • Records dashboard, alert, advanced-search, and MCP milestones at their relevant execution paths.
  • Derives checklist visibility and completion from team eligibility, setup state, and the shared task registry.
  • Updates the cached /me onboarding state without refetching unrelated user data.

Confidence Score: 5/5

The onboarding changes appear safe to merge; no new actionable failures remain after the latest updates.

No accepted new findings remain. Every previous Greptile thread was manually resolved, including the checklist eligibility, query gating, cache-concurrency, search-submission, semantic-color, and file-size findings. The final semantic-color thread was also correctly withdrawn after brandon-pereira explained that Mantine’s dimmed alias is an approved semantic value.

Important Files Changed

Filename Overview
packages/common-utils/src/types.ts Defines the canonical onboarding task registry, schemas, API contracts, and persistable-user guard.
packages/api/src/controllers/user.ts Adds idempotent persistence helpers and non-blocking product-action recording.
packages/api/src/routers/api/me.ts Exposes authenticated task-completion and dismissal routes and defaults legacy user state.
packages/api/src/controllers/dashboard.ts Records dashboard completion only after a persisted dashboard contains at least one tile.
packages/api/src/controllers/alerts.ts Records alert completion across create, edit, and dashboard-tile alert paths.
packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts Derives onboarding eligibility, phase progression, visibility, and celebration state.
packages/app/src/api.ts Adds onboarding mutations with concurrency-safe cached /me state updates.
packages/app/src/DBSearchPage.tsx Records qualifying interactive searches while excluding programmatic catch-up submissions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[User action] --> B{Action surface}
    B -->|Search UI| C[POST onboarding task]
    B -->|Dashboard or alert API| D[Server-side recorder]
    B -->|MCP tool| D
    C --> E[(User onboardingData)]
    D --> E
    E --> F[GET /me]
    F --> G[Onboarding checklist]
    G --> H{All current tasks complete?}
    H -->|No| I[Show next task]
    H -->|Yes| J[Show completion state]
Loading

Reviews (12): Last reviewed commit: "Merge branch 'main' into brandon/brandon..." | Re-trigger Greptile

Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts
Comment thread packages/app/src/api.ts
Comment thread packages/app/src/OnboardingChecklist/OnboardingChecklist.tsx Outdated
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts Outdated
Comment thread packages/api/src/routers/api/__tests__/me.int.test.ts
Comment thread packages/api/src/controllers/dashboard.ts
Comment thread packages/api/src/routers/external-api/v2/dashboards.ts Outdated
Comment thread packages/app/src/api.ts
Comment thread packages/app/src/OnboardingChecklist/OnboardingChecklist.tsx Outdated
Comment thread packages/app/src/OnboardingChecklist/onboardingTasks.ts Outdated
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts Outdated
Comment thread packages/app/src/DBSearchPage.tsx Outdated
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR Review

3 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 3 minor

3 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Deep Review

No critical issues found. No P0/P1 defects surfaced across the reviewers that returned or in a manual read of every changed hot path. The largest prior concerns (team-age gate removal, unbounded system.tables polling, cache-clobbering task responses, unguarded onboardingData deref, flaky fire-and-forget integration tests, duplicated dashboard-tile rule, raw palette colors) are all confirmed addressed in the current diff. Milestone recording is correctly session-scoped, authz-safe, and non-blocking; the tightened MeApiResponseSchema is safe across a rolling deploy because the client type-casts rather than runtime-parses me and all reads are optional-chained.

🟡 P2 — recommended

  • packages/app/src/api.ts:102 — The useCompleteOnboardingTask onSuccess Set-union merge (written specifically to survive out-of-order responses and preserve a concurrent isDismissed) has no test; it is mocked in every consumer and never exercised.
    • Fix: Add a hook test seeding ['me'] with completedTasks:['alert'], firing onSuccess with completedTasks:['dashboard'], and asserting the union plus that a cached isDismissed:true survives a response carrying false.
  • packages/common-utils/src/types.ts:2913 — The read-tolerant completedTasks transform that drops persisted-but-unknown task ids (the guard that keeps GET /me from 500ing after a task is removed/renamed) is never tested end-to-end.
    • Fix: Add a unit test asserting OnboardingDataSchema.parse({completedTasks:['dashboard','bogus']}) yields ['dashboard'] and an int test that GET /me tolerates a stale id in the DB.
    • testing, api-contract
🔵 P3 nitpicks (6)
  • packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts:80 — The row-count probe interpolates source.from.tableName/databaseName directly into the SQL filter string rather than binding parameters; impact is limited to team-owned config against a read-only system.tables query on the team's own connection.
    • Fix: Use a parameterized ClickHouse bind expression (e.g. table = {tableName:String}) for consistency and defense-in-depth.
  • packages/api/src/controllers/dashboard.ts:274updateDashboard records the dashboard milestone on any edit of a dashboard that already has tiles (rename, tag, tile move), and createOrUpdateDashboardAlerts records alert whenever any tile alert is upserted, so touching a teammate's dashboard can tick both without building anything.
    • Fix: Record dashboard only on a 0 → >0 tile transition (using the already-loaded oldDashboard) and record alert only for tiles whose alert was actually inserted.
  • packages/app/src/api.ts:132useCompleteOnboardingTask (network POST + cache merge) and useMarkOnboardingTaskComplete (cache-only, no request) are near-synonymous names with materially different behavior and are used side-by-side, inviting a caller to pick the wrong one.
    • Fix: Rename to convey persist-vs-sync intent (e.g. usePersistOnboardingTask / useSyncOnboardingTaskCache).
  • packages/app/src/OnboardingChecklist/onboardingTasks.ts:44PRODUCT_TASK_ORDER is a pure re-export of ONBOARDING_TASK_IDS with no transformation, adding an indirection for the same tuple.
    • Fix: Iterate ONBOARDING_TASK_IDS directly unless a divergent display order is actually needed.
  • packages/api/src/controllers/alerts.ts:361createAlert takes userId as a required 2nd positional arg while updateAlert takes it as an optional trailing 5th arg, making it easy for a new call site to silently drop onboarding attribution.
    • Fix: Align the signatures (consistent position or an options object).
  • packages/app/src/DBSearchPage.tsx:1296 — The advancedQuery recording branch (the recordExploration/hasExploredData gating that must credit a genuine search but not a programmatic catch-up submit) is untested; isNonTrivialSearch is unit-tested but its wiring into onSubmit is not.
    • Fix: Add a test asserting completeOnboardingTask.mutate('advancedQuery') fires on a real non-trivial submit and never on debouncedCatchUpSubmit.
    • testing, maintainability

Reviewers (7): security, testing, maintainability, performance, reliability, api-contract, previous-comments.

Coverage caveat: correctness, adversarial, frontend-races, kieran-typescript, and project-standards were dispatched but did not return before synthesis. Their focus areas (celebration-latch effect ordering, cache-race safety, and semantic color tokens) were spot-checked manually and appear sound — the latch is gated on inputsReady, the cache merge unions rather than replaces, and StepRow/OnboardingChecklist use var(--color-*) tokens and variant="success" — but they have not had a dedicated adversarial pass.

Testing gaps:

  • isPersistableUserId's 24-hex guard (which rejects the 12-char strings mongoose.isValidObjectId wrongly accepts) has no direct unit test.
  • recordOnboardingTaskCompletion's error-swallow path is untested — no test forces updateOne to reject and asserts the triggering alert/dashboard save still succeeds with no unhandled rejection.
  • The MCP 'mcp' task recording in mcp/utils/tracing.ts (including the non-persistable-user skip) has no coverage.

- Guard onboardingData deref in useMarkOnboardingTaskComplete so a stale
  me cache can't throw and fail a dashboard save
- Skip the system.tables row-count query once the checklist is dismissed
- Union completedTasks into the me cache instead of replacing, so an
  out-of-order completion response can't drop a just-completed task
- Export recordDashboardOnboardingIfHasTiles and reuse it at all five
  dashboard write sites (DRY)
- Drop PRODUCT_TASK_ORDER_WEIGHT by ordering ONBOARDING_TASK_IDS as the
  display order; remove unused isPhaseComplete from the hook return
- Use Text variant=success for the celebration per semantic-token rule
- Extract isNonTrivialSearch and add a unit test for the advancedQuery
  trigger; poll (waitForTask) in me.int recording tests to de-flake the
  fire-and-forget assertions
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts

// A tile alert never goes through the /alerts router, so this is the only
// place a dashboard-tile alert can complete the onboarding task.
if (result.length > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Dashboard saves credit the alert and dashboard tasks to whoever saved last, not to whoever did the action

createOrUpdateDashboardAlerts records alert whenever the tile map is non-empty, and it is reached from updateDashboardsyncDashboardAlerts on every dashboard save that includes tiles (packages/api/src/controllers/dashboard.ts:104). So user B, who has never configured an alert, drags a tile on a dashboard where user A set up a tile alert, and B's alert task ticks off. The same shape applies at dashboard.ts:252: recordDashboardOnboardingIfHasTiles(userId, updatedDashboard.tiles) runs on any update of a tiled dashboard, so renaming or re-tagging someone else's dashboard completes "Build a dashboard". The frontend mirror markDashboardOnboarding in packages/app/src/dashboard.ts:139 has the same behaviour. Record only when the write actually changed the thing: diff alertsByTile against the existing alerts (the oldAlert lookup already inside the map gives you this) and diff old vs. new tiles in updateDashboard.

Comment thread packages/app/src/DBSearchPage.tsx Outdated
Comment thread packages/app/src/__tests__/OnboardingChecklist.test.tsx Outdated
Comment thread packages/app/src/OnboardingChecklist/StepRow.tsx Outdated
Comment thread packages/app/src/OnboardingChecklist/OnboardingChecklist.tsx Outdated
…rding-steps

# Conflicts:
#	packages/api/src/controllers/alerts.ts
#	packages/api/src/mcp/tools/alerts/saveAlert.ts
#	packages/api/src/routers/api/alerts.ts
#	packages/api/src/routers/external-api/v2/alerts.ts
#	packages/app/src/dashboard.ts
@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Sep 10, 2026
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Critical-path files (4) — tenancy, public API, or shipped database config:
    • packages/api/src/models/user.ts
    • packages/api/src/routers/api/me.ts
    • packages/api/src/routers/external-api/v2/alerts.ts
    • packages/api/src/routers/external-api/v2/dashboards.ts
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 25
  • Production lines changed: 1457 (+ 892 in test files, excluded from tier calculation)
  • Critical-path lines changed: 100
  • Branch: brandon/brandon-onboarding-steps
  • Author: brandon-pereira

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

Comment thread packages/app/src/api.ts
Comment thread packages/app/src/OnboardingChecklist/StepRow.tsx Outdated
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts Outdated
Comment thread packages/app/src/OnboardingChecklist/onboardingTasks.ts Outdated
Comment thread packages/app/src/OnboardingChecklist/onboardingTasks.ts Outdated
Comment thread packages/api/src/routers/api/__tests__/me.int.test.ts
Comment thread packages/app/src/__tests__/OnboardingChecklist.test.tsx Outdated
Comment thread packages/app/src/DBSearchPage.tsx Outdated
// isNonTrivialSearch). This runs on every search but the task is a
// one-time milestone, so skip once it's already recorded — otherwise
// every subsequent qualifying search fires a redundant (idempotent) POST.
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — No test covers the search page actually recording the advancedQuery task

isNonTrivialSearch has a unit test and the local-dashboard path has useDashboardOnboarding.test.tsx, but the headline "explore your data" behaviour — the three-way guard !IS_LOCAL_MODE && !hasExploredData && isNonTrivialSearch(where, filters) inside onSubmit — is only stubbed out in DBSearchPage.directTrace.test.tsx. Add a test that submits a search with a where clause and asserts mutate('advancedQuery') fired, and that it does not fire a second time once me.onboardingData.completedTasks contains it.

Comment thread packages/app/src/OnboardingChecklist/OnboardingChecklist.tsx Outdated
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 358 passed • 1 skipped • 1485s

Status Count
✅ Passed 358
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

- Keep isDismissed from the me cache when unioning completedTasks so a
  concurrent dismiss can't be overwritten by a completion response
- Use semantic tokens (--color-*-success / --color-border /
  --color-text-muted) for the checklist status icons; drop hardcoded #fff
- Remove redundant aria-label on the dismiss button (WCAG 2.5.3); select
  it by visible text in the test
- Make the exhaustiveness test assert every PRODUCT_TASKS title renders
  instead of the tautological n/N badge; fix the stale sort comment
- Record advancedQuery only on a genuine user form submit, not the shared
  onSubmit that also runs on saved-search catch-up submits
Comment thread packages/app/src/DBSearchPage.tsx Outdated
Comment thread packages/api/src/routers/api/__tests__/me.int.test.ts
Comment thread packages/app/src/DBSearchPage.tsx Outdated
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts
Comment thread packages/app/src/OnboardingChecklist/onboardingTasks.ts Outdated
Comment thread packages/api/src/controllers/alerts.ts
Comment thread packages/api/src/controllers/user.ts
Comment thread packages/api/src/routers/api/__tests__/me.int.test.ts
Comment thread packages/app/src/api.ts Outdated
- Record advancedQuery from the shared onSubmit (covers Enter-to-search
  and filter-apply, not just the Run button) while a ref suppresses the
  programmatic catch-up submit on source/saved-search load. Fixes a
  regression from moving recording into onFormSubmit.
- Skip recordOnboardingTaskCompletion when the id isn't a valid ObjectId
  (local app mode injects a synthetic _local_user_), avoiding a CastError
  warning on every save/tool call
- Reuse isNonEmptyWhereExpr in isNonTrivialSearch (DRY)
- Point the MCP task at /team?tab=api-agents#team-api-agents-mcp-server
  so it lands on the MCP panel, not the default Data tab
- Drop the unused api.useCompleteOnboardingTask object entry; keep the
  named export both consumers use
- Settle before the tileless-dashboard negative assertion so it can't
  pass vacuously
Comment thread packages/app/src/DBSearchPage.tsx Outdated
Comment thread packages/app/src/__tests__/OnboardingChecklist.test.tsx
…ed tiles for cache

- completeOnboardingTask/setOnboardingDismissed now no-op for a
  non-persistable user id (the synthetic _local_user_ in IS_LOCAL_APP_MODE),
  via a shared isPersistableUserId type guard reused by
  recordOnboardingTaskCompletion. Previously findByIdAndUpdate matched
  nothing and returned default state.
- Client skips onboarding recording when me.id isn't a real ObjectId
  (isRecordableUserId), so the all-in-one-noauth image (server local mode,
  client non-local) no longer re-POSTs /me/onboarding/task on every search
  and every temp-dashboard edit forever.
- Dashboard save hooks now key onboarding off the PATCH/POST response's
  persisted tiles instead of the request payload, so a name/tag-only save
  no longer leaves the me cache disagreeing with the server rule.
…rding-steps

# Conflicts:
#	packages/app/src/OnboardingChecklist.tsx
Comment thread packages/api/src/controllers/alerts.ts
Comment thread packages/api/src/controllers/dashboard.ts
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts
Comment thread packages/api/src/controllers/user.ts Outdated
Comment thread packages/app/src/DBSearchPage.tsx
…gate comment

The enabled gate scopes system.tables to the 7-day product window but not to
the card actually being visible (a 3-7 day team still in setup, or a fully
completed team, is in-window but shows no card). Tightening is circular
(isSetupComplete needs this query's hasData), so add a 5-minute staleTime to
stop react-query refetching the probe on every mount/window-focus for those
hidden-card cases, and fix the overclaiming comment.
Comments now flag only non-obvious decisions/edge cases rather than
restating what the code does; changeset reduced to one paragraph.
Comment thread packages/app/src/OnboardingChecklist/OnboardingChecklist.tsx
);

// Tile alerts never hit the /alerts router, so record here.
if (result.length > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Saving a dashboard credits "Set up an alert" to the saver for pre-existing, unchanged tile alerts

createOrUpdateDashboardAlerts records the task whenever alertsByTile is non-empty, and syncDashboardAlerts (packages/api/src/controllers/dashboard.ts:103) calls it on every save whose payload carries tiles. Concretely: user A configures a tile alert; user B later drags that tile (a PATCH with tiles) — B's onboardingData.completedTasks gains 'alert' even though B never set one up. The client twin at packages/app/src/dashboard.ts:147 does the same from the response tiles, and additionally fires for a name/tag-only PATCH where the server does not, so that credit appears and then vanishes on the next me refetch. Record only when the upsert actually created an alert — the map body already reads oldAlert, so collect oldAlert == null per tile and call recordOnboardingTaskCompletion only if any were new; mirror that on the client by keying off the tiles the caller changed rather than the whole saved set.

)
.expect(200);

// Clear the task recorded by create so the update is the only thing that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The PUT /alerts/:id onboarding test can pass without updateAlert recording anything

createAlert fires recordOnboardingTaskCompletion fire-and-forget just before the POST responds, so that write is still in flight when the test clears completedTasks at line 215. If it lands after the clear, waitForTask(user._id, 'alert') succeeds on the create's write and the assertion holds even if the updateAlert recording were deleted. Await the create's write first — expect(await waitForTask(user._id, 'alert')).toBe(true) before the $set: { 'onboardingData.completedTasks': [] } — so the clear is ordered after it and the PUT is genuinely the only remaining source.

>
<IconCheck size={11} stroke={3} />
</ThemeIcon>
) : step.isLoading ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The per-step loading spinner can never render

shouldShow requires inputsReady (packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts:184), which already requires !isConnectionsLoading && !isSourcesLoading && sourceRowsSettled, so no step is ever rendered while its query is loading and the step.isLoading branch here is unreachable. The !isSourceRowsQueryEnabled || guard on line 183 rests on a premise that no longer holds either — on TanStack Query v5 isLoading is isPending && isFetching, so a disabled query reports isLoading: false, not "forever true". Drop the isLoading field from setupSteps and the Loader branch here (or, if an early skeleton is wanted, gate only the celebration latch on inputsReady rather than shouldShow).

Comment thread packages/app/src/DBSearchPage.tsx
@brandon-pereira
brandon-pereira requested review from a team and karl-power and removed request for a team September 10, 2026 23:08
Comment thread packages/app/src/OnboardingChecklist/useOnboardingCompletion.ts
Comment thread packages/app/src/OnboardingChecklist/onboardingTasks.ts Outdated
Address jordan's review: gate the checklist on a real user id so the
all-in-one-noauth image (synthetic _local_user_) never renders a card
whose dismiss no-ops and whose tasks can never persist.

Dedupe the 24-hex ObjectId regex into common-utils as isPersistableUserId
and call it directly from the api controller and all app consumers.
Comment thread packages/api/src/controllers/alerts.ts
`MCP tool completed: ${toolName}`,
);
// Only reliable signal the user exercised the MCP server.
recordOnboardingTaskCompletion(context.userId, 'mcp');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The mcp milestone and the search-page suppression logic have no test coverage

recordOnboardingTaskCompletion(context.userId, 'mcp') sits on the !result.isError branch of the tool-tracing chokepoint and is the only way the "Connect the MCP server" task is ever completed, yet no test asserts it — neither that a successful call records it nor that an error result does not. The existing MCP suites (packages/api/src/mcp/__tests__/alerts.int.test.ts, queryTool.int.test.ts) already build an McpContext from a real user._id and would need only an assertion on User.findById(...).onboardingData.completedTasks. Likewise, the trickiest new frontend logic — the recordExploration split between onSubmit and debouncedCatchUpSubmit in DBSearchPage.tsx:1282-1327 — is untested; onboardingTasks.test.ts only covers the pure isNonTrivialSearch predicate, and DBSearchPage.directTrace.test.tsx stubs useCompleteOnboardingTask with an unasserted jest.fn().

Comment thread packages/app/src/api.ts
method: 'POST',
json: { taskId },
}).json<OnboardingDataApiResponse>(),
onSuccess: data => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The me-cache completedTasks append is implemented twice in the same file

useCompleteOnboardingTask's onSuccess (lines 102-124) and useMarkOnboardingTaskComplete (lines 132-156) both implement "append task ids to me.onboardingData.completedTasks via setQueryData, without duplicating and while preserving sibling object identity", 8 lines apart. Have the mutation's onSuccess delegate — data.onboardingData.completedTasks.forEach(markOnboardingTaskComplete) — so the union/identity rules that useMarkOnboardingTaskComplete.test.tsx pins live in one place. (useDismissOnboarding.onSuccess at line 371 is a third, narrower copy of the same setQueryData shape.)

@kodiakhq
kodiakhq Bot merged commit f007c37 into main Sep 14, 2026
28 checks passed
@kodiakhq
kodiakhq Bot deleted the brandon/brandon-onboarding-steps branch September 14, 2026 17:55
// in-session completion (wrongly showing + celebrating on load). A disabled
// query reports isLoading forever, so only wait on the probe when enabled.
const sourceRowsSettled = !isSourceRowsQueryEnabled || !isSourceRowsLoading;
const inputsReady =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Per-step isLoading is now unreachable — the card can never render while a setup query is in flight

inputsReady requires !isConnectionsLoading && !isSourcesLoading && sourceRowsSettled, and shouldShow requires inputsReady, so by the time StepRow renders, every isLoading on setupSteps (lines 116/124/132) is false and the Loader branch in StepRow.tsx:1595 is dead code (productSteps never set it at all). The !isSourceRowsQueryEnabled || half of sourceRowsSettled is also dead: the comment says "a disabled query reports isLoading forever", but this is @tanstack/react-query v5 (packages/app/package.json:53), where isLoading = isPending && isFetching is false for a disabled query — useQueriedChartConfig returns query.isLoading || isLoadingMVOptimization (packages/app/src/hooks/useChartConfig.tsx:491) and both are false when disabled. The user-visible effect is a regression from the old component, which rendered immediately and showed a spinner per step: the card now pops in only after the system.tables probe settles. Either gate only the celebration latch on inputsReady (letting the card render with spinners as before) or delete isLoading from OnboardingStep, setupSteps, and the Loader branch in StepRow.

const { data: me } = api.useMe();
// A non-persistable user counts as "already built" so the temp-dashboard POST
// never fires (see isPersistableUserId).
const hasBuiltDashboard =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The "already done for a persistable user" guard is duplicated verbatim in two modules

hasBuiltDashboard here and hasExploredData in DBSearchPage.tsx:1260 are the same expression (!isPersistableUserId(me?.id) || (me?.onboardingData?.completedTasks.includes(<id>) ?? false)) with only the task id differing, and useOnboardingCompletion.ts:35 builds a third reader of the same field. Extract one useIsOnboardingTaskComplete(taskId: OnboardingTaskId) hook next to the task registry in OnboardingChecklist/onboardingTasks.ts and call it from all three, so the non-persistable-user rule lives in one place (repo convention: "DRY — grep for an existing implementation before adding a helper", and "Look for duplicate code across the affected area").

filters,
orderBy,
});
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The client-side advancedQuery recording branch has no test

Only the pure helper is covered (__tests__/onboardingTasks.test.ts); the branch that actually decides whether "Explore your data" completes — the recordExploration flag, the hasExploredData short-circuit, and the debouncedSubmit/debouncedCatchUpSubmit split at lines 1323-1327 — is never exercised. The interesting regression is silent: because one debouncer is shared and Mantine's useDebouncedCallback keeps the last call's args, a handleSetFilters submit landing within 1s of debouncedCatchUpSubmit() (e.g. source switch → filter reconcile at line 1478) overrides the suppression and credits the task without a user search. Add a DBSearchPage test that asserts completeOnboardingTask.mutate is called for a user-initiated submit with a non-empty where and not called for the programmatic catch-up submit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants