fix(glean): Conditionally fire promo view event when Settings view event fires

Because:
* We are seeing this event fire too many times, reproducible when navigating around Settings

This commit:
* Creates a shared function for PageSettings and ProductPromo to use to conditionally send the Glean ping and render the component as expected

fixes FXA-10403
This commit is contained in:
Lauren Zugai 2024-09-10 17:44:36 -05:00
Родитель 662c8f742b
Коммит eac5184264
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 0C86B71E24811D10
7 изменённых файлов: 120 добавлений и 54 удалений

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

@ -6,9 +6,19 @@ import React from 'react';
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import PageSettings from '.';
import { renderWithRouter } from '../../../models/mocks';
import {
MOCK_ACCOUNT,
mockAppContext,
renderWithRouter,
} from '../../../models/mocks';
import * as Metrics from '../../../lib/metrics';
import GleanMetrics from '../../../lib/glean';
import { Account, AppContext } from '../../../models';
import {
ALL_PRODUCT_PROMO_SERVICES,
ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
} from '../../../pages/mocks';
import { MOCK_SERVICES } from '../ConnectedServices/mocks';
jest.mock('../../../lib/metrics', () => ({
setProperties: jest.fn(),
@ -38,6 +48,9 @@ beforeEach(() => {
});
describe('PageSettings', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders without imploding', async () => {
renderWithRouter(<PageSettings />);
expect(screen.getByTestId('settings-profile')).toBeInTheDocument();
@ -68,5 +81,42 @@ describe('PageSettings', () => {
);
expect(GleanMetrics.deleteAccount.settingsSubmit).toHaveBeenCalled();
});
describe('product promo event', () => {
it('user does not have Monitor', async () => {
const account = {
...MOCK_ACCOUNT,
attachedClients: [],
subscriptions: [],
} as unknown as Account;
renderWithRouter(
<AppContext.Provider value={mockAppContext({ account })}>
<PageSettings />
</AppContext.Provider>
);
expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledTimes(1);
expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledWith({
event: { reason: 'free' },
});
});
it('user has all products and subscriptions', async () => {
const attachedClients = MOCK_SERVICES.filter((service) =>
ALL_PRODUCT_PROMO_SERVICES.some(
(promoService) => promoService.name === service.name
)
);
const account = {
...MOCK_ACCOUNT,
attachedClients,
subscriptions: ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
} as unknown as Account;
renderWithRouter(
<AppContext.Provider value={mockAppContext({ account })}>
<PageSettings />
</AppContext.Provider>
);
expect(GleanMetrics.accountPref.promoMonitorView).not.toBeCalled();
});
});
});
});

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

