diff --git a/web/src/app/__tests__/no-analytics.test.ts b/web/src/app/__tests__/no-analytics.test.ts new file mode 100644 index 0000000..ba89312 --- /dev/null +++ b/web/src/app/__tests__/no-analytics.test.ts @@ -0,0 +1,51 @@ +import fs from 'fs'; +import path from 'path'; + +// Ticket #16: the site loads no analytics. Everything the static export is +// built from (web/src, web/public and the next.config files) is scanned byte +// for byte, so a measurement tag cannot return through a component, an asset +// or a config file. Binary assets are read as latin1 so every byte is covered. + +const WEB_ROOT = process.cwd(); +const THIS_FILE = path.join(WEB_ROOT, 'src', 'app', '__tests__', 'no-analytics.test.ts'); +const TREE_ROOTS = [path.join(WEB_ROOT, 'src'), path.join(WEB_ROOT, 'public')]; + +const MARKERS: [string, RegExp][] = [ + ['googletagmanager', /googletagmanager/], + ['google-analytics', /google-analytics/], + ['gtag(', /gtag\(/], + ['G- measurement ID', /G-[A-Z0-9]{8,}/], +]; + +function scanTargets(): string[] { + const targets: string[] = []; + for (const root of TREE_ROOTS) { + for (const rel of fs.readdirSync(root, { encoding: 'utf8', recursive: true })) { + const full = path.join(root, rel); + if (fs.statSync(full).isFile()) targets.push(full); + } + } + for (const name of fs.readdirSync(WEB_ROOT)) { + if (/^next\.config\./.test(name)) targets.push(path.join(WEB_ROOT, name)); + } + return targets.filter((file) => file !== THIS_FILE); +} + +const sources = scanTargets().map((file) => ({ + file: path.relative(WEB_ROOT, file), + text: fs.readFileSync(file, 'latin1'), +})); + +describe('no analytics on the site (ticket #16)', () => { + it('scans the source, public assets and next.config files', () => { + const names = sources.map((source) => source.file); + expect(names).toEqual( + expect.arrayContaining(['src/app/layout.tsx', 'next.config.js', 'public/_headers']), + ); + }); + + it.each(MARKERS)('contains no %s marker', (_label, marker) => { + const offenders = sources.filter((source) => marker.test(source.text)).map((source) => source.file); + expect(offenders).toEqual([]); + }); +}); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index cc4a807..0d69637 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,7 +1,6 @@ import "@/styles/globals.css"; import React from "react"; import { type Metadata } from "next"; -import Script from "next/script"; import { absoluteUrl, DEFAULT_OG_IMAGE_ALT, @@ -173,18 +172,6 @@ export default function RootLayout({ />
- - {children}