diff --git a/apps/base-docs/src/constants.ts b/apps/base-docs/src/constants.ts new file mode 100644 index 0000000..996aaaa --- /dev/null +++ b/apps/base-docs/src/constants.ts @@ -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'; diff --git a/apps/base-docs/src/theme/Root.jsx b/apps/base-docs/src/theme/Root.jsx index c7363bf..e50089f 100644 --- a/apps/base-docs/src/theme/Root.jsx +++ b/apps/base-docs/src/theme/Root.jsx @@ -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 }) { `, }} /> - - {children} - - + + + {children} + + + diff --git a/apps/base-docs/src/utils/initCCA.ts b/apps/base-docs/src/utils/initCCA.ts index e597fb8..2673db9 100644 --- a/apps/base-docs/src/utils/initCCA.ts +++ b/apps/base-docs/src/utils/initCCA.ts @@ -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, diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 75cfe7b..bed6b11 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -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], diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 66a901d..d808bc9 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -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) { - - - + + + + + diff --git a/apps/web/src/constants.ts b/apps/web/src/constants.ts index 14abff8..2e49a12 100644 --- a/apps/web/src/constants.ts +++ b/apps/web/src/constants.ts @@ -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'; diff --git a/apps/web/src/utils/initCCA.ts b/apps/web/src/utils/initCCA.ts index 4785018..4f45f6f 100644 --- a/apps/web/src/utils/initCCA.ts +++ b/apps/web/src/utils/initCCA.ts @@ -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, diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index fa99c62..061a257 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -16,7 +16,7 @@ "next-env.d.ts", "**/*", "**/*.json", - "../../types/**/*" + "../../types/**/*", ], "exclude": [], "references": [ diff --git a/libs/base-ui/constants.ts b/libs/base-ui/constants.ts new file mode 100644 index 0000000..2e49a12 --- /dev/null +++ b/libs/base-ui/constants.ts @@ -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'; diff --git a/libs/base-ui/contexts/Experiments.tsx b/libs/base-ui/contexts/Experiments.tsx new file mode 100644 index 0000000..ba51b4a --- /dev/null +++ b/libs/base-ui/contexts/Experiments.tsx @@ -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({ + 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 {children}; +} + +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; +}; diff --git a/libs/base-ui/package.json b/libs/base-ui/package.json index 4d3fcc4..0490a69 100644 --- a/libs/base-ui/package.json +++ b/libs/base-ui/package.json @@ -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", diff --git a/libs/base-ui/utils/logEvent.ts b/libs/base-ui/utils/logEvent.ts index f827eb7..d147635 100644 --- a/libs/base-ui/utils/logEvent.ts +++ b/libs/base-ui/utils/logEvent.ts @@ -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 = { diff --git a/yarn.lock b/yarn.lock index ac36611..eb818be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"