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
175 changes: 163 additions & 12 deletions packages/fxa-settings/src/pages/Index/container.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ describe('IndexContainer', () => {
queryParamModel: { email: 'test@example.com' },
validationError: null,
});
const gleanSubmitSuccessSpy = jest.spyOn(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The necessity to hoist this is up is because the event can now happen in a useEffect before any page interactions, so we need the spy first. Then, to be consistent all the tests are updated to a similar state

GleanMetrics.emailFirst,
'submitSuccess'
);
renderWithLocalizationProvider(
<IndexContainer
{...{
Expand All @@ -391,12 +395,11 @@ describe('IndexContainer', () => {
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({
Expand All @@ -423,6 +426,10 @@ describe('IndexContainer', () => {
queryParamModel: { email: 'test@example.com' },
validationError: null,
});
const gleanSubmitSuccessSpy = jest.spyOn(
GleanMetrics.emailFirst,
'submitSuccess'
);
renderWithLocalizationProvider(
<IndexContainer
{...{
Expand All @@ -436,12 +443,10 @@ describe('IndexContainer', () => {
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({
Expand Down Expand Up @@ -1043,6 +1048,152 @@ describe('IndexContainer', () => {
event: { reason: 'registration' },
});
});

it('emits submitFail with reason "login" when the user cancels account linking', async () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We didn't have tests to cover the failure state and making sure we emit the correct events, just adding them here

mockOAuthNativeIntegration();
mockUseFxAStatusResult = mockUseFxAStatus({
supportsCanLinkAccountUid: false,
});
(firefox.fxaCanLinkAccount as jest.Mock).mockResolvedValue({
ok: false,
});

const gleanSubmitFailSpy = jest.spyOn(
GleanMetrics.emailFirst,
'submitFail'
);

renderWithLocalizationProvider(
<IndexContainer
{...{
integration,
serviceName: MozServices.Default,
useFxAStatusResult: mockUseFxAStatusResult,
}}
/>
);

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(
<IndexContainer
{...{
integration,
serviceName: MozServices.Default,
useFxAStatusResult: mockUseFxAStatusResult,
}}
/>
);

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(
<IndexContainer
{...{
integration,
serviceName: MozServices.Default,
useFxAStatusResult: mockUseFxAStatusResult,
}}
/>
);

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(
<IndexContainer
{...{
integration,
serviceName: MozServices.Default,
useFxAStatusResult: mockUseFxAStatusResult,
}}
/>
);

await waitFor(() => {
expect(gleanSubmitFailSpy).toHaveBeenCalledWith({
event: { reason: 'login-auto' },
});
});
});
});

it('should redirect cached passwordless account to signin (not OTP) when sessionToken exists', async () => {
Expand Down
23 changes: 17 additions & 6 deletions packages/fxa-settings/src/pages/Index/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is the meat and potatoes, add a suffix to the event if it's not a manual submit so we can differentiate manual vs auto (from email query param)

}`,
},
});

handleSuccessNavigation(
exists,
Expand All @@ -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'
}`,
},
});
Comment thread
nshirley marked this conversation as resolved.
}
// if email verification fails, clear from params to avoid re-use
Expand Down
26 changes: 19 additions & 7 deletions packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
14 changes: 9 additions & 5 deletions packages/fxa-shared/metrics/glean/web/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand All @@ -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`.
*/
Expand Down
Loading