Merge pull request #17034 from mozilla/FXA-9529

feat(glean): Add front-end glean events for reset pwd with code
This commit is contained in:
Valerie Pomerleau 2024-05-31 15:45:14 -07:00 коммит произвёл GitHub
Родитель 317b582cfc 7d9db9b5e4
Коммит 1e25c59475
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
29 изменённых файлов: 613 добавлений и 90 удалений

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

@ -3,14 +3,24 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react'; import React from 'react';
import { screen } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
import LinkRememberPassword from '.'; import LinkRememberPassword, { LinkRememberPasswordProps } from '.';
import { MOCK_ACCOUNT } from '../../models/mocks'; import { MOCK_ACCOUNT } from '../../models/mocks';
import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils'; import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';
import { FluentBundle } from '@fluent/bundle'; import { FluentBundle } from '@fluent/bundle';
import { MOCK_CLIENT_ID, MOCK_EMAIL } from '../../pages/mocks'; import { MOCK_CLIENT_ID, MOCK_EMAIL } from '../../pages/mocks';
import { LocationProvider } from '@reach/router'; import { LocationProvider } from '@reach/router';
import userEvent from '@testing-library/user-event';
jest.mock('../../lib/glean', () => ({
__esModule: true,
default: {
passwordReset: {
emailConfirmationSignin: jest.fn(),
},
},
}));
const mockLocation = () => { const mockLocation = () => {
return { return {
@ -23,9 +33,12 @@ jest.mock('@reach/router', () => ({
useLocation: () => mockLocation(), useLocation: () => mockLocation(),
})); }));
const Subject = ({ email = MOCK_ACCOUNT.primaryEmail.email }) => ( const Subject = ({
email = MOCK_ACCOUNT.primaryEmail.email,
clickHandler,
}: Partial<LinkRememberPasswordProps>) => (
<LocationProvider> <LocationProvider>
<LinkRememberPassword {...{ email }} /> <LinkRememberPassword {...{ email, clickHandler }} />
</LocationProvider> </LocationProvider>
); );
@ -55,4 +68,17 @@ describe('LinkRememberPassword', () => {
`/?client_id=123&prefillEmail=${encodeURIComponent(MOCK_EMAIL)}` `/?client_id=123&prefillEmail=${encodeURIComponent(MOCK_EMAIL)}`
); );
}); });
it('executes a clickHandler if passed in as prop', async () => {
const mockClickHandler = jest.fn();
const user = userEvent.setup();
renderWithLocalizationProvider(<Subject clickHandler={mockClickHandler} />);
await waitFor(() =>
user.click(screen.getByRole('link', { name: /^Sign in/ }))
);
expect(mockClickHandler).toHaveBeenCalledTimes(1);
});
}); });

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

