diff --git a/kea.js b/kea.js index 3061d40b3..f17bc7bc8 100644 --- a/kea.js +++ b/kea.js @@ -3,10 +3,11 @@ import { Provider } from 'react-redux' import { getContext, resetContext } from 'kea' import { loadersPlugin } from 'kea-loaders' import { routerPlugin } from 'kea-router' +import { localStoragePlugin } from 'kea-localstorage' export function initKea(isServer = false, location = '') { resetContext({ - plugins: [loadersPlugin, routerPlugin(isServer ? { location } : {})], + plugins: [loadersPlugin, routerPlugin(isServer ? { location } : {}), localStoragePlugin], }) } diff --git a/package.json b/package.json index 04ea1d63e..d97fa3dee 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "katex": "^0.12.0", "kea": "^2.2.2", "kea-loaders": "^0.3.0", + "kea-localstorage": "^1.1.1", "kea-router": "^0.5.1", "kea-typegen": "^0.5.3", "node-fetch": "^2.6.1", diff --git a/src/components/DeploymentOptionsGrid/DeploymentOptionsGrid.tsx b/src/components/DeploymentOptionsGrid/DeploymentOptionsGrid.tsx index 81dbacddd..a28d7ecfc 100644 --- a/src/components/DeploymentOptionsGrid/DeploymentOptionsGrid.tsx +++ b/src/components/DeploymentOptionsGrid/DeploymentOptionsGrid.tsx @@ -68,9 +68,7 @@ export const DeploymentOptionsGrid = (): JSX.Element => { 'No third-party cookies', ]} /> - reportDeploymentTypeSelected(Realm.hosted, '/docs/self-host/overview')} - > + reportDeploymentTypeSelected(Realm.hosted, '/docs/self-host')}> Select diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index a6c9d1c49..f2fff26b5 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -44,7 +44,7 @@ const PrimaryCta = ({ children, className = '' }: { children: any; className?: s
  • - + {activeVariant !== FEATURE_FLAGS.EMAIL_GATED_SIGNUP_NOT_SKIPPABLE && ( + + )} diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 000000000..977968a63 --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,8 @@ +export const FEATURE_FLAGS = { + EMAIL_GATED_SIGNUP_CONTROL: 'email-gated-signup-control', + EMAIL_GATED_SIGNUP_OLD_FLOW: 'email-gated-signup-old-flow', + EMAIL_GATED_SIGNUP_NOT_SKIPPABLE: 'email-gated-signup-not-skippable', + EMAIL_GATED_SIGNUP_SKIPPABLE: 'email-gated-signup-skippable', +} + +export const EMAIL_GATED_SIGNUP_PREFIX = 'email-gated-signup' diff --git a/src/logic/posthogAnalyticsLogic.js b/src/logic/posthogAnalyticsLogic.js index f772e4b6f..a34fa8f74 100644 --- a/src/logic/posthogAnalyticsLogic.js +++ b/src/logic/posthogAnalyticsLogic.js @@ -1,4 +1,5 @@ import { kea } from 'kea' +import { getCookie } from 'lib/utils' export const posthogAnalyticsLogic = kea({ actions: { @@ -57,4 +58,26 @@ export const posthogAnalyticsLogic = kea({ } }, }), + + selectors: { + activeFeatureFlags: [ + (s) => [s.featureFlags], + (featureFlags) => + Object.entries(featureFlags) + .filter(([, value]) => !!value) + .map(([key]) => key), + ], + isLoggedIn: [ + (s) => [s.posthog], + (posthog) => { + const token = posthog?.config?.token + if (token) { + const rawCookie = getCookie(`ph_${token}_posthog`) + if (!rawCookie) return false + const cookie = JSON.parse(rawCookie) + return !!cookie['$user_id'] + } + }, + ], + }, }) diff --git a/src/logic/signupLogic.ts b/src/logic/signupLogic.ts index 7efed1408..7ad45066b 100644 --- a/src/logic/signupLogic.ts +++ b/src/logic/signupLogic.ts @@ -1,4 +1,5 @@ import { kea } from 'kea' +import { EMAIL_GATED_SIGNUP_PREFIX, FEATURE_FLAGS } from 'lib/constants' import { isValidEmailAddress } from 'lib/utils' import { posthogAnalyticsLogic } from './posthogAnalyticsLogic' @@ -12,18 +13,8 @@ export enum Realm { cloud = 'cloud', } -type HubSpotContact = { - id: string // Hubspot contact ID (number as string) - properties: Record - created_at: string // ISO 8601 - updated_at: string // ISO 8601 - archived: boolean - archived_at: string | null // ISO 8601 -} - export type HubSpotContactResponse = { success: boolean - result: string | HubSpotContact } async function createContact(email: string) { @@ -54,6 +45,7 @@ async function updateContact(email: string, properties: Record) } export const signupLogic = kea({ + path: typeof window === undefined ? undefined : () => ['signup'], actions: { setModalView: (view: SignupModalView) => ({ view }), setEmail: (email: string) => ({ email }), @@ -61,7 +53,14 @@ export const signupLogic = kea({ skipEmailEntry: true, reportModalShown: true, reportDeploymentOptionsShown: true, - reportDeploymentTypeSelected: (deploymentType: Realm, nextHref?: string) => ({ deploymentType, nextHref }), + onRenderSignupPage: true, + reportDeploymentTypeSelected: (deploymentType: Realm, nextHref?: string) => ({ + deploymentType, + nextHref, + }), + setVariants: (newVariants: string[]) => ({ newVariants }), + setActiveVariant: (activeVariant: string) => ({ activeVariant }), + updateAvailableVariants: true, }, reducers: { modalView: [ @@ -76,21 +75,56 @@ export const signupLogic = kea({ setEmail: (_, { email }: { email: string }) => email, }, ], + experimentVariants: [ + {} as Record, + { persist: true }, + { + setVariants: (state: Record, { newVariants }: { newVariants: string[] }) => { + const nextState = {} as Record + const existingVariants = Object.keys(state) + newVariants.forEach((variant) => { + if (existingVariants.includes(variant)) { + // Don't overwrite an existing value + nextState[variant] = state[variant] + } else { + nextState[variant] = false + } + }) + return nextState + }, + setActiveVariant: (state: Record, { activeVariant }: { activeVariant: string }) => { + const nextState = {} as Record + const existingVariants = Object.keys(state) + existingVariants.forEach((variant) => { + nextState[variant] = variant === activeVariant + }) + return nextState + }, + }, + ], }, connect: { - values: [posthogAnalyticsLogic, ['posthog']], + values: [posthogAnalyticsLogic, ['posthog', 'activeFeatureFlags', 'isLoggedIn']], + actions: [posthogAnalyticsLogic, ['setFeatureFlags']], }, listeners: ({ actions, values }) => ({ submitForm: async () => { - const { posthog, email } = values + const { posthog, email, activeVariant } = values + const skipDeploymentOptions = activeVariant === FEATURE_FLAGS.EMAIL_GATED_SIGNUP_OLD_FLOW if (email && isValidEmailAddress(email)) { try { posthog.identify(email, { email: email.toLowerCase() }) // use email as distinct ID; also set it as property posthog.capture('signup: submit email') - actions.setModalView(SignupModalView.DEPLOYMENT_OPTIONS) + if (!skipDeploymentOptions) { + actions.setModalView(SignupModalView.DEPLOYMENT_OPTIONS) + } await createContact(email) } catch (err) { posthog.capture('signup: failed to create contact', { message: err }) + } finally { + if (skipDeploymentOptions) { + window.location.replace(`https://app.posthog.com/signup?email=${encodeURIComponent(email)}`) + } } } }, @@ -119,7 +153,9 @@ export const signupLogic = kea({ reportDeploymentTypeSelected: async ({ deploymentType, nextHref }) => { const { posthog, email } = values try { - posthog.capture('signup: deployment type selected', { selected_deployment_type: deploymentType }) + posthog.capture('signup: deployment type selected', { + selected_deployment_type: deploymentType, + }) await updateContact(email, { selected_deployment_type: deploymentType }) } catch (err) { posthog.capture('signup: failed to update contact', { message: err }) @@ -128,5 +164,68 @@ export const signupLogic = kea({ window.location.replace(nextHref) } }, + onRenderSignupPage: () => { + if (typeof window !== 'undefined') { + if (values.shouldAutoRedirect) { + window.location.replace(`https://app.posthog.com/signup${window.location.search || ''}`) + } else { + actions.reportModalShown() + } + } + }, + setVariants: () => { + const { experimentVariants, posthog } = values + const variantEntries: [string, boolean][] = Object.entries(experimentVariants) + if (variantEntries.length && !variantEntries.some(([, status]) => status === true)) { + // If all available variants are inactive, we need to randomly pick one to activate. + const randomIndex = Math.floor(Math.random() * variantEntries.length) + const [name] = variantEntries[randomIndex] + actions.setActiveVariant(name) + posthog.capture('set email experiment variant', { + variant: name, + $set_once: { + email_experiment_variant: name, + }, + }) + } + }, + updateAvailableVariants: () => { + const { activeFeatureFlags } = values + const variantFlags: string[] = activeFeatureFlags.filter((flag: string) => + flag.includes(EMAIL_GATED_SIGNUP_PREFIX) + ) + actions.setVariants(variantFlags) + }, + [actions.setFeatureFlags]: () => { + actions.updateAvailableVariants() + }, }), + events: ({ actions, values }) => ({ + afterMount: () => { + if (typeof window !== undefined && !!values.posthog) { + actions.updateAvailableVariants() + } + }, + }), + selectors: { + hasEmailGatedSignup: [ + (s) => [s.activeVariant], + (activeVariant: string | null) => + activeVariant && activeVariant !== FEATURE_FLAGS.EMAIL_GATED_SIGNUP_CONTROL, + ], + shouldAutoRedirect: [ + (s) => [s.hasEmailGatedSignup, s.isLoggedIn], + (hasEmailGatedSignup: boolean, isLoggedIn: boolean) => { + return !hasEmailGatedSignup || isLoggedIn + }, + ], + activeVariant: [ + (s) => [s.experimentVariants], + (experimentVariants: Record) => { + const variantEntries: [string, boolean][] = Object.entries(experimentVariants) + const [name] = variantEntries.find(([, value]) => value) || [] + return name || null + }, + ], + }, }) diff --git a/src/pages/index.js b/src/pages/index.js index 562778f2a..e1da187c1 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -9,12 +9,15 @@ import { Tutorials } from '../components/LandingPage/Tutorials' import { RecentBlogPosts } from '../components/LandingPage/RecentBlogPosts' import { Footer } from '../components/Footer/Footer' import { GetStartedModal } from 'components/GetStartedModal' +import { posthogAnalyticsLogic } from 'logic/posthogAnalyticsLogic' +import { useValues } from 'kea' import { SEO } from '../components/seo' import '../components/LandingPage/styles/index.scss' const IndexPage = () => { + useValues(posthogAnalyticsLogic) // mount this logic return (
    { - const { modalView } = useValues(signupLogic) - const { reportModalShown } = useActions(signupLogic) + const { modalView, hasEmailGatedSignup } = useValues(signupLogic) + const { onRenderSignupPage } = useActions(signupLogic) useEffect(() => { - reportModalShown() + onRenderSignupPage() }, []) return ( @@ -24,31 +23,35 @@ const SignUpPage = (): JSX.Element => { title="Sign Up • PostHog" description="Unlock insights. Our Open Source, Scale, and Cloud editions provide flexible deployment of reliable analytics." /> -
    -
    - {modalView === SignupModalView.EMAIL_PROMPT && ( -
    - PostHog Insights interface showing new Funnels features -
    - Unlock a deeper level of insights with Funnels 2.0{' '} - New -
    - - + {hasEmailGatedSignup && ( + <> +
    +
    + {modalView === SignupModalView.EMAIL_PROMPT && ( +
    + PostHog Insights interface showing new Funnels features +
    + Unlock a deeper level of insights with Funnels 2.0{' '} + New +
    + + +
    + )} + {modalView === SignupModalView.DEPLOYMENT_OPTIONS && ( +
    + + + +
    + )}
    - )} - {modalView === SignupModalView.DEPLOYMENT_OPTIONS && ( -
    - - - -
    - )} -
    + + )}
    ) } diff --git a/yarn.lock b/yarn.lock index 35a05ec5a..5f9613692 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3932,9 +3932,9 @@ caniuse-lite@^1.0.30001219: integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A== caniuse-lite@^1.0.30001243: - version "1.0.30001248" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz#26ab45e340f155ea5da2920dadb76a533cb8ebce" - integrity sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw== + version "1.0.30001249" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8" + integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw== caseless@~0.12.0: version "0.12.0" @@ -10435,6 +10435,11 @@ kea-loaders@^0.3.0: resolved "https://registry.yarnpkg.com/kea-loaders/-/kea-loaders-0.3.0.tgz" integrity sha512-aXrUjQf/GdJVDQqLtTWoY4gF1F+dTXv5U1LfCLffPhgrWRYHGTnaBV/K6pWxy1Qh+vMbepy80Nx9hFiK1owtHw== +kea-localstorage@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/kea-localstorage/-/kea-localstorage-1.1.1.tgz#6edf69476779e002708fb10f2360e48333707406" + integrity sha512-YijSF33Y1QpfHAq1hGvulWWoGC9Kckd4lVa+XStHar4t7GfoaDa1aWnTeJxzz9bWnJovfk+MnG8ekP4rWIqINA== + kea-router@^0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/kea-router/-/kea-router-0.5.2.tgz"