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

feat: add the ability to use cookies for consent #466

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,120 @@ also, you can use alternative to default path for googletagmanager script by
<GoogleAnalytics gtagUrl="/gtag.js" />
```

## Using Cookies for Consent

To use cookies to track user consent status you can use the `consentCookie` prop on the `GoogleAnalytics` component. If you specify a cookie, we automatically set consent to "denied" on first load.

```js
import { GoogleAnalytics } from "nextjs-google-analytics";

const App = ({ Component, pageProps }) => {
return (
<>
<GoogleAnalytics consentCookie="ga_consent" />
<Component {...pageProps} />
</>
);
};

export default App;
```

This will automatically save consent between sessions and pages.

The cookie is saved as a URI Component encoded JSON object. You can use the `useConsent` helper functions to your build out your consent prompts.

Here's an example:

```js
import { useConsent } from "nextjs-google-analytics";
import { consentCookieExists } from "nextjs-google-analytics";
import { useRef, useEffect } from "react";

const cookieKey = "ga_consent";

export default function Test() {
// The the first two variables act the same as useState
// The updateConsent function allows you to commit the consent changes
const [consent, setConsent, updateConsent] = useConsent({
// Here we set the initial values that appear in the consent variable
// The cookie is not set to this value until the updateConsent function is
// called.
// This is also the default value.
initialParams: { ad_storage: "denied", analytics_storage: "denied" },
// If cookie key isn't specified, changes will only last for that session.
preferencesCookieKey: cookieKey,
});

const promptRef = useRef(null);

// You can use consentCookieExists to only display this when the cookie
// doesn't exist here
useEffect(() => {
if (promptRef.current && consentCookieExists(cookieKey)) {
// Apply CSS that hides the consent div for us
promptRef.current.style.display = "none";
console.log(`A cookie with the key ${cookieKey} exists. (Prompt hidden)`);
}
}, []);

return (
<div ref={promptRef}>
<div>
<input
type="checkbox"
id="ad_storage"
onChange={(e) => {
let val = e.currentTarget.checked ? "granted" : "denied";

// Same usage as a useState set function
setConsent((prev) => {
return { ...prev, ad_storage: val };
});
}}
checked={consent.ad_storage === "granted"}
/>
<label htmlFor="ad_storage">Ad consent</label>
</div>
<div>
<input
type="checkbox"
id="analytics_storage"
onChange={(e) => {
let val = e.currentTarget.checked ? "granted" : "denied";

setConsent((prev) => {
return { ...prev, analytics_storage: val };
});
}}
checked={consent.analytics_storage === "granted"}
/>
<label htmlFor="analytics_storage">Site analytics</label>
</div>
{/*
Using updateConsent without parameters will use the value stored in the
consent variable
*/}
<button onClick={() => updateConsent()}>Update settings</button>
{/*
The updateConsent function can also take any last minute
consent changes. This is perfect for an "Accept All" button
*/}
<button
onClick={() => {
updateConsent({
ad_storage: "granted",
analytics_storage: "granted",
});
}}
>
Accept All
</button>
</div>
);
}
```

## Page views

To track page views set the `trackPageViews` prop of the `GoogleAnalytics` component to true.
Expand Down
50 changes: 32 additions & 18 deletions src/components/GoogleAnalytics.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import React from "react";
import Script, { ScriptProps } from "next/script";
import { usePageViews } from "../hooks";
import { getConsentCookie } from "../utils";

type GoogleAnalyticsProps = {
export type GoogleAnalyticsProps = {
gaMeasurementId?: string;
gtagUrl?: string;
strategy?: ScriptProps["strategy"];
debugMode?: boolean;
defaultConsent?: "granted" | "denied";
nonce?: string;
};
} & (
| {
defaultConsent?: "granted" | "denied";
consentCookie?: never;
}
| {
consentCookie: string;
defaultConsent?: never;
}
);

type WithPageView = GoogleAnalyticsProps & {
trackPageViews?: boolean;
Expand All @@ -27,6 +36,7 @@ export function GoogleAnalytics({
gtagUrl = "https://www.googletagmanager.com/gtag/js",
strategy = "afterInteractive",
defaultConsent = "granted",
consentCookie,
trackPageViews,
nonce,
}: WithPageView | WithIgnoreHashChange): JSX.Element | null {
Expand All @@ -51,21 +61,25 @@ export function GoogleAnalytics({
<Script src={`${gtagUrl}?id=${_gaMeasurementId}`} strategy={strategy} />
<Script id="nextjs-google-analytics" nonce={nonce}>
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
${
defaultConsent === "denied" ?
`gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied'
});` : ``
}
gtag('config', '${_gaMeasurementId}', {
page_path: window.location.pathname,
${debugMode ? `debug_mode: ${debugMode},` : ""}
});
`}
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
${
defaultConsent === "denied"
? `gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied'
});`
: consentCookie &&
`gtag('consent', 'default',
(${getConsentCookie.toString()})("${consentCookie}")
);`
}
gtag('config', '${_gaMeasurementId}', {
page_path: window.location.pathname,
${debugMode ? `debug_mode: ${debugMode},` : ""}
});
`}
</Script>
</>
);
Expand Down
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { usePagesViews } from "./usePagesViews";
export { usePageViews, UsePageViewsOptions } from "./usePageViews";
export { useConsent } from "./useConsent";
36 changes: 36 additions & 0 deletions src/hooks/useConsent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from "react";
import { consent as _consent } from "../interactions";
import { setConsentCookie } from "../utils";

interface UseConsentOptions {
initialParams?: Gtag.ConsentParams;
preferencesCookieKey?: string;
}

export function useConsent({
initialParams = {
ad_storage: "denied",
analytics_storage: "denied",
},
preferencesCookieKey,
}: UseConsentOptions): [
Gtag.ConsentParams,
React.Dispatch<React.SetStateAction<Gtag.ConsentParams>>,
(newConsent?: Gtag.ConsentParams) => void
] {
const [consent, setConsent] = useState(initialParams);

const updateConsent = (newConsent?: Gtag.ConsentParams) => {
const calculatedConsent = newConsent ? newConsent : consent;

if (preferencesCookieKey) {
setConsentCookie(preferencesCookieKey, calculatedConsent);
}

_consent({ arg: "update", params: calculatedConsent });

if (newConsent) setConsent(newConsent);
};

return [consent, setConsent, updateConsent];
}
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export { GoogleAnalytics } from "./components";
export { usePagesViews, usePageViews, UsePageViewsOptions } from "./hooks";
export {
usePagesViews,
useConsent,
usePageViews,
UsePageViewsOptions,
} from "./hooks";
export { pageView, event, consent } from "./interactions";
export {
getConsentCookie,
setConsentCookie,
consentCookieExists,
} from "./utils";
120 changes: 120 additions & 0 deletions src/utils/cookie.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { consentCookieExists, getConsentCookie, setConsentCookie } from "./cookie";

let cookieJar = "";

// https://stackoverflow.com/a/71500712/7764169
beforeEach(() => {
jest.spyOn(document, "cookie", "set").mockImplementation((cookie) => {
cookieJar += `${cookie};`;
});

jest.spyOn(document, "cookie", "get").mockImplementation(() => cookieJar);
});

afterEach(() => {
cookieJar = "";
});

describe("cookie", () => {
describe("getCookieConsent", () => {
it("should be able to read a cookie", () => {
document.cookie =
"ga_consent=%7B%22ad_storage%22%3A%22denied%22%2C%22analytics_storage%22%3A%22granted%22%7D";

expect(getConsentCookie("ga_consent")).toMatchObject({
ad_storage: "denied",
analytics_storage: "granted",
});
});

it("should be able to discriminate between multiple cookies", () => {
document.cookie =
"test_cookie=test%20value; ga_consent=%7B%22ad_storage%22%3A%22denied%22%2C%22analytics_storage%22%3A%22granted%22%7D; other_cookies=other%20cookie%20value; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342";

expect(getConsentCookie("ga_consent")).toMatchObject({
ad_storage: "denied",
analytics_storage: "granted",
});
});

it("shouldn't be confused cookies that start the same", () => {
document.cookie =
"test_cookie=test%20value; ga_consent=%7B%22ad_storage%22%3A%22denied%22%2C%22analytics_storage%22%3A%22granted%22%7D; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342; ga_consent123=%7B%22ad_storage%22%3A%22granted%22%2C%22analytics_storage%22%3A%22granted%22%7D";

expect(getConsentCookie("ga_consent")).toMatchObject({
ad_storage: "denied",
analytics_storage: "granted",
});
});

it("should return denied if key variable doesn't exist", () => {
document.cookie =
"test_cookie=test%20value; ga_consent=%7B%22ad_storage%22%3A%22denied%22%2C%22analytics_storage%22%3A%22granted%22%7D; other_cookies=other%20cookie%20value; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342";

expect(getConsentCookie(undefined)).toMatchObject({
ad_storage: "denied",
analytics_storage: "denied",
});
});

it("should return denied if cookie isn't found", () => {
document.cookie =
"test_cookie=test%20value; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342; ga_consent123=%7B%22ad_storage%22%3A%22granted%22%2C%22analytics_storage%22%3A%22granted%22%7D";

expect(getConsentCookie("ga_consent")).toMatchObject({
ad_storage: "denied",
analytics_storage: "denied",
});
});

it("should return denied if no cookies exist", () => {
expect(getConsentCookie("ga_consent")).toMatchObject({
ad_storage: "denied",
analytics_storage: "denied",
});
});
});

describe("setConsentCookie", () => {
it("should set the consent cookie correctly", () => {
document.cookie =
"test_cookie=test%20value; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342";

const consentValue: Gtag.ConsentParams = {
ad_storage: "granted",
analytics_storage: "granted",
};

setConsentCookie("ga_consent", consentValue);

expect(getConsentCookie("ga_consent")).toMatchObject(consentValue);
});
});

describe("consentCookieExists", () => {
it("should be able to discriminate between multiple cookies", () => {
document.cookie =
"test_cookie=test%20value; ga_consent=%7B%22ad_storage%22%3A%22denied%22%2C%22analytics_storage%22%3A%22granted%22%7D; other_cookies=other%20cookie%20value; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342";

expect(consentCookieExists("ga_consent")).toBeTruthy();
});

it("should return false if cookie isn't found", () => {
document.cookie =
"test_cookie=test%20value; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342";

expect(consentCookieExists("ga_consent")).toBeFalsy();
});

it("shouldn't be confused cookies that start the same", () => {
document.cookie =
"test_cookie=test%20value; _ga=GA1.31224.321412341234; _ga_TESTVALUE=GA1.31224.321412341234.230948234.12342; ga_consent123=%7B%22ad_storage%22%3A%22granted%22%2C%22analytics_storage%22%3A%22granted%22%7D";

expect(consentCookieExists("ga_consent")).toBeFalsy();
});

it("should return false if no cookies exist", () => {
expect(consentCookieExists("ga_consent")).toBeFalsy();
});
});
});
Loading