Skip to content

Conversation

tristantr
Copy link

No description provided.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 21, 2025

Deploying windmill with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7bc9de2
Status: ✅  Deploy successful!
Preview URL: https://63a8aadf.windmill.pages.dev
Branch Preview URL: https://tl-first-timers-onboarding2.windmill.pages.dev

View logs

@tristantr tristantr marked this pull request as ready for review October 21, 2025 10:55
@claude
Copy link
Contributor

claude bot commented Oct 21, 2025

Claude finished @tristantr's task —— View job


Code Review: Add onboarding form for first timers ✅

I've completed a comprehensive review of this PR. Overall, this is a solid implementation of a first-time user onboarding feature for cloud-hosted instances. Here's my detailed feedback:


✅ Strengths

  1. Clean separation of concerns - OSS stub implementation in users_oss.rs properly isolates enterprise features
  2. Proper conditional logic - Onboarding only triggers for cloud-hosted instances via isCloudHosted() checks
  3. Good UX flow - Two-step onboarding process with clear navigation and progress indicators
  4. Database query optimization - SQLx queries properly include the new first_time_user field

🔍 Issues & Recommendations

Critical Issues

None found. The code is functional and safe.

High Priority Issues

1. Missing Error Handling in Onboarding Component (frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte:78-84)

The error handling gracefully continues to workspaces even on failure, but the user experience could be improved:

} catch (error) {
    console.error('Error submitting onboarding data:', error)
    sendUserToast('Failed to save information: ' + (error?.body || error?.message || error), true)
} finally {
    isSubmitting = false
    goto('/user/workspaces')  // ⚠️ Navigates away even on error
}

Recommendation: Consider whether failed onboarding submission should block navigation or just log silently. Current behavior might confuse users who see an error toast but are immediately redirected.

2. Unused Variable in Backend (backend/windmill-api/src/users.rs:1880)

