Skip to content

Add user funnel analytics tracking#838

Merged
JoaquinBN merged 7 commits into
devfrom
JoaquinBN/user-funnel-analytics
Jun 28, 2026
Merged

Add user funnel analytics tracking#838
JoaquinBN merged 7 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 end-to-end analytics instrumentation across auth, onboarding/profile completion, funnels/journeys, submissions, and key navigation moments (navbar/sidebar/logo).
    • Introduced consent-aware analytics support, including a new analytics consent banner.
    • Improved analytics for connect-wallet and OAuth flows, plus clearer wallet-selector dismissal tracking.
  • Bug Fixes
    • Improved protected/role-locked redirect behavior and analytics consistency.
    • Reduced duplicate view/exit and “first-time” events via deduplication.
  • Tests
    • Added Vitest coverage for analytics gating, sanitization, page views, funnel/lifecycle timing, and one-time event behavior.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JoaquinBN, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 53 seconds. Learn how PR review limits work.

To continue reviewing without waiting, enable usage-based billing in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6b18cec8-9f98-4dc1-86b7-dcc88df30cca

📥 Commits

Reviewing files that changed from the base of the PR and between a8e5845 and 6394c3a.

📒 Files selected for processing (2)
  • frontend/src/components/AnalyticsConsentBanner.svelte
  • frontend/src/routes/CommunityJourney.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, frontend/src/main.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 consent banner
frontend/src/App.svelte, frontend/src/components/AnalyticsConsentBanner.svelte
Protected and role-locked route guards now emit redirect events, set connect-wallet intent context, track page views from verified location and querystring keys, and render the analytics consent banner; the app bootstrap removes the old async analytics initialization.
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/components/WalletSelector.svelte, frontend/src/routes/SubmitContribution.svelte
Navbar, sidebar, social-link, submit-contribution, wallet selector, and auth button flows now set connect-wallet intent metadata, emit click and redirect analytics, and record wallet selector and wallet auth outcomes.
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#842: Both PRs modify frontend/src/components/ProfileCompletionGuard.svelte in the onboarding/profile-submit flow; the other PR changes the post-submit journey-start/redirection behavior while this one adds analytics around submit, success, and error paths.
🚥 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 onboarding and journey 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.

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
frontend/src/components/SocialLink.svelte (1)

364-376: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Track the journey CTA before redirecting into auth.

In the unauthenticated branch, this only stores wallet intent and opens the auth flow. trackOAuthAction() never runs there, so logged-out users do not emit the new journey_step_action_click event with step_id, step_index, and verification_mode.

