Skip to content

feat: signup corporate-only experiment with exposure and new_signup events#7979

Open
Zaimwa9 wants to merge 8 commits into
mainfrom
feat/signup-corporate-only-experiment
Open

feat: signup corporate-only experiment with exposure and new_signup events#7979
Zaimwa9 wants to merge 8 commits into
mainfrom
feat/signup-corporate-only-experiment

Conversation

@Zaimwa9

@Zaimwa9 Zaimwa9 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Sets up our first dogfood experiment, Signup corporate domains only.

The experiment tests whether requiring a work email address at signup affects conversion. Control keeps today's behaviour (an informational message for free email domains); the signup_corporate_only variant disables the Create Account button for free email domains and shows "Please use your work email address to create your account."

  • Upgrade @flagsmith/flagsmith to 12.1.0 for trackEvent, trackExposureEvent, getExperimentFlag and the variant key on flags.
  • New useSignupExperiment hook: identifies anonymous signup visitors with a transient identity (persistent signup_anonymous_id so returning visitors keep their variant), resolves the signup_corporate_only flag's variant, and records a $flag_exposure. Takes a boolean to fall back to environment flags — no identify, no exposure, control experience — for non-participants (invitees, login views, logged-in users, preventEmailPassword).
  • new_signup conversion event on successful registration, attributed to the same anonymous identity, with free_email_domain, invite, signup_method and utm_source metadata.
  • Signup button now also requires non-blank first and last names.

How did you test this code?

Manually, against the staging configuration with the flag and experiment set up:

  1. Open /signup logged out — the network tab shows identities/?identifier=<uuid>&transient=true, followed (≤10s) by a POST /v1/events containing a $flag_exposure with the variant as value (verified 202).
  2. In the treatment variant, entering a gmail.com address disables Create Account and shows the work-email message; a work domain enables it. Control behaviour is unchanged.
  3. Completing a signup sends new_signup with the same identifier and metadata.
  4. Invite links and the login view trigger no identify/exposure and always get control.
  5. Variant bucketing verified ~50/50 across identifiers against the identities API; unit tests (236) and typecheck pass.

@Zaimwa9 Zaimwa9 requested a review from a team as a code owner July 10, 2026 11:07
@Zaimwa9 Zaimwa9 requested review from talissoncosta and removed request for a team July 10, 2026 11:07
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ready Ready Preview, Comment Jul 10, 2026 11:10am
flagsmith-frontend-staging Ready Ready Preview, Comment Jul 10, 2026 11:10am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview Jul 10, 2026 11:10am

Request Review

@github-actions github-actions Bot added the front-end Issue related to the React Front End Dashboard label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The signup flow now records Flagsmith new_signup events with registration metadata. A new hook persists an anonymous identifier and reads the signup_corporate_only experiment variant. The homepage uses that variant to block free email domains, display work-email guidance, and disable account creation. The Flagsmith frontend dependency is updated to version 12.1.0.

Estimated code review effort: 3 (Moderate) | ~20 minutes


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

@github-actions github-actions Bot added the feature New feature or request label Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Docker builds report

Image Build Status Security report
ghcr.io/flagsmith/flagsmith-api-test:pr-7979 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api-test:pr-7979 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-api:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-frontend:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-e2e:pr-7979 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-frontend:pr-7979 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-e2e:pr-7979 Finished ✅ Skipped

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  4 passed

Details

stats  4 tests across 3 suites
duration  35.1 seconds
commit  0f99759
info  🔄 Run: #18155 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  46.5 seconds
commit  71683f0
info  🔄 Run: #18154 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  4 passed

Details

stats  4 tests across 3 suites
duration  46 seconds
commit  0f99759
info  🔄 Run: #18155 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  5 passed

Details

stats  5 tests across 4 suites
duration  47.1 seconds
commit  71683f0
info  🔄 Run: #18154 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  7 passed

Details

stats  7 tests across 5 suites
duration  12 seconds
commit  0f99759
info  🔄 Run: #18155 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  47.4 seconds
commit  0f99759
info  🔄 Run: #18155 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  59.3 seconds
commit  71683f0
info  🔄 Run: #18154 (attempt 1)

@github-actions

Copy link
Copy Markdown
Contributor

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  4 passed

Details

