Skip to content

feat(analytics-controller): add independent marketing consent with purpose-aware delivery - #10232

Merged
gauthierpetetin merged 14 commits into
mainfrom
feat/analytics-marketing-consent
Sep 18, 2026
Merged

gauthierpetetin merged 14 commits into
mainfrom
feat/analytics-marketing-consent

Conversation

@gauthierpetetin

@gauthierpetetin gauthierpetetin commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Explanation

Product analytics (optedIn) and marketing analytics (optedInToMarketing) are independent consents on AnalyticsController.

Named track / view events are classified from persisted eventsConfig (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's context.consent.categoryPreferences, with the capture-time config version in context.eventsConfigVersion. Queues and fragments keep capture-time eventPurposes so 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. identify stays product-only.

Phase 1: there is no remote fetch yet. #fetchEventsConfig is a no-op stub. Classification uses whatever config is already persisted in state.

References

  • Related to marketing consent / purpose-aware analytics work (config-registry events-config fetch deferred to a later phase)
  • Closes the exclusive-lane / context.marketing approach in favor of one-delivery consent context

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

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 new optInToMarketing, optOutOfMarketing, and resetMarketingConsentDecision actions alongside the existing product consent APIs.

Named track and view events are classified from persisted eventsConfig (unlisted names stay product-only). A payload is sent once when any eligible purpose is allowed, with allowed purposes stamped in Segment-style context.consent.categoryPreferences and capture-time context.eventsConfigVersion. identify remains product-only. Pre-consent queues, delivery queues, and event fragments store eventPurposes (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 #fetchEventsConfig as 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.

…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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/analytics-controller/src/AnalyticsController.ts Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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: false is 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 init and #maybeResolveLocation documentation still describes geolocation as being deferred until optIn, but this new condition also starts it after optInToMarketing. 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 #emitEventFragment still sends each declared name through trackEvent, which reclassifies it from the individual name. With product consent off and marketing consent on, a mixed fragment's product initialEvent is 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.

Comment thread packages/analytics-controller/src/AnalyticsController.ts Outdated
Comment thread packages/analytics-controller/src/AnalyticsController.ts Outdated
gauthierpetetin and others added 4 commits September 15, 2026 07:06
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>
@gauthierpetetin gauthierpetetin changed the title feat(analytics-controller): add independent marketing consent and Segment context flag feat(analytics-controller): add independent marketing consent with purpose-aware delivery Sep 17, 2026
@cursor

cursor Bot commented Sep 17, 2026

Copy link
Copy Markdown

Current version of PR was reviewed by /review-bugbot on Sep 17, 06:48 GMT+2. It flagged 0 findings.

Bugbot on commit 923df1e is skipped.

…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/analytics-controller/src/AnalyticsController.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 as events: { Foo: [] } makes #isCaptureAllowed false and silently drops Foo even 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 trackEvent documentation 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

Comment thread packages/analytics-controller/src/AnalyticsController.ts Outdated
Comment thread packages/analytics-controller/src/AnalyticsPlatformAdapter.types.ts Outdated
gauthierpetetin and others added 5 commits September 17, 2026 07:23
…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 NicolasMassart left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would address the missing runtime validation of persisted eventsConfig before approval because this configuration now participates directly in consent-based delivery decisions.

Comment thread packages/analytics-controller/src/AnalyticsController.ts Outdated
Comment thread packages/analytics-controller/src/AnalyticsController.ts Outdated
@gauthierpetetin

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@10.0.1-preview-3e86f66f7
@metamask-previews/accounts-controller@40.0.0-preview-3e86f66f7
@metamask-previews/address-book-controller@8.0.0-preview-3e86f66f7
@metamask-previews/ai-controllers@2.0.0-preview-3e86f66f7
@metamask-previews/analytics-controller@3.0.0-preview-3e86f66f7
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-3e86f66f7
@metamask-previews/announcement-controller@9.0.0-preview-3e86f66f7
@metamask-previews/app-metadata-controller@3.0.0-preview-3e86f66f7
@metamask-previews/approval-controller@10.0.0-preview-3e86f66f7
@metamask-previews/assets-controller@16.0.0-preview-3e86f66f7
@metamask-previews/assets-controllers@112.0.1-preview-3e86f66f7
@metamask-previews/authenticated-user-storage@4.0.0-preview-3e86f66f7
@metamask-previews/base-controller@10.0.0-preview-3e86f66f7
@metamask-previews/base-data-service@2.0.0-preview-3e86f66f7
@metamask-previews/bitcoin-regtest-up@2.0.0-preview-3e86f66f7
@metamask-previews/bridge-controller@81.1.0-preview-3e86f66f7
@metamask-previews/bridge-status-controller@76.2.0-preview-3e86f66f7
@metamask-previews/build-utils@4.0.0-preview-3e86f66f7
@metamask-previews/chain-agnostic-permission@2.0.0-preview-3e86f66f7
@metamask-previews/chomp-api-service@5.0.0-preview-3e86f66f7
@metamask-previews/claims-controller@1.0.1-preview-3e86f66f7
@metamask-previews/client-controller@2.0.0-preview-3e86f66f7
@metamask-previews/client-utils@3.0.0-preview-3e86f66f7
@metamask-previews/compliance-controller@3.0.0-preview-3e86f66f7
@metamask-previews/composable-controller@13.0.0-preview-3e86f66f7
@metamask-previews/config-registry-controller@4.0.0-preview-3e86f66f7
@metamask-previews/connectivity-controller@1.0.0-preview-3e86f66f7
@metamask-previews/controller-utils@13.0.0-preview-3e86f66f7
@metamask-previews/core-backend@10.0.1-preview-3e86f66f7
@metamask-previews/cryptography@0.0.0-preview-3e86f66f7
@metamask-previews/delegation-controller@4.0.0-preview-3e86f66f7
@metamask-previews/earn-controller@13.0.0-preview-3e86f66f7
@metamask-previews/eip-5792-middleware@4.0.0-preview-3e86f66f7
@metamask-previews/eip-7702-internal-rpc-middleware@1.0.0-preview-3e86f66f7
@metamask-previews/eip1193-permission-middleware@3.0.0-preview-3e86f66f7
@metamask-previews/eth-block-tracker@16.0.0-preview-3e86f66f7
@metamask-previews/eth-json-rpc-middleware@25.0.0-preview-3e86f66f7
@metamask-previews/eth-json-rpc-provider@7.0.0-preview-3e86f66f7
@metamask-previews/foundryup@2.0.0-preview-3e86f66f7
@metamask-previews/gas-fee-controller@27.0.0-preview-3e86f66f7
@metamask-previews/gator-permissions-controller@6.0.0-preview-3e86f66f7
@metamask-previews/geolocation-controller@2.0.0-preview-3e86f66f7
@metamask-previews/java-tron-up@2.0.0-preview-3e86f66f7
@metamask-previews/json-rpc-engine@11.0.0-preview-3e86f66f7
@metamask-previews/json-rpc-middleware-stream@9.0.0-preview-3e86f66f7
@metamask-previews/keyring-controller@28.0.0-preview-3e86f66f7
@metamask-previews/kyc-controller@0.3.0-preview-3e86f66f7
@metamask-previews/local-node-utils@2.0.0-preview-3e86f66f7
@metamask-previews/logging-controller@10.0.0-preview-3e86f66f7
@metamask-previews/message-manager@15.0.0-preview-3e86f66f7
@metamask-previews/messenger@3.0.0-preview-3e86f66f7
@metamask-previews/messenger-cli@1.0.0-preview-3e86f66f7
@metamask-previews/money-account-api-data-service@1.0.0-preview-3e86f66f7
@metamask-previews/money-account-balance-service@3.0.0-preview-3e86f66f7
@metamask-previews/money-account-controller@2.0.0-preview-3e86f66f7
@metamask-previews/money-account-upgrade-controller@5.0.0-preview-3e86f66f7
@metamask-previews/money-account-utils@2.0.0-preview-3e86f66f7
@metamask-previews/multichain-account-service@14.0.0-preview-3e86f66f7
@metamask-previews/multichain-api-middleware@5.0.0-preview-3e86f66f7
@metamask-previews/multichain-network-controller@4.0.0-preview-3e86f66f7
@metamask-previews/multichain-transactions-controller@8.0.0-preview-3e86f66f7
@metamask-previews/name-controller@10.0.0-preview-3e86f66f7
@metamask-previews/network-connection-banner-controller@1.0.0-preview-3e86f66f7
@metamask-previews/network-controller@37.0.0-preview-3e86f66f7
@metamask-previews/network-enablement-controller@7.0.0-preview-3e86f66f7
@metamask-previews/notification-services-controller@28.0.1-preview-3e86f66f7
@metamask-previews/passkey-controller@4.0.0-preview-3e86f66f7
@metamask-previews/permission-controller@14.0.0-preview-3e86f66f7
@metamask-previews/permission-log-controller@6.0.0-preview-3e86f66f7
@metamask-previews/perps-controller@17.1.0-preview-3e86f66f7
@metamask-previews/phishing-controller@18.0.0-preview-3e86f66f7
@metamask-previews/platform-api-docs@0.2.0-preview-3e86f66f7
@metamask-previews/polling-controller@17.0.0-preview-3e86f66f7
@metamask-previews/preferences-controller@24.0.0-preview-3e86f66f7
@metamask-previews/profile-metrics-controller@5.1.0-preview-3e86f66f7
@metamask-previews/profile-sync-controller@32.1.0-preview-3e86f66f7
@metamask-previews/ramps-controller@22.0.0-preview-3e86f66f7
@metamask-previews/rate-limit-controller@8.0.0-preview-3e86f66f7
@metamask-previews/react-data-query@2.0.0-preview-3e86f66f7
@metamask-previews/remote-feature-flag-controller@7.0.0-preview-3e86f66f7
@metamask-previews/sample-controllers@6.0.0-preview-3e86f66f7
@metamask-previews/seedless-onboarding-controller@11.0.0-preview-3e86f66f7
@metamask-previews/selected-network-controller@27.0.0-preview-3e86f66f7
@metamask-previews/sentinel-api-service@2.0.0-preview-3e86f66f7
@metamask-previews/shield-controller@7.0.1-preview-3e86f66f7
@metamask-previews/signature-controller@40.0.0-preview-3e86f66f7
@metamask-previews/smart-transactions-controller@27.0.1-preview-3e86f66f7
@metamask-previews/snap-account-service@3.0.0-preview-3e86f66f7
@metamask-previews/social-controllers@3.0.1-preview-3e86f66f7
@metamask-previews/solana-test-validator-up@2.0.0-preview-3e86f66f7
@metamask-previews/stellar-quickstart-up@0.0.0-preview-3e86f66f7
@metamask-previews/storage-service@2.0.0-preview-3e86f66f7
@metamask-previews/subscription-controller@9.0.1-preview-3e86f66f7
@metamask-previews/transaction-controller@70.0.0-preview-3e86f66f7
@metamask-previews/transaction-pay-controller@28.0.2-preview-3e86f66f7
@metamask-previews/user-operation-controller@42.0.0-preview-3e86f66f7
@metamask-previews/utils@12.0.0-preview-3e86f66f7
@metamask-previews/wallet@13.0.0-preview-3e86f66f7
@metamask-previews/wallet-cli@0.0.0-preview-3e86f66f7

@gauthierpetetin
gauthierpetetin added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 34bb3e8 Sep 18, 2026
154 checks passed
@gauthierpetetin
gauthierpetetin deleted the feat/analytics-marketing-consent branch September 18, 2026 13:34
@gauthierpetetin gauthierpetetin mentioned this pull request Sep 18, 2026
3 of 4 tasks
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Sep 18, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants