Commit 7573a30e authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #819 from blockscout/feat/mixpanel

Feature: mixpanel analytics
parents ee02cfae 2fb0ed0a
...@@ -2,4 +2,5 @@ NEXT_PUBLIC_SENTRY_DSN=xxx ...@@ -2,4 +2,5 @@ NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_CSP_REPORT_URI=xxx SENTRY_CSP_REPORT_URI=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
\ No newline at end of file NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
\ No newline at end of file
...@@ -66,6 +66,7 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__ ...@@ -66,6 +66,7 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID__ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID__
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=__PLACEHOLDER_FOR_NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY__ NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=__PLACEHOLDER_FOR_NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY__
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID__ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID__
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=__PLACEHOLDER_FOR_NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN__
# l2 config # l2 config
NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__ NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__
......
...@@ -174,6 +174,9 @@ const config = Object.freeze({ ...@@ -174,6 +174,9 @@ const config = Object.freeze({
googleAnalytics: { googleAnalytics: {
propertyId: getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID), propertyId: getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID),
}, },
mixpanel: {
projectToken: getEnvValue(process.env.NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN),
},
graphQL: { graphQL: {
defaultTxnHash: getEnvValue(process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION) || '', defaultTxnHash: getEnvValue(process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION) || '',
}, },
......
...@@ -143,6 +143,7 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -143,6 +143,7 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | - | - | `<secret>` | | NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | - | - | `<secret>` |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key for [reCAPTCHA](https://developers.google.com/recaptcha) service | - | - | `<secret>` | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key for [reCAPTCHA](https://developers.google.com/recaptcha) service | - | - | `<secret>` |
| NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | - | - | `UA-XXXXXX-X` | | NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | - | - | `UA-XXXXXX-X` |
| NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN | `string` | Project token for [Mixpanel](https://mixpanel.com/) analytics service | - | - | `<secret>` |
## L2 configuration ## L2 configuration
*Note* All variables are required only for roll-up instances *Note* All variables are required only for roll-up instances
......
...@@ -12,5 +12,8 @@ declare global { ...@@ -12,5 +12,8 @@ declare global {
providers?: Array<ExternalProvider>; providers?: Array<ExternalProvider>;
}; };
coinzilla_display: Array<CPreferences>; coinzilla_display: Array<CPreferences>;
ga?: {
getAll: () => Array<{ get: (prop: string) => string }>;
};
} }
} }
<svg viewBox="0 0 201 101" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 201 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M127.992 76.305c0 3.301-.633 6.4-1.9 9.294-1.221 2.895-2.917 5.428-5.088 7.599-2.171 2.17-4.704 3.89-7.598 5.156-2.895 1.22-5.993 1.831-9.295 1.831h-4.138c-3.302 0-6.423-.61-9.362-1.831-2.895-1.267-5.428-2.986-7.599-5.156-2.17-2.171-3.89-4.704-5.156-7.599-1.221-2.895-1.832-5.993-1.832-9.294v-52.24c0-3.3.611-6.399 1.832-9.294 1.267-2.894 2.985-5.427 5.156-7.598 2.171-2.17 4.704-3.867 7.599-5.088 2.94-1.266 6.06-1.9 9.362-1.9h4.138c3.302 0 6.4.633 9.295 1.9 2.894 1.221 5.427 2.917 7.598 5.088 2.171 2.171 3.867 4.704 5.088 7.598 1.267 2.895 1.9 5.993 1.9 9.295v52.239Zm-8.752-52.24c0-2.17-.407-4.183-1.221-6.037-.814-1.9-1.922-3.55-3.324-4.953-1.402-1.402-3.053-2.51-4.953-3.324-1.854-.814-3.867-1.221-6.038-1.221h-3.324c-2.171 0-4.206.407-6.106 1.221a16.718 16.718 0 0 0-4.952 3.324c-1.403 1.402-2.51 3.053-3.325 4.953-.814 1.854-1.22 3.867-1.22 6.038v52.239c0 2.17.406 4.206 1.22 6.105a16.72 16.72 0 0 0 3.325 4.953 16.72 16.72 0 0 0 4.952 3.324c1.9.814 3.935 1.222 6.106 1.222h3.324c2.171 0 4.184-.408 6.038-1.222 1.9-.814 3.551-1.922 4.953-3.324a16.72 16.72 0 0 0 3.324-4.953c.814-1.9 1.221-3.934 1.221-6.105v-52.24ZM47.829 82.005v16.824a8.752 8.752 0 0 1-8.752-8.751v-8.073H3.275a3.275 3.275 0 0 1-2.85-4.89L42.38 3.021a2.914 2.914 0 0 1 5.45 1.436v69.202h8.345a8.345 8.345 0 0 1-8.345 8.344ZM39.077 25.49 12.28 73.659h26.797V25.49ZM198.22 86.25c1.276-2.917 1.914-6.039 1.914-9.365v-6.7c0-2.324-.319-4.534-.957-6.63a22.47 22.47 0 0 0-2.598-5.948 22.916 22.916 0 0 0-3.965-4.922 22.222 22.222 0 0 0-5.195-3.76 25.193 25.193 0 0 0 5.195-3.76 24.48 24.48 0 0 0 3.965-4.99 22.69 22.69 0 0 0 2.598-5.878c.638-2.142.957-4.375.957-6.7v-3.35c0-3.326-.638-6.448-1.914-9.364-1.231-2.917-2.94-5.47-5.127-7.657-2.188-2.187-4.74-3.896-7.657-5.127C182.519.823 179.397.185 176.07.185h-4.17c-3.327 0-6.471.638-9.433 1.914-2.917 1.23-5.469 2.94-7.657 5.127-2.187 2.188-3.919 4.74-5.195 7.657-1.23 2.916-1.846 6.038-1.846 9.365a2.974 2.974 0 0 0 2.497 2.935l2.408.393a3.372 3.372 0 0 0 3.914-3.328c0-2.188.41-4.216 1.23-6.084.82-1.914 1.937-3.578 3.35-4.99a16.835 16.835 0 0 1 4.99-3.35c1.914-.82 3.965-1.23 6.152-1.23h3.35c2.187 0 4.215.41 6.084 1.23 1.914.82 3.577 1.937 4.99 3.35 1.413 1.412 2.529 3.076 3.35 4.99.82 1.868 1.23 3.896 1.23 6.084v4.785c0 2.188-.41 4.238-1.23 6.152a15.982 15.982 0 0 1-3.35 4.922c-1.413 1.413-3.076 2.53-4.99 3.35-1.869.82-3.897 1.23-6.084 1.23h-2.358a4.205 4.205 0 1 0 0 8.409h2.358c2.187 0 4.215.41 6.084 1.23 1.914.82 3.577 1.937 4.99 3.35 1.413 1.412 2.529 3.076 3.35 4.99.82 1.868 1.23 3.896 1.23 6.084v8.135c0 2.187-.41 4.238-1.23 6.152a16.856 16.856 0 0 1-3.35 4.99c-1.413 1.413-3.076 2.53-4.99 3.35-1.869.82-3.897 1.23-6.084 1.23h-3.35c-2.187 0-4.238-.41-6.152-1.23a16.834 16.834 0 0 1-4.99-3.35 16.837 16.837 0 0 1-3.35-4.99c-.82-1.914-1.23-3.965-1.23-6.153a3.34 3.34 0 0 0-3.851-3.299l-2.468.383a2.952 2.952 0 0 0-2.5 2.916c0 3.327.616 6.45 1.846 9.366 1.276 2.916 3.008 5.469 5.195 7.656 2.188 2.188 4.74 3.92 7.657 5.195 2.962 1.231 6.106 1.846 9.433 1.846h4.17c3.327 0 6.449-.615 9.365-1.846 2.917-1.276 5.469-3.007 7.657-5.195 2.187-2.188 3.896-4.74 5.127-7.656Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M127.992 76.305c0 3.301-.633 6.4-1.9 9.294-1.221 2.895-2.917 5.428-5.088 7.599-2.171 2.17-4.704 3.89-7.598 5.156-2.895 1.22-5.993 1.831-9.295 1.831h-4.138c-3.302 0-6.423-.61-9.362-1.831-2.895-1.267-5.428-2.986-7.599-5.156-2.17-2.171-3.89-4.704-5.156-7.599-1.221-2.895-1.832-5.993-1.832-9.294v-52.24c0-3.3.611-6.399 1.832-9.294 1.267-2.894 2.985-5.427 5.156-7.598 2.171-2.17 4.704-3.867 7.599-5.088 2.94-1.266 6.06-1.9 9.362-1.9h4.138c3.302 0 6.4.633 9.295 1.9 2.894 1.221 5.427 2.917 7.598 5.088 2.171 2.171 3.867 4.704 5.088 7.598 1.267 2.895 1.9 5.993 1.9 9.295v52.239Zm-8.752-52.24c0-2.17-.407-4.183-1.221-6.037-.814-1.9-1.922-3.55-3.324-4.953-1.402-1.402-3.053-2.51-4.953-3.324-1.854-.814-3.867-1.221-6.038-1.221h-3.324c-2.171 0-4.206.407-6.106 1.221a16.718 16.718 0 0 0-4.952 3.324c-1.403 1.402-2.51 3.053-3.325 4.953-.814 1.854-1.22 3.867-1.22 6.038v52.239c0 2.17.406 4.206 1.22 6.105a16.72 16.72 0 0 0 3.325 4.953 16.72 16.72 0 0 0 4.952 3.324c1.9.814 3.935 1.222 6.106 1.222h3.324c2.171 0 4.184-.408 6.038-1.222 1.9-.814 3.551-1.922 4.953-3.324a16.72 16.72 0 0 0 3.324-4.953c.814-1.9 1.221-3.934 1.221-6.105v-52.24Zm-71.411 57.94v16.824a8.752 8.752 0 0 1-8.752-8.751v-8.073H3.275a3.275 3.275 0 0 1-2.85-4.89L42.38 3.021a2.914 2.914 0 0 1 5.45 1.436v69.202h8.345a8.345 8.345 0 0 1-8.345 8.344ZM39.077 25.49 12.28 73.659h26.797V25.49ZM198.22 86.25c1.276-2.917 1.914-6.039 1.914-9.365v-6.7c0-2.324-.319-4.534-.957-6.63a22.47 22.47 0 0 0-2.598-5.948 22.916 22.916 0 0 0-3.965-4.922 22.222 22.222 0 0 0-5.195-3.76 25.193 25.193 0 0 0 5.195-3.76 24.48 24.48 0 0 0 3.965-4.99 22.69 22.69 0 0 0 2.598-5.878c.638-2.142.957-4.375.957-6.7v-3.35c0-3.326-.638-6.448-1.914-9.364-1.231-2.917-2.94-5.47-5.127-7.657-2.188-2.187-4.74-3.896-7.657-5.127C182.519.823 179.397.185 176.07.185h-4.17c-3.327 0-6.471.638-9.433 1.914-2.917 1.23-5.469 2.94-7.657 5.127-2.187 2.188-3.919 4.74-5.195 7.657-1.23 2.916-1.846 6.038-1.846 9.365a2.974 2.974 0 0 0 2.497 2.935l2.408.393a3.372 3.372 0 0 0 3.914-3.328c0-2.188.41-4.216 1.23-6.084.82-1.914 1.937-3.578 3.35-4.99a16.835 16.835 0 0 1 4.99-3.35c1.914-.82 3.965-1.23 6.152-1.23h3.35c2.187 0 4.215.41 6.084 1.23 1.914.82 3.577 1.937 4.99 3.35 1.413 1.412 2.529 3.076 3.35 4.99.82 1.868 1.23 3.896 1.23 6.084v4.785c0 2.188-.41 4.238-1.23 6.152a15.982 15.982 0 0 1-3.35 4.922c-1.413 1.413-3.076 2.53-4.99 3.35-1.869.82-3.897 1.23-6.084 1.23h-2.358a4.205 4.205 0 1 0 0 8.409h2.358c2.187 0 4.215.41 6.084 1.23 1.914.82 3.577 1.937 4.99 3.35 1.413 1.412 2.529 3.076 3.35 4.99.82 1.868 1.23 3.896 1.23 6.084v8.135c0 2.187-.41 4.238-1.23 6.152a16.856 16.856 0 0 1-3.35 4.99c-1.413 1.413-3.076 2.53-4.99 3.35-1.869.82-3.897 1.23-6.084 1.23h-3.35c-2.187 0-4.238-.41-6.152-1.23a16.834 16.834 0 0 1-4.99-3.35 16.837 16.837 0 0 1-3.35-4.99c-.82-1.914-1.23-3.965-1.23-6.153a3.34 3.34 0 0 0-3.851-3.299l-2.468.383a2.952 2.952 0 0 0-2.5 2.916c0 3.327.616 6.45 1.846 9.366 1.276 2.916 3.008 5.469 5.195 7.656 2.188 2.188 4.74 3.92 7.657 5.195 2.962 1.231 6.106 1.846 9.433 1.846h4.17c3.327 0 6.449-.615 9.365-1.846 2.917-1.276 5.469-3.007 7.657-5.195 2.187-2.188 3.896-4.74 5.127-7.656Z" fill="currentColor"/>
</svg> </svg>
...@@ -9,6 +9,7 @@ export enum NAMES { ...@@ -9,6 +9,7 @@ export enum NAMES {
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
INDEXING_ALERT='indexing_alert', INDEXING_ALERT='indexing_alert',
ADBLOCK_DETECTED='adblock_detected', ADBLOCK_DETECTED='adblock_detected',
MIXPANEL_DEBUG='_mixpanel_debug',
} }
export function get(name?: NAMES | undefined | null, serverCookie?: string) { export function get(name?: NAMES | undefined | null, serverCookie?: string) {
......
...@@ -9,6 +9,7 @@ function generateCspPolicy() { ...@@ -9,6 +9,7 @@ function generateCspPolicy() {
descriptors.googleAnalytics(), descriptors.googleAnalytics(),
descriptors.googleFonts(), descriptors.googleFonts(),
descriptors.googleReCaptcha(), descriptors.googleReCaptcha(),
descriptors.mixpanel(),
descriptors.monaco(), descriptors.monaco(),
descriptors.sentry(), descriptors.sentry(),
descriptors.walletConnect(), descriptors.walletConnect(),
......
...@@ -4,6 +4,7 @@ export { cloudFlare } from './cloudFlare'; ...@@ -4,6 +4,7 @@ export { cloudFlare } from './cloudFlare';
export { googleAnalytics } from './googleAnalytics'; export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts'; export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha'; export { googleReCaptcha } from './googleReCaptcha';
export { mixpanel } from './mixpanel';
export { monaco } from './monaco'; export { monaco } from './monaco';
export { sentry } from './sentry'; export { sentry } from './sentry';
export { walletConnect } from './walletConnect'; export { walletConnect } from './walletConnect';
import type CspDev from 'csp-dev';
import appConfig from 'configs/app/config';
export function mixpanel(): CspDev.DirectiveDescriptor {
if (!appConfig.mixpanel.projectToken) {
return {};
}
return {
'connect-src': [
'*.mixpanel.com',
],
};
}
export default function getGoogleAnalyticsClientId() {
return window.ga?.getAll()[0].get('clientId');
}
import type { Route } from 'nextjs-routes';
const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/': 'Homepage',
'/txs': 'Transactions',
'/tx/[hash]': 'Transaction details',
'/blocks': 'Blocks',
'/block/[height]': 'Block details',
'/accounts': 'Top accounts',
'/address/[hash]': 'Address details',
'/verified-contracts': 'Verified contracts',
'/address/[hash]/contract_verification': 'Contract verification',
'/tokens': 'Tokens',
'/token/[hash]': 'Token details',
'/token/[hash]/instance/[id]': 'Token Instance',
'/apps': 'Apps',
'/apps/[id]': 'App',
'/stats': 'Stats',
'/api-docs': 'REST API',
'/graphiql': 'GraphQL',
'/search-results': 'Search results',
'/auth/profile': 'Profile',
'/account/watchlist': 'Watchlist',
'/account/api_key': 'API keys',
'/account/custom_abi': 'Custom ABI',
'/account/public_tags_request': 'Public tags',
'/account/tag_address': 'Private tags',
'/account/verified_addresses': 'Verified addresses',
'/withdrawals': 'Withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'Export data to CSV file',
'/l2-deposits': 'Deposits (L1 > L2)',
'/l2-output-roots': 'Output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-withdrawals': 'Withdrawals (L2 > L1)',
// service routes, added only to make typescript happy
'/login': 'Login',
'/api/media-type': 'Node API: Media type',
'/api/proxy': 'Node API: Proxy',
'/api/csrf': 'Node API: CSRF token',
'/auth/auth0': 'Auth',
};
export default function getPageType(pathname: Route['pathname']) {
return PAGE_TYPE_DICT[pathname] || 'Unknown page';
}
import _capitalize from 'lodash/capitalize';
export default function getTabName(tab: string) {
return tab !== '' ? _capitalize(tab.replaceAll('_', ' ')) : 'Default';
}
import getPageType from './getPageType';
import logEvent from './logEvent';
import useInit from './useInit';
import useLogPageView from './useLogPageView';
export * from './utils';
export {
useInit,
useLogPageView,
logEvent,
getPageType,
};
import mixpanel from 'mixpanel-browser';
import type { EventTypes, EventPayload } from './utils';
type TrackFnArgs = Parameters<typeof mixpanel.track>;
export default function logEvent<EventType extends EventTypes>(
type: EventType,
properties?: EventPayload<EventType>,
optionsOrCallback?: TrackFnArgs[2],
callback?: TrackFnArgs[3],
) {
mixpanel.track(type, properties, optionsOrCallback, callback);
}
import _capitalize from 'lodash/capitalize';
import type { Config } from 'mixpanel-browser';
import mixpanel from 'mixpanel-browser';
import { useRouter } from 'next/router';
import React from 'react';
import { deviceType } from 'react-device-detect';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import getQueryParamString from 'lib/router/getQueryParamString';
import getGoogleAnalyticsClientId from './getGoogleAnalyticsClientId';
export default function useMixpanelInit() {
const [ isInited, setIsInited ] = React.useState(false);
const router = useRouter();
const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug));
React.useEffect(() => {
if (!appConfig.mixpanel.projectToken) {
return;
}
const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG);
const config: Partial<Config> = {
debug: Boolean(debugFlagQuery.current || debugFlagCookie),
};
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const userId = getGoogleAnalyticsClientId();
mixpanel.init(appConfig.mixpanel.projectToken, config);
mixpanel.register({
'Chain id': appConfig.network.id,
Environment: appConfig.isDev ? 'Dev' : 'Prod',
Authorized: isAuth,
'Viewport width': window.innerWidth,
'Viewport height': window.innerHeight,
Language: window.navigator.language,
'User id': userId,
'Device type': _capitalize(deviceType),
});
setIsInited(true);
if (debugFlagQuery.current && !debugFlagCookie) {
cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true');
}
}, []);
return isInited;
}
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/router';
import React from 'react';
import appConfig from 'configs/app/config';
import getQueryParamString from 'lib/router/getQueryParamString';
import getPageType from './getPageType';
import getTabName from './getTabName';
import logEvent from './logEvent';
import { EventTypes } from './utils';
export default function useLogPageView(isInited: boolean) {
const router = useRouter();
const pathname = usePathname();
const tab = getQueryParamString(router.query.tab);
const page = getQueryParamString(router.query.page);
React.useEffect(() => {
if (!appConfig.mixpanel.projectToken || !isInited) {
return;
}
logEvent(EventTypes.PAGE_VIEW, {
'Page type': getPageType(router.pathname),
Tab: getTabName(tab),
Page: page || undefined,
});
// these are only deps that should trigger the effect
// in some scenarios page type is not changing (e.g navigation from one address page to another),
// but we still want to log page view
// so we use pathname from 'next/navigation' instead of router.pathname from 'next/router' as deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isInited, page, pathname, tab ]);
}
export enum EventTypes {
PAGE_VIEW = 'Page view',
SEARCH_QUERY = 'Search query',
}
export type EventPayload<Type extends EventTypes> =
Type extends EventTypes.PAGE_VIEW ?
{
'Page type': string;
'Tab': string;
'Page'?: string;
} :
Type extends EventTypes.SEARCH_QUERY ? {
'Search query': string;
'Source page type': string;
'Result URL': string;
} :
undefined;
...@@ -20,7 +20,9 @@ import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; ...@@ -20,7 +20,9 @@ import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import 'lib/setLocale'; import 'lib/setLocale';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry(); useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({ const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Code, Flex, Box } from '@chakra-ui/react'; import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Code, Flex, Box } from '@chakra-ui/react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import mixpanel from 'mixpanel-browser';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -27,6 +28,10 @@ const Login = () => { ...@@ -27,6 +28,10 @@ const Login = () => {
Sentry.captureException(new Error('Test error'), { extra: { foo: 'bar' }, tags: { source: 'test' } }); Sentry.captureException(new Error('Test error'), { extra: { foo: 'bar' }, tags: { source: 'test' } });
}, []); }, []);
const checkMixpanel = React.useCallback(() => {
mixpanel.track('Test event', { my_prop: 'foo bar' });
}, []);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => { const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value); setToken(event.target.value);
}, []); }, []);
...@@ -74,6 +79,7 @@ const Login = () => { ...@@ -74,6 +79,7 @@ const Login = () => {
</> </>
) } ) }
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button> <Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
<Button colorScheme="teal" onClick={ checkMixpanel }>Check Mixpanel</Button>
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box> <Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button> <Button onClick={ handleNumIncrement } size="sm">add</Button>
......
...@@ -5,6 +5,7 @@ import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode'; ...@@ -5,6 +5,7 @@ import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload'; import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
import useAdblockDetect from 'lib/hooks/useAdblockDetect'; import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as mixpanel from 'lib/mixpanel';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus'; import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus';
import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash'; import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash';
...@@ -32,6 +33,9 @@ const Page = ({ ...@@ -32,6 +33,9 @@ const Page = ({
useAdblockDetect(); useAdblockDetect();
const isMixpanelInited = mixpanel.useInit();
mixpanel.useLogPageView(isMixpanelInited);
const renderErrorScreen = React.useCallback((error?: Error) => { const renderErrorScreen = React.useCallback((error?: Error) => {
const statusCode = getErrorCauseStatusCode(error) || 500; const statusCode = getErrorCauseStatusCode(error) || 500;
const resourceErrorPayload = getResourceErrorPayload(error); const resourceErrorPayload = getResourceErrorPayload(error);
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import _debounce from 'lodash/debounce';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import type { FormEvent, FocusEvent } from 'react'; import type { FormEvent, FocusEvent } from 'react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import SearchBarInput from './SearchBarInput'; import SearchBarInput from './SearchBarInput';
import SearchBarSuggest from './SearchBarSuggest'; import SearchBarSuggest from './SearchBarSuggest';
...@@ -20,16 +22,22 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -20,16 +22,22 @@ const SearchBar = ({ isHomepage }: Props) => {
const menuRef = React.useRef<HTMLDivElement>(null); const menuRef = React.useRef<HTMLDivElement>(null);
const menuWidth = React.useRef<number>(0); const menuWidth = React.useRef<number>(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter();
const { searchTerm, handleSearchTermChange, query, redirectCheckQuery } = useSearchQuery(); const { searchTerm, handleSearchTermChange, query, pathname, redirectCheckQuery } = useSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (searchTerm) { if (searchTerm) {
const url = route({ pathname: '/search-results', query: { q: searchTerm } }); const url = route({ pathname: '/search-results', query: { q: searchTerm } });
window.location.assign(url); mixpanel.logEvent(mixpanel.EventTypes.SEARCH_QUERY, {
'Search query': searchTerm,
'Source page type': mixpanel.getPageType(pathname),
'Result URL': url,
});
router.push({ pathname: '/search-results', query: { q: searchTerm } }, undefined, { shallow: true });
} }
}, [ searchTerm ]); }, [ searchTerm, pathname, router ]);
const handleFocus = React.useCallback(() => { const handleFocus = React.useCallback(() => {
onOpen(); onOpen();
...@@ -53,6 +61,15 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -53,6 +61,15 @@ const SearchBar = ({ isHomepage }: Props) => {
inputRef.current?.querySelector('input')?.focus(); inputRef.current?.querySelector('input')?.focus();
}, [ handleSearchTermChange ]); }, [ handleSearchTermChange ]);
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
mixpanel.logEvent(mixpanel.EventTypes.SEARCH_QUERY, {
'Search query': searchTerm,
'Source page type': mixpanel.getPageType(pathname),
'Result URL': event.currentTarget.href,
});
onClose();
}, [ pathname, searchTerm, onClose ]);
const menuPaddingX = isMobile && !isHomepage ? 32 : 0; const menuPaddingX = isMobile && !isHomepage ? 32 : 0;
const calculateMenuWidth = React.useCallback(() => { const calculateMenuWidth = React.useCallback(() => {
menuWidth.current = (inputRef.current?.getBoundingClientRect().width || 0) - menuPaddingX; menuWidth.current = (inputRef.current?.getBoundingClientRect().width || 0) - menuPaddingX;
...@@ -98,7 +115,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -98,7 +115,7 @@ const SearchBar = ({ isHomepage }: Props) => {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }> <PopoverContent w={ `${ menuWidth.current }px` } maxH={{ base: '300px', lg: '500px' }} overflowY="scroll" ref={ menuRef }>
<PopoverBody py={ 6 }> <PopoverBody py={ 6 }>
<SearchBarSuggest query={ query } redirectCheckQuery={ redirectCheckQuery } searchTerm={ searchTerm }/> <SearchBarSuggest query={ query } redirectCheckQuery={ redirectCheckQuery } searchTerm={ searchTerm } onItemClick={ handleItemClick }/>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
......
...@@ -36,9 +36,10 @@ interface Props { ...@@ -36,9 +36,10 @@ interface Props {
}; };
redirectCheckQuery: UseQueryResult<SearchRedirectResult>; redirectCheckQuery: UseQueryResult<SearchRedirectResult>;
searchTerm: string; searchTerm: string;
onItemClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
} }
const SearchBarSuggest = ({ query, redirectCheckQuery, searchTerm }: Props) => { const SearchBarSuggest = ({ query, redirectCheckQuery, searchTerm, onItemClick }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const simpleMatch: SearchResultItem | undefined = React.useMemo(() => { const simpleMatch: SearchResultItem | undefined = React.useMemo(() => {
...@@ -88,7 +89,8 @@ const SearchBarSuggest = ({ query, redirectCheckQuery, searchTerm }: Props) => { ...@@ -88,7 +89,8 @@ const SearchBarSuggest = ({ query, redirectCheckQuery, searchTerm }: Props) => {
return ( return (
<> <>
<Text fontWeight={ 500 } fontSize="sm">Found <Text fontWeight={ 700 } as="span">{ num }</Text> matching { resultText }</Text> <Text fontWeight={ 500 } fontSize="sm">Found <Text fontWeight={ 700 } as="span">{ num }</Text> matching { resultText }</Text>
{ items.map((item, index) => <SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm }/>) } { items.map((item, index) =>
<SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>) }
{ query.isLoading && <ContentLoader text="We are still searching, please wait... " fontSize="sm" mt={ 5 }/> } { query.isLoading && <ContentLoader text="We are still searching, please wait... " fontSize="sm" mt={ 5 }/> }
</> </>
); );
......
import { chakra, Text, Flex, useColorModeValue, Icon, Box } from '@chakra-ui/react'; import { chakra, Text, Flex, useColorModeValue, Icon, Box } from '@chakra-ui/react';
import type { LinkProps as NextLinkProps } from 'next/link';
import NextLink from 'next/link';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -15,9 +17,10 @@ interface Props { ...@@ -15,9 +17,10 @@ interface Props {
data: SearchResultItem; data: SearchResultItem;
isMobile: boolean | undefined; isMobile: boolean | undefined;
searchTerm: string; searchTerm: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
} }
const SearchBarSuggestItem = ({ data, isMobile, searchTerm }: Props) => { const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => {
const url = (() => { const url = (() => {
switch (data.type) { switch (data.type) {
...@@ -142,31 +145,33 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm }: Props) => { ...@@ -142,31 +145,33 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm }: Props) => {
})(); })();
return ( return (
<chakra.a <NextLink href={ url as NextLinkProps['href'] } passHref legacyBehavior>
py={ 3 } <chakra.a
px={ 1 } py={ 3 }
display="flex" px={ 1 }
flexDir="column" display="flex"
rowGap={ 2 } flexDir="column"
borderColor="divider" rowGap={ 2 }
borderBottomWidth="1px" borderColor="divider"
_last={{ borderBottomWidth="1px"
borderBottomWidth: '0', _last={{
}} borderBottomWidth: '0',
_hover={{ }}
bgColor: useColorModeValue('blue.50', 'gray.800'), _hover={{
}} bgColor: useColorModeValue('blue.50', 'gray.800'),
fontSize="sm" }}
href={ url } fontSize="sm"
_first={{ _first={{
mt: 2, mt: 2,
}} }}
> onClick={ onClick }
<Flex display="flex" alignItems="center"> >
{ firstRow } <Flex display="flex" alignItems="center">
</Flex> { firstRow }
{ secondRow } </Flex>
</chakra.a> { secondRow }
</chakra.a>
</NextLink>
); );
}; };
......
...@@ -15,6 +15,7 @@ export default function useSearchQuery(isSearchPage = false) { ...@@ -15,6 +15,7 @@ export default function useSearchQuery(isSearchPage = false) {
const [ searchTerm, setSearchTerm ] = React.useState(initialValue); const [ searchTerm, setSearchTerm ] = React.useState(initialValue);
const debouncedSearchTerm = useDebounce(searchTerm, 300); const debouncedSearchTerm = useDebounce(searchTerm, 300);
const pathname = router.pathname;
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'search', resourceName: 'search',
...@@ -42,5 +43,6 @@ export default function useSearchQuery(isSearchPage = false) { ...@@ -42,5 +43,6 @@ export default function useSearchQuery(isSearchPage = false) {
handleSearchTermChange: setSearchTerm, handleSearchTermChange: setSearchTerm,
query, query,
redirectCheckQuery, redirectCheckQuery,
pathname,
}; };
} }
...@@ -4776,6 +4776,11 @@ ...@@ -4776,6 +4776,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.186.tgz#862e5514dd7bd66ada6c70ee5fce844b06c8ee97" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.186.tgz#862e5514dd7bd66ada6c70ee5fce844b06c8ee97"
integrity sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw== integrity sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==
"@types/mixpanel-browser@^2.38.1":
version "2.38.1"
resolved "https://registry.yarnpkg.com/@types/mixpanel-browser/-/mixpanel-browser-2.38.1.tgz#914cc726194cc0358129b6d84657f078e47f7697"
integrity sha512-XzQbwgiOPsFXUQnjz3vSwcwrvJDbQ35bCiwa/1VXGrHvU1ti9+eqO1GY91DShzkEzKkkEEkxfNshS5dbBZqd7w==
"@types/node@*": "@types/node@*":
version "18.8.4" version "18.8.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.4.tgz#54be907698f40de8a45770b48486aa3cbd3adff7" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.4.tgz#54be907698f40de8a45770b48486aa3cbd3adff7"
...@@ -10406,6 +10411,11 @@ minimist@^1.2.3, minimist@^1.2.6: ...@@ -10406,6 +10411,11 @@ minimist@^1.2.3, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
mixpanel-browser@^2.47.0:
version "2.47.0"
resolved "https://registry.yarnpkg.com/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz#4e7fd3bb660c6f63443efbd169d1cd327db71ed4"
integrity sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3" version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
...@@ -11503,6 +11513,13 @@ react-debounce-input@=3.3.0: ...@@ -11503,6 +11513,13 @@ react-debounce-input@=3.3.0:
lodash.debounce "^4" lodash.debounce "^4"
prop-types "^15.8.1" prop-types "^15.8.1"
react-device-detect@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.3.tgz#97a7ae767cdd004e7c3578260f48cf70c036e7ca"
integrity sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==
dependencies:
ua-parser-js "^1.0.33"
react-dom@18.2.0: react-dom@18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
...@@ -13098,6 +13115,11 @@ typescript@4.9.5: ...@@ -13098,6 +13115,11 @@ typescript@4.9.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
ua-parser-js@^1.0.33:
version "1.0.35"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011"
integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==
uc.micro@^1.0.1, uc.micro@^1.0.5: uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment