From 3e1fddeef730027c06e267710f70f6642c942882 Mon Sep 17 00:00:00 2001 From: Chid Gilovitz Date: Wed, 29 Nov 2023 14:19:36 +0800 Subject: [PATCH] Analytics Improvements (#1159) * chore: remove some properties from posthog unsafe properties * chore: set some analytics values on user profile * chore: add isOnboarded property to account create analytics event * feat: add analytics event for asset transfer fiat value * feat: check for ghosts of the past nft * chore: respond to pr review --- apps/extension/src/core/config/posthog.ts | 3 -- .../src/core/domains/accounts/handler.ts | 32 ++++++++------ apps/extension/src/core/handlers/Extension.ts | 43 ++++++++++++------- apps/extension/src/core/libs/Analytics.ts | 14 +++--- apps/extension/src/core/util/hasErc721Nft.ts | 38 ++++++++++++++++ .../src/core/util/hasGhostsOfThePast.ts | 10 +++++ .../src/ui/domains/SendFunds/useSendFunds.ts | 6 +++ 7 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 apps/extension/src/core/util/hasErc721Nft.ts create mode 100644 apps/extension/src/core/util/hasGhostsOfThePast.ts diff --git a/apps/extension/src/core/config/posthog.ts b/apps/extension/src/core/config/posthog.ts index aa8d5d6e73..2a83e40970 100644 --- a/apps/extension/src/core/config/posthog.ts +++ b/apps/extension/src/core/config/posthog.ts @@ -5,9 +5,6 @@ const unsafeProperties = [ "$os", "$browser", "$device_type", - "$current_url", - "$host", - "$pathname", "$browser_version", "$screen_height", "$screen_width", diff --git a/apps/extension/src/core/domains/accounts/handler.ts b/apps/extension/src/core/domains/accounts/handler.ts index ec55326b2b..857176e303 100644 --- a/apps/extension/src/core/domains/accounts/handler.ts +++ b/apps/extension/src/core/domains/accounts/handler.ts @@ -49,6 +49,14 @@ import { combineLatest } from "rxjs" import { SOURCES } from "../mnemonics/store" export default class AccountsHandler extends ExtensionHandler { + private async captureAccountCreateEvent(type: string | undefined, method: string) { + talismanAnalytics.capture("account create", { + type, + method, + isOnboarded: await this.stores.app.getIsOnboarded(), + }) + } + private async accountCreate({ name, type, ...options }: RequestAccountCreate): Promise { const password = this.stores.password.getPassword() assert(password, "Not logged in") @@ -105,7 +113,7 @@ export default class AccountsHandler extends ExtensionHandler { type ) - talismanAnalytics.capture("account create", { type, method: "derived" }) + this.captureAccountCreateEvent(type, "derived") return pair.address } @@ -166,7 +174,7 @@ export default class AccountsHandler extends ExtensionHandler { type // if undefined, defaults to keyring's default (sr25519 atm) ) - talismanAnalytics.capture("account create", { type, method: "seed" }) + this.captureAccountCreateEvent(type, "seed") return pair.address } catch (error) { @@ -203,7 +211,7 @@ export default class AccountsHandler extends ExtensionHandler { keyring.encryptAccount(pair, password) addresses.push(pair.address) - talismanAnalytics.capture("account create", { type: pair.type, method: "json" }) + this.captureAccountCreateEvent(pair.type, "json") } return addresses @@ -213,7 +221,7 @@ export default class AccountsHandler extends ExtensionHandler { name, address, path, - }: RequestAccountCreateLedgerEthereum): string { + }: RequestAccountCreateLedgerEthereum) { assert(isEthereumAddress(address), "Not an Ethereum address") // ui-keyring's addHardware method only supports substrate accounts, cannot set ethereum type @@ -241,7 +249,7 @@ export default class AccountsHandler extends ExtensionHandler { keyring.keyring.addPair(pair) keyring.saveAccount(pair) - talismanAnalytics.capture("account create", { type: "ethereum", method: "hardware" }) + this.captureAccountCreateEvent("ethereum", "hardware") return pair.address } @@ -292,7 +300,7 @@ export default class AccountsHandler extends ExtensionHandler { keyring.keyring.addPair(pair) keyring.saveAccount(pair) - talismanAnalytics.capture("account create", { type, method: "dcent" }) + this.captureAccountCreateEvent(type, "dcent") return pair.address } @@ -312,7 +320,7 @@ export default class AccountsHandler extends ExtensionHandler { origin: AccountType.Ledger, }) - talismanAnalytics.capture("account create", { type: "substrate", method: "hardware" }) + this.captureAccountCreateEvent("substrate", "hardware") return pair.address } @@ -333,7 +341,7 @@ export default class AccountsHandler extends ExtensionHandler { origin: AccountType.Qr, }) - talismanAnalytics.capture("account create", { type: "substrate", method: "qr" }) + this.captureAccountCreateEvent("substrate", "qr") return pair.address } @@ -376,10 +384,10 @@ export default class AccountsHandler extends ExtensionHandler { keyring.keyring.addPair(pair) keyring.saveAccount(pair) - talismanAnalytics.capture("add watched account", { - type: isEthereumAddress(safeAddress) ? "ethereum" : "substrate", - method: "watched", - }) + this.captureAccountCreateEvent( + isEthereumAddress(safeAddress) ? "ethereum" : "substrate", + "watched" + ) return pair.address } diff --git a/apps/extension/src/core/handlers/Extension.ts b/apps/extension/src/core/handlers/Extension.ts index bff2b72862..1688d3bbb4 100644 --- a/apps/extension/src/core/handlers/Extension.ts +++ b/apps/extension/src/core/handlers/Extension.ts @@ -27,6 +27,7 @@ import { MessageTypes, RequestType, ResponseType } from "@core/types" import { Port, RequestIdOnly } from "@core/types/base" import { awaitKeyringLoaded } from "@core/util/awaitKeyringLoaded" import { CONFIG_RATE_LIMIT_ERROR, getConfig } from "@core/util/getConfig" +import { hasGhostsOfThePast } from "@core/util/hasGhostsOfThePast" import { fetchHasSpiritKey } from "@core/util/hasSpiritKey" import keyring from "@polkadot/ui-keyring" import * as Sentry from "@sentry/browser" @@ -93,6 +94,9 @@ export default class Extension extends ExtensionHandler { await stores.sites.updateSite(url, autoAddSite) }) }) + + this.checkSpiritKeyOwnership() + this.checkGhostsOfThePastOwnership() }) // setup polling for config from github @@ -103,7 +107,6 @@ export default class Extension extends ExtensionHandler { this.initDb() this.initWalletFunding() - this.checkSpiritKeyOwnership() this.cleanup() } @@ -205,23 +208,19 @@ export default class Extension extends ExtensionHandler { }) } - private checkSpiritKeyOwnership() { - // wait 10 seconds as this check is low priority - // also need to be wait for keyring to be loaded and accounts populated - awaitKeyringLoaded().then(async () => { - try { - const hasSpiritKey = await fetchHasSpiritKey() - const currentSpiritKey = await this.stores.app.get("hasSpiritKey") + private async checkSpiritKeyOwnership() { + try { + const hasSpiritKey = await fetchHasSpiritKey() + const currentSpiritKey = await this.stores.app.get("hasSpiritKey") - if (currentSpiritKey !== hasSpiritKey) { - await this.stores.app.set({ hasSpiritKey, needsSpiritKeyUpdate: true }) - await this.updateSpiritKeyOwnership(hasSpiritKey) - } - } catch (err) { - // ignore, don't update app store nor posthog property - log.error("Failed to check Spirit Key ownership", { err }) + if (currentSpiritKey !== hasSpiritKey) { + await this.stores.app.set({ hasSpiritKey, needsSpiritKeyUpdate: true }) + await this.updateSpiritKeyOwnership(hasSpiritKey) } - }) + } catch (err) { + // ignore, don't update app store nor posthog property + log.error("Failed to check Spirit Key ownership", { err }) + } // in case reporting to posthog fails, set a timer so that every 5 min we will re-attempt setInterval(async () => { @@ -243,6 +242,18 @@ export default class Extension extends ExtensionHandler { await this.stores.app.set({ needsSpiritKeyUpdate: false }) } + private async checkGhostsOfThePastOwnership() { + try { + const hasGhosts = await hasGhostsOfThePast() + const hasGhostsNft = Object.values(hasGhosts).some((g) => g) + await talismanAnalytics.capture("Ghosts of the past ownership", { + $set: { hasGhostsOfThePast: hasGhostsNft }, + }) + } catch (err) { + log.error("Failed to check Ghosts of the Past ownership", { err }) + } + } + public async handle( id: string, type: TMessageType, diff --git a/apps/extension/src/core/libs/Analytics.ts b/apps/extension/src/core/libs/Analytics.ts index bd244585cd..0fe50460ca 100644 --- a/apps/extension/src/core/libs/Analytics.ts +++ b/apps/extension/src/core/libs/Analytics.ts @@ -90,7 +90,6 @@ class TalismanAnalytics { // accounts const accounts = keyring.getAccounts() if (accounts.length > 0) { - posthog.capture("accounts count", { count: keyring.getAccounts().length }) // account type breakdown const accountBreakdown = accounts.reduce( (result, account) => { @@ -145,10 +144,6 @@ class TalismanAnalytics { throw error } - posthog.capture("balances fiat sum", { - total: roundToFirstInteger(balances.sum.fiat("usd").total), - }) - // balances top 5 tokens/networks // get Balance list per chain/evmNetwork and token const balancesPerChainToken: Record = balances.each @@ -180,6 +175,15 @@ class TalismanAnalytics { .slice(0, 5) posthog.capture("balances top tokens", topChainTokens) + posthog.capture("balances report", { + $set: { + accountsCount: keyring + .getAccounts() + .filter(({ meta }) => meta.origin !== AccountType.Watched).length, + totalFiatValue: roundToFirstInteger(balances.sum.fiat("usd").total), + topToken: `${topChainTokens[0].chainId}: ${topChainTokens[0].tokenId}`, + }, + }) } } diff --git a/apps/extension/src/core/util/hasErc721Nft.ts b/apps/extension/src/core/util/hasErc721Nft.ts new file mode 100644 index 0000000000..ddf4cf04a5 --- /dev/null +++ b/apps/extension/src/core/util/hasErc721Nft.ts @@ -0,0 +1,38 @@ +import { AccountType } from "@core/domains/accounts/types" +import { chainConnectorEvm } from "@core/rpcs/chain-connector-evm" +import keyring from "@polkadot/ui-keyring" +import { EvmNetworkId } from "@talismn/chaindata-provider" +import { parseAbi } from "viem" + +import { abiErc721 } from "./abi" + +type Address = `0x${string}` + +export const hasErc721Nft = async ({ + evmNetworkId, + contractAddress, +}: { + evmNetworkId: EvmNetworkId + contractAddress: Address +}): Promise> => { + const evmAddresses = keyring + .getPairs() + .filter(({ type, meta }) => type === "ethereum" && meta.origin !== AccountType.Watched) + .map(({ address }) => address as Address) + + const client = await chainConnectorEvm.getPublicClientForEvmNetwork(evmNetworkId) + if (!client) throw new Error(`Unable to connect to EVM network: ${evmNetworkId}`) + + const data = await Promise.all( + evmAddresses.map((address) => + client.readContract({ + address: contractAddress, + abi: parseAbi(abiErc721), + functionName: "balanceOf", + args: [address], + }) + ) + ) + + return Object.fromEntries(evmAddresses.map((address, i) => [address, data[i] > 0n])) +} diff --git a/apps/extension/src/core/util/hasGhostsOfThePast.ts b/apps/extension/src/core/util/hasGhostsOfThePast.ts new file mode 100644 index 0000000000..dfca74b2aa --- /dev/null +++ b/apps/extension/src/core/util/hasGhostsOfThePast.ts @@ -0,0 +1,10 @@ +import { hasErc721Nft } from "./hasErc721Nft" + +const moonbeamEvmNetworkId = "1284" +const contractAddress = "0xDF67E64DC198E5287a6a625a4733841bD147E584" + +export const hasGhostsOfThePast = () => + hasErc721Nft({ + evmNetworkId: moonbeamEvmNetworkId, + contractAddress, + }) diff --git a/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts b/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts index cf33261674..f9bf6d79b9 100644 --- a/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts +++ b/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts @@ -6,6 +6,7 @@ import { } from "@core/domains/ethereum/helpers" import { AssetTransferMethod } from "@core/domains/transfers/types" import { log } from "@core/log" +import { roundToFirstInteger } from "@core/util/roundToFirstInteger" import { HexString } from "@polkadot/util/types" import { provideContext } from "@talisman/util/provideContext" import { Address, Balance, BalanceFormatter } from "@talismn/balances" @@ -536,6 +537,11 @@ const useSendFundsProvider = () => { setIsProcessing(true) + api.analyticsCapture({ + eventName: "asset transfer fiat value", + options: { value: roundToFirstInteger(value.fiat("usd") ?? 0) ?? "0" }, + }) + if (token.chain?.id && chain?.genesisHash) { const { hash } = await api.assetTransfer( token.chain.id,