stats  4 tests across 3 suites
duration  34.1 seconds
commit  71683f0
info  🔄 Run: #18154 (attempt 1)

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Visual Regression

19 screenshots compared. See report for details.
View full report

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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a04fcc89-95a6-4ac2-aec1-a2a6a17d5749

📥 Commits

Reviewing files that changed from the base of the PR and between a7b0d21 and 0f99759.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • frontend/common/stores/account-store.js
  • frontend/common/useSignupExperiment.ts
  • frontend/package.json
  • frontend/web/components/pages/HomePage.tsx

@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react'
import flagsmith from '@flagsmith/flagsmith'
import { storageGet, storageSet } from './safeLocalStorage'

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Relative import violates path-import guideline.

./safeLocalStorage is a relative import; per repository guidelines this file should import via the common/ path instead.

♻️ Proposed fix
-import { storageGet, storageSet } from './safeLocalStorage'
+import { storageGet, storageSet } from 'common/safeLocalStorage'

As per coding guidelines, "Use only common/, components/, and project/ import paths; do not use relative imports" for frontend/**/*.{js,jsx,ts,tsx}.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { storageGet, storageSet } from './safeLocalStorage'
import { storageGet, storageSet } from 'common/safeLocalStorage'

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is enforced by the repo's linter: @dword-design/import-alias/prefer-alias rewrites imports to the common/ alias except for same-directory imports, which it keeps relative — eslint --fix converts common/safeLocalStorage back to ./safeLocalStorage here. Keeping the linter's form.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "import-alias" frontend/.eslintrc* 2>/dev/null
fd -H '.eslintrc*' frontend -x cat {}

Length of output: 5028


Confirmed — the .eslintrc.js config enables @dword-design/import-alias/prefer-alias with common aliased to ./common/, and this plugin intentionally keeps same-directory imports relative rather than rewriting them to the alias. Since useSignupExperiment.ts and safeLocalStorage both live in frontend/common/, the relative import here is the linter's expected/enforced form, not a violation.

@Zaimwa9 thanks for the clarification — that makes sense, and my original suggestion would have been immediately reverted by eslint --fix. Withdrawing the comment.

(=^・ω・^=) ah, makes sense now

