diff --git a/packages/passport/sdk/src/errors/passportError.ts b/packages/passport/sdk/src/errors/passportError.ts index 7f21aca6fb..d419fac342 100644 --- a/packages/passport/sdk/src/errors/passportError.ts +++ b/packages/passport/sdk/src/errors/passportError.ts @@ -1,6 +1,5 @@ import { isAxiosError } from 'axios'; import { imx } from '@imtbl/generated-clients'; -import { trackError } from '@imtbl/metrics'; export enum PassportErrorType { AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR', @@ -41,7 +40,6 @@ export class PassportError extends Error { export const withPassportError = async ( fn: () => Promise, customErrorType: PassportErrorType, - customEventName?: string, ): Promise => { try { return await fn(); @@ -54,12 +52,6 @@ export const withPassportError = async ( errorMessage = (error as Error).message; } - const passportError = new PassportError(errorMessage, customErrorType); - - if (customEventName) { - trackError('passport', customEventName, passportError); - } - - throw passportError; + throw new PassportError(errorMessage, customErrorType); } }; diff --git a/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts b/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts index ac49fb1189..4a0257b3d8 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts @@ -12,6 +12,7 @@ import { UnsignedOrderRequest, UnsignedTransferRequest, } from '@imtbl/x-client'; +import { trackError, trackFlow } from '@imtbl/metrics'; import registerPassportStarkEx from './workflows/registration'; import { mockUser, mockUserImx } from '../test/mocks'; import { PassportError, PassportErrorType } from '../errors/passportError'; @@ -32,6 +33,7 @@ jest.mock('./workflows/registration'); jest.mock('./getStarkSigner'); jest.mock('@imtbl/generated-clients'); jest.mock('@imtbl/x-client'); +jest.mock('@imtbl/metrics'); describe('PassportImxProvider', () => { afterEach(jest.resetAllMocks); @@ -83,6 +85,11 @@ describe('PassportImxProvider', () => { passportEventEmitter = new TypedEventEmitter(); mockAuthManager.getUser.mockResolvedValue(mockUserImx); + // Metrics + (trackFlow as unknown as jest.Mock).mockImplementation(() => ({ + addEvent: jest.fn(), + })); + // Signers magicAdapterMock.login.mockResolvedValue({ getSigner: getSignerMock }); (Web3Provider as unknown as jest.Mock).mockReturnValue({ getSigner: getSignerMock }); @@ -125,6 +132,11 @@ describe('PassportImxProvider', () => { magicAdapterMock.login.mockResolvedValue({}); (getStarkSigner as jest.Mock).mockRejectedValue(new Error('error')); + // Metrics + (trackFlow as unknown as jest.Mock).mockImplementation(() => ({ + addEvent: jest.fn(), + })); + const pp = new PassportImxProvider({ authManager: mockAuthManager as unknown as AuthManager, magicAdapter: magicAdapterMock as unknown as MagicAdapter, @@ -359,15 +371,15 @@ describe('PassportImxProvider', () => { }); describe.each([ - ['transfer' as const, {} as UnsignedTransferRequest], - ['createOrder' as const, {} as UnsignedOrderRequest], - ['cancelOrder' as const, {} as imx.GetSignableCancelOrderRequest], - ['createTrade' as const, {} as imx.GetSignableTradeRequest], - ['batchNftTransfer' as const, [] as NftTransferDetails[]], - ['exchangeTransfer' as const, {} as UnsignedExchangeTransferRequest], - ['getAddress' as const, {} as any], - ['isRegisteredOffchain' as const, {} as any], - ])('when the user has been logged out - %s', (methodName, args) => { + ['transfer' as const, 'imxTransfer', {} as UnsignedTransferRequest], + ['createOrder' as const, 'imxCreateOrder', {} as UnsignedOrderRequest], + ['cancelOrder' as const, 'imxCancelOrder', {} as imx.GetSignableCancelOrderRequest], + ['createTrade' as const, 'imxCreateTrade', {} as imx.GetSignableTradeRequest], + ['batchNftTransfer' as const, 'imxBatchNftTransfer', [] as NftTransferDetails[]], + ['exchangeTransfer' as const, 'imxExchangeTransfer', {} as UnsignedExchangeTransferRequest], + ['getAddress' as const, 'imxGetAddress', {} as any], + ['isRegisteredOffchain' as const, 'imxIsRegisteredOffchain', {} as any], + ])('when the user has been logged out - %s', (methodName, eventName, args) => { beforeEach(() => { passportEventEmitter.emit(PassportEvents.LOGGED_OUT); }); @@ -382,18 +394,34 @@ describe('PassportImxProvider', () => { ), ); }); + + it(`should track metrics when error thrown for ${methodName}`, async () => { + try { + await passportImxProvider[methodName!](args); + } catch (error) { + expect(trackFlow).toHaveBeenCalledWith( + 'passport', + eventName, + ); + expect(trackError).toHaveBeenCalledWith( + 'passport', + eventName, + error, + ); + } + }); }); describe.each([ - ['transfer' as const, {} as UnsignedTransferRequest], - ['createOrder' as const, {} as UnsignedOrderRequest], - ['cancelOrder' as const, {} as imx.GetSignableCancelOrderRequest], - ['createTrade' as const, {} as imx.GetSignableTradeRequest], - ['batchNftTransfer' as const, [] as NftTransferDetails[]], - ['exchangeTransfer' as const, {} as UnsignedExchangeTransferRequest], - ['getAddress' as const, {} as any], - ['isRegisteredOffchain' as const, {} as any], - ])('when the user\'s access token is expired and cannot be retrieved', (methodName, args) => { + ['transfer' as const, 'imxTransfer', {} as UnsignedTransferRequest], + ['createOrder' as const, 'imxCreateOrder', {} as UnsignedOrderRequest], + ['cancelOrder' as const, 'imxCancelOrder', {} as imx.GetSignableCancelOrderRequest], + ['createTrade' as const, 'imxCreateTrade', {} as imx.GetSignableTradeRequest], + ['batchNftTransfer' as const, 'imxBatchNftTransfer', [] as NftTransferDetails[]], + ['exchangeTransfer' as const, 'imxExchangeTransfer', {} as UnsignedExchangeTransferRequest], + ['getAddress' as const, 'imxGetAddress', {} as any], + ['isRegisteredOffchain' as const, 'imxIsRegisteredOffchain', {} as any], + ])('when the user\'s access token is expired and cannot be retrieved', (methodName, eventName, args) => { beforeEach(() => { mockAuthManager.getUser.mockResolvedValue(null); }); @@ -408,5 +436,21 @@ describe('PassportImxProvider', () => { ), ); }); + + it(`should track metrics when error thrown for ${methodName}`, async () => { + try { + await passportImxProvider[methodName!](args); + } catch (error) { + expect(trackFlow).toHaveBeenCalledWith( + 'passport', + eventName, + ); + expect(trackError).toHaveBeenCalledWith( + 'passport', + eventName, + error, + ); + } + }); }); }); diff --git a/packages/passport/sdk/src/starkEx/passportImxProvider.ts b/packages/passport/sdk/src/starkEx/passportImxProvider.ts index a5bd035c34..6bb32308b4 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProvider.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProvider.ts @@ -29,6 +29,7 @@ import { import registerOffchain from './workflows/registerOffchain'; import MagicAdapter from '../magicAdapter'; import { getStarkSigner } from './getStarkSigner'; +import { withMetricsAsync } from '../utils/metrics'; export interface PassportImxProviderOptions { authManager: AuthManager; @@ -173,8 +174,8 @@ export class PassportImxProvider implements IMXProvider { } async transfer(request: UnsignedTransferRequest): Promise { - return ( - this.guardianClient.withDefaultConfirmationScreenTask(async () => { + return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( + async () => { const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); return transfer({ @@ -184,29 +185,38 @@ export class PassportImxProvider implements IMXProvider { transfersApi: this.immutableXClient.transfersApi, guardianClient: this.guardianClient, }); - })() - ); + }, + )(), 'imxTransfer'); } async registerOffchain(): Promise { - const [user, signers] = await Promise.all([ - this.#getAuthenticatedUser(), - this.#getSigners(), - ]); - - return await registerOffchain( - signers.ethSigner, - signers.starkSigner, - user, - this.authManager, - this.imxApiClients, + return withMetricsAsync( + async () => { + const [user, signers] = await Promise.all([ + this.#getAuthenticatedUser(), + this.#getSigners(), + ]); + + return await registerOffchain( + signers.ethSigner, + signers.starkSigner, + user, + this.authManager, + this.imxApiClients, + ); + }, + 'imxRegisterOffchain', ); } async isRegisteredOffchain(): Promise { - const user = await this.#getAuthenticatedUser(); - - return !!user.imx; + return withMetricsAsync( + async () => { + const user = await this.#getAuthenticatedUser(); + return !!user.imx; + }, + 'imxIsRegisteredOffchain', + ); } // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars @@ -218,7 +228,7 @@ export class PassportImxProvider implements IMXProvider { } async createOrder(request: UnsignedOrderRequest): Promise { - return this.guardianClient.withDefaultConfirmationScreenTask( + return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( async () => { const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); return createOrder({ @@ -229,13 +239,13 @@ export class PassportImxProvider implements IMXProvider { guardianClient: this.guardianClient, }); }, - )(); + )(), 'imxCreateOrder'); } async cancelOrder( request: imx.GetSignableCancelOrderRequest, ): Promise { - return this.guardianClient.withDefaultConfirmationScreenTask( + return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( async () => { const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); @@ -247,11 +257,11 @@ export class PassportImxProvider implements IMXProvider { guardianClient: this.guardianClient, }); }, - )(); + )(), 'imxCancelOrder'); } async createTrade(request: imx.GetSignableTradeRequest): Promise { - return this.guardianClient.withDefaultConfirmationScreenTask( + return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( async () => { const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); @@ -263,13 +273,13 @@ export class PassportImxProvider implements IMXProvider { guardianClient: this.guardianClient, }); }, - )(); + )(), 'imxCreateTrade'); } async batchNftTransfer( request: NftTransferDetails[], ): Promise { - return this.guardianClient.withConfirmationScreenTask( + return withMetricsAsync(() => this.guardianClient.withConfirmationScreenTask( { width: 480, height: 784 }, )(async () => { const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); @@ -281,20 +291,22 @@ export class PassportImxProvider implements IMXProvider { transfersApi: this.immutableXClient.transfersApi, guardianClient: this.guardianClient, }); - })(); + })(), 'imxBatchNftTransfer'); } async exchangeTransfer( request: UnsignedExchangeTransferRequest, ): Promise { - const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); + return withMetricsAsync(async () => { + const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); - return exchangeTransfer({ - request, - user, - starkSigner, - exchangesApi: this.immutableXClient.exchangeApi, - }); + return exchangeTransfer({ + request, + user, + starkSigner, + exchangesApi: this.immutableXClient.exchangeApi, + }); + }, 'imxExchangeTransfer'); } // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars @@ -327,14 +339,16 @@ export class PassportImxProvider implements IMXProvider { } async getAddress(): Promise { - const user = await this.#getAuthenticatedUser(); - if (!isUserImx(user)) { - throw new PassportError( - 'User has not been registered with StarkEx', - PassportErrorType.USER_NOT_REGISTERED_ERROR, - ); - } + return withMetricsAsync(async () => { + const user = await this.#getAuthenticatedUser(); + if (!isUserImx(user)) { + throw new PassportError( + 'User has not been registered with StarkEx', + PassportErrorType.USER_NOT_REGISTERED_ERROR, + ); + } - return Promise.resolve(user.imx.ethAddress); + return Promise.resolve(user.imx.ethAddress); + }, 'imxGetAddress'); } } diff --git a/packages/passport/sdk/src/starkEx/workflows/registration.ts b/packages/passport/sdk/src/starkEx/workflows/registration.ts index d9f0abdeba..fac63ac4b8 100644 --- a/packages/passport/sdk/src/starkEx/workflows/registration.ts +++ b/packages/passport/sdk/src/starkEx/workflows/registration.ts @@ -1,6 +1,7 @@ import { signRaw } from '@imtbl/toolkit'; import { WalletConnection } from '@imtbl/x-client'; import { ImxApiClients, imx } from '@imtbl/generated-clients'; +import { PassportErrorType, withPassportError } from '../../errors/passportError'; export type RegisterPassportParams = WalletConnection & { imxApiClients: ImxApiClients; @@ -10,32 +11,34 @@ export default async function registerPassport( { ethSigner, starkSigner, imxApiClients }: RegisterPassportParams, authorization: string, ): Promise { - const [userAddress, starkPublicKey] = await Promise.all([ - ethSigner.getAddress(), - starkSigner.getAddress(), - ]); + return withPassportError(async () => { + const [userAddress, starkPublicKey] = await Promise.all([ + ethSigner.getAddress(), + starkSigner.getAddress(), + ]); - const signableResult = await imxApiClients.usersApi.getSignableRegistrationOffchain({ - getSignableRegistrationRequest: { - ether_key: userAddress, - stark_key: starkPublicKey, - }, - }); + const signableResult = await imxApiClients.usersApi.getSignableRegistrationOffchain({ + getSignableRegistrationRequest: { + ether_key: userAddress, + stark_key: starkPublicKey, + }, + }); - const { signable_message: signableMessage, payload_hash: payloadHash } = signableResult.data; - const [ethSignature, starkSignature] = await Promise.all([ - signRaw(signableMessage, ethSigner), - starkSigner.signMessage(payloadHash), - ]); + const { signable_message: signableMessage, payload_hash: payloadHash } = signableResult.data; + const [ethSignature, starkSignature] = await Promise.all([ + signRaw(signableMessage, ethSigner), + starkSigner.signMessage(payloadHash), + ]); - const response = await imxApiClients.usersApi.registerPassportUserV2({ - authorization: `Bearer ${authorization}`, - registerPassportUserRequest: { - eth_signature: ethSignature, - ether_key: userAddress, - stark_signature: starkSignature, - stark_key: starkPublicKey, - }, - }); - return response.data as imx.RegisterUserResponse; + const response = await imxApiClients.usersApi.registerPassportUserV2({ + authorization: `Bearer ${authorization}`, + registerPassportUserRequest: { + eth_signature: ethSignature, + ether_key: userAddress, + stark_signature: starkSignature, + stark_key: starkPublicKey, + }, + }); + return response.data as imx.RegisterUserResponse; + }, PassportErrorType.USER_REGISTRATION_ERROR); } diff --git a/packages/passport/sdk/src/starkEx/workflows/transfer.ts b/packages/passport/sdk/src/starkEx/workflows/transfer.ts index 4ba6394e89..6934e66a4a 100644 --- a/packages/passport/sdk/src/starkEx/workflows/transfer.ts +++ b/packages/passport/sdk/src/starkEx/workflows/transfer.ts @@ -90,7 +90,7 @@ export async function transfer({ time: responseData.time, transfer_id: responseData.transfer_id, }; - }, PassportErrorType.TRANSFER_ERROR, 'imxTransfer'); + }, PassportErrorType.TRANSFER_ERROR); } export async function batchNftTransfer({ @@ -161,5 +161,5 @@ export async function batchNftTransfer({ return { transfer_ids: response?.data.transfer_ids, }; - }, PassportErrorType.TRANSFER_ERROR, 'imxBatchNftTransfer'); + }, PassportErrorType.TRANSFER_ERROR); }