Skip to content

Commit e9cd53e

Browse files
authored
Merge pull request #18413 from mozilla/FXA-11071-schedule-inactive-delete
feat(inactive account): schedule deletion after third email
2 parents 6e84794 + b7d1a04 commit e9cd53e

8 files changed

Lines changed: 180 additions & 10 deletions

File tree

libs/shared/cloud-tasks/src/lib/delete-account-tasks.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { DeleteAccountTasks } from './delete-account-tasks';
66
import { DeleteAccountTasksFactory } from './account-tasks.factories';
77
import { ReasonForDeletion } from './account-tasks.types';
88

9+
const now = 1736500000000;
10+
jest.useFakeTimers({ now });
11+
912
describe('account-tasks', () => {
1013
const mockStatsd = {
1114
increment: jest.fn(),
@@ -72,8 +75,6 @@ describe('account-tasks', () => {
7275
queueName: 'delete-account',
7376
},
7477
},
75-
publicUrl: 'http://localhost:9000',
76-
apiVersion: '1',
7778
},
7879
mockCloudClient,
7980
mockStatsd
@@ -90,7 +91,8 @@ describe('account-tasks', () => {
9091
});
9192

9293
const taskName = await deleteAccountCloudTask.deleteAccount(
93-
mockDeleteTaskPayload
94+
mockDeleteTaskPayload,
95+
{ taskId: 'wibble', scheduleTime: { seconds: Date.now() / 1000 } }
9496
);
9597
expect(taskName).toEqual('task123');
9698
expect(mockStatsd.increment).toBeCalledWith(
@@ -112,6 +114,10 @@ describe('account-tasks', () => {
112114
mockConfig.cloudTasks.oidc.serviceAccountEmail,
113115
},
114116
},
117+
name: 'projects/pid123/locations/lid123/queues/delete-account/tasks/wibble',
118+
scheduleTime: {
119+
seconds: 1736500000,
120+
},
115121
},
116122
});
117123
});

libs/shared/cloud-tasks/src/lib/delete-account-tasks.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DeleteAccountCloudTaskConfig,
1010
DeleteAccountTask,
1111
} from './account-tasks.types';
12+
import { CloudTaskOptions } from './cloud-tasks.types';
1213

1314
/** Responsible for account deletion tasks */
1415
export class DeleteAccountTasks extends CloudTasks {
@@ -25,13 +26,19 @@ export class DeleteAccountTasks extends CloudTasks {
2526
* @param deleteTask The info necessary to queue an account deletion.
2627
* @returns A taskName
2728
*/
28-
public async deleteAccount(deleteTask: DeleteAccountTask) {
29+
public async deleteAccount(
30+
deleteTask: DeleteAccountTask,
31+
cloudTaskOptions?: CloudTaskOptions
32+
) {
2933
try {
30-
const result = await this.enqueueTask({
31-
queueName: this.config.cloudTasks.deleteAccounts.queueName,
32-
taskUrl: this.config.cloudTasks.deleteAccounts.taskUrl,
33-
taskPayload: deleteTask,
34-
});
34+
const result = await this.enqueueTask(
35+
{
36+
queueName: this.config.cloudTasks.deleteAccounts.queueName,
37+
taskUrl: this.config.cloudTasks.deleteAccounts.taskUrl,
38+
taskPayload: deleteTask,
39+
},
40+
cloudTaskOptions
41+
);
3542
const taskName = result[0].name;
3643

3744
this.statsd.increment('cloud-tasks.account-delete.enqueue.success');

packages/fxa-auth-server/lib/inactive-accounts/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,33 @@ export class InactiveAccountsManager {
287287
);
288288

289289
await this.scheduleNextEmail(taskPayload);
290+
291+
if (
292+
taskPayload.emailType === EmailTypes.INACTIVE_DELETE_FINAL_NOTIFICATION
293+
) {
294+
await this.accountTasks.deleteAccount(
295+
{
296+
uid: taskPayload.uid,
297+
customerId: (
298+
await getAccountCustomerByUid(taskPayload.uid)
299+
)?.stripeCustomerId,
300+
reason: ReasonForDeletion.InactiveAccountScheduled,
301+
},
302+
{
303+
taskId: `${taskPayload.uid}-inactive-account-delete`,
304+
scheduleTime: {
305+
seconds: (now + aDayInMs) / 1000,
306+
},
307+
}
308+
);
309+
await this.glean.inactiveAccountDeletion.deletionScheduled(
310+
requestForGlean,
311+
{
312+
uid: taskPayload.uid,
313+
}
314+
);
315+
this.statsd.increment('account.inactive.deletion.scheduled');
316+
}
290317
}
291318

292319
async scheduleNextEmail(taskReqPayload: SendEmailTaskPayload) {

packages/fxa-auth-server/lib/metrics/glean/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ export function gleanMetrics(config: ConfigType) {
370370
finalEmailSkipped: createEventFn(
371371
'inactive_account_deletion_final_email_skipped'
372372
),
373+
deletionScheduled: createEventFn(
374+
'inactive_account_deletion_deletion_scheduled'
375+
),
373376
},
374377
twoStepAuthPhoneRemove: {
375378
success: createEventFn('two_step_auth_phone_remove_success'),

packages/fxa-auth-server/lib/metrics/glean/server_events.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,88 @@ class EventsServerEventLogger {
638638
event,
639639
});
640640
}
641+
/**
642+
* Record and submit a inactive_account_deletion_deletion_scheduled event:
643+
* The cloud task to delete the account was scheduled.
644+
* Event is logged using internal mozlog logger.
645+
*
646+
* @param {string} user_agent - The user agent.
647+
* @param {string} ip_address - The IP address. Will be used to decode Geo
648+
* information and scrubbed at ingestion.
649+
* @param {string} account_user_id - The firefox/mozilla account id.
650+
* @param {string} account_user_id_sha256 - A hex string of a sha256 hash of the account's uid.
651+
* @param {string} relying_party_oauth_client_id - The client id of the relying party.
652+
* @param {string} relying_party_service - The service name of the relying party.
653+
* @param {string} session_device_type - one of 'mobile', 'tablet', or ''.
654+
* @param {string} session_entrypoint - Entrypoint to the service.
655+
* @param {string} session_entrypoint_experiment - Identifier for the experiment the user is part of at the entrypoint.
656+
* @param {string} session_entrypoint_variation - Identifier for the experiment variation the user is part of at the entrypoint.
657+
* @param {string} session_flow_id - an ID generated by FxA for its flow metrics.
658+
* @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..
659+
* @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..
660+
* @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..
661+
* @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..
662+
* @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..
663+
*/
664+
recordInactiveAccountDeletionDeletionScheduled({
665+
user_agent,
666+
ip_address,
667+
account_user_id,
668+
account_user_id_sha256,
669+
relying_party_oauth_client_id,
670+
relying_party_service,
671+
session_device_type,
672+
session_entrypoint,
673+
session_entrypoint_experiment,
674+
session_entrypoint_variation,
675+
session_flow_id,
676+
utm_campaign,
677+
utm_content,
678+
utm_medium,
679+
utm_source,
680+
utm_term,
681+
}: {
682+
user_agent: string;
683+
ip_address: string;
684+
account_user_id: string;
685+
account_user_id_sha256: string;
686+
relying_party_oauth_client_id: string;
687+
relying_party_service: string;
688+
session_device_type: string;
689+
session_entrypoint: string;
690+
session_entrypoint_experiment: string;
691+
session_entrypoint_variation: string;
692+
session_flow_id: string;
693+
utm_campaign: string;
694+
utm_content: string;
695+
utm_medium: string;
696+
utm_source: string;
697+
utm_term: string;
698+
}) {
699+
const event = {
700+
category: 'inactive_account_deletion',
701+
name: 'deletion_scheduled',
702+
};
703+
this.#record({
704+
user_agent,
705+
ip_address,
706+
account_user_id,
707+
account_user_id_sha256,
708+
relying_party_oauth_client_id,
709+
relying_party_service,
710+
session_device_type,
711+
session_entrypoint,
712+
session_entrypoint_experiment,
713+
session_entrypoint_variation,
714+
session_flow_id,
715+
utm_campaign,
716+
utm_content,
717+
utm_medium,
718+
utm_source,
719+
utm_term,
720+
event,
721+
});
722+
}
641723
/**
642724
* Record and submit a inactive_account_deletion_final_email_skipped event:
643725
* The final email notification was skipped.

packages/fxa-auth-server/test/local/inactive-accounts/index.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const mockGlean = {
4040
finalEmailTaskEnqueued: sandbox.stub(),
4141
finalEmailTaskRejected: sandbox.stub(),
4242
finalEmailTaskRequest: sandbox.stub(),
43+
44+
deletionScheduled: sandbox.stub(),
4345
},
4446
};
4547
const mockLog = mocks.mockLog(sandbox);
@@ -483,7 +485,7 @@ describe('InactiveAccountsManager', () => {
483485
sandbox.assert.calledOnce(inactiveAccountManager.scheduleNextEmail);
484486
});
485487

486-
it('should send the final email', async () => {
488+
it('should send the final email and schedule deletion', async () => {
487489
sandbox.stub(accountEventsManager, 'findEmailEvents').resolves([]);
488490
sandbox.stub(inactiveAccountManager, 'isActive').resolves(false);
489491

@@ -502,6 +504,28 @@ describe('InactiveAccountsManager', () => {
502504
);
503505
// No email cloud task should be run. There are no more emails to schedule.
504506
sandbox.assert.notCalled(mockEmailTasks.scheduleFinalEmail);
507+
508+
sandbox.assert.calledOnceWithExactly(
509+
mockDeleteAccountTasks.deleteAccount,
510+
{
511+
uid: mockPayload.uid,
512+
customerId: undefined,
513+
reason: ReasonForDeletion.InactiveAccountScheduled,
514+
},
515+
{
516+
taskId: `${mockPayload.uid}-inactive-account-delete`,
517+
scheduleTime: {
518+
seconds: (Date.now() + aDayInMs) / 1000,
519+
},
520+
}
521+
);
522+
sandbox.assert.calledOnce(
523+
mockGlean.inactiveAccountDeletion.deletionScheduled
524+
);
525+
sandbox.assert.calledWithExactly(
526+
mockStatsd.increment,
527+
'account.inactive.deletion.scheduled'
528+
);
505529
});
506530
});
507531
});

packages/fxa-auth-server/test/local/metrics/glean.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ const recordInactiveAccountDeletionFinalEmailTaskRequestStub = sinon.stub();
6363
const recordInactiveAccountDeletionFinalEmailTaskEnqueuedStub = sinon.stub();
6464
const recordInactiveAccountDeletionFinalEmailTaskRejectedStub = sinon.stub();
6565
const recordInactiveAccountDeletionFinalEmailSkippedStub = sinon.stub();
66+
const recordInactiveAccountDeletionDeletionScheduleStub = sinon.stub();
6667

6768
const gleanProxy = proxyquire('../../../lib/metrics/glean', {
6869
'./server_events': {
6970
createAccountsEventsEvent: () => ({ record: recordStub }),
71+
// this is out of hand! we need to switch to use sinon.mock or some such thing
7072
createEventsServerEventLogger: () => ({
7173
recordRegAccCreated: recordRegAccCreatedStub,
7274
recordRegEmailSent: recordRegEmailSentStub,
@@ -148,6 +150,8 @@ const gleanProxy = proxyquire('../../../lib/metrics/glean', {
148150
recordInactiveAccountDeletionFinalEmailTaskRejectedStub,
149151
recordInactiveAccountDeletionFinalEmailSkipped:
150152
recordInactiveAccountDeletionFinalEmailSkippedStub,
153+
recordInactiveAccountDeletionDeletionScheduled:
154+
recordInactiveAccountDeletionDeletionScheduleStub,
151155
}),
152156
},
153157
});

packages/fxa-shared/metrics/glean/fxa-backend-metrics.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,23 @@ inactive_account_deletion:
13031303
description: |
13041304
The reason the email was skipped.
13051305
type: string
1306+
deletion_scheduled:
1307+
type: event
1308+
description: |
1309+
The cloud task to delete the account was scheduled.
1310+
lifetime: ping
1311+
send_in_pings:
1312+
- events
1313+
notification_emails:
1314+
1315+
1316+
bugs:
1317+
- https://mozilla-hub.atlassian.net/browse/FXA-11071
1318+
data_reviews:
1319+
- https://bugzilla.mozilla.org/show_bug.cgi?id=1830504
1320+
expires: never
1321+
data_sensitivity:
1322+
- technical
13061323

13071324
two_step_auth_phone_remove:
13081325
success:

0 commit comments

Comments
 (0)