Skip to content

Add Fides.matomo() integration helper#7991

Open
gilluminate wants to merge 7 commits intomainfrom
gill/matomo-integration/fides-matomo-helper
Open

Add Fides.matomo() integration helper#7991
gilluminate wants to merge 7 commits intomainfrom
gill/matomo-integration/fides-matomo-helper

Conversation

@gilluminate
Copy link
Copy Markdown
Contributor

@gilluminate gilluminate commented Apr 21, 2026

Ticket ENG-3295

Description Of Changes

New Fides.matomo() integration helper that automatically syncs Fides consent decisions with Matomo's tracking consent and cookie consent APIs via _paq.push(). Follows the same Gen 2 integration pattern as Fides.gcm(), Fides.aep(), and Fides.blueconic().

Key design decisions:

  • Conditional requireConsent: Rather than always calling Matomo's requireConsent() on init (which would block tracking for users in non-consent regions), the integration defers this call until the first consent event where the analytics or performance key is present in the Fides consent object. This follows GCM's in check pattern and correctly handles both OMIT (default) and INCLUDE non-applicable flag modes without any user configuration.
  • No requireConsent option: Unlike the original PRD spec, this option was removed. All other Fides integrations (GCM, AEP, BlueConic, Shopify) let Fides handle jurisdiction awareness rather than exposing a toggle. The conditional activation approach achieves the same goal automatically.
  • rememberConsent defaults to true: Uses Matomo's rememberConsentGiven() to persist consent via Matomo's mtm_consent cookie, so tracking can resume on page loads before Fides initializes.

Public API:

Fides.matomo({
  consentMode: "tracking" | "cookie" | "both",  // default: "tracking"
  rememberConsent: true | false,                  // default: true
})

Code Changes

  • New clients/fides-js/src/integrations/matomo.ts - core integration
  • New clients/fides-js/__tests__/integrations/matomo.test.ts - 26 unit tests
  • Modified clients/fides-js/src/lib/consent-types.ts - add matomo to FidesGlobal
  • Modified clients/fides-js/src/lib/init-utils.ts - wire matomo into getCoreFides()
  • Modified clients/fides-js/src/docs/fides.ts - JSDoc for Fides.matomo()
  • New Cypress E2E tests in consent-third-party.cy.ts for the Matomo integration
  • Added _paq stub to Cypress visitConsentDemo command
  • Added Fides.matomo() call to fides-js-components-demo.html

Steps to Confirm

Automated:

  1. cd clients/fides-js && npx jest __tests__/integrations/matomo.test.ts - 26 unit tests pass
  2. cd clients/fides-js && npm run check - lint, format, typecheck all pass
  3. cd clients/fides-js && npm run build - build succeeds
  4. Cypress E2E: consent-third-party.cy.ts Matomo tests pass

Manual (demo page):
Use https://developer.matomo.org/guides/tracking-consent for reference

  1. Start the local backend and privacy center
  2. Visit http://localhost:3001/fides-js-demo.html
  3. Open browser DevTools console
  4. Call Fides.matomo() in the console
  5. Inspect window._paq - should contain ["requireConsent"] followed by a grant or revoke command depending on the default consent state
  6. Interact with the consent banner (accept/reject) and re-inspect window._paq - should see ["rememberConsentGiven"] after accepting or ["forgetConsentGiven"] after rejecting
  7. Test with options: Fides.matomo({ consentMode: "both" }) - should see both tracking and cookie consent commands
  8. Test with Fides.matomo({ rememberConsent: false }) - should see ["setConsentGiven"] instead of ["rememberConsentGiven"]

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Apr 22, 2026 8:47pm
fides-privacy-center Ignored Ignored Apr 22, 2026 8:47pm

Request Review

gilluminate and others added 3 commits April 21, 2026 16:09
New integration that syncs Fides consent with Matomo's tracking consent
and cookie consent APIs via _paq.push(). Follows the Gen 2 integration
pattern (same as GCM, AEP, BlueConic).

