Skip to content

chore: improve onboarding flow#2128

Open
mxkaske wants to merge 4 commits intomainfrom
chore/improve-onboarding-flow
Open

chore: improve onboarding flow#2128
mxkaske wants to merge 4 commits intomainfrom
chore/improve-onboarding-flow

Conversation

@mxkaske
Copy link
Copy Markdown
Member

@mxkaske mxkaske commented May 1, 2026

No description provided.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 1, 2026

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

Project Deployment Actions Updated (UTC)
openstatus-dashboard Ready Ready Preview, Comment May 3, 2026 2:49pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
openstatus-status-page Skipped Skipped May 3, 2026 2:49pm
openstatus-web Skipped Skipped May 3, 2026 2:49pm

Request Review

@openstatusHQ openstatusHQ deleted a comment from claude Bot May 1, 2026
@mxkaske mxkaske marked this pull request as ready for review May 1, 2026 18:07
@mxkaske
Copy link
Copy Markdown
Member Author

mxkaske commented May 1, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @mxkaske's task in 3m 13s —— View job


PR Review: chore/improve-onboarding-flow

  • Gather context and read changed files
  • Review new onboarding client/layout/page components
  • Review new streaming service and API route
  • Review new UI components (feature-badges, checks-table, iframe-preview)
  • Review utility/package changes
  • Post comprehensive review feedback

Overall this is a solid, well-structured refactor. The architecture decisions are good: moving onboarding out of the (dashboard) route group, extracting the streaming service properly, and the yieldMany settle-as-they-arrive pattern is correct and thoughtfully tested. Comments below are roughly ordered by severity.


Security

No rate limiting on /api/onboarding/checks (route.ts)

Any authenticated user can POST to this endpoint in a tight loop. Each call fans out to all ~28 checker regions in parallel. There's no per-user throttle, IP limit, or cooldown. The auth check is present, but a single bad actor (or a bug in client retry logic) could saturate your checker fleet.

A simple mitigation is a short-lived Redis/KV key keyed on workspace.id (e.g., allow one stream per workspace per 30 seconds). Even a coarse in-process map would be better than nothing for the edge-function layer.

Fix this →


Correctness

emitOnboardingTelemetry fires two calls on the same mutation object (client.tsx:197–219)

createFeedbackMutation.mutate({ source: "onboarding-completed", ... });
// ...
if (intentMessage) {
  createFeedbackMutation.mutate({ source: "onboarding-intent", ... });
}

react-query mutations track a single in-flight state per mutation object. The second .mutate() call resets isPending/data/error on the same object. Both network requests do fire, but if anything reads mutation state (e.g., a UI spinner or error handler) it will observe only the second call's state. The current usage doesn't observe the state, so it works, but it's fragile. Consider using a separate mutation object for the intent feedback or calling .mutateAsync() in sequence.

checksStartedRef never resets if the user revisits the monitor form (client.tsx:144–177)

The ref prevents double-firing, which is correct for the current linear flow. But the "Retry" button calls startChecks(monitorData.id) directly, bypassing the ref — that's fine and intentional. Just worth keeping in mind if back-navigation is ever added: the ref would need a reset.


Design

Double cast in checks-table.tsx (checks-table.tsx:31)

const COLUMNS = getColumns([]) as unknown as ColumnDef<OnboardingChecksRow>[];

The comment explains the list vs. get shape difference, and the adapter ensures compatible structure at runtime. This is currently safe but creates a silent TS blind spot. If getColumns ever adds a column that reads a field absent from OnboardingChecksRow, it'll break silently at runtime. Consider narrowing the cast or adding a satisfies guard on the adapter output.

workspaceId: "" in the adapter (checks-table-adapter.ts:54–96)

The inline comment acknowledges this is a landmine — good documentation. Just note that it's also present in data/onboarding-checks.ts (the static example data at line 99). Consider extracting a sentinel constant (SYNTHETIC_WORKSPACE_ID = "") so future authors see it's intentional rather than a bug in both places.

exampleChecks now evaluated at module load (data/onboarding-checks.ts:38)

const now = Date.now();

This is module-level, so the timestamps on the placeholder rows are frozen at first import time. In SSR, this means the demo table shows timestamps from when the Node process started, not from the user's page load. If any column renders a relative time ("3 hours ago"), it will drift. If columns show an absolute timestamp this is fine, but worth a check.


Minor / Nits

edge-context.ts workspace cookie parsing (edge-context.ts:37–44)

The cookie value decoding handles =-padded values correctly via slice(indexOf("=") + 1). But URL-encoded characters in the cookie value (e.g., %2F) aren't decoded. Workspace slugs are likely [A-Za-z0-9-] only so this probably doesn't matter in practice — just worth keeping in mind if the cookie format ever changes.

iframe-preview.tsx local dev experience (iframe-preview.tsx:5–6)

const STATUS_PAGE_DOMAIN =
  process.env.NEXT_PUBLIC_STATUS_PAGE_DOMAIN ?? "openstatus.dev";

