Skip to content

Commit bc6410c

Browse files
authored
Merge pull request #518 from jaydonkrooss/493-ga4-oneTrust-integration
493: update Google Analytics settings, use Umich OneTrust banner
2 parents dfbbc16 + 72729b9 commit bc6410c

File tree

7 files changed

+117
-3
lines changed

7 files changed

+117
-3
lines changed

src/assets/src/hooks/useGoogleAnalytics.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
11
import { useState, useEffect } from 'react';
22
import GoogleAnalytics from 'react-ga4';
33
import { useLocation } from 'react-router-dom';
4+
import { useOneTrust } from './useOneTrust';
5+
6+
export enum GoogleAnalyticsConsentValue {
7+
Denied = "denied",
8+
Granted = "granted"
9+
}
410

511
export const useGoogleAnalytics = (googleAnalyticsId?: string, debug?: boolean) => {
612
let location = useLocation();
13+
const [initializeOneTrust] = useOneTrust();
14+
715
const [initialized, setInitialized] = useState(false);
816
const [previousPage, setPreviousPage] = useState(null as string | null);
9-
1017
if (googleAnalyticsId && !initialized) {
11-
setInitialized(true);
18+
GoogleAnalytics.gtag("consent", "default", {
19+
ad_storage: GoogleAnalyticsConsentValue.Denied,
20+
analytics_storage: GoogleAnalyticsConsentValue.Denied,
21+
functionality_storage: GoogleAnalyticsConsentValue.Denied,
22+
personalization_storage: GoogleAnalyticsConsentValue.Denied,
23+
ad_user_data: GoogleAnalyticsConsentValue.Denied,
24+
ad_personalization: GoogleAnalyticsConsentValue.Denied,
25+
wait_for_update: 500
26+
});
1227
GoogleAnalytics.initialize(googleAnalyticsId, { testMode: debug });
28+
if (initializeOneTrust) {
29+
initializeOneTrust(GoogleAnalytics);
30+
}
31+
setInitialized(true);
1332
}
1433

1534
useEffect(() => {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { GA4 } from 'react-ga4/types/ga4';
2+
import Cookies from "js-cookie";
3+
import { GoogleAnalyticsConsentValue } from './useGoogleAnalytics';
4+
5+
declare global {
6+
interface Window {
7+
OnetrustActiveGroups?: string;
8+
OptanonWrapper?: () => void;
9+
}
10+
}
11+
12+
// UofM OneTrust cookie documentation: https://vpcomm.umich.edu/resources/cookie-disclosure/
13+
enum OneTrustCookieCategory {
14+
StrictlyNecessary = "C0001",
15+
Performance = "C0002",
16+
Functionality = "C0003",
17+
Targeting = "C0004",
18+
SocialMedia = "C0005",
19+
}
20+
21+
export const useOneTrust = (): [(googleAnalytics:GA4) => void] | [] =>
22+
{
23+
// Embeds the script for UofM OneTrust consent banner implementation
24+
// See instructions at https://vpcomm.umich.edu/resources/cookie-disclosure/#3rd-party-google-analytics
25+
const initializeOneTrust = (googleAnalytics: GA4) => {
26+
// Callback is used by OneTrust to update Google Analytics consent tags and remove cookies
27+
const updateGtagCallback = () => {
28+
if (!window.OnetrustActiveGroups) {
29+
return;
30+
}
31+
// Update Google Analytics consent based on OneTrust active groups
32+
// "Strictly Necessary Cookies" are always granted. C0001 (StrictlyNecessary), C0003 (Functionality)
33+
if (window.OnetrustActiveGroups.includes(OneTrustCookieCategory.Performance)) {
34+
googleAnalytics.gtag("consent", "update", { analytics_storage: GoogleAnalyticsConsentValue.Granted });
35+
}
36+
if (window.OnetrustActiveGroups.includes(OneTrustCookieCategory.Functionality)) {
37+
googleAnalytics.gtag("consent", "update", { functional_storage: GoogleAnalyticsConsentValue.Granted });
38+
}
39+
40+
// "Analytics & Advertising Cookies" are optional for EU users. C0002 (Performance)
41+
if (window.OnetrustActiveGroups.includes(OneTrustCookieCategory.Targeting)) {
42+
googleAnalytics.gtag("consent", "update", {
43+
ad_storage: GoogleAnalyticsConsentValue.Granted,
44+
ad_user_data: GoogleAnalyticsConsentValue.Granted,
45+
ad_personalization: GoogleAnalyticsConsentValue.Granted,
46+
personalization_storage: GoogleAnalyticsConsentValue.Granted
47+
});
48+
} else {
49+
// Remove Google Analytics cookies if tracking is declined by EU users
50+
// Uses same library as this GA4 implementation: https://dev.to/ramonak/react-enable-google-analytics-after-a-user-grants-consent-5bg3
51+
Cookies.remove("_ga");
52+
Cookies.remove("_gat");
53+
Cookies.remove("_gid");
54+
}
55+
googleAnalytics.event({ action: 'um_consent_updated', category: 'consent' });
56+
}
57+
window.OptanonWrapper = updateGtagCallback;
58+
59+
const oneTrustScriptDomain = "03e0096b-3569-4b70-8a31-918e55aa20da"
60+
const src =`https://cdn.cookielaw.org/consent/${oneTrustScriptDomain}/otSDKStub.js`
61+
62+
const script = document.createElement('script');
63+
script.src = src;
64+
script.type = 'text/javascript';
65+
script.dataset.domainScript = oneTrustScriptDomain;
66+
document.head.appendChild(script);
67+
}
68+
69+
return [ initializeOneTrust ];
70+
};

src/officehours/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def str_to_bool(val):
8686
LOGIN_REDIRECT_URL = '/'
8787
LOGOUT_REDIRECT_URL = '/'
8888

89+
PRIVACY_REDIRECT_URL = 'https://umich.edu/about/privacy/'
90+
8991
OIDC_RP_CLIENT_ID = os.getenv('OIDC_RP_CLIENT_ID')
9092
OIDC_RP_CLIENT_SECRET = os.getenv('OIDC_RP_CLIENT_SECRET')
9193
OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv('OIDC_OP_AUTHORIZATION_ENDPOINT')

src/officehours_ui/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.conf import settings
33
from django.views.generic.base import RedirectView
44

5-
from .views import SpaView, AuthPromptView, auth_callback_view
5+
from .views import SpaView, AuthPromptView, auth_callback_view, privacy_policy_redirect_view
66

77

88
urlpatterns = [
@@ -19,4 +19,5 @@
1919
path('callback/<backend_name>/', auth_callback_view, name='auth_callback'),
2020
path("robots.txt", RedirectView.as_view(url='/static/robots.txt', permanent=True)),
2121
path("favicon.ico", RedirectView.as_view(url='/static/favicon.ico', permanent=True)),
22+
path("privacy/", privacy_policy_redirect_view, name='privacy-policy')
2223
]

src/officehours_ui/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.shortcuts import redirect
12
from django.views.generic import TemplateView
23
from django.conf import settings
34
from django.http import Http404
@@ -45,3 +46,6 @@ def auth_callback_view(request, backend_name: IMPLEMENTED_BACKEND_NAME):
4546
except AttributeError:
4647
raise Http404(f"Backend {backend_name} does not use three-legged OAuth2.")
4748
return auth_callback(request)
49+
50+
def privacy_policy_redirect_view(request):
51+
return redirect(settings.PRIVACY_REDIRECT_URL)

src/package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"@fortawesome/fontawesome-svg-core": "^6.4.0",
66
"@fortawesome/free-solid-svg-icons": "^6.4.0",
77
"@fortawesome/react-fontawesome": "^0.2.0",
8+
"js-cookie": "^3.0.5",
89
"react": "^18.2.0",
910
"react-bootstrap": "^2.7.4",
1011
"react-dom": "^18.2.0",
@@ -21,6 +22,7 @@
2122
"check-types": "tsc"
2223
},
2324
"devDependencies": {
25+
"@types/js-cookie": "^3.0.6",
2426
"@types/react": "^18.2.7",
2527
"@types/react-dom": "^18.2.4",
2628
"css-loader": "~6.8.1",

0 commit comments

Comments
 (0)