зеркало из https://github.com/mozilla/fxa.git
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:
Родитель
662c8f742b
Коммит
eac5184264
|
@ -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 },
|
||||
];
|
||||
|
|
Загрузка…
Ссылка в новой задаче