Proposed fix
   async function linkAccount({ refreshUsername = false } = {}) {
+    if (!refreshUsername) trackOAuthAction();
+
     if (!$authState.isAuthenticated) {
       const meta = oauthStepMeta();
       setConnectWalletIntent({
         surface: meta ? 'journey' : 'form',
         cta_id: meta ? `${meta.step}_oauth_auth` : `${platform}_oauth_auth`,
@@
       document.querySelector('.auth-button')?.click();
       return;
     }
-
-    if (!refreshUsername) trackOAuthAction();
     handledOAuthResult = false;
🤖 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/SocialLink.svelte` around lines 364 - 376, In
linkAccount in SocialLink.svelte, the unauthenticated path only sets
connect-wallet intent and triggers the auth button, so the journey CTA event is
never tracked there. Move or add the tracking call so logged-out users emit the
same journey_step_action_click event before the auth redirect, using the oauth
step metadata from oauthStepMeta() to populate step_id, step_index, and
verification_mode. Keep the authenticated path behavior unchanged and make sure
the tracking happens before document.querySelector('.auth-button')?.click().
frontend/src/components/AuthButton.svelte (1)

112-146: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Carry the original auth-entry context into the wallet outcome events.

handleAuth() saves the entry metadata in walletSelectorContext, but handleWalletSelected() rebuilds a fresh context for wallet_provider_selected, wallet_auth_started, wallet_auth_success, and wallet_auth_failed. That drops the original cta_id, target_route, and selected_role, so route-guard and deep-link attribution disappears at the most important step.

Proposed fix
   async function handleWalletSelected(provider, walletName) {
     // Start the connection process with selected wallet
     loading = true;
     const provider_type = providerType(walletName);
     const context = getAnalyticsContext({
+      ...walletSelectorContext,
       surface: 'modal',
       provider_type,
     });
     trackEvent('wallet_provider_selected', context);
     trackEvent('wallet_auth_started', context);
@@
       markFunnelTime('wallet_auth_success');
       markLifecycleTime('first_wallet_auth_success');
       trackEvent('wallet_auth_success', getAnalyticsContext({
+        ...walletSelectorContext,
         surface: 'modal',
         provider_type,
         time_from_wallet_click_ms: getFunnelDurationMs('wallet_click'),
       }));
@@
     } catch (err) {
       trackEvent('wallet_auth_failed', getAnalyticsContext({
+        ...walletSelectorContext,
         surface: 'modal',
         provider_type,
         error_stage: walletErrorStage(err),
       }));
🤖 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/AuthButton.svelte` around lines 112 - 146, Carry the
original auth-entry metadata through the wallet flow instead of rebuilding a
fresh analytics context in handleWalletSelected. Reuse the context saved by
handleAuth() in walletSelectorContext for wallet_provider_selected,
wallet_auth_started, wallet_auth_success, and wallet_auth_failed so cta_id,
target_route, and selected_role are preserved. Update the analytics calls in
AuthButton.svelte to extend that stored context with the wallet-specific fields
(like surface, provider_type, time_from_wallet_click_ms, and error_stage) rather
than replacing it.
🤖 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/AnalyticsConsentBanner.svelte`:
- Around line 19-122: The AnalyticsConsentBanner component is using a custom
<style> block for layout, spacing, colors, and responsiveness, but the repo
convention for Svelte components is Tailwind utilities only. Move the styling in
AnalyticsConsentBanner.svelte onto the existing markup for the visible section,
consent-copy, consent-actions, consent-button-primary, and
consent-button-secondary elements using Tailwind classes, and remove the custom
CSS entirely while preserving the same responsive behavior and button states.

---

Outside diff comments:
In `@frontend/src/components/AuthButton.svelte`:
- Around line 112-146: Carry the original auth-entry metadata through the wallet
flow instead of rebuilding a fresh analytics context in handleWalletSelected.
Reuse the context saved by handleAuth() in walletSelectorContext for
wallet_provider_selected, wallet_auth_started, wallet_auth_success, and
wallet_auth_failed so cta_id, target_route, and selected_role are preserved.
Update the analytics calls in AuthButton.svelte to extend that stored context
with the wallet-specific fields (like surface, provider_type,
time_from_wallet_click_ms, and error_stage) rather than replacing it.

In `@frontend/src/components/SocialLink.svelte`:
- Around line 364-376: In linkAccount in SocialLink.svelte, the unauthenticated
path only sets connect-wallet intent and triggers the auth button, so the
journey CTA event is never tracked there. Move or add the tracking call so
logged-out users emit the same journey_step_action_click event before the auth
redirect, using the oauth step metadata from oauthStepMeta() to populate
step_id, step_index, and verification_mode. Keep the authenticated path behavior
unchanged and make sure the tracking happens before
document.querySelector('.auth-button')?.click().
🪄 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: 30319b29-900f-4f1b-96cc-70471435edb3

📥 Commits

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

📒 Files selected for processing (8)
  • frontend/src/App.svelte
  • frontend/src/components/AnalyticsConsentBanner.svelte
  • frontend/src/components/AuthButton.svelte
  • frontend/src/components/SocialLink.svelte
  • frontend/src/components/WalletSelector.svelte
  • frontend/src/lib/analytics.js
  • frontend/src/main.js
  • frontend/src/tests/analytics.test.js

Comment thread frontend/src/components/AnalyticsConsentBanner.svelte Outdated
@JoaquinBN JoaquinBN merged commit 61d2cea into dev Jun 28, 2026
3 checks passed
@JoaquinBN JoaquinBN deleted the JoaquinBN/user-funnel-analytics branch June 28, 2026 10:47
JoaquinBN added a commit that referenced this pull request Jun 28, 2026
* Rework portal onboarding into role funnels and journeys

Builders, Validators, and Community each get a role landing that adapts to
three states: signed-out, signed-in without the role, and earned. Dedicated
journey routes guide users step by step to unlock their role, and journeys
now grant the ROLE itself rather than points: farmable point grants are
removed, and points come only from verifiable tasks (starring the boilerplate
repo, posting on X). Journey progress is durable via zero-point marker
contributions that also drive a "started" state.

The Builder journey is connect GitHub plus star the boilerplate repo. The
Community journey is five verified steps (link X, link Discord, follow on X,
join Discord, and a verified X post) and grants the Creator role on
completion, with the post checked through Sorsa against the linked handle.
The Validator journey stays a single waitlist form and keeps the graduation
reward. Linking GitHub now records a contribution like X and Discord.
Community membership is only created by completing the journey: the previous
automatic creation from community contributions, POAP claims, and Discord XP
is removed. Sidebar subsections stay locked until the relevant journey is
complete, the profile shows an in-progress role with an owner-only grey
badge, and the What's New dialog is shown only to returning users.

## Implementation Notes
- frontend/src/components/funnel/: role funnel dispatcher, per-role landings,
  and shared journey building blocks
- frontend/src/routes/{BuilderJourney,CommunityJourney}.svelte, ValidatorWaitlist.svelte:
  dedicated journey views
- frontend/src/lib/roleState.js: funnel state machine (none/started/earned),
  role/category mapping, journey paths
- frontend/src/{App,components/Sidebar,components/profile/*,routes/Profile}.svelte:
  routing, locked subsections, in-progress role card and header badge
- frontend/src/components/WhatsNewDialog.svelte: gate to returning users only
- backend/users/views.py: start/complete journey endpoints, link_github_account,
  community journey status and X-post verification
- backend/creators/community_journey.py, models.py, views.py: five-step status,
  CommunityPostProof, journey-gated membership
- backend/{leaderboard/models,poaps/signals,community_xp/services}.py: remove
  automatic Creator creation
- backend/social_tasks/sorsa_client.py: tweet lookup for X-post verification
- backend/.../migrations: community-link-github contribution type, builder star
  task, CommunityPostProof

* Update changelog

* Dedup journey/link endpoints and trim funnel landing dead code

Clean up the over-engineering surfaced by the funnel review with no
behavior change. Two role landing components dropped a duplicate CTA
label that mirrored the primary one, and the validator landing's
repeated waitlist badge is now a single snippet. On the backend, the
three social-link award endpoints and the two journey-start endpoints
now delegate to shared helpers; every public endpoint and response
stays identical.

## Claude Implementation Notes
- backend/users/views.py: Add `_award_social_link_points` (link_x/discord/github become thin wrappers) and `_mark_journey_started` (start_builder/start_role become thin wrappers). Endpoints, messages and points unchanged. start_builder now also ensures a 0-point `builder-welcome` multiplier exists, matching the generic path (harmless on a 0-point type).
- frontend/src/components/funnel/CommunityLanding.svelte: Remove redundant `finalLabel`, reuse `primaryLabel`.
- frontend/src/components/funnel/ValidatorLanding.svelte: Remove redundant `finalLabel`; extract the duplicated "on the waitlist" badge markup into a `{#snippet}` rendered in both spots.

* Address PR-837 review: harden funnel idempotency, privacy, and copy

Applies the safe CodeRabbit findings across the builder, community, and
validator funnels with behavior preserved on the happy paths.

Backend: the builder/community completion and legacy creator-join endpoints
are now idempotent under concurrent requests (row lock + get_or_create instead
of check-then-create), the welcome-marker write runs in a single locked
transaction, public profiles no longer expose in-progress funnel state
(has_*_welcome / link booleans), the community X-post gate requires an exact
@mention (boundary regex, not substring), CommunityPostProof.verified_at now
tracks re-verification, the GitHub-link seed migration fails closed when the
builder category is missing, and the Sorsa tweet-info parser guards a
malformed user payload instead of 500-ing.

Frontend: journey init/copy/storage errors that were silently swallowed now
surface as toasts, the builder resources route is gated like the locked
sidebar, stored onboarding roles are validated before redirect, the community
landing copy no longer overpromises points (the journey grants the role; tasks
earn points), the hero progress bar collapses to zero at 0%, the inert notice
"dismiss" control is removed, profiles stop showing earned Community members as
"in progress" while status loads, and the shared journey components drop the
banned 'badge' prop name for 'contribution'.

## Claude Implementation Notes
- backend/users/views.py: idempotent + row-locked complete_builder_journey, complete_community_journey, and _mark_journey_started.
- backend/users/serializers.py: strip has_*_welcome / has_community_link_* from public_profile responses.
- backend/creators/views.py: join_creator_view uses get_or_create in an atomic block.
- backend/creators/community_journey.py: post_matches requires an exact @handle via boundary regex.
- backend/creators/models.py + migrations/0003_communitypostproof.py: verified_at auto_now instead of auto_now_add.
- backend/social_tasks/sorsa_client.py: guard non-dict user payload, raise SorsaError.
- backend/contributions/migrations/0073_create_community_link_github.py: fail closed if builder category absent.
- backend/users/tests/test_social_link_rewards.py, test_community_journey.py: fixtures match prod (builder category; handle from genlayer_handle()).
- frontend journeys (JourneyHeroCard/JourneyStepRow): badge prop -> contribution*; progress fill 0-width at 0%.
- frontend funnel/RoleLanding + routes/BuilderJourney/CommunityJourney/ValidatorWaitlist: navigate-on-success, init/storage/copy error toasts, execCommand return check.
- frontend JourneyNotice: remove inert dismiss control.
- frontend App.svelte: gate /builders/resources with roleGatedRoute.
- frontend ProfileCompletionGuard: validate stored onboarding role.
- frontend Sidebar: request-key guard on community lock-state fetch.
- frontend SocialLink: surface GitHub link-reward failure.
- frontend Profile.svelte: only show Community "in progress" for a loaded started-incomplete journey.
- frontend funnel/CommunityLanding: corrected points copy.
- frontend profile/RoleJourneyCard: anchor CTA instead of push().

* Address PR-837 review round 2: route gates, multiplier races, best-effort refresh

Second batch of CodeRabbit findings on the funnel work.

Backend: the default-multiplier bootstrap is now serialized by contribution
type (lock the type row before exists()/create()), so concurrent requests from
different users can no longer duplicate the default multiplier; the Sorsa
tweet-info parser rejects a non-string username instead of letting lstrip raise
an AttributeError that escapes SorsaError handling.

Frontend: the remaining locked validator/builder subsections
(all-contributions, participants, wall-of-shame) are gated at the route level
to match the locked sidebar, so direct navigation can't skip the funnel; and a
transient userStore.loadUser() failure no longer blocks entering a journey or
the waitlist redirect once the server-side mutation has already succeeded
(refresh is now best-effort).

## Claude Implementation Notes
- backend/users/views.py: lock ContributionType row before the multiplier exists()/create() in both _mark_journey_started and _award_social_link_points.
- backend/social_tasks/sorsa_client.py: raise SorsaError when user.username is not a string.
- frontend/src/App.svelte: roleGatedRoute for /builders/all-contributions, /validators/all-contributions, /validators/participants, /validators/wall-of-shame (sidebar already locks these).
- frontend/src/components/funnel/RoleLanding.svelte: navigate right after startRoleJourney succeeds; loadUser best-effort (fire-and-forget).
- frontend/src/routes/ValidatorWaitlist.svelte: startValidatorJourney is the only required step; loadUser + sessionStorage best-effort; redirect always runs on join success.

* Apply role journeys to newcomers, grandfather existing members

The role funnels are for onboarding newcomers only: anyone who already holds
a role (community member, builder, or waitlisted/graduated validator) keeps
full access without being pushed back through a journey. "Earned" is now read
straight from role membership for all three categories, so existing community
members reach their dashboard and unlocked subsections immediately. This drops
the live community journey-status round-trip that previously gated the earned
state, which also removes the spinner on every community entry, the sidebar
lock flash, and the misclassification of an earned member as in-progress on a
transient status-call failure.

Builder status auto-grant on steward-accept and leaderboard recalc no longer
re-creates the removed builder-welcome (+20) and builder (+50) point
contributions (the +20 had also become out-of-range against its now point-free
type); it still grants the role, which is human-gated, not farmable. The
in-progress funnel state stays owner-only but is no longer stripped from the
owner's own public profile, so their private grey "only you can see this"
badge renders again.

## Implementation Notes
- creators/community_journey.py: journey_status treats a Creator as
  started/complete/is_member regardless of the newer marker + step records
- users/views.py: complete_community_journey short-circuits an existing Creator
  with a 200 before the step gate (removes the now-dead later guard)
- leaderboard/models.py: ensure_builder_status only ensures the Builder profile,
  no longer writes the builder-welcome/builder point contributions
- users/serializers.py: in-progress funnel flags are stripped only from other
  viewers on a public profile, not from the owner
- components/funnel/RoleFunnel.svelte, App.svelte, components/Sidebar.svelte:
  community earned/lock state reads role membership synchronously like the
  other roles; removed the async journey-status gating
- users/tests/test_community_journey.py: assert existing Creators are
  grandfathered instead of re-funneled

* Gate builder claim on GitHub+star; protect personal routes

The Builder journey now enables the Claim button as soon as the role-gating
steps are done, connecting GitHub and starring the boilerplate repo, which
matches the backend that grants the role on the star task alone. Adding
networks, getting testnet GEN, and deploying are relabeled as recommended next
steps rather than blockers, so an eligible builder can no longer be stalled by
client-side-only progress that does not survive across devices. The displayed
journey and the actual requirement now agree.

Separately, the personal authenticated-only surfaces (submit contribution, my
submissions, edit submission, edit profile, and notifications) are wrapped so a
signed-out visitor is sent to sign in instead of seeing the page shell.

## Implementation Notes
- routes/BuilderJourney.svelte: claim gated on the required steps
  (wallet/github/star) instead of all six; networks, top-up, and deploy marked
  "Recommended"; hero, notice, and completion copy updated; removed the now
  unused step counters
- App.svelte: wrap /submit-contribution, /my-submissions, /contributions/:id,
  /profile, and /notifications in protectedRoute

* Consolidate funnel role landings and polish journey UI

Introduces a shared AuthenticatedRoleLanding component and trims the per-role
landings (Community and Validator) down onto it, removing duplicated signed-in
markup. The journey hero and step-row components and the builder, community,
and validator journey views get UI polish, and the profile header and rankings
widget are adjusted to match. Playwright artifacts are added to .gitignore.

## Implementation Notes
- components/funnel/AuthenticatedRoleLanding.svelte: new shared signed-in role
  landing
- components/funnel/{RoleLanding,BuilderLanding,CommunityLanding,ValidatorLanding}.svelte:
  route the signed-in state through the shared landing, drop duplicated markup
- components/funnel/journeys/{JourneyHeroCard,JourneyStepRow}.svelte: presentation polish
- routes/{BuilderJourney,CommunityJourney,ValidatorWaitlist}.svelte: journey view polish
- components/profile/{ProfileHeader,RankingsWidget}.svelte: profile adjustments
- users/tests/test_builder_journey.py: extend builder journey coverage
- .gitignore: ignore Playwright report/test-results artifacts

* Fix funnel signed-out flash and validator start atomicity

RoleFunnel now shows a neutral spinner while the session is still being
verified, instead of deriving the signed-in state too early: a logged-in user
with an existing session no longer briefly sees the signed-out landing flash
before their dashboard loads.

The validator journey start now preflights the waitlist contribution type and
wraps the started marker and the waitlist contribution in a single
transaction, so a misconfigured type or a failed write can no longer leave a
user marked "started" without the single waitlist step.

Also reworded an owner-only profile comment to drop forbidden "badge" wording.

## Implementation Notes
- components/funnel/RoleFunnel.svelte: gate rendering on authState.hasVerified
  so the spinner covers the pending-verification window
- users/views.py: start_validator_journey resolves validator-waitlist before
  marking started and runs both writes inside one transaction.atomic()
- users/serializers.py: in-progress funnel state comment uses "indicator"

* Update funnel creator journeys and modals

* Address funnel PR review feedback

* Update route OG previews (#840)

* Update unauthenticated landing pages (#841)

* Fix community leaderboard search test fixture

* Update journey onboarding views (#842)

* Show community journey task points

* Add user funnel analytics tracking (#838)

* Add user funnel analytics tracking

* Address analytics review feedback

* Address analytics consent and funnel gaps

* Use Tailwind for analytics consent banner

* Add network activity heatmap tab

* Add grouped evidence URL requirements for contribution types (#844)

* Add grouped evidence URL requirements for contribution types

Contribution types gain an optional evidence URL group rule: each group
must be satisfied by at least one submitted evidence URL, with AND across
groups and OR within a group, so a type can require a contract code link
and a deployed-contract explorer link together. Grouped types are
implicitly accepted when an accepted-types whitelist is set, matching the
existing required-types behavior. A new GenLayer Explorer Contract evidence
type recognizes Asimov, Bradbury, Studio, and Testnet explorer contract
addresses, and the Studio import contract pattern is tightened to avoid
false matches.

## Implementation Notes
- backend/contributions/models.py: Add required_evidence_url_type_groups JSONField to ContributionType.
- backend/contributions/serializers.py: Enforce the group rule during evidence validation and union grouped types into the accepted whitelist.
- backend/contributions/migrations/0073_contributiontype_required_evidence_url_type_groups.py: Field migration.
- backend/contributions/migrations/0074_intelligent_contracts_evidence_types.py: Seed the GenLayer Explorer Contract type and tighten the studio-contract regex.
- backend/contributions/tests/test_required_evidence_url_type_groups.py: Cover the group rule and whitelist interaction.
- backend/contributions/tests/test_required_evidence_url_types.py: Make setUp robust to migration-seeded URL types.

* Update changelog

* Combine Intelligent Contracts migrations into one

Merge the required_evidence_url_type_groups field add and the evidence URL
type seed into a single migration; the two operations are independent so
they need no separate steps.

## Implementation Notes
- backend/contributions/migrations/0075_intelligent_contracts_evidence_types.py: Single migration with the AddField plus the RunPython seed (explorer type + tightened studio-contract regex).
- backend/contributions/migrations/0076_intelligent_contracts_evidence_types.py: Removed; folded into 0075.
- backend/contributions/tests/test_required_evidence_url_type*.py: Update seed-migration reference in setUp comment.

* Validate grouped evidence URL config and tighten related tests

ContributionType save now rejects a malformed required_evidence_url_type_groups
value (non-list, empty group, or unknown EvidenceURLType slug) instead of
letting it become an impossible submission rule. The whitelist mismatch error
now lists grouped types among the accepted ones, matching the acceptance logic.
The studio-contract test fixture uses the tightened production pattern with a
real import-contract URL so the suite exercises the shipped detection.

## Implementation Notes
- backend/contributions/models.py: Validate required_evidence_url_type_groups shape and slugs in ContributionType.clean().
- backend/contributions/serializers.py: Include grouped type names in the whitelist "Expected" error text.
- backend/contributions/tests/test_required_evidence_url_type_groups.py: Add clean() validation tests for malformed and unknown-slug groups.
- backend/contributions/tests/test_required_evidence_url_types.py: Align the studio-contract fixture and test URL with the tightened regex.

* Address review feedback on network activity heatmap

The heatmap now derives its calendar bounds from the backend's declared
activity window instead of the first/last populated rows, so the grid keeps
its full six-month span even if zero-activity edge days are ever omitted. The
view toggle exposes its selected state to assistive tech via aria-pressed, and
the legacy snapshot upconvert path now normalizes the full v5 schema so older
snapshots and fresh payloads share an identical shape.

## Claude Implementation Notes
- backend/api/overview_metrics.py: default latest_week_by_source to {} when upconverting v3/v4 snapshots to v5
- backend/api/tests.py: assert latest_week_by_source is present in the v3-to-v5 normalization test
- frontend/src/components/portal/ActivityHeatmap.svelte: accept activityWindow prop and prefer it for grid start/end, falling back to row bounds
- frontend/src/components/portal/NetworkActivity.svelte: pass activity_window to the heatmap and add aria-pressed to the Graph/Activity tabs

* Make declared activity window authoritative for heatmap cells

The heatmap grid bounds already followed the backend's declared window, but cell
inclusion still keyed on row presence, so a date missing from inside the window
would blank out and shift month labels. Cell inclusion now derives from the
window range, and absent days render as zero-count cells instead of gaps.

## Claude Implementation Notes
- frontend/src/components/portal/ActivityHeatmap.svelte: compute inRange from the window-derived first/last date range (and past-or-today) rather than Boolean(row); missing rows already default to zero counts

* Add social task eligibility gates (#846)
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