From 3fa1b12b0ff99d148dde91fa9919c8c1ab8abd9b Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Wed, 17 Jul 2024 11:37:27 +0200 Subject: [PATCH] feat: Add metrics flow context provider --- .env | 1 + sentry.client.config.ts | 3 +- .../(redesign)/(public)/FreeScanCta.tsx | 12 +-- .../(redesign)/(public)/PlansTable.tsx | 7 +- .../(redesign)/(public)/SignUpForm.tsx | 14 ++-- .../(redesign)/(public)/page.tsx | 38 ++++++--- src/app/api/v1/accounts-metrics-flow/route.ts | 43 +++++++++++ .../universal/getFreeScanSearchParams.ts | 54 +++++++++++++ src/app/layout.tsx | 1 - src/constants.ts | 2 + .../accounts-metrics-flow.tsx | 77 +++++++++++++++++++ 11 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 src/app/api/v1/accounts-metrics-flow/route.ts create mode 100644 src/app/functions/universal/getFreeScanSearchParams.ts create mode 100644 src/contextProviders/accounts-metrics-flow.tsx diff --git a/.env b/.env index e7538055d..71007bb19 100755 --- a/.env +++ b/.env @@ -37,6 +37,7 @@ FXA_SETTINGS_URL=https://accounts.stage.mozaws.net/settings OAUTH_CLIENT_ID=edd29a80019d61a1 OAUTH_CLIENT_SECRET=get-this-from-groovecoder-or-fxmonitor-engineering OAUTH_AUTHORIZATION_URI=https://oauth.stage.mozaws.net/v1/authorization +OAUTH_METRICS_FLOW_URI=https://accounts.stage.mozaws.net/metrics-flow OAUTH_PROFILE_URI=https://profile.stage.mozaws.net/v1/profile OAUTH_TOKEN_URI=https://oauth.stage.mozaws.net/v1/token OAUTH_ACCOUNT_URI = "https://oauth.accounts.firefox.com/v1" diff --git a/sentry.client.config.ts b/sentry.client.config.ts index a510c5866..d65e6eff7 100644 --- a/sentry.client.config.ts +++ b/sentry.client.config.ts @@ -16,7 +16,8 @@ Sentry.init({ tracesSampleRate: ["local"].includes(getEnvironment()) ? 1.0 : 0.1, // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: getEnvironment() !== "production", + // debug: getEnvironment() !== "production", + debug: false, replaysOnErrorSampleRate: 1.0, diff --git a/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx b/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx index 6cea66f76..3b58db713 100644 --- a/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx @@ -12,8 +12,8 @@ import { modifyAttributionsForUrlSearchParams } from "../../../functions/univers import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; import { useL10n } from "../../../hooks/l10n"; import { WaitlistCta } from "./ScanLimit"; -import { PublicEnvContext } from "../../../../contextProviders/public-env"; import { useContext } from "react"; +import { AccountsMetricsFlowContext } from "../../../../contextProviders/accounts-metrics-flow"; export type Props = { eligibleForPremium: boolean; @@ -30,13 +30,11 @@ export type Props = { export function getAttributionSearchParams({ cookies, emailInput, - publicEnv, experimentData, }: { cookies: { attributionsFirstTouch?: string; }; - publicEnv: Record; emailInput?: string; experimentData?: ExperimentData; }) { @@ -45,7 +43,7 @@ export function getAttributionSearchParams({ { entrypoint: "monitor.mozilla.org-monitor-product-page", form_type: typeof emailInput === "string" ? "email" : "button", - service: publicEnv.OAUTH_CLIENT_ID, + service: process.env.OAUTH_CLIENT_ID as string, ...(emailInput && { email: emailInput }), ...(experimentData && experimentData["landing-page-free-scan-cta"].enabled && { @@ -71,7 +69,8 @@ export const FreeScanCta = ( ) => { const l10n = useL10n(); const [cookies] = useCookies(["attributionsFirstTouch"]); - const publicEnv = useContext(PublicEnvContext); + const accountsMetricsFlowContext = useContext(AccountsMetricsFlowContext); + if ( !props.experimentData["landing-page-free-scan-cta"].enabled || props.experimentData["landing-page-free-scan-cta"].variant === @@ -93,7 +92,9 @@ export const FreeScanCta = ( ) : (
+
{JSON.stringify(accountsMetricsFlowContext, null, 2)}
{ newSearchParam = modifyAttributionsForUrlSearchParams( newSearchParam, { - entrypoint: "monitor.mozilla.org-monitor-product-page", + entrypoint: CONST_URL_MONITOR_LANDING_PAGE_ID, form_type: "button", data_cta_position: "pricing", }, diff --git a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx index 04f71dde7..caf939bb5 100644 --- a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx @@ -4,7 +4,7 @@ "use client"; -import { FormEventHandler, useContext, useId, useState } from "react"; +import { FormEventHandler, useId, useState } from "react"; import { signIn } from "next-auth/react"; import { useL10n } from "../../../hooks/l10n"; import { Button } from "../../../components/client/Button"; @@ -13,9 +13,9 @@ import { useTelemetry } from "../../../hooks/useTelemetry"; import { VisuallyHidden } from "../../../components/server/VisuallyHidden"; import { WaitlistCta } from "./ScanLimit"; import { useCookies } from "react-cookie"; -import { getAttributionSearchParams } from "./FreeScanCta"; import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; -import { PublicEnvContext } from "../../../../contextProviders/public-env"; +import { CONST_URL_MONITOR_LANDING_PAGE_ID } from "../../../../constants"; +import { getFreeScanSearchParams } from "../../../functions/universal/getFreeScanSearchParams"; export type Props = { eligibleForPremium: boolean; @@ -36,18 +36,16 @@ export const SignUpForm = (props: Props) => { const [emailInput, setEmailInput] = useState(""); const record = useTelemetry(); const [cookies] = useCookies(["attributionsFirstTouch"]); - const publicEnv = useContext(PublicEnvContext); const onSubmit: FormEventHandler = (event) => { event.preventDefault(); void signIn( "fxa", { callbackUrl: props.signUpCallbackUrl }, - getAttributionSearchParams({ + getFreeScanSearchParams({ cookies, - emailInput, - experimentData: props.experimentData, - publicEnv, + emailInput: "", + entrypoint: CONST_URL_MONITOR_LANDING_PAGE_ID, }), ); }; diff --git a/src/app/(proper_react)/(redesign)/(public)/page.tsx b/src/app/(proper_react)/(redesign)/(public)/page.tsx index 64c7bc354..3439d066f 100644 --- a/src/app/(proper_react)/(redesign)/(public)/page.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/page.tsx @@ -14,10 +14,14 @@ import { import { getEnabledFeatureFlags } from "../../../../db/tables/featureFlags"; import { getL10n } from "../../../functions/l10n/serverComponents"; import { View } from "./LandingView"; -import { CONST_DAY_MILLISECONDS } from "../../../../constants"; +import { + CONST_DAY_MILLISECONDS, + CONST_URL_MONITOR_LANDING_PAGE_ID, +} from "../../../../constants"; import { getExperimentationId } from "../../../functions/server/getExperimentationId"; import { getExperiments } from "../../../functions/server/getExperiments"; import { getLocale } from "../../../functions/universal/getLocale"; +import { AccountsMetricsFlowProvider } from "../../../../contextProviders/accounts-metrics-flow"; type Props = { searchParams: { @@ -51,13 +55,29 @@ export default async function Page({ searchParams }: Props) { typeof oneRepActivations === "undefined" || oneRepActivations > monthlySubscribersQuota; return ( - + + + ); } diff --git a/src/app/api/v1/accounts-metrics-flow/route.ts b/src/app/api/v1/accounts-metrics-flow/route.ts new file mode 100644 index 000000000..839797adb --- /dev/null +++ b/src/app/api/v1/accounts-metrics-flow/route.ts @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { MetricFlowData } from "../../../functions/universal/getFreeScanSearchParams"; + +async function fetchMetricsFlowParams(searchParams: URLSearchParams) { + const endpoint = new URL(process.env.OAUTH_METRICS_FLOW_URI as string); + for (const [key, value] of Array.from(searchParams)) { + if (value) { + endpoint.searchParams.set(key, value); + } + } + + const response = await fetch(endpoint.href, { + ...(process.env.APP_ENV !== "local" && { + headers: { + origin: process.env.SERVER_URL as string, + }, + }), + }); + + if (!response.ok) { + throw new Error( + `Fetching metrics flow parameters failed with ${response.status}: ${response.statusText}`, + ); + } + + const data: MetricFlowData = await response.json(); + return data; +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const flowData = await fetchMetricsFlowParams(searchParams); + return NextResponse.json({ success: true, flowData }, { status: 200 }); + } catch (e) { + return NextResponse.json({ success: false }, { status: 500 }); + } +} diff --git a/src/app/functions/universal/getFreeScanSearchParams.ts b/src/app/functions/universal/getFreeScanSearchParams.ts new file mode 100644 index 000000000..ee7daa9fb --- /dev/null +++ b/src/app/functions/universal/getFreeScanSearchParams.ts @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { modifyAttributionsForUrlSearchParams } from "../universal/attributions"; + +export type SearchParamArgs = { + cookies: { + attributionsFirstTouch?: string; + }; + emailInput?: string; + metricsFlowData?: MetricFlowData; +}; + +export type MetricFlowParams = { + entrypointExperiment: string; + entrypointVariation: string; + entrypoint: string; +}; + +export type MetricFlowData = { + deviceId: string; + flowId: string; + flowBeginTime: number; +}; + +export function getFreeScanSearchParams({ + cookies, + emailInput, + entrypoint, + metricsFlowData, +}: SearchParamArgs & + Omit) { + const attributionSearchParams = modifyAttributionsForUrlSearchParams( + new URLSearchParams(cookies.attributionsFirstTouch), + { + entrypoint, + form_type: typeof emailInput === "string" ? "email" : "button", + ...(emailInput && { email: emailInput }), + ...(metricsFlowData && { + device_id: metricsFlowData.deviceId, + flow_id: metricsFlowData.flowId, + flow_begin_time: metricsFlowData.flowBeginTime.toString(), + }), + }, + { + utm_source: "product", + utm_medium: "monitor", + utm_campaign: "get_free_scan", + }, + ); + + return attributionSearchParams.toString(); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cbdfbdbe9..b62e46b6d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,7 +19,6 @@ import StripeScript from "./components/client/StripeScript"; // DO NOT ADD SECRETS: Env variables added here become public. const PUBLIC_ENVS = { - OAUTH_CLIENT_ID: process.env.OAUTH_CLIENT_ID ?? "", PUBLIC_APP_ENV: process.env.APP_ENV ?? "", } as const; diff --git a/src/constants.ts b/src/constants.ts index 029b347f9..df2da02d8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -31,3 +31,5 @@ export const CONST_URL_DATA_PRIVACY_PETITION_BANNER = export const CONST_URL_MONITOR_GITHUB = "https://github.com/mozilla/blurts-server"; export const CONST_DAY_MILLISECONDS = 24 * 60 * 60 * 1000; +export const CONST_URL_MONITOR_LANDING_PAGE_ID = + "monitor.mozilla.org-monitor-product-page"; diff --git a/src/contextProviders/accounts-metrics-flow.tsx b/src/contextProviders/accounts-metrics-flow.tsx new file mode 100644 index 000000000..5593a5cfb --- /dev/null +++ b/src/contextProviders/accounts-metrics-flow.tsx @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use client"; + +import { ReactNode, createContext, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { MetricFlowData } from "../app/functions/universal/getFreeScanSearchParams"; + +interface SessionProviderProps { + children: ReactNode; + enabled: boolean; + metricsFlowParams: { + entrypoint: string; + entrypoint_experiment: string; + entrypoint_variation: string; + form_type: string; + service: string; + }; +} + +type ContextValues = { + flowData: MetricFlowData | null; + loading: boolean; +}; + +export const AccountsMetricsFlowContext = createContext({ + flowData: null, + loading: false, +}); + +export const AccountsMetricsFlowProvider = ({ + children, + enabled, + metricsFlowParams, +}: SessionProviderProps) => { + const [flowData, setFlowData] = useState(null); + const [loading, setLoading] = useState(enabled); + const searchParams = useSearchParams(); + + useEffect(() => { + async function fetchMetricsFlowData() { + const updatedSearchParams = new URLSearchParams(searchParams.toString()); + for (const key in metricsFlowParams) { + const value = metricsFlowParams[key as keyof typeof metricsFlowParams]; + if (value) { + updatedSearchParams.set(key, value); + } + } + const queryString = + updatedSearchParams.size > 0 + ? `?${updatedSearchParams.toString()}` + : ""; + const response = await fetch( + `/api/v1/accounts-metrics-flow${queryString}`, + ); + const data: { + success: boolean; + flowData?: MetricFlowData; + } = await response.json(); + + setFlowData(data.flowData ?? null); + setLoading(false); + } + + if (enabled) { + void fetchMetricsFlowData(); + } + }, [enabled, metricsFlowParams, searchParams]); + + return ( + + {children} + + ); +};