telemetry: move chat onto the one door (/v1/event) + observe autocapture
- @hanzo/event ^0.2.0 -> ^0.3.1: chat now posts to the canonical front door
(POST /v1/event) instead of legacy /v1/analytics + /v1/tracker.
- Mount @hanzo/observe (ObserveProvider) on the SAME client: default-on
interaction autocapture ($click/$input/$change/$submit) with a semantic DOM
hierarchy; input values redacted by default (PII-free). nav={false} so the
event layer stays the single pageview counter (no double-count).
- Consent-gated: honor Do Not Track / Global Privacy Control (enabled), applied
to the client and to autocapture.
- Ingest key: read VITE_HANZO_INGEST_KEY so logged-out / guest views emit
accepted telemetry (chat has a first-class guest surface).
- Remove the legacy direct tags from index.html: the analytics.hanzo.ai Umami
script and the inline PostHog Insights snippet (CSP-blocked + key-gated dead
code) — the one @hanzo/event client replaces both. Drop analytics.hanzo.ai
from the script-src CSP (connect-src already allows *.hanzo.ai for /v1/event).
- Document VITE_HANZO_INGEST_KEY / VITE_HANZO_ANALYTICS_HOST in .env.example.
This commit is contained in:
@@ -737,6 +737,15 @@ HELP_AND_FAQ_URL=https://hanzo.ai/chat
|
||||
# Google tag manager id
|
||||
#ANALYTICS_GTM_ID=user provided google tag manager id
|
||||
|
||||
# Hanzo telemetry (the one @hanzo/event client → POST api.hanzo.ai/v1/event).
|
||||
# Build-time (VITE_) vars, baked into the client bundle.
|
||||
# VITE_HANZO_INGEST_KEY: publishable ingest key (pk_…), write-only, safe to ship.
|
||||
# Lets logged-out / guest visitors emit accepted pageviews + errors (Cloud stamps
|
||||
# the org from the key). Provision one per org via POST /v1/ingest/keys.
|
||||
# VITE_HANZO_ANALYTICS_HOST: override the front-door host (default https://api.hanzo.ai).
|
||||
#VITE_HANZO_INGEST_KEY=pk_live_xxx
|
||||
#VITE_HANZO_ANALYTICS_HOST=https://api.hanzo.ai
|
||||
|
||||
# limit conversation file imports to a certain number of bytes in size to avoid the container
|
||||
# maxing out memory limitations by unremarking this line and supplying a file size in bytes
|
||||
# such as the below example of 250 mib
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ const startServer = async () => {
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.setHeader(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://analytics.hanzo.ai https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' data: blob:; connect-src 'self' https://*.hanzo.ai https://*.hanzo.chat wss://*.hanzo.chat https://static.cloudflareinsights.com https://cloudflareinsights.com; frame-ancestors 'none';",
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' data: blob:; connect-src 'self' https://*.hanzo.ai https://*.hanzo.chat wss://*.hanzo.chat https://static.cloudflareinsights.com https://cloudflareinsights.com; frame-ancestors 'none';",
|
||||
);
|
||||
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||
next();
|
||||
|
||||
+1
-12
@@ -45,18 +45,7 @@
|
||||
document.head.appendChild(loadingContainerStyle);
|
||||
</script>
|
||||
<script defer type="module" src="/src/main.jsx"></script>
|
||||
<!-- Hanzo Analytics -->
|
||||
<script defer src="https://analytics.hanzo.ai/script.js" data-website-id="%VITE_ANALYTICS_SITE_ID%"></script>
|
||||
<!-- Hanzo Insights (Insights) - only loaded when VITE_INSIGHTS_KEY is set -->
|
||||
<script>
|
||||
(function() {
|
||||
var key = "%VITE_INSIGHTS_KEY%";
|
||||
var host = "%VITE_INSIGHTS_HOST%" || "https://insights.hanzo.ai";
|
||||
if (!key || key === "%VITE_INSIGHTS_KEY%") return;
|
||||
!function(t,e){var o,n,p,r;e.__SV||(window.insights=e,e._i=[],e.init=function(i,s,a){function g(t,e){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="insights",u.people=u.people||[],u.toString=function(t){var e="insights";return"insights"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+" (stub)"},o="capture identify alias reset opt_in_capturing opt_out_capturing has_opted_out_capturing has_opted_in_capturing register register_once unregister".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.insights||(window.insights=[]));
|
||||
insights.init(key, {api_host: host, person_profiles: 'identified_only'});
|
||||
})();
|
||||
</script>
|
||||
<!-- Telemetry is the one @hanzo/event client (POST /v1/event), mounted in AnalyticsProvider — no direct third-party tags. -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
|
||||
+2
-1
@@ -35,9 +35,10 @@
|
||||
"@dicebear/collection": "^9.2.2",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
"@hanzo/ai": "^0.2.1",
|
||||
"@hanzo/event": "^0.2.0",
|
||||
"@hanzo/event": "^0.3.1",
|
||||
"@hanzo/iam": "^0.13.1",
|
||||
"@hanzo/logo": "^1.0.13",
|
||||
"@hanzo/observe": "^0.1.0",
|
||||
"@hanzo/ui": "npm:@hanzo/ui-shadcn@^5.7.4",
|
||||
"@hanzo/usage": "^0.1.6",
|
||||
"@hanzochat/client": "workspace:*",
|
||||
|
||||
@@ -6,10 +6,32 @@ import {
|
||||
useAnalytics,
|
||||
usePageview,
|
||||
} from '@hanzo/event/react';
|
||||
import { ObserveProvider } from '@hanzo/observe/react';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
|
||||
const ANALYTICS_HOST = import.meta.env.VITE_HANZO_ANALYTICS_HOST || 'https://api.hanzo.ai';
|
||||
|
||||
/** Publishable ingest key (pk_…) — write-only, safe to ship in the bundle. It lets
|
||||
* logged-out / guest visitors reach the ONE front door (POST /v1/event) so their
|
||||
* pageviews + errors land server-side (Cloud stamps the org from the key). Chat has
|
||||
* a first-class guest surface, so this is what makes anonymous guest telemetry
|
||||
* accepted; provision one per org via POST /v1/ingest/keys. */
|
||||
const INGEST_KEY = import.meta.env.VITE_HANZO_INGEST_KEY || undefined;
|
||||
|
||||
/** Consent gate — honor Do Not Track / Global Privacy Control as opt-out. The client
|
||||
* sends no PII (a stable user id only, never email), so the browser's standard
|
||||
* privacy signals are the whole consent surface. Opting out suppresses pageviews,
|
||||
* events, errors, AND interaction autocapture. */
|
||||
function telemetryEnabled(): boolean {
|
||||
if (typeof navigator === 'undefined') return true;
|
||||
const n = navigator as Navigator & { msDoNotTrack?: string | null; globalPrivacyControl?: boolean };
|
||||
const w = typeof window !== 'undefined' ? (window as Window & { doNotTrack?: string | null }) : undefined;
|
||||
const dnt = n.doNotTrack ?? w?.doNotTrack ?? n.msDoNotTrack;
|
||||
if (dnt === '1' || dnt === 'yes') return false;
|
||||
if (n.globalPrivacyControl) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the authenticated user and fires a pageview on every route change.
|
||||
* The provider fires the first pageview itself; this keeps subsequent SPA
|
||||
@@ -33,9 +55,16 @@ function AnalyticsBridge() {
|
||||
}
|
||||
|
||||
/**
|
||||
* First-party product analytics for Hanzo Chat (@hanzo/event → cloud
|
||||
* /v1/analytics). Mounted inside AuthContextProvider so it can read the live JWT
|
||||
* and the resolved user; anonymous events still flow when no token is present.
|
||||
* First-party telemetry for Hanzo Chat — the ONE @hanzo/event client to the ONE
|
||||
* front door (POST api.hanzo.ai/v1/event), which Cloud fans out to the web
|
||||
* (analytics), product (insights), and error (sentry) lenses. Mounted inside
|
||||
* AuthContextProvider so it reads the live JWT and resolved user; a publishable
|
||||
* ingest key carries logged-out / guest views. `<ObserveProvider>` rides the SAME
|
||||
* client and adds default-on interaction autocapture ($click/$input/$change/
|
||||
* $submit) with a semantic DOM hierarchy — input values redacted by default
|
||||
* (PII-free). nav={false}: the event layer already counts pageviews exactly once
|
||||
* (AnalyticsBridge), so observe does not also patch history; `enabled` mirrors the
|
||||
* DNT/GPC consent gate.
|
||||
*/
|
||||
export default function AnalyticsProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuthContext();
|
||||
@@ -45,22 +74,28 @@ export default function AnalyticsProvider({ children }: { children: ReactNode })
|
||||
const tokenRef = useRef<string | undefined>(token);
|
||||
tokenRef.current = token;
|
||||
|
||||
const enabled = telemetryEnabled();
|
||||
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
product: 'chat',
|
||||
host: ANALYTICS_HOST,
|
||||
ingestKey: INGEST_KEY,
|
||||
enabled,
|
||||
getToken: () => {
|
||||
const value = tokenRef.current;
|
||||
return value && value !== 'session' ? value : undefined;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[enabled],
|
||||
);
|
||||
|
||||
return (
|
||||
<CaptureProvider config={config}>
|
||||
<AnalyticsBridge />
|
||||
{children}
|
||||
<ObserveProvider nav={false} enabled={enabled}>
|
||||
<AnalyticsBridge />
|
||||
{children}
|
||||
</ObserveProvider>
|
||||
</CaptureProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Generated
+37
-5
@@ -380,11 +380,17 @@ importers:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1
|
||||
'@hanzo/event':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(react@18.3.1)
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1(react@18.3.1)
|
||||
'@hanzo/iam':
|
||||
specifier: ^0.13.1
|
||||
version: 0.13.1(react@18.3.1)
|
||||
'@hanzo/logo':
|
||||
specifier: ^1.0.13
|
||||
version: 1.0.13(react@18.3.1)
|
||||
'@hanzo/observe':
|
||||
specifier: ^0.1.0
|
||||
version: 0.1.0(react@18.3.1)
|
||||
'@hanzo/ui':
|
||||
specifier: npm:@hanzo/ui-shadcn@^5.7.4
|
||||
version: '@hanzo/ui-shadcn@5.7.4(@hookform/resolvers@3.10.0(react-hook-form@7.71.2(react@18.3.1)))(@modelcontextprotocol/sdk@1.22.0(@cfworker/json-schema@4.1.1))(@tanstack/react-query@4.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(date-fns@3.6.0)(embla-carousel@8.6.0)(framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lucide-react@0.394.0(react@18.3.1))(mermaid@11.13.0)(mobx@6.15.0)(next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.71.2(react@18.3.1))(react-resizable-panels@3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(yaml@2.8.3))(validator@13.15.26)'
|
||||
@@ -3794,8 +3800,8 @@ packages:
|
||||
resolution: {integrity: sha512-m0k6V6Im8m5RXWHUy/E7DvpZ9tFBYUw21NTYFxNxUiPI1l+yEvHEVytqEbEtHZOCa9Ytb8nfuNzJIsfCeF/zZw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@hanzo/event@0.2.0':
|
||||
resolution: {integrity: sha512-jx+xlT5no88PYg/k465/pQ7a+oQcfImB5X0dJ4/ZSSF5bwqPCItvN/9Znt4SQpq18EQeMuR5GiLmxo1wSP5uqw==}
|
||||
'@hanzo/event@0.3.1':
|
||||
resolution: {integrity: sha512-FAA6C/+hP8woUdle9VxRZXreCpCa+K1zlZAVOT8HTYHu6QH37LJIG2KS5NXutx80haSwNPGooJJlZRvx9x07ug==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
@@ -3811,6 +3817,22 @@ packages:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@hanzo/logo@1.0.13':
|
||||
resolution: {integrity: sha512-94VA6S7oVY5UZ816295Ez12p8a76ZGEct14JQwE6r5UCutD3K9TBgx5V01gjwxwVL9tWnXn/yut4PHRAY/9RNQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@hanzo/observe@0.1.0':
|
||||
resolution: {integrity: sha512-VWfMz4F6gexchEaEjK9btxcTOVhWk8km64TN69hWLa1jWB4uTL9VdTj3cWZRDl/OP1lS8YcjpLdSI1q+C7fymg==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@hanzo/ui-shadcn@5.7.4':
|
||||
resolution: {integrity: sha512-MFGPp+GYzaBUyd79F2mrk40SDLTFgUBEoPiSFVuJjekOVr5bJvUVNIWdoNtrl5ITL86N2ruiCNRhKGmEJyHFQA==}
|
||||
hasBin: true
|
||||
@@ -17693,7 +17715,7 @@ snapshots:
|
||||
|
||||
'@hanzo/ai@0.2.1': {}
|
||||
|
||||
'@hanzo/event@0.2.0(react@18.3.1)':
|
||||
'@hanzo/event@0.3.1(react@18.3.1)':
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
@@ -17705,6 +17727,16 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@hanzo/logo@1.0.13(react@18.3.1)':
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@hanzo/observe@0.1.0(react@18.3.1)':
|
||||
dependencies:
|
||||
'@hanzo/event': 0.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@hanzo/ui-shadcn@5.7.4(@hookform/resolvers@3.10.0(react-hook-form@7.71.2(react@18.3.1)))(@modelcontextprotocol/sdk@1.22.0(@cfworker/json-schema@4.1.1))(@tanstack/react-query@4.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(date-fns@3.6.0)(embla-carousel@8.6.0)(framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lucide-react@0.394.0(react@18.3.1))(mermaid@11.13.0)(mobx@6.15.0)(next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.71.2(react@18.3.1))(react-resizable-panels@3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(yaml@2.8.3))(validator@13.15.26)':
|
||||
dependencies:
|
||||
'@hookform/resolvers': 3.10.0(react-hook-form@7.71.2(react@18.3.1))
|
||||
|
||||
Reference in New Issue
Block a user