From 7a0234df722747a34e32024248813eeb01cdcf24 Mon Sep 17 00:00:00 2001 From: Justin Bradfield Date: Thu, 6 Aug 2026 12:23:04 -0500 Subject: [PATCH] console: remove onboarding survey, land new users on enable-region New organizations previously had to complete (or skip) an onboarding survey before reaching the region enablement screen. Remove the survey entirely and make enable-region the default destination of the environment-not-ready flow. Old onboarding-survey URLs redirect there. Also expand the welcome copy on the enable-region page to explain what a region is: every new region starts with a default 25cc quickstart cluster that accrues a baseline cost (linked to the billing docs) from enablement until dropped. Removes the survey component, its option lists, and the HubSpot survey submission plumbing. The post-enablement tutorial slides are unchanged. Adds EnvironmentNotReadyRoutes tests covering the enable-region page, the new copy, and the redirects. Updates the e2e specs to wait for the enable-region page (new data-testid) instead of the survey. Co-Authored-By: Claude Fable 5 --- console/e2e-tests/platform.spec.ts | 5 +- console/e2e-tests/util.ts | 4 +- console/src/analytics/hubspot.test.ts | 83 ----- console/src/analytics/hubspot.ts | 125 ------- .../src/analytics/onboardingSurveyOptions.ts | 97 ------ .../environment-not-ready/EnableRegion.tsx | 27 ++ .../EnvironmentNotReadyRoutes.test.tsx | 94 ++++++ .../EnvironmentNotReadyRoutes.tsx | 10 +- .../OnboardingSurvey.tsx | 315 ------------------ 9 files changed, 126 insertions(+), 634 deletions(-) delete mode 100644 console/src/analytics/onboardingSurveyOptions.ts create mode 100644 console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.test.tsx delete mode 100644 console/src/platform/environment-not-ready/OnboardingSurvey.tsx diff --git a/console/e2e-tests/platform.spec.ts b/console/e2e-tests/platform.spec.ts index 46a704ef9ff22..2563abf9c1ec2 100644 --- a/console/e2e-tests/platform.spec.ts +++ b/console/e2e-tests/platform.spec.ts @@ -130,9 +130,8 @@ for (const region of REGIONS) { // await testAccountBlocking(page, context, region); // } - // Wait for the onboarding survey to load then skip it - await page.getByTestId("onboarding-survey").waitFor(); - await context.goto(`${CONSOLE_ADDR}/environment-not-ready/enable-region`); + // Wait for the enable region page to load + await page.getByTestId("enable-region").waitFor(); await retry( async () => { diff --git a/console/e2e-tests/util.ts b/console/e2e-tests/util.ts index c22bb042ba58e..d51131f43fbd5 100644 --- a/console/e2e-tests/util.ts +++ b/console/e2e-tests/util.ts @@ -168,8 +168,8 @@ export class TestContext { // Navigate to the home page && wait for that to load. await context.goto(CONSOLE_ADDR); - // We assume the first page is the onboarding survey - await page.waitForSelector("[data-testid=onboarding-survey]"); + // We assume the first page is the enable region page + await page.waitForSelector("[data-testid=enable-region]"); return context; } diff --git a/console/src/analytics/hubspot.test.ts b/console/src/analytics/hubspot.test.ts index ab5367bad1eed..79e3d35e6c4c9 100644 --- a/console/src/analytics/hubspot.test.ts +++ b/console/src/analytics/hubspot.test.ts @@ -17,8 +17,6 @@ import { import { getStore } from "~/jotai"; import { - ONBOARDING_SURVEY_API_ENDPOINT, - submitOnboardingSurvey, TRACK_SIGNUP_API_ENDPOINT, trackSignupInHubspot, trackSignUpInHubspotActions, @@ -35,22 +33,6 @@ const trackSignupErrorHandler = http.post(TRACK_SIGNUP_API_ENDPOINT, () => { return HttpResponse.error(); }); -const onboardingSurveySuccessHandlerSpy = vi.fn(); - -const onboardingSurveySuccessHandler = http.post( - ONBOARDING_SURVEY_API_ENDPOINT, - () => { - onboardingSurveySuccessHandlerSpy(); - return HttpResponse.json({ ok: true }); - }, -); -const onboardingSurveyErrorHandler = http.post( - ONBOARDING_SURVEY_API_ENDPOINT, - () => { - return HttpResponse.error(); - }, -); - describe("trackSignupInHubspot", () => { beforeEach(() => { vi.resetAllMocks(); @@ -117,71 +99,6 @@ describe("trackSignupInHubspot", () => { }); }); -describe("submitOnboardingSurvey", () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it("successfully calls the hubspot api", async () => { - server.use(onboardingSurveySuccessHandler); - await submitOnboardingSurvey({ - email: "someuser@example.com", - organizationId: "9e8b1bc8-fa08-4870-a21f-22ac01cc3808", - userId: "some-user-id", - roleDescription: "Other", - materializeUseCase: "Other", - projectDescription: "Other", - primaryDataSource: "Other", - }); - expect(onboardingSurveySuccessHandlerSpy).toHaveBeenCalledOnce(); - }); - - it("retries if the the call fails initially", async () => { - server.use( - http.post( - TRACK_SIGNUP_API_ENDPOINT, - () => { - return HttpResponse.error(); - }, - { once: true }, - ), - onboardingSurveySuccessHandler, - ); - await submitOnboardingSurvey( - { - email: "someuser@example.com", - organizationId: "9e8b1bc8-fa08-4870-a21f-22ac01cc3808", - userId: "some-user-id", - roleDescription: "Other", - materializeUseCase: "Other", - projectDescription: "Other", - primaryDataSource: "Other", - }, - { retryDelay: 10 }, - ); - expect(onboardingSurveySuccessHandlerSpy).toHaveBeenCalledOnce(); - }); - - it("fails after 3 tries", async () => { - server.use(onboardingSurveyErrorHandler); - await expect(() => - submitOnboardingSurvey( - { - email: "someuser@example.com", - organizationId: "9e8b1bc8-fa08-4870-a21f-22ac01cc3808", - userId: "some-user-id", - roleDescription: "Other", - materializeUseCase: "Other", - projectDescription: "Other", - primaryDataSource: "Other", - }, - { retryDelay: 10 }, - ), - ).rejects.toThrowError(); - expect(onboardingSurveySuccessHandlerSpy).not.toHaveBeenCalled(); - }); -}); - describe("trackSignUpInHubspotActions", () => { it("should reset state on userVerified when in signedUp state", async () => { const store = getStore(); diff --git a/console/src/analytics/hubspot.ts b/console/src/analytics/hubspot.ts index 597677e4e970c..99f1e0350df31 100644 --- a/console/src/analytics/hubspot.ts +++ b/console/src/analytics/hubspot.ts @@ -17,14 +17,7 @@ import { } from "~/external-library-wrappers/frontegg"; import { getStore } from "~/jotai"; import { getQueryClient } from "~/queryClient"; -import { notNullOrUndefined } from "~/util"; -import { - MaterializeUseCase, - PrimaryDataSource, - ProjectDescription, - RoleDescription, -} from "./onboardingSurveyOptions"; import { segment } from "./segment"; async function getAnonymousId() { @@ -60,11 +53,6 @@ export const TRACK_SIGNUP_API_ENDPOINT = buildFormSubmissionUrl({ formGuid: "e1f0065e-c5ec-4004-afb7-d4aeab96c6b2", }); -export const ONBOARDING_SURVEY_API_ENDPOINT = buildFormSubmissionUrl({ - portalId: "23399445", - formGuid: "7038d05b-0ad2-4c85-8ef7-36a163c6ddaf", -}); - type HubspotPayload = { fields: { name: string; value: string }[]; context: { pageUri: string; hutk?: string }; @@ -76,10 +64,6 @@ export const hubspotQueryKeys = { ...hubspotQueryKeys.all(), buildGlobalQueryKey("track-signup"), ], - onboardingSurvey: () => [ - ...hubspotQueryKeys.all(), - buildGlobalQueryKey("onboarding-survey"), - ], }; export async function trackSignupInHubspot( @@ -204,112 +188,3 @@ export const trackSignUpInHubspotActions = { } }, }; - -type OnboardingSurveyPayload = { - email: string; - organizationId: string; - userId: string; - roleDescription: RoleDescription; - roleDescriptionDetails?: string; - materializeUseCase: MaterializeUseCase; - materializeUseCaseDetails?: string; - projectDescription: ProjectDescription; - projectDescriptionDetails?: string; - primaryDataSource: PrimaryDataSource; - primaryDataSourceDetails?: string; -}; - -export async function submitOnboardingSurvey( - payload: OnboardingSurveyPayload, - options?: { retryDelay?: number }, -) { - const data: HubspotPayload = { - fields: [ - { name: "email", value: payload.email }, - { name: "organization_id", value: payload.organizationId }, - { name: "mz_user_id", value: payload.userId }, - - { - name: "how_would_you_describe_your_role_", - value: payload.roleDescription, - }, - - { - name: "why_are_you_trying_materialize_", - value: payload.materializeUseCase, - }, - - { - name: "what_are_you_trying_to_solve_with_materialize_", - value: payload.projectDescription, - }, - - { - name: "what_is_the_primary_or_origin_data_source_", - value: payload.primaryDataSource, - }, - ], - context: { - pageUri: window.location.href, - }, - }; - - [ - { - name: "how_would_you_describe_your_role__details", - value: payload.roleDescriptionDetails, - }, - { - name: "why_are_you_trying_materialize__details", - value: payload.materializeUseCaseDetails, - }, - { - name: "what_are_you_trying_to_solve_with_materialize__details", - value: payload.projectDescriptionDetails, - }, - { - name: "what_is_the_primary_or_origin_data_source__details", - value: payload.primaryDataSourceDetails, - }, - ].forEach(({ name, value }) => { - if (notNullOrUndefined(value)) { - data.fields.push({ - name: name, - value: value, - }); - } - }); - - const hutk = getHubspotUtk(); - if (hutk) { - data.context = { - ...data.context, - hutk, - }; - } - - return getQueryClient() - .getMutationCache() - .build(getQueryClient(), { - mutationKey: hubspotQueryKeys.onboardingSurvey(), - mutationFn: (params: HubspotPayload) => { - return fetch(ONBOARDING_SURVEY_API_ENDPOINT, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }); - }, - retry: 3, - retryDelay: options?.retryDelay, - onError: (error) => { - Sentry.captureException( - new Error("Hubspot onboarding survey submit failed", { - cause: error, - }), - ); - }, - }) - .execute(data); -} diff --git a/console/src/analytics/onboardingSurveyOptions.ts b/console/src/analytics/onboardingSurveyOptions.ts deleted file mode 100644 index d22778cbc6239..0000000000000 --- a/console/src/analytics/onboardingSurveyOptions.ts +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -// Form values for the onboarding survey that's submitted via Hubspot - -export const OTHER_OPTION = { label: "Other", value: "Other" } as const; - -export const ROLE_DESCRIPTION_OPTIONS = [ - { label: "Data Engineer", value: "Data Engineer" }, - { label: "Data Analyst", value: "Data Analyst" }, - { label: "ML Engineer", value: "ML Engineer" }, - { label: "Software Engineer", value: "Software Engineer" }, - { label: "Data Architect", value: "Data Architect" }, - { label: "Database Administrator", value: "Database Administrator" }, - { label: "Technical Executive", value: "Technical Executive" }, - { label: "Student", value: "Student" }, - OTHER_OPTION, -] as const; - -export type RoleDescription = - (typeof ROLE_DESCRIPTION_OPTIONS)[number]["value"]; - -export const MATERIALIZE_USE_CASE_OPTIONS = [ - { - label: "My company is evaluating Materialize", - value: "My company is evaluating Materialize", - }, - { - label: "I’m evaluating Materialize for a client", - value: "I’m evaluating Materialize for a client", - }, - { - label: "I’m exploring new technologies", - value: "I’m exploring new technologies", - }, - OTHER_OPTION, -] as const; - -export type MaterializeUseCase = - (typeof MATERIALIZE_USE_CASE_OPTIONS)[number]["value"]; - -export const PROJECT_DESCRIPTION_OPTIONS = [ - { - label: "Improve the performance of complex reads", - value: "Improve the performance of complex reads", - }, - { - label: "Join and act on data from multiple sources", - value: "Join and act on data from multiple sources", - }, - { - label: "Share operational data across teams and services", - value: "Share operational data across teams and services", - }, - OTHER_OPTION, -] as const; - -export type ProjectDescription = - (typeof PROJECT_DESCRIPTION_OPTIONS)[number]["value"]; - -export const PRIMARY_DATA_SOURCE_OPTIONS = [ - { - label: - "PostgreSQL (Amazon RDS, Amazon Aurora, Azure DB, Google Cloud SQL, AlloyDB, Self-hosted PostgresSQL)", - value: "PostgreSQL", - }, - { - label: - "MySQL (Amazon RDS, Amazon Aurora, Azure DB, Google Cloud SQL, Self-hosted MySQL)", - value: "MySQL", - }, - { - label: - "Kafka (Apache Kafka, Confluent Cloud, Amazon MSK, WarpStream, Redpanda (Cloud))", - value: "Kafka", - }, - { - label: - "Cloud Data Warehouse (Amazon Redshift, Google BigQuery, Microsoft Azure, Snowflake)", - value: "Cloud Data Warehouse", - }, - { - label: "Object storage (AWS S3, GCP Cloud Storage, Azure Blob Storage)", - value: "Object storage / S3", - }, - { label: "Webhooks", value: "Webhooks" }, - OTHER_OPTION, -] as const; - -export type PrimaryDataSource = - (typeof PRIMARY_DATA_SOURCE_OPTIONS)[number]["value"]; diff --git a/console/src/platform/environment-not-ready/EnableRegion.tsx b/console/src/platform/environment-not-ready/EnableRegion.tsx index 67637d147c06c..77c47e7c4bf89 100644 --- a/console/src/platform/environment-not-ready/EnableRegion.tsx +++ b/console/src/platform/environment-not-ready/EnableRegion.tsx @@ -14,7 +14,9 @@ import { Navigate } from "react-router-dom"; import ErrorBox from "~/components/ErrorBox"; import { RegionSelectorForm } from "~/components/RegionSelectorForm"; +import TextLink from "~/components/TextLink"; import { User } from "~/external-library-wrappers/frontegg"; +import docUrls from "~/mz-doc-urls.json"; import CreateEnvironmentButton from "~/platform/environment-not-ready/CreateEnvironmentButton"; import useCreateEnvironment from "~/platform/environment-not-ready/useCreateEnvironment"; import { homePagePath } from "~/platform/routeHelpers"; @@ -60,6 +62,30 @@ const EnableRegionContent = ({ user }: { user: User }) => { Welcome to Materialize + + To get started, enable a region. A region is a dedicated Materialize + environment that runs in the cloud provider region you choose. + + + Every new region starts with a default 25cc quickstart + cluster for trying out Materialize. This cluster accrues a{" "} + + baseline cost + {" "} + from the moment the region is enabled, until you drop it. + { w="full" alignItems="center" justifyContent="center" + data-testid="enable-region" > diff --git a/console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.test.tsx b/console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.test.tsx new file mode 100644 index 0000000000000..02ca23bdfeea7 --- /dev/null +++ b/console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.test.tsx @@ -0,0 +1,94 @@ +// Copyright Materialize, Inc. and contributors. All rights reserved. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0. + +import { screen, waitFor } from "@testing-library/react"; +import React from "react"; + +import server from "~/api/mocks/server"; +import { dummyValidUser } from "~/external-library-wrappers/__mocks__/frontegg"; +import { + disabledEnvironment, + InitializeStateFn, + renderComponent, + RenderWithPathname, + setFakeEnvironment, +} from "~/test/utils"; + +import { EnvironmentNotReadyRoutes } from "./EnvironmentNotReadyRoutes"; + +const renderRoutes = ( + initialRouterEntries: string[], + initializeState: InitializeStateFn = ({ set }) => + setFakeEnvironment(set, "aws/us-east-1", disabledEnvironment), +) => { + return renderComponent( + + + , + { initializeState, initialRouterEntries }, + ); +}; + +describe("EnvironmentNotReadyRoutes", () => { + afterEach(() => { + server.resetHandlers(); + vi.clearAllMocks(); + }); + + it("shows the region selector on the enable-region route", async () => { + await renderRoutes(["/enable-region"]); + expect( + await screen.findByText( + "Where would you like to run your Materialize environment?", + ), + ).toBeVisible(); + }); + + it("explains what a region is and its baseline cost", async () => { + await renderRoutes(["/enable-region"]); + expect( + await screen.findByText( + "A region is a dedicated Materialize environment", + { exact: false }, + ), + ).toBeVisible(); + expect( + screen.getByText("Every new region starts with a default", { + exact: false, + }), + ).toBeVisible(); + expect(screen.getByRole("link", { name: "baseline cost" })).toHaveAttribute( + "href", + expect.stringContaining("billing"), + ); + }); + + it("redirects the root path directly to enable-region", async () => { + await renderRoutes(["/"]); + await waitFor(() => { + expect(screen.getByTestId("pathname")).toHaveTextContent( + "/enable-region", + ); + }); + expect( + await screen.findByText( + "Where would you like to run your Materialize environment?", + ), + ).toBeVisible(); + }); + + it("redirects the removed onboarding-survey route to enable-region", async () => { + await renderRoutes(["/onboarding-survey"]); + await waitFor(() => { + expect(screen.getByTestId("pathname")).toHaveTextContent( + "/enable-region", + ); + }); + }); +}); diff --git a/console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.tsx b/console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.tsx index 770b7a2f8516b..34c8214aec1c5 100644 --- a/console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.tsx +++ b/console/src/platform/environment-not-ready/EnvironmentNotReadyRoutes.tsx @@ -16,22 +16,14 @@ import { SentryRoutes } from "~/sentry"; import EnableRegion from "./EnableRegion"; import { EnvironmentNotReadyLayout } from "./Layout"; import { OnboardingSteps } from "./OnboardingSteps"; -import OnboardingSurvey from "./OnboardingSurvey"; export const EnvironmentNotReadyRoutes = ({ user }: { user: User }) => { return ( - } - /> } /> } /> - } - /> + } /> ); diff --git a/console/src/platform/environment-not-ready/OnboardingSurvey.tsx b/console/src/platform/environment-not-ready/OnboardingSurvey.tsx deleted file mode 100644 index d2809b54f4960..0000000000000 --- a/console/src/platform/environment-not-ready/OnboardingSurvey.tsx +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright Materialize, Inc. and contributors. All rights reserved. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0. - -import { - Button, - chakra, - FormControl, - FormErrorMessage, - FormLabel, - HStack, - Text, - Textarea, - Tooltip, - VStack, -} from "@chakra-ui/react"; -import React from "react"; -import { useForm } from "react-hook-form"; -import { Navigate, useNavigate } from "react-router-dom"; - -import { submitOnboardingSurvey } from "~/analytics/hubspot"; -import { - MATERIALIZE_USE_CASE_OPTIONS, - MaterializeUseCase, - OTHER_OPTION, - PRIMARY_DATA_SOURCE_OPTIONS, - PrimaryDataSource, - PROJECT_DESCRIPTION_OPTIONS, - ProjectDescription, - ROLE_DESCRIPTION_OPTIONS, - RoleDescription, -} from "~/analytics/onboardingSurveyOptions"; -import SimpleSelect from "~/components/SimpleSelect"; -import { AppConfigSwitch } from "~/config/AppConfigSwitch"; -import { User } from "~/external-library-wrappers/frontegg"; -import { useEnvironmentsWithHealth } from "~/store/environments"; - -import ContactSalesCta from "./ContactSalesCta"; -import { NAVBAR_HEIGHT } from "./Layout"; - -const PLACEHOLDER_TEXT = "Select..."; - -type FormState = { - roleDescription: RoleDescription; - roleDescriptionDetails?: string; - materializeUseCase: MaterializeUseCase; - materializeUseCaseDetails?: string; - projectDescription: ProjectDescription; - projectDescriptionDetails?: string; - primaryDataSource: PrimaryDataSource; - primaryDataSourceDetails?: string; -}; - -const SkipSurveyButton = ({ - onClick, -}: { - onClick: (e: React.MouseEvent) => void; -}) => ( - -); - -const OnboardingSurvey = ({ user }: { user: User }) => { - const environments = useEnvironmentsWithHealth(); - const navigate = useNavigate(); - - const { register, handleSubmit, formState, watch } = useForm({ - mode: "onTouched", - }); - - const [ - roleDescription, - materializeUseCase, - projectDescription, - primaryDataSource, - ] = watch([ - "roleDescription", - "materializeUseCase", - "projectDescription", - "primaryDataSource", - ]); - - const { errors } = formState; - - const someEnvironmentNotDisabled = Array.from(environments.values()).some( - (env) => env.state !== "disabled", - ); - - if (someEnvironmentNotDisabled) { - return ; - } - - const onSubmit = handleSubmit(async (data) => { - try { - await submitOnboardingSurvey({ - ...data, - email: user.email, - organizationId: user.tenantId, - userId: user.id, - }); - } catch { - // Even if this survey fails to submit, we don't want to block - // onboarding - console.error("Failed to submit onboarding survey"); - } - navigate("../enable-region"); - }); - - return ( - - - - - - Your information - - - We'd love to help you get started on the right foot with - Materialize! - - - - What is your role? - - {ROLE_DESCRIPTION_OPTIONS.map((role) => ( - - ))} - - {errors.roleDescription && ( - - {errors.roleDescription.message} - - )} - - {roleDescription === OTHER_OPTION.value && ( - - Please describe your role -