Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/functional-tests/pages/signinPasswordlessCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ export class SigninPasswordlessCodePage extends BaseTokenCodePage {
get resendSuccessBanner() {
return this.page.getByText(/A new code was sent/);
}

get useDifferentAccountLink() {
this.checkPath();
return this.page.getByRole('link', { name: 'Use a different account' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ test.describe('severity-1 #smoke', () => {
await page.waitForURL(/signin_passwordless_code/);
await expect(signinPasswordlessCode.heading).toBeVisible();

// Get OTP code from email
// Click "Use a different account" to verify change email glean event,
// then re-enter same email to complete the flow
await signinPasswordlessCode.useDifferentAccountLink.click();
await expect(page).not.toHaveURL(/signin_passwordless_code/);
await target.emailClient.clear(email);
await signin.fillOutEmailFirstForm(email);
await page.waitForURL(/signin_passwordless_code/);

// Get the fresh OTP code (previous codes were cleared)
const code = await target.emailClient.getPasswordlessSignupCode(email);
await signinPasswordlessCode.fillOutCodeForm(code);

Expand All @@ -41,6 +49,7 @@ test.describe('severity-1 #smoke', () => {
gleanEventsHelper.assertEventOrder([
'email_first_view',
'reg_otp_view',
'reg_otp_change_email',
'reg_otp_submit',
'reg_otp_submit_success',
]);
Expand Down
1 change: 1 addition & 0 deletions packages/fxa-auth-server/lib/routes/passwordless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ class PasswordlessHandler {
this.statsd.increment('passwordless.sendCode.success', {
...getClientServiceTags(request),
isResend: String(isResend),
isNewAccount: String(isNewAccount),
});

// Record security event
Expand Down
6 changes: 6 additions & 0 deletions packages/fxa-settings/src/lib/glean/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,9 @@ const recordEventMetric = (
case 'reg_otp_email_confirmation_resend_code':
reg.otpEmailConfirmationResendCode.record();
break;
case 'reg_otp_change_email':
reg.otpChangeEmail.record();
break;
case 'login_otp_view':
login.otpView.record();
break;
Expand All @@ -663,6 +666,9 @@ const recordEventMetric = (
case 'login_otp_email_confirmation_resend_code':
login.otpEmailConfirmationResendCode.record();
break;
case 'login_otp_change_email':
login.otpChangeEmail.record();
break;
case 'error_view':
error.view.record({
reason: gleanPingMetrics?.event?.['reason'] || '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jest.mock('../../../lib/glean', () => ({
submitSuccess: jest.fn(),
error: jest.fn(),
resendCode: jest.fn(),
changeEmail: jest.fn(),
},
passwordlessReg: {
view: jest.fn(),
Expand All @@ -55,6 +56,7 @@ jest.mock('../../../lib/glean', () => ({
submitSuccess: jest.fn(),
error: jest.fn(),
resendCode: jest.fn(),
changeEmail: jest.fn(),
},
isDone: jest.fn().mockResolvedValue(undefined),
},
Expand Down Expand Up @@ -736,14 +738,20 @@ describe('SigninPasswordlessCode page', () => {
expect(mockGleanPasswordlessLogin.view).not.toHaveBeenCalled();
});

it('emits engage event on first focus of code input', async () => {
it('does not emit engage on mount', () => {
render({ isSignup: false });
expect(mockGleanPasswordlessLogin.engage).not.toHaveBeenCalled();
});

it('emits engage event on first keystroke', async () => {
render({ isSignup: false });
const user = userEvent.setup();
const input = screen.getByLabelText('Enter 8-digit code');
fireEvent.focus(input);
await user.type(input, '1');
expect(mockGleanPasswordlessLogin.engage).toHaveBeenCalledTimes(1);

// Subsequent focuses should not re-emit
fireEvent.focus(input);
// Subsequent keystrokes should not re-emit
await user.type(input, '2');
expect(mockGleanPasswordlessLogin.engage).toHaveBeenCalledTimes(1);
});

Expand Down Expand Up @@ -816,6 +824,22 @@ describe('SigninPasswordlessCode page', () => {
});
});

it('emits changeEmail event on "Use a different account" click for signin', () => {
render({ isSignup: false });
const link = screen.getByText('Use a different account');
fireEvent.click(link);
expect(mockGleanPasswordlessLogin.changeEmail).toHaveBeenCalledTimes(1);
expect(mockGleanPasswordlessReg.changeEmail).not.toHaveBeenCalled();
});

it('emits changeEmail event on "Use a different account" click for signup', () => {
render({ isSignup: true });
const link = screen.getByText('Use a different account');
fireEvent.click(link);
expect(mockGleanPasswordlessReg.changeEmail).toHaveBeenCalledTimes(1);
expect(mockGleanPasswordlessLogin.changeEmail).not.toHaveBeenCalled();
});

it('uses reg metrics for signup flow on submit', async () => {
render({ isSignup: true });
await submitCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const SigninPasswordlessCode = ({
? GleanMetrics.passwordlessReg
: GleanMetrics.passwordlessLogin;

const [hasEngaged, setHasEngaged] = useState(false);
const gleanViewTracked = useRef(false);

useEffect(() => {
Expand Down Expand Up @@ -426,6 +427,7 @@ const SigninPasswordlessCode = ({
e: React.MouseEvent<HTMLAnchorElement>
) => {
e.preventDefault();
gleanOtp.changeEmail();

// Remove email from query params if present
const searchParams = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -511,7 +513,14 @@ const SigninPasswordlessCode = ({
color: cmsInfo?.shared?.buttonColor,
text: cmsButtonText,
},
onEngageCb: () => gleanOtp.engage(),
setClearMessages: () => {
// Only log the engage event once. Note that this text box is
// autofocused, so using autofocus wouldn't be a good way to do this.
if (hasEngaged === false) {
setHasEngaged(true);
gleanOtp.engage();
}
},
Comment on lines +516 to +523
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setClearMessages is being repurposed here to record the OTP engage metric. In FormVerifyCode, setClearMessages is specifically used to clear error messages on input change (setClearMessages(true)), so passing a custom function here means code errors will no longer be cleared while the user types (and the boolean argument is ignored). It also overloads the intent of this prop and makes the engage behavior hard to reason about. Suggested fix: keep setClearMessages for its intended purpose (or omit it so FormVerifyCode falls back to setCodeErrorMessage('')), and introduce a dedicated way to emit the engage metric on first user input (e.g., add a new prop to FormVerifyCode for onFirstInputChange / onChangeCb, or update FormVerifyCode to optionally trigger onEngageCb on first change instead of focus for this page).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with this one. Is there a different way to do this?

}}
/>

Expand Down
36 changes: 36 additions & 0 deletions packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,24 @@ login:
expires: never
data_sensitivity:
- interaction
otp_change_email:
type: event
description: |
Passwordless OTP Change Email (Login)
Event that indicates the user clicked "Use a different account" on the passwordless OTP code page during login.
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-13393
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
password_reset:
create_new_recovery_key_message_click:
type: event
Expand Down Expand Up @@ -1825,6 +1843,24 @@ reg:
expires: never
data_sensitivity:
- interaction
otp_change_email:
type: event
description: |
Passwordless OTP Change Email (Registration)
Event that indicates the user clicked "Use a different account" on the passwordless OTP code page during registration.
send_in_pings:
- events
notification_emails:
- vzare@mozilla.com
- fxa-staff@mozilla.com
bugs:
- https://mozilla-hub.atlassian.net/browse/FXA-13393
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

cad_firefox:
view:
Expand Down
2 changes: 1 addition & 1 deletion packages/fxa-shared/metrics/glean/web/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

// AUTOGENERATED BY glean_parser v14.5.2. DO NOT EDIT. DO NOT COMMIT.

import StringMetricType from '@mozilla/glean/private/metrics/string';
import BooleanMetricType from '@mozilla/glean/private/metrics/boolean';
import StringMetricType from '@mozilla/glean/private/metrics/string';

/**
* The name of the framework used by the app (ie React or Backbone).
Expand Down
2 changes: 2 additions & 0 deletions packages/fxa-shared/metrics/glean/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ export const eventsMap = {
submitSuccess: 'reg_otp_submit_success',
error: 'reg_otp_submit_frontend_error',
resendCode: 'reg_otp_email_confirmation_resend_code',
changeEmail: 'reg_otp_change_email',
},

passwordlessLogin: {
Expand All @@ -257,6 +258,7 @@ export const eventsMap = {
submitSuccess: 'login_otp_submit_success',
error: 'login_otp_submit_frontend_error',
resendCode: 'login_otp_email_confirmation_resend_code',
changeEmail: 'login_otp_change_email',
},

error: {
Expand Down
18 changes: 18 additions & 0 deletions packages/fxa-shared/metrics/glean/web/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,24 @@ export const lockedAccountBannerView = new EventMetricType(
[]
);

/**
* Passwordless OTP Change Email (Login)
* Event that indicates the user clicked "Use a different account" on the
* passwordless OTP code page during login.
*
* Generated from `login.otp_change_email`.
*/
export const otpChangeEmail = new EventMetricType(
{
category: 'login',
name: 'otp_change_email',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);

/**
* Passwordless OTP Email Confirmation Resend Code (Login)
* Event that indicates the user requested a new OTP code during login.
Expand Down
18 changes: 18 additions & 0 deletions packages/fxa-shared/metrics/glean/web/reg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ export const engage = new EventMetricType<{
['reason']
);

/**
* Passwordless OTP Change Email (Registration)
* Event that indicates the user clicked "Use a different account" on the
* passwordless OTP code page during registration.
*
* Generated from `reg.otp_change_email`.
*/
export const otpChangeEmail = new EventMetricType(
{
category: 'reg',
name: 'otp_change_email',
sendInPings: ['events'],
lifetime: 'ping',
disabled: false,
},
[]
);

/**
* Passwordless OTP Email Confirmation Resend Code (Registration)
* Event that indicates the user requested a new OTP code during registration.
Expand Down
Loading