diff --git a/packages/fxa-settings/src/components/App/index.test.tsx b/packages/fxa-settings/src/components/App/index.test.tsx index 21467ec7c5c..7e4e15044cc 100644 --- a/packages/fxa-settings/src/components/App/index.test.tsx +++ b/packages/fxa-settings/src/components/App/index.test.tsx @@ -120,7 +120,7 @@ jest.mock('../../lib/glean', () => ({ initialize: jest.fn(), getEnabled: jest.fn(), useGlean: jest.fn().mockReturnValue({ enabled: true }), - accountPref: { view: jest.fn(), promoMonitorView: jest.fn() }, + accountPref: { view: jest.fn(), promoVpnView: jest.fn() }, emailFirst: { view: jest.fn(), engage: jest.fn() }, error: { view: jest.fn() }, pageLoad: jest.fn(), diff --git a/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx b/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx index 1430aaff9d9..eeebee79233 100644 --- a/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx +++ b/packages/fxa-settings/src/components/Settings/PageSettings/index.test.tsx @@ -38,7 +38,7 @@ jest.mock('../../../lib/glean', () => ({ default: { accountPref: { view: jest.fn(), - promoMonitorView: jest.fn(), + promoVpnView: jest.fn(), }, deleteAccount: { settingsSubmit: jest.fn(), @@ -53,7 +53,6 @@ jest.mock('../../../lib/glean', () => ({ const mockGetProductPromoData = jest.fn().mockReturnValue({ hidePromo: false, - gleanEvent: { event: { reason: 'default' } }, }); jest.mock('../ProductPromo', () => ({ __esModule: true, @@ -155,10 +154,9 @@ describe('PageSettings', () => { }); describe('product promo event', () => { - it('user does not have Monitor, promo is shown and glean metric is called', async () => { + it('user does not have VPN, promo is shown and glean metric is called', async () => { mockGetProductPromoData.mockReturnValue({ hidePromo: false, - gleanEvent: { event: { reason: 'default' } }, }); renderWithRouter( { ); await waitFor(() => - expect( - GleanMetrics.accountPref.promoMonitorView - ).toHaveBeenCalledTimes(1) + expect(GleanMetrics.accountPref.promoVpnView).toHaveBeenCalledTimes(1) ); - expect(GleanMetrics.accountPref.promoMonitorView).toHaveBeenCalledWith({ - event: { reason: 'default' }, - }); }); - it('user has Monitor, promo is not shown and glean metric is not called', async () => { + it('user has VPN, promo is not shown and glean metric is not called', async () => { mockGetProductPromoData.mockReturnValue({ hidePromo: true, }); @@ -189,9 +182,7 @@ describe('PageSettings', () => { ); await waitFor(() => - expect( - GleanMetrics.accountPref.promoMonitorView - ).not.toHaveBeenCalled() + expect(GleanMetrics.accountPref.promoVpnView).not.toHaveBeenCalled() ); }); }); diff --git a/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx b/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx index 380f09041ee..a7805339ca0 100644 --- a/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx +++ b/packages/fxa-settings/src/components/Settings/PageSettings/index.tsx @@ -17,7 +17,7 @@ import DataCollection from '../DataCollection'; import GleanMetrics from '../../../lib/glean'; import ProductPromo, { getProductPromoData, - MonitorPromoData, + VpnPromoData, } from '../ProductPromo'; import SideBar from '../Sidebar'; import Head from 'fxa-react/components/Head'; @@ -59,9 +59,7 @@ export const PageSettings = ({ GleanMetrics.accountPref.view(); }, []); - const [monitorPromo, setMonitorPromo] = useState( - null - ); + const [vpnPromo, setVpnPromo] = useState(null); const [productPromoGleanEventSent, setProductPromoGleanEventSent] = useState(false); @@ -98,32 +96,27 @@ export const PageSettings = ({ useEffect(() => { (async () => { - if (monitorPromo !== null) { + if (vpnPromo !== null) { return; } const promoData = getProductPromoData(account.attachedClients); - setMonitorPromo(promoData); + setVpnPromo(promoData); })(); - }, [account, monitorPromo]); + }, [account, vpnPromo]); // -- Relying party promotion checks -- useEffect(() => { - if (!monitorPromo) return; + if (!vpnPromo) return; // We want this view event to fire whenever the account settings page view // event fires, if the user is shown the promo. - if (!monitorPromo.hidePromo && !productPromoGleanEventSent) { - GleanMetrics.accountPref.promoMonitorView(monitorPromo.gleanEvent); + if (!vpnPromo.hidePromo && !productPromoGleanEventSent) { + GleanMetrics.accountPref.promoVpnView(); // Keep track of this because `attachedClients` can change on disconnect setProductPromoGleanEventSent(true); } - }, [ - attachedClients, - monitorPromo, - subscriptions, - productPromoGleanEventSent, - ]); + }, [attachedClients, vpnPromo, subscriptions, productPromoGleanEventSent]); useEffect(() => { const targetEmail = (() => { @@ -187,7 +180,7 @@ export const PageSettings = ({ connectedServicesRef, linkedAccountsRef, dataCollectionRef, - monitorPromo, + vpnPromo, }} /> @@ -211,8 +204,8 @@ export const PageSettings = ({ - {monitorPromo && !monitorPromo.hidePromo && ( - + {vpnPromo && !vpnPromo.hidePromo && ( + )} diff --git a/packages/fxa-settings/src/components/Settings/ProductPromo/en.ftl b/packages/fxa-settings/src/components/Settings/ProductPromo/en.ftl index 5a97f389c84..0516a951fa8 100644 --- a/packages/fxa-settings/src/components/Settings/ProductPromo/en.ftl +++ b/packages/fxa-settings/src/components/Settings/ProductPromo/en.ftl @@ -5,3 +5,11 @@ product-promo-monitor = product-promo-monitor-description-v2 = Find where your private info is exposed and take control # Links out to the Monitor site product-promo-monitor-cta = Get free scan + +product-promo-vpn = + .alt = { -product-mozilla-vpn } +product-promo-vpn-description = Discover an added layer of anonymous browsing and protection. +# Links out to the VPN site +product-promo-vpn-cta = Get { -product-mozilla-vpn-short } + +## diff --git a/packages/fxa-settings/src/components/Settings/ProductPromo/index.stories.tsx b/packages/fxa-settings/src/components/Settings/ProductPromo/index.stories.tsx index eec4a9d8d1d..4db6d7626ab 100644 --- a/packages/fxa-settings/src/components/Settings/ProductPromo/index.stories.tsx +++ b/packages/fxa-settings/src/components/Settings/ProductPromo/index.stories.tsx @@ -28,14 +28,14 @@ function storyWithProps(props: ProductPromoProps, storyName?: string) { export const MobilePromo = storyWithProps( { type: 'settings', - monitorPromo: { hidePromo: false }, + vpnPromo: { hidePromo: false }, }, - 'Monitor promo - Banner - mobile only' + 'VPN promo - Banner - mobile only' ); export const DesktopPromo = storyWithProps( { type: 'sidebar', - monitorPromo: { hidePromo: false }, + vpnPromo: { hidePromo: false }, }, - 'Monitor promo - Sidebar - desktop' + 'VPN promo - Sidebar - desktop' ); diff --git a/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx b/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx index 34c961a267b..1a759312269 100644 --- a/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx +++ b/packages/fxa-settings/src/components/Settings/ProductPromo/index.test.tsx @@ -15,8 +15,8 @@ jest.mock('../../../lib/glean', () => ({ __esModule: true, default: { accountPref: { - promoMonitorView: jest.fn(), - promoMonitorSubmit: jest.fn(), + promoVpnView: jest.fn(), + promoVpnSubmit: jest.fn(), }, }, })); @@ -28,32 +28,30 @@ describe('ProductPromo', () => { it('can hide the promo', async () => { renderWithLocalizationProvider( - + ); // nothing should render await waitFor(() => - expect(screen.queryByAltText('Mozilla Monitor')).toBeNull() + expect(screen.queryByAltText('Mozilla VPN')).toBeNull() ); }); it('can show promo', async () => { renderWithLocalizationProvider( - + ); await waitFor(() => expect( screen.getByText( - 'Find where your private info is exposed and take control' + 'Discover an added layer of anonymous browsing and protection.' ) ).toBeVisible() ); - expect( - screen.getByRole('link', { name: /Get free scan/i }) - ).toHaveAttribute( + expect(screen.getByRole('link', { name: /Get VPN/i })).toHaveAttribute( 'href', - 'https://monitor.mozilla.org/?utm_source=moz-account&utm_medium=referral&utm_term=settings&utm_content=get-free-scan-global&utm_campaign=settings-promo' + 'https://vpn.mozilla.org/?utm_source=moz-account&utm_medium=mozilla-websites&utm_term=settings&utm_content=vpn&utm_campaign=promo' ); }); @@ -62,9 +60,8 @@ describe('ProductPromo', () => { renderWithLocalizationProvider( ); @@ -72,49 +69,38 @@ describe('ProductPromo', () => { await waitFor(() => expect( screen.getByText( - 'Find where your private info is exposed and take control' + 'Discover an added layer of anonymous browsing and protection.' ) ).toBeVisible() ); await user.click( screen.getByRole('link', { - name: /Get free scan/i, + name: /Get VPN/i, }) ); await waitFor(() => { - expect(GleanMetrics.accountPref.promoMonitorSubmit).toHaveBeenCalledWith({ - event: { reason: 'default' }, - }); + expect(GleanMetrics.accountPref.promoVpnSubmit).toHaveBeenCalledWith(); }); }); describe('getProductPromoData', () => { - it('hides promo when Monitor is present', () => { + it('hides promo when VPN is present', () => { const result = getProductPromoData([ - { name: MozServices.Monitor }, + { name: MozServices.MozillaVPN }, ] as AttachedClient[]); expect(result).toEqual({ hidePromo: true }); }); - it('hides promo when Monitor Stage is present', () => { - const result = getProductPromoData([ - { name: MozServices.MonitorStage }, - ] as AttachedClient[]); - expect(result).toEqual({ hidePromo: true }); - }); - - it('shows promo and provides gleanEvent when Monitor not present', () => { + it('shows promo when VPN not present', () => { const result = getProductPromoData([ { name: MozServices.Default }, ] as AttachedClient[]); expect(result.hidePromo).toBe(false); - expect(result.gleanEvent).toEqual({ event: { reason: 'default' } }); }); - it('shows promo with gleanEvent when there are no attached clients', () => { + it('shows promo when there are no attached clients', () => { const result = getProductPromoData([] as AttachedClient[]); expect(result.hidePromo).toBe(false); - expect(result.gleanEvent).toEqual({ event: { reason: 'default' } }); }); }); }); diff --git a/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx b/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx index 819b8a76a17..5c8f5aba2a1 100644 --- a/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx +++ b/packages/fxa-settings/src/components/Settings/ProductPromo/index.tsx @@ -4,11 +4,11 @@ import React from 'react'; import LinkExternal from 'fxa-react/components/LinkExternal'; -import { ReactComponent as MonitorTextLogo } from './monitor-text-logo.svg'; +import { ReactComponent as VpnTextLogo } from './vpn-text-logo.svg'; import { FtlMsg } from 'fxa-react/lib/utils'; import classNames from 'classnames'; import { MozServices } from '../../../lib/types'; -import { AccountData, useConfig } from '../../../models'; +import { AccountData } from '../../../models'; import { constructHrefWithUtm } from '../../../lib/utilities'; import { LINK } from '../../../constants'; import GleanMetrics from '../../../lib/glean'; @@ -17,72 +17,62 @@ type ProductPromoType = 'sidebar' | 'settings'; export interface ProductPromoProps { type?: ProductPromoType; - monitorPromo: MonitorPromoData; + vpnPromo: VpnPromoData; } -export type MonitorPromoData = { +export type VpnPromoData = { hidePromo?: boolean; - gleanEvent?: { event: { reason: string } }; }; export function getProductPromoData( attachedClients: AccountData['attachedClients'] ) { - const hasMonitor = attachedClients.some( - ({ name }) => - name === MozServices.Monitor || name === MozServices.MonitorStage + const hasVpn = attachedClients.some( + ({ name }) => name === MozServices.MozillaVPN ); - // Existing Monitor users should not see the promo - if (hasMonitor) { + // Existing VPN users should not see the promo + if (hasVpn) { return { hidePromo: true } as const; } - const gleanEvent = { event: { reason: 'default' } }; - - return { hidePromo: false, gleanEvent }; + return { hidePromo: false }; } +const VPN_PROMO_URL = constructHrefWithUtm( + LINK.VPN, + 'mozilla-websites', + 'moz-account', + 'settings', + 'vpn', + 'promo' +); + export const ProductPromo = ({ type = 'sidebar', - monitorPromo, + vpnPromo, }: ProductPromoProps) => { - const { sentry } = useConfig(); - - if (monitorPromo.hidePromo) { + if (vpnPromo.hidePromo) { return null; } - const MONITOR_PROMO_URL = constructHrefWithUtm( - // using sentry.env because it differentiates between 'dev', 'stage' and 'prod' - // (vs 'env' which marks all hosted environments as 'production') - sentry.env !== 'stage' ? LINK.MONITOR : LINK.MONITOR_STAGE, - 'referral', - 'moz-account', - type === 'sidebar' ? 'sidebar' : 'settings', - 'get-free-scan-global', - 'settings-promo' - ); - const promoContent = ( <>

- - Find where your private info is exposed and take control + + Discover an added layer of anonymous browsing and protection.

- GleanMetrics.accountPref.promoMonitorSubmit(monitorPromo.gleanEvent) - } + onClick={() => GleanMetrics.accountPref.promoVpnSubmit()} > - Get free scan + Get VPN ); @@ -103,10 +93,10 @@ export const ProductPromo = ({ )} >

- - + diff --git a/packages/fxa-settings/src/components/Settings/ProductPromo/vpn-text-logo.svg b/packages/fxa-settings/src/components/Settings/ProductPromo/vpn-text-logo.svg new file mode 100644 index 00000000000..5bd55596695 --- /dev/null +++ b/packages/fxa-settings/src/components/Settings/ProductPromo/vpn-text-logo.svg @@ -0,0 +1 @@ + diff --git a/packages/fxa-settings/src/components/Settings/Sidebar/index.tsx b/packages/fxa-settings/src/components/Settings/Sidebar/index.tsx index bb09f5a62d2..8302f328256 100644 --- a/packages/fxa-settings/src/components/Settings/Sidebar/index.tsx +++ b/packages/fxa-settings/src/components/Settings/Sidebar/index.tsx @@ -4,7 +4,7 @@ import React from 'react'; import Nav, { NavRefProps } from '../Nav'; -import ProductPromo, { MonitorPromoData } from '../ProductPromo'; +import ProductPromo, { VpnPromoData } from '../ProductPromo'; export const SideBar = ({ profileRef, @@ -12,8 +12,8 @@ export const SideBar = ({ connectedServicesRef, linkedAccountsRef, dataCollectionRef, - monitorPromo, -}: NavRefProps & { monitorPromo?: MonitorPromoData | null }) => { + vpnPromo, +}: NavRefProps & { vpnPromo?: VpnPromoData | null }) => { // top-[7.69rem] allows the sticky nav header to align exactly with first section heading return (
@@ -26,8 +26,8 @@ export const SideBar = ({ dataCollectionRef, }} /> - {monitorPromo && !monitorPromo.hidePromo && ( - + {vpnPromo && !vpnPromo.hidePromo && ( + )}
); diff --git a/packages/fxa-settings/src/lib/glean/index.test.ts b/packages/fxa-settings/src/lib/glean/index.test.ts index efb8e1b905d..ac590165e72 100644 --- a/packages/fxa-settings/src/lib/glean/index.test.ts +++ b/packages/fxa-settings/src/lib/glean/index.test.ts @@ -866,6 +866,30 @@ describe('lib/glean', () => { sinon.assert.called(spy); }); + it('submits a ping with the account_pref_promo_vpn_view event name', async () => { + GleanMetrics.accountPref.promoVpnView(); + const spy = sandbox.spy(accountPref.promoVpnView, 'record'); + await GleanMetrics.isDone(); + sinon.assert.calledOnce(setEventNameStub); + sinon.assert.calledWith( + setEventNameStub, + 'account_pref_promo_vpn_view' + ); + sinon.assert.called(spy); + }); + + it('submits a ping with the account_pref_promo_vpn_submit event name', async () => { + GleanMetrics.accountPref.promoVpnSubmit(); + const spy = sandbox.spy(accountPref.promoVpnSubmit, 'record'); + await GleanMetrics.isDone(); + sinon.assert.calledOnce(setEventNameStub); + sinon.assert.calledWith( + setEventNameStub, + 'account_pref_promo_vpn_submit' + ); + sinon.assert.called(spy); + }); + it('submits a ping with the account_pref_bento_view event name', async () => { GleanMetrics.accountPref.bentoView(); const spy = sandbox.spy(accountPref.bentoView, 'record'); diff --git a/packages/fxa-settings/src/lib/glean/index.ts b/packages/fxa-settings/src/lib/glean/index.ts index 54a52d20e77..672af091dc2 100644 --- a/packages/fxa-settings/src/lib/glean/index.ts +++ b/packages/fxa-settings/src/lib/glean/index.ts @@ -652,6 +652,12 @@ const recordEventMetric = ( reason: gleanPingMetrics?.event?.['reason'] || '', }); break; + case 'account_pref_promo_vpn_view': + accountPref.promoVpnView.record(); + break; + case 'account_pref_promo_vpn_submit': + accountPref.promoVpnSubmit.record(); + break; case 'account_pref_mfa_guard_view': accountPref.mfaGuardView.record({ reason: gleanPingMetrics?.event?.['reason'] || '', diff --git a/packages/fxa-settings/src/lib/utilities.ts b/packages/fxa-settings/src/lib/utilities.ts index c043aba9bac..7579361e18b 100644 --- a/packages/fxa-settings/src/lib/utilities.ts +++ b/packages/fxa-settings/src/lib/utilities.ts @@ -142,12 +142,12 @@ export function resetOnce() { /** * Constructs a URL with UTM parameters appended to the query string. * - * @param {string} pathname - The base URL path. - * @param {'mozilla-websites' | 'product-partnership' | 'referral'} utmMedium - The medium through which the link is being shared. - * @param {'moz-account'} utmSource - The source of the traffic. - * @param {'bento' | 'sidebar' | 'settings' } utmTerm - The search term or keyword associated with the campaign. - * @param {'fx-desktop' | 'fx-mobile' | 'monitor' | 'monitor-free' | 'relay' | 'vpn' | 'get-free-scan-global' | 'get-year-round-protection-us' } utmContent - The specific content or product that the link is associated with. - * @param {'permanent' | 'settings-promo' | 'connect-device'} utmCampaign - The name of the marketing campaign. + * @param pathname - The base URL path. + * @param utmMedium - The medium through which the link is being shared. + * @param utmSource - The source of the traffic. + * @param utmTerm - The search term or keyword associated with the campaign. + * @param utmContent - The specific content or product that the link is associated with. + * @param utmCampaign - The name of the marketing campaign. * @returns {string} - The constructed URL with UTM parameters. */ export const constructHrefWithUtm = ( @@ -164,7 +164,7 @@ export const constructHrefWithUtm = ( | 'vpn' | 'get-free-scan-global' | 'get-year-round-protection-us', - utmCampaign?: 'permanent' | 'settings-promo' | 'connect-device' + utmCampaign?: 'permanent' | 'settings-promo' | 'promo' | 'connect-device' ) => { const pairs: [string, string | undefined | null][] = [ ['utm_source', utmSource], diff --git a/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml b/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml index 00bbaee3389..ebd8b65aa18 100644 --- a/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml +++ b/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml @@ -2961,6 +2961,40 @@ account_pref: reason: description: What plan user is using `free` or `plus` type: string + promo_vpn_view: + type: event + description: | + User sees the VPN promo on their settings page. + send_in_pings: + - events + notification_emails: + - vzare@mozilla.com + - fxa-staff@mozilla.com + bugs: + - https://mozilla-hub.atlassian.net/browse/FXA-12815 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1830504 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1844121 + expires: never + data_sensitivity: + - interaction + promo_vpn_submit: + type: event + description: | + User clicks on the Get VPN link on the VPN promotion in the account settings. + send_in_pings: + - events + notification_emails: + - vzare@mozilla.com + - fxa-staff@mozilla.com + bugs: + - https://mozilla-hub.atlassian.net/browse/FXA-12815 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1830504 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1844121 + expires: never + data_sensitivity: + - interaction bento_view: type: event description: | diff --git a/packages/fxa-shared/metrics/glean/web/accountPref.ts b/packages/fxa-shared/metrics/glean/web/accountPref.ts index f54eb5b55d7..6dcf17218bb 100644 --- a/packages/fxa-shared/metrics/glean/web/accountPref.ts +++ b/packages/fxa-shared/metrics/glean/web/accountPref.ts @@ -353,6 +353,38 @@ export const promoMonitorView = new EventMetricType<{ ['reason'] ); +/** + * User clicks on the Get VPN link on the VPN promotion in the account settings. + * + * Generated from `account_pref.promo_vpn_submit`. + */ +export const promoVpnSubmit = new EventMetricType( + { + category: 'account_pref', + name: 'promo_vpn_submit', + sendInPings: ['events'], + lifetime: 'ping', + disabled: false, + }, + [] +); + +/** + * User sees the VPN promo on their settings page. + * + * Generated from `account_pref.promo_vpn_view`. + */ +export const promoVpnView = new EventMetricType( + { + category: 'account_pref', + name: 'promo_vpn_view', + sendInPings: ['events'], + lifetime: 'ping', + disabled: false, + }, + [] +); + /** * Click on "Create" or "Change" button on account settings page to add a recovery * key to the account diff --git a/packages/fxa-shared/metrics/glean/web/index.ts b/packages/fxa-shared/metrics/glean/web/index.ts index f968fc420df..7bd258a544b 100644 --- a/packages/fxa-shared/metrics/glean/web/index.ts +++ b/packages/fxa-shared/metrics/glean/web/index.ts @@ -235,6 +235,8 @@ export const eventsMap = { help: 'account_pref_help', promoMonitorView: 'account_pref_promo_monitor_view', promoMonitorSubmit: 'account_pref_promo_monitor_submit', + promoVpnView: 'account_pref_promo_vpn_view', + promoVpnSubmit: 'account_pref_promo_vpn_submit', bentoView: 'account_pref_bento_view', bentoFirefoxDesktop: 'account_pref_bento_firefox_desktop', bentoFirefoxMobile: 'account_pref_bento_firefox_mobile',