@ -2,7 +2,7 @@
* 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 React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Link, RouteComponentProps } from '@reach/router';
import Security from '../Security';
import { Profile } from '../Profile';
@ -15,14 +15,17 @@ import { SETTINGS_PATH } from 'fxa-settings/src/constants';
import { Localized } from '@fluent/react';
import DataCollection from '../DataCollection';
import GleanMetrics from '../../../lib/glean';
import ProductPromo, { ProductPromoType } from '../ProductPromo';
import ProductPromo, {
getProductPromoData,
ProductPromoType,
} from '../ProductPromo';
import SideBar from '../Sidebar';
import NotificationPromoBanner from '../../NotificationPromoBanner';
import keyImage from '../../NotificationPromoBanner/key.svg';
import Head from 'fxa-react/components/Head';
export const PageSettings = (_: RouteComponentProps) => {
const { uid, recoveryKey } = useAccount();
const { uid, recoveryKey, attachedClients, subscriptions } = useAccount();
const ftlMsgResolver = useFtlMsgResolver();
Metrics.setProperties({
@ -35,6 +38,23 @@ export const PageSettings = (_: RouteComponentProps) => {
GleanMetrics.accountPref.view();
}, []);
const [productPromoGleanEventSent, setProductPromoGleanEventSent] =
useState(false);
useEffect(() => {
// We want this view event to fire whenever the account settings page view
// event fires, if the user is shown the promo.
const { gleanEvent, hasAllPromoProducts } = getProductPromoData(
attachedClients,
subscriptions
);
if (!hasAllPromoProducts && !productPromoGleanEventSent) {
GleanMetrics.accountPref.promoMonitorView(gleanEvent);
// Keep track of this because `attachedClients` can change on disconnect
setProductPromoGleanEventSent(true);
}
}, [attachedClients, subscriptions, productPromoGleanEventSent]);
const accountRecoveryNotificationProps = {
headerImage: keyImage,
ctaText: ftlMsgResolver.getMsg(

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

@ -6,15 +6,14 @@ import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import ProductPromo, { ProductPromoType } from '.';
import { Account, AppContext } from '../../../models';
import { MOCK_SERVICES } from '../ConnectedServices/mocks';
import { MozServices } from '../../../lib/types';
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
import { mockAppContext } from '../../../models/mocks';
import GleanMetrics from '../../../lib/glean';
// List all services this component handles
const PRODUCT_PROMO_SERVICES = [MozServices.Monitor];
const PRODUCT_PROMO_SUBSCRIPTIONS = [{ productName: MozServices.MonitorPlus }];
import {
ALL_PRODUCT_PROMO_SERVICES,
ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
} from '../../../pages/mocks';
jest.mock('../../../lib/glean', () => ({
__esModule: true,
@ -32,12 +31,8 @@ describe('ProductPromo', () => {
});
it('renders nothing if user has Monitor but not MonitorPlus, and MonitorPlus Promo is disabled', () => {
const services = MOCK_SERVICES.filter((service) =>
// TODO: MozServices / string discrepancy, FXA-6802
PRODUCT_PROMO_SERVICES.includes(service.name as MozServices)
);
const account = {
attachedClients: services,
attachedClients: ALL_PRODUCT_PROMO_SERVICES,
subscriptions: [],
} as unknown as Account;
@ -48,17 +43,12 @@ describe('ProductPromo', () => {
);
expect(container.firstChild).toBeNull();
expect(GleanMetrics.accountPref.promoMonitorView).not.toHaveBeenCalled();
});
it('renders nothing if user has all products and subscriptions', async () => {
const services = MOCK_SERVICES.filter((service) =>
// TODO: MozServices / string discrepancy, FXA-6802
PRODUCT_PROMO_SERVICES.includes(service.name as MozServices)
);
const account = {
attachedClients: services,
subscriptions: PRODUCT_PROMO_SUBSCRIPTIONS,
attachedClients: ALL_PRODUCT_PROMO_SERVICES,
subscriptions: ALL_PRODUCT_PROMO_SUBSCRIPTIONS,
} as unknown as Account;
const { container } = renderWithLocalizationProvider(
@ -68,7 +58,6 @@ describe('ProductPromo', () => {
);
expect(container.firstChild).toBeNull();
expect(GleanMetrics.accountPref.promoMonitorView).not.toHaveBeenCalled();
});
it('renders Monitor promo if user does not have Monitor', async () => {
const account = {
@ -89,9 +78,6 @@ describe('ProductPromo', () => {
'href',
'https://monitor.mozilla.org/?utm_source=moz-account&utm_medium=product-partnership&utm_term=sidebar&utm_content=monitor-free&utm_campaign=settings-promo'
);
expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledWith({
event: { reason: 'free' },
});
});
it('renders Monitor Plus promo if user does not have Monitor Plus', async () => {
@ -120,9 +106,6 @@ describe('ProductPromo', () => {
'href',
'https://monitor.mozilla.org/#pricing?utm_source=moz-account&utm_medium=product-partnership&utm_term=sidebar&utm_content=monitor-plus&utm_campaign=settings-promo'
);
expect(GleanMetrics.accountPref.promoMonitorView).toBeCalledWith({
event: { reason: 'plus' },
});
});
it('emits metric when user clicks call to action', async () => {

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

@ -8,7 +8,7 @@ import monitorTextLogo from './monitor-text-logo.svg';
import { FtlMsg } from 'fxa-react/lib/utils';
import classNames from 'classnames';
import { MozServices } from '../../../lib/types';
import { useAccount, useConfig } from '../../../models';
import { AccountData, useAccount, useConfig } from '../../../models';
import { constructHrefWithUtm } from '../../../lib/utilities';
import { LINK } from '../../../constants';
import GleanMetrics from '../../../lib/glean';
@ -27,13 +27,12 @@ export interface ProductPromoProps {
monitorPlusEnabled?: boolean;
}
export const ProductPromo = ({
type = ProductPromoType.Sidebar,
monitorPlusEnabled = MONITOR_PLUS_ENABLED,
}: ProductPromoProps) => {
const { attachedClients, subscriptions } = useAccount();
const { env } = useConfig();
export function getProductPromoData(
attachedClients: AccountData['attachedClients'],
subscriptions: AccountData['subscriptions'],
// Temporary until we work on MonitorPlus
monitorPlusEnabled = MONITOR_PLUS_ENABLED
) {
const hasMonitor = attachedClients.some(
({ name }) => name === MozServices.Monitor
);
@ -42,10 +41,29 @@ export const ProductPromo = ({
({ productName }) => productName === MozServices.MonitorPlus
);
// Temporary until we work on MonitorPlus
const showMonitorPlusPromo =
hasMonitor && !hasMonitorPlus && monitorPlusEnabled;
const hasAllPromoProducts = hasMonitor && !showMonitorPlusPromo;
if (hasMonitor && !showMonitorPlusPromo) {
const gleanEvent = showMonitorPlusPromo
? { event: { reason: 'plus' } }
: { event: { reason: 'free' } };
return { showMonitorPlusPromo, hasAllPromoProducts, gleanEvent };
}
export const ProductPromo = ({
type = ProductPromoType.Sidebar,
// Temporary until we work on MonitorPlus
monitorPlusEnabled = MONITOR_PLUS_ENABLED,
}: ProductPromoProps) => {
const { attachedClients, subscriptions } = useAccount();
const { env } = useConfig();
const { showMonitorPlusPromo, hasAllPromoProducts, gleanEvent } =
getProductPromoData(attachedClients, subscriptions, monitorPlusEnabled);
if (hasAllPromoProducts) {
return <></>;
}
@ -67,17 +85,6 @@ export const ProductPromo = ({
'settings-promo'
);
const gleanEvent = showMonitorPlusPromo
? { event: { reason: 'plus' } }
: { event: { reason: 'free' } };
// NOTE, this is a quick fix to prevent double 'view' event firing
// since we use this component in two places (sidebar + settings).
// We will want to refactor this to be less fragile.
if (type === ProductPromoType.Settings) {
GleanMetrics.accountPref.promoMonitorView(gleanEvent);
}
const promoContent = showMonitorPlusPromo ? (
<>
<p className="my-2">

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

@ -83,6 +83,11 @@ export interface AttachedClient {
refreshTokenId: string | null;
}
interface Subscription {
created: number;
productName: string;
}
export interface AccountData {
uid: hexstring;
displayName: string | null;
@ -102,10 +107,7 @@ export interface AccountData {
attachedClients: AttachedClient[];
linkedAccounts: LinkedAccount[];
totp: AccountTotp;
subscriptions: {
created: number;
productName: string;
}[];
subscriptions: Subscription[];
securityEvents: SecurityEvent[];
}

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

@ -26,7 +26,7 @@ export type SigninPushCodeConfirmProps = {
errorMessage?: string;
};
const ProductPromotion = () => {
const Products = () => {
const products = [
{
icon: monitorIcon,
@ -99,7 +99,7 @@ const LoginApprovedMessage = () => {
Your login has been approved. Please close this window.
</p>
</FtlMsg>
<ProductPromotion />
<Products />
</div>
);
};

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

@ -66,3 +66,7 @@ export function mockLoadingSpinnerModule() {
}
export const MOCK_RECOVERY_KEY = 'ARJDF300TFEPRJ7SFYB8QVNVYT60WWS2';
export const MOCK_REMOTE_METADATA = JSON.stringify({});
export const ALL_PRODUCT_PROMO_SERVICES = [{ name: MozServices.Monitor }];
export const ALL_PRODUCT_PROMO_SUBSCRIPTIONS = [
{ productName: MozServices.MonitorPlus },
];