Skip to content

support the sign up experiment#8285

Merged
smallst merged 7 commits intomasterfrom
yuqiang/sign-up-experiment
Apr 16, 2026
Merged

support the sign up experiment#8285
smallst merged 7 commits intomasterfrom
yuqiang/sign-up-experiment

Conversation

@smallst
Copy link
Copy Markdown
Contributor

@smallst smallst commented Apr 14, 2026

see https://github.com/codecombat/codecombat-server/pull/1430 too

db steps:
edit mandate in admin page to add require-sign-up experiment control/beta
edit campaign.levels to add allowAnonymous to true.

Summary by CodeRabbit

  • New Features

    • Per-level "Require SignUp" toggle to control anonymous access; blocked anonymous users are prompted to create an account with custom messaging.
    • A/B experiment gating for select campaigns to control when anonymous users see the signup prompt.
    • Campaign settings include classroom and anonymous-access controls.
    • New English copy for the account prompt when unlocking the next level.
  • Localization

    • Simplified Chinese subscription wording and billing frequency updated.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8726e89a-d9ef-46bb-a6cf-9ffea8d80e09

📥 Commits

Reviewing files that changed from the base of the PR and between 1d67048 and 4a76c82.

📒 Files selected for processing (1)
  • app/views/play/CampaignView.js

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

Adds a require-signup experiment and helpers on the User model, a per-level requiresSignUp flag (propagated into Campaign denormalized levels), and anonymous-user gates in CampaignView and HeroVictoryModal that prompt account creation for users assigned the experiment.

Changes

Cohort / File(s) Summary
User experiment methods
app/models/User.js
Add REQUIRE_SIGN_UP_EXPERIMENT mapping and instance methods getRequireSignupExperimentValue(CAMPAIGN) and getOrStartRequireSignupExperimentValue(CAMPAIGN) to resolve or start require-signup experiments (prefers query param, respects anonymity).
Level & campaign schema
app/schemas/models/level.js, app/schemas/models/campaign.schema.js
Add requiresSignUp boolean property to LevelSchema; include classroomSub and requiresSignUp in CampaignSchema.denormalizedLevelProperties and ensure CampaignSchema.definitions.classroomSub is deep-cloned from LevelSchema.definitions?.classroomSub.
Play view gating
app/views/play/CampaignView.js, app/views/play/level/modal/HeroVictoryModal.js
When target level has requiresSignUp and me.isAnonymous(), call me.getOrStartRequireSignupExperimentValue(CAMPAIGN) and, if 'beta', open CreateAccountModal via promptForSignup({ accountRequiredMessage }) and abort further start/navigation flow. promptForSignup now accepts options = {}.
Localization
app/locale/en.js, app/locale/zh-HANS.js
Add translation.account.unlock_next_level_with_sign_up (en). Update Simplified Chinese translations: buy_gems.prompt_button.buy_premium and subscribe.stripe_description.

Sequence Diagram(s)

sequenceDiagram
    participant CV as CampaignView
    participant HM as HeroVictoryModal
    participant User as UserModel
    participant Exp as ExperimentService
    participant UI as CreateAccountModal

    CV->>User: onClickStartLevel(level) — check level.requiresSignUp && me.isAnonymous()
    alt anonymous && requiresSignUp
        User->>Exp: getOrStartRequireSignupExperimentValue(CAMPAIGN)
        Exp-->>User: "beta" / "control" / null
        alt "beta"
            CV->>UI: promptForSignup({accountRequiredMessage})
            UI-->>CV: modal opened (stop start flow)
        else
            CV->>CV: proceed with normal start flow
        end
    else
        CV->>CV: proceed with normal start flow
    end

    HM->>User: onClickContinue — determine next/practice level requiresSignUp
    alt anonymous && requiresSignUp
        User->>Exp: getOrStartRequireSignupExperimentValue('junior')
        Exp-->>User: "beta" / "control" / null
        alt "beta"
            HM->>UI: open CreateAccountModal(accountRequiredMessage)
            UI-->>HM: modal opened (stop navigation)
        else
            HM->>HM: continue navigation
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • mrfinch
  • nwinter

