Skip to content

Add user funnel analytics tracking#838

Open
JoaquinBN wants to merge 4 commits into
devfrom
JoaquinBN/user-funnel-analytics
Open

Add user funnel analytics tracking#838
JoaquinBN wants to merge 4 commits into
devfrom
JoaquinBN/user-funnel-analytics

Conversation

@JoaquinBN

@JoaquinBN JoaquinBN commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Adds privacy-safe Google Analytics tracking across onboarding, role journeys, validator waitlist, contribution submission, and submission lifecycle surfaces.

Centralizes sanitization, route templating, lifecycle timers, and one-shot events so funnel metrics can be measured without sending raw user identifiers or free-text content.

Tightens event attribution for dynamic page views, OAuth journey links, click-through tasks, and deep-linked contribution forms.

Summary by CodeRabbit

  • New Features

    • Added comprehensive analytics instrumentation across authentication, onboarding/profile completion, funnels, journeys, submissions, and key navigation moments (navbar/sidebar/logo).
    • Introduced an analytics utility with parameter sanitization, funnel/lifecycle timing, connect-wallet intent persistence, and safer route/context enrichment.
    • Added client-side page-view tracking with deduping to prevent duplicate views on the same URL.
  • Bug Fixes

    • Improved consistency of protected/role-locked redirect and navigation analytics, including clearer redirect target tracking.
    • Reduced duplicate “view/exit” events via stable dedupe keys.
  • Tests

    • Added Vitest coverage for analytics initialization, sanitization, page views, funnel/lifecycle timing, and one-time events.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f77a4418-a6e0-4d36-a1d1-7dfc1882a36e

📥 Commits

Reviewing files that changed from the base of the PR and between 95aa928 and 3ccb696.

📒 Files selected for processing (5)
  • frontend/src/components/ProfileCompletionGuard.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/routes/BuilderJourney.svelte
  • frontend/src/routes/CommunityJourney.svelte
  • frontend/src/routes/ValidatorWaitlist.svelte

📝 Walkthrough

Walkthrough

The PR adds a shared analytics helper and instruments route guards, navigation/auth entry points, onboarding and submission views, and builder/community/validator journeys with page, click, intent, timing, and outcome tracking. It also adds tests for the analytics module.

Changes

Frontend analytics instrumentation

Layer / File(s) Summary
Analytics helper and tests
frontend/src/lib/analytics.js, frontend/src/tests/analytics.test.js
Shared analytics helpers now sanitize payloads, initialize GA, track events and page views, persist timing and wallet intent state, and build shared context; tests cover initialization, templating, sanitization, timing, and context output.
Route redirects and page views
frontend/src/App.svelte
Protected and role-locked route guards now emit redirect events, set connect-wallet intent context, and track page views from verified location and querystring keys.
Wallet intent and auth entry tracking
frontend/src/components/AuthButton.svelte, frontend/src/components/Navbar.svelte, frontend/src/components/Sidebar.svelte, frontend/src/components/SocialLink.svelte, frontend/src/routes/SubmitContribution.svelte
Navbar, sidebar, social-link, and submit-contribution entry points now set connect-wallet intent metadata and emit click analytics; AuthButton consumes the intent and records wallet selection and auth start, success, and failure events.
Role onboarding and profile completion
frontend/src/components/ProfileCompletionGuard.svelte, frontend/src/components/funnel/RoleFunnel.svelte, frontend/src/components/funnel/RoleLanding.svelte
Role landing, profile completion, and earned-state dashboard views now emit deduped view, selection, submit, success, error, and lifecycle analytics.
Contribution form and history
frontend/src/components/portal/submit-contribution/SubmitContribution.svelte, frontend/src/routes/MySubmissions.svelte
The contribution form and submissions dashboard now emit view, submit-attempt, success, error, and lifecycle-snapshot analytics, and the submission buttons share one tracked navigation handler.
Builder journey and shared task cards
frontend/src/components/social-tasks/SocialTaskCard.svelte, frontend/src/routes/BuilderJourney.svelte
Builder step cards and the builder journey now emit step action, verification, error, exit, and completion analytics, including wallet-connect intent updates and lifecycle timing.
Community and validator journeys
frontend/src/routes/CommunityJourney.svelte, frontend/src/routes/ValidatorWaitlist.svelte
Community and validator routes now track deduped journey views and exits, step actions, verification results, journey starts, waitlist joins, and completion events.

Sequence Diagram(s)

