From e443167697795359c8935900e539ec8a397dc20c Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Thu, 11 Jul 2024 22:09:54 +0200 Subject: [PATCH 1/9] feat: Add element view telemetry --- package-lock.json | 9 ++--- package.json | 2 +- .../(redesign)/(public)/LandingView.tsx | 5 +++ .../(redesign)/(public)/SignUpForm.tsx | 13 ++++++- src/app/components/client/PageLoadEvent.tsx | 4 ++- src/app/hooks/useTelemetry.ts | 8 +++-- src/app/hooks/useViewTelemetry.ts | 32 +++++++++++++++++ src/telemetry/metrics.yaml | 34 +++++++++++++++++++ 8 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 src/app/hooks/useViewTelemetry.ts diff --git a/package-lock.json b/package-lock.json index c5812133fe1..aef691d5c51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", "prettier": "3.3.2", - "react-intersection-observer": "^9.10.3", + "react-intersection-observer": "^9.13.0", "sass": "^1.77.6", "storybook": "^8.1.11", "stylelint": "^16.6.1", @@ -26699,10 +26699,11 @@ "dev": true }, "node_modules/react-intersection-observer": { - "version": "9.10.3", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.10.3.tgz", - "integrity": "sha512-9NYfKwPZRovB6QJee7fDg0zz/SyYrqXtn5xTZU0vwLtLVBtfu9aZt1pVmr825REE49VPDZ7Lm5SNHjJBOTZHpA==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", + "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", "dev": true, + "license": "MIT", "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index 4dad71cb813..d7b257a612c 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", "prettier": "3.3.2", - "react-intersection-observer": "^9.10.3", + "react-intersection-observer": "^9.13.0", "sass": "^1.77.6", "storybook": "^8.1.11", "stylelint": "^16.6.1", diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx index 4c162b90403..4a815645cfc 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx @@ -65,6 +65,7 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_header", field: "entered_email_address_header", + view: "viewed_get_scan_header", }} /> )} @@ -136,6 +137,7 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_second", field: "entered_email_address_second", + view: "viewed_get_scan_second", }} /> @@ -182,6 +184,7 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_third", field: "entered_email_address_third", + view: "viewed_get_scan_third", }} /> @@ -201,6 +204,7 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_fourth", field: "entered_email_address_fourth", + view: "viewed_get_scan_fourth", }} scanLimitReached={props.scanLimitReached} /> @@ -244,6 +248,7 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_last", field: "entered_email_address_last", + view: "viewed_get_scan_last", }} scanLimitReached={props.scanLimitReached} /> diff --git a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx index 40dbc1ad6ed..c3047a10652 100644 --- a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx @@ -10,6 +10,7 @@ import { useL10n } from "../../../hooks/l10n"; import { Button } from "../../../components/client/Button"; import styles from "./SignUpForm.module.scss"; import { useTelemetry } from "../../../hooks/useTelemetry"; +import { useViewTelemetry } from "../../../hooks/useViewTelemetry"; import { VisuallyHidden } from "../../../components/server/VisuallyHidden"; import { WaitlistCta } from "./ScanLimit"; import { useCookies } from "react-cookie"; @@ -22,6 +23,7 @@ export type Props = { eventId: { cta: string; field?: string; + view?: string; }; scanLimitReached: boolean; placeholder?: string; @@ -32,6 +34,15 @@ export const SignUpForm = (props: Props) => { const l10n = useL10n(); const [emailInput, setEmailInput] = useState(""); const record = useTelemetry(); + const { view } = props.eventId; + const refViewTelemetry = useViewTelemetry( + { + element_id: view, + }, + { + skip: typeof view === "undefined", + }, + ); const [cookies] = useCookies(["attributionsFirstTouch"]); let attributionSearchParams = new URLSearchParams( cookies.attributionsFirstTouch, @@ -72,7 +83,7 @@ export const SignUpForm = (props: Props) => { return props.scanLimitReached ? ( ) : ( -
+ { ]); const pathname = usePathname(); - const recordTelemetry = useTelemetry(props.experimentationId); + const recordTelemetry = useTelemetry({ + experimentationId: props.experimentationId, + }); if ( props.experimentationId.startsWith("guest") && diff --git a/src/app/hooks/useTelemetry.ts b/src/app/hooks/useTelemetry.ts index 28cb014be1d..f880534fa40 100644 --- a/src/app/hooks/useTelemetry.ts +++ b/src/app/hooks/useTelemetry.ts @@ -17,9 +17,13 @@ const TelemetryPlatforms = { Ga: "ga", } as const; -export const useTelemetry = (experimentationId?: string) => { +export type TelemetryArgs = { + experimentationId?: string; +}; + +export const useTelemetry = (args?: TelemetryArgs) => { const path = usePathname(); - const recordGlean = useGlean(experimentationId); + const recordGlean = useGlean(args?.experimentationId); const { Glean, Ga } = TelemetryPlatforms; const recordTelemetry = < diff --git a/src/app/hooks/useViewTelemetry.ts b/src/app/hooks/useViewTelemetry.ts new file mode 100644 index 00000000000..49c8528c5c8 --- /dev/null +++ b/src/app/hooks/useViewTelemetry.ts @@ -0,0 +1,32 @@ +/* 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 { IntersectionOptions, useInView } from "react-intersection-observer"; +import { TelemetryArgs, useTelemetry } from "./useTelemetry"; +import { GleanMetricMap } from "../../telemetry/generated/_map"; + +export function useViewTelemetry< + EventModule extends "view", + EventName extends keyof GleanMetricMap[EventModule], +>( + args: TelemetryArgs & GleanMetricMap[EventModule][EventName], + options?: IntersectionOptions, +) { + const { experimentationId, ...telemetryArgs } = args; + const recordTelemetry = useTelemetry({ experimentationId }); + const { ref } = useInView({ + skip: Object.keys(telemetryArgs).length === 0, + threshold: 1, + triggerOnce: true, + ...options, + onChange: (inView) => { + if (!inView) { + return; + } + recordTelemetry("view", "enter", telemetryArgs); + }, + }); + + return ref; +} diff --git a/src/telemetry/metrics.yaml b/src/telemetry/metrics.yaml index 71c2dfca2d0..b4e52c276c9 100644 --- a/src/telemetry/metrics.yaml +++ b/src/telemetry/metrics.yaml @@ -396,6 +396,40 @@ collapse: description: Which tier of plan the user is on [Free, Plus] type: string +view: + enter: + type: event + description: | + A DOM element entered the viewport. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1823766 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1823766 + data_sensitivity: + - interaction + notification_emails: + - rhelmer@mozilla.com + expires: never + extra_keys: + path: + description: The path of the page. + type: string + user_id: + description: Mozilla accounts user ID. + type: string + session_id: + description: An ID that allows us to track “sessions” of the user experience within the product. + type: string + flow_id: + description: A randomly generated unique identifier for following user flows within the FxA system. + type: string + element_id: + description: The ID of the element that entered the viewport, or some way to identify where on the viewport the element is located. + type: string + plan_tier: + description: Which tier of plan the user is on [Free, Plus] + type: string + cta_button: click: type: event From 53a62e817694ff90cc06e6e71f910832985e9729 Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Fri, 12 Jul 2024 12:20:35 +0200 Subject: [PATCH 2/9] chore: Revert react-intersection-observer upgrade --- package-lock.json | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index aef691d5c51..c5812133fe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", "prettier": "3.3.2", - "react-intersection-observer": "^9.13.0", + "react-intersection-observer": "^9.10.3", "sass": "^1.77.6", "storybook": "^8.1.11", "stylelint": "^16.6.1", @@ -26699,11 +26699,10 @@ "dev": true }, "node_modules/react-intersection-observer": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", - "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", + "version": "9.10.3", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.10.3.tgz", + "integrity": "sha512-9NYfKwPZRovB6QJee7fDg0zz/SyYrqXtn5xTZU0vwLtLVBtfu9aZt1pVmr825REE49VPDZ7Lm5SNHjJBOTZHpA==", "dev": true, - "license": "MIT", "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index d7b257a612c..4dad71cb813 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", "prettier": "3.3.2", - "react-intersection-observer": "^9.13.0", + "react-intersection-observer": "^9.10.3", "sass": "^1.77.6", "storybook": "^8.1.11", "stylelint": "^16.6.1", From d6972e13af89a43f4a3f3c38de6589c1d6dd3b15 Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Fri, 12 Jul 2024 12:39:13 +0200 Subject: [PATCH 3/9] fix: Args passed to useTelemetry --- .../(authenticated)/user/(dashboard)/dashboard/View.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx index 801d04cba3d..52337c82436 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx @@ -87,7 +87,9 @@ export type TabData = { export const View = (props: Props) => { const l10n = useL10n(); - const recordTelemetry = useTelemetry(props.experimentationId); + const recordTelemetry = useTelemetry({ + experimentationId: props.experimentationId, + }); const countryCode = useContext(CountryCodeContext); const pathname = usePathname(); From c05d519bde1f7bbc0c7a07158da25d540b29cd6e Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Thu, 18 Jul 2024 15:36:53 +0200 Subject: [PATCH 4/9] chore: Add ctaButton view event --- .../(redesign)/(public)/SignUpForm.tsx | 3 ++- src/app/hooks/useViewTelemetry.ts | 5 +++-- src/telemetry/metrics.yaml | 18 ++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx index cb02d4c31f0..c3efb8b3b5f 100644 --- a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx @@ -38,8 +38,9 @@ export const SignUpForm = (props: Props) => { const record = useTelemetry(); const { view } = props.eventId; const refViewTelemetry = useViewTelemetry( + "ctaButton", { - element_id: view, + button_id: view, }, { skip: typeof view === "undefined", diff --git a/src/app/hooks/useViewTelemetry.ts b/src/app/hooks/useViewTelemetry.ts index 49c8528c5c8..863785c12db 100644 --- a/src/app/hooks/useViewTelemetry.ts +++ b/src/app/hooks/useViewTelemetry.ts @@ -7,9 +7,10 @@ import { TelemetryArgs, useTelemetry } from "./useTelemetry"; import { GleanMetricMap } from "../../telemetry/generated/_map"; export function useViewTelemetry< - EventModule extends "view", + EventModule extends keyof Pick, EventName extends keyof GleanMetricMap[EventModule], >( + eventModule: EventModule, args: TelemetryArgs & GleanMetricMap[EventModule][EventName], options?: IntersectionOptions, ) { @@ -24,7 +25,7 @@ export function useViewTelemetry< if (!inView) { return; } - recordTelemetry("view", "enter", telemetryArgs); + recordTelemetry(eventModule, "view", telemetryArgs); }, }); diff --git a/src/telemetry/metrics.yaml b/src/telemetry/metrics.yaml index b4e52c276c9..37d1cd582c1 100644 --- a/src/telemetry/metrics.yaml +++ b/src/telemetry/metrics.yaml @@ -396,11 +396,11 @@ collapse: description: Which tier of plan the user is on [Free, Plus] type: string -view: - enter: +cta_button: + click: type: event description: | - A DOM element entered the viewport. + A click on a button that has a specific call-to-action. bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1823766 data_reviews: @@ -423,18 +423,16 @@ view: flow_id: description: A randomly generated unique identifier for following user flows within the FxA system. type: string - element_id: - description: The ID of the element that entered the viewport, or some way to identify where on the viewport the element is located. - type: string + button_id: + description: The ID of the button that was clicked on, or some way to identify where on the page the interaction is located. plan_tier: description: Which tier of plan the user is on [Free, Plus] type: string -cta_button: - click: + view: type: event description: | - A click on a button that has a specific call-to-action. + A button that has a specific call-to-action entered the viewport. bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1823766 data_reviews: @@ -458,7 +456,7 @@ cta_button: description: A randomly generated unique identifier for following user flows within the FxA system. type: string button_id: - description: The ID of the button that was clicked on, or some way to identify where on the page the interaction is located. + description: The ID of the button that entered the viewport, or some way to identify where on the page the interaction is located. plan_tier: description: Which tier of plan the user is on [Free, Plus] type: string From f0c641071edba69db62778e59e2052ee44e4d24b Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Fri, 19 Jul 2024 03:30:34 +0200 Subject: [PATCH 5/9] chore: Add unit test coverage --- .../(redesign)/(public)/FreeScanCta.tsx | 25 +++++-------- .../(redesign)/(public)/LandingView.test.tsx | 37 ++++++++++++++++++- .../(redesign)/(public)/LandingView.tsx | 5 --- .../(redesign)/(public)/SignUpForm.tsx | 18 +++------ src/app/hooks/useViewTelemetry.ts | 5 +++ 5 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx b/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx index be4ddc2d119..7f715142692 100644 --- a/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx @@ -6,24 +6,14 @@ import { signIn } from "next-auth/react"; import { useCookies } from "react-cookie"; -import { SignUpForm, Props as SignUpFormProps } from "./SignUpForm"; +import { Props, SignUpForm } from "./SignUpForm"; import { TelemetryButton } from "../../../components/client/TelemetryButton"; import { modifyAttributionsForUrlSearchParams } from "../../../functions/universal/attributions"; import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; import { useL10n } from "../../../hooks/l10n"; import { WaitlistCta } from "./ScanLimit"; - -export type Props = { - eligibleForPremium: boolean; - signUpCallbackUrl: string; - isHero?: boolean; - eventId: { - cta: string; - field?: string; - }; - scanLimitReached: boolean; - placeholder?: string; -}; +import { useViewTelemetry } from "../../../hooks/useViewTelemetry"; +import { RefObject } from "react"; export function getAttributionSearchParams({ cookies, @@ -60,12 +50,16 @@ export function getAttributionSearchParams({ } export const FreeScanCta = ( - props: SignUpFormProps & { + props: Props & { experimentData: ExperimentData; }, ) => { const l10n = useL10n(); const [cookies] = useCookies(["attributionsFirstTouch"]); + const telemetryButtonId = `${props.eventId.cta}-${props.experimentData["landing-page-free-scan-cta"].variant}`; + const refViewTelemetry = useViewTelemetry("ctaButton", { + button_id: telemetryButtonId, + }); if ( !props.experimentData["landing-page-free-scan-cta"].enabled || props.experimentData["landing-page-free-scan-cta"].variant === @@ -88,12 +82,13 @@ export const FreeScanCta = ( ) : (
} variant="primary" event={{ module: "ctaButton", name: "click", data: { - button_id: `${props.eventId.cta}-${props.experimentData["landing-page-free-scan-cta"].variant}`, + button_id: telemetryButtonId, }, }} onPress={() => { diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.test.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.test.tsx index cff25795f40..3793bf44a68 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.test.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.test.tsx @@ -5,6 +5,7 @@ import { it, expect } from "@jest/globals"; import { composeStory } from "@storybook/react"; import { + act, getAllByRole, getByRole, getByText, @@ -27,6 +28,7 @@ import Meta, { } from "./LandingView.stories"; import { deleteAllCookies } from "../../../functions/client/deleteAllCookies"; import { defaultExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; +import { mockIsIntersecting } from "react-intersection-observer/test-utils"; jest.mock("next-auth/react", () => { return { @@ -950,7 +952,7 @@ describe("Free scan CTA experiment", () => { expect(waitlistCta[0]).toBeInTheDocument(); }); - it("sends telemetry for the different experiment variants", async () => { + it("sends telemetry when clicking on one of the experiment variants", async () => { const mockedRecord = useTelemetry(); const user = userEvent.setup(); const ComposedDashboard = composeStory(LandingUs, Meta); @@ -983,6 +985,39 @@ describe("Free scan CTA experiment", () => { ); }); + it("sends telemetry when a free scan CTA is shown in the viewport", () => { + const mockedRecord = useTelemetry(); + const ComposedDashboard = composeStory(LandingUs, Meta); + render( + , + ); + + // jsdom will complain about not being able to navigate to a different page + // after clicking the link; suppress that error, as it's not relevant to the + // test: + jest.spyOn(console, "error").mockImplementation(() => undefined); + + const submitButton = screen.getAllByRole("button", { + name: "Get free scan", + }); + act(() => { + mockIsIntersecting(submitButton[0], true); + }); + expect(mockedRecord).toHaveBeenCalledWith( + "ctaButton", + "view", + expect.objectContaining({ button_id: "clicked_get_scan_header-ctaOnly" }), + ); + }); + it("passes the expected URL to the identity provider", async () => { const user = userEvent.setup(); const ComposedDashboard = composeStory(LandingUs, Meta); diff --git a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx index a3f2aa96eac..82ec8d4bff5 100644 --- a/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/LandingView.tsx @@ -67,7 +67,6 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_header", field: "entered_email_address_header", - view: "viewed_get_scan_header", }} experimentData={props.experimentData} /> @@ -140,7 +139,6 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_second", field: "entered_email_address_second", - view: "viewed_get_scan_second", }} experimentData={props.experimentData} /> @@ -188,7 +186,6 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_third", field: "entered_email_address_third", - view: "viewed_get_scan_third", }} experimentData={props.experimentData} /> @@ -209,7 +206,6 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_fourth", field: "entered_email_address_fourth", - view: "viewed_get_scan_fourth", }} scanLimitReached={props.scanLimitReached} experimentData={props.experimentData} @@ -254,7 +250,6 @@ export const View = (props: Props) => { eventId={{ cta: "clicked_get_scan_last", field: "entered_email_address_last", - view: "viewed_get_scan_last", }} scanLimitReached={props.scanLimitReached} experimentData={props.experimentData} diff --git a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx index c3efb8b3b5f..9e40827f2de 100644 --- a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx @@ -19,15 +19,14 @@ import { ExperimentData } from "../../../../telemetry/generated/nimbus/experimen export type Props = { eligibleForPremium: boolean; - signUpCallbackUrl: string; - isHero?: boolean; eventId: { cta: string; field?: string; - view?: string; }; scanLimitReached: boolean; + signUpCallbackUrl: string; experimentData?: ExperimentData; + isHero?: boolean; placeholder?: string; }; @@ -36,16 +35,9 @@ export const SignUpForm = (props: Props) => { const l10n = useL10n(); const [emailInput, setEmailInput] = useState(""); const record = useTelemetry(); - const { view } = props.eventId; - const refViewTelemetry = useViewTelemetry( - "ctaButton", - { - button_id: view, - }, - { - skip: typeof view === "undefined", - }, - ); + const refViewTelemetry = useViewTelemetry("ctaButton", { + button_id: props.eventId.cta, + }); const [cookies] = useCookies(["attributionsFirstTouch"]); const onSubmit: FormEventHandler = (event) => { diff --git a/src/app/hooks/useViewTelemetry.ts b/src/app/hooks/useViewTelemetry.ts index 863785c12db..b7ff9e720fb 100644 --- a/src/app/hooks/useViewTelemetry.ts +++ b/src/app/hooks/useViewTelemetry.ts @@ -22,6 +22,11 @@ export function useViewTelemetry< triggerOnce: true, ...options, onChange: (inView) => { + // Since this function is only triggered once when an element is entering + // the viewport and not again after leaving it. With the current setting + // the following condition is not expected to get called. Keeping the + // condition in place in case this changes. + /* c8 ignore next 3 */ if (!inView) { return; } From 3d29d1de50acc06b4ccd56a45bc69429cb45e299 Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Mon, 22 Jul 2024 16:36:51 +0200 Subject: [PATCH 6/9] chore: Add react-intersection-observer as dependency --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93df26589f2..827d63181e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "react-aria": "^3.33.1", "react-cookie": "^7.1.0", "react-dom": "^18.2.0", + "react-intersection-observer": "^9.13.0", "react-stately": "^3.31.1", "server-only": "^0.0.1", "uuid": "^9.0.1", @@ -94,7 +95,6 @@ "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", "prettier": "3.3.2", - "react-intersection-observer": "^9.10.3", "sass": "^1.77.6", "storybook": "^8.1.11", "stylelint": "^16.7.0", @@ -24240,10 +24240,10 @@ "dev": true }, "node_modules/react-intersection-observer": { - "version": "9.10.3", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.10.3.tgz", - "integrity": "sha512-9NYfKwPZRovB6QJee7fDg0zz/SyYrqXtn5xTZU0vwLtLVBtfu9aZt1pVmr825REE49VPDZ7Lm5SNHjJBOTZHpA==", - "dev": true, + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", + "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", + "license": "MIT", "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index d24420e07e9..03779eb5d35 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "react-aria": "^3.33.1", "react-cookie": "^7.1.0", "react-dom": "^18.2.0", + "react-intersection-observer": "^9.13.0", "react-stately": "^3.31.1", "server-only": "^0.0.1", "uuid": "^9.0.1", @@ -145,7 +146,6 @@ "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", "prettier": "3.3.2", - "react-intersection-observer": "^9.10.3", "sass": "^1.77.6", "storybook": "^8.1.11", "stylelint": "^16.7.0", From 6618a558dc6acc841886a6187cc795998b4f7eae Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Mon, 22 Jul 2024 16:43:06 +0200 Subject: [PATCH 7/9] chore: Move type assertion to useTelemetryView hook --- src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx | 2 +- src/app/hooks/useViewTelemetry.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx b/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx index 7f715142692..0251217dd56 100644 --- a/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx +++ b/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx @@ -82,7 +82,7 @@ export const FreeScanCta = ( ) : (
} + buttonRef={refViewTelemetry as RefObject} variant="primary" event={{ module: "ctaButton", diff --git a/src/app/hooks/useViewTelemetry.ts b/src/app/hooks/useViewTelemetry.ts index b7ff9e720fb..5bafa869f1c 100644 --- a/src/app/hooks/useViewTelemetry.ts +++ b/src/app/hooks/useViewTelemetry.ts @@ -5,6 +5,7 @@ import { IntersectionOptions, useInView } from "react-intersection-observer"; import { TelemetryArgs, useTelemetry } from "./useTelemetry"; import { GleanMetricMap } from "../../telemetry/generated/_map"; +import { RefObject } from "react"; export function useViewTelemetry< EventModule extends keyof Pick, @@ -34,5 +35,5 @@ export function useViewTelemetry< }, }); - return ref; + return ref as unknown as RefObject; } From 017c203fa627046937fa5c1106e860fc7dbaaeaa Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Mon, 22 Jul 2024 17:13:40 +0200 Subject: [PATCH 8/9] fix: HTMLFormElement type --- src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx index 9e40827f2de..83051921292 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, useId, useState } from "react"; +import { FormEventHandler, RefObject, useId, useState } from "react"; import { signIn } from "next-auth/react"; import { useL10n } from "../../../hooks/l10n"; import { Button } from "../../../components/client/Button"; @@ -66,7 +66,11 @@ export const SignUpForm = (props: Props) => { return props.scanLimitReached ? ( ) : ( - + } + className={styles.form} + onSubmit={onSubmit} + > Date: Mon, 22 Jul 2024 17:38:00 +0200 Subject: [PATCH 9/9] fix: Unit tests --- package-lock.json | 25 +++++++++++++------------ package.json | 24 +++++++++++------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19c16beeabd..6ee0d3399a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,6 @@ "react-aria": "^3.33.1", "react-cookie": "^7.1.0", "react-dom": "^18.2.0", - "react-intersection-observer": "^9.13.0", "react-stately": "^3.31.1", "server-only": "^0.0.1", "uuid": "^9.0.1", @@ -53,13 +52,13 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.43.1", - "@storybook/addon-a11y": "^8.2.5", - "@storybook/addon-actions": "^8.2.5", - "@storybook/addon-essentials": "^8.2.5", - "@storybook/addon-interactions": "^8.2.5", - "@storybook/addon-links": "^8.2.5", + "@storybook/addon-a11y": "^8.2.2", + "@storybook/addon-actions": "^8.2.2", + "@storybook/addon-essentials": "^8.2.2", + "@storybook/addon-interactions": "^8.2.2", + "@storybook/addon-links": "^8.2.2", "@storybook/blocks": "^8.0.0", - "@storybook/nextjs": "^8.2.5", + "@storybook/nextjs": "^8.2.2", "@storybook/react": "^8.0.0", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", @@ -94,7 +93,7 @@ "jest-fail-on-console": "^3.3.0", "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", - "prettier": "3.3.3", + "prettier": "3.3.2", "react-intersection-observer": "^9.10.3", "sass": "^1.77.6", "storybook": "^8.1.11", @@ -106,7 +105,7 @@ "yaml": "^2.4.5" }, "engines": { - "node": "22.4.x", + "node": "22.3.x", "npm": "10.8.x" } }, @@ -23834,10 +23833,11 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -24312,6 +24312,7 @@ "version": "9.13.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/package.json b/package.json index 72234a65788..ac5bfa839e6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Firefox Monitor", "engines": { - "node": "22.4.x", + "node": "22.3.x", "npm": "10.8.x" }, "type": "module", @@ -11,7 +11,6 @@ "dev": "npm run build-nimbus && next dev --port=6060", "dev:cron:first-data-broker-removal-fixed": "tsx --tsconfig tsconfig.cronjobs.json src/scripts/cronjobs/firstDataBrokerRemovalFixed.tsx", "dev:cron:monthly-activity": "tsx --tsconfig tsconfig.cronjobs.json src/scripts/cronjobs/monthlyActivity.tsx", - "dev:cron:breach-alerts": "tsx --tsconfig tsconfig.cronjobs.json src/scripts/cronjobs/emailBreachAlerts.ts", "dev:cron:db-delete-unverified-subscribers": "tsx --tsconfig tsconfig.cronjobs.json src/scripts/cronjobs/deleteUnverifiedSubscribers.ts", "dev:cron:db-pull-breaches": "tsx --tsconfig tsconfig.cronjobs.json src/scripts/cronjobs/syncBreaches.ts", "dev:cron:remote-settings-pull-breaches": "tsx --tsconfig tsconfig.cronjobs.json src/scripts/cronjobs/updateBreachesInRemoteSettings.ts", @@ -28,7 +27,7 @@ "e2e:smoke": "playwright test src/e2e/ --grep @smoke", "cron:first-data-broker-removal-fixed": "node dist/scripts/cronjobs/firstDataBrokerRemovalFixed.js", "cron:monthly-activity": "node dist/scripts/cronjobs/monthlyActivity.js", - "cron:breach-alerts": "node dist/scripts/cronjobs/emailBreachAlerts.js", + "cron:breach-alerts": "node src/scripts/emailBreachAlerts.js", "cron:db-delete-unverified-subscribers": "node dist/scripts/cronjobs/deleteUnverifiedSubscribers.js", "cron:db-pull-breaches": "node dist/scripts/cronjobs/syncBreaches.js", "cron:remote-settings-pull-breaches": "node dist/scripts/cronjobs/updateBreachesInRemoteSettings.js", @@ -57,8 +56,8 @@ "homepage": "https://github.com/mozilla/blurts-server", "license": "MPL-2.0", "volta": { - "node": "22.4.1", - "npm": "10.8.1" + "node": "22.3.0", + "npm": "10.8.0" }, "dependencies": { "@aws-sdk/client-s3": "^3.614.0", @@ -96,7 +95,6 @@ "react-aria": "^3.33.1", "react-cookie": "^7.1.0", "react-dom": "^18.2.0", - "react-intersection-observer": "^9.13.0", "react-stately": "^3.31.1", "server-only": "^0.0.1", "uuid": "^9.0.1", @@ -105,13 +103,13 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.43.1", - "@storybook/addon-a11y": "^8.2.5", - "@storybook/addon-actions": "^8.2.5", - "@storybook/addon-essentials": "^8.2.5", - "@storybook/addon-interactions": "^8.2.5", - "@storybook/addon-links": "^8.2.5", + "@storybook/addon-a11y": "^8.2.2", + "@storybook/addon-actions": "^8.2.2", + "@storybook/addon-essentials": "^8.2.2", + "@storybook/addon-interactions": "^8.2.2", + "@storybook/addon-links": "^8.2.2", "@storybook/blocks": "^8.0.0", - "@storybook/nextjs": "^8.2.5", + "@storybook/nextjs": "^8.2.2", "@storybook/react": "^8.0.0", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", @@ -146,7 +144,7 @@ "jest-fail-on-console": "^3.3.0", "lint-staged": "^15.2.5", "mjml-browser": "^4.15.3", - "prettier": "3.3.3", + "prettier": "3.3.2", "react-intersection-observer": "^9.10.3", "sass": "^1.77.6", "storybook": "^8.1.11",