Poem

🐰 I hopped through code and tests tonight,
A soft beta gate in carrot-colored light,
Anonymous hops nudged to save their quest,
I pop a modal, cozy and blessed,
Carrots, levels, and signups — delight! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'support the sign up experiment' is vague and generic, failing to describe the specific implementation details or main changes introduced in the changeset. Consider a more specific title such as 'Add sign-up experiment gates for anonymous users on dungeon and junior campaigns' or 'Implement require-sign-up experiment with conditional account prompts'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yuqiang/sign-up-experiment

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/schemas/models/level.js (1)

436-436: Make allowAnonymous default explicit for schema clarity.

Line [436] currently relies on implicit falsy handling downstream; adding an explicit default makes intent clearer and avoids ambiguous editor behavior.

🧹 Suggested tweak
-  allowAnonymous: { title: 'Allow Anonymous Play', description: 'Whether this level is available to anonymous user', type: 'boolean', inEditor: 'codecombat' },
+  allowAnonymous: { title: 'Allow Anonymous Play', description: 'Whether this level is available to anonymous users', type: 'boolean', default: false, inEditor: 'codecombat' },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/schemas/models/level.js` at line 436, The allowAnonymous property in the
Level schema should have an explicit default to avoid implicit falsy
assumptions; update the schema definition for allowAnonymous (the property
currently titled 'Allow Anonymous Play' in the exported Level model) to include
default: false so the editor and validators receive an explicit value and
downstream code behaves predictably.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/schemas/models/campaign.schema.js`:
- Around line 271-272: The CampaignSchema is copying
LevelSchema.properties.classroomSub which contains a $ref to
'#/definitions/classroomSub' that CampaignSchema does not define, causing a
dangling reference; fix by replacing the cloned reference with an inline schema
or adding the missing definition: either (A) inline the actual classroomSub
schema where you currently copy LevelSchema.properties.classroomSub (remove the
$ref and expand its properties) or (B) add a definitions.classroomSub entry to
CampaignSchema matching the referenced schema so the $ref resolves; locate the
cloning code that references LevelSchema.properties.classroomSub and update it
to one of these two options (ensure symbols: LevelSchema.properties.classroomSub
and CampaignSchema are updated).

In `@app/views/play/CampaignView.js`:
- Around line 1931-1936: The code currently does "return this.promptForSignup()"
inside the me.isAnonymous() / requireSignup check which can block level start
when promptForSignup() is a no-op; instead invoke promptForSignup() without
unconditionally returning and only short-circuit if the call explicitly signals
a blocking action. Change the block around me.isAnonymous(),
me.getOrStartRequireSignupExperimentValue(), and level.allowAnonymous so you
call this.promptForSignup() and then "if (this.promptForSignup()) return;" (or
store the result in a var and return only when truthy) rather than always
returning its result.

---