When running locally without the env var, the demo iframe (and the user's freshly-published page preview in step 2) will point to production openstatus.dev. The Docker Compose setup would need NEXT_PUBLIC_STATUS_PAGE_DOMAIN=localhost:3003 (or similar) for local testing to work. Worth noting in the Docker env example file if not already there.

requestStatus for 1xx (checks-table-adapter.ts:69)

const requestStatus =
  result.status >= 400 ? "error" : result.status >= 200 ? "success" : "error";

1xx responses map to "error". Unlikely to occur in practice since the Go checker probably follows redirects, but if requestStatus ever drives a "success" badge colour this could be surprising.


What's well done

  • The yieldMany settle-as-they-arrive pattern in packages/utils/src/stream.ts is correctly implemented — the comment about the Promise.race/splice race condition is accurate and the fix is right.
  • The API route "primes" the generator synchronously before opening the stream so workspace auth errors return a proper 404 instead of a 200-then-stream-close — good pattern.
  • slugifyHostname correctly picks the registrable label (api.mycompany.com → mycompany) and falls back to a random slug cleanly.
  • callbackUrl redirect validation is careful: origin-check + pathname guard + protocol check.
  • checksStartedRef cleanly prevents double-streaming on monitorData effect re-runs without adding complex state.
  • Test coverage for streamMonitorPreview covers the important workspace-isolation and happy-path cases.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 42 files

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 3, 2026

Deploying openstatus-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8c1399f
Status: ✅  Deploy successful!
Preview URL: https://00685dfe.openstatus-1sy.pages.dev
Branch Preview URL: https://chore-improve-onboarding-flo.openstatus-1sy.pages.dev

View logs

@vercel vercel Bot temporarily deployed to Preview – openstatus-web May 3, 2026 14:47 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page May 3, 2026 14:47 Inactive
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 43 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/dashboard/src/components/onboarding/checks-table-adapter.ts">

<violation number="1" location="apps/dashboard/src/components/onboarding/checks-table-adapter.ts:77">
P2: Do not replace the measured latency with summed phase durations; this can show incorrect latency values in onboarding results.</violation>
</file>

<file name="apps/dashboard/src/components/onboarding/checks-table.tsx">

<violation number="1" location="apps/dashboard/src/components/onboarding/checks-table.tsx:159">
P2: Use the count of visible columns for `colSpan` instead of total defined columns.</violation>
</file>

<file name="apps/dashboard/src/data/onboarding-checks.ts">

<violation number="1" location="apps/dashboard/src/data/onboarding-checks.ts:38">
P2: Avoid using `Date.now()` for pre-rendered mock rows; it can produce different server/client timestamp output during hydration. Use a fixed seed timestamp so initial HTML stays stable.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// and the column looks broken. Use the phase sum as the denominator
// so the bar always fills the cell.
const phases = result.timing ? calculatePhases(result.timing) : null;
const phaseLatency = phases ? sumPhases(phases) : 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.

P2: Do not replace the measured latency with summed phase durations; this can show incorrect latency values in onboarding results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/components/onboarding/checks-table-adapter.ts, line 77:

<comment>Do not replace the measured latency with summed phase durations; this can show incorrect latency values in onboarding results.</comment>

<file context>
@@ -0,0 +1,100 @@
+  // and the column looks broken. Use the phase sum as the denominator
+  // so the bar always fills the cell.
+  const phases = result.timing ? calculatePhases(result.timing) : null;
+  const phaseLatency = phases ? sumPhases(phases) : 0;
+  const latency = phaseLatency > 0 ? phaseLatency : result.latency;
+
</file context>

{isStreaming && rows.length < totalRegions ? (
<TableRow className="hover:bg-transparent">
<TableCell
colSpan={COLUMNS.length}
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.

P2: Use the count of visible columns for colSpan instead of total defined columns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/components/onboarding/checks-table.tsx, line 159:

<comment>Use the count of visible columns for `colSpan` instead of total defined columns.</comment>

<file context>
@@ -0,0 +1,177 @@
+            {isStreaming && rows.length < totalRegions ? (
+              <TableRow className="hover:bg-transparent">
+                <TableCell
+                  colSpan={COLUMNS.length}
+                  className="text-center text-muted-foreground text-xs"
+                >
</file context>

{ region: "eze", status: 200, latency: 245 },
];

const now = Date.now();
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.

P2: Avoid using Date.now() for pre-rendered mock rows; it can produce different server/client timestamp output during hydration. Use a fixed seed timestamp so initial HTML stays stable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/data/onboarding-checks.ts, line 38:

<comment>Avoid using `Date.now()` for pre-rendered mock rows; it can produce different server/client timestamp output during hydration. Use a fixed seed timestamp so initial HTML stays stable.</comment>

<file context>
@@ -0,0 +1,101 @@
+  { region: "eze", status: 200, latency: 245 },
+];
+
+const now = Date.now();
+
+// Deterministic pseudo-random in [0, 1) seeded by string — keeps SSR/CSR stable.
</file context>

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant