Skip to content

feat(payments-api): Delete glean/nimbus data on account deletion#20381

Open
david1alvarez wants to merge 1 commit intomainfrom
PAY-3438
Open

feat(payments-api): Delete glean/nimbus data on account deletion#20381
david1alvarez wants to merge 1 commit intomainfrom
PAY-3438

Conversation

@david1alvarez
Copy link
Copy Markdown
Contributor

Because:

  • When an account is deleted, subplat data associated with the user (including nimbus-associated data) needs to be removed as well

This commit:

  • Adds in a record-keeping deletionNamespaces field to the nimbus config, for ensuring deletion of data associated with historical nimbus UUID generations
  • Logs a record via the winston logger in a format designed to be intercepted by a bigquery ETL query to delete glean data with a matching nimbus user id
  • Wires in the deletion logger method into the fxa webhook call for the delete-user fxa events

Closes #PAY-3438

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

@david1alvarez david1alvarez requested a review from a team as a code owner April 15, 2026 19:06
Copilot AI review requested due to automatic review settings April 15, 2026 19:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds account-deletion cleanup hooks for Payments telemetry by generating all historical Nimbus user IDs and emitting deletion log records when FxA sends a delete-user webhook event, enabling downstream deletion of associated Glean/Nimbus-subplat data.

Changes:

  • Wire PaymentsGleanService.handleUserDelete() into the FxA delete-user webhook handler.
  • Add deletion-time Nimbus ID generation across historical namespaces (deletionNamespaces) and log one deletion record per generated Nimbus ID.
  • Update tests and sample env files to include the new Nimbus manager config field.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
libs/payments/webhooks/src/lib/fxa-webhooks.service.ts Calls glean deletion handler during FxA delete-user webhook processing.
libs/payments/webhooks/src/lib/fxa-webhooks.service.spec.ts Mocks PaymentsGleanService and asserts delete-user triggers handleUserDelete.
libs/payments/webhooks/src/lib/fxa-webhooks.controller.spec.ts Adds PaymentsGleanService provider for controller test module setup.
libs/payments/metrics/src/lib/glean/glean.service.ts Adds handleUserDelete to log deletion events per Nimbus ID.
libs/payments/metrics/src/lib/glean/glean.service.spec.ts Adds unit tests for handleUserDelete logging behavior.
libs/payments/experiments/src/lib/nimbus.manager.ts Adds generateAllNimbusIdsForDeletion using deletionNamespaces.
libs/payments/experiments/src/lib/nimbus.manager.spec.ts Adds tests for generateAllNimbusIdsForDeletion behavior and fallback.
libs/payments/experiments/src/lib/nimbus.manager.config.ts Adds deletionNamespaces config parsing/validation.
apps/payments/next/.env Adds NIMBUS_MANAGER__DELETION_NAMESPACES sample value.
apps/payments/api/.env Adds Nimbus manager config section (currently empty values).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to +21
@Transform(({ value }) =>
typeof value === 'string' ? value.split(',') : value
)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

deletionNamespaces is populated via value.split(','), which turns an empty env var into [""] and keeps whitespace (e.g. "uuid1, uuid2"). Downstream this is used as a UUID namespace for uuidv5, so empty strings / whitespace will cause runtime errors. Consider trimming and filtering falsy entries, and returning undefined when the resulting list is empty (so code can fall back to namespace).

Suggested change
@Transform(({ value }) =>
typeof value === 'string' ? value.split(',') : value
)
@Transform(({ value }) => {
if (typeof value !== 'string') {
return value;
}
const deletionNamespaces = value
.split(',')
.map((namespace) => namespace.trim())
.filter(Boolean);
return deletionNamespaces.length > 0 ? deletionNamespaces : undefined;
})

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +68
const namespaces =
this.nimbusManagerConfig.deletionNamespaces ??
[this.nimbusManagerConfig.namespace];
return namespaces.map((ns) => generateNimbusId(ns, fxaUid));
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

generateAllNimbusIdsForDeletion uses deletionNamespaces ?? [namespace], which will not fall back when deletionNamespaces is an empty array (or contains only empty strings after parsing). That can silently produce no IDs (no deletion logs) or can throw if a namespace is invalid. Consider normalizing with filter(Boolean) and falling back when the normalized list is empty; also consider always including this.nimbusManagerConfig.namespace and de-duplicating to reduce misconfiguration risk (missing current namespace would skip deleting the current nimbus id).