@ -3,28 +3,46 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react'; import React from 'react';
import { FtlMsg } from 'fxa-react/lib/utils'; import { FtlMsg, hardNavigate } from 'fxa-react/lib/utils';
import { useLocation } from '@reach/router'; import { useLocation } from '@reach/router';
import { isEmailValid } from 'fxa-shared/email/helpers'; import { isEmailValid } from 'fxa-shared/email/helpers';
export type LinkRememberPasswordProps = { export type LinkRememberPasswordProps = {
email?: string; email?: string;
forceAuth?: boolean; clickHandler?: () => void;
}; };
const LinkRememberPassword = ({ email }: LinkRememberPasswordProps) => { const LinkRememberPassword = ({
email,
clickHandler,
}: LinkRememberPasswordProps) => {
let linkHref: string;
const location = useLocation(); const location = useLocation();
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
let linkHref: string; params.delete('email');
params.delete('hasLinkedAccount');
params.delete('hasPassword');
params.delete('showReactApp');
if (email && isEmailValid(email)) { if (email && isEmailValid(email)) {
params.set('prefillEmail', email); params.set('prefillEmail', email);
// react and backbone signin handle email/prefill params differently so linkHref = `/?${params.toString()}`;
// go back to index - any errors (like throttling) will be shown there on submit
linkHref = `/?${params}`;
} else { } else {
linkHref = params.size > 0 ? `/?${params}` : '/'; linkHref = params.size > 0 ? `/?${params.toString()}` : '/';
} }
const handleClick = (
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) => {
event.preventDefault();
if (clickHandler) {
// additional optional click handlong behavior
clickHandler();
}
hardNavigate(linkHref);
};
return ( return (
<div className="flex flex-wrap gap-2 justify-center text-sm mt-6"> <div className="flex flex-wrap gap-2 justify-center text-sm mt-6">
<FtlMsg id="remember-password-text"> <FtlMsg id="remember-password-text">
@ -32,7 +50,12 @@ const LinkRememberPassword = ({ email }: LinkRememberPasswordProps) => {
</FtlMsg> </FtlMsg>
<FtlMsg id="remember-password-signin-link"> <FtlMsg id="remember-password-signin-link">
{/* TODO in FXA-8636 replace with Link component */} {/* TODO in FXA-8636 replace with Link component */}
<a href={linkHref} className="link-blue" id="remember-password"> <a
href={linkHref}
className="link-blue"
id="remember-password"
onClick={handleClick}
>
Sign in Sign in
</a> </a>
</FtlMsg> </FtlMsg>

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

@ -21,7 +21,7 @@ jest.mock('../../lib/metrics', () => ({
jest.mock('../../lib/glean', () => ({ jest.mock('../../lib/glean', () => ({
__esModule: true, __esModule: true,
default: { default: {
resetPassword: { createNewSuccess: jest.fn() }, passwordReset: { createNewSuccess: jest.fn() },
}, },
})); }));
@ -36,7 +36,7 @@ describe('Ready', () => {
// }); // });
beforeEach(() => { beforeEach(() => {
(GleanMetrics.resetPassword.createNewSuccess as jest.Mock).mockClear(); (GleanMetrics.passwordReset.createNewSuccess as jest.Mock).mockClear();
}); });
it('renders as expected with default values', () => { it('renders as expected with default values', () => {
@ -139,7 +139,7 @@ describe('Ready', () => {
it('emits a metrics event on render', () => { it('emits a metrics event on render', () => {
renderWithLocalizationProvider(<Ready {...{ viewName, isSignedIn }} />); renderWithLocalizationProvider(<Ready {...{ viewName, isSignedIn }} />);
expect(usePageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT); expect(usePageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT);
expect(GleanMetrics.resetPassword.createNewSuccess).toHaveBeenCalledTimes( expect(GleanMetrics.passwordReset.createNewSuccess).toHaveBeenCalledTimes(
1 1
); );
}); });

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

@ -96,7 +96,7 @@ const Ready = ({
useEffect(() => { useEffect(() => {
if (viewName === 'reset-password-confirmed') { if (viewName === 'reset-password-confirmed') {
GleanMetrics.resetPassword.createNewSuccess(); GleanMetrics.passwordReset.createNewSuccess();
} }
}, [viewName]); }, [viewName]);

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

@ -234,6 +234,9 @@ const recordEventMetric = (
case 'login_totp_code_success_view': case 'login_totp_code_success_view':
login.totpCodeSuccessView.record(); login.totpCodeSuccessView.record();
break; break;
case 'password_reset_create_new_recovery_key_message_click':
passwordReset.createNewRecoveryKeyMessageClick.record();
break;
case 'password_reset_create_new_submit': case 'password_reset_create_new_submit':
passwordReset.createNewSubmit.record(); passwordReset.createNewSubmit.record();
break; break;
@ -243,6 +246,24 @@ const recordEventMetric = (
case 'password_reset_create_new_view': case 'password_reset_create_new_view':
passwordReset.createNewView.record(); passwordReset.createNewView.record();
break; break;
case 'password_reset_email_confirmation_different_account':
passwordReset.emailConfirmationDifferentAccount.record();
break;
case 'password_reset_email_confirmation_signin':
passwordReset.emailConfirmationSignin.record();
break;
case 'password_reset_email_confirmation_submit':
passwordReset.emailConfirmationSubmit.record();
break;
case 'password_reset_email_confirmation_view':
passwordReset.emailConfirmationView.record();
break;
case 'password_reset_email_confirmation_resend_code':
passwordReset.emailConfirmationResendCode.record();
break;
case 'password_reset_recovery_key_cannot_find':
passwordReset.recoveryKeyCannotFind.record();
break;
case 'password_reset_recovery_key_create_new_submit': case 'password_reset_recovery_key_create_new_submit':
passwordReset.recoveryKeyCreateNewSubmit.record(); passwordReset.recoveryKeyCreateNewSubmit.record();
break; break;

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

@ -35,7 +35,7 @@ jest.mock('../../../lib/metrics', () => ({
logViewEvent: jest.fn(), logViewEvent: jest.fn(),
})); }));
jest.mock('../../../lib/glean', () => ({ jest.mock('../../../lib/glean', () => ({
resetPassword: { passwordReset: {
recoveryKeyView: jest.fn(), recoveryKeyView: jest.fn(),
recoveryKeySubmit: jest.fn(), recoveryKeySubmit: jest.fn(),
}, },
@ -154,7 +154,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
.spyOn(console, 'warn') .spyOn(console, 'warn')
.mockImplementation(() => {}); .mockImplementation(() => {});
(GleanMetrics.resetPassword.recoveryKeyView as jest.Mock).mockReset(); (GleanMetrics.passwordReset.recoveryKeyView as jest.Mock).mockReset();
}); });
afterEach(() => { afterEach(() => {
@ -171,7 +171,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
'The link you clicked was missing characters, and may have been broken by your email client. Copy the address carefully, and try again.' 'The link you clicked was missing characters, and may have been broken by your email client. Copy the address carefully, and try again.'
); );
expect(mockConsoleWarn).toBeCalled(); expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.recoveryKeyView).not.toHaveBeenCalled(); expect(GleanMetrics.passwordReset.recoveryKeyView).not.toHaveBeenCalled();
}); });
it('with missing code', async () => { it('with missing code', async () => {
renderSubject({ params: paramsWithMissingCode }); renderSubject({ params: paramsWithMissingCode });
@ -180,7 +180,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
name: 'Reset password link damaged', name: 'Reset password link damaged',
}); });
expect(mockConsoleWarn).toBeCalled(); expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.recoveryKeyView).not.toHaveBeenCalled(); expect(GleanMetrics.passwordReset.recoveryKeyView).not.toHaveBeenCalled();
}); });
it('with missing email', async () => { it('with missing email', async () => {
renderSubject({ params: paramsWithMissingEmail }); renderSubject({ params: paramsWithMissingEmail });
@ -189,7 +189,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
name: 'Reset password link damaged', name: 'Reset password link damaged',
}); });
expect(mockConsoleWarn).toBeCalled(); expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.recoveryKeyView).not.toHaveBeenCalled(); expect(GleanMetrics.passwordReset.recoveryKeyView).not.toHaveBeenCalled();
}); });
}); });
@ -349,8 +349,8 @@ describe('PageAccountRecoveryConfirmKey', () => {
describe('emits metrics events', () => { describe('emits metrics events', () => {
beforeEach(() => { beforeEach(() => {
(GleanMetrics.resetPassword.recoveryKeyView as jest.Mock).mockReset(); (GleanMetrics.passwordReset.recoveryKeyView as jest.Mock).mockReset();
(GleanMetrics.resetPassword.recoveryKeySubmit as jest.Mock).mockReset(); (GleanMetrics.passwordReset.recoveryKeySubmit as jest.Mock).mockReset();
}); });
afterEach(() => jest.clearAllMocks()); afterEach(() => jest.clearAllMocks());
it('on engage, submit, success', async () => { it('on engage, submit, success', async () => {
@ -361,7 +361,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
expect(logPageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT); expect(logPageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT);
expect( expect(
GleanMetrics.resetPassword.recoveryKeyView as jest.Mock GleanMetrics.passwordReset.recoveryKeyView as jest.Mock
).toHaveBeenCalledTimes(1); ).toHaveBeenCalledTimes(1);
await typeByLabelText('Enter account recovery key')(MOCK_RECOVERY_KEY); await typeByLabelText('Enter account recovery key')(MOCK_RECOVERY_KEY);
@ -388,7 +388,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
); );
expect( expect(
GleanMetrics.resetPassword.recoveryKeySubmit as jest.Mock GleanMetrics.passwordReset.recoveryKeySubmit as jest.Mock
).toHaveBeenCalledTimes(1); ).toHaveBeenCalledTimes(1);
}); });
}); });

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

@ -63,7 +63,7 @@ const AccountRecoveryConfirmKey = ({
setShowLoadingSpinner(false); setShowLoadingSpinner(false);
logPageViewEvent(viewName, REACT_ENTRYPOINT); logPageViewEvent(viewName, REACT_ENTRYPOINT);
GleanMetrics.resetPassword.recoveryKeyView(); GleanMetrics.passwordReset.recoveryKeyView();
} else { } else {
setLinkStatus(LinkStatus.expired); setLinkStatus(LinkStatus.expired);
} }
@ -193,7 +193,7 @@ const AccountRecoveryConfirmKey = ({
setIsLoading(true); setIsLoading(true);
setBannerMessage(undefined); setBannerMessage(undefined);
logViewEvent('flow', `${viewName}.submit`, REACT_ENTRYPOINT); logViewEvent('flow', `${viewName}.submit`, REACT_ENTRYPOINT);
GleanMetrics.resetPassword.recoveryKeySubmit(); GleanMetrics.passwordReset.recoveryKeySubmit();
// if the submitted key does not match the expected format, // if the submitted key does not match the expected format,
// abort before submitting to the auth server // abort before submitting to the auth server

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

@ -34,7 +34,7 @@ import GleanMetrics from '../../../lib/glean';
jest.mock('../../../lib/glean', () => ({ jest.mock('../../../lib/glean', () => ({
__esModule: true, __esModule: true,
default: { default: {
resetPassword: { passwordReset: {
recoveryKeyCreatePasswordView: jest.fn(), recoveryKeyCreatePasswordView: jest.fn(),
recoveryKeyCreatePasswordSubmit: jest.fn(), recoveryKeyCreatePasswordSubmit: jest.fn(),
}, },
@ -219,7 +219,7 @@ describe('AccountRecoveryResetPassword page', () => {
REACT_ENTRYPOINT REACT_ENTRYPOINT
); );
expect( expect(
GleanMetrics.resetPassword.recoveryKeyCreatePasswordView GleanMetrics.passwordReset.recoveryKeyCreatePasswordView
).toHaveBeenCalled(); ).toHaveBeenCalled();
}); });
@ -256,7 +256,7 @@ describe('AccountRecoveryResetPassword page', () => {
'verification.success' 'verification.success'
); );
expect( expect(
GleanMetrics.resetPassword.recoveryKeyCreatePasswordSubmit GleanMetrics.passwordReset.recoveryKeyCreatePasswordSubmit
).toHaveBeenCalled(); ).toHaveBeenCalled();
}); });

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

@ -53,7 +53,7 @@ const AccountRecoveryResetPassword = ({
integration, integration,
}: AccountRecoveryResetPasswordProps) => { }: AccountRecoveryResetPasswordProps) => {
usePageViewEvent(viewName, REACT_ENTRYPOINT); usePageViewEvent(viewName, REACT_ENTRYPOINT);
GleanMetrics.resetPassword.recoveryKeyCreatePasswordView(); GleanMetrics.passwordReset.recoveryKeyCreatePasswordView();
const account = useAccount(); const account = useAccount();
const navigate = useNavigate(); const navigate = useNavigate();
@ -199,7 +199,7 @@ const AccountRecoveryResetPassword = ({
async function onSubmit(data: AccountRecoveryResetPasswordFormData) { async function onSubmit(data: AccountRecoveryResetPasswordFormData) {
const password = data.newPassword; const password = data.newPassword;
const email = verificationInfo.email; const email = verificationInfo.email;
GleanMetrics.resetPassword.recoveryKeyCreatePasswordSubmit(); GleanMetrics.passwordReset.recoveryKeyCreatePasswordSubmit();
try { try {
const options = { const options = {

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

@ -48,7 +48,7 @@ jest.mock('../../../lib/metrics', () => ({
jest.mock('../../../lib/glean', () => ({ jest.mock('../../../lib/glean', () => ({
__esModule: true, __esModule: true,
default: { default: {
resetPassword: { createNewView: jest.fn(), createNewSubmit: jest.fn() }, passwordReset: { createNewView: jest.fn(), createNewSubmit: jest.fn() },
}, },
})); }));
@ -122,7 +122,7 @@ describe('CompleteResetPassword page', () => {
session = mockSession(true, false); session = mockSession(true, false);
(GleanMetrics.resetPassword.createNewView as jest.Mock).mockReset(); (GleanMetrics.passwordReset.createNewView as jest.Mock).mockReset();
}); });
afterEach(() => { afterEach(() => {
@ -202,7 +202,7 @@ describe('CompleteResetPassword page', () => {
'The link you clicked was missing characters, and may have been broken by your email client. Copy the address carefully, and try again.' 'The link you clicked was missing characters, and may have been broken by your email client. Copy the address carefully, and try again.'
); );
expect(mockConsoleWarn).toBeCalled(); expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.createNewView).not.toBeCalled(); expect(GleanMetrics.passwordReset.createNewView).not.toBeCalled();
}); });
it('with missing code', async () => { it('with missing code', async () => {
render(<Subject params={paramsWithMissingCode} />, account); render(<Subject params={paramsWithMissingCode} />, account);
@ -211,7 +211,7 @@ describe('CompleteResetPassword page', () => {
name: 'Reset password link damaged', name: 'Reset password link damaged',
}); });
expect(mockConsoleWarn).toBeCalled(); expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.createNewView).not.toBeCalled(); expect(GleanMetrics.passwordReset.createNewView).not.toBeCalled();
}); });
it('with missing email', async () => { it('with missing email', async () => {
render(<Subject params={paramsWithMissingEmail} />, account); render(<Subject params={paramsWithMissingEmail} />, account);
@ -220,7 +220,7 @@ describe('CompleteResetPassword page', () => {
name: 'Reset password link damaged', name: 'Reset password link damaged',
}); });
expect(mockConsoleWarn).toBeCalled(); expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.createNewView).not.toBeCalled(); expect(GleanMetrics.passwordReset.createNewView).not.toBeCalled();
}); });
}); });
@ -236,7 +236,7 @@ describe('CompleteResetPassword page', () => {
'complete-reset-password', 'complete-reset-password',
REACT_ENTRYPOINT REACT_ENTRYPOINT
); );
expect(GleanMetrics.resetPassword.createNewView).toBeCalledTimes(1); expect(GleanMetrics.passwordReset.createNewView).toBeCalledTimes(1);
}); });
describe('errors', () => { describe('errors', () => {
@ -357,7 +357,7 @@ describe('CompleteResetPassword page', () => {
expect( expect(
(account.completeResetPassword as jest.Mock).mock.calls[0] (account.completeResetPassword as jest.Mock).mock.calls[0]
).toBeTruthy(); ).toBeTruthy();
expect(GleanMetrics.resetPassword.createNewSubmit).toBeCalledTimes(1); expect(GleanMetrics.passwordReset.createNewSubmit).toBeCalledTimes(1);
}); });
it('submits with emailToHashWith if present', async () => { it('submits with emailToHashWith if present', async () => {

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

@ -154,7 +154,7 @@ const CompleteResetPassword = ({
const renderCompleteResetPassword = () => { const renderCompleteResetPassword = () => {
setShowLoadingSpinner(false); setShowLoadingSpinner(false);
logPageViewEvent(viewName, REACT_ENTRYPOINT); logPageViewEvent(viewName, REACT_ENTRYPOINT);
GleanMetrics.resetPassword.createNewView(); GleanMetrics.passwordReset.createNewView();
}; };
/* When the user clicks the confirm password reset link from their email, we check /* When the user clicks the confirm password reset link from their email, we check
@ -214,7 +214,7 @@ const CompleteResetPassword = ({
// how account password hashing works previously. // how account password hashing works previously.
const emailToUse = emailToHashWith || email; const emailToUse = emailToHashWith || email;
GleanMetrics.resetPassword.createNewSubmit(); GleanMetrics.passwordReset.createNewSubmit();
const accountResetData = await account.completeResetPassword( const accountResetData = await account.completeResetPassword(
keyStretchExperiment.queryParamModel.isV2(config), keyStretchExperiment.queryParamModel.isV2(config),

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

@ -27,7 +27,7 @@ jest.mock('../../../lib/metrics', () => ({
jest.mock('../../../lib/glean', () => ({ jest.mock('../../../lib/glean', () => ({
__esModule: true, __esModule: true,
default: { default: {
resetPassword: { passwordReset: {
recoveryKeyResetSuccessView: jest.fn(), recoveryKeyResetSuccessView: jest.fn(),
}, },
}, },
@ -135,7 +135,7 @@ describe('ResetPasswordWithRecoveryKeyVerified', () => {
REACT_ENTRYPOINT REACT_ENTRYPOINT
); );
expect( expect(
GleanMetrics.resetPassword.recoveryKeyResetSuccessView GleanMetrics.passwordReset.recoveryKeyResetSuccessView
).toHaveBeenCalled(); ).toHaveBeenCalled();
}); });
}); });

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

@ -26,7 +26,7 @@ const ResetPasswordWithRecoveryKeyVerified = ({
integration, integration,
}: ResetPasswordWithRecoveryKeyVerifiedProps & RouteComponentProps) => { }: ResetPasswordWithRecoveryKeyVerifiedProps & RouteComponentProps) => {
usePageViewEvent(viewName, REACT_ENTRYPOINT); usePageViewEvent(viewName, REACT_ENTRYPOINT);
GleanMetrics.resetPassword.recoveryKeyResetSuccessView(); GleanMetrics.passwordReset.recoveryKeyResetSuccessView();
const navigate = useNavigate(); const navigate = useNavigate();

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

@ -50,7 +50,7 @@ jest.mock('@reach/router', () => ({
jest.mock('../../lib/glean', () => ({ jest.mock('../../lib/glean', () => ({
__esModule: true, __esModule: true,
default: { resetPassword: { view: jest.fn(), submit: jest.fn() } }, default: { passwordReset: { view: jest.fn(), submit: jest.fn() } },
})); }));
const route = '/reset_password'; const route = '/reset_password';
@ -85,8 +85,8 @@ describe('PageResetPassword', () => {
// }); // });
beforeEach(() => { beforeEach(() => {
(GleanMetrics.resetPassword.view as jest.Mock).mockClear(); (GleanMetrics.passwordReset.view as jest.Mock).mockClear();
(GleanMetrics.resetPassword.submit as jest.Mock).mockClear(); (GleanMetrics.passwordReset.submit as jest.Mock).mockClear();
}); });
it('renders as expected', async () => { it('renders as expected', async () => {
@ -127,7 +127,7 @@ describe('PageResetPassword', () => {
render(<ResetPasswordWithWebIntegration />); render(<ResetPasswordWithWebIntegration />);
await screen.findByText('Reset password'); await screen.findByText('Reset password');
expect(usePageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT); expect(usePageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT);
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
}); });
it('submit success with OAuth integration', async () => { it('submit success with OAuth integration', async () => {
@ -155,7 +155,7 @@ describe('PageResetPassword', () => {
fireEvent.click(await screen.findByText('Begin reset')); fireEvent.click(await screen.findByText('Begin reset'));
}); });
expect(GleanMetrics.resetPassword.submit).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.submit).toHaveBeenCalledTimes(1);
expect(account.resetPassword).toHaveBeenCalled(); expect(account.resetPassword).toHaveBeenCalled();
@ -295,7 +295,7 @@ describe('PageResetPassword', () => {
fireEvent.click(screen.getByRole('button', { name: 'Begin reset' })); fireEvent.click(screen.getByRole('button', { name: 'Begin reset' }));
await screen.findByText('Unknown account'); await screen.findByText('Unknown account');
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
}); });
it('displays an error when rate limiting kicks in', async () => { it('displays an error when rate limiting kicks in', async () => {
@ -333,7 +333,7 @@ describe('PageResetPassword', () => {
'Youve tried too many times. Please try again in 15 minutes.' 'Youve tried too many times. Please try again in 15 minutes.'
); );
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
}); });
it('handles unexpected errors on submit', async () => { it('handles unexpected errors on submit', async () => {

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

@ -58,7 +58,7 @@ const ResetPassword = ({
const serviceName = integration.getServiceName(); const serviceName = integration.getServiceName();
useEffect(() => { useEffect(() => {
GleanMetrics.resetPassword.view(); GleanMetrics.passwordReset.view();
}, []); }, []);
const { control, getValues, handleSubmit, register } = const { control, getValues, handleSubmit, register } =
@ -154,7 +154,7 @@ const ResetPassword = ({
ftlMsgResolver.getMsg('auth-error-1011', 'Valid email required') ftlMsgResolver.getMsg('auth-error-1011', 'Valid email required')
); );
} else { } else {
GleanMetrics.resetPassword.submit(); GleanMetrics.passwordReset.submit();
submitEmail(sanitizedEmail, { submitEmail(sanitizedEmail, {
metricsContext: queryParamsToMetricsContext( metricsContext: queryParamsToMetricsContext(
flowQueryParams as unknown as Record<string, string> flowQueryParams as unknown as Record<string, string>

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

@ -16,7 +16,7 @@ const mockVerifyRecoveryKey = jest.fn((_recoveryKey: string) =>
); );
jest.mock('../../../lib/glean', () => ({ jest.mock('../../../lib/glean', () => ({
resetPassword: { passwordReset: {
recoveryKeyView: jest.fn(), recoveryKeyView: jest.fn(),
recoveryKeySubmit: jest.fn(), recoveryKeySubmit: jest.fn(),
}, },
@ -24,8 +24,8 @@ jest.mock('../../../lib/glean', () => ({
describe('AccountRecoveryConfirmKey', () => { describe('AccountRecoveryConfirmKey', () => {
beforeEach(() => { beforeEach(() => {
(GleanMetrics.resetPassword.recoveryKeyView as jest.Mock).mockReset(); (GleanMetrics.passwordReset.recoveryKeyView as jest.Mock).mockReset();
(GleanMetrics.resetPassword.recoveryKeySubmit as jest.Mock).mockReset(); (GleanMetrics.passwordReset.recoveryKeySubmit as jest.Mock).mockReset();
mockVerifyRecoveryKey.mockClear(); mockVerifyRecoveryKey.mockClear();
}); });
@ -85,7 +85,7 @@ describe('AccountRecoveryConfirmKey', () => {
expect(mockVerifyRecoveryKey).toHaveBeenCalled(); expect(mockVerifyRecoveryKey).toHaveBeenCalled();
expect( expect(
GleanMetrics.resetPassword.recoveryKeySubmit GleanMetrics.passwordReset.recoveryKeySubmit
).toHaveBeenCalledTimes(1); ).toHaveBeenCalledTimes(1);
}); });

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

@ -52,7 +52,7 @@ const AccountRecoveryConfirmKey = ({
}); });
useEffect(() => { useEffect(() => {
GleanMetrics.resetPassword.recoveryKeyView(); GleanMetrics.passwordReset.recoveryKeyView();
}, []); }, []);
const onSubmit = () => { const onSubmit = () => {
@ -64,7 +64,7 @@ const AccountRecoveryConfirmKey = ({
if (recoveryKey.length === 32 && isBase32Crockford(recoveryKey)) { if (recoveryKey.length === 32 && isBase32Crockford(recoveryKey)) {
setIsSubmitting(true); setIsSubmitting(true);
GleanMetrics.resetPassword.recoveryKeySubmit(); GleanMetrics.passwordReset.recoveryKeySubmit();
verifyRecoveryKey(recoveryKey); verifyRecoveryKey(recoveryKey);
} else { } else {
// if the submitted key does not match the expected format, // if the submitted key does not match the expected format,
@ -157,6 +157,7 @@ const AccountRecoveryConfirmKey = ({
token, token,
uid, uid,
}} }}
onClick={() => GleanMetrics.passwordReset.recoveryKeyCannotFind()}
> >
Dont have an account recovery key? Dont have an account recovery key?
</Link> </Link>

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

@ -141,7 +141,7 @@ const CompleteResetPasswordContainer = ({
const emailToUse = emailToHashWith || email; const emailToUse = emailToHashWith || email;
if (hasConfirmedRecoveryKey) { if (hasConfirmedRecoveryKey) {
GleanMetrics.resetPassword.recoveryKeyCreatePasswordSubmit(); GleanMetrics.passwordReset.recoveryKeyCreatePasswordSubmit();
const accountResetData = await resetPasswordWithRecoveryKey( const accountResetData = await resetPasswordWithRecoveryKey(
accountResetToken, accountResetToken,
emailToUse, emailToUse,
@ -153,7 +153,7 @@ const CompleteResetPasswordContainer = ({
notifyBrowserOfSignin(accountResetData); notifyBrowserOfSignin(accountResetData);
handleNavigationWithRecoveryKey(); handleNavigationWithRecoveryKey();
} else if (isResetWithoutRecoveryKey) { } else if (isResetWithoutRecoveryKey) {
GleanMetrics.resetPassword.createNewSubmit(); GleanMetrics.passwordReset.createNewSubmit();
const accountResetData = await resetPasswordWithoutRecoveryKey( const accountResetData = await resetPasswordWithoutRecoveryKey(
code, code,
emailToUse, emailToUse,

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

@ -16,7 +16,7 @@ const mockSubmitNewPassword = jest.fn((newPassword: string) =>
jest.mock('../../../lib/glean', () => ({ jest.mock('../../../lib/glean', () => ({
__esModule: true, __esModule: true,
default: { default: {
resetPassword: { passwordReset: {
createNewView: jest.fn(), createNewView: jest.fn(),
recoveryKeyCreatePasswordView: jest.fn(), recoveryKeyCreatePasswordView: jest.fn(),
}, },
@ -25,9 +25,9 @@ jest.mock('../../../lib/glean', () => ({
describe('CompleteResetPassword page', () => { describe('CompleteResetPassword page', () => {
beforeEach(() => { beforeEach(() => {
(GleanMetrics.resetPassword.createNewView as jest.Mock).mockClear(); (GleanMetrics.passwordReset.createNewView as jest.Mock).mockClear();
( (
GleanMetrics.resetPassword.recoveryKeyCreatePasswordView as jest.Mock GleanMetrics.passwordReset.recoveryKeyCreatePasswordView as jest.Mock
).mockClear(); ).mockClear();
mockSubmitNewPassword.mockClear(); mockSubmitNewPassword.mockClear();
}); });
@ -72,7 +72,7 @@ describe('CompleteResetPassword page', () => {
it('sends the expected metrics on render', () => { it('sends the expected metrics on render', () => {
renderWithLocalizationProvider(<Subject />); renderWithLocalizationProvider(<Subject />);
expect(GleanMetrics.resetPassword.createNewView).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.createNewView).toHaveBeenCalledTimes(1);
}); });
}); });
@ -118,7 +118,7 @@ describe('CompleteResetPassword page', () => {
it('sends the expected metrics on render', () => { it('sends the expected metrics on render', () => {
renderWithLocalizationProvider(<Subject hasConfirmedRecoveryKey />); renderWithLocalizationProvider(<Subject hasConfirmedRecoveryKey />);
expect( expect(
GleanMetrics.resetPassword.recoveryKeyCreatePasswordView GleanMetrics.passwordReset.recoveryKeyCreatePasswordView
).toHaveBeenCalledTimes(1); ).toHaveBeenCalledTimes(1);
}); });
}); });

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

@ -31,8 +31,8 @@ const CompleteResetPassword = ({
useEffect(() => { useEffect(() => {
hasConfirmedRecoveryKey hasConfirmedRecoveryKey
? GleanMetrics.resetPassword.recoveryKeyCreatePasswordView() ? GleanMetrics.passwordReset.recoveryKeyCreatePasswordView()
: GleanMetrics.resetPassword.createNewView(); : GleanMetrics.passwordReset.createNewView();
}, [hasConfirmedRecoveryKey]); }, [hasConfirmedRecoveryKey]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -76,6 +76,9 @@ const CompleteResetPassword = ({
to={`/account_recovery_confirm_key${location.search}`} to={`/account_recovery_confirm_key${location.search}`}
state={locationState} state={locationState}
className="link-white underline-offset-4" className="link-white underline-offset-4"
onClick={() =>
GleanMetrics.passwordReset.createNewClickRecoveryKeyMessage()
}
> >
Reset your password with your account recovery key. Reset your password with your account recovery key.
</Link> </Link>

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

@ -13,6 +13,7 @@ import {
import { ResendStatus } from '../../../lib/types'; import { ResendStatus } from '../../../lib/types';
import { useNavigateWithQuery } from '../../../lib/hooks/useNavigateWithQuery'; import { useNavigateWithQuery } from '../../../lib/hooks/useNavigateWithQuery';
import { getLocalizedErrorMessage } from '../../../lib/error-utils'; import { getLocalizedErrorMessage } from '../../../lib/error-utils';
import GleanMetrics from '../../../lib/glean';
const ConfirmResetPasswordContainer = (_: RouteComponentProps) => { const ConfirmResetPasswordContainer = (_: RouteComponentProps) => {
const [resendStatus, setResendStatus] = useState<ResendStatus>( const [resendStatus, setResendStatus] = useState<ResendStatus>(
@ -91,6 +92,7 @@ const ConfirmResetPasswordContainer = (_: RouteComponentProps) => {
setResendStatus(ResendStatus['not sent']); setResendStatus(ResendStatus['not sent']);
const options = { metricsContext }; const options = { metricsContext };
try { try {
GleanMetrics.passwordReset.emailConfirmationSubmit();
const { code, emailToHashWith, token, uid } = const { code, emailToHashWith, token, uid } =
await authClient.passwordForgotVerifyOtp(email, otpCode, options); await authClient.passwordForgotVerifyOtp(email, otpCode, options);
const { exists: recoveryKeyExists, estimatedSyncDeviceCount } = const { exists: recoveryKeyExists, estimatedSyncDeviceCount } =

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

@ -7,19 +7,52 @@
import React from 'react'; import React from 'react';
import { Subject } from './mocks'; import { Subject } from './mocks';
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
import * as utils from 'fxa-react/lib/utils';
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { MOCK_EMAIL } from '../../mocks'; import { MOCK_EMAIL } from '../../mocks';
import GleanMetrics from '../../../lib/glean';
// add Glean mocks jest.mock('../../../lib/glean', () => ({
__esModule: true,
default: {
passwordReset: {
emailConfirmationView: jest.fn(),
emailConfirmationResendCode: jest.fn(),
emailConfirmationDifferentAccount: jest.fn(),
emailConfirmationSignin: jest.fn(),
},
},
}));
jest.mock('fxa-react/lib/utils', () => ({
...jest.requireActual('fxa-react/lib/utils'),
hardNavigate: jest.fn(),
}));
const mockResendCode = jest.fn(() => Promise.resolve(true)); const mockResendCode = jest.fn(() => Promise.resolve(true));
const mockVerifyCode = jest.fn((code: string) => Promise.resolve()); const mockVerifyCode = jest.fn((code: string) => Promise.resolve());
describe('ConfirmResetPassword', () => { describe('ConfirmResetPassword', () => {
let locationAssignSpy: jest.Mock;
beforeEach(() => { beforeEach(() => {
mockResendCode.mockClear(); jest.clearAllMocks();
mockVerifyCode.mockClear();
locationAssignSpy = jest.fn();
Object.defineProperty(window, 'location', {
value: {
// mock content server url for URL constructor
origin: 'http://localhost:3030',
assign: locationAssignSpy,
},
writable: true,
});
});
afterEach(() => {
locationAssignSpy.mockRestore();
}); });
it('renders as expected', async () => { it('renders as expected', async () => {
@ -45,6 +78,18 @@ describe('ConfirmResetPassword', () => {
expect(links[2]).toHaveTextContent('Use a different account'); expect(links[2]).toHaveTextContent('Use a different account');
}); });
it('emits the expected metrics event on render', async () => {
renderWithLocalizationProvider(<Subject />);
await expect(
screen.getByRole('heading', { name: 'Check your email' })
).toBeVisible();
expect(
GleanMetrics.passwordReset.emailConfirmationView
).toHaveBeenCalledTimes(1);
});
it('submits with valid code', async () => { it('submits with valid code', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderWithLocalizationProvider(<Subject verifyCode={mockVerifyCode} />); renderWithLocalizationProvider(<Subject verifyCode={mockVerifyCode} />);
@ -66,6 +111,20 @@ describe('ConfirmResetPassword', () => {
expect(mockVerifyCode).toHaveBeenCalledWith('12345678'); expect(mockVerifyCode).toHaveBeenCalledWith('12345678');
}); });
it('handles click on signin', async () => {
const user = userEvent.setup();
renderWithLocalizationProvider(<Subject />);
const signinLink = screen.getByRole('link', {
name: 'Sign in',
});
await waitFor(() => user.click(signinLink));
expect(
GleanMetrics.passwordReset.emailConfirmationSignin
).toHaveBeenCalledTimes(1);
});
it('handles resend code', async () => { it('handles resend code', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderWithLocalizationProvider(<Subject resendCode={mockResendCode} />); renderWithLocalizationProvider(<Subject resendCode={mockResendCode} />);
@ -76,6 +135,9 @@ describe('ConfirmResetPassword', () => {
await waitFor(() => user.click(resendButton)); await waitFor(() => user.click(resendButton));
expect(mockResendCode).toHaveBeenCalledTimes(1); expect(mockResendCode).toHaveBeenCalledTimes(1);
expect(
GleanMetrics.passwordReset.emailConfirmationResendCode
).toHaveBeenCalledTimes(1);
expect( expect(
screen.getByText( screen.getByText(
@ -83,4 +145,22 @@ describe('ConfirmResetPassword', () => {
) )
).toBeVisible(); ).toBeVisible();
}); });
it('handles Use different account link', async () => {
let hardNavigateSpy: jest.SpyInstance;
hardNavigateSpy = jest
.spyOn(utils, 'hardNavigate')
.mockImplementation(() => {});
const user = userEvent.setup();
renderWithLocalizationProvider(<Subject />);
await waitFor(() =>
user.click(screen.getByRole('link', { name: /^Use a different account/ }))
);
expect(hardNavigateSpy).toHaveBeenCalledTimes(1);
expect(
GleanMetrics.passwordReset.emailConfirmationDifferentAccount
).toHaveBeenCalledTimes(1);
});
}); });

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

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react'; import React, { useEffect } from 'react';
import AppLayout from '../../../components/AppLayout'; import AppLayout from '../../../components/AppLayout';
import FormVerifyTotp from '../../../components/FormVerifyTotp'; import FormVerifyTotp from '../../../components/FormVerifyTotp';
import { ConfirmResetPasswordProps } from './interfaces'; import { ConfirmResetPasswordProps } from './interfaces';
@ -13,6 +13,7 @@ import { FtlMsg, hardNavigate } from 'fxa-react/lib/utils';
import { ResendEmailSuccessBanner } from '../../../components/Banner'; import { ResendEmailSuccessBanner } from '../../../components/Banner';
import { ResendStatus } from '../../../lib/types'; import { ResendStatus } from '../../../lib/types';
import { EmailCodeImage } from '../../../components/images'; import { EmailCodeImage } from '../../../components/images';
import GleanMetrics from '../../../lib/glean';
const ConfirmResetPassword = ({ const ConfirmResetPassword = ({
email, email,
@ -23,6 +24,10 @@ const ConfirmResetPassword = ({
setResendStatus, setResendStatus,
verifyCode, verifyCode,
}: ConfirmResetPasswordProps & RouteComponentProps) => { }: ConfirmResetPasswordProps & RouteComponentProps) => {
useEffect(() => {
GleanMetrics.passwordReset.emailConfirmationView();
}, []);
const ftlMsgResolver = useFtlMsgResolver(); const ftlMsgResolver = useFtlMsgResolver();
const location = useLocation(); const location = useLocation();
@ -39,12 +44,17 @@ const ConfirmResetPassword = ({
const handleResend = async () => { const handleResend = async () => {
setResendStatus(ResendStatus['not sent']); setResendStatus(ResendStatus['not sent']);
GleanMetrics.passwordReset.emailConfirmationResendCode();
const result = await resendCode(); const result = await resendCode();
if (result === true) { if (result === true) {
setResendStatus(ResendStatus.sent); setResendStatus(ResendStatus.sent);
} }
}; };
const signinClickHandler = () => {
GleanMetrics.passwordReset.emailConfirmationSignin();
};
return ( return (
<AppLayout> <AppLayout>
<FtlMsg id="password-reset-flow-heading"> <FtlMsg id="password-reset-flow-heading">
@ -74,7 +84,7 @@ const ConfirmResetPassword = ({
verifyCode, verifyCode,
}} }}
/> />
<LinkRememberPassword {...{ email }} /> <LinkRememberPassword {...{ email }} clickHandler={signinClickHandler} />
<div className="flex justify-between mt-8 text-sm"> <div className="flex justify-between mt-8 text-sm">
<FtlMsg id="confirm-reset-password-otp-resend-code-button"> <FtlMsg id="confirm-reset-password-otp-resend-code-button">
<button type="button" className="link-blue" onClick={handleResend}> <button type="button" className="link-blue" onClick={handleResend}>
@ -87,6 +97,7 @@ const ConfirmResetPassword = ({
className="text-sm link-blue" className="text-sm link-blue"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
GleanMetrics.passwordReset.emailConfirmationDifferentAccount();
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
// Tell content-server to stay on index and prefill the email // Tell content-server to stay on index and prefill the email
params.set('prefillEmail', email); params.set('prefillEmail', email);

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

@ -13,7 +13,7 @@ import { MOCK_EMAIL } from '../../mocks';
jest.mock('../../../lib/glean', () => ({ jest.mock('../../../lib/glean', () => ({
__esModule: true, __esModule: true,
default: { resetPassword: { view: jest.fn(), submit: jest.fn() } }, default: { passwordReset: { view: jest.fn(), submit: jest.fn() } },
})); }));
const mockRequestResetPasswordCode = jest.fn((email: string) => const mockRequestResetPasswordCode = jest.fn((email: string) =>
@ -22,9 +22,7 @@ const mockRequestResetPasswordCode = jest.fn((email: string) =>
describe('ResetPassword', () => { describe('ResetPassword', () => {
beforeEach(() => { beforeEach(() => {
(GleanMetrics.resetPassword.view as jest.Mock).mockClear(); jest.clearAllMocks();
(GleanMetrics.resetPassword.submit as jest.Mock).mockClear();
mockRequestResetPasswordCode.mockClear();
}); });
describe('renders', () => { describe('renders', () => {
@ -47,7 +45,7 @@ describe('ResetPassword', () => {
it('emits a Glean event on render', async () => { it('emits a Glean event on render', async () => {
renderWithLocalizationProvider(<Subject />); renderWithLocalizationProvider(<Subject />);
await expect(screen.getByRole('heading', { level: 1 })).toBeVisible(); await expect(screen.getByRole('heading', { level: 1 })).toBeVisible();
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
}); });
}); });
@ -68,8 +66,8 @@ describe('ResetPassword', () => {
expect(mockRequestResetPasswordCode).toBeCalledWith(MOCK_EMAIL); expect(mockRequestResetPasswordCode).toBeCalledWith(MOCK_EMAIL);
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
expect(GleanMetrics.resetPassword.submit).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.submit).toHaveBeenCalledTimes(1);
}); });
it('trims leading space in email', async () => { it('trims leading space in email', async () => {
@ -87,8 +85,8 @@ describe('ResetPassword', () => {
await waitFor(() => user.click(screen.getByRole('button'))); await waitFor(() => user.click(screen.getByRole('button')));
expect(mockRequestResetPasswordCode).toBeCalledWith(MOCK_EMAIL); expect(mockRequestResetPasswordCode).toBeCalledWith(MOCK_EMAIL);
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
expect(GleanMetrics.resetPassword.submit).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.submit).toHaveBeenCalledTimes(1);
}); });
describe('handles errors', () => { describe('handles errors', () => {
@ -103,8 +101,8 @@ describe('ResetPassword', () => {
expect(screen.getByText('Valid email required')).toBeVisible(); expect(screen.getByText('Valid email required')).toBeVisible();
expect(mockRequestResetPasswordCode).not.toBeCalled(); expect(mockRequestResetPasswordCode).not.toBeCalled();
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
expect(GleanMetrics.resetPassword.submit).not.toHaveBeenCalled(); expect(GleanMetrics.passwordReset.submit).not.toHaveBeenCalled();
}); });
it('with an invalid email', async () => { it('with an invalid email', async () => {
@ -120,8 +118,8 @@ describe('ResetPassword', () => {
expect(screen.getByText('Valid email required')).toBeVisible(); expect(screen.getByText('Valid email required')).toBeVisible();
expect(mockRequestResetPasswordCode).not.toBeCalled(); expect(mockRequestResetPasswordCode).not.toBeCalled();
expect(GleanMetrics.resetPassword.view).toHaveBeenCalledTimes(1); expect(GleanMetrics.passwordReset.view).toHaveBeenCalledTimes(1);
expect(GleanMetrics.resetPassword.submit).not.toHaveBeenCalled(); expect(GleanMetrics.passwordReset.submit).not.toHaveBeenCalled();
}); });
}); });
}); });

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

@ -32,7 +32,7 @@ const ResetPassword = ({
const ftlMsgResolver = useFtlMsgResolver(); const ftlMsgResolver = useFtlMsgResolver();
useEffect(() => { useEffect(() => {
GleanMetrics.resetPassword.view(); GleanMetrics.passwordReset.view();
}, []); }, []);
const { control, getValues, handleSubmit, register } = const { control, getValues, handleSubmit, register } =
@ -55,7 +55,7 @@ const ResetPassword = ({
ftlMsgResolver.getMsg('auth-error-1011', 'Valid email required') ftlMsgResolver.getMsg('auth-error-1011', 'Valid email required')
); );
} else { } else {
GleanMetrics.resetPassword.submit(); GleanMetrics.passwordReset.submit();
await requestResetPasswordCode(email); await requestResetPasswordCode(email);
} }
setIsSubmitting(false); setIsSubmitting(false);

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

@ -621,6 +621,24 @@ login:
data_sensitivity: data_sensitivity:
- interaction - interaction
password_reset: password_reset:
create_new_recovery_key_message_click:
type: event
description: |
Reset Password Create New Password See Recovery Key Question Click
User clicks the button for "reset your password with your recovery key"'
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-9529
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
create_new_submit: create_new_submit:
type: event type: event
description: | description: |
@ -675,6 +693,114 @@ password_reset:
expires: never expires: never
data_sensitivity: data_sensitivity:
- interaction - interaction
email_confirmation_different_account:
type: event
description: |
Forgot Password Confirmation Code Use a different account
User clicks the "use a different account" button on the "Enter Confirmation Code" screen'
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-9529
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
email_confirmation_signin:
type: event
description: |
Forgot Password Confirmation Code Sign In
User clicks the "sign in" button on the "Enter Confirmation Code" screen'
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-9529
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
email_confirmation_submit:
type: event
description: |
Forgot Password Confirmation Code Submit
User clicks the "Continue" button on the "Enter Confirmation Code" screen'
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-9529
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
email_confirmation_view:
type: event
description: |
Forgot Password Confirmation Code View
User views the "Enter Confirmation Code" screen'
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-9529
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
email_confirmation_resend_code:
type: event
description: |
Forgot Password Confirmation Code Resend
User clicks the "resend code" button on the "Enter Confirmation Code" screen'
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-9529
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
recovery_key_cannot_find:
type: event
description: |
Reset Password Can't Find Key
User clicks the "Can't find your account recovery key?" button on the confirm reccovery key page'
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-9529
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
recovery_key_create_new_submit: recovery_key_create_new_submit:
type: event type: event
description: | description: |

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

@ -0,0 +1,98 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
// AUTOGENERATED BY glean_parser v14.0.1. DO NOT EDIT. DO NOT COMMIT.
import EventMetricType from '@mozilla/glean/private/metrics/event';
/**
* User engaged on the "Connect another device" screen with choice options,
* selecting either of "I already have FF for mobile" or "I don't have FF for
* mobile", which is provided in the 'reason' for this event
*
* Generated from `cad_firefox.choice_engage`.
*/
export const choiceEngage = new EventMetricType<{
reason?: string;
}>(
{
category: 'cad_firefox',
name: 'choice_engage',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
['reason']
);
/**
* User submitted on the "Connect another device" screen with choice options,
* submitting either of "I already have FF for mobile" or "I don't have FF for
* mobile", which is provided in the 'reason' for this event
*
* Generated from `cad_firefox.choice_submit`.
*/
export const choiceSubmit = new EventMetricType<{
reason?: string;
}>(
{
category: 'cad_firefox',
name: 'choice_submit',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
['reason']
);
/**
* User viewed the "Connect another device" screen with choice options to download
* FF for mobile or not
*
* Generated from `cad_firefox.choice_view`.
*/
export const choiceView = new EventMetricType(
{
category: 'cad_firefox',
name: 'choice_view',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/**
* User clicked the "Continue to sync" button on the "Download Firefox for mobile"
* screen
*
* Generated from `cad_firefox.sync_device_submit`.
*/
export const syncDeviceSubmit = new EventMetricType(
{
category: 'cad_firefox',
name: 'sync_device_submit',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/**
* User viewed the "Download Firefox for mobile" screen after choosing and
* submitting the "I don't have Firefox for mobile" option
*
* Generated from `cad_firefox.view`.
*/
export const view = new EventMetricType(
{
category: 'cad_firefox',
name: 'view',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);

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

@ -80,14 +80,27 @@ export const eventsMap = {
success: 'login_totp_code_success_view', success: 'login_totp_code_success_view',
}, },
resetPassword: { passwordReset: {
view: 'password_reset_view', view: 'password_reset_view',
submit: 'password_reset_submit', submit: 'password_reset_submit',
createNewView: 'password_reset_create_new_view', createNewView: 'password_reset_create_new_view',
createNewSubmit: 'password_reset_create_new_submit', createNewSubmit: 'password_reset_create_new_submit',
createNewSuccess: 'password_reset_create_new_success_view', createNewSuccess: 'password_reset_create_new_success_view',
createNewClickRecoveryKeyMessage:
'password_reset_create_new_recovery_key_message_click',
emailConfirmationView: 'password_reset_email_confirmation_view',
emailConfirmationSubmit: 'password_reset_email_confirmation_submit',
emailConfirmationDifferentAccount:
'password_reset_email_confirmation_different_account',
emailConfirmationSignin: 'password_reset_email_confirmation_signin',
emailConfirmationResendCode:
'password_reset_email_confirmation_resend_code',
recoveryKeyView: 'password_reset_recovery_key_view', recoveryKeyView: 'password_reset_recovery_key_view',
recoveryKeySubmit: 'password_reset_recovery_key_submit', recoveryKeySubmit: 'password_reset_recovery_key_submit',
recoveryKeyCannotFind: 'password_reset_recovery_key_cannot_find',
recoveryKeyCreatePasswordView: recoveryKeyCreatePasswordView:
'password_reset_recovery_key_create_new_view', 'password_reset_recovery_key_create_new_view',

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

@ -6,6 +6,23 @@
import EventMetricType from '@mozilla/glean/private/metrics/event'; import EventMetricType from '@mozilla/glean/private/metrics/event';
/**
* Reset Password Create New Password See Recovery Key Question Click
* User clicks the button for "reset your password with your recovery key"'
*
* Generated from `password_reset.create_new_recovery_key_message_click`.
*/
export const createNewRecoveryKeyMessageClick = new EventMetricType(
{
category: 'password_reset',
name: 'create_new_recovery_key_message_click',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/** /**
* Create New Password Submit * Create New Password Submit
* User attemps to submit the create new password form' * User attemps to submit the create new password form'
@ -57,6 +74,110 @@ export const createNewView = new EventMetricType(
[] []
); );
/**
* Forgot Password Confirmation Code Use a different account
* User clicks the "use a different account" button on the "Enter Confirmation
* Code" screen'
*
* Generated from `password_reset.email_confirmation_different_account`.
*/
export const emailConfirmationDifferentAccount = new EventMetricType(
{
category: 'password_reset',
name: 'email_confirmation_different_account',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/**
* Forgot Password Confirmation Code Resend
* User clicks the "resend code" button on the "Enter Confirmation Code" screen'
*
* Generated from `password_reset.email_confirmation_resend_code`.
*/
export const emailConfirmationResendCode = new EventMetricType(
{
category: 'password_reset',
name: 'email_confirmation_resend_code',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/**
* Forgot Password Confirmation Code Sign In
* User clicks the "sign in" button on the "Enter Confirmation Code" screen'
*
* Generated from `password_reset.email_confirmation_signin`.
*/
export const emailConfirmationSignin = new EventMetricType(
{
category: 'password_reset',
name: 'email_confirmation_signin',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/**
* Forgot Password Confirmation Code Submit
* User clicks the "Continue" button on the "Enter Confirmation Code" screen'
*
* Generated from `password_reset.email_confirmation_submit`.
*/
export const emailConfirmationSubmit = new EventMetricType(
{
category: 'password_reset',
name: 'email_confirmation_submit',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/**
* Forgot Password Confirmation Code View
* User views the "Enter Confirmation Code" screen'
*
* Generated from `password_reset.email_confirmation_view`.
*/
export const emailConfirmationView = new EventMetricType(
{
category: 'password_reset',
name: 'email_confirmation_view',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/**
* Reset Password Can't Find Key
* User clicks the "Can't find your account recovery key?" button on the confirm
* reccovery key page'
*
* Generated from `password_reset.recovery_key_cannot_find`.
*/
export const recoveryKeyCannotFind = new EventMetricType(
{
category: 'password_reset',
name: 'recovery_key_cannot_find',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);
/** /**
* Forgot Password w/ Recovery Key Create New Password Submit * Forgot Password w/ Recovery Key Create New Password Submit
* User attempts to submit the create new password form' * User attempts to submit the create new password form'