feat(glean): Enable automatic click events, add 3x GetDataTrio events

Because:
* We want to add automatic Glean click events so we don't have to edit the YAML and generate files for every click interaction
* We want events for copy, download, and print actions for 2FA recovery codes

This commit:
* Adds the click event from the Glean API to be callable from our custom GleanMetrics object, sets up the global event listener once application state has settled
* Adds click events with data-glean-id and data-glean-type for all states (setup, change, inline setup) of 2FA GetDataTrio

closes FXA-10264, closes FXA-9580, closes FXA-9581, closes FXA-9582
This commit is contained in:
Lauren Zugai 2024-08-21 14:23:28 -05:00
Родитель a1b2161d18
Коммит c2e3a067f2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 0C86B71E24811D10
13 изменённых файлов: 146 добавлений и 6 удалений

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

@ -353,6 +353,7 @@ export const GleanMetrics = {
// this ensures that events are uploaded.
maxEvents: 1,
enableAutoPageLoadEvents: true,
enableAutoElementClickEvents: true,
});
Glean.setLogPings(config.logPings);
if (config.debugViewTag) {

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

@ -174,6 +174,7 @@ describe('lib/glean', () => {
serverEndpoint: mockConfig.serverEndpoint,
maxEvents: 1,
enableAutoPageLoadEvents: true,
enableAutoElementClickEvents: true,
}
);
sinon.assert.calledWith(logPingsStub, mockConfig.logPings);

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

@ -7,6 +7,7 @@ import GetDataTrio, {
DownloadContentType,
GetDataCopySingleton,
GetDataCopySingletonInline,
GetDataTrioGleanData,
} from '../GetDataTrio';
import { Tooltip } from '../Tooltip';
import { FtlMsg } from 'fxa-react/lib/utils';
@ -30,6 +31,11 @@ export type DataBlockProps = {
isIOS?: boolean;
isInline?: boolean;
email: string;
gleanDataAttrs: {
copy?: GetDataTrioGleanData;
download?: GetDataTrioGleanData;
print?: GetDataTrioGleanData;
};
};
export const DataBlock = ({
@ -42,6 +48,7 @@ export const DataBlock = ({
isIOS = false,
isInline = false,
email,
gleanDataAttrs,
}: DataBlockProps) => {
const valueIsArray = Array.isArray(value);
const [performedAction, setPerformedAction] = useState<actions>();
@ -97,13 +104,25 @@ export const DataBlock = ({
)}
{isInline && (
<GetDataCopySingletonInline
{...{ value, onAction: actionCb, setTooltipVisible, email }}
{...{
value,
onAction: actionCb,
setTooltipVisible,
email,
gleanDataAttrs,
}}
/>
)}
</div>
{isIOS && !isInline && (
<GetDataCopySingleton
{...{ value, onAction: actionCb, setTooltipVisible, email }}
{...{
value,
onAction: actionCb,
setTooltipVisible,
email,
gleanDataAttrs,
}}
/>
)}
{!isIOS && !isInline && (
@ -114,6 +133,7 @@ export const DataBlock = ({
onAction: actionCb,
setTooltipVisible,
email,
gleanDataAttrs,
}}
/>
)}

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

@ -10,5 +10,5 @@ export const Subject = ({
value = 'ANMD 1S09 7Y2Y 4EES 02CW BJ6Z PYKP H69F',
...props // overrides
}: Partial<DataBlockProps> = {}) => (
<DataBlock email={MOCK_EMAIL} {...{ value, ...props }} />
<DataBlock email={MOCK_EMAIL} {...{ value, ...props }} gleanDataAttrs={{}} />
);

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

@ -25,6 +25,7 @@ export const Default = () => {
value="Copy that"
email={MOCK_EMAIL}
{...{ setTooltipVisible }}
gleanDataAttrs={{}}
/>
</div>
);
@ -38,6 +39,7 @@ export const SingleCopyButton = () => {
value="Copy that"
email={MOCK_EMAIL}
{...{ setTooltipVisible }}
gleanDataAttrs={{}}
/>
</div>
);
@ -51,6 +53,7 @@ export const SingleCopyButtonInline = () => {
value="Copy that"
email={MOCK_EMAIL}
{...{ setTooltipVisible }}
gleanDataAttrs={{}}
/>
</div>
);

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

@ -20,6 +20,7 @@ it('renders Trio as expected', () => {
<GetDataTrio
{...{ value, contentType, url, setTooltipVisible }}
email={MOCK_EMAIL}
gleanDataAttrs={{}}
/>
);
expect(screen.getByTestId('databutton-download')).toBeInTheDocument();
@ -37,6 +38,7 @@ it('renders single Copy button as expected', () => {
<GetDataTrio
{...{ value, contentType, url, setTooltipVisible }}
email={MOCK_EMAIL}
gleanDataAttrs={{}}
/>
);
expect(screen.getByTestId('databutton-copy')).toBeInTheDocument();

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

@ -23,18 +23,33 @@ const DownloadContentTypeL10nMapping: Record<DownloadContentType, string> = {
'Firefox account recovery key': 'get-data-trio-title-firefox-recovery-key',
};
export interface GetDataTrioGleanData {
id:
| 'two_step_auth_codes_download'
| 'two_step_auth_codes_copy'
| 'two_step_auth_codes_print'
| 'account_pref_recovery_key_copy';
type?: 'setup' | 'inline setup' | 'replace';
}
export type GetDataTrioProps = {
value: string | string[];
contentType?: DownloadContentType;
onAction?: (type: 'download' | 'copy' | 'print') => void;
setTooltipVisible: React.Dispatch<React.SetStateAction<boolean>>;
email: string;
gleanDataAttrs: {
copy?: GetDataTrioGleanData;
download?: GetDataTrioGleanData;
print?: GetDataTrioGleanData;
};
};
export const GetDataCopySingleton = ({
value,
onAction,
setTooltipVisible,
gleanDataAttrs,
}: GetDataTrioProps) => {
return (
<FtlMsg id="get-data-trio-copy-2" attrs={{ title: true, ariaLabel: true }}>
@ -50,6 +65,12 @@ export const GetDataCopySingleton = ({
onBlur={() => setTooltipVisible(false)}
data-testid="databutton-copy"
className="w-12 h-12 relative inline-block text-grey-500 rounded active:text-blue-600 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-blue-500 hover:bg-grey-50"
{...(gleanDataAttrs?.copy && {
'data-glean-id': gleanDataAttrs.copy.id,
})}
{...(gleanDataAttrs?.copy?.type && {
'data-glean-type': gleanDataAttrs.copy.type,
})}
>
<CopyIcon
aria-label="Copy"
@ -66,6 +87,7 @@ export const GetDataCopySingletonInline = ({
value,
onAction,
setTooltipVisible,
gleanDataAttrs,
}: GetDataTrioProps) => {
return (
<>
@ -84,6 +106,12 @@ export const GetDataCopySingletonInline = ({
}}
onBlur={() => setTooltipVisible(false)}
data-testid="databutton-copy"
{...(gleanDataAttrs?.copy && {
'data-glean-id': gleanDataAttrs.copy.id,
})}
{...(gleanDataAttrs?.copy?.type && {
'data-glean-type': gleanDataAttrs.copy.type,
})}
className="-my-3 -me-4 p-3 rounded text-grey-500 bg-transparent border border-transparent hover:bg-grey-100 active:bg-grey-200 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-blue-500 focus:bg-grey-50"
>
<InlineCopyIcon
@ -117,6 +145,7 @@ export const GetDataTrio = ({
onAction,
setTooltipVisible,
email,
gleanDataAttrs,
}: GetDataTrioProps) => {
const ftlMsgResolver = useFtlMsgResolver();
@ -161,6 +190,12 @@ export const GetDataTrio = ({
onBlur={() => {
setTooltipVisible(false);
}}
{...(gleanDataAttrs?.download && {
'data-glean-id': gleanDataAttrs.download.id,
})}
{...(gleanDataAttrs?.download?.type && {
'data-glean-type': gleanDataAttrs.download.type,
})}
>
<DownloadIcon
aria-label="Download"
@ -172,7 +207,7 @@ export const GetDataTrio = ({
</FtlMsg>
<GetDataCopySingleton
{...{ onAction, value, setTooltipVisible, email }}
{...{ onAction, value, setTooltipVisible, email, gleanDataAttrs }}
/>
{/** This only opens the page that is responsible
@ -193,6 +228,12 @@ export const GetDataTrio = ({
onBlur={() => setTooltipVisible(false)}
data-testid="databutton-print"
className="w-12 h-12 relative inline-block text-grey-500 rounded active:text-blue-600 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-blue-500 hover:bg-grey-50"
{...(gleanDataAttrs?.print && {
'data-glean-id': gleanDataAttrs.print.id,
})}
{...(gleanDataAttrs?.print?.type && {
'data-glean-type': gleanDataAttrs.print.type,
})}
>
<PrintIcon
aria-label="Print"

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

@ -78,6 +78,11 @@ export const FlowRecoveryKeyDownload = ({
}
isInline
{...{ email }}
gleanDataAttrs={{
copy: {
id: 'account_pref_recovery_key_copy',
},
}}
/>
<div className="bg-grey-10 p-4 rounded-lg text-grey-400 text-sm">
<FtlMsg id="flow-recovery-key-download-storage-ideas-heading-v2">

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

@ -148,6 +148,20 @@ export const Page2faReplaceRecoveryCodes = (_: RouteComponentProps) => {
onCopy={copyRecoveryCodes}
contentType="Backup authentication codes"
email={account.email}
gleanDataAttrs={{
download: {
id: 'two_step_auth_codes_download',
type: 'replace',
},
copy: {
id: 'two_step_auth_codes_copy',
type: 'replace',
},
print: {
id: 'two_step_auth_codes_print',
type: 'replace',
},
}}
/>
) : (
<LoadingSpinner />

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

@ -330,6 +330,20 @@ export const PageTwoStepAuthentication = (_: RouteComponentProps) => {
onCopy={copyRecoveryCodes}
contentType="Backup authentication codes"
email={account.email}
gleanDataAttrs={{
download: {
id: 'two_step_auth_codes_download',
type: 'setup',
},
copy: {
id: 'two_step_auth_codes_copy',
type: 'setup',
},
print: {
id: 'two_step_auth_codes_print',
type: 'setup',
},
}}
/>
</div>
</div>

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

@ -76,7 +76,8 @@ describe('lib/glean', () => {
setUtmTermStub: SinonStub,
setEntrypointExperimentStub: SinonStub,
setEntrypointVariationStub: SinonStub,
pageLoadStub: SinonStub;
pageLoadStub: SinonStub,
handleClickEvent: SinonStub;
beforeEach(async () => {
mockMetricsContext.metricsFlow = {
@ -117,6 +118,10 @@ describe('lib/glean', () => {
setEntrypointVariationStub = sandbox.stub(entrypointQuery.variation, 'set');
submitPingStub = sandbox.stub(pings.accountsEvents, 'submit');
pageLoadStub = sandbox.stub(GleanMetricsAPI.default, 'pageLoad');
handleClickEvent = sandbox.stub(
GleanMetricsAPI.default,
'handleClickEvent'
);
await testResetGlean('glean-test');
});
@ -193,6 +198,7 @@ describe('lib/glean', () => {
channel: mockConfig.channel,
serverEndpoint: mockConfig.serverEndpoint,
enableAutoPageLoadEvents: true,
enableAutoElementClickEvents: true,
}
);
sinon.assert.calledWith(logPingsStub, mockConfig.logPings);
@ -947,4 +953,12 @@ describe('lib/glean', () => {
sinon.assert.calledOnce(pageLoadStub);
});
});
describe('handleClickEvent', () => {
it('resolves', async () => {
const fakeEvent = new Event('click');
GleanMetrics.handleClickEvent(fakeEvent);
sinon.assert.calledOnce(handleClickEvent);
sinon.assert.calledWith(handleClickEvent, fakeEvent);
});
});
});

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

@ -64,6 +64,7 @@ type GleanMetricsT = {
getEnabled: () => boolean;
isDone: () => Promise<void>;
pageLoad: () => void;
handleClickEvent(event: Event): void;
} & {
[k in EventMapKeys]: { [eventKey in keyof EventsMap[k]]: PingFn };
};
@ -514,7 +515,12 @@ const createEventFn =
export const GleanMetrics: Pick<
GleanMetricsT,
'initialize' | 'setEnabled' | 'getEnabled' | 'isDone' | 'pageLoad'
| 'initialize'
| 'setEnabled'
| 'getEnabled'
| 'isDone'
| 'pageLoad'
| 'handleClickEvent'
> = {
initialize: (config: GleanMetricsConfig, context: GleanMetricsContext) => {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1859629
@ -527,6 +533,7 @@ export const GleanMetrics: Pick<
channel: config.channel,
serverEndpoint: config.serverEndpoint,
enableAutoPageLoadEvents: true,
enableAutoElementClickEvents: true,
});
Glean.setLogPings(config.logPings);
if (config.debugViewTag) {
@ -556,6 +563,10 @@ export const GleanMetrics: Pick<
GleanMetricsAPI.pageLoad();
},
handleClickEvent(event: Event) {
GleanMetricsAPI.handleClickEvent(event);
},
/**
* The ping calls are awaited internally for ease of use and that works in
* most cases. But in the scenario where we want to wait for the pings to

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

@ -202,6 +202,20 @@ const InlineRecoverySetup = ({
onCopy={copyRecoveryCodes}
contentType="Backup authentication codes"
{...{ email }}
gleanDataAttrs={{
download: {
id: 'two_step_auth_codes_download',
type: 'inline setup',
},
copy: {
id: 'two_step_auth_codes_copy',
type: 'inline setup',
},
print: {
id: 'two_step_auth_codes_print',
type: 'inline setup',
},
}}
/>
<div className="flex justify-center mt-6 mb-4 mx-auto max-w-64">
<FtlMsg id="inline-recovery-cancel-button">