Skip to content

Add Matomo analytics integration with GDPR consent support#341

Merged
t0mdavid-m merged 4 commits intomainfrom
claude/add-matomo-tracking-mode-L7f5S
Feb 20, 2026
Merged

Add Matomo analytics integration with GDPR consent support#341
t0mdavid-m merged 4 commits intomainfrom
claude/add-matomo-tracking-mode-L7f5S

Conversation

@t0mdavid-m
Copy link
Member

@t0mdavid-m t0mdavid-m commented Feb 19, 2026

Summary

This PR adds support for Matomo Tag Manager as an analytics provider alongside existing Google Analytics and Piwik Pro integrations. The implementation includes GDPR consent management integration to ensure user privacy compliance.

Key Changes

  • Analytics Hook (hooks/hook-analytics.py): Added matomo_head() function to inject Matomo Tag Manager script into the HTML head section with proper initialization
  • Settings Configuration (settings.json): Added Matomo configuration section with enabled, url, and tag fields (disabled by default)
  • Consent Management (src/common/captcha_.py): Extended consent flow to include Matomo alongside existing analytics providers, passing matomo flag to GDPR consent component
  • Streamlit Integration (src/common/common.py): Added Matomo consent tracking that pushes MTMSetConsentGiven event to Matomo when user grants consent
  • GDPR Consent Component (gdpr_consent/src/main.ts): Extended Klaro configuration to register Matomo as a service with analytics purpose and consent callbacks
  • Bundle Update (gdpr_consent/dist/bundle.js): Compiled bundle reflecting TypeScript changes to consent component

Implementation Details

  • Matomo integration follows the same pattern as existing analytics providers (Google Analytics, Piwik Pro)
  • Consent is checked before injecting Matomo tracking code, respecting user privacy preferences
  • The Matomo Tag Manager script is injected into the page head with async loading
  • Matomo is integrated into the Klaro consent management system for unified GDPR compliance

https://claude.ai/code/session_0165AXHkmRZ6bx23n7Tbyz8h

Summary by CodeRabbit

  • New Features
    • Added Matomo analytics integration with GDPR consent support.
    • New configurable Matomo settings to enable/disable the provider, set server URL and tag.
    • Matomo consent is now honored alongside existing analytics providers so user preferences apply consistently across tracking.

Adds Matomo Tag Manager support alongside existing Google Analytics and
Piwik Pro integrations. Includes settings.json configuration (url + tag),
build-time script injection via hook-analytics.py, Klaro GDPR consent
banner integration, and runtime consent granting via MTM data layer API.

https://claude.ai/code/session_0165AXHkmRZ6bx23n7Tbyz8h
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds Matomo analytics support: new analytics.matomo settings, Matomo Tag Manager script injected into the HTML head, Matomo included in the Klaro consent UI, and runtime consent handling that signals Matomo when users accept or decline tracking.

Changes

Cohort / File(s) Summary
Configuration
settings.json
Added/updated analytics.matomo block (enabled, url, tag).
Consent UI
gdpr_consent/src/main.ts
Detects data.args['matomo'] and pushes a Matomo service entry into klaroConfig.services with the same onAccept/onDecline callbacks.
Head Injection
hooks/hook-analytics.py
Added matomo_head(matomo_url, matomo_tag) and conditional insertion of Matomo Tag Manager script into the HTML head when analytics.matomo.enabled is true.
Runtime Consent Flow
src/common/captcha_.py, src/common/common.py
Extended consent checks to include Matomo; added runtime script to push MTMSetConsentGiven to Matomo data layer when Matomo consent is granted; consent flow triggers rerun/stop behavior unchanged.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page
    participant Config
    participant HeadInjector
    participant ConsentChecker
    participant GDPRUI
    participant RuntimeHandler
    participant Matomo

    User->>Page: Request page
    Page->>Config: Read analytics settings (includes matomo)
    alt matomo.enabled == true
        Page->>HeadInjector: Insert Matomo Tag Manager (matomo_head)
        Page->>ConsentChecker: Get tracking_consent
        ConsentChecker->>GDPRUI: Render consent UI (includes Matomo)
        GDPRUI-->>User: Show banner / choices
        User-->>GDPRUI: Accept or Decline
        GDPRUI-->>ConsentChecker: Return consent result
        ConsentChecker-->>Page: Store consent and trigger rerun if needed
        alt consent granted
            Page->>RuntimeHandler: Execute Matomo consent script
            RuntimeHandler->>Matomo: Push MTMSetConsentGiven
        else consent denied
            Note right of Matomo: Do not enable tracking
        end
    else
        Note right of Page: Skip Matomo injection and consent
    end
