diff --git a/components/Discovery.tsx b/components/Discovery.tsx index ab0b4e9f..591f3486 100644 --- a/components/Discovery.tsx +++ b/components/Discovery.tsx @@ -19,8 +19,8 @@ import { Service } from '../types' import { useConfig, useRpc } from '../contexts/FclContext' import { handleCancel } from '../helpers/window' import { useDevice } from '../contexts/DeviceContext' -import { DeviceType } from '../helpers/device' import { getCompatibleInstallLinks } from '../hooks/useInstallLinks' +import { useTelemetry } from '../hooks/useTelemetry' export enum VIEWS { WALLET_SELECTION, @@ -47,6 +47,7 @@ export default function Discovery() { const { deviceInfo } = useDevice() const { rpcEnabled } = useRpc() const { supportedStrategies } = useConfig() + const telemetry = useTelemetry() // Skip the connect page if there is only one service available and no install links const shouldSkipConnectPage = (wallet: Wallet) => @@ -78,6 +79,10 @@ export default function Discovery() { setCurrentView(VIEWS.CONNECT_EXTENSION) } else { fcl.WalletUtils.redirect(service) + telemetry.trackWalletConnected( + wallet.uid, + service.method as FCL_SERVICE_METHODS, + ) } setSelectedWallet(wallet) diff --git a/components/views/ConnectExtension.tsx b/components/views/ConnectExtension.tsx index d763f0c2..7bed932f 100644 --- a/components/views/ConnectExtension.tsx +++ b/components/views/ConnectExtension.tsx @@ -1,6 +1,6 @@ import { Button, Heading, Spinner, Stack, Text } from '@chakra-ui/react' import { Wallet } from '../../data/wallets' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { FCL_SERVICE_METHODS } from '../../helpers/constants' import { useRpc } from '../../contexts/FclContext' import { FclRequest } from '../../helpers/rpc' @@ -8,6 +8,7 @@ import WalletIcon from '../icons/WalletIcon' import { useWalletHistory } from '../../hooks/useWalletHistory' import { handleCancel } from '../../helpers/window' import { ViewContainer } from '../layout/ViewContainer' +import { useTelemetry } from '../../hooks/useTelemetry' type ConnectExtensionProps = { wallet: Wallet @@ -19,14 +20,16 @@ export default function ConnectExtension({ wallet }: ConnectExtensionProps) { const hasAttemptedConnection = useRef(true) const showSpinner = !rpc || isConnecting const { setLastUsed } = useWalletHistory() + const telemetry = useTelemetry() - function connect() { + const connect = () => { setIsConnecting(true) wallet.services.forEach(service => { if (service.method === FCL_SERVICE_METHODS.EXT) { rpc .request(FclRequest.EXEC_SERVICE, { service }) .then(() => { + telemetry.trackWalletConnected(wallet.uid, FCL_SERVICE_METHODS.EXT) setLastUsed(wallet) handleCancel() }) diff --git a/components/views/ScanConnect/ScanConnectDesktop.tsx b/components/views/ScanConnect/ScanConnectDesktop.tsx index c4cd31e9..2f7c40cc 100644 --- a/components/views/ScanConnect/ScanConnectDesktop.tsx +++ b/components/views/ScanConnect/ScanConnectDesktop.tsx @@ -7,6 +7,8 @@ import { useWcUri } from '../../../hooks/useWcUri' import { useWalletHistory } from '../../../hooks/useWalletHistory' import { handleCancel } from '../../../helpers/window' import { ViewContainer } from '../../layout/ViewContainer' +import { FCL_SERVICE_METHODS } from '../../../helpers/constants' +import { useTelemetry } from '../../../hooks/useTelemetry' interface ScanConnectDesktopProps { wallet: Wallet @@ -18,7 +20,9 @@ export default function ScanConnectDesktop({ onGetWallet, }: ScanConnectDesktopProps) { const { setLastUsed } = useWalletHistory() + const telemetry = useTelemetry() const { uri, connecting, error, isLoading } = useWcUri(() => { + telemetry.trackWalletConnected(wallet.uid, FCL_SERVICE_METHODS.WC) setLastUsed(wallet) handleCancel() }) diff --git a/components/views/ScanConnect/ScanConnectMobile.tsx b/components/views/ScanConnect/ScanConnectMobile.tsx index e49f09b4..33b2372c 100644 --- a/components/views/ScanConnect/ScanConnectMobile.tsx +++ b/components/views/ScanConnect/ScanConnectMobile.tsx @@ -8,6 +8,7 @@ import { useEffect, useRef } from 'react' import { FCL_SERVICE_METHODS } from '../../../helpers/constants' import WalletIcon from '../../icons/WalletIcon' import { ViewContainer } from '../../layout/ViewContainer' +import { useTelemetry } from '../../../hooks/useTelemetry' interface ScanConnectMobileProps { wallet: Wallet @@ -21,7 +22,9 @@ export default function ScanConnectMobile({ noDeepLink, }: ScanConnectMobileProps) { const { setLastUsed } = useWalletHistory() + const telemetry = useTelemetry() const { uri, connecting, error, isLoading } = useWcUri(() => { + telemetry.trackWalletConnected(wallet.uid, FCL_SERVICE_METHODS.WC) setLastUsed(wallet) handleCancel() }) diff --git a/config/mixpanel.server.js b/config/mixpanel.server.js deleted file mode 100644 index 300939bc..00000000 --- a/config/mixpanel.server.js +++ /dev/null @@ -1,14 +0,0 @@ -// This lib is only for server side -import Mixpanel from 'mixpanel' - -let mixpanel - -const initMixpanel = () => { - if (process.env.MIXPANEL_ID) { - mixpanel = Mixpanel.init(process.env.MIXPANEL_ID) - } -} - -initMixpanel() - -export default mixpanel diff --git a/helpers/telemetry/telemetry.client.ts b/helpers/telemetry/telemetry.client.ts new file mode 100644 index 00000000..9cc9d0e7 --- /dev/null +++ b/helpers/telemetry/telemetry.client.ts @@ -0,0 +1,19 @@ +import Mixpanel from 'mixpanel-browser' +import { trackWalletConnected, trackWalletDiscoveryRequest } from './telemetry' +import { TelemetryDataClient } from './types' + +let mixpanel: any = null + +export function getTelemetryClient(baseData: TelemetryDataClient) { + if (process.env.NEXT_PUBLIC_MIXPANEL_ID && !mixpanel) { + mixpanel = Mixpanel.init(process.env.NEXT_PUBLIC_MIXPANEL_ID) + } + + return { + trackWalletDiscoveryRequest: trackWalletDiscoveryRequest( + mixpanel, + baseData, + ), + trackWalletConnected: trackWalletConnected(mixpanel, baseData), + } +} diff --git a/helpers/telemetry/telemetry.server.ts b/helpers/telemetry/telemetry.server.ts new file mode 100644 index 00000000..12c55e60 --- /dev/null +++ b/helpers/telemetry/telemetry.server.ts @@ -0,0 +1,19 @@ +import Mixpanel from 'mixpanel' +import { TelemetryDataServer } from './types' +import { trackWalletConnected, trackWalletDiscoveryRequest } from './telemetry' + +let mixpanel: Mixpanel.Mixpanel | null = null + +export function getTelemetryServer(baseData: TelemetryDataServer) { + if (process.env.NEXT_PUBLIC_MIXPANEL_ID && !mixpanel) { + mixpanel = Mixpanel.init(process.env.NEXT_PUBLIC_MIXPANEL_ID) + } + + return { + trackWalletDiscoveryRequest: trackWalletDiscoveryRequest( + mixpanel, + baseData, + ), + trackWalletConnected: trackWalletConnected(mixpanel, baseData), + } +} diff --git a/helpers/telemetry/telemetry.ts b/helpers/telemetry/telemetry.ts new file mode 100644 index 00000000..6bab651d --- /dev/null +++ b/helpers/telemetry/telemetry.ts @@ -0,0 +1,24 @@ +import Mixpanel from 'mixpanel' +import { FCL_SERVICE_METHODS } from '../constants' + +export function trackWalletDiscoveryRequest( + mixpanel: Mixpanel.Mixpanel, + baseData: any, +) { + return () => { + mixpanel?.track('Wallet Discovery Request', baseData) + } +} + +export function trackWalletConnected( + mixpanel: Mixpanel.Mixpanel, + baseData: any, +) { + return (walletUid: string, serviceMethod: FCL_SERVICE_METHODS) => { + mixpanel?.track('Wallet Connected', { + walletUid: walletUid, + method: serviceMethod, + ...baseData, + }) + } +} diff --git a/helpers/telemetry/types.ts b/helpers/telemetry/types.ts new file mode 100644 index 00000000..1b3e2c8c --- /dev/null +++ b/helpers/telemetry/types.ts @@ -0,0 +1,13 @@ +export type TelemetryData = { + fclVersion: string + type: 'UI' | 'API' + network: string +} + +export type TelemetryDataServer = TelemetryData & { + origin?: string +} + +export type TelemetryDataClient = TelemetryData & { + parent: string +} diff --git a/hooks/useTelemetry.ts b/hooks/useTelemetry.ts new file mode 100644 index 00000000..689d3bb3 --- /dev/null +++ b/hooks/useTelemetry.ts @@ -0,0 +1,13 @@ +import { useConfig } from '../contexts/FclContext' +import { getTelemetryClient } from '../helpers/telemetry/telemetry.client' + +export function useTelemetry() { + const cfg = useConfig() + return getTelemetryClient({ + network: cfg.network, + type: 'UI', + fclVersion: cfg.appVersion, + parent: + window.location != window.parent.location ? document.referrer : undefined, + }) +} diff --git a/hooks/useWallets.ts b/hooks/useWallets.ts index 8582fd43..9f83ed1e 100644 --- a/hooks/useWallets.ts +++ b/hooks/useWallets.ts @@ -33,7 +33,12 @@ export function useWallets() { port, } = useConfig() - const requestUrl = `/api/${network.toLowerCase()}/wallets?discoveryType=UI&enableExperimentalWalletsEndpoint=true` + const params = new URLSearchParams() + params.append('discoveryType', 'UI') + params.append('enableExperimentalWalletsEndpoint', 'true') + params.append('origin', window.location.origin) + + const requestUrl = `/api/${network.toLowerCase()}/wallets?${params.toString()}` const body = { type: ['authn'], fclVersion: appVersion, diff --git a/package-lock.json b/package-lock.json index 60403bdd..c85b61bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "eslint-config-next": "^14.2.4", "framer-motion": "^8.4.6", "mixpanel": "^0.14.0", + "mixpanel-browser": "^2.56.0", "next": "^14.2.4", "qrcode": "^1.5.4", "rambda": "^9.2.1", @@ -36,6 +37,7 @@ "@testing-library/react": "^16.0.0", "@types/cors": "^2.8.17", "@types/jest": "^29.5.12", + "@types/mixpanel-browser": "^2.50.2", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.3", "babel-plugin-styled-components": "^2.0.7", @@ -3899,6 +3901,11 @@ } } }, + "node_modules/@rrweb/types": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.18.tgz", + "integrity": "sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==" + }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", @@ -4579,6 +4586,11 @@ "@types/node": "*" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" + }, "node_modules/@types/eslint": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", @@ -4861,6 +4873,12 @@ "@types/lodash": "*" } }, + "node_modules/@types/mixpanel-browser": { + "version": "2.50.2", + "resolved": "https://registry.npmjs.org/@types/mixpanel-browser/-/mixpanel-browser-2.50.2.tgz", + "integrity": "sha512-Iw8cBzplUPfHoeYuasqeYwdbGTNXhN+5kFT9kU+C7zm0NtaiPpKoiuzITr2ZH9KgBsWi2MbG0FOzIg9sQepauQ==", + "dev": true + }, "node_modules/@types/mysql": { "version": "2.15.22", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.22.tgz", @@ -5247,6 +5265,11 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xstate/fsm": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5795,6 +5818,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7705,6 +7736,11 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -10320,6 +10356,11 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, "node_modules/mixpanel": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.14.0.tgz", @@ -10331,6 +10372,14 @@ "node": ">=10.0" } }, + "node_modules/mixpanel-browser": { + "version": "2.56.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.56.0.tgz", + "integrity": "sha512-GYeEz58pV2M9MZtK8vSPL4oJmCwGS08FDDRZvZwr5VJpWdT4Lgyg6zXhmNfCmSTEIw2coaarm7HZ4FL9dAVvnA==", + "dependencies": { + "rrweb": "2.0.0-alpha.13" + } + }, "node_modules/mixpanel/node_modules/https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -10870,9 +10919,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -11693,6 +11742,64 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrdom": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.0.0-alpha.18.tgz", + "integrity": "sha512-fSFzFFxbqAViITyYVA4Z0o5G6p1nEqEr/N8vdgSKie9Rn0FJxDSNJgjV0yiCIzcDs0QR+hpvgFhpbdZ6JIr5Nw==", + "dependencies": { + "rrweb-snapshot": "^2.0.0-alpha.18" + } + }, + "node_modules/rrweb": { + "version": "2.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.0.0-alpha.13.tgz", + "integrity": "sha512-a8GXOCnzWHNaVZPa7hsrLZtNZ3CGjiL+YrkpLo0TfmxGLhjNZbWY2r7pE06p+FcjFNlgUVTmFrSJbK3kO7yxvw==", + "dependencies": { + "@rrweb/types": "^2.0.0-alpha.13", + "@types/css-font-loading-module": "0.0.7", + "@xstate/fsm": "^1.4.0", + "base64-arraybuffer": "^1.0.1", + "fflate": "^0.4.4", + "mitt": "^3.0.0", + "rrdom": "^2.0.0-alpha.13", + "rrweb-snapshot": "^2.0.0-alpha.13" + } + }, + "node_modules/rrweb-snapshot": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.tgz", + "integrity": "sha512-hBHZL/NfgQX6wO1D9mpwqFu1NJPpim+moIcKhFEjVTZVRUfCln+LOugRc4teVTCISYHN8Cw5e2iNTWCSm+SkoA==", + "dependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/rrweb-snapshot/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11956,9 +12063,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index 8df96a04..ee98d6d3 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "eslint-config-next": "^14.2.4", "framer-motion": "^8.4.6", "mixpanel": "^0.14.0", + "mixpanel-browser": "^2.56.0", "next": "^14.2.4", "qrcode": "^1.5.4", "rambda": "^9.2.1", @@ -47,6 +48,7 @@ "@testing-library/react": "^16.0.0", "@types/cors": "^2.8.17", "@types/jest": "^29.5.12", + "@types/mixpanel-browser": "^2.50.2", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.3", "babel-plugin-styled-components": "^2.0.7", diff --git a/pages/api/[network]/_common.ts b/pages/api/[network]/_common.ts index 27855407..eb20c590 100644 --- a/pages/api/[network]/_common.ts +++ b/pages/api/[network]/_common.ts @@ -5,7 +5,7 @@ import { findMatchingPipeVersion } from '../../../helpers/version' import { NETWORKS } from '../../../helpers/constants' import { getWalletPipes } from '../../../helpers/wallet-pipes' import { NextApiRequest } from 'next' -import mixpanel from '../../../config/mixpanel.server' +import { getTelemetryServer } from '../../../helpers/telemetry/telemetry.server' // Initializing the cors middleware export const cors = Cors({ @@ -21,10 +21,12 @@ export async function getWalletsFromRequest( network, discoveryType, port: portQuery, + origin, } = req.query as { network: string discoveryType: string port: string + origin: string } const { fclVersion, @@ -39,11 +41,12 @@ export async function getWalletsFromRequest( const discoveryRequestType = discoveryType || 'API' const services = clientServices || extensions || [] - mixpanel?.track('Wallet Discovery Request', { - type: discoveryRequestType, + getTelemetryServer({ + type: discoveryRequestType as 'UI' | 'API', network, fclVersion, - }) + origin, + }).trackWalletDiscoveryRequest() if (!isValidNetwork) { throw new Error('Invalid network')