From 9fba9e2a1a8ca15dcf54bc2f886a3532d455841a Mon Sep 17 00:00:00 2001 From: ryanml Date: Mon, 5 Aug 2024 13:00:52 -0700 Subject: [PATCH] feat: adding TransactionTimeToConfirmation event for confirmations Signed-off-by: ryanml --- .../handlers/avalanche_sendTransaction.ts | 14 ++++++ .../handlers/bitcoin_sendTransaction.test.ts | 29 +++++++++---- .../handlers/bitcoin_sendTransaction.ts | 43 ++++++++++++++----- .../eth_sendTransaction.test.ts | 11 +++++ .../eth_sendTransaction.ts | 22 +++++++++- .../wallet/utils/measureTransactionTime.ts | 32 ++++++++++++++ src/tests/setupTests.ts | 2 + 7 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 src/background/services/wallet/utils/measureTransactionTime.ts diff --git a/src/background/services/wallet/handlers/avalanche_sendTransaction.ts b/src/background/services/wallet/handlers/avalanche_sendTransaction.ts index 1c4d87030..5d9958172 100644 --- a/src/background/services/wallet/handlers/avalanche_sendTransaction.ts +++ b/src/background/services/wallet/handlers/avalanche_sendTransaction.ts @@ -24,6 +24,7 @@ import getProvidedUtxos from '../utils/getProvidedUtxos'; import { AnalyticsServicePosthog } from '../../analytics/AnalyticsServicePosthog'; import { ChainId } from '@avalabs/core-chains-sdk'; import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; +import { measureTransactionTime } from '@src/background/services/wallet/utils/measureTransactionTime'; type TxParams = { transactionHex: string; @@ -231,6 +232,7 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler< const usedNetwork = this.#getChainIdForVM(vm); try { + measureTransactionTime().startMeasure(); // Parse the json into a tx object const unsignedTx = vm === EVM @@ -272,6 +274,18 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler< }, }); + measureTransactionTime().endMeasure(async (duration) => { + this.analyticsServicePosthog.captureEvent({ + name: 'TransactionTimeToConfirmation', + windowId: crypto.randomUUID(), + properties: { + duration, + txType: 'txType', + chainId: usedNetwork, + }, + }); + }); + // If we already have the transaction hash (i.e. it was dispatched by WalletConnect), // we just return it to the caller. onSuccess(txHash); diff --git a/src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts b/src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts index f938a7e6b..350984b6b 100644 --- a/src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts +++ b/src/background/services/wallet/handlers/bitcoin_sendTransaction.test.ts @@ -32,6 +32,7 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( const signMock = jest.fn(); const sendTransactionMock = jest.fn(); const getBalancesForNetworksMock = jest.fn(); + const captureEventMock = jest.fn(); const getBitcoinNetworkMock = jest.fn(); const activeAccountMock = { @@ -51,6 +52,9 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( const balanceAggregatorServiceMock = { getBalancesForNetworks: getBalancesForNetworksMock, }; + const analyticsServiceMock = { + captureEvent: captureEventMock, + }; beforeEach(() => { jest.resetAllMocks(); @@ -91,6 +95,7 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( {} as any, {} as any, {} as any, + {} as any, {} as any ); const result = await handler.handleUnauthenticated(buildRpcCall(request)); @@ -113,7 +118,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( type: AccountType.PRIMARY, }, } as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); it('returns error if the active account is imported via WalletConnect', async () => { @@ -128,7 +134,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( type: AccountType.WALLET_CONNECT, }, } as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); const result = await sendHandler.handleAuthenticated( @@ -153,7 +160,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( addressC: 'abcd1234', }, } as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); const result = await sendHandler.handleAuthenticated( @@ -230,7 +238,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( walletServiceMock as any, networkServiceMock as any, {} as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); const result = await sendHandler.handleAuthenticated({ request } as any); expect(result).toEqual({ @@ -260,7 +269,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( walletServiceMock as any, networkServiceMock as any, accountsServiceMock as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); const result = await sendHandler.handleAuthenticated( @@ -298,7 +308,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( }, } as any, accountsServiceMock as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); const result = await sendHandler.handleAuthenticated( @@ -329,7 +340,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( walletServiceMock as any, networkServiceMock as any, accountsServiceMock as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); getBitcoinNetworkMock.mockResolvedValue({ @@ -362,7 +374,8 @@ describe('src/background/services/wallet/handlers/bitcoin_sendTransaction.ts', ( walletServiceMock as any, networkServiceMock as any, accountsServiceMock as any, - balanceAggregatorServiceMock as any + balanceAggregatorServiceMock as any, + analyticsServiceMock as any ); getBitcoinNetworkMock.mockResolvedValue({ diff --git a/src/background/services/wallet/handlers/bitcoin_sendTransaction.ts b/src/background/services/wallet/handlers/bitcoin_sendTransaction.ts index c675b42f6..2f8711d4e 100644 --- a/src/background/services/wallet/handlers/bitcoin_sendTransaction.ts +++ b/src/background/services/wallet/handlers/bitcoin_sendTransaction.ts @@ -8,6 +8,7 @@ import { DAppRequestHandler } from '@src/background/connections/dAppConnection/D import { Action } from '../../actions/models'; import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; import { NetworkService } from '@src/background/services/network/NetworkService'; +import { AnalyticsServicePosthog } from '@src/background/services/analytics/AnalyticsServicePosthog'; import { ethErrors } from 'eth-rpc-errors'; import { DisplayData_BitcoinSendTx, @@ -33,6 +34,7 @@ import { resolve } from '@avalabs/core-utils-sdk'; import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; import { runtime } from 'webextension-polyfill'; +import { measureTransactionTime } from '@src/background/services/wallet/utils/measureTransactionTime'; type BitcoinTxParams = [ address: string, @@ -52,7 +54,8 @@ export class BitcoinSendTransactionHandler extends DAppRequestHandler< private walletService: WalletService, private networkService: NetworkService, private accountService: AccountsService, - private balanceAggregatorService: BalanceAggregatorService + private balanceAggregatorService: BalanceAggregatorService, + private analyticsServicePosthog: AnalyticsServicePosthog ) { super(); } @@ -256,8 +259,12 @@ export class BitcoinSendTransactionHandler extends DAppRequestHandler< frontendTabId?: number ) => { try { + measureTransactionTime().startMeasure(); const { address, amount, from, feeRate, balance } = pendingAction.displayData; + const btcChainID = this.networkService.isMainnet() + ? ChainId.BITCOIN + : ChainId.BITCOIN_TESTNET; const [network, networkError] = await resolve( this.networkService.getBitcoinNetwork() @@ -266,16 +273,14 @@ export class BitcoinSendTransactionHandler extends DAppRequestHandler< throw new Error('Bitcoin network not found'); } - const { inputs, outputs } = await buildBtcTx( - from, - getProviderForNetwork(network) as BitcoinProvider, - { - amount, - address, - token: balance, - feeRate, - } - ); + const provider = getProviderForNetwork(network) as BitcoinProvider; + + const { inputs, outputs } = await buildBtcTx(from, provider, { + amount, + address, + token: balance, + feeRate, + }); if (!inputs || !outputs) { throw new Error('Unable to create transaction'); @@ -293,7 +298,23 @@ export class BitcoinSendTransactionHandler extends DAppRequestHandler< if (this.#isSupportedAccount(this.accountService.activeAccount)) { this.#getBalance(this.accountService.activeAccount); } + onSuccess(hash); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + provider.waitForTx(hash).then((_tx) => { + measureTransactionTime().endMeasure(async (duration) => { + this.analyticsServicePosthog.captureEvent({ + name: 'TransactionTimeToConfirmation', + windowId: crypto.randomUUID(), + properties: { + duration, + txType: 'txType', + chainId: btcChainID, + }, + }); + }); + }); } catch (e) { onError(e); } diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts b/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts index 5c74ea948..dcc945edf 100644 --- a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts +++ b/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.test.ts @@ -52,6 +52,17 @@ jest.mock('@src/background/services/analytics/utils/encryptAnalyticsData'); jest.mock('./contracts/contractParsers/parseWithERC20Abi'); jest.mock('./utils/getTxDescription'); jest.mock('./contracts/contractParsers/utils/parseBasicDisplayValues'); +jest.mock( + '@src/background/services/wallet/utils/measureTransactionTime', + () => ({ + measureTransactionTime: function () { + return { + startMeasure: jest.fn(), + endMeasure: jest.fn(), + }; + }, + }) +); jest.mock('./contracts/contractParsers/contractParserMap', () => ({ contractParserMap: new Map([['function', jest.fn()]]), })); diff --git a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts b/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts index 73eaa23eb..b80e8dff3 100644 --- a/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts +++ b/src/background/services/wallet/handlers/eth_sendTransaction/eth_sendTransaction.ts @@ -45,6 +45,7 @@ import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; import { EnsureDefined } from '@src/background/models'; import { caipToChainId } from '@src/utils/caipConversion'; import { TxDisplayOptions } from '../models'; +import { measureTransactionTime } from '@src/background/services/wallet/utils/measureTransactionTime'; type TxPayload = EthSendTransactionParams | ContractTransaction; type Params = [TxPayload] | [TxPayload, TxDisplayOptions]; @@ -207,6 +208,7 @@ export class EthSendTransactionHandler extends DAppRequestHandler< tabId?: number | undefined ) => { try { + measureTransactionTime().startMeasure(); const network = await getTargetNetworkForTx( pendingAction.displayData.txParams, this.networkService, @@ -223,6 +225,7 @@ export class EthSendTransactionHandler extends DAppRequestHandler< const nonce = await provider.getTransactionCount( pendingAction.displayData.txParams.from ); + const chainId = pendingAction.displayData.chainId; const { maxFeePerGas, @@ -237,7 +240,7 @@ export class EthSendTransactionHandler extends DAppRequestHandler< const signingResult = await this.walletService.sign( { nonce, - chainId: Number(BigInt(pendingAction.displayData.chainId)), + chainId: Number(BigInt(chainId)), maxFeePerGas, maxPriorityFeePerGas, gasLimit: gasLimit, @@ -296,11 +299,26 @@ export class EthSendTransactionHandler extends DAppRequestHandler< address: this.accountsService.activeAccount?.addressC, txHash, method: pendingAction.method, - chainId: pendingAction.displayData.chainId, + chainId, }, }); onSuccess(txHash); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + provider.waitForTransaction(txHash).then((_tx) => { + measureTransactionTime().endMeasure(async (duration) => { + this.analyticsServicePosthog.captureEvent({ + name: 'TransactionTimeToConfirmation', + windowId: crypto.randomUUID(), + properties: { + duration, + txType: 'txType', + chainId, + }, + }); + }); + }); } catch (err: any) { const errorMessage: string = err instanceof Error ? err.message : err.toString(); diff --git a/src/background/services/wallet/utils/measureTransactionTime.ts b/src/background/services/wallet/utils/measureTransactionTime.ts new file mode 100644 index 000000000..5bec156df --- /dev/null +++ b/src/background/services/wallet/utils/measureTransactionTime.ts @@ -0,0 +1,32 @@ +enum TransactionTimeEvents { + TRANSACTION_TIMED = 'transaction-timed', + TRANSACTION_SUCCEEDED = 'transaction-succeeded', + TRANSACTION_APPROVED = 'transaction-approved', +} + +export const measureTransactionTime = (): { + startMeasure: () => void; + endMeasure: (callback: (duration: number) => void) => void; +} => { + const startMeasure = () => { + performance.mark(TransactionTimeEvents.TRANSACTION_APPROVED); + }; + + const endMeasure = (callback: (duration: number) => void) => { + performance.mark(TransactionTimeEvents.TRANSACTION_SUCCEEDED); + + const measurement = performance.measure( + TransactionTimeEvents.TRANSACTION_TIMED, + TransactionTimeEvents.TRANSACTION_APPROVED, + TransactionTimeEvents.TRANSACTION_SUCCEEDED + ); + + performance.clearMarks(TransactionTimeEvents.TRANSACTION_APPROVED); + performance.clearMarks(TransactionTimeEvents.TRANSACTION_SUCCEEDED); + performance.clearMarks(TransactionTimeEvents.TRANSACTION_TIMED); + + callback(measurement.duration); + }; + + return { startMeasure, endMeasure }; +}; diff --git a/src/tests/setupTests.ts b/src/tests/setupTests.ts index 2b5d63e64..4b561df01 100644 --- a/src/tests/setupTests.ts +++ b/src/tests/setupTests.ts @@ -26,6 +26,8 @@ Object.defineProperty(global.document, 'prerendering', { value: false, }); +performance.mark = jest.fn(); + global.chrome = { runtime: { id: 'testid',