Nitpick comments:
In `@app/schemas/models/level.js`:
- Line 436: The allowAnonymous property in the Level schema should have an
explicit default to avoid implicit falsy assumptions; update the schema
definition for allowAnonymous (the property currently titled 'Allow Anonymous
Play' in the exported Level model) to include default: false so the editor and
validators receive an explicit value and downstream code behaves predictably.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 255faf8f-44b5-48a8-ac15-8a63f6a9e808

📥 Commits

Reviewing files that changed from the base of the PR and between dbb0452 and 3ab63a1.

📒 Files selected for processing (4)
  • app/models/User.js
  • app/schemas/models/campaign.schema.js
  • app/schemas/models/level.js
  • app/views/play/CampaignView.js

Comment thread app/schemas/models/campaign.schema.js Outdated
Comment thread app/views/play/CampaignView.js Outdated
Comment thread app/views/play/CampaignView.js Outdated
Comment thread app/models/User.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/views/play/level/modal/HeroVictoryModal.js`:
- Around line 621-627: The current gating logic in HeroVictoryModal.js checks
this.nextLevel before honoring options.sendToPracticeLevel, which can read
allowAnonymous from the wrong destination; update the branch so that when
options.sendToPracticeLevel is true and this.practiceLevel?.get('slug') exists
you set allowAnonymous = this.practiceLevel.get('allowAnonymous'), otherwise
fall back to this.nextLevel?.get('slug') and its allowAnonymous; keep the
existing product check for 'codecombat-junior' and ensure you use safe null
checks (this.practiceLevel and this.nextLevel) when reading
get('allowAnonymous') so the practice path is chosen when sendToPracticeLevel is
true.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa62504e-7c2a-4505-ad71-af88c4bbe671

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab63a1 and 480c290.

📒 Files selected for processing (4)
  • app/locale/zh-HANS.js
  • app/models/User.js
  • app/views/play/CampaignView.js
  • app/views/play/level/modal/HeroVictoryModal.js
✅ Files skipped from review due to trivial changes (2)
  • app/views/play/CampaignView.js
  • app/models/User.js

Comment thread app/views/play/level/modal/HeroVictoryModal.js Outdated
Copy link
Copy Markdown
Contributor

@mrfinch mrfinch left a comment

Choose a reason for hiding this comment

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

When the signup modal opens with the date page, can u add these message there:
You've unlocked the next level! Create a free account to save your progress and continue your coding journey.
below Parents, use your own birthdate. probably passing via: accountRequiredMessage

Comment thread app/views/play/level/modal/HeroVictoryModal.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
app/views/play/CampaignView.js (1)

1931-1936: ⚠️ Potential issue | 🟠 Major

Avoid hard-return when signup prompt is suppressed.

On Line 1935, returning this.promptForSignup(...) can block level start when promptForSignup no-ops (HoC/noAuth paths) and returns nothing.

Proposed fix
     if (!level.allowAnonymous && me.isAnonymous()) {
       const CAMPAIGN = this.campaign?.get('name')?.toLowerCase() || this.terrain
       const requireSignup = me.getOrStartRequireSignupExperimentValue?.(CAMPAIGN)
       if (requireSignup === 'beta') {
-        return this.promptForSignup({ accountRequiredMessage: $.i18n.t('account.unlock_next_level_with_sign_up') })
+        const promptResult = this.promptForSignup({ accountRequiredMessage: $.i18n.t('account.unlock_next_level_with_sign_up') })
+        if (promptResult) {
+          return promptResult
+        }
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/play/CampaignView.js` around lines 1931 - 1936, The current code
returns the result of this.promptForSignup(...) inside the block guarding
level.allowAnonymous/me.isAnonymous(), which can unintentionally short-circuit
level start when promptForSignup is a no-op and returns nothing; instead call
this.promptForSignup(...) (await if it is async) without returning its value so
execution continues normally — update the conditional that uses
level.allowAnonymous, me.isAnonymous(), CAMPAIGN, requireSignup and replace
"return this.promptForSignup(...)" with a non-returning invocation (e.g.,
this.promptForSignup(...); and then continue/return undefined explicitly if
needed) to avoid blocking level start.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/views/play/CampaignView.js`:
- Around line 1931-1936: The current code returns the result of
this.promptForSignup(...) inside the block guarding
level.allowAnonymous/me.isAnonymous(), which can unintentionally short-circuit
level start when promptForSignup is a no-op and returns nothing; instead call
this.promptForSignup(...) (await if it is async) without returning its value so
execution continues normally — update the conditional that uses
level.allowAnonymous, me.isAnonymous(), CAMPAIGN, requireSignup and replace
"return this.promptForSignup(...)" with a non-returning invocation (e.g.,
this.promptForSignup(...); and then continue/return undefined explicitly if
needed) to avoid blocking level start.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59cd5387-442c-4b28-ba71-e05b84e5403f

📥 Commits

Reviewing files that changed from the base of the PR and between 480c290 and 61b67b5.

📒 Files selected for processing (4)
  • app/locale/en.js
  • app/schemas/models/campaign.schema.js
  • app/views/play/CampaignView.js
  • app/views/play/level/modal/HeroVictoryModal.js
✅ Files skipped from review due to trivial changes (1)
  • app/locale/en.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/schemas/models/campaign.schema.js

Comment thread app/views/play/CampaignView.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/models/User.js (1)

1485-1495: Missing validation for invalid CAMPAIGN parameter.

Unlike getOrStartRequireSignupExperimentValue, this method doesn't validate that CAMPAIGN is a key in REQUIRE_SIGN_UP_EXPERIMENT. If called directly with an invalid campaign, REQUIRE_SIGN_UP_EXPERIMENT[CAMPAIGN] will be undefined, which gets passed to getExperimentValueFromQuery and getExperimentValue potentially causing unexpected behavior.

Consider adding validation or documenting that callers should use getOrStartRequireSignupExperimentValue instead.

Proposed defensive check
 getRequireSignupExperimentValue (CAMPAIGN) {
+  if (!REQUIRE_SIGN_UP_EXPERIMENT[CAMPAIGN]) {
+    return null
+  }
   if (!me.isAnonymous()) {
     return 'control'
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/User.js` around lines 1485 - 1495, The method
getRequireSignupExperimentValue lacks validation of the CAMPAIGN argument
against the REQUIRE_SIGN_UP_EXPERIMENT map; add a defensive check at the start
of getRequireSignupExperimentValue to verify
REQUIRE_SIGN_UP_EXPERIMENT.hasOwnProperty(CAMPAIGN) (or CAMPAIGN in
REQUIRE_SIGN_UP_EXPERIMENT) and return null or 'control' (match existing
behavior) when invalid, so you don't pass undefined into
utils.getExperimentValueFromQuery or me.getExperimentValue; ensure the change
references getRequireSignupExperimentValue and REQUIRE_SIGN_UP_EXPERIMENT and
keeps existing anonymous-user early-return logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/models/User.js`:
- Around line 1485-1495: The method getRequireSignupExperimentValue lacks
validation of the CAMPAIGN argument against the REQUIRE_SIGN_UP_EXPERIMENT map;
add a defensive check at the start of getRequireSignupExperimentValue to verify
REQUIRE_SIGN_UP_EXPERIMENT.hasOwnProperty(CAMPAIGN) (or CAMPAIGN in
REQUIRE_SIGN_UP_EXPERIMENT) and return null or 'control' (match existing
behavior) when invalid, so you don't pass undefined into
utils.getExperimentValueFromQuery or me.getExperimentValue; ensure the change
references getRequireSignupExperimentValue and REQUIRE_SIGN_UP_EXPERIMENT and
keeps existing anonymous-user early-return logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c0362250-c038-464f-b546-c87dc4b88019

📥 Commits

Reviewing files that changed from the base of the PR and between 61b67b5 and 1d67048.

📒 Files selected for processing (5)
  • app/models/User.js
  • app/schemas/models/campaign.schema.js
  • app/schemas/models/level.js
  • app/views/play/CampaignView.js
  • app/views/play/level/modal/HeroVictoryModal.js
✅ Files skipped from review due to trivial changes (1)
  • app/schemas/models/level.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/schemas/models/campaign.schema.js
  • app/views/play/CampaignView.js

@smallst smallst merged commit 563a0fa into master Apr 16, 2026
1 of 3 checks passed
@smallst smallst deleted the yuqiang/sign-up-experiment branch April 16, 2026 10:36
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.

2 participants