2 Commits
Author SHA1 Message Date
westfarn d6bed439d7 Switch Tianji tracking from opt-in consent to acknowledge notice.
Load analytics on page load so visitors who never interact still produce data, while an informational banner lets users acknowledge the practice. Migrate legacy declined and accepted consent values so prior opt-outs are cleared and tracking resumes.

Closes #7
2026-07-02 09:18:43 -05:00
westfarn 351994673a Switch Tianji tracking from opt-in consent to acknowledge notice.
Load analytics on page load so visitors who never interact still produce data, while an informational banner lets users acknowledge the practice. Migrate legacy declined and accepted consent values so prior opt-outs are cleared and tracking resumes.

Closes #7
2026-07-02 11:52:20 +00:00
5 changed files with 64 additions and 61 deletions
+1 -1
View File
@@ -156,6 +156,6 @@ EMAIL_USE_TLS = True
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/'
# Tianji analytics (loaded only after user consent) # Tianji analytics (loaded on page load; users acknowledge via notice banner)
TIANJI_TRACKER_URL = 'https://tianji.aimloperations.com/tracker.js' TIANJI_TRACKER_URL = 'https://tianji.aimloperations.com/tracker.js'
TIANJI_WEBSITE_ID = 'cm7w80pyy020oddswy2evl957' TIANJI_WEBSITE_ID = 'cm7w80pyy020oddswy2evl957'
@@ -1,7 +1,9 @@
(function () { (function () {
'use strict'; 'use strict';
var CONSENT_KEY = 'aiml_analytics_consent'; var NOTICE_KEY = 'aiml_analytics_notice';
var LEGACY_CONSENT_KEY = 'aiml_analytics_consent';
var LEGACY_DISABLED_KEY = 'tianji.disabled';
var configEl = document.getElementById('tianji-config'); var configEl = document.getElementById('tianji-config');
if (!configEl) { if (!configEl) {
return; return;
@@ -10,19 +12,26 @@
var trackerUrl = configEl.dataset.trackerUrl; var trackerUrl = configEl.dataset.trackerUrl;
var websiteId = configEl.dataset.websiteId; var websiteId = configEl.dataset.websiteId;
var userId = configEl.dataset.userId || ''; var userId = configEl.dataset.userId || '';
var pendingConsentEvent = false;
function getConsent() { function getStorageItem(key) {
try { try {
return localStorage.getItem(CONSENT_KEY); return localStorage.getItem(key);
} catch (e) { } catch (e) {
return null; return null;
} }
} }
function setConsent(value) { function setStorageItem(key, value) {
try { try {
localStorage.setItem(CONSENT_KEY, value); localStorage.setItem(key, value);
} catch (e) {
/* ignore storage errors */
}
}
function removeStorageItem(key) {
try {
localStorage.removeItem(key);
} catch (e) { } catch (e) {
/* ignore storage errors */ /* ignore storage errors */
} }
@@ -71,51 +80,44 @@
script.onload = function () { script.onload = function () {
identifyUser(); identifyUser();
if (pendingConsentEvent) {
trackEvent('consent_accepted');
pendingConsentEvent = false;
}
document.dispatchEvent(new CustomEvent('tianji:ready')); document.dispatchEvent(new CustomEvent('tianji:ready'));
}; };
document.head.appendChild(script); document.head.appendChild(script);
} }
function acceptTracking() { function migrateLegacyConsent() {
setConsent('accepted'); var legacyConsent = getStorageItem(LEGACY_CONSENT_KEY);
hideBanner(); var legacyDisabled = getStorageItem(LEGACY_DISABLED_KEY);
try {
localStorage.removeItem('tianji.disabled'); if (legacyConsent === 'accepted') {
} catch (e) { setStorageItem(NOTICE_KEY, 'acknowledged');
/* ignore storage errors */ removeStorageItem(LEGACY_CONSENT_KEY);
removeStorageItem(LEGACY_DISABLED_KEY);
return 'migrated_acknowledged';
} }
pendingConsentEvent = true;
loadTracker(); if (legacyConsent === 'declined' || legacyDisabled === '1') {
removeStorageItem(LEGACY_CONSENT_KEY);
removeStorageItem(LEGACY_DISABLED_KEY);
return 'migrated_declined';
}
return null;
} }
function declineTracking() { function acknowledgeNotice() {
setConsent('declined'); setStorageItem(NOTICE_KEY, 'acknowledged');
hideBanner(); hideBanner();
try { window.aimlTrackWhenReady('notice_acknowledged');
localStorage.setItem('tianji.disabled', '1');
} catch (e) {
/* ignore storage errors */
}
if (window.aimlTrack) {
window.aimlTrack('consent_declined');
}
} }
function bindBannerControls() { function bindBannerControls() {
var acceptBtn = document.getElementById('cookie-consent-accept'); var acknowledgeBtn = document.getElementById('cookie-consent-acknowledge');
var declineBtn = document.getElementById('cookie-consent-decline');
var manageLinks = document.querySelectorAll('[data-open-cookie-preferences]'); var manageLinks = document.querySelectorAll('[data-open-cookie-preferences]');
if (acceptBtn) { if (acknowledgeBtn) {
acceptBtn.addEventListener('click', acceptTracking); acknowledgeBtn.addEventListener('click', acknowledgeNotice);
}
if (declineBtn) {
declineBtn.addEventListener('click', declineTracking);
} }
manageLinks.forEach(function (link) { manageLinks.forEach(function (link) {
link.addEventListener('click', function (event) { link.addEventListener('click', function (event) {
@@ -140,16 +142,14 @@
}); });
}; };
function initConsent() { function initNotice() {
bindBannerControls(); bindBannerControls();
loadTracker();
var consent = getConsent(); var migration = migrateLegacyConsent();
if (consent === 'accepted') { var notice = getStorageItem(NOTICE_KEY);
hideBanner();
loadTracker(); if (notice === 'acknowledged' || migration === 'migrated_acknowledged') {
return;
}
if (consent === 'declined') {
hideBanner(); hideBanner();
return; return;
} }
@@ -157,5 +157,5 @@
showBanner(); showBanner();
} }
initConsent(); initNotice();
})(); })();
+4 -5
View File
@@ -137,15 +137,14 @@
<div id="cookie-consent-banner" class="cookie-consent-banner" hidden role="dialog" aria-modal="true" <div id="cookie-consent-banner" class="cookie-consent-banner" hidden role="dialog" aria-modal="true"
aria-labelledby="cookie-consent-title" aria-describedby="cookie-consent-description"> aria-labelledby="cookie-consent-title" aria-describedby="cookie-consent-description">
<div class="cookie-consent-content"> <div class="cookie-consent-content">
<h2 id="cookie-consent-title" class="visually-hidden">Cookie consent</h2> <h2 id="cookie-consent-title" class="visually-hidden">Analytics notice</h2>
<p class="cookie-consent-text" id="cookie-consent-description"> <p class="cookie-consent-text" id="cookie-consent-description">
<span class="cookie-consent-text-full">We use analytics tracking to understand how visitors use our site. Tracking runs only if you accept. <span class="cookie-consent-text-full">We use analytics to understand how visitors use our site. This helps us improve performance and content.
See our <a href="{% url 'terms_of_service' %}">Terms of Service &amp; Privacy Policy</a> for details.</span> See our <a href="{% url 'terms_of_service' %}">Terms of Service &amp; Privacy Policy</a> for details.</span>
<span class="cookie-consent-text-short">We use analytics if you accept. <a href="{% url 'terms_of_service' %}">Privacy Policy</a></span> <span class="cookie-consent-text-short">We use analytics on this site. <a href="{% url 'terms_of_service' %}">Privacy Policy</a></span>
</p> </p>
<div class="cookie-consent-actions"> <div class="cookie-consent-actions">
<button type="button" id="cookie-consent-decline" class="btn btn-outline">Decline</button> <button type="button" id="cookie-consent-acknowledge" class="btn">Acknowledge</button>
<button type="button" id="cookie-consent-accept" class="btn">Accept Analytics</button>
</div> </div>
</div> </div>
</div> </div>
@@ -52,7 +52,7 @@
<ul> <ul>
<li><strong>Information you provide:</strong> name, email address, message content, and other details submitted through contact forms or account-related requests.</li> <li><strong>Information you provide:</strong> name, email address, message content, and other details submitted through contact forms or account-related requests.</li>
<li><strong>Account information:</strong> username and authentication data for authorized users of internal tools.</li> <li><strong>Account information:</strong> username and authentication data for authorized users of internal tools.</li>
<li><strong>Usage and analytics data:</strong> if you accept analytics in our cookie banner, we collect information about how you interact with the site, as described in Section 4.2.</li> <li><strong>Usage and analytics data:</strong> information about how you interact with the site, as described in Section 4.2.</li>
<li><strong>Technical information:</strong> browser type, device type, operating system, language, referring URLs, and similar data collected automatically when you use the site.</li> <li><strong>Technical information:</strong> browser type, device type, operating system, language, referring URLs, and similar data collected automatically when you use the site.</li>
</ul> </ul>
<p> <p>
@@ -63,9 +63,9 @@
<h3>4.2 Analytics and Usage Tracking</h3> <h3>4.2 Analytics and Usage Tracking</h3>
<p> <p>
We use Tianji, a self-hosted analytics platform, to understand how visitors use our website and internal We use Tianji, a self-hosted analytics platform, to understand how visitors use our website and internal
tools. Tracking is enabled only after you provide consent through our cookie banner. tools. Analytics run when you use the site so we can measure usage and improve the experience.
</p> </p>
<p>When you accept analytics, we may collect information such as:</p> <p>We may collect information such as:</p>
<ul> <ul>
<li>Pages viewed and navigation paths</li> <li>Pages viewed and navigation paths</li>
<li>Approximate geographic location derived from truncated IP addresses</li> <li>Approximate geographic location derived from truncated IP addresses</li>
@@ -76,16 +76,16 @@
</ul> </ul>
<p> <p>
We do not use Tianji to sell your personal information. Analytics data helps us improve site performance, We do not use Tianji to sell your personal information. Analytics data helps us improve site performance,
content, and product usability. You may decline analytics at any time using the cookie preferences link content, and product usability. We show an analytics notice when you first visit the site. You can review it
in the site footer. If you decline, the analytics script will not load. again at any time using the cookie preferences link in the site footer.
</p> </p>
<p> <p>
We store your consent choice locally in your browser so we can remember your preference on future visits. We store your acknowledgement locally in your browser so we do not show the notice on every visit.
</p> </p>
<h3>4.3 Cookies and Local Storage</h3> <h3>4.3 Cookies and Local Storage</h3>
<p> <p>
In addition to analytics consent storage, our site uses essential cookies and session storage required for In addition to analytics notice storage, our site uses essential cookies and session storage required for
authentication, security (including CSRF protection), and basic site functionality. These essential authentication, security (including CSRF protection), and basic site functionality. These essential
technologies are necessary for the site to operate and are not used for marketing analytics. technologies are necessary for the site to operate and are not used for marketing analytics.
</p> </p>
@@ -126,7 +126,7 @@
<h3>4.7 Your Choices and Rights</h3> <h3>4.7 Your Choices and Rights</h3>
<p>You can:</p> <p>You can:</p>
<ul> <ul>
<li>Accept or decline analytics tracking through our cookie banner or the cookie preferences link in the footer</li> <li>Acknowledge our analytics notice through the banner or review it again using the cookie preferences link in the footer</li>
<li>Contact us to request access to, correction of, or deletion of personal information you have provided, subject to applicable law</li> <li>Contact us to request access to, correction of, or deletion of personal information you have provided, subject to applicable law</li>
<li>Disable non-essential browser storage or cookies through your browser settings, though essential site features may not function properly</li> <li>Disable non-essential browser storage or cookies through your browser settings, though essential site features may not function properly</li>
</ul> </ul>
+6 -2
View File
@@ -115,16 +115,20 @@ class ContactViewTests(TestCase):
@override_settings(DEBUG=True, TIANJI_ENABLED=True) @override_settings(DEBUG=True, TIANJI_ENABLED=True)
class TianjiTrackingTests(TestCase): class TianjiTrackingTests(TestCase):
def test_homepage_includes_consent_banner_when_enabled(self): def test_homepage_includes_analytics_notice_when_enabled(self):
response = self.client.get(reverse("public_index")) response = self.client.get(reverse("public_index"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "cookie-consent-banner") self.assertContains(response, "cookie-consent-banner")
self.assertContains(response, "cookie-consent-acknowledge")
self.assertContains(response, "Acknowledge")
self.assertNotContains(response, "cookie-consent-decline")
self.assertNotContains(response, "Accept Analytics")
self.assertContains(response, "tianji-config") self.assertContains(response, "tianji-config")
self.assertContains(response, "tianji-consent.js") self.assertContains(response, "tianji-consent.js")
@override_settings(TIANJI_ENABLED=False) @override_settings(TIANJI_ENABLED=False)
def test_homepage_omits_consent_banner_when_disabled(self): def test_homepage_omits_analytics_notice_when_disabled(self):
response = self.client.get(reverse("public_index")) response = self.client.get(reverse("public_index"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)