sequenceDiagram
  participant RouteGuard as App.svelte
  participant EntryPoint as Navbar/Sidebar/SocialLink/SubmitContribution
  participant Analytics as frontend/src/lib/analytics.js
  participant AuthButton as AuthButton.svelte
  participant Journey as Builder/Community/Validator routes

  RouteGuard->>Analytics: trackEvent('protected_route_redirect'/'role_locked_redirect', ...)
  RouteGuard->>Analytics: trackPageView($location, getAnalyticsContext())

  EntryPoint->>Analytics: setConnectWalletIntent(...)
  EntryPoint->>Analytics: trackEvent(...)
  AuthButton->>Analytics: consumeConnectWalletIntent()
  AuthButton->>Analytics: trackEvent('wallet_provider_selected'/'wallet_auth_started'/'wallet_auth_success'/'wallet_auth_failed', ...)

  Journey->>Analytics: trackEvent('journey_view'/'journey_step_visible'/'journey_step_action_click', ...)
  Journey->>Analytics: markFunnelTime()/markLifecycleTime()
  Journey->>Analytics: trackEvent('journey_started'/'journey_start_error'/'journey_exit'/'journey_completed', ...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • genlayer-foundation/points#839: Both PRs modify frontend/src/components/portal/submit-contribution/SubmitContribution.svelte’s submission flow; the other PR changes submission gating in the same component while this one adds analytics around submit attempts and outcomes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding funnel analytics tracking across user flows.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch JoaquinBN/user-funnel-analytics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/ProfileCompletionGuard.svelte`:
- Line 70: The `ProfileCompletionGuard` analytics payload is sending `$location`
directly as `source_route`, which can leak wallet-address segments into
analytics. Update the `ProfileCompletionGuard.svelte` flow to pass
`source_route` through `templateRoute` from `lib/analytics.js` before emission,
and make sure the import is present where the analytics event is built.

In `@frontend/src/components/social-tasks/SocialTaskCard.svelte`:
- Around line 116-119: The Verify flow in SocialTaskCard.svelte is
double-counting journey_step_action_click because callComplete defaults
trackAction to true and the Verify button path is not suppressing it. Update the
Verify button handler to call callComplete with trackAction set to false,
matching the click-through path and preventing the duplicate event emission from
handleOpened/callComplete.

In `@frontend/src/lib/analytics.js`:
- Around line 44-46: URL_RE in analytics sanitization is too narrow because it
only matches strings that start with http(s), so embedded links inside free-text
values can still be sent. Update the value-checking logic in analytics.js to use
a separate unanchored URL detector for string values, and apply it in the same
sanitization flow that already uses ADDRESS_RE and EMAIL_RE so embedded URLs are
blocked before truncation.
- Around line 167-173: The shouldDropKey helper currently only blocks keys
ending in _id, so bare sensitive identifiers like id, slug, and token can still
be sent to analytics. Update shouldDropKey in analytics.js to explicitly reject
these bare identifier keys, either by extending FORBIDDEN_KEYS or by switching
the helper to a strict allowlist, and keep the existing checks around
SAFE_ID_KEYS, error_message, and raw intact.
- Line 96: The route label mapping in analytics is still emitting “badge” for
legacy matches, so update the template alias in analytics.js to use the current
product term instead. In the route-label mapping near the badge-style regex
entry, keep matching the legacy path if needed but change the emitted analytics
label to the contribution term (for example, use the contribution equivalent of
the current template) so reports no longer contain “badge”.

In `@frontend/src/routes/BuilderJourney.svelte`:
- Around line 479-483: The `error_code` branch in `BuilderJourney.svelte` is
unreachable because the `if (error?.code !== 4001)` guard excludes user
rejections, so `journey_step_error` never records `user_rejected`. Update the
`wallet_add_chain` error handling around `trackBuilderStepEvent` so rejection
analytics are emitted outside or before the toast guard, and keep the
conditional only for the toast/display logic. Use the `error?.code` check and
`trackBuilderStepEvent('journey_step_error', 'networks', ...)` block as the
anchor when refactoring.

In `@frontend/src/tests/analytics.test.js`:
- Around line 198-205: The analytics test for getAnalyticsContext currently only
checks a partial shape with toMatchObject, so it would miss accidental exposure
of identifier fields. Update the assertion around getAnalyticsContext() to keep
the existing expected context checks and add negative assertions that user,
email, and address are not present in the returned object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c34c85a8-8cbb-4194-9209-def6fad9ea96

📥 Commits

Reviewing files that changed from the base of the PR and between 5f065f3 and ed9f71d.

📒 Files selected for processing (17)
  • frontend/src/App.svelte
  • frontend/src/components/AuthButton.svelte
  • frontend/src/components/Navbar.svelte
  • frontend/src/components/ProfileCompletionGuard.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/components/SocialLink.svelte
  • frontend/src/components/funnel/RoleFunnel.svelte
  • frontend/src/components/funnel/RoleLanding.svelte
  • frontend/src/components/portal/submit-contribution/SubmitContribution.svelte
  • frontend/src/components/social-tasks/SocialTaskCard.svelte
  • frontend/src/lib/analytics.js
  • frontend/src/routes/BuilderJourney.svelte
  • frontend/src/routes/CommunityJourney.svelte
  • frontend/src/routes/MySubmissions.svelte
  • frontend/src/routes/SubmitContribution.svelte
  • frontend/src/routes/ValidatorWaitlist.svelte
  • frontend/src/tests/analytics.test.js

Comment thread frontend/src/components/ProfileCompletionGuard.svelte Outdated
Comment thread frontend/src/components/social-tasks/SocialTaskCard.svelte
Comment thread frontend/src/lib/analytics.js
Comment thread frontend/src/lib/analytics.js Outdated
Comment thread frontend/src/lib/analytics.js
Comment thread frontend/src/routes/BuilderJourney.svelte Outdated
Comment thread frontend/src/tests/analytics.test.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/routes/BuilderJourney.svelte (1)

238-265: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wait for user hydration before auto-starting the builder journey.

When user is still undefined on first mount, !user?.has_builder_welcome && !user?.builder evaluates to true, so startBuilderJourney() can fire too early. That can create false journey_started / journey_start_error analytics and an unnecessary backend call. Move this into a one-shot effect that waits for $userStore.loading === false and a real user object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/BuilderJourney.svelte` around lines 238 - 265, The
auto-start logic in BuilderJourney should not run until user hydration is
complete, because the current `!user?.has_builder_welcome && !user?.builder`
check can fire while `user` is still undefined. Move the `startBuilderJourney()`
call into a one-time effect tied to `userStore.loading === false` and a resolved
`user` object, and guard it so it only runs once after hydration; keep the
existing analytics (`journey_started`, `journey_start_error`) and warning
handling inside that hydrated flow.
frontend/src/components/ProfileCompletionGuard.svelte (2)

162-188: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Trim the profile fields before validating and submitting.

if (!email || !name) treats whitespace-only input as valid, but this guard already considers trimmed-empty values incomplete. A " " display name can get submitted and drop the user straight back into the completion gate.

Suggested fix
 async function handleProfileSubmit() {
+  const trimmedEmail = email.trim();
+  const trimmedName = name.trim();
+
   trackEvent('profile_completion_submit', getAnalyticsContext({
       selected_role: selectedRole,
       preselected_role: preselectedRole,
   }));

   // Validate inputs
-  if (!email || !name) {
+  if (!trimmedEmail || !trimmedName) {
     trackEvent('profile_completion_error', getAnalyticsContext({
       selected_role: selectedRole,
       error_stage: 'validation',
     }));
     profileError = 'Please provide both email and display name';
     return;
   }

-  if (!isValidEmail(email)) {
+  if (!isValidEmail(trimmedEmail)) {
     trackEvent('profile_completion_error', getAnalyticsContext({
       selected_role: selectedRole,
       error_stage: 'validation',
     }));
     profileError = 'Please enter a valid email address';
     return;
   }
@@
-  if (email) updateData.email = email;
-  if (name) updateData.name = name;
+  updateData.email = trimmedEmail;
+  updateData.name = trimmedName;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/ProfileCompletionGuard.svelte` around lines 162 -
188, Trim the profile inputs in ProfileCompletionGuard.svelte before running the
validation and building the update payload so whitespace-only values are treated
as empty. Update the logic around the validation block that checks email and
name to use trimmed email/name values for the !email || !name and isValidEmail
checks, and use those trimmed values when populating updateData before submit.
Keep the existing analytics tracking and error messages, but make sure the
submit flow cannot send a display name consisting only of spaces.

163-177: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Route submit failures through the toast system too.

These validation/backend branches only set profileError, so this flow bypasses the shared notification channel required for components and routes. Keep the inline message if you want, but also emit a toast here. As per coding guidelines, frontend/**/{components,routes}/**/*.{ts,tsx,js,svelte}: Always handle API errors with try-catch blocks and provide user-facing error messages via toast notifications.

Also applies to: 215-233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/ProfileCompletionGuard.svelte` around lines 163 -
177, The validation and backend failure paths in ProfileCompletionGuard
currently only set profileError, so they bypass the shared toast notification
flow. Update the submit/error handling in the profile completion logic to also
emit a user-facing toast alongside the inline message, including the existing
validation branches and the backend-related handling later in the same flow. Use
the existing trackEvent/profileError flow as the anchor, and make sure the
route/component error handling is wrapped with try-catch where applicable so
failures are surfaced through the toast system.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/components/ProfileCompletionGuard.svelte`:
- Around line 162-188: Trim the profile inputs in ProfileCompletionGuard.svelte
before running the validation and building the update payload so whitespace-only
values are treated as empty. Update the logic around the validation block that
checks email and name to use trimmed email/name values for the !email || !name
and isValidEmail checks, and use those trimmed values when populating updateData
before submit. Keep the existing analytics tracking and error messages, but make
sure the submit flow cannot send a display name consisting only of spaces.
- Around line 163-177: The validation and backend failure paths in
ProfileCompletionGuard currently only set profileError, so they bypass the
shared toast notification flow. Update the submit/error handling in the profile
completion logic to also emit a user-facing toast alongside the inline message,
including the existing validation branches and the backend-related handling
later in the same flow. Use the existing trackEvent/profileError flow as the
anchor, and make sure the route/component error handling is wrapped with
try-catch where applicable so failures are surfaced through the toast system.

In `@frontend/src/routes/BuilderJourney.svelte`:
- Around line 238-265: The auto-start logic in BuilderJourney should not run
until user hydration is complete, because the current
`!user?.has_builder_welcome && !user?.builder` check can fire while `user` is
still undefined. Move the `startBuilderJourney()` call into a one-time effect
tied to `userStore.loading === false` and a resolved `user` object, and guard it
so it only runs once after hydration; keep the existing analytics
(`journey_started`, `journey_start_error`) and warning handling inside that
hydrated flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: af524d57-88ad-4324-adf8-cb0433dcce98

📥 Commits

Reviewing files that changed from the base of the PR and between ed9f71d and 95aa928.

📒 Files selected for processing (5)
  • frontend/src/components/ProfileCompletionGuard.svelte
  • frontend/src/components/social-tasks/SocialTaskCard.svelte
  • frontend/src/lib/analytics.js
  • frontend/src/routes/BuilderJourney.svelte
  • frontend/src/tests/analytics.test.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
frontend/src/components/Sidebar.svelte (1)

269-269: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep these as plain in-app anchors.

These link updates call preventDefault() and route through navigate(...); for normal in-app navigation, leave the <a href="..."> behavior intact and track clicks without taking over navigation.

As per coding guidelines, “Use plain <a href="/path"> anchors for in-app navigation (global interceptor handles SPA routing) instead of calling push() in click handlers.”

Also applies to: 493-493, 734-734, 923-927

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Sidebar.svelte` at line 269, The Sidebar.svelte link
handlers are overriding normal anchor behavior by calling preventDefault() and
navigate(...), which should be removed for in-app navigation. Update the
affected anchor elements in the Sidebar component to use plain <a href="...">
links and keep any click tracking separate from routing, so the global
interceptor can handle SPA navigation; apply this to the leaderboard and the
other matching links referenced in the Sidebar markup.

Source: Coding guidelines

frontend/src/routes/ValidatorWaitlist.svelte (1)

187-193: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the caught error when classifying start failures.

This always reports journey_start_error.error_stage as backend; network failures lose their classification.

🐛 Proposed fix
-    } catch (_) {
+    } catch (err) {
       trackEvent('journey_start_error', getAnalyticsContext({
         role_context: 'validator',
         selected_role: 'validator',
         surface: 'journey',
-        error_stage: 'backend',
+        error_stage: err.response?.status ? 'backend' : 'network',
       }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/ValidatorWaitlist.svelte` around lines 187 - 193, The
start-failure tracking in ValidatorWaitlist.svelte is discarding the caught
exception, so every journey_start_error is classified as backend and network
failures are misreported. Update the catch block around the journey start flow
to retain the caught error and derive error_stage from it before calling
trackEvent/getAnalyticsContext, using the existing journey_start_error reporting
path and the validator role_context/selected_role fields so the classification
reflects the actual failure type.
frontend/src/routes/CommunityJourney.svelte (1)

425-456: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recompute lifecycle durations after marking role unlock.

claimParams is created before markLifecycleTime('role_unlocked:community'), so the success/completed/unlocked analytics can miss the just-recorded unlock timing.

🐛 Proposed fix
       if (res.data?.user) userStore.updateUser(res.data.user);
       await userStore.loadUser?.();
       markLifecycleTime('role_unlocked:community');
-      trackEvent('community_role_claim_success', getAnalyticsContext(claimParams));
+      const successParams = {
+        ...claimParams,
+        ...getLifecycleDurations('community'),
+      };
+      trackEvent('community_role_claim_success', getAnalyticsContext(successParams));
       trackEvent('journey_completed', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         journey_state: 'completed',
       }));
       trackEvent('role_unlocked', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         role_context: 'community',
         selected_role: 'community',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/CommunityJourney.svelte` around lines 425 - 456, The
analytics payload in completeJourney() is built too early, so the
success/completed/unlocked events can omit the newly recorded role unlock
timing. Move the lifecycle timing update and then recompute the claim analytics
context after markLifecycleTime('role_unlocked:community') in
CommunityJourney.svelte, or build a fresh payload for the post-unlock trackEvent
calls so getLifecycleDurations('community') reflects the latest state.
frontend/src/routes/BuilderJourney.svelte (2)

456-484: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Emit journey_step_verified for individual network adds.

Bulk add reports networks as verified, but adding Bradbury/Asimov/Studio one-by-one never emits the verified event when the final missing network is added.

🐛 Proposed fix
 async function addNetwork(network, kind, { silent = false } = {}) {
   if (typeof window === 'undefined') return;
   const walletProvider = provider || window.ethereum;
   if (!walletProvider) {
     showWarning('Please connect your wallet first.');
     return false;
   }
+  const networksCompleteBefore = NETWORKS.every((item) => networkDone(item.kind));

   if (kind === 'bradbury') isAddingBradbury = true;
   if (kind === 'asimov') isAddingAsimov = true;
   if (kind === 'studio') isAddingStudio = true;
@@
       });
       markNetworkAdded(kind);
+      if (!silent && !networksCompleteBefore && NETWORKS.every((item) => networkDone(item.kind))) {
+        trackBuilderStepEvent('journey_step_verified', 'networks');
+      }
       if (!silent) showSuccess(`${network.chainName} added.`);
       return true;
     } catch (error) {
       if (isAlreadyAddedNetworkError(error)) {
         markNetworkAdded(kind);
+        if (!silent && !networksCompleteBefore && NETWORKS.every((item) => networkDone(item.kind))) {
+          trackBuilderStepEvent('journey_step_verified', 'networks');
+        }
         if (!silent) showSuccess(`${network.chainName} already exists in your wallet.`);
         return true;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/BuilderJourney.svelte` around lines 456 - 484, The
individual network add flow in addNetwork currently updates local state and
success messages, but never emits journey_step_verified when the last missing
network is added. Update addNetwork to detect when Bradbury, Asimov, and Studio
are all marked added after a successful add or already-added path, and emit
journey_step_verified with networks from that function (using markNetworkAdded
and the existing tracking logic as the hook). Keep the bulk-add behavior
unchanged and ensure the event fires only once when the final required network
becomes verified.

626-657: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recompute lifecycle durations after marking role unlock.

claimParams is built before markLifecycleTime('role_unlocked:builder'), so success/completion/unlock events reuse stale lifecycle timing data.

🐛 Proposed fix
       await journeyAPI.completeBuilderJourney();
       await userStore.loadUser();
       markLifecycleTime('role_unlocked:builder');
-      trackEvent('builder_role_claim_success', getAnalyticsContext(claimParams));
+      const successParams = {
+        ...claimParams,
+        ...getLifecycleDurations('builder'),
+      };
+      trackEvent('builder_role_claim_success', getAnalyticsContext(successParams));
       trackEvent('journey_completed', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         journey_state: 'completed',
       }));
       trackEvent('role_unlocked', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         role_context: 'builder',
         selected_role: 'builder',
         surface: 'journey',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/BuilderJourney.svelte` around lines 626 - 657, The
lifecycle timing in completeJourney is captured too early, so the
success/completion/unlock analytics use stale values. Update BuilderJourney’s
completeJourney flow so markLifecycleTime('role_unlocked:builder') happens
before collecting lifecycle durations, then rebuild the analytics payload used
by trackEvent calls (including builder_role_claim_success, journey_completed,
and role_unlocked) with fresh getLifecycleDurations('builder') data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/ProfileCompletionGuard.svelte`:
- Around line 242-246: The post-success reload in ProfileCompletionGuard.svelte
is still inside the failure-sensitive flow, so a loadUser() error can block the
redirect even after updateUserProfile and journey start succeed. Make the
userStore.loadUser() refresh best-effort by catching its failure locally, or
move the push(journeyPath(targetRole)) navigation so it cannot be skipped by a
reload error; keep the redirect logic anchored around updateUserProfile,
userStore.loadUser, and push.
- Around line 232-240: The caught API failure paths in
ProfileCompletionGuard.svelte only track analytics or update state, but they do
not notify the user. Update the relevant try-catch blocks around the journey/API
actions, including the catch for journey start and the other affected API error
handlers in this component, to call showWarning or showError from the toast
store with a user-facing message. Keep the existing analytics/state handling,
and make sure each API failure path surfaces a toast using the existing toast
helpers.

---

Outside diff comments:
In `@frontend/src/components/Sidebar.svelte`:
- Line 269: The Sidebar.svelte link handlers are overriding normal anchor
behavior by calling preventDefault() and navigate(...), which should be removed
for in-app navigation. Update the affected anchor elements in the Sidebar
component to use plain <a href="..."> links and keep any click tracking separate
from routing, so the global interceptor can handle SPA navigation; apply this to
the leaderboard and the other matching links referenced in the Sidebar markup.

In `@frontend/src/routes/BuilderJourney.svelte`:
- Around line 456-484: The individual network add flow in addNetwork currently
updates local state and success messages, but never emits journey_step_verified
when the last missing network is added. Update addNetwork to detect when
Bradbury, Asimov, and Studio are all marked added after a successful add or
already-added path, and emit journey_step_verified with networks from that
function (using markNetworkAdded and the existing tracking logic as the hook).
Keep the bulk-add behavior unchanged and ensure the event fires only once when
the final required network becomes verified.
- Around line 626-657: The lifecycle timing in completeJourney is captured too
early, so the success/completion/unlock analytics use stale values. Update
BuilderJourney’s completeJourney flow so
markLifecycleTime('role_unlocked:builder') happens before collecting lifecycle
durations, then rebuild the analytics payload used by trackEvent calls
(including builder_role_claim_success, journey_completed, and role_unlocked)
with fresh getLifecycleDurations('builder') data.

In `@frontend/src/routes/CommunityJourney.svelte`:
- Around line 425-456: The analytics payload in completeJourney() is built too
early, so the success/completed/unlocked events can omit the newly recorded role
unlock timing. Move the lifecycle timing update and then recompute the claim
analytics context after markLifecycleTime('role_unlocked:community') in
CommunityJourney.svelte, or build a fresh payload for the post-unlock trackEvent
calls so getLifecycleDurations('community') reflects the latest state.

In `@frontend/src/routes/ValidatorWaitlist.svelte`:
- Around line 187-193: The start-failure tracking in ValidatorWaitlist.svelte is
discarding the caught exception, so every journey_start_error is classified as
backend and network failures are misreported. Update the catch block around the
journey start flow to retain the caught error and derive error_stage from it
before calling trackEvent/getAnalyticsContext, using the existing
journey_start_error reporting path and the validator role_context/selected_role
fields so the classification reflects the actual failure type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f77a4418-a6e0-4d36-a1d1-7dfc1882a36e

📥 Commits

Reviewing files that changed from the base of the PR and between 95aa928 and 3ccb696.

📒 Files selected for processing (5)
  • frontend/src/components/ProfileCompletionGuard.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/routes/BuilderJourney.svelte
  • frontend/src/routes/CommunityJourney.svelte
  • frontend/src/routes/ValidatorWaitlist.svelte

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
frontend/src/components/Sidebar.svelte (1)

269-269: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep these as plain in-app anchors.

These link updates call preventDefault() and route through navigate(...); for normal in-app navigation, leave the <a href="..."> behavior intact and track clicks without taking over navigation.

As per coding guidelines, “Use plain <a href="/path"> anchors for in-app navigation (global interceptor handles SPA routing) instead of calling push() in click handlers.”

Also applies to: 493-493, 734-734, 923-927

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Sidebar.svelte` at line 269, The Sidebar.svelte link
handlers are overriding normal anchor behavior by calling preventDefault() and
navigate(...), which should be removed for in-app navigation. Update the
affected anchor elements in the Sidebar component to use plain <a href="...">
links and keep any click tracking separate from routing, so the global
interceptor can handle SPA navigation; apply this to the leaderboard and the
other matching links referenced in the Sidebar markup.

Source: Coding guidelines

frontend/src/routes/ValidatorWaitlist.svelte (1)

187-193: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the caught error when classifying start failures.

This always reports journey_start_error.error_stage as backend; network failures lose their classification.

🐛 Proposed fix
-    } catch (_) {
+    } catch (err) {
       trackEvent('journey_start_error', getAnalyticsContext({
         role_context: 'validator',
         selected_role: 'validator',
         surface: 'journey',
-        error_stage: 'backend',
+        error_stage: err.response?.status ? 'backend' : 'network',
       }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/ValidatorWaitlist.svelte` around lines 187 - 193, The
start-failure tracking in ValidatorWaitlist.svelte is discarding the caught
exception, so every journey_start_error is classified as backend and network
failures are misreported. Update the catch block around the journey start flow
to retain the caught error and derive error_stage from it before calling
trackEvent/getAnalyticsContext, using the existing journey_start_error reporting
path and the validator role_context/selected_role fields so the classification
reflects the actual failure type.
frontend/src/routes/CommunityJourney.svelte (1)

425-456: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recompute lifecycle durations after marking role unlock.

claimParams is created before markLifecycleTime('role_unlocked:community'), so the success/completed/unlocked analytics can miss the just-recorded unlock timing.

🐛 Proposed fix
       if (res.data?.user) userStore.updateUser(res.data.user);
       await userStore.loadUser?.();
       markLifecycleTime('role_unlocked:community');
-      trackEvent('community_role_claim_success', getAnalyticsContext(claimParams));
+      const successParams = {
+        ...claimParams,
+        ...getLifecycleDurations('community'),
+      };
+      trackEvent('community_role_claim_success', getAnalyticsContext(successParams));
       trackEvent('journey_completed', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         journey_state: 'completed',
       }));
       trackEvent('role_unlocked', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         role_context: 'community',
         selected_role: 'community',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/CommunityJourney.svelte` around lines 425 - 456, The
analytics payload in completeJourney() is built too early, so the
success/completed/unlocked events can omit the newly recorded role unlock
timing. Move the lifecycle timing update and then recompute the claim analytics
context after markLifecycleTime('role_unlocked:community') in
CommunityJourney.svelte, or build a fresh payload for the post-unlock trackEvent
calls so getLifecycleDurations('community') reflects the latest state.
frontend/src/routes/BuilderJourney.svelte (2)

456-484: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Emit journey_step_verified for individual network adds.

Bulk add reports networks as verified, but adding Bradbury/Asimov/Studio one-by-one never emits the verified event when the final missing network is added.

🐛 Proposed fix
 async function addNetwork(network, kind, { silent = false } = {}) {
   if (typeof window === 'undefined') return;
   const walletProvider = provider || window.ethereum;
   if (!walletProvider) {
     showWarning('Please connect your wallet first.');
     return false;
   }
+  const networksCompleteBefore = NETWORKS.every((item) => networkDone(item.kind));

   if (kind === 'bradbury') isAddingBradbury = true;
   if (kind === 'asimov') isAddingAsimov = true;
   if (kind === 'studio') isAddingStudio = true;
@@
       });
       markNetworkAdded(kind);
+      if (!silent && !networksCompleteBefore && NETWORKS.every((item) => networkDone(item.kind))) {
+        trackBuilderStepEvent('journey_step_verified', 'networks');
+      }
       if (!silent) showSuccess(`${network.chainName} added.`);
       return true;
     } catch (error) {
       if (isAlreadyAddedNetworkError(error)) {
         markNetworkAdded(kind);
+        if (!silent && !networksCompleteBefore && NETWORKS.every((item) => networkDone(item.kind))) {
+          trackBuilderStepEvent('journey_step_verified', 'networks');
+        }
         if (!silent) showSuccess(`${network.chainName} already exists in your wallet.`);
         return true;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/BuilderJourney.svelte` around lines 456 - 484, The
individual network add flow in addNetwork currently updates local state and
success messages, but never emits journey_step_verified when the last missing
network is added. Update addNetwork to detect when Bradbury, Asimov, and Studio
are all marked added after a successful add or already-added path, and emit
journey_step_verified with networks from that function (using markNetworkAdded
and the existing tracking logic as the hook). Keep the bulk-add behavior
unchanged and ensure the event fires only once when the final required network
becomes verified.

626-657: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recompute lifecycle durations after marking role unlock.

claimParams is built before markLifecycleTime('role_unlocked:builder'), so success/completion/unlock events reuse stale lifecycle timing data.

🐛 Proposed fix
       await journeyAPI.completeBuilderJourney();
       await userStore.loadUser();
       markLifecycleTime('role_unlocked:builder');
-      trackEvent('builder_role_claim_success', getAnalyticsContext(claimParams));
+      const successParams = {
+        ...claimParams,
+        ...getLifecycleDurations('builder'),
+      };
+      trackEvent('builder_role_claim_success', getAnalyticsContext(successParams));
       trackEvent('journey_completed', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         journey_state: 'completed',
       }));
       trackEvent('role_unlocked', getAnalyticsContext({
-        ...claimParams,
+        ...successParams,
         role_context: 'builder',
         selected_role: 'builder',
         surface: 'journey',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/routes/BuilderJourney.svelte` around lines 626 - 657, The
lifecycle timing in completeJourney is captured too early, so the
success/completion/unlock analytics use stale values. Update BuilderJourney’s
completeJourney flow so markLifecycleTime('role_unlocked:builder') happens
before collecting lifecycle durations, then rebuild the analytics payload used
by trackEvent calls (including builder_role_claim_success, journey_completed,
and role_unlocked) with fresh getLifecycleDurations('builder') data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/ProfileCompletionGuard.svelte`:
- Around line 242-246: The post-success reload in ProfileCompletionGuard.svelte
is still inside the failure-sensitive flow, so a loadUser() error can block the
redirect even after updateUserProfile and journey start succeed. Make the
userStore.loadUser() refresh best-effort by catching its failure locally, or
move the push(journeyPath(targetRole)) navigation so it cannot be skipped by a
reload error; keep the redirect logic anchored around updateUserProfile,
userStore.loadUser, and push.
- Around line 232-240: The caught API failure paths in
ProfileCompletionGuard.svelte only track analytics or update state, but they do
not notify the user. Update the relevant try-catch blocks around the journey/API
actions, including the catch for journey start and the other affected API error
handlers in this component, to call showWarning or showError from the toast
store with a user-facing message. Keep the existing analytics/state handling,
and make sure each API failure path surfaces a toast using the existing toast
helpers.

---

Outside diff comments:
In `@frontend/src/components/Sidebar.svelte`:
- Line 269: The Sidebar.svelte link handlers are overriding normal anchor
behavior by calling preventDefault() and navigate(...), which should be removed
for in-app navigation. Update the affected anchor elements in the Sidebar
component to use plain <a href="..."> links and keep any click tracking separate
from routing, so the global interceptor can handle SPA navigation; apply this to
the leaderboard and the other matching links referenced in the Sidebar markup.

In `@frontend/src/routes/BuilderJourney.svelte`:
- Around line 456-484: The individual network add flow in addNetwork currently
updates local state and success messages, but never emits journey_step_verified
when the last missing network is added. Update addNetwork to detect when
Bradbury, Asimov, and Studio are all marked added after a successful add or
already-added path, and emit journey_step_verified with networks from that
function (using markNetworkAdded and the existing tracking logic as the hook).
Keep the bulk-add behavior unchanged and ensure the event fires only once when
the final required network becomes verified.
- Around line 626-657: The lifecycle timing in completeJourney is captured too
early, so the success/completion/unlock analytics use stale values. Update
BuilderJourney’s completeJourney flow so
markLifecycleTime('role_unlocked:builder') happens before collecting lifecycle
durations, then rebuild the analytics payload used by trackEvent calls
(including builder_role_claim_success, journey_completed, and role_unlocked)
with fresh getLifecycleDurations('builder') data.

In `@frontend/src/routes/CommunityJourney.svelte`:
- Around line 425-456: The analytics payload in completeJourney() is built too
early, so the success/completed/unlocked events can omit the newly recorded role
unlock timing. Move the lifecycle timing update and then recompute the claim
analytics context after markLifecycleTime('role_unlocked:community') in
CommunityJourney.svelte, or build a fresh payload for the post-unlock trackEvent
calls so getLifecycleDurations('community') reflects the latest state.

In `@frontend/src/routes/ValidatorWaitlist.svelte`:
- Around line 187-193: The start-failure tracking in ValidatorWaitlist.svelte is
discarding the caught exception, so every journey_start_error is classified as
backend and network failures are misreported. Update the catch block around the
journey start flow to retain the caught error and derive error_stage from it
before calling trackEvent/getAnalyticsContext, using the existing
journey_start_error reporting path and the validator role_context/selected_role
fields so the classification reflects the actual failure type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f77a4418-a6e0-4d36-a1d1-7dfc1882a36e

📥 Commits

Reviewing files that changed from the base of the PR and between 95aa928 and 3ccb696.

📒 Files selected for processing (5)
  • frontend/src/components/ProfileCompletionGuard.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/routes/BuilderJourney.svelte
  • frontend/src/routes/CommunityJourney.svelte
  • frontend/src/routes/ValidatorWaitlist.svelte
🛑 Comments failed to post (2)
frontend/src/components/ProfileCompletionGuard.svelte (2)

232-240: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Show toast feedback for API failures.

The new/updated API failure paths only emit analytics or set inline state. Add showWarning/showError from the toast store for these caught API errors.

As per coding guidelines, “Always handle API errors with try-catch blocks and provide user-facing error messages via toast notifications.”

Also applies to: 247-265

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/ProfileCompletionGuard.svelte` around lines 232 -
240, The caught API failure paths in ProfileCompletionGuard.svelte only track
analytics or update state, but they do not notify the user. Update the relevant
try-catch blocks around the journey/API actions, including the catch for journey
start and the other affected API error handlers in this component, to call
showWarning or showError from the toast store with a user-facing message. Keep
the existing analytics/state handling, and make sure each API failure path
surfaces a toast using the existing toast helpers.

Source: Coding guidelines


242-246: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don’t let the post-success reload strand the user.

After updateUserProfile and journey start succeed, await userStore.loadUser() can still fail and fall into the outer “profile completion failed” path, preventing the redirect even though the profile was already saved. Make the reload best-effort or move navigation out of this failure path.

Also applies to: 247-251

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/ProfileCompletionGuard.svelte` around lines 242 -
246, The post-success reload in ProfileCompletionGuard.svelte is still inside
the failure-sensitive flow, so a loadUser() error can block the redirect even
after updateUserProfile and journey start succeed. Make the userStore.loadUser()
refresh best-effort by catching its failure locally, or move the
push(journeyPath(targetRole)) navigation so it cannot be skipped by a reload
error; keep the redirect logic anchored around updateUserProfile,
userStore.loadUser, and push.

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