mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
|
||||
### 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+ |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Generated
+15
@@ -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])
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user