release: v2.4.47 — instrument world: Sentry + analytics.hanzo.ai (Umami) + GTM-ready, clean AnalyticsPanel/analytics naming
Wire world's telemetry to the live Hanzo observability services, all env-gated
from KMS (no-op until IDs provisioned — ships inert, activates on config):
- Sentry: VITE_SENTRY_DSN build-secret → errors to sentry.hanzo.ai.
- Analytics: bootstrap/analytics.ts injects the analytics.hanzo.ai (Umami) script
on VITE_ANALYTICS_WEBSITE_ID; track() forwards custom product events (→
/v1/insights/events + AnalyticsPanel pageviews). track('variant_view') wired.
- GTM-ready: initGtm() loads one Google Tag Manager container (VITE_GTM_ID) that
manages GA / Facebook / LinkedIn / X tags from GTM's UI; CSP script-src allows
googletagmanager/google-analytics/facebook/linkedin/x.
- Naming: CloudAnalyticsPanel → AnalyticsPanel, org-analytics.ts → analytics.ts.
OpenGraph/Twitter/JSON-LD SEO already comprehensive.
Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
This commit is contained in:
@@ -19,6 +19,9 @@ images:
|
||||
# env=prod) by hanzoai/ci — the key name IS the build-arg. Never in git.
|
||||
build_secrets:
|
||||
- VITE_MAPBOX_TOKEN
|
||||
- VITE_SENTRY_DSN
|
||||
- VITE_ANALYTICS_WEBSITE_ID
|
||||
- VITE_GTM_ID
|
||||
|
||||
# Deploy: roll the freshly-built image onto the `world` Service CR. Fires only
|
||||
# on `main` (+ tags); PRs and forks build but never deploy. KMS hands back the
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' https: http://localhost:5173 http://127.0.0.1:46123 ws: wss: blob: data:; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://www.youtube.com https://static.cloudflareinsights.com https://s3.tradingview.com https://s.tradingview.com; worker-src 'self' blob:; font-src 'self' data: https:; media-src 'self' data: blob: https:; frame-src 'self' http://127.0.0.1:46123 https://www.youtube.com https://www.youtube-nocookie.com https://www.tradingview-widget.com https://s.tradingview.com https://www.tradingview.com;" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' https: http://localhost:5173 http://127.0.0.1:46123 ws: wss: blob: data:; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://analytics.hanzo.ai https://www.youtube.com https://static.cloudflareinsights.com https://www.googletagmanager.com https://www.google-analytics.com https://connect.facebook.net https://snap.licdn.com https://static.ads-twitter.com https://s3.tradingview.com https://s.tradingview.com; worker-src 'self' blob:; font-src 'self' data: https:; media-src 'self' data: blob: https:; frame-src 'self' http://127.0.0.1:46123 https://www.youtube.com https://www.youtube-nocookie.com https://www.tradingview-widget.com https://s.tradingview.com https://www.tradingview.com;" />
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "@hanzo/world",
|
||||
"description": "Hanzo World — real-time global intelligence dashboard.",
|
||||
"private": true,
|
||||
"version": "2.4.46",
|
||||
"version": "2.4.47",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint:md": "markdownlint-cli2 '**/*.md'",
|
||||
|
||||
+5
-2
@@ -22,6 +22,7 @@ import {
|
||||
MONITOR_COLORS,
|
||||
} from '@/config';
|
||||
import { BETA_MODE } from '@/config/beta';
|
||||
import { track } from '@/bootstrap/analytics';
|
||||
import { fetchCategoryFeeds, getFeedFailures, fetchMultipleStocks, fetchCrypto, fetchPredictions, fetchEarthquakes, fetchWeatherAlerts, fetchFredData, fetchInternetOutages, isOutagesConfigured, fetchAisSignals, initAisStream, getAisStatus, disconnectAisStream, isAisConfigured, fetchCableActivity, fetchProtestEvents, getProtestStatus, fetchFlightDelays, fetchMilitaryFlights, fetchMilitaryVessels, initMilitaryVesselStream, isMilitaryVesselTrackingConfigured, initDB, updateBaseline, calculateDeviation, addToSignalHistory, saveSnapshot, cleanOldSnapshots, analysisWorker, fetchPizzIntStatus, fetchGdeltTensions, fetchNaturalEvents, fetchRecentAwards, fetchOilAnalytics, fetchChinaMacro, fetchCyberThreats, drainTrendingSignals } from '@/services';
|
||||
import { fetchCountryMarkets } from '@/services/polymarket';
|
||||
import { mlWorker } from '@/services/ml-worker';
|
||||
@@ -130,7 +131,7 @@ import {
|
||||
ClusterPanel,
|
||||
QueuePanel,
|
||||
HanzoStatusPanel,
|
||||
CloudAnalyticsPanel,
|
||||
AnalyticsPanel,
|
||||
LlmUsagePanel,
|
||||
EnsoBenchmarkPanel,
|
||||
BlockchainPanel,
|
||||
@@ -2542,7 +2543,7 @@ export class App {
|
||||
['cloud-clusters', 'Clusters & Nodes', () => new ClusterPanel()],
|
||||
['cloud-queue', 'GPU Queue', () => new QueuePanel()],
|
||||
['llm-usage', 'LLM observability', () => new LlmUsagePanel()],
|
||||
['cloud-analytics', 'Web analytics', () => new CloudAnalyticsPanel()],
|
||||
['cloud-analytics', 'Web analytics', () => new AnalyticsPanel()],
|
||||
['enso-benchmarks', 'Enso benchmarks', () => new EnsoBenchmarkPanel()],
|
||||
];
|
||||
|
||||
@@ -2896,6 +2897,8 @@ export class App {
|
||||
if (!target) return false; // unknown variant — no-op
|
||||
if (target === prev) return true;
|
||||
|
||||
track('variant_view', { variant: target });
|
||||
|
||||
const cfg = variantConfig(target);
|
||||
// Perf probe: the in-place switch must stay cheap (a few ms) — the whole point is
|
||||
// that it does NOT cold-start. We record the synchronous cost on window for the
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Product analytics, code-split out of the entry chunk (loaded lazily from
|
||||
// main.ts once the browser is idle, like Sentry). Injects the analytics.hanzo.ai
|
||||
// tracking script, guarded on a build-time website id so it stays a no-op until
|
||||
// one is provisioned from KMS (VITE_ANALYTICS_WEBSITE_ID). track() forwards
|
||||
// custom product events to the same collector and no-ops when the script or id
|
||||
// is absent, so call sites never branch on availability.
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
umami?: { track?: (name: string, data?: Record<string, unknown>) => void };
|
||||
}
|
||||
}
|
||||
|
||||
export function initAnalytics(): void {
|
||||
const id = import.meta.env.VITE_ANALYTICS_WEBSITE_ID;
|
||||
if (!id) return;
|
||||
// Same enable guard as Sentry: never POST to the shared collector from a dev
|
||||
// machine (localhost) or the offline Tauri desktop shell.
|
||||
if (location.hostname.startsWith('localhost') || '__TAURI_INTERNALS__' in window) return;
|
||||
if (document.querySelector('script[data-website-id]')) return;
|
||||
const s = document.createElement('script');
|
||||
s.defer = true;
|
||||
s.src = 'https://analytics.hanzo.ai/script.js';
|
||||
s.dataset.websiteId = id;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
/** Forward a custom product event to analytics.hanzo.ai. No-op until the script loads. */
|
||||
export function track(name: string, data?: Record<string, unknown>): void {
|
||||
window.umami?.track?.(name, data);
|
||||
}
|
||||
|
||||
/** Google Tag Manager container — one env-gated container that manages the GA /
|
||||
* Facebook / LinkedIn / X marketing tags from GTM's UI (no per-pixel code). No-op
|
||||
* until VITE_GTM_ID is provisioned from KMS. */
|
||||
export function initGtm(): void {
|
||||
const id = import.meta.env.VITE_GTM_ID;
|
||||
if (!id) return;
|
||||
if (location.hostname.startsWith('localhost') || '__TAURI_INTERNALS__' in window) return;
|
||||
if (document.querySelector('script[data-gtm]')) return;
|
||||
const dl = ((window as unknown as { dataLayer?: unknown[] }).dataLayer ||= []);
|
||||
dl.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
|
||||
const s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(id)}`;
|
||||
s.dataset.gtm = '1';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { icon } from '@/utils/icons';
|
||||
// Umami-style) across every registered site — top pages / referrers / countries,
|
||||
// live visitors, pageviews. Admin-only (server enforces 403). Honest empty state
|
||||
// when the analytics product has no data yet.
|
||||
export class CloudAnalyticsPanel extends Panel {
|
||||
export class AnalyticsPanel extends Panel {
|
||||
private data: CloudAnalytics | null = null;
|
||||
private loaded = false;
|
||||
private timer: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type AnalyticsOverview,
|
||||
type AnalyticsTimeseries,
|
||||
type AnalyticsTop,
|
||||
} from '@/services/org-analytics';
|
||||
} from '@/services/analytics';
|
||||
import { escapeHtml } from '@/utils/sanitize';
|
||||
import { fmtCompact, fmtInt, fmtPct, fmtUsd, statTile, sparkline, shareBar } from '@/utils/cloud-format';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Panel } from './Panel';
|
||||
import { isAuthenticated, login } from '@/services/iam';
|
||||
import { getInsightsEvents, type InsightsEvent } from '@/services/org-analytics';
|
||||
import { getInsightsEvents, type InsightsEvent } from '@/services/analytics';
|
||||
import { escapeHtml } from '@/utils/sanitize';
|
||||
import { fmtInt, statTile, sparkline, shareBar, fmtAgo } from '@/utils/cloud-format';
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ export { CloudServicesPanel } from './CloudServicesPanel';
|
||||
export { ClusterPanel } from './ClusterPanel';
|
||||
export { QueuePanel } from './QueuePanel';
|
||||
export { HanzoStatusPanel } from './HanzoStatusPanel';
|
||||
export { CloudAnalyticsPanel } from './CloudAnalyticsPanel';
|
||||
export { AnalyticsPanel } from './AnalyticsPanel';
|
||||
export { LlmUsagePanel } from './LlmUsagePanel';
|
||||
export { EnsoBenchmarkPanel } from './EnsoBenchmarkPanel';
|
||||
export { BlockchainPanel } from './BlockchainPanel';
|
||||
|
||||
+7
-1
@@ -1,6 +1,7 @@
|
||||
import './styles/main.css';
|
||||
import { App } from './App';
|
||||
import type { EarlyError } from './bootstrap/sentry';
|
||||
import { initAnalytics, initGtm } from './bootstrap/analytics';
|
||||
|
||||
// Telemetry (Sentry — ~460 KB raw / ~130 KB gzip) is code-split out of the entry
|
||||
// chunk. On a low-end laptop every eager KB is main-thread parse/compile time
|
||||
@@ -28,7 +29,11 @@ const whenIdle = (cb: () => void): void => {
|
||||
else setTimeout(cb, 1200);
|
||||
};
|
||||
|
||||
// After first paint, load Sentry off the critical path and replay any buffered errors.
|
||||
// After first paint, install telemetry off the critical path: Sentry (heavy, so
|
||||
// code-split and dynamically imported here, replaying any buffered errors) and
|
||||
// product analytics (tiny — track() is used synchronously by App so it lives in
|
||||
// the main chunk; only its script-injecting init is deferred). Both are env-gated
|
||||
// no-ops until their IDs are provisioned, and best-effort so they never break boot.
|
||||
whenIdle(() => {
|
||||
import('./bootstrap/sentry').then(({ initSentry }) => {
|
||||
initSentry(earlyErrors);
|
||||
@@ -36,6 +41,7 @@ whenIdle(() => {
|
||||
window.removeEventListener('unhandledrejection', bufferError);
|
||||
earlyErrors.length = 0;
|
||||
}).catch(() => { /* telemetry is best-effort — never break boot */ });
|
||||
try { initAnalytics(); initGtm(); } catch { /* telemetry is best-effort — never break boot */ }
|
||||
});
|
||||
|
||||
import { debugInjectTestEvents, debugGetCells, getCellCount } from '@/services/geo-convergence';
|
||||
|
||||
Vendored
+2
@@ -4,4 +4,6 @@ declare const __APP_VERSION__: string;
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SENTRY_DSN?: string;
|
||||
readonly VITE_ANALYTICS_WEBSITE_ID?: string;
|
||||
readonly VITE_GTM_ID?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user