Send explorer pageviews to Hanzo Insights

explore.lux.network measured nothing. It carried a Google Analytics component
that no deployment had ever given a property id, so every visit to the explorer
went unrecorded and there was no way to tell whether a change to the site
helped anyone. This is one of seven Lux surfaces moving onto first-party
analytics.

Pageviews now go to Hanzo Insights through @hanzo/event, the client the rest of
the fleet already reports with. It mounts once in _app, in the slot the Google
tag used to occupy, so every route is covered: the provider records the first
view and the route tracker records each one after it. The ingest key is
publishable and write-only, so it belongs in the bundle, and a deploy can point
the surface at another project with NEXT_PUBLIC_PUBLISHABLE_KEY. The new CSP
entry allows api.hanzo.ai so the beacon survives wherever that policy is
enforced.

Google Analytics is removed rather than left running alongside, together with
its feature config, CSP entry and environment variable, because two stacks
measuring the same site is how you end up trusting neither. Error reporting
stays with Rollbar, which this app already runs.
This commit is contained in:
2026-08-13 12:10:44 -07:00
parent e47a1993c4
commit 2ff64fe352
18 changed files with 107 additions and 97 deletions
-1
View File
@@ -1,7 +1,6 @@
NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
-25
View File
@@ -1,25 +0,0 @@
import type { Feature } from './types';
import app from '../app';
import { getEnvValue } from '../utils';
const propertyId = getEnvValue('NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID');
const title = 'Google analytics';
const config: Feature<{ propertyId: string }> = (() => {
if (!app.isPrivateMode && propertyId) {
return Object.freeze({
title,
isEnabled: true,
propertyId,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
+29
View File
@@ -0,0 +1,29 @@
import type { Feature } from './types';
import app from '../app';
import { getEnvValue } from '../utils';
// Publishable ingest key for this explorer. It is write-only — it attributes a
// write and never mints a reading principal — so it belongs in the browser
// bundle. NEXT_PUBLIC_PUBLISHABLE_KEY is the fleet's one spelling for this
// value and overrides the default when a deploy supplies it.
const ingestKey = getEnvValue('NEXT_PUBLIC_PUBLISHABLE_KEY') || 'pk-gUZp6ZVfhJzSwK-rb4oLbVkpCnMBx5uSCpxf_5yEhQk';
const title = 'Hanzo Insights';
const config: Feature<{ ingestKey: string }> = (() => {
if (!app.isPrivateMode && ingestKey) {
return Object.freeze({
title,
isEnabled: true,
ingestKey,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
+1 -1
View File
@@ -22,7 +22,7 @@ export { default as faultProofSystem } from './faultProofSystem';
export { default as flashblocks } from './flashblocks';
export { default as gasTracker } from './gasTracker';
export { default as getGasButton } from './getGasButton';
export { default as googleAnalytics } from './googleAnalytics';
export { default as hanzoInsights } from './hanzoInsights';
export { default as hotContracts } from './hotContracts';
export { default as marketplace } from './marketplace';
export { default as megaEth } from './megaEth';
@@ -7,7 +7,7 @@ export default yup.object({
NEXT_PUBLIC_WALLET_CONNECT_FEATURED_WALLET_IDS: yup.array().transform(replaceQuotes).json().of(yup.string()),
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
NEXT_PUBLIC_PUBLISHABLE_KEY: yup.string(),
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: yup.string(),
@@ -4,7 +4,6 @@ NEXT_PUBLIC_ACCOUNT_API_KEYS_BUTTON=false
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_WALLET_CONNECT_FEATURED_WALLET_IDS=['xxx']
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES='{"record_sessions_percent": 0.5,"record_heatmap_data": true}'
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
@@ -29,6 +29,5 @@ NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_ADVANCED_FILTER_ENABLED=false
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=xxx
NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN=xxx
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
-1
View File
@@ -81,7 +81,6 @@ frontend:
NEXT_PUBLIC_CLUSTERS_CDN_URL: https://cdn.clusters.xyz
envFromSecret:
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY
+7 -3
View File
@@ -44,7 +44,7 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
- [User operations](#user-operations-erc-4337)
- [Rollup chain](#rollup-chain)
- [Export data to CSV file](#export-data-to-csv-file)
- [Google analytics](#google-analytics)
- [Hanzo Insights](#hanzo-insights)
- [Mixpanel analytics](#mixpanel-analytics)
- [GrowthBook feature flagging and A/B testing](#growthbook-feature-flagging-and-ab-testing)
- [GraphQL API documentation](#graphql-api-documentation)
@@ -579,11 +579,15 @@ feature enabled but no key, the dialog says so rather than failing silently.
&nbsp;
### Google analytics
### Hanzo Insights
Pageviews go to Hanzo Insights through `@hanzo/event`. The key is publishable and
write-only, and the app carries a default for this deployment, so this variable is
only needed to point the surface at a different project.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | true | - | `UA-XXXXXX-X` | v1.0.x+ |
| NEXT_PUBLIC_PUBLISHABLE_KEY | `string` | Publishable ingest key for [Hanzo Insights](https://insights.hanzo.ai/) | false | - | `pk-...` | v1.0.x+ |
&nbsp;
+1 -1
View File
@@ -9,9 +9,9 @@ function generateCspPolicy(isPrivateMode = false) {
isPrivateMode ? {} : descriptors.blockchainInteraction(),
descriptors.cloudFlare(),
descriptors.flashblocks(),
isPrivateMode ? {} : descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
isPrivateMode ? {} : descriptors.hanzoInsights(),
descriptors.helia(),
isPrivateMode ? {} : descriptors.marketplace(),
descriptors.megaEth(),
-29
View File
@@ -1,29 +0,0 @@
import type CspDev from 'csp-dev';
import config from 'configs/app';
export function googleAnalytics(): CspDev.DirectiveDescriptor {
if (!config.features.googleAnalytics.isEnabled) {
return {};
}
return {
'connect-src': [
'*.google-analytics.com',
'*.analytics.google.com',
'https://www.googletagmanager.com',
'https://stats.g.doubleclick.net',
],
'script-src': [
// inline script hash, see ui/shared/GoogleAnalytics.tsx
'\'sha256-WXRwCtfSfMoCPzPUIOUAosSaADdGgct0/Lhmnbm7MCA=\'',
'https://www.googletagmanager.com',
'*.google-analytics.com',
'*.analytics.google.com',
],
'img-src': [
'*.google-analytics.com',
'*.analytics.google.com',
],
};
}
+15
View File
@@ -0,0 +1,15 @@
import type CspDev from 'csp-dev';
import config from 'configs/app';
export function hanzoInsights(): CspDev.DirectiveDescriptor {
if (!config.features.hanzoInsights.isEnabled) {
return {};
}
return {
'connect-src': [
'https://api.hanzo.ai',
],
};
}
+1 -1
View File
@@ -3,9 +3,9 @@ export { app } from './app';
export { blockchainInteraction } from './blockchainInteraction';
export { cloudFlare } from './cloudFlare';
export { flashblocks } from './flashblocks';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
export { hanzoInsights } from './hanzoInsights';
export { helia } from './helia';
export { marketplace } from './marketplace';
export { megaEth } from './megaEth';
+1
View File
@@ -46,6 +46,7 @@
"@dynamic-labs/ethereum": "4.52.2",
"@dynamic-labs/sdk-react-core": "4.52.2",
"@dynamic-labs/wagmi-connector": "4.52.2",
"@hanzo/event": "^0.3.21",
"@helia/verified-fetch": "2.6.12",
"@luxfi/bens-types": "1.4.3",
"@luxfi/interchain-indexer-types": "0.0.10",
+2 -2
View File
@@ -77,7 +77,7 @@ import { SocketProvider } from 'lib/socket/context';
import { Provider as ThemeProvider } from 'toolkit/next/provider';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import HanzoInsights from 'ui/shared/HanzoInsights';
import Layout from 'ui/shared/layout/Layout';
import Web3Provider from 'ui/shared/web3/Web3Provider';
@@ -225,7 +225,7 @@ function MyApp({ Component, pageProps, router }: AppPropsWithLayout) {
</MarketplaceContextProvider>
</RewardsProvider>
</SocketProvider>
<GoogleAnalytics/>
<HanzoInsights/>
</AppContextProvider>
</Web3Provider>
</QueryClientProvider>
+15
View File
@@ -60,6 +60,9 @@ importers:
'@dynamic-labs/wagmi-connector':
specifier: 4.52.2
version: 4.52.2(2ec052942d9018144c9e8da474b03fb7)
'@hanzo/event':
specifier: ^0.3.21
version: 0.3.21([email protected])
'@helia/verified-fetch':
specifier: 2.6.12
version: 2.6.12([email protected])([email protected](@babel/[email protected])(@react-native/[email protected](@babel/[email protected])([email protected])([email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected])
@@ -2674,6 +2677,14 @@ packages:
engines: {node: '>=6'}
hasBin: true
'@hanzo/[email protected]':
resolution: {integrity: sha512-gUEql7ZteyNIkxdX9G44plCNYMNppdU3K9s9uc3IBtwdMjCS6iRM7Z7EP9aOtZm8LqPnAJU9l7IRYYFKhG9cRQ==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
react:
optional: true
'@hanzo/[email protected]':
resolution: {integrity: sha512-VJw3CocBJztLIil55NpCv2XxL7mIQHebTnhR/R7EDoJ19RUWATqqzn6r+jalPnzRR33D0eUKT7VR0IWz9GhQ8w==}
peerDependencies:
@@ -17703,6 +17714,10 @@ snapshots:
protobufjs: 7.3.2
yargs: 17.7.2
'@hanzo/[email protected]([email protected])':
optionalDependencies:
react: 19.2.4
'@hanzo/[email protected]([email protected])([email protected]([email protected]))([email protected]([email protected]([email protected]))([email protected]))([email protected](@babel/[email protected])(@react-native/[email protected](@babel/[email protected])([email protected])([email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])':
dependencies:
'@hanzogui/accordion': 7.3.0([email protected]([email protected]))([email protected](@babel/[email protected])(@react-native/[email protected](@babel/[email protected])([email protected])([email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])
-30
View File
@@ -1,30 +0,0 @@
import Script from 'next/script';
import React from 'react';
import config from 'configs/app';
const feature = config.features.googleAnalytics;
const GoogleAnalytics = () => {
if (!feature.isEnabled) {
return null;
}
const id = feature.propertyId;
return (
<>
<Script strategy="lazyOnload" src={ `https://www.googletagmanager.com/gtag/js?id=${ id }` }/>
<Script strategy="lazyOnload" id="google-analytics">
{ `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', window.__envs.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID);
` }
</Script>
</>
);
};
export default React.memo(GoogleAnalytics);
+34
View File
@@ -0,0 +1,34 @@
import { AnalyticsProvider, usePageview } from '@hanzo/event/react';
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
const feature = config.features.hanzoInsights;
// The provider records the first pageview when it mounts. A client-side route
// change never remounts it, so this records every pageview after the first.
const RouteTrackerContent = () => {
const router = useRouter();
usePageview(router.asPath);
return null;
};
const RouteTracker = React.memo(RouteTrackerContent);
const HanzoInsights = () => {
if (!feature.isEnabled) {
return null;
}
return (
// Errors stay with Rollbar, which this app already runs — one reporter, not two.
<AnalyticsProvider config={{ product: 'explore', ingestKey: feature.ingestKey, captureErrors: false }}>
<RouteTracker/>
</AnalyticsProvider>
);
};
export default React.memo(HanzoInsights);