Skip to content

Commit f34a5f8

Browse files
authored
Merge branch 'main' into lighthouse-report
2 parents 9ad4e73 + bc950fc commit f34a5f8

File tree

11 files changed

+446
-27
lines changed

11 files changed

+446
-27
lines changed

src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/[[...slug]]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export default async function DashboardPage({ params, searchParams }: Props) {
167167
totalNumberOfPerformedScans={profileStats?.total}
168168
isNewUser={isNewUser}
169169
elapsedTimeInDaysSinceInitialScan={elapsedTimeInDaysSinceInitialScan}
170-
experimentData={experimentData}
170+
experimentData={experimentData["Features"]}
171171
activeTab={activeTab}
172172
hasFirstMonitoringScan={hasFirstMonitoringScan}
173173
signInCount={signInCount}

src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/[[...slug]]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export default async function SettingsPage({ params, searchParams }: Props) {
125125
yearlySubscriptionUrl={`${yearlySubscriptionUrl}&${additionalSubplatParams.toString()}`}
126126
subscriptionBillingAmount={getSubscriptionBillingAmount()}
127127
enabledFeatureFlags={enabledFeatureFlags}
128-
experimentData={experimentData}
128+
experimentData={experimentData["Features"]}
129129
lastScanDate={lastOneRepScan?.created_at}
130130
isMonthlySubscriber={isMonthlySubscriber}
131131
activeTab={activeTab}

src/app/(proper_react)/(redesign)/(authenticated)/user/welcome/[[...slug]]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export default async function Onboarding({ params, searchParams }: Props) {
7777
breachesTotalCount={allBreachesCount}
7878
stepId={firstSlug === FreeScanSlug ? "enterInfo" : "getStarted"}
7979
previousRoute={previousRoute}
80-
experimentData={experimentData}
80+
experimentData={experimentData["Features"]}
8181
/>
8282
);
8383
}

src/app/(proper_react)/(redesign)/(public)/page.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,39 +59,41 @@ export default async function Page({ searchParams }: Props) {
5959
oneRepActivations > monthlySubscribersQuota;
6060
return (
6161
<AccountsMetricsFlowProvider
62-
enabled={experimentData["landing-page-free-scan-cta"].enabled}
62+
enabled={experimentData["Features"]["landing-page-free-scan-cta"].enabled}
6363
metricsFlowParams={{
6464
entrypoint: CONST_URL_MONITOR_LANDING_PAGE_ID,
6565
entrypoint_experiment: "landing-page-free-scan-cta",
6666
entrypoint_variation:
67-
experimentData["landing-page-free-scan-cta"].variant,
67+
experimentData["Features"]["landing-page-free-scan-cta"].variant,
6868
form_type:
69-
experimentData["landing-page-free-scan-cta"].variant ===
69+
experimentData["Features"]["landing-page-free-scan-cta"].variant ===
7070
"ctaWithEmail"
7171
? "email"
7272
: "button",
7373
service: process.env.OAUTH_CLIENT_ID as string,
7474
}}
7575
>
7676
{enabledFeatureFlags.includes("LandingPageRedesign") &&
77-
experimentData["landing-page-redesign-plus-eligible-experiment"]
78-
.enabled &&
79-
experimentData["landing-page-redesign-plus-eligible-experiment"]
80-
.variant === "redesign" ? (
77+
experimentData["Features"][
78+
"landing-page-redesign-plus-eligible-experiment"
79+
].enabled &&
80+
experimentData["Features"][
81+
"landing-page-redesign-plus-eligible-experiment"
82+
].variant === "redesign" ? (
8183
<LandingViewRedesign
8284
eligibleForPremium={eligibleForPremium}
8385
l10n={getL10n()}
8486
countryCode={countryCode}
8587
scanLimitReached={scanLimitReached}
86-
experimentData={experimentData}
88+
experimentData={experimentData["Features"]}
8789
/>
8890
) : (
8991
<LandingView
9092
eligibleForPremium={eligibleForPremium}
9193
l10n={getL10n()}
9294
countryCode={countryCode}
9395
scanLimitReached={scanLimitReached}
94-
experimentData={experimentData}
96+
experimentData={experimentData["Features"]}
9597
/>
9698
)}
9799
</AccountsMetricsFlowProvider>

src/app/api/v1/user/welcome-scan/create/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export async function POST(
9898
previewMode: searchParams.get("nimbus_preview") === "true",
9999
});
100100
const optionalInfoExperimentData =
101-
experimentData["welcome-scan-optional-info"];
101+
experimentData["Features"]["welcome-scan-optional-info"];
102102

103103
const profileData: CreateProfileRequest = {
104104
first_name: firstName,

src/app/functions/server/getExperiments.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import {
1212
import { ExperimentationId } from "./getExperimentationId";
1313
import { getEnabledFeatureFlags } from "../../../db/tables/featureFlags";
1414

15+
/**
16+
* After we removing the `CirrusV2` flag, we can return the full `ExperimentData`/
17+
* But until then, we can make the old experiment data object look like the new one,
18+
* but we can't backfill the `Enrollments` property.
19+
*/
20+
export type ExperimentData_V2_Or_V2LikeV1 = Partial<ExperimentData> &
21+
Required<Pick<ExperimentData, "Features">>;
22+
1523
/**
1624
* Call the Cirrus sidecar, which returns a list of eligible experiments for the current user.
1725
*
@@ -28,9 +36,9 @@ export async function getExperiments(params: {
2836
locale: string;
2937
countryCode: string;
3038
previewMode: boolean;
31-
}): Promise<ExperimentData["Features"]> {
39+
}): Promise<ExperimentData_V2_Or_V2LikeV1> {
3240
if (["local"].includes(process.env.APP_ENV ?? "local")) {
33-
return localExperimentData["Features"];
41+
return localExperimentData;
3442
}
3543

3644
if (!process.env.NIMBUS_SIDECAR_URL) {
@@ -78,19 +86,22 @@ export async function getExperiments(params: {
7886
throw new Error(`Cirrus request failed: ${response.statusText}`);
7987
}
8088

81-
const json = await response.json();
89+
// With the `CirrusV2` flag enabled, the response is `ExperimentData`,
90+
// otherwise, it's just `ExperimentData["Features"]`.
91+
const json = (await response.json()) as
92+
| ExperimentData
93+
| ExperimentData["Features"];
8294

83-
let experimentData;
95+
let experimentData: ExperimentData_V2_Or_V2LikeV1;
8496
if (flags.includes("CirrusV2")) {
85-
experimentData = json["Features"];
97+
experimentData = json as ExperimentData;
8698
} else {
87-
experimentData = json;
99+
experimentData = {
100+
Features: json as ExperimentData["Features"],
101+
};
88102
}
89103

90-
return (
91-
(experimentData as ExperimentData["Features"]) ??
92-
defaultExperimentData["Features"]
93-
);
104+
return (experimentData as ExperimentData) ?? defaultExperimentData;
94105
} catch (ex) {
95106
logger.error("Could not connect to Cirrus", {
96107
serverUrl,
@@ -99,6 +110,6 @@ export async function getExperiments(params: {
99110
params,
100111
});
101112
captureException(ex);
102-
return defaultExperimentData["Features"];
113+
return defaultExperimentData;
103114
}
104115
}

src/app/hooks/useGlean.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import EventMetricType from "@mozilla/glean/private/metrics/event";
99
import type { GleanMetricMap } from "../../telemetry/generated/_map";
1010
import { useSession } from "next-auth/react";
1111
import { hasPremium } from "../functions/universal/user";
12+
import { useExperiments } from "../../contextProviders/experiments";
1213

1314
export const useGlean = () => {
1415
const session = useSession();
16+
const experimentData = useExperiments();
17+
// Telemetry recording is mocked in our unit tests, therefore we
18+
// do not have test coverage for this method.
19+
/* c8 ignore start */
1520
const isPremiumUser = hasPremium(session.data?.user);
1621
const record = useCallback(
1722
async <
@@ -36,10 +41,34 @@ export const useGlean = () => {
3641
? "Plus"
3742
: "Free";
3843

44+
// Record the `nimbus_*` keys on all events.
45+
// `nimbus_*` is set on every metric, but it's too much work for TypeScript
46+
// to infer that — hence the type assertion.
47+
48+
if (
49+
experimentData &&
50+
typeof experimentData["Enrollments"] !== "undefined"
51+
) {
52+
(data as GleanMetricMap["button"]["click"]).nimbus_user_id =
53+
experimentData["Enrollments"]["nimbus_user_id"];
54+
(data as GleanMetricMap["button"]["click"]).nimbus_app_id =
55+
experimentData["Enrollments"]["app_id"];
56+
(data as GleanMetricMap["button"]["click"]).nimbus_experiment =
57+
experimentData["Enrollments"]["experiment"];
58+
(data as GleanMetricMap["button"]["click"]).nimbus_branch =
59+
experimentData["Enrollments"]["branch"];
60+
(data as GleanMetricMap["button"]["click"]).nimbus_experiment_type =
61+
experimentData["Enrollments"]["experiment_type"];
62+
(data as GleanMetricMap["button"]["click"]).nimbus_is_preview =
63+
experimentData["Enrollments"]["is_preview"].toString();
64+
} else {
65+
console.warn("No experiment data available for Glean");
66+
}
67+
3968
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4069
mod[event].record(data as any);
4170
},
42-
[isPremiumUser],
71+
[isPremiumUser, experimentData],
4372
);
4473
/* c8 ignore end */
4574

src/app/layout.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import { GoogleAnalyticsWorkaround } from "./components/client/GoogleAnalyticsWo
1717
import StripeScript from "./components/client/StripeScript";
1818
import { GleanScript } from "./components/client/GleanScript";
1919
import { getExperimentationId } from "./functions/server/getExperimentationId";
20+
import { getExperiments } from "./functions/server/getExperiments";
21+
import { getCountryCode } from "./functions/server/getCountryCode";
22+
import { ExperimentsProvider } from "../contextProviders/experiments";
23+
import * as Sentry from "@sentry/nextjs";
2024

2125
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
2226

@@ -54,6 +58,29 @@ export default async function RootLayout({
5458
const nonce = headers().get("x-nonce") ?? "";
5559
const currentLocale = getLocale(getL10nBundles());
5660
const session = await getServerSession();
61+
const headersList = headers();
62+
const countryCode = getCountryCode(headersList);
63+
64+
// Check for Nimbus preview mode. Note that this requires a full page reload
65+
// to activate: https://nextjs.org/docs/app/api-reference/file-conventions/layout#caveats
66+
const nimbusPreviewMode = headers().get("x-nimbus-preview-mode");
67+
const experimentationId = getExperimentationId(session?.user ?? null);
68+
const experimentData = await getExperiments({
69+
experimentationId: experimentationId,
70+
countryCode: countryCode,
71+
locale: currentLocale,
72+
previewMode: nimbusPreviewMode === "true",
73+
});
74+
75+
const nimbus_user_id = experimentData["Enrollments"]?.nimbus_user_id;
76+
if (
77+
typeof nimbus_user_id !== "undefined" &&
78+
nimbus_user_id !== experimentationId
79+
) {
80+
Sentry.captureMessage(
81+
`Nimbus user ID from Cirrus: [${nimbus_user_id}] did not match experimentationId: [${experimentationId}]`,
82+
);
83+
}
5784

5885
return (
5986
<html lang={currentLocale}>
@@ -64,12 +91,14 @@ export default async function RootLayout({
6491
data-ga4-measurement-id={CONST_GA4_MEASUREMENT_ID}
6592
data-node-env={process.env.NODE_ENV}
6693
>
67-
<SessionProvider session={session}>{children}</SessionProvider>
94+
<ExperimentsProvider experimentData={experimentData}>
95+
<SessionProvider session={session}>{children}</SessionProvider>
96+
</ExperimentsProvider>
6897
</body>
6998
<StripeScript />
7099
<GleanScript
71100
channel={process.env.APP_ENV ?? ""}
72-
experimentationId={getExperimentationId(session?.user ?? null)}
101+
experimentationId={experimentationId}
73102
/>
74103
{headers().get("DNT") !== "1" && (
75104
<GoogleAnalyticsWorkaround
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
"use client";
6+
7+
import { ReactNode, createContext, useContext } from "react";
8+
import { ExperimentData_V2_Or_V2LikeV1 } from "../app/functions/server/getExperiments";
9+
10+
interface ExperimentsProviderProps {
11+
children: ReactNode;
12+
experimentData: ExperimentData_V2_Or_V2LikeV1;
13+
}
14+
15+
export const ExperimentsContext =
16+
createContext<ExperimentData_V2_Or_V2LikeV1 | null>(null);
17+
18+
export const ExperimentsProvider = ({
19+
children,
20+
experimentData,
21+
}: ExperimentsProviderProps) => {
22+
return (
23+
<ExperimentsContext.Provider value={experimentData}>
24+
{children}
25+
</ExperimentsContext.Provider>
26+
);
27+
};
28+
29+
export const useExperiments = () => {
30+
const context = useContext(ExperimentsContext);
31+
return context;
32+
};

src/middleware.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export function middleware(request: NextRequest) {
2929
existingExperimentationId?.value ?? `guest-${crypto.randomUUID()}`;
3030
requestHeaders.set("x-experimentation-id", experimentationId);
3131

32+
// Check for Nimbus preview mode. Note that this requires a full page reload
33+
// to activate: https://nextjs.org/docs/app/api-reference/file-conventions/layout#caveats
34+
const nimbusPreviewMode = request.nextUrl.searchParams.get("nimbus_preview");
35+
requestHeaders.set(
36+
"x-nimbus-preview-mode",
37+
nimbusPreviewMode === "true" ? "true" : "false",
38+
);
39+
3240
const response = NextResponse.next({
3341
request: {
3442
headers: requestHeaders,

0 commit comments

Comments
 (0)