diff --git a/package-lock.json b/package-lock.json
index 06809b49497..dcec0566781 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.10.3",
"react-stately": "^3.31.1",
"server-only": "^0.0.1",
"uuid": "^9.0.1",
@@ -94,7 +95,6 @@
"lint-staged": "^15.2.7",
"mjml-browser": "^4.15.3",
"prettier": "3.3.3",
- "react-intersection-observer": "^9.10.3",
"sass": "^1.77.6",
"storybook": "^8.1.11",
"stylelint": "^16.7.0",
@@ -24313,7 +24313,6 @@
"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,
"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 e45c082685f..5af6960ec94 100644
--- a/package.json
+++ b/package.json
@@ -96,6 +96,7 @@
"react-aria": "^3.33.1",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0",
+ "react-intersection-observer": "^9.10.3",
"react-stately": "^3.31.1",
"server-only": "^0.0.1",
"uuid": "^9.0.1",
@@ -146,7 +147,6 @@
"lint-staged": "^15.2.7",
"mjml-browser": "^4.15.3",
"prettier": "3.3.3",
- "react-intersection-observer": "^9.10.3",
"sass": "^1.77.6",
"storybook": "^8.1.11",
"stylelint": "^16.7.0",
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();
diff --git a/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx b/src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx
index be4ddc2d119..0251217dd56 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)/SignUpForm.tsx b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx
index d46a9264310..83051921292 100644
--- a/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx
+++ b/src/app/(proper_react)/(redesign)/(public)/SignUpForm.tsx
@@ -4,12 +4,13 @@
"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";
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";
@@ -18,14 +19,14 @@ import { ExperimentData } from "../../../../telemetry/generated/nimbus/experimen
export type Props = {
eligibleForPremium: boolean;
- signUpCallbackUrl: string;
- isHero?: boolean;
eventId: {
cta: string;
field?: string;
};
scanLimitReached: boolean;
+ signUpCallbackUrl: string;
experimentData?: ExperimentData;
+ isHero?: boolean;
placeholder?: string;
};
@@ -34,6 +35,9 @@ export const SignUpForm = (props: Props) => {
const l10n = useL10n();
const [emailInput, setEmailInput] = useState("");
const record = useTelemetry();
+ const refViewTelemetry = useViewTelemetry("ctaButton", {
+ button_id: props.eventId.cta,
+ });
const [cookies] = useCookies(["attributionsFirstTouch"]);
const onSubmit: FormEventHandler = (event) => {
@@ -62,7 +66,11 @@ export const SignUpForm = (props: Props) => {
return props.scanLimitReached ? (
) : (
-