feat(analytics-controller): add independent marketing consent with purpose-aware delivery - #10232
Conversation
…ment context flag Classify named track/view events as marketing or product, gate each lane on its own consent, and stamp context.marketing for destinations. Phase 1 keeps marketingEventNames as a persisted/seeded list without a remote fetch. Co-authored-by: Cursor <cursoragent@cursor.com>
Regenerate messenger action types, fix formatting, link the changelog to #10232, and classify fragments from event names only so caller context.marketing cannot bypass the correct consent lane. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🟡 Changes recommended
Consent-lane handling issues remain for identify events, persisted fragments, and mixed-fragment emission.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds independent marketing consent handling and Segment context flags to AnalyticsController.
Changes:
- Adds marketing consent state, selectors, APIs, and actions.
- Classifies events and routes queues/fragments by consent lane.
- Updates tests, documentation, and changelog.
File summaries
| File | Summary |
|---|---|
packages/analytics-controller/src/selectors.ts |
Adds marketing consent selectors. |
packages/analytics-controller/src/selectors.test.ts |
Tests the new selectors. |
packages/analytics-controller/src/index.ts |
Exports new action types. |
packages/analytics-controller/src/AnalyticsController.ts |
Implements consent lanes and event classification. |
packages/analytics-controller/src/AnalyticsController.test.ts |
Tests marketing consent behavior and lane handling. |
packages/analytics-controller/src/AnalyticsController-method-action-types.ts |
Defines marketing consent actions. |
packages/analytics-controller/README.md |
Documents marketing consent and event classification. |
packages/analytics-controller/CHANGELOG.md |
Records the new functionality. |
Review details
Suppressed comments (3)
packages/analytics-controller/src/AnalyticsController.test.ts:5012
- This test name says the context remains unset, but the assertion verifies that
context.marketing: falseis added. The contradictory name makes the intended backward-compatibility behavior unclear; rename it to describe the stamped context.
it('keeps context unset when updating a persisted fragment that has none', async () => {
packages/analytics-controller/src/AnalyticsController.ts:809
- The public
initand#maybeResolveLocationdocumentation still describes geolocation as being deferred untiloptIn, but this new condition also starts it afteroptInToMarketing. Update those JSDoc blocks to document both consent lanes, since this changes when location data is requested.
// Resolve geolocation only when the user is already opted in to product or
// marketing analytics. For undecided or opted-out users it is deferred to
// {@link optIn} / {@link optInToMarketing}. Awaited so that an already-opted-in
// session has location available before events replay.
packages/analytics-controller/src/AnalyticsController.ts:1623
- This admits a mixed fragment as the marketing lane, but
#emitEventFragmentstill sends each declared name throughtrackEvent, which reclassifies it from the individual name. With product consent off and marketing consent on, a mixed fragment's productinitialEventis therefore silently dropped even though the changelog says mixed fragments are treated as marketing. Propagate the fragment lane through emission (or otherwise make the mixed-fragment policy consistent) so all of its lifecycle events are gated and stamped the same way.
const captureAllowed = fragment
? this.#isCaptureAllowed(this.#laneFromFragment(fragment))
: this.#isCaptureAllowed(AnalyticsLane.Product) ||
this.#isCaptureAllowed(AnalyticsLane.Marketing);
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Classify identify before trusting context.marketing so a caller-supplied marketing stamp cannot retain identify across product opt-out. Co-authored-by: Cursor <cursoragent@cursor.com>
…ng lanes Keep persisted fragments on their capture-time lane when marketingEventNames is missing or changed, while create still classifies and stamps from names. Co-authored-by: Cursor <cursoragent@cursor.com>
Make #laneFromQueuedEvent exhaustive after the identify-first check so the unreachable product fallback no longer breaks the coverage threshold. Co-authored-by: Cursor <cursoragent@cursor.com>
…delivery Replace exclusive marketing/product lanes and context.marketing with purpose classification, consent.categoryPreferences stamping, and capture-time eventPurposes snapshots on queues and fragments. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Current version of PR was reviewed by /review-bugbot on Sep 17, 06:48 GMT+2. It flagged 0 findings. Bugbot on commit |
…refresh Cover the no-op path in #refreshQueuedEventConsent and reformat the test file so CI lint:misc and package coverage checks pass. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 473b405. Configure here.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues remain in queue consent refresh, config fallback handling, and adapter context typing.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
packages/analytics-controller/src/AnalyticsController.ts:1007
- The fallback only handles an absent map entry.
AnalyticsPurpose[]permits an empty array, and persisted config is not runtime-validated, so an entry such asevents: { Foo: [] }makes#isCaptureAllowedfalse and silently dropsFooeven when product consent is enabled. Treat invalid/empty configured values as product-only (or validate the config before populating this map) so malformed persisted data cannot suppress events.
#purposesFromName(name: string): AnalyticsPurpose[] {
return [...(this.#eventPurposes.get(name) ?? [AnalyticsPurpose.Product])];
}
packages/analytics-controller/src/AnalyticsController.ts:1859
- The public
trackEventdocumentation immediately above this new purpose-based dispatch still says events are tracked only when analytics is enabled, meaning product opt-in. This contradicts the added behavior that delivers marketing-only events with product consent off; update the method documentation to describe the independent purpose gate and optional pre-consent holding.
this.#purposesFromName(event.name),
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Lite
…ing wording Use eventsConfig, eventsConfigVersion, and fetchEventsConfig so classification naming is purpose-agnostic while marketing consent APIs stay unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
…-in races Retain pre-consent queue entries that are already allowed so overlapping consent changes during the geolocation await cannot drop them before replay. Co-authored-by: Cursor <cursoragent@cursor.com>
Re-stamp queued events with current consent before replaying so adapters do not receive stale categoryPreferences after consent changes. Co-authored-by: Cursor <cursoragent@cursor.com>
Document product and marketing as optional categoryPreferences fields without narrowing the public context API for other Segment consent data. Co-authored-by: Cursor <cursoragent@cursor.com>
…uneAllForConsent The helper prunes both queued events and fragments after consent changes. Co-authored-by: Cursor <cursoragent@cursor.com>
NicolasMassart
left a comment
There was a problem hiding this comment.
I would address the missing runtime validation of persisted eventsConfig before approval because this configuration now participates directly in consent-based delivery decisions.
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
## Explanation Creates monorepo release 1265.0.0 with one package release: - `@metamask/analytics-controller` 3.1.0 This release ships independent marketing consent and purpose-aware event classification. It adds `optedInToMarketing`, `optInToMarketing` / `optOutOfMarketing` / `resetMarketingConsentDecision`, and a persisted `eventsConfig` whose unlisted events default to product-only. Named `track` and `view` payloads are delivered once with their allowed purposes in `context.consent.categoryPreferences` and their capture-time config version in `context.eventsConfigVersion`. Queues and fragments retain capture-time purpose classification so config changes cannot reclassify captured events. Mixed-purpose fragments classify each declared lifecycle event independently. Workspace dependents `@metamask/network-controller` and `@metamask/wallet-cli` are aligned to `@metamask/analytics-controller` `^3.1.0`. The release also includes pending dependency bumps for `uuid` (`^8.3.2` → `^11.1.1`) and `@metamask/utils` (`^11.12.0` → `^12.0.0`). ## References - MetaMask#10232 - MetaMask#10117 - MetaMask#10243 - MetaMask#10192 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them Made with [Cursor](https://cursor.com) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>

Explanation
Product analytics (
optedIn) and marketing analytics (optedInToMarketing) are independent consents onAnalyticsController.Named
track/viewevents are classified from persistedeventsConfig(unlisted names default to product-only). Each payload is delivered once when at least one eligible purpose is opted in. Allowed purposes are stamped in Segment'scontext.consent.categoryPreferences, with the capture-time config version incontext.eventsConfigVersion. Queues and fragments keep capture-timeeventPurposesso later config changes cannot reclassify already-captured events. Dual-purpose events sent while one purpose is opted in and the other undecided are not replayed after the second decision.identifystays product-only.Phase 1: there is no remote fetch yet.
#fetchEventsConfigis a no-op stub. Classification uses whatever config is already persisted in state.References
context.marketingapproach in favor of one-delivery consent contextChecklist
Note
Medium Risk
Changes consent gating, queue/fragment lifecycle, and payload context for all analytics delivery paths; mistakes could drop or mis-stamp events, though behavior is heavily covered by new tests.
Overview
AnalyticsController now treats product (
optedIn) and marketing (optedInToMarketing) as separate consents, with newoptInToMarketing,optOutOfMarketing, andresetMarketingConsentDecisionactions alongside the existing product consent APIs.Named
trackandviewevents are classified from persistedeventsConfig(unlisted names stay product-only). A payload is sent once when any eligible purpose is allowed, with allowed purposes stamped in Segment-stylecontext.consent.categoryPreferencesand capture-timecontext.eventsConfigVersion.identifyremains product-only. Pre-consent queues, delivery queues, and event fragments storeeventPurposes(and config version) at capture time so later config or consent changes cannot reclassify or replay dual-purpose events that were already sent for one purpose.Consent reconciliation prunes queued events and fragments by purpose (e.g. marketing opt-out drops marketing-only work while product tracks can remain). Geolocation resolution now runs when either product or marketing consent is active. Phase 1 leaves
#fetchEventsConfigas a stub—classification uses config already in state until a remote registry is wired up.Reviewed by Cursor Bugbot for commit 3e86f66. Bugbot is set up for automated code reviews on this repo. Configure here.