Skip to content

Commit e200694

Browse files
committed
feat(metrics): Add missing metrics for recovery phone
1 parent 39a64bd commit e200694

5 files changed

Lines changed: 145 additions & 18 deletions

File tree

libs/accounts/recovery-phone/src/lib/sms.manager.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ describe('SmsManager', () => {
1717
messages: {
1818
create: jest.fn(),
1919
},
20+
lookups: {
21+
v2: {
22+
phoneNumbers: jest.fn(),
23+
},
24+
},
2025
};
2126
const mockMetrics = {
2227
increment: jest.fn(),
28+
histogram: jest.fn(),
2329
};
2430
const mockLog = {
2531
log: jest.fn(),
@@ -29,6 +35,7 @@ describe('SmsManager', () => {
2935
validNumberPrefixes: ['+1'],
3036
maxMessageLength: 160,
3137
maxRetries: 2,
38+
extraLookupFields: ['sms_pumping_risk'],
3239
};
3340

3441
let manager: SmsManager;
@@ -187,4 +194,61 @@ describe('SmsManager', () => {
187194
expect(number2).toBeDefined();
188195
expect(number1).not.toEqual(number2);
189196
});
197+
198+
describe('phoneNumberLookup', () => {
199+
it('should increment metrics and return result on success', async () => {
200+
const phoneNumber = '+15005551111';
201+
const mockResult = {
202+
simSwap: { lastSimSwap: { swappedPeriod: '1' } },
203+
smsPumpingRisk: { smsPumpingRiskScore: 0.5 },
204+
phoneNumberQualityScore: 0.8,
205+
toJSON: jest.fn().mockReturnValue({ phoneNumber }),
206+
};
207+
208+
mockTwilioSmsClient.lookups.v2.phoneNumbers.mockReturnValue({
209+
fetch: jest.fn().mockResolvedValue(mockResult),
210+
});
211+
212+
const result = await manager.phoneNumberLookup(phoneNumber);
213+
214+
expect(mockMetrics.increment).toHaveBeenCalledWith(
215+
'sms.phoneNumberLookup.start'
216+
);
217+
expect(mockMetrics.increment).toHaveBeenCalledWith(
218+
'sms.phoneNumberLookup.success'
219+
);
220+
expect(mockMetrics.increment).toHaveBeenCalledWith(
221+
'sms.phoneNumberLookup.success.simSwap',
222+
{ period: '1' }
223+
);
224+
expect(mockMetrics.histogram).toHaveBeenCalledWith(
225+
'sms.phoneNumberLookup.success.smsPumpingRisk',
226+
0.5
227+
);
228+
expect(mockMetrics.histogram).toHaveBeenCalledWith(
229+
'sms.phoneNumberLookup.success.phoneNumberQualityScore',
230+
0.8
231+
);
232+
expect(result).toEqual({ phoneNumber });
233+
});
234+
235+
it('should increment failed metric and throw error on failure', async () => {
236+
const phoneNumber = '+15005551111';
237+
const mockError = new Error('Lookup failed');
238+
239+
mockTwilioSmsClient.lookups.v2.phoneNumbers.mockReturnValue({
240+
fetch: jest.fn().mockRejectedValue(mockError),
241+
});
242+
243+
await expect(manager.phoneNumberLookup(phoneNumber)).rejects.toThrow(
244+
mockError
245+
);
246+
expect(mockMetrics.increment).toHaveBeenCalledWith(
247+
'sms.phoneNumberLookup.start'
248+
);
249+
expect(mockMetrics.increment).toHaveBeenCalledWith(
250+
'sms.phoneNumberLookup.failure'
251+
);
252+
});
253+
});
190254
});

libs/accounts/recovery-phone/src/lib/sms.manager.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,47 @@ export class SmsManager {
2929
) {}
3030

3131
public async phoneNumberLookup(phoneNumber: string) {
32-
const result = await this.client.lookups.v2
33-
.phoneNumbers(phoneNumber)
34-
.fetch({ fields: this.config.extraLookupFields.join(',') });
35-
// Calling toJSON converts PhoneNumberInstance into a
36-
// object that just holds state and can be serialized.
37-
return result.toJSON();
32+
try {
33+
this.metrics.increment('sms.phoneNumberLookup.start');
34+
const result = await this.client.lookups.v2
35+
.phoneNumbers(phoneNumber)
36+
.fetch({ fields: this.config.extraLookupFields.join(',') });
37+
38+
this.metrics.increment('sms.phoneNumberLookup.success');
39+
40+
/**
41+
* See https://www.twilio.com/docs/lookup/v2-api/sim-swap for more details
42+
* on the sim swap object returned by Twilio. We currently track only
43+
* the last sim swap period.
44+
*/
45+
const simSwapPeriod = result.simSwap?.lastSimSwap?.swappedPeriod;
46+
if (simSwapPeriod) {
47+
this.metrics.increment('sms.phoneNumberLookup.success.simSwap', {
48+
period: simSwapPeriod,
49+
});
50+
}
51+
const smsPumpingRisk = result?.smsPumpingRisk?.smsPumpingRiskScore;
52+
if (smsPumpingRisk) {
53+
this.metrics.histogram(
54+
'sms.phoneNumberLookup.success.smsPumpingRisk',
55+
smsPumpingRisk
56+
);
57+
}
58+
const phoneNumberQualityScore = result.phoneNumberQualityScore;
59+
if (phoneNumberQualityScore) {
60+
this.metrics.histogram(
61+
'sms.phoneNumberLookup.success.phoneNumberQualityScore',
62+
phoneNumberQualityScore
63+
);
64+
}
65+
66+
// Calling toJSON converts PhoneNumberInstance into a
67+
// object that just holds state and can be serialized.
68+
return result.toJSON();
69+
} catch (err) {
70+
this.metrics.increment('sms.phoneNumberLookup.failure');
71+
throw err;
72+
}
3873
}
3974

4075
public async sendSMS({

packages/fxa-auth-server/lib/routes/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ module.exports = function (
141141
db,
142142
glean,
143143
log,
144-
mailer
144+
mailer,
145+
statsd
145146
);
146147
const securityEvents = require('./security-events')(log, db, config);
147148
const session = require('./session')(

packages/fxa-auth-server/lib/routes/recovery-phone.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class RecoveryPhoneHandler {
5252
private readonly db: any,
5353
private readonly glean: GleanMetricsType,
5454
private readonly log: any,
55-
private readonly mailer: any
55+
private readonly mailer: any,
56+
private readonly statsd: any
5657
) {
5758
this.recoveryPhoneService = Container.get(RecoveryPhoneService);
5859
this.accountManager = Container.get(AccountManager);
@@ -138,7 +139,7 @@ class RecoveryPhoneHandler {
138139
}
139140

140141
if (success) {
141-
this.log.info('account.recoveryPhone.signinSendCode.success', { uid });
142+
this.statsd.increment('account.recoveryPhone.signinSendCode.success');
142143
await this.glean.twoStepAuthPhoneCode.sent(request);
143144

144145
this.accountEventsManager.recordSecurityEvent(this.db, {
@@ -185,9 +186,7 @@ class RecoveryPhoneHandler {
185186
getFormattedMessage
186187
);
187188
if (success) {
188-
this.log.info('account.recoveryPhone.setupPhoneNumber.success', {
189-
uid,
190-
});
189+
this.statsd.increment('account.recoveryPhone.setupPhoneNumber.success');
191190
await this.glean.twoStepAuthPhoneCode.sent(request);
192191

193192
let nationalFormat: string | null = null;
@@ -299,7 +298,7 @@ class RecoveryPhoneHandler {
299298
const { acceptLanguage, geo, ua } = request.app;
300299

301300
if (isSetup) {
302-
this.log.info('account.recoveryPhone.phoneAdded.success', { uid });
301+
this.statsd.increment('account.recoveryPhone.phoneAdded.success');
303302

304303
try {
305304
const { phoneNumber, nationalFormat } =
@@ -344,7 +343,7 @@ class RecoveryPhoneHandler {
344343
});
345344
}
346345
} else {
347-
this.log.info('account.recoveryPhone.phoneSignin.success', { uid });
346+
this.statsd.increment('account.recoveryPhone.phoneSignin.success');
348347

349348
this.accountEventsManager.recordSecurityEvent(this.db, {
350349
name: 'account.recovery_phone_signin_complete',
@@ -422,7 +421,7 @@ class RecoveryPhoneHandler {
422421
}
423422

424423
if (success) {
425-
this.log.info('account.recoveryPhone.phoneRemoved.success', { uid });
424+
this.statsd.increment('account.recoveryPhone.phoneRemoved.success');
426425
await this.glean.twoStepAuthPhoneRemove.success(request);
427426

428427
const account = await this.db.account(uid);
@@ -540,14 +539,16 @@ export const recoveryPhoneRoutes = (
540539
db: any,
541540
glean: GleanMetricsType,
542541
log: any,
543-
mailer: any
542+
mailer: any,
543+
statsd: any
544544
) => {
545545
const recoveryPhoneHandler = new RecoveryPhoneHandler(
546546
customs,
547547
db,
548548
glean,
549549
log,
550-
mailer
550+
mailer,
551+
statsd
551552
);
552553
const routes = [
553554
{

packages/fxa-auth-server/test/local/routes/recovery-phone.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ describe('/recovery_phone', () => {
3838
check: sandbox.fake(),
3939
checkAuthenticated: sandbox.fake(),
4040
};
41+
const mockStatsd = {
42+
increment: sandbox.fake(),
43+
histogram: sandbox.fake(),
44+
};
4145
const mockGlean = {
4246
twoStepAuthPhoneCode: {
4347
sent: sandbox.fake(),
@@ -75,7 +79,8 @@ describe('/recovery_phone', () => {
7579
mockDb,
7680
mockGlean,
7781
mockLog,
78-
mockMailer
82+
mockMailer,
83+
mockStatsd
7984
);
8085
});
8186

@@ -124,6 +129,10 @@ describe('/recovery_phone', () => {
124129
tokenId: undefined,
125130
}
126131
);
132+
assert.calledOnceWithExactly(
133+
mockStatsd.increment,
134+
'account.recoveryPhone.signinSendCode.success'
135+
);
127136
});
128137

129138
it('handles failure to send recovery phone code', async () => {
@@ -211,6 +220,10 @@ describe('/recovery_phone', () => {
211220
mockCustoms.checkAuthenticated.getCall(0).args[2],
212221
'recoveryPhoneCreate'
213222
);
223+
assert.calledOnceWithExactly(
224+
mockStatsd.increment,
225+
'account.recoveryPhone.setupPhoneNumber.success'
226+
);
214227
});
215228

216229
it('indicates failure sending sms', async () => {
@@ -363,6 +376,11 @@ describe('/recovery_phone', () => {
363376
tokenId: undefined,
364377
}
365378
);
379+
380+
assert.calledOnceWithExactly(
381+
mockStatsd.increment,
382+
'account.recoveryPhone.phoneAdded.success'
383+
);
366384
});
367385

368386
it('indicates a failure confirming code', async () => {
@@ -439,6 +457,10 @@ describe('/recovery_phone', () => {
439457
tokenId: undefined,
440458
}
441459
);
460+
assert.calledOnceWithExactly(
461+
mockStatsd.increment,
462+
'account.recoveryPhone.phoneSignin.success'
463+
);
442464
});
443465

444466
it('fails confirms a code during signin', async () => {
@@ -496,6 +518,10 @@ describe('/recovery_phone', () => {
496518
tokenId: undefined,
497519
}
498520
);
521+
assert.calledOnceWithExactly(
522+
mockStatsd.increment,
523+
'account.recoveryPhone.phoneRemoved.success'
524+
);
499525
});
500526

501527
it('indicates service failure while removing phone', async () => {

0 commit comments

Comments
 (0)