Key behavior: requireConsent is conditionally activated based on the
presence of the consent key in the Fides consent object, matching GCM's
pattern. This correctly handles both OMIT and INCLUDE non-applicable
flag modes without any user configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add _paq stub to Cypress visitConsentDemo command
- Add Fides.matomo() call to fides-js-components-demo.html
- Add Matomo test section to consent-third-party.cy.ts covering:
  - _paq exists on window
  - No commands pushed when analytics key is absent
  - requireConsent + forgetConsentGiven on init with opt-out default
  - rememberConsentGiven after user opts in via banner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gilluminate gilluminate force-pushed the gill/matomo-integration/fides-matomo-helper branch from bc3b9c2 to 5411855 Compare April 21, 2026 22:09
@gilluminate
Copy link
Copy Markdown
Contributor Author

/code-review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 8%
6.33% (2799/44197) 5.57% (1402/25139) 4.43% (579/13069)
fides-js Coverage: 78%
79.4% (2012/2534) 65.99% (1238/1876) 73.09% (345/472)
privacy-center Coverage: 88%
85.97% (331/385) 81.36% (179/220) 78.87% (56/71)

claude[bot]

This comment was marked as outdated.

@gilluminate
Copy link
Copy Markdown
Contributor Author

Re: missing test for consentMode: "both" + rememberConsent: false — added the test case. It asserts setConsentGiven and setCookieConsentGiven are pushed (not the remember* variants).

- Capture _paq reference once, remove non-null assertions
- Document hardcoded consent key limitation in customer-facing docs
- Use optional chaining in demo HTML
- Remove tautological Cypress test
- Add missing test for consentMode "both" + rememberConsent false
- Regenerate typedoc markdown

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gilluminate gilluminate marked this pull request as ready for review April 21, 2026 23:22
@gilluminate gilluminate requested a review from a team as a code owner April 21, 2026 23:22
@gilluminate gilluminate requested review from lucanovera and removed request for a team April 21, 2026 23:22
@gilluminate
Copy link
Copy Markdown
Contributor Author

/code-review

@gilluminate gilluminate requested review from tvandort and removed request for lucanovera April 21, 2026 23:22
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Code Review: Fides.matomo() integration helper

The core implementation in matomo.ts is clean and well-structured. It follows the established Gen 2 integration pattern accurately, the module decomposition (pushRequireConsent / pushConsentUpdate / findConsentKey) is clear, and the 26 unit tests cover the key behavioral paths well. The JSDoc examples are thorough and the changelog entry is correct.

Must Fix

Misleading Cypress test name (inline comment on consent-third-party.cy.ts:531): The test is named "pushes requireConsent on FidesReady when analytics key is present" but asserts the opposite — no calls when the key is absent. Quick one-line rename.

Should Address

Unconditional matomo() call in demo page (inline comment on fides-js-components-demo.html:223): The previous gtm() call used an explicit falsy guard (!!window.fides_overrides && window.fides_overrides.gtmOptions) that passed false when the option was absent. Both gtm() and matomo() now receive undefined via optional chaining, which causes them to apply defaults and run unconditionally. For matomo() this silently initializes _paq and pushes requireConsent on every page load even without intent. The demo page is a reference pattern for integrators; guarding with if (window.fides_overrides?.matomoOptions) would restore the original semantics.

Test isolation fragility (inline comment on matomo.test.ts:67): The comment documents a real, unresolved ordering dependency — subscribeToConsent listeners cannot be cleaned up, so each matomo() call in tests accumulates permanent window event handlers. The current suite is safe only because this describe block runs first. Adding any new matomo() call above it would silently corrupt the assertion. If returning an unsubscribe function from subscribeToConsent is feasible, it would fix this for all integrations. At minimum the comment should be more explicit about the file-level ordering invariant.

Suggestions

  • requireConsentPushed is per-instance, not per-_paq (inline comment on matomo.ts:124): If matomo() is called twice, both closures push requireConsent and both consent commands on each event. Worth a doc note or a single-call guard.
  • DEFAULT_CONSENT_KEYS is not configurable (inline comment on matomo.ts:45): Sites with non-standard consent key names silently fall to free-tracking mode. An optional consentKeys override in MatomoOptions would be additive and non-breaking.
  • EMPTY_BUNDLE suppression lacks a comment (inline comment on rollup.config.mjs:316): A one-line comment explaining why the warning is expected would prevent future confusion.

