Feat: Implement Amplitude Experiment infra (#639)

* Added Amplitude Experiments Initialization to initCCA

* create useVariant hook to pull experimental variants

* refactored useVariant to create simpler interface

* linted

* automated defaultDeploymentKey logic

* added defaultDeploymentKey for prod env

* Created ExperimentsContext for web app

* added more specificity to amplitude domains for CSP

* additional properties on eventData

* added amplitude deployment keys to constants

* deleted unused useVariant hook

* refactored experiment initialization in initCCA

* refactored Experiments context

* refactored usage of Experiments provider

* refactored ampDeploymentKey logic

* removed experiment initialization from initCCA

* restored index page to prior state

* refactored initCCA to pull constants into dedicated file

* created Experiments context in base-docs

* implemented experiments context in base-docs Root

* moved Experiments context to libs

* updated Experiments context integration in base-web

* fixed import statement

* deleted unused context in favor of shared version in libs

* implemented shared experiments context

* moved Experiments context to be innermost context provider in base-web

* removed unused imports from initCCA

* refactored Amplitude Experiments package into libs

* fixed yarn issue

* added type declaration for

* refactored window type declaration
This commit is contained in:
Brendan from DeFi
2024-07-23 09:18:38 -07:00
committed by GitHub
parent ddcd2bc5db
commit b6df0ab1cb
13 changed files with 220 additions and 25 deletions

View File

@@ -0,0 +1,9 @@
import { customFields } from './utils/docusaurusCustomFields';
export const isDevelopment = customFields.nodeEnv === 'development';
export const amplitudeApiKey = isDevelopment
? 'ca92bbcb548f7ec4b8ebe9194b8eda81'
: '2b38c7ac93c0dccc83ebf9acc5107413';
export const ampDeploymentKey = isDevelopment
? 'client-Wvf63OdaukDZyCBtwgbOvHgGTuASBZFG'
: 'client-agFoQg5AOvZ2ZiOChny9RrGk21jG3VrH';

View File

@@ -28,6 +28,7 @@ import { createClient } from 'viem';
import useSprig from 'base-ui/hooks/useSprig';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import ExperimentsProvider from 'base-ui/contexts/Experiments';
coinbaseWallet.preference = 'all';
@@ -204,18 +205,20 @@ export default function Root({ children }) {
`,
}}
/>
<CookieManagerProvider
projectName="base_docs"
locale="en"
region={Region.DEFAULT}
log={console.log}
onError={handleLogError}
onPreferenceChange={setTrackingPreference}
config={cookieManagerConfig}
>
{children}
<CookieBanner companyName="Base" link="/cookie-policy" theme={cookieBannerTheme} />
</CookieManagerProvider>
<ExperimentsProvider>
<CookieManagerProvider
projectName="base_docs"
locale="en"
region={Region.DEFAULT}
log={console.log}
onError={handleLogError}
onPreferenceChange={setTrackingPreference}
config={cookieManagerConfig}
>
{children}
<CookieBanner companyName="Base" link="/cookie-policy" theme={cookieBannerTheme} />
</CookieManagerProvider>
</ExperimentsProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>

View File

@@ -3,11 +3,10 @@
/* eslint-disable */
// @ts-nocheck
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import { customFields } from './docusaurusCustomFields';
import { setCookie, getCookie, deserializeCookie } from './cookieManagement';
import { TrackingPreference } from '@coinbase/cookie-manager';
import { isDevelopment, amplitudeApiKey } from '../constants';
const isDevelopment = customFields.nodeEnv === 'development';
// Initialize Client Analytics
const initCCA = () => {
@@ -31,9 +30,7 @@ const initCCA = () => {
init({
isProd: !isDevelopment,
amplitudeApiKey: isDevelopment
? 'ca92bbcb548f7ec4b8ebe9194b8eda81'
: '2b38c7ac93c0dccc83ebf9acc5107413',
amplitudeApiKey: amplitudeApiKey,
platform: PlatformName.web,
projectName: 'base_docs',
showDebugLogging: isDevelopment,

View File

@@ -89,6 +89,8 @@ const contentSecurityPolicy = {
'https://api.opensea.io', // enables getting ENS avatars
isLocalDevelopment ? 'ws://localhost:3000/' : '',
isLocalDevelopment ? 'http://localhost:3000/' : '',
'https://flag.lab.amplitude.com/sdk/v2/flags',
'https://api.lab.amplitude.com/sdk/v2/vardata',
],
'frame-ancestors': ["'self'", baseXYZDomains],
'form-action': ["'self'", baseXYZDomains],

View File

@@ -25,6 +25,7 @@ import { base, baseSepolia, mainnet, sepolia } from 'wagmi/chains';
import ClientAnalyticsScript from '../src/components/ClientAnalyticsScript/ClientAnalyticsScript';
import { Layout } from '../src/components/Layout/Layout';
import { cookieManagerConfig } from '../src/utils/cookieManagerConfig';
import ExperimentsProvider from 'base-ui/contexts/Experiments';
coinbaseWallet.preference = 'all';
@@ -115,9 +116,11 @@ export default function StaticApp({ Component, pageProps }: AppProps) {
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider modalSize="compact">
<Layout>
<Component {...pageProps} />
</Layout>
<ExperimentsProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</ExperimentsProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>

View File

@@ -7,3 +7,6 @@ export const mainnetLaunchBlogPostURL =
process.env.MAINNET_LAUNCH_BLOG_POST_URL ?? 'https://base.mirror.xyz/';
export const mainnetLaunchFlag = process.env.MAINNET_LAUNCH_FLAG ?? 'false';
export const isDevelopment = nodeEnv === 'development';
export const ampDeploymentKey = isDevelopment
? 'client-Wvf63OdaukDZyCBtwgbOvHgGTuASBZFG'
: 'client-agFoQg5AOvZ2ZiOChny9RrGk21jG3VrH';

View File

@@ -16,6 +16,9 @@ const initCCA = (
) => {
let deviceId: string | undefined = deviceIdCookie;
const trackingAllowed: boolean = trackingPreference?.consent.includes('performance');
const amplitudeApiKey: string = isDevelopment
? 'ca92bbcb548f7ec4b8ebe9194b8eda81'
: '2b38c7ac93c0dccc83ebf9acc5107413';
if (!trackingAllowed) {
deviceId = 'base_web_device_id';
@@ -29,9 +32,7 @@ const initCCA = (
init({
isProd: !isDevelopment,
amplitudeApiKey: isDevelopment
? 'ca92bbcb548f7ec4b8ebe9194b8eda81'
: '2b38c7ac93c0dccc83ebf9acc5107413',
amplitudeApiKey,
platform: PlatformName.web,
projectName: 'base_web',
showDebugLogging: isDevelopment,

View File

@@ -16,7 +16,7 @@
"next-env.d.ts",
"**/*",
"**/*.json",
"../../types/**/*"
"../../types/**/*",
],
"exclude": [],
"references": [

12
libs/base-ui/constants.ts Normal file
View File

@@ -0,0 +1,12 @@
export const nodeEnv = process.env.NODE_ENV;
export const docsUrl = process.env.DOCS_URL ?? 'https://docs.base.org';
export const bridgeUrl = process.env.BRIDGE_URL ?? 'https://bridge.base.org';
export const greenhouseApiUrl =
process.env.GREENHOUSE_HTTPS ?? 'https://boards-api.greenhouse.io/v1';
export const mainnetLaunchBlogPostURL =
process.env.MAINNET_LAUNCH_BLOG_POST_URL ?? 'https://base.mirror.xyz/';
export const mainnetLaunchFlag = process.env.MAINNET_LAUNCH_FLAG ?? 'false';
export const isDevelopment = nodeEnv === 'development';
export const ampDeploymentKey = isDevelopment
? 'client-Wvf63OdaukDZyCBtwgbOvHgGTuASBZFG'
: 'client-agFoQg5AOvZ2ZiOChny9RrGk21jG3VrH';

View File

@@ -0,0 +1,110 @@
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useMemo,
useCallback,
} from 'react';
import { Experiment, ExperimentClient } from '@amplitude/experiment-js-client';
import { ampDeploymentKey } from '../constants';
import logEvent, { AnalyticsEventImportance } from '../utils/logEvent';
declare const window: WindowWithAnalytics;
const ExperimentsContext = createContext<ExperimentsContextProps>({
experimentClient: null,
isReady: false,
getUserVariant: () => '',
});
const experimentClient = Experiment.initialize(ampDeploymentKey, {
exposureTrackingProvider: {
track: (exposure) => {
logEvent('$exposure', exposure, AnalyticsEventImportance.high);
},
},
userProvider: {
getUser: () => {
return {
user_id: window.ClientAnalytics.identity.userId,
device_id: window.ClientAnalytics.identity.deviceId,
os: window.ClientAnalytics.identity.device_os,
language: window.ClientAnalytics.identity.languageCode,
country: window.ClientAnalytics.identity.countryCode,
};
},
},
});
export default function ExperimentsProvider({ children }: ExperimentsProviderProps) {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
async function startExperiments() {
try {
await experimentClient.start();
setIsReady(true);
} catch (exception) {
console.log(`Error starting experiments for ${ampDeploymentKey}:`, exception);
}
}
void startExperiments();
}, []);
const getUserVariant = useCallback(
(flagKey: string): string => {
if (!isReady) {
return '';
}
if (!experimentClient) {
console.error('No experiment clients found');
return '';
}
const variant = experimentClient.variant(flagKey);
return variant.value ?? '';
},
[isReady],
);
const values = useMemo(() => {
return { experimentClient, isReady, getUserVariant };
}, [isReady, getUserVariant]);
return <ExperimentsContext.Provider value={values}>{children}</ExperimentsContext.Provider>;
}
const useExperiments = () => {
const context = useContext(ExperimentsContext);
if (context === undefined) {
throw new Error('useExperiments must be used within an ExperimentsProvider');
}
return context;
};
export { useExperiments };
type WindowWithAnalytics = Window &
typeof globalThis & {
ClientAnalytics: {
identity: {
userId: string;
deviceId: string;
device_os: string;
languageCode: string;
countryCode: string;
};
};
};
type ExperimentsContextProps = {
experimentClient: ExperimentClient | null;
isReady: boolean;
getUserVariant: (flagKey: string) => string;
};
type ExperimentsProviderProps = {
children: ReactNode;
};

View File

@@ -6,6 +6,7 @@
"types": "./index.ts",
"main": "./index.ts",
"dependencies": {
"@amplitude/experiment-js-client": "^1.11.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-select": "^1.2.1",
"@sprig-technologies/sprig-browser": "^2.29.0",

View File

@@ -65,6 +65,9 @@ type CCAEventData = {
context?: string;
userId?: string;
wallet_type?: string;
flag_key?: string;
variant?: string | undefined;
experiment_key?: string | undefined;
};
type AnalyticsEventData = {

View File

@@ -215,6 +215,42 @@ __metadata:
languageName: node
linkType: hard
"@amplitude/analytics-connector@npm:^1.5.0":
version: 1.5.0
resolution: "@amplitude/analytics-connector@npm:1.5.0"
checksum: 157115b642e8b254a88184fe3294150321fdd482388f262bdc08f9ab0c68b01b5f10d2e35a924edb8a8380f0944f8b1363133c2a3f42b59e33da319bfe21c8fa
languageName: node
linkType: hard
"@amplitude/experiment-core@npm:^0.8.0":
version: 0.8.0
resolution: "@amplitude/experiment-core@npm:0.8.0"
dependencies:
js-base64: ^3.7.5
checksum: 2e2c28f187b4f8072fe30589af95378c220917e949ae8a4c444dd8a6fdbe8bd9c8d0d105808b37e5e5fd59f16cbfa486c66f60847c211a0e424c6e7deb1693aa
languageName: node
linkType: hard
"@amplitude/experiment-js-client@npm:^1.11.0":
version: 1.11.0
resolution: "@amplitude/experiment-js-client@npm:1.11.0"
dependencies:
"@amplitude/analytics-connector": ^1.5.0
"@amplitude/experiment-core": ^0.8.0
"@amplitude/ua-parser-js": ^0.7.31
base64-js: 1.5.1
unfetch: 4.1.0
checksum: bc7ee5a521248ac83903b079fa61c7a884a3e27bd3573e4cedbb9f455ebe7af1e6fdc16a3e62988bf807ab9d74319de729fde78c42d4b0825f7c1a52d7753ff1
languageName: node
linkType: hard
"@amplitude/ua-parser-js@npm:^0.7.31":
version: 0.7.33
resolution: "@amplitude/ua-parser-js@npm:0.7.33"
checksum: b08ce4cd4e96fed9eebffadb4060d24751cea80caf4265e6b829b7cfdb6561431386015674b3ac330a8987b990e2ca70725d4298eb1438beccb85264848b0b34
languageName: node
linkType: hard
"@ampproject/remapping@npm:^2.2.0":
version: 2.2.1
resolution: "@ampproject/remapping@npm:2.2.1"
@@ -9563,6 +9599,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "base-ui@workspace:libs/base-ui"
dependencies:
"@amplitude/experiment-js-client": ^1.11.0
"@radix-ui/react-icons": ^1.3.0
"@radix-ui/react-select": ^1.2.1
"@sprig-technologies/sprig-browser": ^2.29.0
@@ -9582,7 +9619,7 @@ __metadata:
languageName: node
linkType: hard
"base64-js@npm:^1.3.1":
"base64-js@npm:1.5.1, base64-js@npm:^1.3.1":
version: 1.5.1
resolution: "base64-js@npm:1.5.1"
checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005
@@ -16235,6 +16272,13 @@ __metadata:
languageName: node
linkType: hard
"js-base64@npm:^3.7.5":
version: 3.7.7
resolution: "js-base64@npm:3.7.7"
checksum: d1b02971db9dc0fd35baecfaf6ba499731fb44fe3373e7e1d6681fbd3ba665f29e8d9d17910254ef8104e2cb8b44117fe4202d3dc54c7cafe9ba300fe5433358
languageName: node
linkType: hard
"js-cookie@npm:^3.0.5":
version: 3.0.5
resolution: "js-cookie@npm:3.0.5"
@@ -23082,6 +23126,13 @@ __metadata:
languageName: node
linkType: hard
"unfetch@npm:4.1.0":
version: 4.1.0
resolution: "unfetch@npm:4.1.0"
checksum: 8a0fee1e0f6ad8b3a2966fa199d07716affc3682d8e1c2c0cc138e5e5d2a2e0627d8c3321a4529a79e8a58332955bf80ac6c018f1dcc6de652026a7c3257d726
languageName: node
linkType: hard
"unfetch@npm:^4.2.0":
version: 4.2.0
resolution: "unfetch@npm:4.2.0"