Loading

Poem

🐰 I nudged the tag into the head,
Matomo wakes from its cozy bed,
A banner hops, users choose with care,
Consent whispered on the datalayer,
🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Matomo analytics integration with GDPR consent support' accurately and specifically describes the main objective of the PR—adding Matomo analytics with GDPR compliance.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/add-matomo-tracking-mode-L7f5S

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hooks/hook-analytics.py`:
- Around line 59-72: The matomo_head function can produce a double-slash when
matomo_url ends with '/', so update matomo_head to normalize matomo_url by
stripping any trailing slashes (e.g., using matomo_url.rstrip('/')) before
interpolating it into the script src; use the normalized value when building the
container URL (container_{matomo_tag}.js) so the final src is always
".../js/container_{matomo_tag}.js" with a single slash regardless of input.

In `@src/common/common.py`:
- Around line 408-410: The conditional uses an explicit equality check
"st.session_state.tracking_consent['matomo'] == True" which triggers Ruff E712;
change it to a truthiness check so the condition reads as a boolean expression
(e.g., use the existing left-hand check with just
st.session_state.tracking_consent['matomo']), and apply the same change pattern
to the similar GA and Piwik Pro checks; edit the conditional containing
st.session_state.settings["analytics"]["matomo"]["enabled"] and
st.session_state.tracking_consent["matomo"] to remove the "== True" comparison
and rely on truthiness instead.

Comment on lines 59 to 72
def matomo_head(matomo_url, matomo_tag):
return f"""
<!-- Matomo Tag Manager -->
<script type="text/javascript">
window._mtm = window._mtm || [];
window._mtm.push({{'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'}});
(function() {{
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='{matomo_url}/js/container_{matomo_tag}.js';
s.parentNode.insertBefore(g,s);
}})();
</script>
<!-- End Matomo Tag Manager -->
"""
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Strip trailing slash from matomo_url to avoid a double-slash URL.

If matomo_url is configured as https://analytics.example.com/ (with a trailing slash), line 67 produces https://analytics.example.com//js/container_{tag}.js. While many servers normalize double slashes, some CDNs and strict reverse proxies will reject them, causing the MTM container script to fail silently.

🔧 Proposed fix
 def matomo_head(matomo_url, matomo_tag):
+    matomo_url = matomo_url.rstrip('/')
     return f"""
     <!-- Matomo Tag Manager -->
     <script type="text/javascript">
     window._mtm = window._mtm || [];
     window._mtm.push({{'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'}});
     (function() {{
         var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
         g.async=true; g.src='{matomo_url}/js/container_{matomo_tag}.js';
         s.parentNode.insertBefore(g,s);
     }})();
     </script>
     <!-- End Matomo Tag Manager -->
     """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/hook-analytics.py` around lines 59 - 72, The matomo_head function can
produce a double-slash when matomo_url ends with '/', so update matomo_head to
normalize matomo_url by stripping any trailing slashes (e.g., using
matomo_url.rstrip('/')) before interpolating it into the script src; use the
normalized value when building the container URL (container_{matomo_tag}.js) so
the final src is always ".../js/container_{matomo_tag}.js" with a single slash
regardless of input.

Comment on lines +408 to +410
if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and (
st.session_state.tracking_consent["matomo"] == True
):
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace == True equality comparison with a truthiness check (Ruff E712).

Ruff flags this as E712. The same pattern exists for the pre-existing GA and Piwik Pro blocks (lines 368, 386), but should be fixed here in the new code.

🔧 Proposed fix
-        if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and (
-            st.session_state.tracking_consent["matomo"] == True
-        ):
+        if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and (
+            st.session_state.tracking_consent["matomo"]
+        ):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and (
st.session_state.tracking_consent["matomo"] == True
):
if (st.session_state.settings["analytics"]["matomo"]["enabled"]) and (
st.session_state.tracking_consent["matomo"]
):
🧰 Tools
🪛 Ruff (0.15.1)

[error] 409-409: Avoid equality comparisons to True; use st.session_state.tracking_consent["matomo"]: for truth checks

Replace with st.session_state.tracking_consent["matomo"]

(E712)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common/common.py` around lines 408 - 410, The conditional uses an
explicit equality check "st.session_state.tracking_consent['matomo'] == True"
which triggers Ruff E712; change it to a truthiness check so the condition reads
as a boolean expression (e.g., use the existing left-hand check with just
st.session_state.tracking_consent['matomo']), and apply the same change pattern
to the similar GA and Piwik Pro checks; edit the conditional containing
st.session_state.settings["analytics"]["matomo"]["enabled"] and
st.session_state.tracking_consent["matomo"] to remove the "== True" comparison
and rely on truthiness instead.

- Accept full container JS URL instead of separate url + tag fields,
  supporting both self-hosted and Matomo Cloud URL patterns
- Match the official snippet: var _mtm alias, _mtm.push shorthand
- Remove redundant type="text/javascript" attribute
- Remove unused "tag" field from settings.json

https://claude.ai/code/session_0165AXHkmRZ6bx23n7Tbyz8h
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@settings.json`:
- Around line 11-13: The Piwik Pro analytics config in settings.json currently
contains a real container ID and is enabled; update the "piwik-pro" block by
setting "enabled" to false and replacing the live "tag" value
("57690c44-d635-43b0-ab43-f8bd3064ca06") with a placeholder like
"YOUR_PIWIK_PRO_TAG_HERE" so templates do not send data to the original account;
ensure the changes are made to the "piwik-pro" object keys "enabled" and "tag".
- Around line 15-18: The matomo config can produce a broken <script src="">
because hook-analytics.py's matomo_head() interpolates matomo_url without
validation; update hook-analytics.py to check the matomo["url"] (and enabled
flag) and only generate/inject the Matomo script when the URL is non-empty and
matches a simple URL pattern (e.g., starts with http:// or https:// and contains
a domain), returning early or skipping injection otherwise, and add a clear
error/info log when enabled is true but URL is missing/invalid; alternatively
(or additionally) add README docs describing the matomo.url expected value (full
container script URL like https://<your-domain>/js/container_<id>.js) and note
the field-name asymmetry versus tag for other providers.

settings.json Outdated
Comment on lines 11 to 13
"piwik-pro": {
"enabled": true,
"tag": "57690c44-d635-43b0-ab43-f8bd3064ca06"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pre-existing: real Piwik Pro container ID committed in a public template

"tag": "57690c44-d635-43b0-ab43-f8bd3064ca06" with "enabled": true appears to be a live container ID. In a public template repository, every fork/deployment that doesn't override this value will silently send analytics to the original account. Since the analytics block is being touched in this PR, this is a good opportunity to replace the real tag with a placeholder (e.g., "YOUR_PIWIK_PRO_TAG_HERE") and flip enabled to false, matching the pattern used by the other two providers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@settings.json` around lines 11 - 13, The Piwik Pro analytics config in
settings.json currently contains a real container ID and is enabled; update the
"piwik-pro" block by setting "enabled" to false and replacing the live "tag"
value ("57690c44-d635-43b0-ab43-f8bd3064ca06") with a placeholder like
"YOUR_PIWIK_PRO_TAG_HERE" so templates do not send data to the original account;
ensure the changes are made to the "piwik-pro" object keys "enabled" and "tag".

Separate the Matomo setting into `url` (base URL, e.g.
https://cdn.matomo.cloud/openms.matomo.cloud) and `tag` (container ID,
e.g. yDGK8bfY), consistent with how other providers use a tag field.
The script constructs the full path: {url}/container_{tag}.js

https://claude.ai/code/session_0165AXHkmRZ6bx23n7Tbyz8h
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@hooks/hook-analytics.py`:
- Around line 59-71: The matomo_head function can produce a double-slash when
matomo_url ends with '/', so normalize matomo_url by stripping trailing slashes
before using it in the template (e.g., compute a local variable like base_url =
matomo_url.rstrip('/') and use base_url when building the container script src).
Update matomo_head to use the normalized URL (reference: matomo_head(matomo_url,
matomo_tag)) so the generated src is always single-slash between host and
container path.

@t0mdavid-m t0mdavid-m merged commit 44415ba into main Feb 20, 2026
5 of 8 checks passed
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