diff --git a/app/util/sentry/__snapshots__/utils.test.ts.snap b/app/util/sentry/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..1b4d18ae6eb --- /dev/null +++ b/app/util/sentry/__snapshots__/utils.test.ts.snap @@ -0,0 +1,232 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`captureSentryFeedback maskObject masks initial root state fixture 1`] = ` +{ + "accounts": { + "reloadAccounts": false, + }, + "alert": { + "autodismiss": null, + "content": null, + "data": null, + "isVisible": false, + }, + "bookmarks": {}, + "browser": { + "activeTab": null, + "favicons": [], + "history": [], + "tabs": [], + "visitedDappsByHostname": {}, + "whitelist": [], + }, + "collectibles": { + "favorites": {}, + "isNftFetchingProgress": false, + }, + "engine": { + "backgroundState": { + "AccountTrackerController": { + "accounts": "object", + "accountsByChainId": "object", + }, + "AccountsController": { + "internalAccounts": { + "accounts": "object", + "selectedAccount": "string", + }, + }, + "AddressBookController": { + "addressBook": "object", + }, + "ApprovalController": { + "approvalFlows": "object", + "pendingApprovalCount": "number", + "pendingApprovals": "object", + }, + "AssetsContractController": {}, + "CurrencyRateController": { + "currencyRates": { + "ETH": { + "conversionDate": 1720196397083, + "conversionRate": 298514, + "usdConversionRate": 298514, + }, + }, + "currentCurrency": "usd", + }, + "GasFeeController": { + "estimatedGasFeeTimeBounds": {}, + "gasEstimateType": "none", + "gasFeeEstimates": {}, + "gasFeeEstimatesByChainId": {}, + "nonRPCGasFeeApisDisabled": "boolean", + }, + "KeyringController": { + "isUnlocked": true, + "keyrings": "object", + "vault": "string", + }, + "LoggingController": { + "logs": "object", + }, + "NetworkController": { + "networkConfigurations": "object", + "networksMetadata": { + "mainnet": { + "EIPS": { + "1559": true, + }, + "status": "available", + }, + }, + "providerConfig": { + "chainId": "0x1", + "ticker": "ETH", + "type": "mainnet", + }, + "selectedNetworkClientId": "string", + }, + "NftController": { + "allNftContracts": "object", + "allNfts": "object", + "ignoredNfts": "object", + }, + "NftDetectionController": "object", + "PermissionController": {}, + "PreferencesController": { + "disabledRpcMethodPreferences": { + "eth_sign": false, + }, + "displayNftMedia": true, + "featureFlags": {}, + "identities": "object", + "ipfsGateway": "string", + "isIpfsGatewayEnabled": true, + "isMultiAccountBalancesEnabled": "boolean", + "lostIdentities": "object", + "securityAlertsEnabled": "boolean", + "selectedAddress": "string", + "showIncomingTransactions": "object", + "showTestNetworks": "boolean", + "smartTransactionsOptInStatus": "boolean", + "useNftDetection": true, + "useSafeChainsListValidation": "boolean", + "useTokenDetection": true, + "useTransactionSimulations": true, + }, + "SmartTransactionsController": { + "smartTransactionsState": { + "fees": {}, + "feesByChainId": "object", + "liveness": true, + "livenessByChainId": "object", + "smartTransactions": "object", + }, + }, + }, + }, + "experimentalSettings": { + "securityAlertsEnabled": true, + }, + "fiatOrders": "object", + "infuraAvailability": { + "isBlocked": false, + }, + "inpageProvider": { + "networkId": "1", + }, + "legalNotices": { + "newPrivacyPolicyToastClickedOrClosed": true, + "newPrivacyPolicyToastShownDate": null, + }, + "modals": { + "collectibleContractModalVisible": false, + "dappTransactionModalVisible": false, + "networkModalVisible": false, + "receiveAsset": undefined, + "receiveModalVisible": false, + "shouldNetworkSwitchPopToWallet": true, + "signMessageModalVisible": true, + }, + "navigation": { + "currentBottomNavRoute": "Wallet", + "currentRoute": "Login", + }, + "networkOnboarded": { + "networkOnboardedState": {}, + "networkState": { + "nativeToken": "", + "networkType": "", + "networkUrl": "", + "showNetworkOnboarding": false, + }, + "switchedNetwork": { + "networkStatus": false, + "networkUrl": "", + }, + }, + "notification": { + "notification": { + "notificationsSettings": {}, + }, + "notifications": [], + }, + "onboarding": { + "events": [], + }, + "privacy": {}, + "rpcEvents": { + "signingEvent": { + "eventStage": "idle", + "rpcName": "", + }, + }, + "sdk": { + "approvedHosts": {}, + "connections": {}, + "dappConnections": {}, + "wc2Metadata": undefined, + }, + "security": { + "allowLoginWithRememberMe": false, + "automaticSecurityChecksEnabled": false, + "dataCollectionForMarketing": null, + "hasUserSelectedAutomaticSecurityCheckOption": false, + "isAutomaticSecurityChecksModalOpen": false, + "isNFTAutoDetectionModalViewed": false, + }, + "settings": { + "basicFunctionalityEnabled": true, + "hideZeroBalanceTokens": false, + "lockTime": 30000, + "primaryCurrency": "ETH", + "searchEngine": "DuckDuckGo", + "useBlockieIcon": true, + }, + "signatureRequest": "object", + "smartTransactions": { + "optInModalAppVersionSeen": null, + }, + "swaps": "object", + "transaction": "object", + "transactionMetrics": "object", + "user": { + "ambiguousAddressEntries": "object", + "appTheme": "os", + "backUpSeedphraseVisible": false, + "gasEducationCarouselSeen": false, + "initialScreen": "", + "isAuthChecked": false, + "loadingMsg": "", + "loadingSet": false, + "passwordSet": true, + "protectWalletModalVisible": false, + "seedphraseBackedUp": true, + "userLoggedIn": true, + }, + "wizard": { + "step": 1, + }, +} +`; diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js index 598e79ea1d6..b63a7944e6c 100644 --- a/app/util/sentry/utils.js +++ b/app/util/sentry/utils.js @@ -6,6 +6,194 @@ import DefaultPreference from 'react-native-default-preference'; import { regex } from '../regex'; import { AGREED, METRICS_OPT_IN } from '../../constants/storage'; import { isTest } from '../test/utils'; +import { store } from '../../store'; + +/** + * This symbol matches all object properties when used in a mask + */ +export const AllProperties = Symbol('*'); + +// This describes the subset of background controller state attached to errors +// sent to Sentry These properties have some potential to be useful for +// debugging, and they do not contain any identifiable information. +export const sentryStateMask = { + accounts: true, + alert: true, + bookmarks: true, + browser: true, + collectibles: true, + engine: { + backgroundState: { + AccountTrackerController: { + [AllProperties]: false, + }, + AccountsController: { + internalAccounts: { + [AllProperties]: false, + }, + }, + AddressBookController: { + [AllProperties]: false, + }, + ApprovalController: { + [AllProperties]: false, + }, + AssetsContractController: {}, + CurrencyRateController: { + currencyRates: true, + currentCurrency: true, + }, + GasFeeController: { + estimatedGasFeeTimeBounds: true, + gasEstimateType: true, + gasFeeEstimates: true, + gasFeeEstimatesByChainId: true, + }, + KeyringController: { + isUnlocked: true, + }, + LoggingController: { + [AllProperties]: false, + }, + NetworkController: { + networksMetadata: true, + providerConfig: { + chainId: true, + id: true, + nickname: true, + ticker: true, + type: true, + }, + }, + NftController: { + [AllProperties]: false, + }, + PPOMController: { + storageMetadata: [], + versionInfo: [], + }, + PermissionController: { + [AllProperties]: false, + }, + PhishingController: {}, + PreferencesController: { + disabledRpcMethodPreferences: true, + featureFlags: true, + isIpfsGatewayEnabled: true, + displayNftMedia: true, + useNftDetection: true, + useTokenDetection: true, + useTransactionSimulations: true, + }, + SignatureController: { + unapprovedMsgCount: true, + unapprovedPersonalMsgCount: true, + unapprovedTypedMessagesCount: true, + }, + SmartTransactionsController: { + smartTransactionsState: { + fees: { + approvalTxFees: true, + tradeTxFees: true, + }, + liveness: true, + userOptIn: true, + userOptInV2: true, + }, + }, + SnapController: { + [AllProperties]: false, + }, + SnapInterface: { + [AllProperties]: false, + }, + SnapsRegistry: { + [AllProperties]: false, + }, + SubjectMetadataController: { + [AllProperties]: false, + }, + SwapsController: { + swapsState: { + customGasPrice: true, + customMaxFeePerGas: true, + customMaxGas: true, + customMaxPriorityFeePerGas: true, + errorKey: true, + fetchParams: true, + quotesLastFetched: true, + quotesPollingLimitEnabled: true, + routeState: true, + saveFetchedQuotes: true, + selectedAggId: true, + swapsFeatureFlags: true, + swapsFeatureIsLive: true, + swapsQuotePrefetchingRefreshTime: true, + swapsQuoteRefreshTime: true, + swapsStxBatchStatusRefreshTime: true, + swapsStxGetTransactionsRefreshTime: true, + swapsStxMaxFeeMultiplier: true, + swapsUserFeeLevel: true, + }, + }, + TokenDetectionController: { + [AllProperties]: false, + }, + TokenListController: { + preventPollingOnNetworkRestart: true, + tokensChainsCache: { + [AllProperties]: false, + }, + }, + TokenRatesController: { + [AllProperties]: false, + }, + TokensController: { + allDetectedTokens: { + [AllProperties]: false, + }, + allIgnoredTokens: { + [AllProperties]: false, + }, + allTokens: { + [AllProperties]: false, + }, + }, + TransactionController: { + [AllProperties]: false, + }, + }, + }, + experimentalSettings: true, + infuraAvailability: true, + inpageProvider: true, + legalNotices: true, + modals: true, + navigation: true, + networkOnboarded: true, + notification: true, + onboarding: true, + privacy: true, + rpcEvents: true, + sdk: true, + security: true, + settings: true, + smartTransactions: true, + user: { + appTheme: true, + backUpSeedphraseVisible: true, + gasEducationCarouselSeen: true, + initialScreen: true, + isAuthChecked: true, + loadingMsg: true, + loadingSet: true, + passwordSet: true, + protectWalletModalVisible: true, + seedphraseBackedUp: true, + userLoggedIn: true, + }, + wizard: true, +}; const METAMASK_ENVIRONMENT = process.env['METAMASK_ENVIRONMENT'] || 'local'; // eslint-disable-line dot-notation const METAMASK_BUILD_TYPE = process.env['METAMASK_BUILD_TYPE'] || 'main'; // eslint-disable-line dot-notation @@ -117,6 +305,53 @@ function removeSES(report) { } } +/** + * Return a "masked" copy of the given object. The returned object includes + * only the properties present in the mask. + * + * The mask is an object that mirrors the structure of the given object, except + * the only values are `true`, `false, a sub-mask, or the 'AllProperties" + * symbol. `true` implies the property should be included, and `false` will + * exclude it. A sub-mask implies the property should be further masked + * according to that sub-mask. The "AllProperties" symbol is used for objects + * with dynamic keys, and applies a rule (either `true`, `false`, or a + * sub-mask`) to every property in that object. + * + * If a property is excluded, its type is included instead. + * + * @param {object} objectToMask - The object to mask + * @param {{[key: string]: object | boolean}} mask - The mask to apply to the object + * @returns {object} - The masked object + */ +export function maskObject(objectToMask, mask = {}) { + if (!objectToMask) return {}; + let maskAllProperties = false; + if (Object.keys(mask).includes(AllProperties)) { + if (Object.keys(mask).length > 1) { + throw new Error('AllProperties mask key does not support sibling keys'); + } + maskAllProperties = true; + } + + return Object.keys(objectToMask).reduce((maskedObject, key) => { + const maskKey = maskAllProperties ? mask[AllProperties] : mask[key]; + const shouldPrintValue = maskKey === true; + const shouldIterateSubMask = + Boolean(maskKey) && typeof maskKey === 'object'; + const shouldPrintType = maskKey === undefined || maskKey === false; + if (shouldPrintValue) { + maskedObject[key] = objectToMask[key]; + } else if (shouldIterateSubMask) { + maskedObject[key] = maskObject(objectToMask[key], maskKey); + } else if (shouldPrintType) { + // Since typeof null is object, it is more valuable to us having the null instead of object + maskedObject[key] = + objectToMask[key] === null ? 'null' : typeof objectToMask[key]; + } + return maskedObject; + }, {}); +} + function rewriteReport(report) { try { // filter out SES from error stack trace @@ -134,6 +369,10 @@ function rewriteReport(report) { removeDeviceTimezone(report); // remove device name removeDeviceName(report); + + const appState = store?.getState(); + const maskedState = maskObject(appState, sentryStateMask); + report.contexts.appState = maskedState; } catch (err) { console.error('ENTER ERROR OF REPORT ', err); throw err; diff --git a/app/util/sentry/utils.test.ts b/app/util/sentry/utils.test.ts index 7b75edd20eb..59a49207651 100644 --- a/app/util/sentry/utils.test.ts +++ b/app/util/sentry/utils.test.ts @@ -4,7 +4,13 @@ import { deriveSentryEnvironment, excludeEvents, captureSentryFeedback, + maskObject, + sentryStateMask, + AllProperties, } from './utils'; +import { DeepPartial } from '../test/renderWithProvider'; +import { RootState } from '../../reducers'; +import { NetworkStatus } from '@metamask/network-controller'; jest.mock('@sentry/react-native', () => ({ ...jest.requireActual('@sentry/react-native'), @@ -13,7 +19,7 @@ jest.mock('@sentry/react-native', () => ({ const mockedCaptureUserFeedback = jest.mocked(captureUserFeedback); describe('deriveSentryEnvironment', () => { - test('returns production-flask for non-dev production environment and flask build type', async () => { + it('returns production-flask for non-dev production environment and flask build type', async () => { const METAMASK_ENVIRONMENT = 'production'; const METAMASK_BUILD_TYPE = 'flask'; const isDev = false; @@ -26,7 +32,7 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('production-flask'); }); - test('returns local-flask for non-dev undefined environment and flask build type', async () => { + it('returns local-flask for non-dev undefined environment and flask build type', async () => { const METAMASK_BUILD_TYPE = 'flask'; const isDev = false; @@ -34,7 +40,7 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('local-flask'); }); - test('returns debug-flask for non-dev flask environment debug build type', async () => { + it('returns debug-flask for non-dev flask environment debug build type', async () => { const METAMASK_BUILD_TYPE = 'flask'; const METAMASK_ENVIRONMENT = 'debug'; const isDev = false; @@ -47,7 +53,7 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('debug-flask'); }); - test('returns local for non-dev local environment and undefined build type', async () => { + it('returns local for non-dev local environment and undefined build type', async () => { const isDev = false; const METAMASK_ENVIRONMENT = 'local'; @@ -55,14 +61,14 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('local'); }); - test('returns local for non-dev with both undefined environment and build type', async () => { + it('returns local for non-dev with both undefined environment and build type', async () => { const isDev = false; const env = deriveSentryEnvironment(isDev); expect(env).toBe('local'); }); - test('returns production for non-dev production environment and undefined build type', async () => { + it('returns production for non-dev production environment and undefined build type', async () => { const METAMASK_ENVIRONMENT = 'production'; const isDev = false; @@ -70,14 +76,14 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('production'); }); - test('returns development for dev environment', async () => { + it('returns development for dev environment', async () => { const isDev = true; const env = deriveSentryEnvironment(isDev, '', ''); expect(env).toBe('development'); }); - test('returns development for dev environment regardless of environment and build type', async () => { + it('returns development for dev environment regardless of environment and build type', async () => { const isDev = true; const METAMASK_ENVIRONMENT = 'production'; const METAMASK_BUILD_TYPE = 'flask'; @@ -90,26 +96,26 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('development'); }); - test('return performance event Route Change', async () => { + it('returns performance event Route Change', async () => { const event = { transaction: 'Route Change' }; const eventExcluded = excludeEvents(event); expect(eventExcluded).toBe(null); }); - test('return performance event anything', async () => { + it('returns performance event anything', async () => { const event = { transaction: 'Login' }; const eventExcluded = excludeEvents(event); expect(eventExcluded).toBe(event); }); - test('return performance event null if empty', async () => { + it('returns performance event null if empty', async () => { const eventExcluded = excludeEvents(null); expect(eventExcluded).toBe(null); }); }); describe('captureSentryFeedback', () => { - it('should capture Sentry user feedback', async () => { + it('captures Sentry user feedback', async () => { const mockSentryId = '123'; const mockComments = 'Comment'; const expectedUserFeedback: UserFeedback = { @@ -126,4 +132,493 @@ describe('captureSentryFeedback', () => { expectedUserFeedback, ); }); + + describe('maskObject', () => { + const rootState: DeepPartial = { + legalNotices: { + newPrivacyPolicyToastClickedOrClosed: true, + newPrivacyPolicyToastShownDate: null, + }, + collectibles: { favorites: {}, isNftFetchingProgress: false }, + engine: { + backgroundState: { + AccountTrackerController: { + accounts: { + '0x6312c98831D74754F86dd4936668A13B7e9bA411': { + balance: '0x0', + }, + }, + accountsByChainId: { + '0x1': { + '0x6312c98831D74754F86dd4936668A13B7e9bA411': { + balance: '0x0', + }, + }, + }, + }, + AccountsController: { + internalAccounts: { + accounts: { + '1be55f5b-eba9-41a7-a9ed-a6a8274aca27': { + address: '0x6312c98831d74754f86dd4936668a13b7e9ba411', + id: '1be55f5b-eba9-41a7-a9ed-a6a8274aca27', + metadata: { + importTime: 1720023898234, + keyring: { + type: 'HD Key Tree', + }, + lastSelected: 1720023898236, + name: 'Account 1', + }, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + options: {}, + type: 'eip155:eoa', + }, + }, + selectedAccount: '1be55f5b-eba9-41a7-a9ed-a6a8274aca27', + }, + }, + AddressBookController: { + addressBook: {}, + }, + ApprovalController: { + approvalFlows: [], + pendingApprovalCount: 0, + pendingApprovals: {}, + }, + AssetsContractController: {}, + CurrencyRateController: { + currencyRates: { + ETH: { + conversionDate: 1720196397083, + conversionRate: 298514, + usdConversionRate: 298514, + }, + }, + currentCurrency: 'usd', + }, + GasFeeController: { + estimatedGasFeeTimeBounds: {}, + gasEstimateType: 'none', + gasFeeEstimates: {}, + gasFeeEstimatesByChainId: {}, + nonRPCGasFeeApisDisabled: false, + }, + KeyringController: { + isUnlocked: true, + keyrings: [ + { + accounts: ['0x6312c98831d74754f86dd4936668a13b7e9ba411'], + type: 'HD Key Tree', + }, + ], + vault: '{"cipher":""}', + }, + LoggingController: { + logs: {}, + }, + NetworkController: { + networkConfigurations: {}, + networksMetadata: { + mainnet: { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Available, + }, + }, + providerConfig: { + chainId: '0x1', + ticker: 'ETH', + type: 'mainnet', + }, + selectedNetworkClientId: 'mainnet', + }, + NftController: { + allNftContracts: {}, + allNfts: {}, + ignoredNfts: [], + }, + NftDetectionController: {}, + PermissionController: undefined, + PreferencesController: { + disabledRpcMethodPreferences: { + eth_sign: false, + }, + displayNftMedia: true, + featureFlags: {}, + identities: { + '0x6312c98831D74754F86dd4936668A13B7e9bA411': { + address: '0x6312c98831D74754F86dd4936668A13B7e9bA411', + importTime: 1720023898223, + name: 'Account 1', + }, + }, + ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', + isIpfsGatewayEnabled: true, + isMultiAccountBalancesEnabled: true, + lostIdentities: {}, + securityAlertsEnabled: true, + selectedAddress: '0x6312c98831D74754F86dd4936668A13B7e9bA411', + showIncomingTransactions: { + '0x1': true, + '0x13881': true, + '0x38': true, + '0x5': true, + '0x504': true, + '0x505': true, + '0x507': true, + '0x61': true, + '0x64': true, + '0x89': true, + '0xa': true, + '0xa869': true, + '0xa86a': true, + '0xaa36a7': true, + '0xaa37dc': true, + '0xe704': true, + '0xe705': true, + '0xe708': true, + '0xfa': true, + '0xfa2': true, + }, + showTestNetworks: false, + smartTransactionsOptInStatus: false, + useNftDetection: true, + useSafeChainsListValidation: true, + useTokenDetection: true, + useTransactionSimulations: true, + }, + SmartTransactionsController: { + smartTransactionsState: { + fees: {}, + feesByChainId: { + '0x1': {}, + '0xaa36a7': {}, + }, + liveness: true, + livenessByChainId: { + '0x1': true, + '0xaa36a7': true, + }, + smartTransactions: { + '0x1': [], + }, + }, + }, + }, + }, + privacy: {}, + bookmarks: {}, + browser: { + activeTab: null, + favicons: [], + history: [], + tabs: [], + visitedDappsByHostname: {}, + whitelist: [], + }, + modals: { + collectibleContractModalVisible: false, + dappTransactionModalVisible: false, + networkModalVisible: false, + receiveAsset: undefined, + receiveModalVisible: false, + shouldNetworkSwitchPopToWallet: true, + signMessageModalVisible: true, + }, + settings: { + basicFunctionalityEnabled: true, + hideZeroBalanceTokens: false, + lockTime: 30000, + primaryCurrency: 'ETH', + searchEngine: 'DuckDuckGo', + useBlockieIcon: true, + }, + alert: { + autodismiss: null, + content: null, + data: null, + isVisible: false, + }, + transaction: { + assetType: undefined, + ensRecipient: undefined, + id: undefined, + nonce: undefined, + paymentRequest: undefined, + proposedNonce: undefined, + readableValue: undefined, + selectedAsset: {}, + symbol: undefined, + transaction: { + data: undefined, + from: undefined, + gas: undefined, + gasPrice: undefined, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + to: undefined, + value: undefined, + }, + transactionFromName: undefined, + transactionTo: undefined, + transactionToName: undefined, + transactionValue: undefined, + type: undefined, + warningGasPriceHigh: undefined, + }, + smartTransactions: { + optInModalAppVersionSeen: null, + }, + user: { + ambiguousAddressEntries: {}, + appTheme: 'os', + backUpSeedphraseVisible: false, + gasEducationCarouselSeen: false, + initialScreen: '', + isAuthChecked: false, + loadingMsg: '', + loadingSet: false, + passwordSet: true, + protectWalletModalVisible: false, + seedphraseBackedUp: true, + userLoggedIn: true, + }, + wizard: { + step: 1, + }, + onboarding: { + events: [], + }, + notification: { + notification: { + notificationsSettings: {}, + }, + notifications: [], + }, + swaps: { + '0x1': { + isLive: true, + }, + featureFlags: undefined, + hasOnboarded: true, + isLive: true, + }, + fiatOrders: { + activationKeys: [], + authenticationUrls: [], + customOrderIds: [], + getStartedAgg: false, + getStartedSell: false, + networks: [], + orders: [], + selectedPaymentMethodAgg: null, + selectedRegionAgg: null, + }, + infuraAvailability: { + isBlocked: false, + }, + navigation: { + currentBottomNavRoute: 'Wallet', + currentRoute: 'Login', + }, + networkOnboarded: { + networkOnboardedState: {}, + networkState: { + nativeToken: '', + networkType: '', + networkUrl: '', + showNetworkOnboarding: false, + }, + switchedNetwork: { + networkStatus: false, + networkUrl: '', + }, + }, + security: { + allowLoginWithRememberMe: false, + automaticSecurityChecksEnabled: false, + dataCollectionForMarketing: null, + hasUserSelectedAutomaticSecurityCheckOption: false, + isAutomaticSecurityChecksModalOpen: false, + isNFTAutoDetectionModalViewed: false, + }, + signatureRequest: { + securityAlertResponse: undefined, + }, + sdk: { + connections: {}, + approvedHosts: {}, + dappConnections: {}, + wc2Metadata: undefined, + }, + experimentalSettings: { + securityAlertsEnabled: true, + }, + rpcEvents: { + signingEvent: { + eventStage: 'idle', + rpcName: '', + }, + }, + accounts: { + reloadAccounts: false, + }, + inpageProvider: { + networkId: '1', + }, + transactionMetrics: { + metricsByTransactionId: {}, + }, + }; + + it('masks initial root state fixture', () => { + const maskedState = maskObject(rootState, sentryStateMask); + + expect(maskedState).toMatchSnapshot(); + }); + it('handles undefined mask', () => { + const maskedState = maskObject(rootState, undefined); + expect(maskedState).toEqual({ + accounts: 'object', + alert: 'object', + bookmarks: 'object', + browser: 'object', + collectibles: 'object', + engine: 'object', + experimentalSettings: 'object', + fiatOrders: 'object', + infuraAvailability: 'object', + inpageProvider: 'object', + legalNotices: 'object', + modals: 'object', + navigation: 'object', + networkOnboarded: 'object', + notification: 'object', + onboarding: 'object', + privacy: 'object', + rpcEvents: 'object', + sdk: 'object', + security: 'object', + settings: 'object', + signatureRequest: 'object', + smartTransactions: 'object', + swaps: 'object', + transaction: 'object', + transactionMetrics: 'object', + user: 'object', + wizard: 'object', + }); + }); + it('handles empty rootState', () => { + const maskedState = maskObject({}, sentryStateMask); + expect(maskedState).toEqual({}); + }); + it('handles rootState with more keys than what is defined in the mask', () => { + const maskedState = maskObject(rootState, { legalNotices: true }); + expect(maskedState).toEqual({ + legalNotices: { + newPrivacyPolicyToastClickedOrClosed: true, + newPrivacyPolicyToastShownDate: null, + }, + accounts: 'object', + alert: 'object', + bookmarks: 'object', + browser: 'object', + collectibles: 'object', + engine: 'object', + experimentalSettings: 'object', + fiatOrders: 'object', + infuraAvailability: 'object', + inpageProvider: 'object', + modals: 'object', + navigation: 'object', + networkOnboarded: 'object', + notification: 'object', + onboarding: 'object', + privacy: 'object', + rpcEvents: 'object', + sdk: 'object', + security: 'object', + settings: 'object', + signatureRequest: 'object', + smartTransactions: 'object', + swaps: 'object', + transaction: 'object', + transactionMetrics: 'object', + user: 'object', + wizard: 'object', + }); + }); + it('handles submask with { [AllProperties]: false, enabled: true }', () => { + const submask = { + [AllProperties]: false, + enabled: true, + }; + const maskedState = maskObject(rootState, submask); + expect(maskedState).toEqual({ + accounts: 'object', + alert: 'object', + bookmarks: 'object', + browser: 'object', + collectibles: 'object', + engine: 'object', + experimentalSettings: 'object', + fiatOrders: 'object', + infuraAvailability: 'object', + inpageProvider: 'object', + legalNotices: 'object', + modals: 'object', + navigation: 'object', + networkOnboarded: 'object', + notification: 'object', + onboarding: 'object', + privacy: 'object', + rpcEvents: 'object', + sdk: 'object', + security: 'object', + settings: 'object', + signatureRequest: 'object', + smartTransactions: 'object', + swaps: 'object', + transaction: 'object', + transactionMetrics: 'object', + user: 'object', + wizard: 'object', + }); + }); + }); + + it('handle root state with value null and mask false', () => { + const submask = { + SnapsController: { + [AllProperties]: false, + }, + }; + const maskedState = maskObject( + { + SnapsController: { + enabled: undefined, + data: null, + exampleObj: {}, + }, + }, + submask, + ); + expect(maskedState).toEqual({ + SnapsController: { + enabled: 'undefined', + data: 'null', + exampleObj: 'object', + }, + }); + }); });