diff --git a/packages/fxa-content-server/app/scripts/lib/glean/index.ts b/packages/fxa-content-server/app/scripts/lib/glean/index.ts index 84a5ff2166..d308f5094a 100644 --- a/packages/fxa-content-server/app/scripts/lib/glean/index.ts +++ b/packages/fxa-content-server/app/scripts/lib/glean/index.ts @@ -18,6 +18,7 @@ import { accountsEvents } from './pings'; import * as reg from './reg'; import { oauthClientId, service } from './relyingParty'; import { deviceType, entrypoint, flowId } from './session'; +import * as thirdPartyAuth from './thirdPartyAuth'; import * as thirdPartyAuthSetPassword from './thirdPartyAuthSetPassword'; import * as utm from './utm'; @@ -281,6 +282,12 @@ const recordEventMetric = (eventName: string, properties: EventProperties) => { case 'third_party_auth_set_password_success': thirdPartyAuthSetPassword.success.record(); break; + case 'google_deeplink': + thirdPartyAuth.googleDeeplink.record(); + break; + case 'apple_deeplink': + thirdPartyAuth.appleDeeplink.record(); + break; } }; @@ -339,6 +346,25 @@ export const GleanMetrics = { Glean.setUploadEnabled(gleanEnabled); }, + /** + * The ping calls are awaited internally for ease of use and that works in + * most cases. But in the scenario where we want to wait for the pings to + * finish before we unload the page, we are doing so, crudely, here. Do not + * emit more pings after calling this function. + */ + isDone: () => + new Promise((resolve) => { + const checkForEmptyFnList = () => { + if (lambdas.length === 0) { + resolve(undefined); + } else { + setTimeout(checkForEmptyFnList, lambdas.length * 5); + } + }; + + checkForEmptyFnList(); + }), + emailFirst: { view: createEventFn('email_first_view'), appleOauthStart: createEventFn('apple_oauth_email_first_start'), @@ -412,6 +438,10 @@ export const GleanMetrics = { submit: createEventFn('third_party_auth_set_password_submit'), success: createEventFn('third_party_auth_set_password_success'), }, + thirdPartyAuth: { + googleDeeplink: createEventFn('google_deeplink'), + appleDeeplink: createEventFn('apple_deeplink'), + }, }; export default GleanMetrics; diff --git a/packages/fxa-content-server/app/scripts/lib/glean/thirdPartyAuth.js b/packages/fxa-content-server/app/scripts/lib/glean/thirdPartyAuth.js new file mode 100644 index 0000000000..98d93d0f46 --- /dev/null +++ b/packages/fxa-content-server/app/scripts/lib/glean/thirdPartyAuth.js @@ -0,0 +1,41 @@ +/* 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.1.2. DO NOT EDIT. DO NOT COMMIT. + +import EventMetricType from '@mozilla/glean/private/metrics/event'; + +/** + * User sees Apple link and bypasses Mozilla Accounts email first/login + * screens (relevant for Pocket) + * + * Generated from `third_party_auth.apple_deeplink`. + */ +export const appleDeeplink = new EventMetricType( + { + category: 'third_party_auth', + name: 'apple_deeplink', + sendInPings: ['events'], + lifetime: 'ping', + disabled: false, + }, + [] +); + +/** + * User sees Google link and bypasses Mozilla Accounts email first/login + * screens (relevant for Pocket) + * + * Generated from `third_party_auth.google_deeplink`. + */ +export const googleDeeplink = new EventMetricType( + { + category: 'third_party_auth', + name: 'google_deeplink', + sendInPings: ['events'], + lifetime: 'ping', + disabled: false, + }, + [] +); diff --git a/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js b/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js index 6f149c0e58..fa9e90ef9a 100644 --- a/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js +++ b/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js @@ -31,14 +31,22 @@ export default { const params = new URLSearchParams(this.window.location.search); if (params.get('deeplink') === 'googleLogin') { this.logFlowEvent('google.deeplink'); - return new Promise(() => { - this.googleSignIn(); - }); + GleanMetrics.thirdPartyAuth.googleDeeplink(); + return GleanMetrics.isDone().then( + () => + new Promise(() => { + this.googleSignIn(); + }) + ); } else if (params.get('deeplink') === 'appleLogin') { this.logFlowEvent('apple.deeplink'); - return new Promise(() => { - this.appleSignIn(); - }); + GleanMetrics.thirdPartyAuth.appleDeeplink(); + return GleanMetrics.isDone().then( + () => + new Promise(() => { + this.appleSignIn(); + }) + ); } // Check to see if this page is being redirected to at the end of a diff --git a/packages/fxa-content-server/app/tests/spec/lib/glean.js b/packages/fxa-content-server/app/tests/spec/lib/glean.js index 53acf75e40..cf20446c38 100644 --- a/packages/fxa-content-server/app/tests/spec/lib/glean.js +++ b/packages/fxa-content-server/app/tests/spec/lib/glean.js @@ -107,12 +107,14 @@ describe('lib/glean', () => { }); it('does not submit a ping on an event', async () => { - await GleanMetrics.registration.view(); + GleanMetrics.registration.view(); + await GleanMetrics.isDone(); sinon.assert.notCalled(submitPingStub); }); it('does not set the metrics values', async () => { - await GleanMetrics.registration.view(); + GleanMetrics.registration.view(); + await GleanMetrics.isDone(); sinon.assert.notCalled(setOauthClientIdStub); sinon.assert.notCalled(setServiceStub); @@ -134,7 +136,8 @@ describe('lib/glean', () => { const config = { ...mockConfig, enabled: true }; const initStub = sandbox.stub(Glean, 'initialize').throws(); GleanMetrics.initialize(config, { metrics, relier, user, userAgent }); - await GleanMetrics.registration.view(); + GleanMetrics.registration.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(initStub); assert.isFalse(config.enabled); // does not try to set a value since internal enabled state is false @@ -321,7 +324,8 @@ describe('lib/glean', () => { describe('email first', () => { it('submits a ping with the email_first_view event name', async () => { - await GleanMetrics.emailFirst.view(); + GleanMetrics.emailFirst.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'email_first_view'); }); @@ -329,7 +333,8 @@ describe('lib/glean', () => { describe('apple oauth email first', () => { it('submits a ping with the apple_oauth_email_first_start event name', async () => { - await GleanMetrics.emailFirst.appleOauthStart(); + GleanMetrics.emailFirst.appleOauthStart(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -340,7 +345,8 @@ describe('lib/glean', () => { describe('google oauth email first', () => { it('submits a ping with the google_oauth_email_first_start event name', async () => { - await GleanMetrics.emailFirst.googleOauthStart(); + GleanMetrics.emailFirst.googleOauthStart(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -351,19 +357,22 @@ describe('lib/glean', () => { describe('registration', () => { it('submits a ping with the reg_view event name', async () => { - await GleanMetrics.registration.view(); + GleanMetrics.registration.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'reg_view'); }); it('submits a ping with the reg_submit event name', async () => { - await GleanMetrics.registration.submit(); + GleanMetrics.registration.submit(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'reg_submit'); }); it('submits a ping with the reg_submit_success event name', async () => { - await GleanMetrics.registration.success(); + GleanMetrics.registration.success(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'reg_submit_success'); }); @@ -371,13 +380,15 @@ describe('lib/glean', () => { describe('signup confirmation code', () => { it('submits a ping with the reg_signup_code_view event name', async () => { - await GleanMetrics.signupConfirmation.view(); + GleanMetrics.signupConfirmation.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'reg_signup_code_view'); }); it('submits a ping with the reg_signup_code_submit event name', async () => { - await GleanMetrics.signupConfirmation.submit(); + GleanMetrics.signupConfirmation.submit(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'reg_signup_code_submit'); }); @@ -385,7 +396,8 @@ describe('lib/glean', () => { describe('loginConfirmation', () => { it('submits a ping with the login_email_confirmation_view event name', async () => { - await GleanMetrics.loginConfirmation.view(); + GleanMetrics.loginConfirmation.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -394,7 +406,8 @@ describe('lib/glean', () => { }); it('submits a ping with the reg_submit event name', async () => { - await GleanMetrics.loginConfirmation.submit(); + GleanMetrics.loginConfirmation.submit(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -405,19 +418,22 @@ describe('lib/glean', () => { describe('totpForm', () => { it('submits a ping with the login_totp_form_view event name', async () => { - await GleanMetrics.totpForm.view(); + GleanMetrics.totpForm.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'login_totp_form_view'); }); it('submits a ping with the login_totp_code_submit event name', async () => { - await GleanMetrics.totpForm.submit(); + GleanMetrics.totpForm.submit(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'login_totp_code_submit'); }); it('submits a ping with the login_totp_code_success_view event name', async () => { - await GleanMetrics.totpForm.success(); + GleanMetrics.totpForm.success(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -426,27 +442,47 @@ describe('lib/glean', () => { }); }); + describe('thirdPartyAuth', () => { + it('submits a ping with the google_deeplink event name', async () => { + GleanMetrics.thirdPartyAuth.googleDeeplink(); + await GleanMetrics.isDone(); + sinon.assert.calledOnce(setEventNameStub); + sinon.assert.calledWith(setEventNameStub, 'google_deeplink'); + }); + + it('submits a ping with the apple_deeplink event name', async () => { + GleanMetrics.thirdPartyAuth.appleDeeplink(); + await GleanMetrics.isDone(); + sinon.assert.calledOnce(setEventNameStub); + sinon.assert.calledWith(setEventNameStub, 'apple_deeplink'); + }); + }); + describe('login', () => { - it('submits a ping with the login_view event name', () => { + it('submits a ping with the login_view event name', async () => { GleanMetrics.login.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'login_view'); }); - it('submits a ping with the login_submit event name', () => { + it('submits a ping with the login_submit event name', async () => { GleanMetrics.login.submit(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'login_submit'); }); - it('submits a ping with the login_submit_success event name', () => { + it('submits a ping with the login_submit_success event name', async () => { GleanMetrics.login.success(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith(setEventNameStub, 'login_submit_success'); }); - it('submits a ping with the login_submit_frontend_error event name and a reason', () => { + it('submits a ping with the login_submit_frontend_error event name and a reason', async () => { GleanMetrics.login.error({ reason: 'quux' }); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -459,7 +495,8 @@ describe('lib/glean', () => { describe('third_party_auth_set_password', () => { it('submits a ping with the third_party_auth_set_password_view event name', async () => { - await GleanMetrics.setPasswordThirdPartyAuth.view(); + GleanMetrics.setPasswordThirdPartyAuth.view(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -468,7 +505,8 @@ describe('lib/glean', () => { }); it('submits a ping with the third_party_auth_set_password_engage event name', async () => { - await GleanMetrics.setPasswordThirdPartyAuth.engage(); + GleanMetrics.setPasswordThirdPartyAuth.engage(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -477,7 +515,8 @@ describe('lib/glean', () => { }); it('submits a ping with the third_party_auth_set_password_submit event name', async () => { - await GleanMetrics.setPasswordThirdPartyAuth.submit(); + GleanMetrics.setPasswordThirdPartyAuth.submit(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, @@ -486,7 +525,8 @@ describe('lib/glean', () => { }); it('submits a ping with the third_party_auth_set_password_success event name', async () => { - await GleanMetrics.setPasswordThirdPartyAuth.success(); + GleanMetrics.setPasswordThirdPartyAuth.success(); + await GleanMetrics.isDone(); sinon.assert.calledOnce(setEventNameStub); sinon.assert.calledWith( setEventNameStub, diff --git a/packages/fxa-content-server/app/tests/spec/views/mixins/third-party-auth-mixin.js b/packages/fxa-content-server/app/tests/spec/views/mixins/third-party-auth-mixin.js index 5571d6bec6..7a9f113b0b 100644 --- a/packages/fxa-content-server/app/tests/spec/views/mixins/third-party-auth-mixin.js +++ b/packages/fxa-content-server/app/tests/spec/views/mixins/third-party-auth-mixin.js @@ -10,6 +10,7 @@ import sinon from 'sinon'; import ThirdPartyAuthMixin from 'views/mixins/third-party-auth-mixin'; import Notifier from 'lib/channels/notifier'; import Metrics from 'lib/metrics'; +import GleanMetrics from '../../../../scripts/lib/glean'; import SentryMetrics from 'lib/sentry'; import WindowMock from '../../../mocks/window'; import Storage from 'lib/storage'; @@ -83,9 +84,16 @@ describe('views/mixins/third-party-auth-mixin', function () { flowBeginTime: '456', deviceId: '789', })); + sinon.stub(GleanMetrics.thirdPartyAuth, 'appleDeeplink'); + sinon.stub(GleanMetrics.thirdPartyAuth, 'googleDeeplink'); await view.render(); }); + afterEach(() => { + GleanMetrics.thirdPartyAuth.appleDeeplink.restore(); + GleanMetrics.thirdPartyAuth.googleDeeplink.restore(); + }); + describe('beforeRender', () => { beforeEach(() => { sinon.spy(view, 'logViewEvent'); @@ -124,16 +132,26 @@ describe('views/mixins/third-party-auth-mixin', function () { it('google login deeplink', () => { windowMock.location.search = '?deeplink=googleLogin'; - view.beforeRender(); - assert.isTrue(view.googleSignIn.calledOnce); - assert.isTrue(view.logFlowEvent.calledOnceWith('google.deeplink')); + view + .beforeRender() + .then(() => { + assert.isTrue(view.googleSignIn.calledOnce); + assert.isTrue(view.logFlowEvent.calledOnceWith('google.deeplink')); + assert.isTrue(GleanMetrics.thirdPartyAuth.googleDeeplink.calledOnce); + }) + .catch(() => assert.fail()); }); it('apple login deeplink', () => { windowMock.location.search = '?deeplink=appleLogin'; - view.beforeRender(); - assert.isTrue(view.appleSignIn.calledOnce); - assert.isTrue(view.logFlowEvent.calledOnceWith('apple.deeplink')); + view + .beforeRender() + .then(() => { + assert.isTrue(view.appleSignIn.calledOnce); + assert.isTrue(view.logFlowEvent.calledOnceWith('apple.deeplink')); + assert.isTrue(GleanMetrics.thirdPartyAuth.appleDeeplink.calledOnce); + }) + .catch(() => assert.fail()); }); }); diff --git a/packages/fxa-settings/src/lib/glean/index.ts b/packages/fxa-settings/src/lib/glean/index.ts index 450a540741..e9770665c2 100644 --- a/packages/fxa-settings/src/lib/glean/index.ts +++ b/packages/fxa-settings/src/lib/glean/index.ts @@ -400,7 +400,7 @@ export const GleanMetrics: Pick< if (lambdas.length === 0) { resolve(); } else { - setTimeout(checkForEmptyFnList, 100); + setTimeout(checkForEmptyFnList, lambdas.length * 5); } }; diff --git a/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml b/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml index 071e623e0f..c84460db7e 100644 --- a/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml +++ b/packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml @@ -1519,7 +1519,45 @@ cad: expires: never data_sensitivity: - interaction - +third_party_auth: + apple_deeplink: + type: event + description: | + User clicked Apple Signin link from an RP hosted site and is taken + directly to Apple authentication flow, bypassing Mozilla Accounts email + first page. + send_in_pings: + - events + notification_emails: + - vzare@mozilla.com + - fxa-staff@mozilla.com + bugs: + - https://mozilla-hub.atlassian.net/browse/FXA-9116 + 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 + google_deeplink: + type: event + description: | + User clicked Google Signin link from an RP hosted site and is taken + directly to Google authentication flow, bypassing Mozilla Accounts email + first page. + send_in_pings: + - events + notification_emails: + - vzare@mozilla.com + - fxa-staff@mozilla.com + bugs: + - https://mozilla-hub.atlassian.net/browse/FXA-9116 + 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 third_party_auth_set_password: view: type: event