From 8e8a451ba2db9836017af7de668223ecf88688ae Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 19:54:30 +0530 Subject: [PATCH] fix(web): remove Google Analytics from the site The root layout loaded the gtag script for measurement ID G-7NBBNJNNQ7, and the privacy page described it. Remove both. The privacy page now says the site runs no analytics or tracking scripts, sets no cookies, and loads its header and footer logos from bongbetic.com. A jest test scans web/src, web/public and the next.config files for googletagmanager, google-analytics, gtag( and G- measurement IDs, so the tag cannot return unnoticed. Refs #16 --- web/src/app/__tests__/no-analytics.test.ts | 51 ++++++++++++++++++++++ web/src/app/layout.tsx | 13 ------ web/src/app/privacy/page.tsx | 17 ++++---- 3 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 web/src/app/__tests__/no-analytics.test.ts 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} diff --git a/web/src/app/privacy/page.tsx b/web/src/app/privacy/page.tsx index 6083a49..9902a92 100644 --- a/web/src/app/privacy/page.tsx +++ b/web/src/app/privacy/page.tsx @@ -24,7 +24,7 @@ export default function PrivacyPage() { headline: "Voculinux Privacy Policy", description: "How Voculinux handles voice data: local engines on your machine, optional Remote API to a server you configure.", - dateModified: "2026-09-23", + dateModified: "2026-11-06", author: { "@type": "Person", name: "Jatin K Malik", @@ -158,14 +158,13 @@ export default function PrivacyPage() {

- voculinux.com loads Google Analytics via the googletagmanager gtag - script for measurement ID{" "} - G-7NBBNJNNQ7{" "} - and runs gtag config with no IP anonymization and no cookie flags. - That sends page views and a Google Analytics client identifier (a - cookie) to Google. Google processes IP addresses and device and - browser data. The installed Voculinux app does not send usage - telemetry. + voculinux.com has no analytics, measurement or tracking scripts, + and its code sets no cookies. +

+

+ The Bongbetic logos in the header and footer are images served from + bongbetic.com. Loading any page therefore makes a request to that + host.