@statsbygg/consent
v0.1.2
Published
Cookie consent banner and telemetry gatekeeper for Statsbygg applications
Keywords
Readme
@statsbygg/consent
A lightweight, self-contained cookie consent and telemetry gatekeeper designed for Statsbygg's multi-zone Next.js architecture.
Why this exists
Statsbygg hosts multiple independent frontend applications under a single domain. This package provides a unified cookie consent banner and centralized state management. Because localStorage is shared across the domain, a user's consent choice in one application is automatically respected by all other Statsbygg applications.
Features
- Compliant: Blocks tracking cookies until explicit consent is given, while still allowing cookieless pageview tracking.
- Hydration Safe: Safe to use in Next.js App Router (SSR) without client/server visual mismatches.
- Built-in Translations: Supports Bokmål (
nb), Nynorsk (nn), and English (en) via a simple prop. - Cross-App Sync: State persists across all apps on the same domain via
localStorage(using the keystatsbygg.analytics.cookies). - Dynamic Runtime Config: Enforces fetching Matomo configurations at runtime, ensuring a single Docker build can be deployed across multiple environments.
Installation
Ensure the package and its required peer dependencies are installed in your consuming Next.js application:
pnpm add @statsbygg/consent matomo-tracker-for-reactIntegration Guide: The Golden Path
To achieve compliance and ensure build-once-deploy-anywhere compatibility, Statsbygg applications must fetch their telemetry configuration dynamically at runtime.
Step 1: Create the Config Loader
Do not use the TelemetryManager directly in your layout with environment variables. Instead, create a dedicated Client Component (e.g., MatomoConfigLoader.tsx) that fetches your runtime configuration and conditionally renders the telemetry manager.
// src/app/_components/MatomoConfigLoader/MatomoConfigLoader.tsx
"use client";
import { useEffect, useState } from "react";
import { TelemetryManager } from "@statsbygg/consent";
import { fetchRuntimeConfig } from "@/lib/utils/runtime-config";
export function MatomoConfigLoader() {
const [matomoUrl, setMatomoUrl] = useState<string | undefined>();
const [matomoSiteId, setMatomoSiteId] = useState<number | undefined>();
const [ready, setReady] = useState(false);
useEffect(() => {
let cancelled = false;
fetchRuntimeConfig()
.then((cfg) => {
if (cancelled) return;
if (cfg.matomo.enabled) {
setMatomoUrl(cfg.matomo.urlBase);
setMatomoSiteId(cfg.matomo.siteId);
}
})
.catch(() => {
if (process.env.NODE_ENV === "development") {
console.warn("[Matomo] Failed to fetch runtime config");
}
})
.finally(() => {
if (!cancelled) setReady(true);
});
return () => {
cancelled = true;
};
}, []);
if (!ready) return null;
return (
<TelemetryManager matomoUrlBase={matomoUrl} matomoSiteId={matomoSiteId} />
);
}Step 2: Add Components to Root Layout
In your application's src/app/layout.tsx, import the CSS, the Banner, and your new loader. These should be placed near the bottom of the <body>, outside of your main application tree.
// src/app/layout.tsx
import "@statsbygg/consent/styles.css";
import { CookieBanner } from "@statsbygg/consent";
import { MatomoConfigLoader } from "./_components/MatomoConfigLoader";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="nb">
<body>
<Providers, RootLayout etc>
{children}
</Providers, RootLayout etc>
<CookieBanner locale="nb" />
<MatomoConfigLoader />
</body>
</html>
);
}Advanced Usage
Gating Custom UI Components
If you have a UI component that relies on third-party tracking (e.g., embedding a YouTube video that drops tracking cookies), you can use the <ConsentGate> component to hide it until consent is given.
import { ConsentGate } from '@statsbygg/consent';
export function VideoPlayer() {
return (
<ConsentGate fallback={<p">Vennligst godta informasjonskapsler for å se videoen.</p>}>}
<iframe src="[https://youtube.com/](https://youtube.com/)..." />
</ConsentGate>
);
}Gating Azure Application Insights
Because @statsbygg/consent keeps bundle sizes small, it does not include the heavy Azure App Insights SDK. Instead, use <ConsentGate> to safely load your App Insights instance only when permitted.
import { ConsentGate } from "@statsbygg/consent";
import { AppInsights } from "@/lib/utils/app-insights"; // Your initialized instance
// In layout.tsx:
<ConsentGate>
{process.env.NODE_ENV !== "development" && <AppInsights />}
</ConsentGate>;Tracking Custom Events
This package automatically handles pageview tracking via matomo-tracker-for-react. However, it does not export a React hook for custom event tracking (e.g., button clicks or downloads). This deliberately decouples tracking from the React rendering lifecycle.
Because Matomo exposes a global queue, you can track custom events from anywhere in your application (React components, Zustand stores, or standard utility files).
Recommended Pattern: Create a utility function inside your frontend application to interact with the global queue.
// src/lib/analytics/tracking.ts
type AppEventCategory = "Document" | "Form";
type AppEventAction = "Download" | "Submit";
export function trackEvent(
category: AppEventCategory,
action: AppEventAction,
name?: string,
) {
if (typeof window !== "undefined" && window._paq) {
window._paq.push(["trackEvent", category, action, name]);
}
}Then, use this utility directly in your handlers:
import { trackEvent } from "@/lib/analytics/tracking";
import { Button } from "@digdir/designsystemet-react";
export function DownloadReport() {
return (
<Button
onClick={() => {
trackEvent("Document", "Download", "Report_2024.pdf");
downloadFile();
}}
>
Last ned rapport
</Button>
);
}Customizing Banner Language
The banner supports built-in translations. Pass the locale prop to set the language. If omitted, it defaults to "nb". Supported values are "nb", "nn", and "en".
<CookieBanner locale="nn" />