From 9dbbada90a5a36eba7c4d744a0d63620e2144131 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:36:35 +1100 Subject: [PATCH 1/3] [FEATURE] Call mixpanel from background Closes #288 --- _raw/manifest/manifest.dev.json | 1 + _raw/manifest/manifest.pro.json | 1 + src/background/controller/wallet.ts | 20 +- src/background/service/mixpanel.ts | 300 ++++++++++++++++-- src/background/service/openapi.ts | 9 +- src/background/service/user.ts | 9 +- src/shared/types/tracking-types.ts | 12 + src/ui/utils/index.ts | 2 - src/ui/utils/mixpanelBrowserService.ts | 147 --------- src/ui/utils/useMixpanel.ts | 32 -- .../AddWelcome/AddRegister/SetPassword.tsx | 7 +- src/ui/views/RecoverRegister/SetPassword.tsx | 4 +- src/ui/views/Register/AllSet.tsx | 16 +- src/ui/views/Register/SetPassword.tsx | 5 +- src/ui/views/Wallet/OnRampList.tsx | 7 +- src/ui/views/index.tsx | 25 +- 16 files changed, 353 insertions(+), 244 deletions(-) delete mode 100644 src/ui/utils/mixpanelBrowserService.ts delete mode 100644 src/ui/utils/useMixpanel.ts diff --git a/_raw/manifest/manifest.dev.json b/_raw/manifest/manifest.dev.json index 7351d005..6d61dbaf 100644 --- a/_raw/manifest/manifest.dev.json +++ b/_raw/manifest/manifest.dev.json @@ -40,6 +40,7 @@ "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self' 'wasm-unsafe-eval';" }, "permissions": ["storage", "activeTab", "tabs", "notifications", "identity", "camera"], + "host_permissions": ["https://api.mixpanel.com/*"], "web_accessible_resources": [ { "resources": [ diff --git a/_raw/manifest/manifest.pro.json b/_raw/manifest/manifest.pro.json index 548c3006..461ae561 100644 --- a/_raw/manifest/manifest.pro.json +++ b/_raw/manifest/manifest.pro.json @@ -40,6 +40,7 @@ "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self' 'wasm-unsafe-eval';" }, "permissions": ["storage", "activeTab", "tabs", "notifications", "identity", "camera", "*://*/*"], + "host_permissions": ["https://api.mixpanel.com/*"], "web_accessible_resources": [ { "resources": [ diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index 1eec4920..4937c449 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -11,9 +11,10 @@ import { getAuth } from 'firebase/auth'; import web3, { TransactionError } from 'web3'; import eventBus from '@/eventBus'; +import { type TrackingEvents } from '@/shared/types/tracking-types'; import { isValidEthereumAddress, withPrefix } from '@/shared/utils/address'; import { getHashAlgo, getSignAlgo } from '@/shared/utils/algo'; -// eslint-disable-next-line import/order,no-restricted-imports +// eslint-disable-next-line no-restricted-imports import { findAddressWithNetwork } from '@/ui/utils/modules/findAddressWithPK'; import { keyringService, @@ -4099,6 +4100,23 @@ export class WalletController extends BaseController { source: source, }); }; + + // This is called from the front end, we should find a better way to track this event + trackAccountRecovered = async () => { + mixpanelTrack.track('account_recovered', { + address: (await this.getCurrentAddress()) || '', + mechanism: 'multi-backup', + methods: [], + }); + }; + + trackPageView = async (pathname: string) => { + mixpanelTrack.trackPageView(pathname); + }; + + trackTime = async (eventName: keyof TrackingEvents) => { + mixpanelTrack.time(eventName); + }; } export default new WalletController(); diff --git a/src/background/service/mixpanel.ts b/src/background/service/mixpanel.ts index ed0e2c6e..2d9c7dd7 100644 --- a/src/background/service/mixpanel.ts +++ b/src/background/service/mixpanel.ts @@ -1,15 +1,101 @@ -import eventBus from '@/eventBus'; import type { TrackingEvents } from '@/shared/types/tracking-types'; -// TODO: Look at using a server side proxy service to send events to Mixpanel -// Note: Mixpanel is initialized in the browser side. Yes... it is possible for events to be lost if there is no listener. -// At some point, we should migrate to a more reliable event bus. +import packageJson from '../../../package.json'; +const { version } = packageJson; + +const DISTINCT_ID_KEY = 't_distinct_id'; +const DEVICE_ID_PREFIX = '$device:'; + +interface IDInfo { + $user_id?: string; + $device_id: string; +} + +interface MixpanelEvent { + event: string; + properties: { + $duration?: number; + token?: string; + distinct_id: string; + time: number; + $app_version_string: string; + $browser: string; + $browser_version: string; + $os: string; + } & T; +} + +interface MixpanelIdentifyData { + $distinct_id: string; + $set_once: { + $name: string; + first_seen: string; + }; + $time: number; +} + +type MixpanelRequestData = + | MixpanelEvent + | MixpanelIdentifyData; + +// Super properties that will be sent with every event +type SuperProperties = { + app_version: string; + platform: 'extension'; + environment: 'development' | 'production'; + wallet_type: 'flow'; +}; + class MixpanelService { private static instance: MixpanelService; private initialized = false; + private eventTimers: Partial> = {}; + + private distinctId?: string; + private readonly API_URL = 'https://api.mixpanel.com'; + private readonly token: string; + private superProperties: SuperProperties = { + app_version: version, + platform: 'extension', + environment: process.env.NODE_ENV === 'production' ? 'production' : 'development', + wallet_type: 'flow', + }; + + async #getExtraProps() { + const extensionVersion = chrome.runtime.getManifest().version; + const browserInfo = getBrowserInfo(); + const extraProps = { + $app_version_string: extensionVersion, + $browser: browserInfo.browser, + $browser_version: browserInfo.version, + // $insert_id: "", // for deduplication! + time: timestamp() / 1000, // epoch time in seconds + $os: (await chrome.runtime.getPlatformInfo()).os, + }; + return extraProps; + } + + async getIdInfo(): Promise { + const res = await chrome.storage.local.get(DISTINCT_ID_KEY); + const idInfo = res?.[DISTINCT_ID_KEY] as IDInfo | undefined; + return idInfo; + } + + async setIdInfo(info: Partial) { + const _info = await this.getIdInfo(); + const newInfo = { + ...(_info ? _info : {}), + ...info, + }; + await chrome.storage.local.set({ [DISTINCT_ID_KEY]: newInfo }); + } private constructor() { // Private constructor for singleton + this.token = process.env.MIXPANEL_TOKEN!; + if (!this.token) { + console.error('MIXPANEL_TOKEN is not defined in environment variables'); + } } static getInstance(): MixpanelService { @@ -19,42 +105,208 @@ class MixpanelService { return MixpanelService.instance; } - init() { + async init() { if (this.initialized) return; + const ids = await this.getIdInfo(); + if (!ids?.$device_id) { + await this.setIdInfo({ $device_id: UUID() }); + } this.initialized = true; } - track(eventName: T, properties?: TrackingEvents[T]) { - chrome.runtime.sendMessage({ - msg: 'track_event', - eventName, - properties, - }); + + private async sendRequest(endpoint: string, data: MixpanelRequestData) { + await this.init(); + + const body = { + ...data, + }; + console.log('body', body); + try { + const response = await fetch(`${this.API_URL}${endpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + }, + body: JSON.stringify([body]), + }); + + if (!response.ok) { + throw new Error(`Mixpanel API error: ${response.statusText}`); + } + + console.log('response', response); + const responseText = await response.text(); + console.log('responseText', responseText); + + if (responseText !== '1') { + throw new Error(`Mixpanel API returned unexpected response: ${responseText}`); + } + } catch (error) { + console.error('error sending event to Mixpanel - raw', error); + if (error instanceof Error) { + console.error('Error sending event to Mixpanel:', error.message); + } + } } - time(eventName: T) { - chrome.runtime.sendMessage({ - msg: 'track_time', - eventName, - }); + private removeTimer(eventName: keyof TrackingEvents) { + const startTimeStamp = this.eventTimers[eventName]; + this.eventTimers[eventName] = undefined; + + return startTimeStamp; } - identify(userId: string) { - if (!this.initialized) return; + async time(eventName: T) { + await this.init(); - chrome.runtime.sendMessage({ - msg: 'track_user', - userId, + // Start the timer for the event + this.eventTimers[eventName] = Date.now(); + } + + async track(eventName: T, properties: TrackingEvents[T]) { + await this.init(); + + const ids = await this.getIdInfo(); + const deviceId = ids?.$device_id; + const userId = ids?.$user_id; + const distinct_id = userId || DEVICE_ID_PREFIX + deviceId; + + const baseProperties = { + token: this.token, + distinct_id, + ...(await this.#getExtraProps()), + ...this.superProperties, + }; + + const event: MixpanelEvent = { + event: eventName, + properties: { + ...baseProperties, + ...properties, + }, + }; + // Add duration if the timer was started + const startTimeStamp = this.removeTimer(eventName); + if (startTimeStamp !== undefined) { + event.properties.$duration = Date.now() - startTimeStamp; + } + + await this.sendRequest('/track', event); + } + async trackPageView(pathname: string) { + await this.init(); + + await this.track('$mp_web_page_view', { + current_page_title: 'Flow Wallet', + current_domain: 'flow-extension', + current_url_path: pathname, + current_url_protocol: 'chrome-extension', + }); + } + async identify(userId: string, name?: string) { + await this.init(); + // get previous id. + const ids = await this.getIdInfo(); + const deviceId = ids?.$device_id; + if (!deviceId) return; + if (deviceId === userId) return; + await this.track('$identify', { + distinct_id: userId, + $anon_distinct_id: deviceId, + $name: name, }); + await this.setIdInfo({ $user_id: userId }); } - reset() { + async reset() { if (!this.initialized) return; + this.distinctId = undefined; - chrome.runtime.sendMessage({ - msg: 'track_reset', + return chrome.storage.local.remove(DISTINCT_ID_KEY).then(() => { + return this.setIdInfo({ $device_id: UUID() }); }); } } export const mixpanelTrack = MixpanelService.getInstance(); + +// https://github.com/mixpanel/mixpanel-js/blob/3623fe0132860386eeed31756e0d7eb4e61997ed/src/utils.js#L862C5-L889C7 +function UUID() { + const T = function () { + const time = +new Date(); // cross-browser version of Date.now() + let ticks = 0; + + while (time === +new Date()) { + ticks++; + } + return time.toString(16) + Math.floor(ticks).toString(16); + }; + const R = function () { + return Math.random().toString(16).replace('.', ''); + }; + const UA = function () { + const ua = navigator.userAgent; + let i, + ch, + buffer: number[] = [], + ret = 0; + + function xor(result: number, byte_array: number[]) { + let j, + tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= buffer[j] << (j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xff); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + const se = (Math.floor(Math.random() * 1000) * Math.floor(Math.random() * 10000)).toString(16); + return T() + '-' + R() + '-' + UA() + '-' + se + '-' + T(); +} + +function timestamp() { + Date.now = + Date.now || + function () { + return +new Date(); + }; + return Date.now(); +} + +function getBrowserInfo() { + const userAgent = navigator.userAgent; + const vendor = navigator.vendor; + let browser, version; + + if (userAgent.includes('Firefox')) { + browser = 'Firefox'; + version = userAgent.match(/Firefox\/([0-9]+)/)?.[1]; + } else if (userAgent.includes(' OPR/')) { + browser = 'Opera'; + version = userAgent.match(/Safari\/([0-9]+)/)?.[1]; // not tested + } else if (vendor && vendor.includes('Apple')) { + browser = 'Safari'; + version = userAgent.match(/Safari\/([0-9]+)/)?.[1]; + } else if (userAgent.includes('Chrome')) { + browser = 'Chrome'; + version = userAgent.match(/Chrome\/([0-9]+)/)?.[1]; + } + return { browser, version }; +} diff --git a/src/background/service/openapi.ts b/src/background/service/openapi.ts index 54297c71..3f207be9 100644 --- a/src/background/service/openapi.ts +++ b/src/background/service/openapi.ts @@ -10,6 +10,7 @@ import { signInAnonymously, onAuthStateChanged, type Unsubscribe, + type User, } from 'firebase/auth'; import { getInstallations, getId } from 'firebase/installations'; import type { TokenInfo } from 'flow-native-token-registry'; @@ -89,7 +90,7 @@ const waitForAuthInit = async () => { (await unsubscribe!)(); }; -onAuthStateChanged(auth, (user) => { +onAuthStateChanged(auth, (user: User | null) => { if (user) { // User is signed in, see docs for a list of available properties // https://firebase.google.com/docs/reference/js/firebase.User @@ -97,6 +98,9 @@ onAuthStateChanged(auth, (user) => { console.log('User is signed in'); if (user.isAnonymous) { console.log('User is anonymous'); + } else { + mixpanelTrack.identify(user.uid, user.displayName ?? user.uid); + console.log('User is signed in'); } } else { // User is signed out @@ -661,6 +665,9 @@ class OpenApiService { }; register = async (account_key: AccountKey, username: string) => { + // Track the time until account_created is called + mixpanelTrack.time('account_created'); + const config = this.store.config.register; const data = await this.sendRequest( config.method, diff --git a/src/background/service/user.ts b/src/background/service/user.ts index 4739a452..302b7476 100644 --- a/src/background/service/user.ts +++ b/src/background/service/user.ts @@ -60,15 +60,18 @@ class UserInfo { this.store.avatar = data['avatar']; // identify the user - mixpanelTrack.identify(this.store.user_id); + if (this.store.user_id) { + mixpanelTrack.identify(this.store.user_id, this.store.username); + } // TODO: track the user info if not in private mode }; addUserId = (userId: string) => { this.store.user_id = userId; - // identify the user - mixpanelTrack.identify(this.store.user_id); + if (this.store.user_id) { + mixpanelTrack.identify(this.store.user_id); + } }; removeUserInfo = () => { diff --git a/src/shared/types/tracking-types.ts b/src/shared/types/tracking-types.ts index 4a8bbf2f..8f274f42 100644 --- a/src/shared/types/tracking-types.ts +++ b/src/shared/types/tracking-types.ts @@ -14,6 +14,18 @@ type RecoveryMechanismType = type AddressType = 'flow' | 'evm' | 'child' | 'coa'; export type TrackingEvents = { + // Mixpanel Events + $identify: { + distinct_id: string; // The distinct id of the user + $anon_distinct_id: string; // The anonymous distinct id of the user + $name?: string; // The name of the user + }; + $mp_web_page_view: { + current_page_title: string; // The title of the current page + current_domain: string; // The domain of the current page + current_url_path: string; // The path of the current page + current_url_protocol: string; + }; // General Events script_error: { error: string; // Error message of the script, e.g., Rate limit exceeded diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index 3c0f1846..be33285d 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -15,8 +15,6 @@ export * from './number'; export * from './saveStorage'; -export * from './mixpanelBrowserService'; - const UI_TYPE = { Tab: 'index', Pop: 'popup', diff --git a/src/ui/utils/mixpanelBrowserService.ts b/src/ui/utils/mixpanelBrowserService.ts deleted file mode 100644 index 0a362d23..00000000 --- a/src/ui/utils/mixpanelBrowserService.ts +++ /dev/null @@ -1,147 +0,0 @@ -import mixpanel from 'mixpanel-browser'; - -import type { TrackingEvents, TrackMessage } from '@/shared/types/tracking-types'; - -import packageJson from '../../../package.json'; -const { version } = packageJson; - -// Super properties that will be sent with every event -type SuperProperties = { - app_version: string; - platform: 'extension'; - environment: 'development' | 'production'; - wallet_type: 'flow'; -}; - -class MixpanelBrowserService { - private static instance: MixpanelBrowserService; - private initialized = false; - - private mixpanelEventMessageHandler: (message: TrackMessage) => void; - - private constructor() { - this.initMixpanel(); - - this.mixpanelEventMessageHandler = (message: TrackMessage) => { - switch (message.msg) { - case 'track_event': - // TypeScript knows eventName and properties are available here - this.track(message.eventName, message.properties); - break; - case 'track_user': - // TypeScript knows userId is available here - this.identify(message.userId); - break; - case 'track_reset': - // TypeScript knows this is just a reset message - this.reset(); - break; - case 'track_time': - // TypeScript knows eventName is available here - this.time(message.eventName); - break; - } - }; - - this.setupEventListener(); - } - - private setupEventListener() { - // Listen for messages from the background script - // This feels blunt as we have to switch on the message type - // TODO: We should use a more elegant approach to filter messages based on the sender - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - switch (message.msg) { - case 'track_event': - case 'track_user': - case 'track_reset': - case 'track_time': - this.mixpanelEventMessageHandler(message); - sendResponse({ success: true }); - break; - } - return true; // Keep the message channel open for asynchronous response - }); - } - - init() { - // Don't need to do anything here - // Mixpanel is initialized in the constructor - } - cleanup() { - // Remove the event listener - chrome.runtime.onMessage.removeListener(this.mixpanelEventMessageHandler); - } - - static getInstance(): MixpanelBrowserService { - if (!MixpanelBrowserService.instance) { - MixpanelBrowserService.instance = new MixpanelBrowserService(); - } - return MixpanelBrowserService.instance; - } - - private registerSuperProperties() { - const superProperties: SuperProperties = { - app_version: version, - platform: 'extension', - environment: process.env.NODE_ENV === 'production' ? 'production' : 'development', - wallet_type: 'flow', - }; - - mixpanel.register(superProperties); - } - - private initMixpanel() { - if (this.initialized) return; - - const token = process.env.MIXPANEL_TOKEN; - if (!token) { - console.warn('Mixpanel token not found'); - return; - } - - mixpanel.init(token, { - debug: process.env.NODE_ENV !== 'production', - track_pageview: 'full-url', // track the full url including the hash - persistence: 'localStorage', - batch_requests: true, - batch_size: 10, - batch_flush_interval_ms: 2000, - }); - - this.registerSuperProperties(); - this.initialized = true; - } - - track(eventName: T, properties?: TrackingEvents[T]) { - if (!this.initialized) { - console.warn('Mixpanel not initialized'); - return; - } - - const baseProps = { - timestamp: Date.now(), - }; - - mixpanel.track(eventName, { - ...baseProps, - ...properties, - }); - } - - time(eventName: T) { - mixpanel.time_event(eventName); - } - - identify(userId: string) { - if (!this.initialized) return; - mixpanel.identify(userId); - } - - reset() { - if (!this.initialized) return; - mixpanel.reset(); - } -} - -export const mixpanelBrowserService = MixpanelBrowserService.getInstance(); diff --git a/src/ui/utils/useMixpanel.ts b/src/ui/utils/useMixpanel.ts deleted file mode 100644 index 24809766..00000000 --- a/src/ui/utils/useMixpanel.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useCallback } from 'react'; - -import type { TrackingEvents } from '@/shared/types/tracking-types'; - -import { mixpanelBrowserService } from './mixpanelBrowserService'; - -export const useMixpanel = () => { - const track = useCallback( - (eventName: T, properties?: TrackingEvents[T]) => { - mixpanelBrowserService.track(eventName, properties); - }, - [] - ); - const time = useCallback((eventName: T) => { - mixpanelBrowserService.time(eventName); - }, []); - - const identify = useCallback((userId: string) => { - mixpanelBrowserService.identify(userId); - }, []); - - const reset = useCallback(() => { - mixpanelBrowserService.reset(); - }, []); - - return { - track, - time, - identify, - reset, - }; -}; diff --git a/src/ui/views/AddWelcome/AddRegister/SetPassword.tsx b/src/ui/views/AddWelcome/AddRegister/SetPassword.tsx index 3f765398..7300e076 100644 --- a/src/ui/views/AddWelcome/AddRegister/SetPassword.tsx +++ b/src/ui/views/AddWelcome/AddRegister/SetPassword.tsx @@ -14,17 +14,17 @@ import { Checkbox, FormControlLabel, } from '@mui/material'; -import { makeStyles, styled } from '@mui/styles'; +import { makeStyles } from '@mui/styles'; import { Box } from '@mui/system'; import HDWallet from 'ethereum-hdwallet'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import zxcvbn from 'zxcvbn'; import { storage } from '@/background/webapi'; import SlideRelative from '@/ui/FRWComponent/SlideRelative'; import { type AccountKey } from 'background/service/networkModel'; import { LLSpinner } from 'ui/FRWComponent'; -import { useWallet, saveIndex, mixpanelBrowserService } from 'ui/utils'; +import { useWallet, saveIndex } from 'ui/utils'; import CheckCircleIcon from '../../../../components/iconfont/IconCheckmark'; import CancelIcon from '../../../../components/iconfont/IconClose'; @@ -189,7 +189,6 @@ const SetPassword = ({ handleClick, mnemonic, username, setExPassword, tempPassw await saveIndex(username); const accountKey = getAccountKey(mnemonic); // track the time until account_created is called - mixpanelBrowserService.time('account_created'); wallet.openapi .register(accountKey, username) .then((response) => { diff --git a/src/ui/views/RecoverRegister/SetPassword.tsx b/src/ui/views/RecoverRegister/SetPassword.tsx index 14d1c945..c370f903 100644 --- a/src/ui/views/RecoverRegister/SetPassword.tsx +++ b/src/ui/views/RecoverRegister/SetPassword.tsx @@ -24,7 +24,7 @@ import { storage } from '@/background/webapi'; import { LLSpinner } from '@/ui/FRWComponent'; import SlideRelative from '@/ui/FRWComponent/SlideRelative'; import { type AccountKey } from 'background/service/networkModel'; -import { useWallet, saveIndex, mixpanelBrowserService } from 'ui/utils'; +import { useWallet, saveIndex } from 'ui/utils'; import CheckCircleIcon from '../../../components/iconfont/IconCheckmark'; import CancelIcon from '../../../components/iconfont/IconClose'; @@ -191,8 +191,6 @@ const SetPassword = ({ handleClick, mnemonic, username }) => { await saveIndex(username); const accountKey = getAccountKey(mnemonic); - // track the time until account_created is called - mixpanelBrowserService.time('account_created'); wallet.openapi .register(accountKey, username) .then((response) => { diff --git a/src/ui/views/Register/AllSet.tsx b/src/ui/views/Register/AllSet.tsx index d04a6f72..8fc7bed1 100644 --- a/src/ui/views/Register/AllSet.tsx +++ b/src/ui/views/Register/AllSet.tsx @@ -3,7 +3,7 @@ import { Box } from '@mui/system'; import React, { useCallback, useEffect } from 'react'; import AllSetIcon from 'ui/FRWAssets/svg/allset.svg'; -import { useWallet, mixpanelBrowserService } from 'ui/utils'; +import { useWallet } from 'ui/utils'; const AllSet = ({ handleClick }) => { const wallet = useWallet(); @@ -12,21 +12,11 @@ const AllSet = ({ handleClick }) => { await wallet.getCadenceScripts(); }, [wallet]); - const trackAccountRecovered = useCallback(async () => { - // I'm not sure if this is the best way to track this event - // It's hard to know at which point the user recovers the account - mixpanelBrowserService.track('account_recovered', { - address: (await wallet.getMainAddress()) || '', - mechanism: 'multi-backup', - methods: [], - }); - }, [wallet]); - useEffect(() => { loadScript().then(() => { - trackAccountRecovered(); + wallet.trackAccountRecovered(); }); - }, [loadScript, trackAccountRecovered]); + }, [loadScript, wallet]); return ( <> diff --git a/src/ui/views/Register/SetPassword.tsx b/src/ui/views/Register/SetPassword.tsx index af1b5184..872b193b 100644 --- a/src/ui/views/Register/SetPassword.tsx +++ b/src/ui/views/Register/SetPassword.tsx @@ -24,7 +24,7 @@ import { storage } from '@/background/webapi'; import SlideRelative from '@/ui/FRWComponent/SlideRelative'; import { type AccountKey } from 'background/service/networkModel'; import { LLSpinner } from 'ui/FRWComponent'; -import { useWallet, saveIndex, mixpanelBrowserService } from 'ui/utils'; +import { useWallet, saveIndex } from 'ui/utils'; import CheckCircleIcon from '../../../components/iconfont/IconCheckmark'; import CancelIcon from '../../../components/iconfont/IconClose'; @@ -192,8 +192,7 @@ const SetPassword = ({ handleClick, mnemonic, username, setExPassword }) => { await saveIndex(username); const accountKey = getAccountKey(mnemonic); - // track the time until account_created is called - mixpanelBrowserService.time('account_created'); + wallet.openapi .register(accountKey, username) .then((response) => { diff --git a/src/ui/views/Wallet/OnRampList.tsx b/src/ui/views/Wallet/OnRampList.tsx index 4e3aec85..7dbc2105 100644 --- a/src/ui/views/Wallet/OnRampList.tsx +++ b/src/ui/views/Wallet/OnRampList.tsx @@ -38,12 +38,11 @@ const OnRampList = ({ close }) => { const response = await wallet.openapi.getMoonpayURL(url); if (response?.data?.url) { + wallet.trackOnRampClicked('moonpay'); await chrome.tabs.create({ url: response?.data?.url, }); } - - wallet.trackOnRampClicked('moonpay'); }; const loadCoinbasePay = async () => { @@ -57,11 +56,13 @@ const OnRampList = ({ close }) => { }); if (onRampURL) { + // Track before opening the tab + wallet.trackOnRampClicked('coinbase'); + await chrome.tabs.create({ url: onRampURL, }); } - wallet.trackOnRampClicked('coinbase'); }; return ( diff --git a/src/ui/views/index.tsx b/src/ui/views/index.tsx index 9b18799a..e0e45b8b 100644 --- a/src/ui/views/index.tsx +++ b/src/ui/views/index.tsx @@ -2,12 +2,12 @@ import { CssBaseline } from '@mui/material'; import GlobalStyles from '@mui/material/GlobalStyles'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import React from 'react'; -import { HashRouter as Router, Route } from 'react-router-dom'; +import { HashRouter as Router, Route, useLocation } from 'react-router-dom'; import themeOptions from '@/ui/style/LLTheme'; import { NewsProvider } from '@/ui/utils/NewsContext'; import { PrivateRoute } from 'ui/component'; -import { WalletProvider, mixpanelBrowserService } from 'ui/utils'; +import { WalletProvider, useWallet } from 'ui/utils'; import Approval from './Approval'; import InnerRoute from './InnerRoute'; @@ -19,15 +19,16 @@ import Unlock from './Unlock'; const theme = createTheme(themeOptions); -function Main() { +const Routes = () => { + const location = useLocation(); + const wallet = useWallet(); + React.useEffect(() => { - // Initialize mixpanel in the popup - // Note: Mixpanel is initialized in the constructor, just calling init here to make sure it is initialized - mixpanelBrowserService.init(); - }, []); + wallet.trackPageView(location.pathname); + }, [location, wallet]); return ( - + <> @@ -42,6 +43,14 @@ function Main() { + + ); +}; + +function Main() { + return ( + + ); } From 4f6bbdfbd9a92f1c7a9efb14ce6d4fbb9762f66a Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:17:49 +1100 Subject: [PATCH 2/3] Added ip address geo location Fixes #288 --- src/background/service/mixpanel.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/background/service/mixpanel.ts b/src/background/service/mixpanel.ts index 2d9c7dd7..e30840ee 100644 --- a/src/background/service/mixpanel.ts +++ b/src/background/service/mixpanel.ts @@ -64,12 +64,13 @@ class MixpanelService { async #getExtraProps() { const extensionVersion = chrome.runtime.getManifest().version; const browserInfo = getBrowserInfo(); + //const geoLocation = await this.getGeoLocation(); + const extraProps = { $app_version_string: extensionVersion, $browser: browserInfo.browser, $browser_version: browserInfo.version, - // $insert_id: "", // for deduplication! - time: timestamp() / 1000, // epoch time in seconds + time: timestamp() / 1000, $os: (await chrome.runtime.getPlatformInfo()).os, }; return extraProps; @@ -121,7 +122,6 @@ class MixpanelService { const body = { ...data, }; - console.log('body', body); try { const response = await fetch(`${this.API_URL}${endpoint}`, { method: 'POST', @@ -136,9 +136,7 @@ class MixpanelService { throw new Error(`Mixpanel API error: ${response.statusText}`); } - console.log('response', response); const responseText = await response.text(); - console.log('responseText', responseText); if (responseText !== '1') { throw new Error(`Mixpanel API returned unexpected response: ${responseText}`); @@ -193,7 +191,8 @@ class MixpanelService { event.properties.$duration = Date.now() - startTimeStamp; } - await this.sendRequest('/track', event); + //Set determine geo location from ip as ip=1 + await this.sendRequest('/track?ip=1', event); } async trackPageView(pathname: string) { await this.init(); @@ -202,7 +201,7 @@ class MixpanelService { current_page_title: 'Flow Wallet', current_domain: 'flow-extension', current_url_path: pathname, - current_url_protocol: 'chrome-extension', + current_url_protocol: 'chrome-extension:', }); } async identify(userId: string, name?: string) { From 6851afc03ba9624fbed359683adb1ce2bc99a941 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:55:00 +1100 Subject: [PATCH 3/3] Removed mixpanel dependencies Fixes #288 --- package.json | 2 -- pnpm-lock.yaml | 79 -------------------------------------------------- 2 files changed, 81 deletions(-) diff --git a/package.json b/package.json index 58079bb2..37bc085f 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "@trustwallet/wallet-core": "^4.1.19", "@tsparticles/engine": "^3.6.0", "@tsparticles/react": "^3.0.0", - "@types/mixpanel-browser": "^2.50.2", "@walletconnect/core": "^2.17.2", "@walletconnect/jsonrpc-utils": "^1.0.8", "@walletconnect/modal": "^2.7.0", @@ -98,7 +97,6 @@ "lodash": "^4.17.21", "loglevel": "^1.9.2", "lru-cache": "^6.0.0", - "mixpanel-browser": "^2.56.0", "nanoid": "^3.3.7", "obs-store": "^4.0.3", "process": "^0.11.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1595f42c..9f063632 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,9 +104,6 @@ importers: '@tsparticles/react': specifier: ^3.0.0 version: 3.0.0(@tsparticles/engine@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/mixpanel-browser': - specifier: ^2.50.2 - version: 2.50.2 '@walletconnect/core': specifier: ^2.17.2 version: 2.17.2 @@ -227,9 +224,6 @@ importers: lru-cache: specifier: ^6.0.0 version: 6.0.0 - mixpanel-browser: - specifier: ^2.56.0 - version: 2.56.0 nanoid: specifier: ^3.3.7 version: 3.3.8 @@ -2507,9 +2501,6 @@ packages: cpu: [x64] os: [win32] - '@rrweb/types@2.0.0-alpha.17': - resolution: {integrity: sha512-AfDTVUuCyCaIG0lTSqYtrZqJX39ZEYzs4fYKnexhQ+id+kbZIpIJtaut5cto6dWZbB3SEe4fW0o90Po3LvTmfg==} - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2951,9 +2942,6 @@ packages: '@types/cookies@0.9.0': resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==} - '@types/css-font-loading-module@0.0.7': - resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==} - '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -3089,9 +3077,6 @@ packages: '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/mixpanel-browser@2.50.2': - resolution: {integrity: sha512-Iw8cBzplUPfHoeYuasqeYwdbGTNXhN+5kFT9kU+C7zm0NtaiPpKoiuzITr2ZH9KgBsWi2MbG0FOzIg9sQepauQ==} - '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -3529,9 +3514,6 @@ packages: '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} - '@xstate/fsm@1.6.5': - resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==} - '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -3941,10 +3923,6 @@ packages: base-x@3.0.10: resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} - base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -5484,9 +5462,6 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fflate@0.4.8: - resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} - file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -7236,16 +7211,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - mixin-deep@1.3.2: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} - mixpanel-browser@2.56.0: - resolution: {integrity: sha512-GYeEz58pV2M9MZtK8vSPL4oJmCwGS08FDDRZvZwr5VJpWdT4Lgyg6zXhmNfCmSTEIw2coaarm7HZ4FL9dAVvnA==} - mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -8514,15 +8483,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rrdom@2.0.0-alpha.17: - resolution: {integrity: sha512-b6caDiNcFO96Opp7TGdcVd4OLGSXu5dJe+A0IDiAu8mk7OmhqZCSDlgQdTKmdO5wMf4zPsUTgb8H/aNvR3kDHA==} - - rrweb-snapshot@2.0.0-alpha.18: - resolution: {integrity: sha512-hBHZL/NfgQX6wO1D9mpwqFu1NJPpim+moIcKhFEjVTZVRUfCln+LOugRc4teVTCISYHN8Cw5e2iNTWCSm+SkoA==} - - rrweb@2.0.0-alpha.13: - resolution: {integrity: sha512-a8GXOCnzWHNaVZPa7hsrLZtNZ3CGjiL+YrkpLo0TfmxGLhjNZbWY2r7pE06p+FcjFNlgUVTmFrSJbK3kO7yxvw==} - rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} @@ -12950,10 +12910,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.28.1': optional: true - '@rrweb/types@2.0.0-alpha.17': - dependencies: - rrweb-snapshot: 2.0.0-alpha.18 - '@rtsao/scc@1.1.0': {} '@safe-global/safe-apps-provider@0.18.5(typescript@5.7.2)(zod@3.23.8)': @@ -13584,8 +13540,6 @@ snapshots: '@types/keygrip': 1.0.6 '@types/node': 22.10.1 - '@types/css-font-loading-module@0.0.7': {} - '@types/d3-array@3.2.1': {} '@types/d3-color@3.1.3': {} @@ -13734,8 +13688,6 @@ snapshots: '@types/minimatch@5.1.2': {} - '@types/mixpanel-browser@2.50.2': {} - '@types/ms@0.7.34': {} '@types/node@16.18.121': {} @@ -14862,8 +14814,6 @@ snapshots: '@xobotyi/scrollbar-width@1.9.5': {} - '@xstate/fsm@1.6.5': {} - '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -15302,8 +15252,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - base64-arraybuffer@1.0.2: {} - base64-js@1.5.1: {} base@0.11.2: @@ -17310,8 +17258,6 @@ snapshots: dependencies: pend: 1.2.0 - fflate@0.4.8: {} - file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -19581,17 +19527,11 @@ snapshots: minipass@7.1.2: {} - mitt@3.0.1: {} - mixin-deep@1.3.2: dependencies: for-in: 1.0.2 is-extendable: 1.0.1 - mixpanel-browser@2.56.0: - dependencies: - rrweb: 2.0.0-alpha.13 - mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -20993,25 +20933,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.28.1 fsevents: 2.3.3 - rrdom@2.0.0-alpha.17: - dependencies: - rrweb-snapshot: 2.0.0-alpha.18 - - rrweb-snapshot@2.0.0-alpha.18: - dependencies: - postcss: 8.4.49 - - rrweb@2.0.0-alpha.13: - dependencies: - '@rrweb/types': 2.0.0-alpha.17 - '@types/css-font-loading-module': 0.0.7 - '@xstate/fsm': 1.6.5 - base64-arraybuffer: 1.0.2 - fflate: 0.4.8 - mitt: 3.0.1 - rrdom: 2.0.0-alpha.17 - rrweb-snapshot: 2.0.0-alpha.18 - rtl-css-js@1.16.1: dependencies: '@babel/runtime': 7.26.0