mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
feat(analytics): add GA4 (G-15XZEQ6G9S), Meta Pixel, Hanzo Insights, and interaction tracking
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,33 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { keyForPage } from '@hanzo/event';
|
||||
import { AnalyticsProvider, usePageview } from '@hanzo/event/react';
|
||||
import { AnalyticsProvider, usePageview, useAnalytics } from '@hanzo/event/react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
/** The client, for a page that sends an event of its own, and the catalog
|
||||
* (@hanzo/events) it takes the event's name from. */
|
||||
export { useAnalytics } from '@hanzo/event/react';
|
||||
export { EVENTS } from '@hanzo/event';
|
||||
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. */
|
||||
/** 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();
|
||||
/** Google Analytics 4 Measurement ID for docs.hanzo.ai */
|
||||
const GA_ID = process.env.NEXT_PUBLIC_GA_ID || 'G-15XZEQ6G9S';
|
||||
|
||||
/** Meta / Facebook Pixel ID */
|
||||
const PIXEL_ID = process.env.NEXT_PUBLIC_FB_PIXEL_ID || '1790308611893001';
|
||||
|
||||
/** Cross-domain session linker */
|
||||
const DOMAINS = [
|
||||
'hanzo.ai',
|
||||
'docs.hanzo.ai',
|
||||
'hanzo.id',
|
||||
'hanzo.build',
|
||||
'platform.hanzo.ai',
|
||||
'hanzo.app',
|
||||
'hanzo.chat',
|
||||
'pay.hanzo.ai',
|
||||
];
|
||||
|
||||
/** Publishable ingest key */
|
||||
const INGEST_KEY =
|
||||
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY?.trim() ||
|
||||
keyForPage() ||
|
||||
'pk-CmfLA2K6kvsPflrS9DSkt06H_kSoQB_21sjedt6VJdc';
|
||||
|
||||
/** Sentry DSN for error and span trace capture */
|
||||
const EVENT_DSN =
|
||||
process.env.NEXT_PUBLIC_HANZO_EVENT_DSN ||
|
||||
process.env.NEXT_PUBLIC_SENTRY_DSN ||
|
||||
'https://[email protected]/1';
|
||||
|
||||
/** 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 & {
|
||||
@@ -40,7 +53,178 @@ function consented(): boolean {
|
||||
}
|
||||
|
||||
function Pageview() {
|
||||
usePageview(usePathname());
|
||||
const pathname = usePathname();
|
||||
usePageview(pathname);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// GA4 pageview
|
||||
const w = window as unknown as {
|
||||
gtag?: (...args: unknown[]) => void;
|
||||
fbq?: (...args: unknown[]) => void;
|
||||
hi?: { capture: (event: string, properties?: Record<string, unknown>) => void };
|
||||
};
|
||||
|
||||
if (w.gtag) {
|
||||
w.gtag('event', 'page_view', {
|
||||
page_path: pathname,
|
||||
page_location: window.location.href,
|
||||
page_title: document.title,
|
||||
});
|
||||
}
|
||||
|
||||
// Meta Pixel pageview
|
||||
if (w.fbq) {
|
||||
w.fbq('track', 'PageView');
|
||||
}
|
||||
|
||||
// Hanzo Insights pageview
|
||||
if (w.hi) {
|
||||
w.hi.capture('$pageview', {
|
||||
$current_url: window.location.href,
|
||||
pathname,
|
||||
});
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ExternalTags() {
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !consented()) return;
|
||||
|
||||
const w = window as any;
|
||||
|
||||
// 1. Google Analytics (gtag.js)
|
||||
if (!w.dataLayer) {
|
||||
w.dataLayer = [];
|
||||
w.gtag = function () {
|
||||
w.dataLayer.push(arguments);
|
||||
};
|
||||
w.gtag('js', new Date());
|
||||
w.gtag('config', GA_ID, {
|
||||
linker: { domains: DOMAINS },
|
||||
send_page_view: false, // Pageview handled on route change
|
||||
});
|
||||
|
||||
const gaScript = document.createElement('script');
|
||||
gaScript.async = true;
|
||||
gaScript.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
|
||||
document.head.appendChild(gaScript);
|
||||
}
|
||||
|
||||
// 2. Meta Pixel (fbevents.js)
|
||||
if (!w.fbq) {
|
||||
const fbq: any = function () {
|
||||
fbq.callMethod ? fbq.callMethod.apply(fbq, arguments) : fbq.queue.push(arguments);
|
||||
};
|
||||
if (!w._fbq) w._fbq = fbq;
|
||||
fbq.push = fbq;
|
||||
fbq.loaded = true;
|
||||
fbq.version = '2.0';
|
||||
fbq.queue = [];
|
||||
w.fbq = fbq;
|
||||
|
||||
w.fbq('init', PIXEL_ID);
|
||||
|
||||
const fbScript = document.createElement('script');
|
||||
fbScript.async = true;
|
||||
fbScript.src = 'https://connect.facebook.net/en_US/fbevents.js';
|
||||
document.head.appendChild(fbScript);
|
||||
}
|
||||
|
||||
// 3. Hanzo Insights (PostHog-compatible)
|
||||
if (!w.hi) {
|
||||
(function (t: any, e: any) {
|
||||
var o: any, n: any, p: any, r: any;
|
||||
e.__SV ||
|
||||
((window as any).hi = e),
|
||||
(e._i = []),
|
||||
(e.init = function (i: any, s: any, a: any) {
|
||||
function g(t: any, e: any) {
|
||||
var o = e.split('.');
|
||||
2 == o.length && ((t = t[o[0]]), (e = o[1])),
|
||||
(t[e] = function () {
|
||||
t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
|
||||
});
|
||||
}
|
||||
((p = t.createElement('script')).type = 'text/javascript'),
|
||||
(p.crossOrigin = 'anonymous'),
|
||||
(p.async = !0),
|
||||
(p.src = s.api_host + '/static/array.js'),
|
||||
(r = t.getElementsByTagName('script')[0]).parentNode.insertBefore(p, r);
|
||||
var u = e;
|
||||
for (
|
||||
void 0 !== a ? (u = e[a] = []) : (a = 'hi'),
|
||||
u.people = u.people || [],
|
||||
u.toString = function (t: any) {
|
||||
var e = 'hi';
|
||||
return 'hi' !== a && (e += '.' + a), t || (e += ' (stub)'), e;
|
||||
},
|
||||
u.people.toString = function () {
|
||||
return u.toString(1) + '.people (stub)';
|
||||
},
|
||||
o =
|
||||
'init capture captureException identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId setPersonProperties'.split(
|
||||
' ',
|
||||
),
|
||||
n = 0;
|
||||
n < o.length;
|
||||
n++
|
||||
)
|
||||
g(u, o[n]);
|
||||
e._i.push([i, s, a]);
|
||||
}),
|
||||
(e.__SV = 1);
|
||||
})(document, (window as any).hi || []);
|
||||
|
||||
w.hi?.init('hi_e16a2d5a8033442d87f090b24c606825', {
|
||||
api_host: 'https://insights.hanzo.ai',
|
||||
person_profiles: 'identified_only',
|
||||
});
|
||||
w.hi?.register({ app: 'docs', org: 'hanzo' });
|
||||
}
|
||||
|
||||
// 4. Hanzo Analytics native script
|
||||
if (!document.querySelector('script[src*="analytics.hanzo.ai/script.js"]')) {
|
||||
const umScript = document.createElement('script');
|
||||
umScript.defer = true;
|
||||
umScript.src = 'https://analytics.hanzo.ai/script.js';
|
||||
umScript.setAttribute('data-website-id', 'a323a8ae-c811-4061-9626-22caaffc612f');
|
||||
document.head.appendChild(umScript);
|
||||
}
|
||||
|
||||
// 5. Global interaction listeners for docs
|
||||
const handleCopy = (e: ClipboardEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target?.closest('pre, code, [data-code]')) {
|
||||
w.gtag?.('event', 'copy_code', { page_location: window.location.href });
|
||||
w.hi?.capture('code_copied', { url: window.location.href });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const anchor = (e.target as HTMLElement | null)?.closest('a[href]') as HTMLAnchorElement | null;
|
||||
if (!anchor) return;
|
||||
const href = anchor.href;
|
||||
if (href.includes('hanzo.ai') && (href.includes('/pricing') || href.includes('/login') || href.includes('/signup') || href.includes('/pay'))) {
|
||||
w.gtag?.('event', 'begin_checkout', { link_url: href });
|
||||
w.fbq?.('track', 'InitiateCheckout', { content_name: 'docs_cta' });
|
||||
w.hi?.capture('checkout_started', { cta: 'docs_link', href });
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('copy', handleCopy);
|
||||
document.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('copy', handleCopy);
|
||||
document.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,7 +232,7 @@ function Pageview() {
|
||||
* 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
|
||||
@@ -57,13 +241,21 @@ function Pageview() {
|
||||
* here is docs-specific except the default host and consent gate — `product` is
|
||||
* the only knob, because Cloud derives everything else server-side.
|
||||
*/
|
||||
export function Analytics({ product, children }: { product: string; children?: ReactNode }) {
|
||||
export function Analytics({ product = 'docs', children }: { product?: string; children?: ReactNode }) {
|
||||
return (
|
||||
<AnalyticsProvider
|
||||
config={{ product, host: HOST, ingestKey: INGEST_KEY, enabled: consented() }}
|
||||
config={{
|
||||
product,
|
||||
host: HOST,
|
||||
dsn: EVENT_DSN,
|
||||
ingestKey: INGEST_KEY,
|
||||
enabled: consented(),
|
||||
}}
|
||||
>
|
||||
<ExternalTags />
|
||||
<Pageview />
|
||||
{children}
|
||||
</AnalyticsProvider>
|
||||
);
|
||||
}
|
||||
export default Analytics;
|
||||
|
||||
Reference in New Issue
Block a user