diff --git a/.changeset/twenty-tools-yell.md b/.changeset/twenty-tools-yell.md new file mode 100644 index 00000000..36f20344 --- /dev/null +++ b/.changeset/twenty-tools-yell.md @@ -0,0 +1,5 @@ +--- +'@stacks/wallet-web': minor +--- + +Add a new state that asks users for permission to record application diagnostics diff --git a/.github/workflows/build-extension.yml b/.github/workflows/build-extension.yml index 9e1cf189..9d6a2046 100644 --- a/.github/workflows/build-extension.yml +++ b/.github/workflows/build-extension.yml @@ -1,6 +1,9 @@ name: Build beta extensions on: [pull_request, workflow_dispatch] +env: + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + jobs: pre_run: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-extensions.yml b/.github/workflows/publish-extensions.yml index 52debfaf..c697f1d6 100644 --- a/.github/workflows/publish-extensions.yml +++ b/.github/workflows/publish-extensions.yml @@ -5,6 +5,9 @@ on: - 'v*' workflow_dispatch: +env: + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + jobs: pre_run: runs-on: ubuntu-latest diff --git a/package.json b/package.json index f785e933..3c5453a8 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "@reach/utils": "0.15.3", "@reach/visually-hidden": "0.15.2", "@rehooks/document-title": "1.0.2", + "@sentry/react": "6.12.0", + "@sentry/tracing": "6.12.0", "@stacks/auth": "2.0.0-beta.1", "@stacks/blockchain-api-client": "0.65.0", "@stacks/common": "2.0.0-beta.0", diff --git a/scripts/generate-manifest.js b/scripts/generate-manifest.js index b7e862b1..5e9aba79 100644 --- a/scripts/generate-manifest.js +++ b/scripts/generate-manifest.js @@ -60,7 +60,7 @@ const devManifest = { const prodManifest = { name: 'Hiro Wallet', content_security_policy: - "script-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';", + "default-src 'none'; connect-src *; style-src 'unsafe-inline'; script-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';", icons: { 128: 'assets/connect-logo/Stacks128w.png', 256: 'assets/connect-logo/Stacks256w.png', diff --git a/src/background/background.ts b/src/background/background.ts index 71ac8073..d2d045f0 100755 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -5,6 +5,8 @@ needed and unloaded when it goes idle. https://developer.chrome.com/docs/extensions/mv3/architecture-overview/#background_script */ +import * as Sentry from '@sentry/react'; + import { storePayload, StorageKey } from '@common/storage'; import { ScreenPaths } from '@common/types'; import { @@ -17,9 +19,12 @@ import type { VaultActions } from '@background/vault-types'; import { popupCenter } from '@background/popup'; import { vaultMessageHandler } from '@background/vault'; import { initContextMenuActions } from '@background/init-context-menus'; +import { initSentry } from '@common/sentry-init'; const IS_TEST_ENV = process.env.TEST_ENV === 'true'; +initSentry(); + initContextMenuActions(); // Playwright does not currently support Chrome extension popup testing: @@ -31,68 +36,74 @@ async function openRequestInFullPage(path: string, urlParams: URLSearchParams) { } // Listen for install event -chrome.runtime.onInstalled.addListener(async details => { - if (details.reason === 'install' && !IS_TEST_ENV) { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`index.html#${ScreenPaths.INSTALLED}`), - }); - } +chrome.runtime.onInstalled.addListener(details => { + Sentry.wrap(async () => { + if (details.reason === 'install' && !IS_TEST_ENV) { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`index.html#${ScreenPaths.INSTALLED}`), + }); + } + }); }); // Listen for connection to the content-script - port for two-way communication -chrome.runtime.onConnect.addListener(port => { - // Listen for auth and transaction events - if (port.name === CONTENT_SCRIPT_PORT) { - port.onMessage.addListener(async (message: MessageFromContentScript, port) => { - const { payload } = message; - switch (message.method) { - case ExternalMethods.authenticationRequest: { - void storePayload({ - payload, - storageKey: StorageKey.authenticationRequests, - port, - }); - const path = ScreenPaths.GENERATION; - const urlParams = new URLSearchParams(); - urlParams.set('authRequest', payload); - if (IS_TEST_ENV) { - await openRequestInFullPage(path, urlParams); - } else { - popupCenter({ url: `/popup.html#${path}?${urlParams.toString()}` }); +chrome.runtime.onConnect.addListener(port => + Sentry.wrap(() => { + // Listen for auth and transaction events + if (port.name === CONTENT_SCRIPT_PORT) { + port.onMessage.addListener(async (message: MessageFromContentScript, port) => { + const { payload } = message; + switch (message.method) { + case ExternalMethods.authenticationRequest: { + void storePayload({ + payload, + storageKey: StorageKey.authenticationRequests, + port, + }); + const path = ScreenPaths.GENERATION; + const urlParams = new URLSearchParams(); + urlParams.set('authRequest', payload); + if (IS_TEST_ENV) { + await openRequestInFullPage(path, urlParams); + } else { + popupCenter({ url: `/popup.html#${path}?${urlParams.toString()}` }); + } + break; } - break; - } - case ExternalMethods.transactionRequest: { - void storePayload({ - payload, - storageKey: StorageKey.transactionRequests, - port, - }); - const path = ScreenPaths.TRANSACTION_POPUP; - const urlParams = new URLSearchParams(); - urlParams.set('request', payload); - if (IS_TEST_ENV) { - await openRequestInFullPage(path, urlParams); - } else { - popupCenter({ url: `/popup.html#${path}?${urlParams.toString()}` }); + case ExternalMethods.transactionRequest: { + void storePayload({ + payload, + storageKey: StorageKey.transactionRequests, + port, + }); + const path = ScreenPaths.TRANSACTION_POPUP; + const urlParams = new URLSearchParams(); + urlParams.set('request', payload); + if (IS_TEST_ENV) { + await openRequestInFullPage(path, urlParams); + } else { + popupCenter({ url: `/popup.html#${path}?${urlParams.toString()}` }); + } + break; } - break; + default: + break; } - default: - break; - } - }); - } -}); + }); + } + }) +); // Listen for events triggered by the background memory vault -chrome.runtime.onMessage.addListener((message: VaultActions, sender, sendResponse) => { - // Only respond to internal messages from our UI, not content scripts in other applications - if (!sender.url?.startsWith(chrome.runtime.getURL(''))) return; - void vaultMessageHandler(message).then(sendResponse).catch(sendResponse); - // Return true to specify that we are responding async - return true; -}); +chrome.runtime.onMessage.addListener((message: VaultActions, sender, sendResponse) => + Sentry.wrap(() => { + // Only respond to internal messages from our UI, not content scripts in other applications + if (!sender.url?.startsWith(chrome.runtime.getURL(''))) return; + void vaultMessageHandler(message).then(sendResponse).catch(sendResponse); + // Return true to specify that we are responding async + return true; + }) +); if (IS_TEST_ENV) { // Expose a helper function to open a new tab with the wallet from tests diff --git a/src/common/hooks/use-diagnostic-permission-prompt.ts b/src/common/hooks/use-diagnostic-permission-prompt.ts new file mode 100644 index 00000000..e761a9bc --- /dev/null +++ b/src/common/hooks/use-diagnostic-permission-prompt.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; + +import { ScreenPaths } from '@common/types'; +import { IS_TEST_ENV } from '@common/constants'; +import { userHasAllowedDiagnosticsKey } from '@store/onboarding/onboarding.hooks'; + +import { useChangeScreen } from './use-change-screen'; + +export function usePromptUserToSetDiagnosticPermissions() { + const changeScreen = useChangeScreen(); + + useEffect(() => { + if (IS_TEST_ENV) return; + const persistedUserDiagnosticDecision = localStorage.getItem(userHasAllowedDiagnosticsKey); + if (persistedUserDiagnosticDecision === null) changeScreen(ScreenPaths.REQUEST_DIAGNOSTICS); + }, [changeScreen]); +} diff --git a/src/common/sentry-init.ts b/src/common/sentry-init.ts new file mode 100644 index 00000000..0381e651 --- /dev/null +++ b/src/common/sentry-init.ts @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/react'; +import { Integrations } from '@sentry/tracing'; + +import { userHasAllowedDiagnosticsKey } from '@store/onboarding/onboarding.hooks'; + +function checkUserHasGrantedPermission() { + return localStorage.getItem(userHasAllowedDiagnosticsKey) === 'true'; +} + +export function initSentry() { + Sentry.init({ + dsn: process.env.SENTRY_DSN, + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 1.0, + enabled: checkUserHasGrantedPermission(), + beforeSend(event) { + if (!checkUserHasGrantedPermission()) return null; + return event; + }, + }); +} diff --git a/src/common/types.ts b/src/common/types.ts index fcb57a55..e2487751 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -40,6 +40,7 @@ export enum ScreenPaths { POPUP_SEND = '/send', POPUP_RECEIVE = '/receive', ADD_NETWORK = '/add-network', + REQUEST_DIAGNOSTICS = '/request-diagnostics', EDIT_POST_CONDITIONS = '/transaction/post-conditions', TRANSACTION_POPUP = '/transaction', } diff --git a/src/index.tsx b/src/index.tsx index 37789449..aa109982 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './app'; + import { persistAndRenderApp } from '@common/persistence'; +import { initSentry } from '@common/sentry-init'; + +initSentry(); function renderApp() { ReactDOM.render(, document.getElementById('actions-root')); diff --git a/src/pages/allow-diagnostics/allow-diagnostics-layout.tsx b/src/pages/allow-diagnostics/allow-diagnostics-layout.tsx new file mode 100644 index 00000000..0bd49f15 --- /dev/null +++ b/src/pages/allow-diagnostics/allow-diagnostics-layout.tsx @@ -0,0 +1,65 @@ +import React, { FC } from 'react'; + +import { Body } from '@components/typography'; +import { Box, Button, Flex, color, Stack } from '@stacks/ui'; +import { BaseDrawer } from '@components/drawer'; +import { FiCheck } from 'react-icons/fi'; + +interface ReasonToAllowDiagnosticsProps { + text: string; +} +const ReasonToAllowDiagnostics: FC = ({ text }) => { + return ( + + + + + {text} + + ); +}; + +interface AllowDiagnosticsLayoutProps { + onUserAllowDiagnostics(): void; + onUserDenyDiagnosticsPermissions(): void; +} +export const AllowDiagnosticsLayout: FC = props => { + const { onUserAllowDiagnostics, onUserDenyDiagnosticsPermissions } = props; + + const title = 'Help us improve'; + + return ( + onUserDenyDiagnosticsPermissions()}> + + + Hiro would like to gather anonymous data to help improve the experience of using Stacks + apps and the wallet. + + + + + + + + + + + + + ); +}; diff --git a/src/pages/allow-diagnostics/allow-diagnostics.tsx b/src/pages/allow-diagnostics/allow-diagnostics.tsx new file mode 100644 index 00000000..698bef92 --- /dev/null +++ b/src/pages/allow-diagnostics/allow-diagnostics.tsx @@ -0,0 +1,29 @@ +import React, { useCallback } from 'react'; + +import { ScreenPaths } from '@common/types'; +import { useChangeScreen } from '@common/hooks/use-change-screen'; +import { useHasAllowedDiagnostics } from '@store/onboarding/onboarding.hooks'; + +import { AllowDiagnosticsLayout } from './allow-diagnostics-layout'; +import { initSentry } from '@common/sentry-init'; + +export const AllowDiagnosticsDrawer = () => { + const changeScreen = useChangeScreen(); + const [, setHasAllowedDiagnostics] = useHasAllowedDiagnostics(); + + const goHomeAndSetDiagnosticsPermissionTo = useCallback( + (areDiagnosticsAllowed: boolean) => { + changeScreen(ScreenPaths.HOME); + setHasAllowedDiagnostics(areDiagnosticsAllowed); + if (areDiagnosticsAllowed) initSentry(); + }, + [changeScreen, setHasAllowedDiagnostics] + ); + + return ( + goHomeAndSetDiagnosticsPermissionTo(false)} + onUserAllowDiagnostics={() => goHomeAndSetDiagnosticsPermissionTo(true)} + /> + ); +}; diff --git a/src/pages/home/home.tsx b/src/pages/home/home.tsx index 82cd49bb..e5707b19 100644 --- a/src/pages/home/home.tsx +++ b/src/pages/home/home.tsx @@ -8,7 +8,11 @@ import { UserAccount } from '@pages/home/components/user-area'; import { HomeActions } from '@pages/home/components/actions'; import { HiroMessages } from '@features/hiro-messages/hiro-messages'; +import { usePromptUserToSetDiagnosticPermissions } from '@common/hooks/use-diagnostic-permission-prompt'; + export const PopupHome = () => { + usePromptUserToSetDiagnosticPermissions(); + return ( <> = props => { const { onUserDeleteWallet, onUserSafelyReturnToHomepage } = props; diff --git a/src/routes.tsx b/src/routes.tsx index 68b56b1a..19ba49b0 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -27,6 +27,7 @@ import { Unlock } from '@pages/unlock'; import { PopupHome } from '@pages/home/home'; import { useUpdateLastSeenStore } from '@store/wallet/wallet.hooks'; import { SignOutConfirmDrawer } from '@pages/sign-out-confirm/sign-out-confirm'; +import { AllowDiagnosticsDrawer } from '@pages/allow-diagnostics/allow-diagnostics'; interface RouteProps { path: ScreenPaths; @@ -90,6 +91,7 @@ export const Routes: React.FC = () => { } /> + } /> {/* Installation */} } /> diff --git a/src/store/onboarding/onboarding.hooks.ts b/src/store/onboarding/onboarding.hooks.ts index c6a16bbf..32e4bd5b 100644 --- a/src/store/onboarding/onboarding.hooks.ts +++ b/src/store/onboarding/onboarding.hooks.ts @@ -3,6 +3,7 @@ import { useAtomValue, useUpdateAtom } from 'jotai/utils'; import { authRequestState, currentScreenState, + hasAllowedDiagnosticsState, magicRecoveryCodePasswordState, magicRecoveryCodeState, onboardingPathState, @@ -10,9 +11,12 @@ import { secretKeyState, seedInputErrorState, seedInputState, + userHasAllowedDiagnosticsKey, usernameState, } from './onboarding'; +export { userHasAllowedDiagnosticsKey }; + export function useAuthRequest() { return useAtomValue(authRequestState); } @@ -64,3 +68,7 @@ export function useUsernameState() { export function useOnboardingPathState() { return useAtomValue(onboardingPathState); } + +export function useHasAllowedDiagnostics() { + return useAtom(hasAllowedDiagnosticsState); +} diff --git a/src/store/onboarding/onboarding.ts b/src/store/onboarding/onboarding.ts index 51830954..ecb0d444 100644 --- a/src/store/onboarding/onboarding.ts +++ b/src/store/onboarding/onboarding.ts @@ -1,5 +1,5 @@ import { atom } from 'jotai'; -import { atomWithDefault } from 'jotai/utils'; +import { atomWithDefault, atomWithStorage } from 'jotai/utils'; import { ScreenPaths } from '@common/types'; import { DecodedAuthRequest } from '@common/dev/types'; @@ -43,6 +43,10 @@ export const authRequestState = atom({ appURL: undefined, }); +export const userHasAllowedDiagnosticsKey = 'stacks-wallet-has-allowed-diagnostics'; + +export const hasAllowedDiagnosticsState = atomWithStorage(userHasAllowedDiagnosticsKey, false); + magicRecoveryCodePasswordState.debugLabel = 'magicRecoveryCodePasswordState'; seedInputState.debugLabel = 'seedInputState'; seedInputErrorState.debugLabel = 'seedInputErrorState'; diff --git a/webpack/webpack.config.base.js b/webpack/webpack.config.base.js index 178b6ce1..0f74686d 100755 --- a/webpack/webpack.config.base.js +++ b/webpack/webpack.config.base.js @@ -199,6 +199,11 @@ const config = { 'process.env.USERNAMES_ENABLED': JSON.stringify(process.env.USERNAMES_ENABLED || 'false'), 'process.env.TEST_ENV': JSON.stringify(TEST_ENV ? 'true' : 'false'), }), + + new webpack.EnvironmentPlugin({ + SENTRY_DSN: process.env.SENTRY_DSN ?? '', + }), + new webpack.ProvidePlugin({ process: 'process/browser.js', Buffer: ['buffer', 'Buffer'], diff --git a/yarn.lock b/yarn.lock index 38d11a5f..1998f2a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2840,6 +2840,81 @@ resolved "https://registry.yarnpkg.com/@schemastore/web-manifest/-/web-manifest-0.0.5.tgz#97f0b1f14d095189c5672309e4975760278461b2" integrity sha512-3SF3OwzJ+PIqYDVW0MXoUAyypyx7N5RlYj2zek36qVuDUgoiI65q0ietwuxyVtbTRYJyP64KBGKvKqHzbIxdfA== +"@sentry/browser@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.12.0.tgz#970cd68fa117a1e1336fdb373e3b1fa76cd63e2d" + integrity sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ== + dependencies: + "@sentry/core" "6.12.0" + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/core@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.12.0.tgz#bc7c5f0785b6a392d9ad47bd9b1fae3f5389996c" + integrity sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ== + dependencies: + "@sentry/hub" "6.12.0" + "@sentry/minimal" "6.12.0" + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/hub@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.12.0.tgz#29e323ab6a95e178fb14fffb684aa0e09707197f" + integrity sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg== + dependencies: + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/minimal@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.12.0.tgz#cbe20e95056cedb9709d7d5b2119ef95206a9f8c" + integrity sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw== + dependencies: + "@sentry/hub" "6.12.0" + "@sentry/types" "6.12.0" + tslib "^1.9.3" + +"@sentry/react@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.12.0.tgz#8ae2680d226fafb0da0f3d8366bb285004ba6c2e" + integrity sha512-E8Nw9PPzP/EyMy64ksr9xcyYYlBmUA5ROnkPQp7o5wF0xf5/J+nMS1tQdyPnLQe2KUgHlN4kVs2HHft1m7mSYQ== + dependencies: + "@sentry/browser" "6.12.0" + "@sentry/minimal" "6.12.0" + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + hoist-non-react-statics "^3.3.2" + tslib "^1.9.3" + +"@sentry/tracing@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.12.0.tgz#a05c8985ee7fed7310b029b147d8f9f14f2a2e67" + integrity sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA== + dependencies: + "@sentry/hub" "6.12.0" + "@sentry/minimal" "6.12.0" + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/types@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.12.0.tgz#b7395688a79403c6df8d8bb8d81deb8222519853" + integrity sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA== + +"@sentry/utils@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.12.0.tgz#3de261e8d11bdfdc7add64a3065d43517802e975" + integrity sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA== + dependencies: + "@sentry/types" "6.12.0" + tslib "^1.9.3" + "@sideway/address@^4.1.0": version "4.1.2" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1" @@ -9364,7 +9439,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -15328,7 +15403,7 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==