Suggested change
const namespaces =
this.nimbusManagerConfig.deletionNamespaces ??
[this.nimbusManagerConfig.namespace];
return namespaces.map((ns) => generateNimbusId(ns, fxaUid));
const configuredNamespaces = (
this.nimbusManagerConfig.deletionNamespaces ?? []
).filter(Boolean);
const namespaces = [
...new Set([
...configuredNamespaces,
this.nimbusManagerConfig.namespace,
]),
];
return (namespaces.length > 0
? namespaces
: [this.nimbusManagerConfig.namespace]
).map((ns) => generateNimbusId(ns, fxaUid));

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +213
const nimbusUserIds =
this.nimbusManager.generateAllNimbusIdsForDeletion(uid);
for (const nimbusUserId of nimbusUserIds) {
// PAY-3438: Log format consumed by BigQuery ETL for Glean Shredder.
// Do not change jsonPayload.type or field names without updating the ETL query.
this.log.log('glean.user.delete', {
uid,
nimbus_user_id: nimbusUserId,
});
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

handleUserDelete can throw if any namespace used to generate the nimbus id is missing/invalid (e.g., empty NIMBUS_MANAGER__DELETION_NAMESPACES parses into [""] and uuidv5 will error). Since this runs inside the FxA delete-user webhook handler, that would fail the webhook request and prevent emitting deletion logs. Consider validating/normalizing namespaces (or catching generation errors) so this path can’t take down webhook handling.

Suggested change
const nimbusUserIds =
this.nimbusManager.generateAllNimbusIdsForDeletion(uid);
for (const nimbusUserId of nimbusUserIds) {
// PAY-3438: Log format consumed by BigQuery ETL for Glean Shredder.
// Do not change jsonPayload.type or field names without updating the ETL query.
this.log.log('glean.user.delete', {
uid,
nimbus_user_id: nimbusUserId,
});
try {
const nimbusUserIds =
this.nimbusManager.generateAllNimbusIdsForDeletion(uid);
for (const nimbusUserId of nimbusUserIds) {
// PAY-3438: Log format consumed by BigQuery ETL for Glean Shredder.
// Do not change jsonPayload.type or field names without updating the ETL query.
this.log.log('glean.user.delete', {
uid,
nimbus_user_id: nimbusUserId,
});
}
} catch (error) {
this.log.error(
`Failed to generate Nimbus deletion ids for uid ${uid}`,
error instanceof Error ? error.stack : undefined
);

Copilot uses AI. Check for mistakes.
Comment thread apps/payments/api/.env
Comment on lines +63 to +64
NIMBUS_MANAGER__NAMESPACE=
NIMBUS_MANAGER__DELETION_NAMESPACES=
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

NIMBUS_MANAGER__NAMESPACE and NIMBUS_MANAGER__DELETION_NAMESPACES are set to empty values here. With the current parsing (split(',')), an empty DELETION_NAMESPACES becomes [""], and the delete-user webhook path will attempt to generate a UUID v5 with an invalid namespace, throwing at runtime. Use a valid UUID for NIMBUS_MANAGER__NAMESPACE (even if ENABLED=false), and either omit DELETION_NAMESPACES or set it to a valid comma-separated UUID list.

Suggested change
NIMBUS_MANAGER__NAMESPACE=
NIMBUS_MANAGER__DELETION_NAMESPACES=
NIMBUS_MANAGER__NAMESPACE=6ba7b810-9dad-11d1-80b4-00c04fd430c8
NIMBUS_MANAGER__DELETION_NAMESPACES=6ba7b810-9dad-11d1-80b4-00c04fd430c8

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +214
// PAY-3438: Log format consumed by BigQuery ETL for Glean Shredder.
// Do not change jsonPayload.type or field names without updating the ETL query.
this.log.log('glean.user.delete', {
uid,
nimbus_user_id: nimbusUserId,
});
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

The comment says the ETL depends on jsonPayload.type and field names, but this call uses Nest's Logger.log(message, context?) signature where the second argument is typically a context string. If the runtime logger isn’t a winston-compatible structured logger, the { uid, nimbus_user_id } object may be dropped or coerced to "[object Object]", breaking the BigQuery shredder. Consider logging a single structured object (including an explicit type field) via the winston/shared logger provider used in production logging.

Copilot uses AI. Check for mistakes.
Because:

* When an account is deleted, subplat data associated with the user (including nimbus-associated data) needs to be removed as well

This commit:

* Adds in a record-keeping deletionNamespaces field to the nimbus config, for ensuring deletion of data associated with historical nimbus UUID generations
* Logs a record via the winston logger in a format designed to be intercepted by a bigquery ETL query to delete glean data with a matching nimbus user id
* Wires in the deletion logger method into the fxa webhook call for the delete-user fxa events

Closes #PAY-3438
david1alvarez added a commit to david1alvarez/bigquery-etl that referenced this pull request Apr 16, 2026
Because:

* Current glean deletion ETL query duplicates the nimbus_user_id derivation logic that lives in the fxa repository

This commit:

* Updates the script to match the paired [PR](mozilla/fxa#20381), while still supporting the existing approach

Closes #PAY-3438
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants