Commit 538ecdc9 authored by tom's avatar tom

base implementation

parent a3d1398c
......@@ -4,5 +4,6 @@ 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
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
FAVICON_GENERATOR_API_KEY=xxx
\ No newline at end of file
......@@ -9,7 +9,7 @@ const baseUrl = [
appHost,
appPort && ':' + appPort,
].filter(Boolean).join('');
const isDev = getEnvValue('NODE_ENV') === 'development';
const isDev = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'development';
const app = Object.freeze({
isDev,
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const clientKey = getEnvValue('NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY');
const title = 'GrowthBook feature flagging and A/B testing';
const config: Feature<{ clientKey: string }> = (() => {
if (clientKey) {
return Object.freeze({
title,
isEnabled: true,
clientKey,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
......@@ -8,6 +8,7 @@ export { default as blockchainInteraction } from './blockchainInteraction';
export { default as csvExport } from './csvExport';
export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace';
export { default as mixpanel } from './mixpanel';
export { default as restApiDocs } from './restApiDocs';
......
......@@ -451,6 +451,7 @@ const schema = yup
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
// Misc
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
......
......@@ -38,6 +38,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Export data to CSV file](ENVS.md#export-data-to-csv-file)
- [Google analytics](ENVS.md#google-analytics)
- [Mixpanel analytics](ENVS.md#mixpanel-analytics)
- [GrowthBook feature flagging and A/B testing](ENVS.md#growthbook-feature-flagging-and-ab-testing)
- [GraphQL API documentation](ENVS.md#graphql-api-documentation)
- [REST API documentation](ENVS.md#rest-api-documentation)
- [Marketplace](ENVS.md#marketplace)
......@@ -386,6 +387,14 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&nbsp;
### GrowthBook feature flagging and A/B testing
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY | `string` | Client SDK key for [GrowthBook](https://www.growthbook.io/) service | true | - | `<your-secret>` |
&nbsp;
### GraphQL API documentation
This feature is **always enabled**, but you can configure its behavior by passing the following variables.
......
import { GrowthBook } from '@growthbook/growthbook-react';
import mixpanel from 'mixpanel-browser';
import config from 'configs/app';
export const growthBook = (() => {
const feature = config.features.growthBook;
if (!feature.isEnabled) {
return;
}
return new GrowthBook({
apiHost: 'https://cdn.growthbook.io',
clientKey: feature.clientKey,
enableDevMode: config.app.isDev,
trackingCallback: (experiment, result) => {
mixpanel.track('$experiment_started', {
'Experiment name': experiment.key,
'Variant name': result.variationId,
$source: 'growthbook',
});
},
});
})();
export default function getGoogleAnalyticsClientId() {
return window.ga?.getAll()[0].get('clientId');
}
import config from 'configs/app';
import delay from 'lib/delay';
export default function isGoogleAnalyticsLoaded(retries = 3): Promise<boolean> {
if (!retries || !config.features.googleAnalytics.isEnabled) {
return Promise.resolve(false);
}
return typeof window.ga?.getAll === 'function' ? Promise.resolve(true) : delay(500).then(() => isGoogleAnalyticsLoaded(retries - 1));
}
......@@ -7,47 +7,48 @@ import { deviceType } from 'react-device-detect';
import config from 'configs/app';
import * as cookies from 'lib/cookies';
import { growthBook } from 'lib/growthbook/index';
import getQueryParamString from 'lib/router/getQueryParamString';
import getGoogleAnalyticsClientId from './getGoogleAnalyticsClientId';
import isGoogleAnalyticsLoaded from './isGoogleAnalyticsLoaded';
export default function useMixpanelInit() {
const [ isInited, setIsInited ] = React.useState(false);
const router = useRouter();
const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug));
React.useEffect(() => {
isGoogleAnalyticsLoaded().then((isGALoaded) => {
const feature = config.features.mixpanel;
if (!feature.isEnabled) {
return;
}
const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG);
const mixpanelConfig: Partial<Config> = {
debug: Boolean(debugFlagQuery.current || debugFlagCookie),
};
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
mixpanel.init(feature.projectToken, mixpanelConfig);
mixpanel.register({
'Chain id': config.chain.id,
Environment: config.app.isDev ? 'Dev' : 'Prod',
Authorized: isAuth,
'Viewport width': window.innerWidth,
'Viewport height': window.innerHeight,
Language: window.navigator.language,
'User id': isGALoaded ? getGoogleAnalyticsClientId() : undefined,
'Device type': _capitalize(deviceType),
});
setIsInited(true);
if (debugFlagQuery.current && !debugFlagCookie) {
cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true');
}
const feature = config.features.mixpanel;
if (!feature.isEnabled) {
return;
}
const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG);
const mixpanelConfig: Partial<Config> = {
debug: Boolean(debugFlagQuery.current || debugFlagCookie),
loaded: function(mixpanel) {
growthBook?.setAttributes({
...growthBook.getAttributes(),
id: mixpanel.get_distinct_id(),
});
},
};
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
mixpanel.init(feature.projectToken, mixpanelConfig);
mixpanel.register({
'Chain id': config.chain.id,
Environment: config.app.isDev ? 'Dev' : 'Prod',
Authorized: isAuth,
'Viewport width': window.innerWidth,
'Viewport height': window.innerHeight,
Language: window.navigator.language,
'Device type': _capitalize(deviceType),
});
setIsInited(true);
if (debugFlagQuery.current && !debugFlagCookie) {
cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true');
}
}, []);
return isInited;
......
......@@ -9,6 +9,7 @@ function generateCspPolicy() {
descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
descriptors.growthBook(),
descriptors.mixpanel(),
descriptors.monaco(),
descriptors.safe(),
......
import type CspDev from 'csp-dev';
import config from 'configs/app';
export function growthBook(): CspDev.DirectiveDescriptor {
if (!config.features.growthBook.isEnabled) {
return {};
}
return {
'connect-src': [
'cdn.growthbook.io',
],
};
}
......@@ -4,6 +4,7 @@ export { cloudFlare } from './cloudFlare';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
export { growthBook } from './growthBook';
export { mixpanel } from './mixpanel';
export { monaco } from './monaco';
export { safe } from './safe';
......
import type { ChakraProps } from '@chakra-ui/react';
import { GrowthBookProvider } from '@growthbook/growthbook-react';
import * as Sentry from '@sentry/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
......@@ -12,6 +13,7 @@ import useQueryClientConfig from 'lib/api/useQueryClientConfig';
import { AppContextProvider } from 'lib/contexts/app';
import { ChakraProvider } from 'lib/contexts/chakra';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { growthBook } from 'lib/growthbook/index';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
......@@ -39,6 +41,10 @@ const ERROR_SCREEN_STYLES: ChakraProps = {
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
React.useEffect(() => {
growthBook?.loadFeatures();
}, []);
const queryClient = useQueryClientConfig();
const handleError = React.useCallback((error: Error) => {
......@@ -56,11 +62,13 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<Web3ModalProvider>
<AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }>
<ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
<GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
</GrowthBookProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Code, Flex, Box } from '@chakra-ui/react';
import { useFeatureValue } from '@growthbook/growthbook-react';
import * as Sentry from '@sentry/react';
import mixpanel from 'mixpanel-browser';
import type { ChangeEvent } from 'react';
......@@ -15,6 +16,8 @@ const Login = () => {
const toast = useToast();
const [ num, setNum ] = useGradualIncrement(0);
const testFeatureValue = useFeatureValue('test_value', 'fallback');
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
......@@ -77,12 +80,15 @@ const Login = () => {
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
<Button colorScheme="teal" onClick={ checkMixpanel }>Check Mixpanel</Button>
<Flex columnGap={ 2 }>
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
<Button colorScheme="teal" onClick={ checkMixpanel }>Check Mixpanel</Button>
</Flex>
<Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button>
</Flex>
<Box>Test feature value: <b>{ testFeatureValue }</b></Box>
</VStack>
);
......
......@@ -2859,6 +2859,20 @@
"@n1ru4l/push-pull-async-iterable-iterator" "^3.1.0"
meros "^1.1.4"
"@growthbook/growthbook-react@0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@growthbook/growthbook-react/-/growthbook-react-0.21.0.tgz#e3aee1bda7faf3ea036badfbca09e2230884b7f7"
integrity sha512-LM4jG9xThSYCMfRQECeLtTr8sYyiAxC5NLmT4U3uxKyvVUCB+s0k/vdKrvHcv3YXNJ37mW3VtK9XkNSuW16mcw==
dependencies:
"@growthbook/growthbook" "^0.31.0"
"@growthbook/growthbook@^0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@growthbook/growthbook/-/growthbook-0.31.0.tgz#66273344805c45dc406940ab719856385afa4750"
integrity sha512-VtI5IibZHhvtrlZhfopGYhVXZ++FewgY0BtQQyEqFSrhCYFvEEtq+GkuSm4GcC0JxNuHiYJr9/R3GpffJV/T4Q==
dependencies:
dom-mutator "^0.6.0"
"@grpc/grpc-js@^1.7.1":
version "1.9.9"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.9.tgz#ce3a05439b1c957ec64c2ecdc6f1e4f54e8af797"
......@@ -9314,6 +9328,11 @@ dom-helpers@^5.0.1:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dom-mutator@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/dom-mutator/-/dom-mutator-0.6.0.tgz#079d7a4b3e8981a562cd777548b99baab51d65c5"
integrity sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg==
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
......
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