Comment on lines +5 to +24
export default function useSignupExperiment(useEnvironmentFlag: boolean) {
const [variant, setVariant] = useState<string>()

useEffect(() => {
if (useEnvironmentFlag) {
return
}
const identifyAndExpose = async () => {
const id = storageGet('signup_anonymous_id') || crypto.randomUUID()
storageSet('signup_anonymous_id', id)
// @ts-expect-error transient is missing from the SDK's identify type
await flagsmith.identify(id, {}, true)
const flag = flagsmith.getExperimentFlag('signup_corporate_only')
setVariant(flag?.enabled ? flag.variant : undefined)
}
identifyAndExpose()
}, [useEnvironmentFlag])

return variant
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Local useState in a common/ hook may conflict with the Redux Toolkit state-management guideline.

This hook manages variant with plain React useState rather than Redux Toolkit/RTK Query as prescribed for files under frontend/common/**/*.{ts,tsx}. If this experiment variant needs to be consumed by more than one component, consider modelling it as store state instead of a bespoke hook-local state.

As per coding guidelines, "Use Redux Toolkit and RTK Query for state management, with the store located in common/store.ts" for frontend/common/**/*.{ts,tsx}.

Source: Coding guidelines

Comment on lines +8 to +21
useEffect(() => {
if (useEnvironmentFlag) {
return
}
const identifyAndExpose = async () => {
const id = storageGet('signup_anonymous_id') || crypto.randomUUID()
storageSet('signup_anonymous_id', id)
// @ts-expect-error transient is missing from the SDK's identify type
await flagsmith.identify(id, {}, true)
const flag = flagsmith.getExperimentFlag('signup_corporate_only')
setVariant(flag?.enabled ? flag.variant : undefined)
}
identifyAndExpose()
}, [useEnvironmentFlag])

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files 'frontend/common/*' | rg 'useSignupExperiment|HomePage|signup|Experiment'

Repository: Flagsmith/flagsmith

Length of output: 406


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files 'frontend/common/*' | rg 'useSignupExperiment|HomePage|signup|Experiment'

Repository: Flagsmith/flagsmith

Length of output: 413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Search usages:\n'
rg -n "useSignupExperiment" frontend/common || true

printf '\nHook file:\n'
sed -n '1,200p' frontend/common/useSignupExperiment.ts

printf '\nHomePage candidates:\n'
rg -n "blockGenericEmailDomain|signup_corporate_only|variant" frontend/common || true

Repository: Flagsmith/flagsmith

Length of output: 1970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
paths = [
    Path('frontend/common/useSignupExperiment.ts'),
]
for p in paths:
    print(f'--- {p} ---')
    if p.exists():
        for i, line in enumerate(p.read_text().splitlines(), 1):
            print(f'{i:4}: {line}')
    else:
        print('missing')
PY

Repository: Flagsmith/flagsmith

Length of output: 1185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'useSignupExperiment usages:\n'
rg -n "useSignupExperiment" frontend/common || true

printf '\nuseSignupExperiment.ts:\n'
cat -n frontend/common/useSignupExperiment.ts

printf '\nPotential consumers / related terms:\n'
rg -n "blockGenericEmailDomain|signup_corporate_only|variant" frontend/common || true

Repository: Flagsmith/flagsmith

Length of output: 2180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'frontend/**/*.ts' 'frontend/**/*.tsx' | rg 'useSignupExperiment|HomePage|Signup|signup' || true

Repository: Flagsmith/flagsmith

Length of output: 239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'HomePage outline:\n'
ast-grep outline frontend/web/components/pages/HomePage.tsx --view expanded || true

printf '\nHomePage relevant section:\n'
sed -n '1,260p' frontend/web/components/pages/HomePage.tsx

printf '\nSearch for signup email gating:\n'
rg -n "blockGenericEmailDomain|useSignupExperiment|signup_corporate_only|generic email|corporate" frontend/web/components/pages/HomePage.tsx frontend/common || true

Repository: Flagsmith/flagsmith

Length of output: 10025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'HomePage around email gating and submit:\n'
sed -n '560,760p' frontend/web/components/pages/HomePage.tsx

printf '\nSubmit handlers:\n'
rg -n "handle.*Submit|onSubmit|blockGenericEmailDomain|isFreeEmailDomain|signupVariant" frontend/web/components/pages/HomePage.tsx

Repository: Flagsmith/flagsmith

Length of output: 8780


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Submit handler around first form:\n'
sed -n '340,430p' frontend/web/components/pages/HomePage.tsx

printf '\nSubmit handler around second form:\n'
sed -n '490,550p' frontend/web/components/pages/HomePage.tsx

Repository: Flagsmith/flagsmith

Length of output: 7473


Wait for the experiment flag before treating signup as ready. useSignupExperiment() returns undefined on the first render, and HomePage only enforces the restriction in render state. A fast submit can reach register() before flagsmith.identify() resolves, so the corporate-only gate and exposure never apply for that attempt. Add a ready/loading signal or re-check in the submit handler, and guard the async state update on unmount.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ignoring

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Zaimwa9, understood — I'll leave this as-is. Let me know if you'd like to revisit it later.

(๑•́ - •̀๑)

Comment on lines +184 to +189

const signupVariant = useSignupExperiment(
!isSignup || isInvite || !!preventEmailPassword || !!AccountStore.getUser(),
)
const blockGenericEmailDomain =
signupVariant === 'signup_corporate_only' && isFreeEmailDomain(email)

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated magic string 'signup_corporate_only' across files.

The variant name is hardcoded here and again in useSignupExperiment.ts (getExperimentFlag('signup_corporate_only')). Extracting a shared constant would prevent silent drift if the flag/variant name ever changes.

♻️ Suggested shared constant
// common/useSignupExperiment.ts
export const SIGNUP_CORPORATE_ONLY_VARIANT = 'signup_corporate_only'
-  const blockGenericEmailDomain =
-    signupVariant === 'signup_corporate_only' && isFreeEmailDomain(email)
+  const blockGenericEmailDomain =
+    signupVariant === SIGNUP_CORPORATE_ONLY_VARIANT && isFreeEmailDomain(email)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ignoring

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Zaimwa9, understood — I'll leave it as is.

(◡﹏◡✿)

Comment thread frontend/web/components/pages/HomePage.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request front-end Issue related to the React Front End Dashboard

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants