diff --git a/packages/fxa-settings/src/pages/Index/container.test.tsx b/packages/fxa-settings/src/pages/Index/container.test.tsx index c6b2957db6f..7d001ffaf93 100644 --- a/packages/fxa-settings/src/pages/Index/container.test.tsx +++ b/packages/fxa-settings/src/pages/Index/container.test.tsx @@ -378,6 +378,10 @@ describe('IndexContainer', () => { queryParamModel: { email: 'test@example.com' }, validationError: null, }); + const gleanSubmitSuccessSpy = jest.spyOn( + GleanMetrics.emailFirst, + 'submitSuccess' + ); renderWithLocalizationProvider( { await waitFor(() => { expect(mockNavigate).toHaveBeenCalledTimes(1); }); - // Glean event not emitted on automatic redirect, only on successful manual submission - const gleanSubmitSuccessSpy = jest.spyOn( - GleanMetrics.emailFirst, - 'submitSuccess' - ); - expect(gleanSubmitSuccessSpy).not.toHaveBeenCalled(); + // Auto-submit emits with the '-auto' reason suffix, + // so we can differentiate from manual submission + expect(gleanSubmitSuccessSpy).toHaveBeenCalledWith({ + event: { reason: 'login-auto' }, + }); const [calledUrl, options] = mockNavigate.mock.calls[0]; expect(calledUrl).toMatch(/\/signin$/); expect(options).toEqual({ @@ -423,6 +426,10 @@ describe('IndexContainer', () => { queryParamModel: { email: 'test@example.com' }, validationError: null, }); + const gleanSubmitSuccessSpy = jest.spyOn( + GleanMetrics.emailFirst, + 'submitSuccess' + ); renderWithLocalizationProvider( { await waitFor(() => { expect(mockNavigate).toHaveBeenCalledTimes(1); }); - // Glean event not emitted on automatic redirect, only on successful manual submission - const gleanSubmitSuccessSpy = jest.spyOn( - GleanMetrics.emailFirst, - 'submitSuccess' - ); - expect(gleanSubmitSuccessSpy).not.toHaveBeenCalled(); + + expect(gleanSubmitSuccessSpy).toHaveBeenCalledWith({ + event: { reason: 'registration-auto' }, + }); const [calledUrl, options] = mockNavigate.mock.calls[0]; expect(calledUrl).toMatch(/\/signup$/); expect(options).toEqual({ @@ -1043,6 +1048,152 @@ describe('IndexContainer', () => { event: { reason: 'registration' }, }); }); + + it('emits submitFail with reason "login" when the user cancels account linking', async () => { + mockOAuthNativeIntegration(); + mockUseFxAStatusResult = mockUseFxAStatus({ + supportsCanLinkAccountUid: false, + }); + (firefox.fxaCanLinkAccount as jest.Mock).mockResolvedValue({ + ok: false, + }); + + const gleanSubmitFailSpy = jest.spyOn( + GleanMetrics.emailFirst, + 'submitFail' + ); + + renderWithLocalizationProvider( + + ); + + await waitFor(() => { + expect(currentIndexProps?.processEmailSubmission).toBeDefined(); + }); + + await act(async () => { + await currentIndexProps?.processEmailSubmission(MOCK_EMAIL); + }); + + expect(gleanSubmitFailSpy).toHaveBeenCalledWith({ + event: { reason: 'login' }, + }); + }); + + it('emits submitFail with reason "registration-auto" when auto-submit fails domain validation', async () => { + mockUseValidatedQueryParams.mockReturnValue({ + queryParamModel: { email: 'test@example.com' }, + validationError: null, + }); + mockUseAuthClient.mockReturnValue({ + accountStatusByEmail: jest.fn().mockResolvedValue({ + exists: false, + hasLinkedAccount: false, + hasPassword: false, + }), + }); + (checkEmailDomain as jest.Mock).mockRejectedValueOnce( + AuthUiErrors.INVALID_EMAIL_DOMAIN + ); + + const gleanSubmitFailSpy = jest.spyOn( + GleanMetrics.emailFirst, + 'submitFail' + ); + + renderWithLocalizationProvider( + + ); + + await waitFor(() => { + expect(gleanSubmitFailSpy).toHaveBeenCalledWith({ + event: { reason: 'registration-auto' }, + }); + }); + }); + + it('does not emit submitFail when accountStatusByEmail rejects before accountExists is known', async () => { + // If we reach the catch before accountStatusByEmail resolves we can't + // attribute the failure to login vs registration, so the event should + // be skipped rather than recording a misleading reason. + mockUseAuthClient.mockReturnValue({ + accountStatusByEmail: jest + .fn() + .mockRejectedValue(new Error('network error')), + }); + + const gleanSubmitFailSpy = jest.spyOn( + GleanMetrics.emailFirst, + 'submitFail' + ); + + renderWithLocalizationProvider( + + ); + + await waitFor(() => { + expect(currentIndexProps?.processEmailSubmission).toBeDefined(); + }); + + await act(async () => { + await currentIndexProps?.processEmailSubmission(MOCK_EMAIL); + }); + + expect(gleanSubmitFailSpy).not.toHaveBeenCalled(); + }); + + it('emits submitFail with reason "login-auto" when auto-submit hits a canceled can_link_account', async () => { + mockOAuthNativeIntegration(); + mockUseFxAStatusResult = mockUseFxAStatus({ + supportsCanLinkAccountUid: false, + }); + mockUseValidatedQueryParams.mockReturnValue({ + queryParamModel: { email: MOCK_EMAIL }, + validationError: null, + }); + (firefox.fxaCanLinkAccount as jest.Mock).mockResolvedValue({ + ok: false, + }); + + const gleanSubmitFailSpy = jest.spyOn( + GleanMetrics.emailFirst, + 'submitFail' + ); + + renderWithLocalizationProvider( + + ); + + await waitFor(() => { + expect(gleanSubmitFailSpy).toHaveBeenCalledWith({ + event: { reason: 'login-auto' }, + }); + }); + }); }); it('should redirect cached passwordless account to signin (not OTP) when sessionToken exists', async () => { diff --git a/packages/fxa-settings/src/pages/Index/container.tsx b/packages/fxa-settings/src/pages/Index/container.tsx index 37301f8641b..c4bf70dfccd 100644 --- a/packages/fxa-settings/src/pages/Index/container.tsx +++ b/packages/fxa-settings/src/pages/Index/container.tsx @@ -328,10 +328,13 @@ const IndexContainer = ({ canLinkAccountOk = true; } - isManualSubmission && - GleanMetrics.emailFirst.submitSuccess({ - event: { reason: accountExists ? 'login' : 'registration' }, - }); + GleanMetrics.emailFirst.submitSuccess({ + event: { + reason: `${accountExists ? 'login' : 'registration'}${ + isManualSubmission ? '' : '-auto' + }`, + }, + }); handleSuccessNavigation( exists, @@ -342,9 +345,17 @@ const IndexContainer = ({ passwordlessSupported ); } catch (error) { - if (isManualSubmission && isEmail(email)) { + // If we reach the catch before accountStatusByEmail resolved (e.g. a + // network error), accountExists is undefined and we can't attribute + // the failure to login vs registration. Skip the event in that case + // rather than recording a misleading reason. + if (isEmail(email) && typeof accountExists === 'boolean') { GleanMetrics.emailFirst.submitFail({ - event: { reason: accountExists ? 'login' : 'registration' }, + event: { + reason: `${accountExists ? 'login' : 'registration'}${ + isManualSubmission ? '' : '-auto' + }`, + }, }); } // if email verification fails, clear from params to avoid re-use diff --git a/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml b/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml index f24eae485b2..23b492f799a 100644 --- a/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml +++ b/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml @@ -589,8 +589,10 @@ email: first_submit_success: type: event description: | - User clicked on the button to submit an email on the email first page - and is navigated onwards to sign in or sign up. + An email was submitted on the email first page (either by the user + clicking the submit button, or auto-submitted when an email is + supplied via query params or a cached account) and the flow is + navigated onwards to sign in or sign up. send_in_pings: - events notification_emails: @@ -606,14 +608,20 @@ email: - interaction extra_keys: reason: - description: additional information - 'login' or 'registration' + description: | + additional information - 'login', 'registration', 'login-auto', + or 'registration-auto'. The '-auto' suffix indicates the email + was auto-submitted from query params or a cached account rather + than via direct user submission. type: string first_submit_fail: type: event description: | - The user clicked on the button to submit their email on the email-first page - but email submission failed, likely due to a failed email domain check. - The user is not navigated onwards to sign in or sign up. + An email was submitted on the email-first page (either by the user + clicking the submit button, or auto-submitted when an email is + supplied via query params or a cached account) but email submission + failed, likely due to a failed email domain check. The user is not + navigated onwards to sign in or sign up. send_in_pings: - events notification_emails: @@ -629,7 +637,11 @@ email: - interaction extra_keys: reason: - description: additional information - 'login' or 'registration' + description: | + additional information - 'login', 'registration', 'login-auto', + or 'registration-auto'. The '-auto' suffix indicates the email + was auto-submitted from query params or a cached account rather + than via direct user submission. type: string login: email_confirmation_submit: diff --git a/packages/fxa-shared/metrics/glean/web/email.ts b/packages/fxa-shared/metrics/glean/web/email.ts index dee44b7fb63..b6fd235c149 100644 --- a/packages/fxa-shared/metrics/glean/web/email.ts +++ b/packages/fxa-shared/metrics/glean/web/email.ts @@ -55,9 +55,11 @@ export const firstGoogleOauthStart = new EventMetricType( ); /** - * The user clicked on the button to submit their email on the email-first page - * but email submission failed, likely due to a failed email domain check. - * The user is not navigated onwards to sign in or sign up. + * An email was submitted on the email-first page (either by the user + * clicking the submit button, or auto-submitted when an email is + * supplied via query params or a cached account) but email submission + * failed, likely due to a failed email domain check. The user is not + * navigated onwards to sign in or sign up. * * Generated from `email.first_submit_fail`. */ @@ -75,8 +77,10 @@ export const firstSubmitFail = new EventMetricType<{ ); /** - * User clicked on the button to submit an email on the email first page - * and is navigated onwards to sign in or sign up. + * An email was submitted on the email first page (either by the user + * clicking the submit button, or auto-submitted when an email is + * supplied via query params or a cached account) and the flow is + * navigated onwards to sign in or sign up. * * Generated from `email.first_submit_success`. */