Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookiebanner + Umami tracking #3596

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a935ed8
:construction: umami test script
JulianNymark Feb 14, 2025
f66a7e6
:construction: CSP for umami
JulianNymark Feb 14, 2025
68ab8e1
:construction: test data-tag, set no auto tracking
JulianNymark Feb 14, 2025
b0f385f
Merge branch 'main' into umami
JulianNymark Feb 17, 2025
21ec03b
:recycle: make consentBanner more self contained
JulianNymark Feb 18, 2025
6c47f34
:construction: working implementation for consent modal
JulianNymark Feb 18, 2025
07417fa
Merge branch 'cookiebanner' into next
JulianNymark Feb 18, 2025
53634a4
:recycle: move ConsentBanner to website-modules + small changes
JulianNymark Feb 18, 2025
0ba0f89
use typescript-cookies over localStorage API
JulianNymark Feb 18, 2025
f063f89
Copy Nav.no consent text
JulianNymark Feb 18, 2025
45b8104
better name for cookie
JulianNymark Feb 18, 2025
ee999ab
:construction: crude complex consent object
JulianNymark Feb 18, 2025
a346db8
Update aksel.nav.no/website/components/website-modules/ConsentBanner.tsx
JulianNymark Feb 19, 2025
757d931
classify traffic better (umami)
JulianNymark Feb 19, 2025
19a14eb
Update aksel.nav.no/website/components/website-modules/ConsentBanner.tsx
JulianNymark Feb 19, 2025
681eb55
link to our own personvernerklering + disable modal on that specific …
JulianNymark Feb 19, 2025
6548473
use same conditions for all showModal() triggers
JulianNymark Feb 19, 2025
b784022
update comment
JulianNymark Feb 19, 2025
9fb714e
limit cookie to specific subdomain (prevent it getting sent by client…
JulianNymark Feb 20, 2025
76d39b1
remove the MutationObserver, but keep query param to disable banner (…
JulianNymark Feb 21, 2025
637bbd9
bypass banner in e2e tests
JulianNymark Feb 21, 2025
d3ea856
store "no action" consent data in sessionStorage
JulianNymark Feb 21, 2025
c1fb2bf
Merge branch 'main' into next
JulianNymark Feb 21, 2025
162aa09
Merge branch 'main' into next
JulianNymark Feb 24, 2025
305d8ea
:construction: change to in document flow style banner (no more modal)
JulianNymark Feb 24, 2025
21659c8
:iphone: responsive consent banner++
JulianNymark Feb 24, 2025
739e4a6
website: set cookie domain dynamically (from serving site)
JulianNymark Feb 24, 2025
2fe38f4
Merge branch 'main' into next
JulianNymark Feb 24, 2025
e93f7a2
hardcode /side/personvernerklaering
JulianNymark Feb 25, 2025
ed07421
:construction: placeholder cookie preferences box
JulianNymark Feb 25, 2025
2ec879a
cookie preferences box++
JulianNymark Feb 25, 2025
fe8afa9
dokmøte plenumsendringer
JulianNymark Feb 25, 2025
99139f0
move page personvernserklaering
JulianNymark Feb 25, 2025
536bf2a
make the cookie preference form functional
JulianNymark Feb 25, 2025
f4e2a8a
:wheelchair: add missing aria-labelledby target
JulianNymark Feb 25, 2025
781407e
re-enable banner on e2e search tests
JulianNymark Feb 26, 2025
3136c45
Add explanation to organic and polluted traffic
larseirikhansen Feb 26, 2025
905e54e
extract consent state into custom hook
JulianNymark Feb 26, 2025
5be3981
PR review changes
JulianNymark Feb 26, 2025
0a5f16d
:sparkles: Serverside-check for cookie consent
KenAJoh Feb 26, 2025
ccca716
Merge branch 'main' into next
JulianNymark Feb 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions aksel.nav.no/website/components/utils/get-current-environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

export const classifyTraffic = () => {
const isProdUrl = () => window.location.host === "aksel.nav.no";
const isPreview = () => !!document.getElementById("exit-preview-id");
const isExample = () => window.location.pathname.startsWith("/eksempler/");
const isTemplate = () => window.location.pathname.startsWith("/templates/");
const isAdmin = () => window.location.pathname.startsWith("/admin/");

if (
isProdUrl() &&
!isPreview() &&
!isExample() &&
!isTemplate() &&
!isAdmin()
) {
return "organic";
}
return "polluted";
};
162 changes: 162 additions & 0 deletions aksel.nav.no/website/components/website-modules/ConsentBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"use client";

import Link from "next/link";
import Script from "next/script";
import { useEffect, useRef, useState } from "react";
import { Cookies } from "typescript-cookie";
import { BodyLong, Button, Modal } from "@navikt/ds-react";
import { classifyTraffic } from "../utils/get-current-environment";

const CONSENT_TRACKER_ID = "aksel-consent";

type CONSENT_TRACKER_STATE =
| "undecided"
| "accepted"
| "rejected"
| "no_action";

type CookieData = {
createdAt: string;
updatedAt: string;
version: number;
consents: {
tracking?: CONSENT_TRACKER_STATE;
};
};

export const getStorageAcceptedTracking = () => {
const rawState = (Cookies.get(CONSENT_TRACKER_ID) ??
sessionStorage.getItem(CONSENT_TRACKER_ID)) as string;

if (!rawState) {
return "undecided";
}

const cookieData = JSON.parse(rawState) as CookieData;

return cookieData.consents.tracking as CONSENT_TRACKER_STATE;
};

export const setStorageAcceptedTracking = (state: CONSENT_TRACKER_STATE) => {
const cookieData: CookieData = {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
version: 1,
consents: {},
};

cookieData.consents.tracking = state;

const cookieJson = JSON.stringify(cookieData);

if (state === "no_action") {
sessionStorage.setItem(CONSENT_TRACKER_ID, cookieJson);
return;
}

Cookies.set(CONSENT_TRACKER_ID, cookieJson, {
expires: 365,
domain: "aksel.ansatt.dev.nav.no",
});
};

/*
* close handler will always run after both clicking a button or the 'X'
* therefore we have to check if the user closed via one of the buttons
* (a cookie or sessionStorage was set) or if it was simply closed via the 'X' (no action taken)
* this is where we can then act on "no action taken"*/
// NOTE: maybe we should have a specific handler for "no action",
// that is run when either clicking the 'x' or clicking outside (when applicable)
// now i have to check for side effects (or store separate state)
// to detect it like checking for cookies/sessionStorage
const closeHandler = () => {
const rawState = (Cookies.get(CONSENT_TRACKER_ID) ??
sessionStorage.getItem(CONSENT_TRACKER_ID)) as string;

if (!rawState) {
setStorageAcceptedTracking("no_action");
}
};

export const ConsentBanner = () => {
const ref = useRef<HTMLDialogElement>(null);
const [umamiTag, setUmamiTag] = useState<string | undefined>();
const [clientAcceptsTracking, setClientAcceptsTracking] = useState(false);

useEffect(() => {
const consentAnswer = getStorageAcceptedTracking();

const disabledModalParam = new URLSearchParams(window.location.search).get(
"no_consent_modal",
);
if (consentAnswer === "undecided" && !disabledModalParam) {
ref.current?.showModal();
}

setUmamiTag(classifyTraffic());
setClientAcceptsTracking(getStorageAcceptedTracking() === "accepted");
}, []);

return (
<div>
{umamiTag && (
<Script
defer
src="https://cdn.nav.no/team-researchops/sporing/sporing.js"
data-host-url="https://umami.nav.no"
data-website-id="7b9fb2cd-40f4-4a30-b208-5b4dba026b57"
data-auto-track={clientAcceptsTracking}
data-tag={umamiTag}
></Script>
)}
<Modal
ref={ref}
header={{ heading: "Velg hvilke informasjonskapsler Aksel får bruke" }}
onClose={closeHandler}
>
<Modal.Body>
<BodyLong className="mb-2">
Nødvendige informasjonskapsler sørger for at nettstedet fungerer og
er sikkert, og kan ikke velges bort. Andre brukes til statistikk og
analyse. Godkjenner du alle, hjelper du oss å lage bedre nettsider
og tjenester.{" "}
<Link
href="/side/personvernerklaering?no_consent_modal=true"
onClick={() => {
ref.current?.close();
}}
>
Mer om våre informasjonskapsler.
</Link>
</BodyLong>
</Modal.Body>

<Modal.Footer>
<Button
type="button"
onClick={() => {
setStorageAcceptedTracking("accepted");
// NOTE: umami _should_ exist on window object here (loaded via <Script>)
// we call track manually this _one_ time to ensure the current page is
// accounted for, any new page loads will be captured by data-auto-track
// https://umami.is/docs/tracker-configuration
umami.track();
ref.current?.close();
}}
>
Godkjenn alle
</Button>
<Button
type="button"
onClick={() => {
setStorageAcceptedTracking("rejected");
ref.current?.close();
}}
>
Bare nødvendige
</Button>
</Modal.Footer>
</Modal>
</div>
);
};
4 changes: 2 additions & 2 deletions aksel.nav.no/website/e2e/search.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test";

test.describe("Check website search", () => {
test("Check newest article list", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.goto("http://localhost:3000/?no_consent_modal=true");
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(5000);
await page.getByRole("button", { name: "Søk" }).click();
Expand All @@ -16,7 +16,7 @@ test.describe("Check website search", () => {
});

test("Test searching for 'link'", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.goto("http://localhost:3000/?no_consent_modal=true");
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(5000);
await page.getByRole("button", { name: "Søk" }).click();
Expand Down
2 changes: 1 addition & 1 deletion aksel.nav.no/website/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ContentSecurityPolicy = `
img-src 'self' cdn.sanity.io ${dekoratorUrl} https://avatars.githubusercontent.com data: ${cdnUrl} ${hotjarUrl};
script-src 'self' ${dekoratorUrl} ${cdnUrl} ${hotjarUrl} https://in2.taskanalytics.com/tm.js 'nonce-4e1aa203a32e' 'unsafe-eval';
style-src 'self' ${dekoratorUrl} ${cdnUrl} ${hotjarUrl} 'unsafe-inline';
connect-src 'self' ${dekoratorUrl} ${cdnUrl} ${hotjarUrl} https://*.hotjar.io wss://*.hotjar.com https://raw.githubusercontent.com/navikt/ https://hnbe3yhs.apicdn.sanity.io wss://hnbe3yhs.api.sanity.io cdn.sanity.io *.api.sanity.io https://amplitude.nav.no https://in2.taskanalytics.com/03346;
connect-src 'self' ${dekoratorUrl} ${cdnUrl} ${hotjarUrl} https://*.hotjar.io wss://*.hotjar.com https://raw.githubusercontent.com/navikt/ https://hnbe3yhs.apicdn.sanity.io wss://hnbe3yhs.api.sanity.io cdn.sanity.io *.api.sanity.io https://umami.nav.no https://in2.taskanalytics.com/03346;
frame-ancestors 'self' localhost:3000;
media-src 'self' ${cdnUrl} cdn.sanity.io;
frame-src 'self' https://web.microsoftstream.com localhost:3000 https://aksel.ekstern.dev.nav.no;
Expand Down
2 changes: 2 additions & 0 deletions aksel.nav.no/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"swr": "^1.1.2",
"tailwindcss": "^3.3.3",
"tinycolor2": "^1.6.0",
"typescript-cookie": "^1.0.6",
"zod": "^3.22.4"
},
"_note": "When upgrading @axe-core/playwright, try removing the resolution entry in the root package.json",
Expand All @@ -89,6 +90,7 @@
"@types/jscodeshift": "^0.11.11",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.0.0",
"@types/umami": "^2.10.0",
"autoprefixer": "^10.4.20",
"babel-loader": "^9.1.3",
"copyfiles": "^2.4.1",
Expand Down
2 changes: 2 additions & 0 deletions aksel.nav.no/website/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "@navikt/ds-tokens/darkside-css";
import { useCheckAuth } from "@/hooks/useCheckAuth";
import { useHashScroll } from "@/hooks/useHashScroll";
import { SanityDataContext } from "@/hooks/useSanityData";
import { ConsentBanner } from "@/web/ConsentBanner";
import { BaseSEO } from "@/web/seo/BaseSEO";
import "../components/styles/index.css";

Expand Down Expand Up @@ -33,6 +34,7 @@ function App({ Component, pageProps, router }: AppProps) {

return (
<>
<ConsentBanner />
<BaseSEO path={router.asPath} />
<SanityDataContext.Provider
value={{ id: pageProps?.id ?? pageProps?.page?._id, validUser }}
Expand Down
2 changes: 1 addition & 1 deletion aksel.nav.no/website/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"jsx": "preserve",
"noImplicitAny": false,
"incremental": true,
"types": ["node"],
"types": ["node", "umami"],
"paths": {
"@/sb-util": ["./.storybook/utils.tsx"],
"@/types": ["./components/types/index.ts"],
Expand Down
16 changes: 16 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6643,6 +6643,13 @@ __metadata:
languageName: node
linkType: hard

"@types/umami@npm:^2.10.0":
version: 2.10.0
resolution: "@types/umami@npm:2.10.0"
checksum: 10/8e489202966fb6429ac079f10dc0717fe44daeedbf37927e326017b9eed9c821dc6bf5d9aaad69fd4ce5eb92df0e53fafb57c9b47185f73a547969e4db365d66
languageName: node
linkType: hard

"@types/unist@npm:*, @types/unist@npm:^3.0.0":
version: 3.0.3
resolution: "@types/unist@npm:3.0.3"
Expand Down Expand Up @@ -23565,6 +23572,13 @@ __metadata:
languageName: node
linkType: hard

"typescript-cookie@npm:^1.0.6":
version: 1.0.6
resolution: "typescript-cookie@npm:1.0.6"
checksum: 10/105d4774ee847daf3e5e9a4433746eb34cd40feb1703bc7c9bde0bfc77c94ee4318386b94af66f9380fd24fa36600cc8e730ead7dfa67097ebbeff158e8020c3
languageName: node
linkType: hard

"typescript@npm:5.5.4":
version: 5.5.4
resolution: "typescript@npm:5.5.4"
Expand Down Expand Up @@ -24775,6 +24789,7 @@ __metadata:
"@types/jscodeshift": "npm:^0.11.11"
"@types/react": "npm:^18.3.11"
"@types/react-dom": "npm:^18.0.0"
"@types/umami": "npm:^2.10.0"
autoprefixer: "npm:^10.4.20"
babel-loader: "npm:^9.1.3"
boring-avatars: "npm:1.10.1"
Expand Down Expand Up @@ -24819,6 +24834,7 @@ __metadata:
tinycolor2: "npm:^1.6.0"
tsx: "npm:^4.19.1"
typescript: "npm:5.5.4"
typescript-cookie: "npm:^1.0.6"
vitest: "npm:^2.1.8"
zod: "npm:^3.22.4"
languageName: unknown
Expand Down
Loading