feat: Add metrics flow context provider

This commit is contained in:
Florian Zia 2024-07-17 11:37:27 +02:00
Родитель de3c91fa01
Коммит 3fa1b12b0f
Не найден ключ, соответствующий данной подписи
11 изменённых файлов: 225 добавлений и 27 удалений

1
.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"

Просмотреть файл

@ -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,

Просмотреть файл

@ -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<string, string>;
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 = (
<WaitlistCta />
) : (
<div>
<pre>{JSON.stringify(accountsMetricsFlowContext, null, 2)}</pre>
<TelemetryButton
disabled={accountsMetricsFlowContext.loading}
variant="primary"
event={{
module: "ctaButton",
@ -109,7 +110,6 @@ export const FreeScanCta = (
getAttributionSearchParams({
cookies,
experimentData: props.experimentData,
publicEnv,
}),
);
}}

Просмотреть файл

@ -55,7 +55,10 @@ import {
import { getLocale } from "../../../functions/universal/getLocale";
import { signIn } from "next-auth/react";
import { useTelemetry } from "../../../hooks/useTelemetry";
import { CONST_ONEREP_DATA_BROKER_COUNT } from "../../../../constants";
import {
CONST_ONEREP_DATA_BROKER_COUNT,
CONST_URL_MONITOR_LANDING_PAGE_ID,
} from "../../../../constants";
import { useCookies } from "react-cookie";
import { modifyAttributionsForUrlSearchParams } from "../../../functions/universal/attributions";
import { TelemetryButton } from "../../../components/client/TelemetryButton";
@ -98,7 +101,7 @@ export const PlansTable = (props: Props & ScanLimitProp) => {
newSearchParam = modifyAttributionsForUrlSearchParams(
newSearchParam,
{
entrypoint: "monitor.mozilla.org-monitor-product-page",
entrypoint: CONST_URL_MONITOR_LANDING_PAGE_ID,
form_type: "button",
data_cta_position: "pricing",
},

Просмотреть файл

@ -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,
}),
);
};

Просмотреть файл

@ -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 (
<View
eligibleForPremium={eligibleForPremium}
l10n={getL10n()}
countryCode={countryCode}
scanLimitReached={scanLimitReached}
enabledFlags={enabledFlags}
experimentData={experimentData}
/>
<AccountsMetricsFlowProvider
enabled={experimentData["landing-page-free-scan-cta"].enabled}
metricsFlowParams={{
entrypoint: CONST_URL_MONITOR_LANDING_PAGE_ID,
entrypoint_experiment: "landing-page-free-scan-cta",
entrypoint_variation:
experimentData["landing-page-free-scan-cta"].variant,
form_type:
experimentData["landing-page-free-scan-cta"].variant ===
"ctaWithEmail"
? "email"
: "button",
service: process.env.OAUTH_CLIENT_ID as string,
}}
>
<View
eligibleForPremium={eligibleForPremium}
l10n={getL10n()}
countryCode={countryCode}
scanLimitReached={scanLimitReached}
enabledFlags={enabledFlags}
experimentData={experimentData}
/>
</AccountsMetricsFlowProvider>
);
}

Просмотреть файл

@ -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 });
}
}

Просмотреть файл

@ -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<MetricFlowParams, "entrypointExperiment" | "entrypointVariation">) {
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();
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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";

Просмотреть файл

@ -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<ContextValues>({
flowData: null,
loading: false,
});
export const AccountsMetricsFlowProvider = ({
children,
enabled,
metricsFlowParams,
}: SessionProviderProps) => {
const [flowData, setFlowData] = useState<ContextValues["flowData"]>(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 (
<AccountsMetricsFlowContext.Provider value={{ flowData, loading }}>
{children}
</AccountsMetricsFlowContext.Provider>
);
};