if let Some((email, hash, super_admin, _first_time_user)) = email_w_h {

The _first_time_user is fetched from the database but prefixed with _ (unused). This was previously used for cookie-based onboarding detection which has been removed.

Recommendation: Either remove it from the query entirely or document why it's kept (possibly for future use).


Medium Priority Issues

3. Svelte 5 Compliance - Missing Runes Best Practices

The onboarding component uses Svelte 5 runes ($state, $derived) but could follow best practices more closely:

let currentStep = $state(1)
let useCaseText = $state('')
let selectedSource = $state<string | null>(null)
let isSubmitting = $state(false)

According to frontend/svelte5-best-practices.mdc, this is correct, but the component doesn't use $derived where it could be beneficial.

Potential improvement: Consider using $derived for computed states like button disabled state:

const canContinue = $derived(selectedSource !== null && !isSubmitting)

4. API Type Safety

The onboarding component constructs the request body manually:

await UserService.submitOnboardingData({
    requestBody: {
        email,
        customer_id: null,
        is_ee_trial: null,
        touch_point: touchPoint,
        use_case: useCaseText
    }
})

Recommendation: Verify that the TypeScript types generated from the OpenAPI spec are being used. If UserService.submitOnboardingData isn't auto-generated yet, ensure the frontend build regenerates types from the updated openapi.yaml.

5. Performance - Multiple globalWhoami() Calls

In Login.svelte (line 123) and login_callback/+page.svelte (line 51), the code calls UserService.globalWhoami() to check first_time_user:

const globalUserInfo = await UserService.globalWhoami()
if (globalUserInfo.first_time_user) {
    goto('/user/onboarding')
    return
}

Observation: This adds an extra API call on every login. Consider whether this check could be included in the login response itself to reduce round trips.

6. Missing Input Validation

The textarea in step 2 has no length validation:

<textarea
    bind:value={useCaseText}
   
    class="input mt-1"
    rows="8"
></textarea>

Recommendation: Add maxlength validation to prevent excessively long submissions:

<textarea
    bind:value={useCaseText}
    maxlength="1000"
    ...
/>

Low Priority / Style Issues

7. Console Logging in Production Code

onboarding/+page.svelte:61-62:

if (!email) {
    console.error('No email found in global user info:', globalUserInfo)
    throw new Error('User email not found')
}

Recommendation: This is fine for debugging, but consider using a proper logging service in production.

8. Hardcoded Success Message

onboarding/+page.svelte:77:

sendUserToast('Information saved successfully')

Recommendation: Consider making toast messages consistent across the app or using i18n if internationalization is planned.

9. Button Disabled State Could Be More Explicit

onboarding/+page.svelte:151-152:

disabled={isSubmitting}
loading={isSubmitting}

Recommendation: The "Continue" button should also check if useCaseText is provided (though the skip option makes this optional, clarify if use case is truly optional).


🔒 Security Review

No security issues found:

  • API endpoint properly uses ApiAuthed for authentication
  • No SQL injection risks (using SQLx parameterized queries)
  • No XSS risks (Svelte auto-escapes template values)
  • OSS version properly stubs out the feature
  • Input data is properly typed and validated on backend (in EE version)

⚡ Performance Considerations

Positive:

  • Efficient database queries with proper indexing
  • Minimal bundle size impact (single new route)
  • Conditional loading (only for cloud instances)

Minor concern:


📋 Code Quality & Best Practices

Backend (Rust):

  • ✅ Follows project structure (OSS stubs in users_oss.rs)
  • ✅ Proper error handling with Result<T, Error>
  • ✅ OpenAPI spec updated correctly
  • ✅ SQLx query cache files committed
  • ⚠️ Unused variable in login function (see issue Typo on site homepage #2)

Frontend (Svelte 5):


🎯 Specific Recommendations

  1. Decide on error handling strategy: Should onboarding failures block navigation or silently continue?
  2. Clean up unused database field: Remove or document _first_time_user in login query
  3. Add input validation: Implement maxlength on textarea
  4. Consider performance optimization: Possibly include first_time_user in login response to avoid extra API call
  5. Verify type generation: Ensure OpenAPI types are regenerated for the new endpoint

🧪 Testing Recommendations

Since this is a user-facing feature, ensure testing covers:

  • ✅ First-time cloud users see onboarding
  • ✅ Returning users skip onboarding
  • ✅ Self-hosted instances skip onboarding
  • ✅ Skip button works correctly
  • ✅ Form submission handles API failures gracefully
  • ✅ OAuth login flows trigger onboarding correctly
  • ✅ Password login flows trigger onboarding correctly

📝 Reproduction Instructions for Testers

To test this feature on a cloud-hosted instance, follow these steps:

Testing the onboarding flow:

  1. Create a fresh user account or reset the first_time_user flag in the database for an existing user: UPDATE password SET first_time_user = true WHERE email = '[email protected]';
  2. Log in to Windmill using either password authentication or OAuth (GitHub, Google, etc.)
  3. After successful authentication, you should be automatically redirected to /user/onboarding instead of the workspace list
  4. On the first step, select where you heard about Windmill from the 10 options provided (Search engine, YouTube, LinkedIn, Twitter, GitHub, Blog/Article, Word of mouth, Company, Event, or Other)
  5. The interface should automatically advance to step 2 after selection
  6. On step 2, enter your intended use case in the textarea (e.g., "Building AI agents and automation workflows")
  7. Click "Continue" - the data should be submitted and you'll be redirected to /user/workspaces
  8. Verify that subsequent logins do NOT show the onboarding flow (user is marked as completed)

Testing the skip functionality:

  • On step 1, click "Skip" and verify you're redirected to workspaces without submitting data

Testing self-hosted behavior:

  • On a self-hosted instance, verify that the onboarding flow never appears regardless of first_time_user status

Verifying data submission:

  • Check network tab to confirm the POST request to /api/users/onboarding succeeds
  • In enterprise edition, verify the data is properly stored

Summary

This is a well-implemented feature with clean code and good separation between OSS and EE versions. The main improvements needed are around error handling clarity, removing unused code, and minor optimizations. No blocking issues were found.

Recommendation: Approve with minor suggestions

I've also left inline comments on the specific files for the most important issues.


} finally {
isSubmitting = false
goto('/user/workspaces')
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Error Handling Concern: The goto('/user/workspaces') in the finally block means users are redirected even when submission fails. This might be confusing since they'll see an error toast but immediately navigate away.

Suggestion: Consider one of these approaches:

  1. Only navigate on success (move goto inside try block after line 77)
  2. Or make it clear to users that skipping/continuing without saving is acceptable
// Option 1: Navigate only on success
try {
    await UserService.submitOnboardingData({ ... })
    sendUserToast('Information saved successfully')
    goto('/user/workspaces')
} catch (error) {
    console.error('Error submitting onboarding data:', error)
    sendUserToast('Failed to save information. You can continue anyway.', true)
    // Let user decide whether to retry or skip
} finally {
    isSubmitting = false
}

</p>
<div class="mb-6">
<textarea
bind:value={useCaseText}
Copy link
Contributor

Choose a reason for hiding this comment

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

Input Validation Missing: Consider adding a maxlength attribute to prevent excessively long submissions:

<textarea
    bind:value={useCaseText}
   
    class="input mt-1"
    rows="8"
    maxlength="1000"
></textarea>

This provides better UX and prevents potential backend issues with very large payloads.

.await?;

if let Some((email, hash, super_admin, first_time_user)) = email_w_h {
if let Some((email, hash, super_admin, _first_time_user)) = email_w_h {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused Variable: The _first_time_user field is fetched from the database but not used (prefixed with _). This was previously used for cookie-based onboarding but that code was removed.

Options:

  1. Remove it from the query entirely to save a tiny bit of performance
  2. Keep it if there are plans to use it for other purposes
  3. Add a comment explaining why it's kept
// If removing (option 1):
"SELECT email, password_hash, super_admin FROM password WHERE ..."
// Returns: Option<(String, String, bool)>

// If keeping with documentation (option 2):
// Note: first_time_user fetched but unused - kept for future analytics
if let Some((email, hash, super_admin, _first_time_user)) = email_w_h {

// Only show onboarding for cloud-hosted instances
if (isCloudHosted()) {
try {
const globalUserInfo = await UserService.globalWhoami()
Copy link
Contributor

Choose a reason for hiding this comment

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

Performance Consideration: This adds an extra globalWhoami() API call on every login for cloud-hosted instances.

Potential optimization: Consider including the first_time_user flag directly in the login response to eliminate this extra round-trip. This would require backend changes to the login endpoint, but could improve login performance slightly.

// Current: Login → Check globalWhoami() → Redirect
// Optimized: Login (returns first_time_user) → Redirect

Not critical, but worth considering if login performance becomes a concern.

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct OnboardingData {
pub email: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

user info should not come from the frontend to avoid impersonation
You can get the email & username from ApiAuthed instead

Copy link
Contributor

Choose a reason for hiding this comment

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

ideally, only use_case and touch_point comes from the front (i.e only what the user actually inputted)
and everything else comes from the backend which cannot be tampered with

return
}
if (firstTime) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just checked and this value comes from users.rs (is_first_time_setup()).
The function returns true only if the instance has a single user [email protected]:changeme

so this should never happen on cloud

showPassword = true
console.error('Could not load logins', e)
// Only log if it's not a 404
if (e?.status !== 404) {
Copy link
Contributor

Choose a reason for hiding this comment

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

No reason to only log 404s, avoid silent errors at all cost

function selectSource(sourceId: string) {
selectedSource = sourceId
// Auto-advance to next step for all selections
currentStep = 2
Copy link
Contributor

Choose a reason for hiding this comment

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

currentStep = Math.min(totalStepCount, currentStep + 1)

But the core reason is :
Numbers have no semantic meaning in this case, do not rely on the fact that "1 = source selection"
Maybe one day the steps will change and this is prone to future bugs

}
function goToPreviousStep() {
if (currentStep === 2) {
Copy link
Contributor

Choose a reason for hiding this comment

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

same here, this relies on the fact that total steps count = 2 (avoid magic numbers)

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.

3 participants