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/. */
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 LinkRememberPassword from '.';
import LinkRememberPassword, { LinkRememberPasswordProps } from '.';
import { MOCK_ACCOUNT } from '../../models/mocks';
import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';
import { FluentBundle } from '@fluent/bundle';
import { MOCK_CLIENT_ID, MOCK_EMAIL } from '../../pages/mocks';
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 = () => {
return {
@ -23,9 +33,12 @@ jest.mock('@reach/router', () => ({
useLocation: () => mockLocation(),
}));
const Subject = ({ email = MOCK_ACCOUNT.primaryEmail.email }) => (
const Subject = ({
email = MOCK_ACCOUNT.primaryEmail.email,
clickHandler,
}: Partial<LinkRememberPasswordProps>) => (
<LocationProvider>
<LinkRememberPassword {...{ email }} />
<LinkRememberPassword {...{ email, clickHandler }} />
</LocationProvider>
);
@ -55,4 +68,17 @@ describe('LinkRememberPassword', () => {
`/?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/. */
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 { isEmailValid } from 'fxa-shared/email/helpers';
export type LinkRememberPasswordProps = {
email?: string;
forceAuth?: boolean;
clickHandler?: () => void;
};
const LinkRememberPassword = ({ email }: LinkRememberPasswordProps) => {
const LinkRememberPassword = ({
email,
clickHandler,
}: LinkRememberPasswordProps) => {
let linkHref: string;
const location = useLocation();
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)) {
params.set('prefillEmail', email);
// react and backbone signin handle email/prefill params differently so
// go back to index - any errors (like throttling) will be shown there on submit
linkHref = `/?${params}`;
linkHref = `/?${params.toString()}`;
} 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 (
<div className="flex flex-wrap gap-2 justify-center text-sm mt-6">
<FtlMsg id="remember-password-text">
@ -32,7 +50,12 @@ const LinkRememberPassword = ({ email }: LinkRememberPasswordProps) => {
</FtlMsg>
<FtlMsg id="remember-password-signin-link">
{/* 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
</a>
</FtlMsg>

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

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

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

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

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

@ -234,6 +234,9 @@ const recordEventMetric = (
case 'login_totp_code_success_view':
login.totpCodeSuccessView.record();
break;
case 'password_reset_create_new_recovery_key_message_click':
passwordReset.createNewRecoveryKeyMessageClick.record();
break;
case 'password_reset_create_new_submit':
passwordReset.createNewSubmit.record();
break;
@ -243,6 +246,24 @@ const recordEventMetric = (
case 'password_reset_create_new_view':
passwordReset.createNewView.record();
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':
passwordReset.recoveryKeyCreateNewSubmit.record();
break;

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

@ -35,7 +35,7 @@ jest.mock('../../../lib/metrics', () => ({
logViewEvent: jest.fn(),
}));
jest.mock('../../../lib/glean', () => ({
resetPassword: {
passwordReset: {
recoveryKeyView: jest.fn(),
recoveryKeySubmit: jest.fn(),
},
@ -154,7 +154,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
.spyOn(console, 'warn')
.mockImplementation(() => {});
(GleanMetrics.resetPassword.recoveryKeyView as jest.Mock).mockReset();
(GleanMetrics.passwordReset.recoveryKeyView as jest.Mock).mockReset();
});
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.'
);
expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.recoveryKeyView).not.toHaveBeenCalled();
expect(GleanMetrics.passwordReset.recoveryKeyView).not.toHaveBeenCalled();
});
it('with missing code', async () => {
renderSubject({ params: paramsWithMissingCode });
@ -180,7 +180,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
name: 'Reset password link damaged',
});
expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.recoveryKeyView).not.toHaveBeenCalled();
expect(GleanMetrics.passwordReset.recoveryKeyView).not.toHaveBeenCalled();
});
it('with missing email', async () => {
renderSubject({ params: paramsWithMissingEmail });
@ -189,7 +189,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
name: 'Reset password link damaged',
});
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', () => {
beforeEach(() => {
(GleanMetrics.resetPassword.recoveryKeyView as jest.Mock).mockReset();
(GleanMetrics.resetPassword.recoveryKeySubmit as jest.Mock).mockReset();
(GleanMetrics.passwordReset.recoveryKeyView as jest.Mock).mockReset();
(GleanMetrics.passwordReset.recoveryKeySubmit as jest.Mock).mockReset();
});
afterEach(() => jest.clearAllMocks());
it('on engage, submit, success', async () => {
@ -361,7 +361,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
expect(logPageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT);
expect(
GleanMetrics.resetPassword.recoveryKeyView as jest.Mock
GleanMetrics.passwordReset.recoveryKeyView as jest.Mock
).toHaveBeenCalledTimes(1);
await typeByLabelText('Enter account recovery key')(MOCK_RECOVERY_KEY);
@ -388,7 +388,7 @@ describe('PageAccountRecoveryConfirmKey', () => {
);
expect(
GleanMetrics.resetPassword.recoveryKeySubmit as jest.Mock
GleanMetrics.passwordReset.recoveryKeySubmit as jest.Mock
).toHaveBeenCalledTimes(1);
});
});

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

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

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

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

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

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

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

@ -48,7 +48,7 @@ jest.mock('../../../lib/metrics', () => ({
jest.mock('../../../lib/glean', () => ({
__esModule: true,
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);
(GleanMetrics.resetPassword.createNewView as jest.Mock).mockReset();
(GleanMetrics.passwordReset.createNewView as jest.Mock).mockReset();
});
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.'
);
expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.createNewView).not.toBeCalled();
expect(GleanMetrics.passwordReset.createNewView).not.toBeCalled();
});
it('with missing code', async () => {
render(<Subject params={paramsWithMissingCode} />, account);
@ -211,7 +211,7 @@ describe('CompleteResetPassword page', () => {
name: 'Reset password link damaged',
});
expect(mockConsoleWarn).toBeCalled();
expect(GleanMetrics.resetPassword.createNewView).not.toBeCalled();
expect(GleanMetrics.passwordReset.createNewView).not.toBeCalled();
});
it('with missing email', async () => {
render(<Subject params={paramsWithMissingEmail} />, account);
@ -220,7 +220,7 @@ describe('CompleteResetPassword page', () => {
name: 'Reset password link damaged',
});
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',
REACT_ENTRYPOINT
);
expect(GleanMetrics.resetPassword.createNewView).toBeCalledTimes(1);
expect(GleanMetrics.passwordReset.createNewView).toBeCalledTimes(1);
});
describe('errors', () => {
@ -357,7 +357,7 @@ describe('CompleteResetPassword page', () => {
expect(
(account.completeResetPassword as jest.Mock).mock.calls[0]
).toBeTruthy();
expect(GleanMetrics.resetPassword.createNewSubmit).toBeCalledTimes(1);
expect(GleanMetrics.passwordReset.createNewSubmit).toBeCalledTimes(1);
});
it('submits with emailToHashWith if present', async () => {

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

@ -154,7 +154,7 @@ const CompleteResetPassword = ({
const renderCompleteResetPassword = () => {
setShowLoadingSpinner(false);
logPageViewEvent(viewName, REACT_ENTRYPOINT);
GleanMetrics.resetPassword.createNewView();
GleanMetrics.passwordReset.createNewView();
};
/* 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.
const emailToUse = emailToHashWith || email;
GleanMetrics.resetPassword.createNewSubmit();
GleanMetrics.passwordReset.createNewSubmit();
const accountResetData = await account.completeResetPassword(
keyStretchExperiment.queryParamModel.isV2(config),

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

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

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

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

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

@ -50,7 +50,7 @@ jest.mock('@reach/router', () => ({
jest.mock('../../lib/glean', () => ({
__esModule: true,
default: { resetPassword: { view: jest.fn(), submit: jest.fn() } },
default: { passwordReset: { view: jest.fn(), submit: jest.fn() } },
}));
const route = '/reset_password';
@ -85,8 +85,8 @@ describe('PageResetPassword', () => {
// });
beforeEach(() => {
(GleanMetrics.resetPassword.view as jest.Mock).mockClear();
(GleanMetrics.resetPassword.submit as jest.Mock).mockClear();
(GleanMetrics.passwordReset.view as jest.Mock).mockClear();
(GleanMetrics.passwordReset.submit as jest.Mock).mockClear();
});
it('renders as expected', async () => {
@ -127,7 +127,7 @@ describe('PageResetPassword', () => {
render(<ResetPasswordWithWebIntegration />);
await screen.findByText('Reset password');
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 () => {
@ -155,7 +155,7 @@ describe('PageResetPassword', () => {
fireEvent.click(await screen.findByText('Begin reset'));
});
expect(GleanMetrics.resetPassword.submit).toHaveBeenCalledTimes(1);
expect(GleanMetrics.passwordReset.submit).toHaveBeenCalledTimes(1);
expect(account.resetPassword).toHaveBeenCalled();
@ -295,7 +295,7 @@ describe('PageResetPassword', () => {
fireEvent.click(screen.getByRole('button', { name: 'Begin reset' }));
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 () => {
@ -333,7 +333,7 @@ describe('PageResetPassword', () => {
'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 () => {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -7,19 +7,52 @@
import React from 'react';
import { Subject } from './mocks';
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 userEvent from '@testing-library/user-event';
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 mockVerifyCode = jest.fn((code: string) => Promise.resolve());
describe('ConfirmResetPassword', () => {
let locationAssignSpy: jest.Mock;
beforeEach(() => {
mockResendCode.mockClear();
mockVerifyCode.mockClear();
jest.clearAllMocks();
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 () => {
@ -45,6 +78,18 @@ describe('ConfirmResetPassword', () => {
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 () => {
const user = userEvent.setup();
renderWithLocalizationProvider(<Subject verifyCode={mockVerifyCode} />);
@ -66,6 +111,20 @@ describe('ConfirmResetPassword', () => {
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 () => {
const user = userEvent.setup();
renderWithLocalizationProvider(<Subject resendCode={mockResendCode} />);
@ -76,6 +135,9 @@ describe('ConfirmResetPassword', () => {
await waitFor(() => user.click(resendButton));
expect(mockResendCode).toHaveBeenCalledTimes(1);
expect(
GleanMetrics.passwordReset.emailConfirmationResendCode
).toHaveBeenCalledTimes(1);
expect(
screen.getByText(
@ -83,4 +145,22 @@ describe('ConfirmResetPassword', () => {
)
).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
* 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 FormVerifyTotp from '../../../components/FormVerifyTotp';
import { ConfirmResetPasswordProps } from './interfaces';
@ -13,6 +13,7 @@ import { FtlMsg, hardNavigate } from 'fxa-react/lib/utils';
import { ResendEmailSuccessBanner } from '../../../components/Banner';
import { ResendStatus } from '../../../lib/types';
import { EmailCodeImage } from '../../../components/images';
import GleanMetrics from '../../../lib/glean';
const ConfirmResetPassword = ({
email,
@ -23,6 +24,10 @@ const ConfirmResetPassword = ({
setResendStatus,
verifyCode,
}: ConfirmResetPasswordProps & RouteComponentProps) => {
useEffect(() => {
GleanMetrics.passwordReset.emailConfirmationView();
}, []);
const ftlMsgResolver = useFtlMsgResolver();
const location = useLocation();
@ -39,12 +44,17 @@ const ConfirmResetPassword = ({
const handleResend = async () => {
setResendStatus(ResendStatus['not sent']);
GleanMetrics.passwordReset.emailConfirmationResendCode();
const result = await resendCode();
if (result === true) {
setResendStatus(ResendStatus.sent);
}
};
const signinClickHandler = () => {
GleanMetrics.passwordReset.emailConfirmationSignin();
};
return (
<AppLayout>
<FtlMsg id="password-reset-flow-heading">
@ -74,7 +84,7 @@ const ConfirmResetPassword = ({
verifyCode,
}}
/>
<LinkRememberPassword {...{ email }} />
<LinkRememberPassword {...{ email }} clickHandler={signinClickHandler} />
<div className="flex justify-between mt-8 text-sm">
<FtlMsg id="confirm-reset-password-otp-resend-code-button">
<button type="button" className="link-blue" onClick={handleResend}>
@ -87,6 +97,7 @@ const ConfirmResetPassword = ({
className="text-sm link-blue"
onClick={(e) => {
e.preventDefault();
GleanMetrics.passwordReset.emailConfirmationDifferentAccount();
const params = new URLSearchParams(location.search);
// Tell content-server to stay on index and prefill the email
params.set('prefillEmail', email);

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

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

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

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

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

@ -621,6 +621,24 @@ login:
data_sensitivity:
- interaction
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:
type: event
description: |
@ -675,6 +693,114 @@ password_reset:
expires: never
data_sensitivity:
- 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:
type: event
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',
},
resetPassword: {
passwordReset: {
view: 'password_reset_view',
submit: 'password_reset_submit',
createNewView: 'password_reset_create_new_view',
createNewSubmit: 'password_reset_create_new_submit',
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',
recoveryKeySubmit: 'password_reset_recovery_key_submit',
recoveryKeyCannotFind: 'password_reset_recovery_key_cannot_find',
recoveryKeyCreatePasswordView:
'password_reset_recovery_key_create_new_view',

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

@ -6,6 +6,23 @@
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
* 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
* User attempts to submit the create new password form'