diff --git a/packages/fxa-auth-server/lib/metrics/glean/index.ts b/packages/fxa-auth-server/lib/metrics/glean/index.ts index dcbf28b14b..0cdd1430e8 100644 --- a/packages/fxa-auth-server/lib/metrics/glean/index.ts +++ b/packages/fxa-auth-server/lib/metrics/glean/index.ts @@ -235,6 +235,12 @@ const createEventFn = linking: metricsData?.reason === 'linking', }); break; + + case 'third_party_auth_set_password_complete': + gleanServerEventLogger.recordThirdPartyAuthSetPasswordComplete( + commonMetrics + ); + break; } await gleanEventLogger.record({ @@ -302,6 +308,9 @@ export function gleanMetrics(config: ConfigType) { thirdPartyAuth: { googleLoginComplete: createEventFn('google_login_complete'), + setPasswordComplete: createEventFn( + 'third_party_auth_set_password_complete' + ), }, }; } diff --git a/packages/fxa-auth-server/lib/metrics/glean/server_events.ts b/packages/fxa-auth-server/lib/metrics/glean/server_events.ts index 1086be5284..9336f4a7be 100644 --- a/packages/fxa-auth-server/lib/metrics/glean/server_events.ts +++ b/packages/fxa-auth-server/lib/metrics/glean/server_events.ts @@ -1911,6 +1911,76 @@ class EventsServerEventLogger { event, }); } + /** + * Record and submit a third_party_auth_set_password_complete event: + * Password for third party user was successfully created + * Event is logged using internal mozlog logger. + * + * @param {string} user_agent - The user agent. + * @param {string} ip_address - The IP address. Will be used to decode Geo + * information and scrubbed at ingestion. + * @param {string} account_user_id_sha256 - A hex string of a sha256 hash of the account's uid. + * @param {string} relying_party_oauth_client_id - The client id of the relying party. + * @param {string} relying_party_service - The service name of the relying party. + * @param {string} session_device_type - one of 'mobile', 'tablet', or ''. + * @param {string} session_entrypoint - entrypoint to the service. + * @param {string} session_flow_id - an ID generated by FxA for its flow metrics. + * @param {string} utm_campaign - A marketing campaign. For example, if a user signs into FxA from selecting a Mozilla VPN plan on Mozilla VPN's product site, then the value of this metric could be 'vpn-product-page'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters. The special value of 'page+referral+-+not+part+of+a+campaign' is also allowed.. + * @param {string} utm_content - The content on which the user acted. For example, if the user clicked on the (previously available) "Get started here" link in "Looking for Firefox Sync? Get started here", then the value for this metric would be 'fx-sync-get-started'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters.. + * @param {string} utm_medium - The "medium" on which the user acted. For example, if the user clicked on a link in an email, then the value of this metric would be 'email'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters.. + * @param {string} utm_source - The source from where the user started. For example, if the user clicked on a link on the Mozilla accounts web site, this value could be 'fx-website'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters.. + * @param {string} utm_term - This metric is similar to the `utm.source`; it is used in the Firefox browser. For example, if the user started from about:welcome, then the value could be 'aboutwelcome-default-screen'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters.. + */ + recordThirdPartyAuthSetPasswordComplete({ + user_agent, + ip_address, + account_user_id_sha256, + relying_party_oauth_client_id, + relying_party_service, + session_device_type, + session_entrypoint, + session_flow_id, + utm_campaign, + utm_content, + utm_medium, + utm_source, + utm_term, + }: { + user_agent: string; + ip_address: string; + account_user_id_sha256: string; + relying_party_oauth_client_id: string; + relying_party_service: string; + session_device_type: string; + session_entrypoint: string; + session_flow_id: string; + utm_campaign: string; + utm_content: string; + utm_medium: string; + utm_source: string; + utm_term: string; + }) { + const event = { + category: 'third_party_auth_set_password', + name: 'complete', + }; + this.#record({ + user_agent, + ip_address, + account_user_id_sha256, + relying_party_oauth_client_id, + relying_party_service, + session_device_type, + session_entrypoint, + session_flow_id, + utm_campaign, + utm_content, + utm_medium, + utm_source, + utm_term, + event, + }); + } } /** diff --git a/packages/fxa-auth-server/lib/routes/password.ts b/packages/fxa-auth-server/lib/routes/password.ts index a89d63e672..5e68fadfbe 100644 --- a/packages/fxa-auth-server/lib/routes/password.ts +++ b/packages/fxa-auth-server/lib/routes/password.ts @@ -1144,6 +1144,20 @@ module.exports = function ( account: { uid }, }); + // We need to track when users with third party accounts actually set a passwords. Note that + // this handler will exit early if the user already has a password, and this code would not + // be reached, which ensures this event will only fire when a user is setting their linked + // account password for the first time. + const linkedAccounts = await db.getLinkedAccounts(uid); + if ( + linkedAccounts?.length > 0 && + linkedAccounts.some((x: { enabled: boolean }) => x.enabled) + ) { + glean.thirdPartyAuth.setPasswordComplete(request, { + uid, + }); + } + return passwordCreated; }, }, diff --git a/packages/fxa-auth-server/test/local/metrics/glean.ts b/packages/fxa-auth-server/test/local/metrics/glean.ts index 16f4690353..eeae3f9709 100644 --- a/packages/fxa-auth-server/test/local/metrics/glean.ts +++ b/packages/fxa-auth-server/test/local/metrics/glean.ts @@ -31,6 +31,7 @@ const recordPasswordResetRecoveryKeyCreateSuccessStub = sinon.stub(); const recordAccessTokenCreatedStub = sinon.stub(); const recordAccessTokenCheckedStub = sinon.stub(); const recordThirdPartyAuthGoogleLoginCompleteStub = sinon.stub(); +const recordThirdPartyAuthSetPasswordCompleteStub = sinon.stub(); const { gleanMetrics, logErrorWithGlean } = proxyquire.load( '../../../lib/metrics/glean', @@ -64,6 +65,8 @@ const { gleanMetrics, logErrorWithGlean } = proxyquire.load( recordAccessTokenChecked: recordAccessTokenCheckedStub, recordThirdPartyAuthGoogleLoginComplete: recordThirdPartyAuthGoogleLoginCompleteStub, + recordThirdPartyAuthSetPasswordComplete: + recordThirdPartyAuthSetPasswordCompleteStub, }), }, } @@ -536,6 +539,23 @@ describe('Glean server side events', () => { ); }); }); + describe('setPassword', () => { + beforeEach(() => { + recordThirdPartyAuthSetPasswordCompleteStub.reset(); + }); + + it('log string and event metrics with account linking', async () => { + const glean = gleanMetrics(config); + await glean.thirdPartyAuth.setPasswordComplete(request); + sinon.assert.calledOnce(recordStub); + const metrics = recordStub.args[0][0]; + assert.equal( + metrics['event_name'], + 'third_party_auth_set_password_complete' + ); + sinon.assert.calledOnce(recordThirdPartyAuthSetPasswordCompleteStub); + }); + }); }); describe('logErrorWithGlean hapi preResponse error logger', () => { diff --git a/packages/fxa-auth-server/test/local/routes/password.js b/packages/fxa-auth-server/test/local/routes/password.js index d2c1b9d426..6824e1898e 100644 --- a/packages/fxa-auth-server/test/local/routes/password.js +++ b/packages/fxa-auth-server/test/local/routes/password.js @@ -1223,6 +1223,9 @@ describe('/password', () => { enabled: true, }; }); + mockDB.getLinkedAccounts = sinon.spy(() => { + return Promise.resolve([{ enabled: true }]); + }); passwordRoutes = makeRoutes({ db: mockDB, mailer: mockMailer, @@ -1235,6 +1238,8 @@ describe('/password', () => { ); assert.equal(mockDB.account.callCount, 1); assert.equal(mockDB.createPassword.callCount, 1); + assert.equal(mockDB.getLinkedAccounts.callCount, 1); + assert.equal(glean.thirdPartyAuth.setPasswordComplete.callCount, 1); assert.deepEqual(res, 1584397692000); }); }); diff --git a/packages/fxa-auth-server/test/mocks.js b/packages/fxa-auth-server/test/mocks.js index 2da8b46159..6061f108f5 100644 --- a/packages/fxa-auth-server/test/mocks.js +++ b/packages/fxa-auth-server/test/mocks.js @@ -110,6 +110,7 @@ const DB_METHOD_NAMES = [ 'reactivateAccountSubscription', 'fetchAccountSubscriptions', 'getLinkedAccount', + 'getLinkedAccounts', 'createLinkedAccount', 'deleteLinkedAccount', 'accountExists', diff --git a/packages/fxa-shared/metrics/glean/fxa-backend-metrics.yaml b/packages/fxa-shared/metrics/glean/fxa-backend-metrics.yaml index c384dbdeae..028fbadb5e 100644 --- a/packages/fxa-shared/metrics/glean/fxa-backend-metrics.yaml +++ b/packages/fxa-shared/metrics/glean/fxa-backend-metrics.yaml @@ -720,3 +720,22 @@ third_party_auth: description: | Indicates the initial linking of the Mozilla account and the third-party account. type: boolean + +third_party_auth_set_password: + complete: + type: event + description: | + Password for third party user was successfully created + send_in_pings: + - events + notification_emails: + - vzare@mozilla.com + - fxa-staff@mozilla.com + bugs: + - https://mozilla-hub.atlassian.net/browse/FXA-9988 + 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