🔬 Codegraph: unavailable


💡 Write /code-review in a comment to re-run this review.

Comment thread clients/privacy-center/public/fides-js-components-demo.html
Comment thread clients/fides-js/__tests__/integrations/matomo.test.ts
Comment thread clients/fides-js/src/integrations/matomo.ts Outdated
Comment thread clients/fides-js/src/integrations/matomo.ts
Comment thread clients/fides-js/rollup.config.mjs
- Fix misleading Cypress test name (says "present", tests "absent")
- Use module-level WeakSet guard for requireConsentPushed to prevent
  double-push when matomo() is called multiple times
- Add explanatory comment for EMPTY_BUNDLE rollup suppression

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

const DEFAULT_CONSENT_KEYS = ["analytics", "performance"];

const paqsWithRequireConsent = new WeakSet<unknown[][]>();
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'm curious what the rationale behind both WeakSet and this not being some sort of flag-like value is?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. The WeakSet came from a Claude Code review suggestion - it flagged that the original per-closure boolean would allow duplicate requireConsent pushes if matomo() was called more than once. I moved the guard to module scope per that review, and went with WeakSet keyed on the _paq array reference so it auto-resets if window._paq is ever replaced.

That said, none of our other integrations (GCM, AEP, BlueConic, GTM, Shopify) use this kind of guard or worry about the global array being swapped out. A simple let requireConsentPushed = false boolean would be more consistent with the rest of the codebase and easier to grok. Happy to simplify if you'd prefer that.

paq.push([command]);
fidesDebugger(`[Fides Matomo] Pushed ${command}`);
} else {
paq.push(["forgetConsentGiven"]);
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.

Should we always push forgetConsentGiven when analytics is false? I'd need to check Matomo's behavior but I can imagine a scenario in which a client using this extension could swap it's default behavior to just cookie for instance, deploy that change. Then a data subject could go opt out of analytics and we'd never push forgetConsentGiven I also wonder if Fides sees tracking as false whether it should also push forgetConsentGiven?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Matomo's forgetConsentGiven/forgetCookieConsentGiven are safe no-ops when consent was never granted, so I'll always push both revoke commands regardless of consentMode and only gate the grant path.

Comment on lines +97 to +106
if (granted) {
const command = rememberConsent
? "rememberCookieConsentGiven"
: "setCookieConsentGiven";
paq.push([command]);
fidesDebugger(`[Fides Matomo] Pushed ${command}`);
} else {
paq.push(["forgetCookieConsentGiven"]);
fidesDebugger("[Fides Matomo] Pushed forgetCookieConsentGiven");
}
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the same change as the tracking side fix above. Revoke commands will always push for both consent types regardless of consentMode.

paq: unknown[][],
consentMode: MatomoConsentMode,
): void => {
if (consentMode === "tracking" || consentMode === "both") {
Copy link
Copy Markdown
Contributor

@tvandort tvandort Apr 22, 2026

Choose a reason for hiding this comment

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

This is a small file and I don't think it's likely to change but IMO these checks would be better packaged into something like

object MatomoDecisions(consentMode) {
	ctor() {
		this.consentMode = consentMode
	}

	isTracking() {
      return consentMode === "tracking" || consentMode === "both";
    }

	/* etc */
}

// Usage
/* initialize matomoDecision object */
if (matomoDecision.isTracking()) { /* dostuff */ }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! Extracted consentModeChecks(consentMode) with isTracking() and isCookie() helpers, used in both pushRequireConsent and pushConsentUpdate.

gilluminate and others added 2 commits April 22, 2026 14:40
- Always push both forgetConsentGiven and forgetCookieConsentGiven on
  revoke regardless of consentMode to clean up stale consent cookies
- Extract consentModeChecks() helper for isTracking/isCookie checks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gilluminate gilluminate requested a review from tvandort April 22, 2026 21:23
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