feat(analytics): docs send through @hanzo/event and the tag manager; the GA4, Pixel, Insights and analytics scripts leave every layout; the one consent banner

This commit is contained in:
2026-09-30 07:15:43 -07:00
parent 07d81d0dcf
commit d4e0db1673
8 changed files with 57 additions and 50 deletions
-3
View File
@@ -32,9 +32,6 @@ export const viewport: Viewport = {
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={`${Zen.variable} ${ZenMono.variable}`} suppressHydrationWarning>
<head>
<script defer src="https://analytics.hanzo.ai/script.js" data-website-id="89cb4513-3384-491f-8eba-c393d51a16ef" data-do-not-track="true" data-exclude-search="true" />
</head>
<Body>
<NextProvider>
<TreeContextProvider tree={source.getPageTree()}>
-1
View File
@@ -41,7 +41,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<body className="antialiased">{children}
<Footer />
<Analytics product="flow" /></body>
<script defer src="https://analytics.hanzo.ai/script.js" data-website-id="811b5039-8438-453a-9c8a-e1f4cff05353" data-do-not-track="true" data-exclude-search="true" />
</html>
);
}
-1
View File
@@ -41,7 +41,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<body className="antialiased">{children}
<Footer />
<Analytics product="team" /></body>
<script defer src="https://analytics.hanzo.ai/script.js" data-website-id="9e8eaacf-fb42-4b3b-a95f-11c58d50aec0" data-do-not-track="true" data-exclude-search="true" />
</html>
);
}
-1
View File
@@ -33,7 +33,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={`${Zen.variable} ${ZenMono.variable}`} suppressHydrationWarning>
<head>
<script defer src="https://analytics.hanzo.ai/script.js" data-website-id="PLACEHOLDER" />
</head>
<Body>
<NextProvider>
+1 -1
View File
@@ -46,7 +46,7 @@ spec:
# enough for Scalar to execute.
env:
- name: HANZO_STATIC_CSP
value: "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.hanzo.ai https://analytics.hanzo.ai; worker-src 'self' blob:; child-src 'self' blob:; manifest-src 'self'"
value: "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://www.googletagmanager.com https://connect.facebook.net https://snap.licdn.com https://static.ads-twitter.com https://analytics.tiktok.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.hanzo.ai https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com https://www.facebook.com https://connect.facebook.net https://px.ads.linkedin.com https://analytics.twitter.com https://analytics.tiktok.com; worker-src 'self' blob:; child-src 'self' blob:; manifest-src 'self'"
ports:
- name: http
containerPort: 3000
+3 -1
View File
@@ -23,7 +23,9 @@
"types:check": "tsc --noEmit"
},
"dependencies": {
"@hanzo/event": "catalog:"
"@hanzo/event": "catalog:",
"@hanzo/gui": "catalog:",
"@hanzo/ui": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
+45 -40
View File
@@ -1,67 +1,72 @@
'use client';
import { keyForPage } from '@hanzo/event';
import { AnalyticsProvider, usePageview } from '@hanzo/event/react';
/** The client, for a page that sends an event of its own. */
export { useAnalytics } from '@hanzo/event/react';
import { useEffect, type ReactNode } from 'react';
import { startTags } from '@hanzo/event';
import { AnalyticsProvider, useConsent, usePageview } from '@hanzo/event/react';
import { GuiProvider } from '@hanzo/gui';
import guiConfig from '@hanzo/ui/gui-config';
import { Consent } from '@hanzo/ui/consent';
import { usePathname } from 'next/navigation';
import type { ReactNode } from 'react';
/** The ONE Hanzo Cloud telemetry front door — POST api.hanzo.ai/v1/event. Cloud
* fans the one batched stream out to the web (analytics), product (insights) and
* error (sentry) lenses; the client never sends the org — Cloud resolves the
* tenant server-side from the publishable ingest key. */
export { useAnalytics } from '@hanzo/event/react';
export { EVENTS } from '@hanzo/event';
/** The ONE Hanzo Cloud telemetry front door — POST api.hanzo.ai/v1/event. */
const HOST = 'https://api.hanzo.ai';
/** Publishable ingest key: the org's, from the keyring @hanzo/event carries,
* resolved from the host the page is served on — docs.hanzo.ai is the hanzo
* org's. A docs site is read-only and logged out, so no bearer can ride the
* request — this write-only, bundle-safe key IS how anonymous pageviews and
* errors resolve to an org. A host no brand claims gets none and reports
* nothing, rather than filing its visitors under another org. */
const INGEST_KEY = keyForPage();
/** Honor an explicit browser opt-out (Global Privacy Control, then legacy DNT).
* SSR (no navigator) defaults to consented; the browser reads the real signal on
* hydration. Opting out suppresses pageviews AND errors — the `enabled` gate is
* the whole consent story. */
function consented(): boolean {
if (typeof navigator === 'undefined') return true;
const nav = navigator as Navigator & {
globalPrivacyControl?: boolean;
doNotTrack?: string | null;
};
if (nav.globalPrivacyControl === true) return false;
const dnt = nav.doNotTrack;
return dnt !== '1' && dnt !== 'yes';
}
/** The sites one visit can cross; GA4 keeps it one session across them. */
const DOMAINS = [
'hanzo.ai',
'docs.hanzo.ai',
'hanzo.id',
'hanzo.build',
'platform.hanzo.ai',
'hanzo.app',
'hanzo.chat',
'pay.hanzo.ai',
];
function Pageview() {
usePageview(usePathname());
return null;
}
/**
* The ad tags. `startTags` fetches this site's tag set from cloud (GA4 and the
* Pixel are configuration there, never ids in this repo) and the consent rule
* for this visitor's region, and loads only what consent allows.
*/
function Tags() {
useEffect(() => startTags({ domains: [window.location.hostname, ...DOMAINS] }), []);
return null;
}
/**
* Telemetry for a Hanzo docs site — mount once in the root layout, around the
* app when a page sends events of its own (`useAnalytics` from @hanzo/event/react):
*
* <Analytics product="zen-docs">{app}</Analytics>
* <Analytics product="docs">{app}</Analytics>
*
* The provider owns the ONE @hanzo/event client: it fires the first pageview,
* registers auto error capture (window.onerror + unhandledrejection), and flushes
* the batch on unload; <Pageview> adds one pageview per client-side route change.
* Errors are just events on the same stream, so there is no separate DSN. Nothing
* here is docs-specific except the default host and consent gate — `product` is
* the only knob, because Cloud derives everything else server-side.
* Errors are events on the same stream (`type: 'error'`). The stream runs while
* the visitor allows analytics: cloud serves the rule for their region, and
* <Consent> is the one banner that asks where the region requires it and offers
* "Cookie settings" everywhere. Its key is the site's own project key, found by host.
* `product` is the only knob.
*/
export function Analytics({ product, children }: { product: string; children?: ReactNode }) {
export function Analytics({ product = 'docs', children }: { product?: string; children?: ReactNode }) {
const consent = useConsent();
return (
<AnalyticsProvider
config={{ product, host: HOST, ingestKey: INGEST_KEY, enabled: consented() }}
>
<AnalyticsProvider config={{ product, host: HOST, enabled: consent.analytics }}>
<Tags />
<Pageview />
{children}
<GuiProvider config={guiConfig as never} defaultTheme="dark">
<Consent />
</GuiProvider>
</AnalyticsProvider>
);
}
export default Analytics;
+8 -2
View File
@@ -4370,6 +4370,12 @@ importers:
'@hanzo/event':
specifier: 'catalog:'
version: 0.3.50([email protected])
'@hanzo/gui':
specifier: 'catalog:'
version: 8.3.5([email protected]([email protected]))([email protected](@babel/[email protected]([email protected]))(@react-native/[email protected](@babel/[email protected]([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected]))([email protected])
'@hanzo/ui':
specifier: 'catalog:'
version: 8.27.35(ffc64b3d71a990f72cef419463133daf)
react:
specifier: '>=19.2.8'
version: 19.2.8
@@ -24297,7 +24303,7 @@ snapshots:
'@hanzo/[email protected]([email protected])':
dependencies:
'@hanzo/event': 0.3.41([email protected])
'@hanzo/event': 0.3.50([email protected])
optionalDependencies:
react: 19.2.8
@@ -26527,7 +26533,7 @@ snapshots:
'@hanzogui/[email protected]([email protected])':
dependencies:
'@hanzo/event': 0.3.41([email protected])
'@hanzo/event': 0.3.50([email protected])
'@hanzo/observe': 0.1.11([email protected])
react: 19.2.8