diff --git a/nym-connect/desktop/package.json b/nym-connect/desktop/package.json index 299d162628..60ea3b4830 100644 --- a/nym-connect/desktop/package.json +++ b/nym-connect/desktop/package.json @@ -28,11 +28,13 @@ "@emotion/styled": "^11.6.0", "@hookform/resolvers": "^2.8.0", "@mui/icons-material": "^5.2.0", + "@mui/lab": "^5.0.0-alpha.72", "@mui/material": "^5.2.2", "@mui/styles": "^5.2.2", "@mui/system": ">= 5", - "@mui/lab": "^5.0.0-alpha.72", "@nymproject/react": "^1.0.0", + "@sentry/integrations": "^7.54.0", + "@sentry/react": "^7.54.0", "@tauri-apps/api": "^1.2.0", "@tauri-apps/tauri-forage": "^1.0.0-beta.2", "clsx": "^1.1.1", diff --git a/nym-connect/desktop/src-tauri/src/main.rs b/nym-connect/desktop/src-tauri/src/main.rs index b1a599c41e..aef1f08fae 100644 --- a/nym-connect/desktop/src-tauri/src/main.rs +++ b/nym-connect/desktop/src-tauri/src/main.rs @@ -40,6 +40,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ crate::operations::config::get_config_file_location, crate::operations::config::get_config_id, + crate::operations::common::get_env, crate::operations::connection::connect::get_gateway, crate::operations::connection::connect::get_service_provider, crate::operations::connection::connect::set_gateway, diff --git a/nym-connect/desktop/src-tauri/src/operations/common.rs b/nym-connect/desktop/src-tauri/src/operations/common.rs new file mode 100644 index 0000000000..707dd19860 --- /dev/null +++ b/nym-connect/desktop/src-tauri/src/operations/common.rs @@ -0,0 +1,9 @@ +use std::env; + +#[tauri::command] +pub async fn get_env(variable: String) -> Option { + let var = env::var(&variable).ok(); + log::trace!("get_env {variable} {:?}", var); + + var +} diff --git a/nym-connect/desktop/src-tauri/src/operations/mod.rs b/nym-connect/desktop/src-tauri/src/operations/mod.rs index 87e01e79da..977dcc5110 100644 --- a/nym-connect/desktop/src-tauri/src/operations/mod.rs +++ b/nym-connect/desktop/src-tauri/src/operations/mod.rs @@ -1,3 +1,4 @@ +pub mod common; pub mod config; pub mod connection; pub mod directory; diff --git a/nym-connect/desktop/src/context/main.tsx b/nym-connect/desktop/src/context/main.tsx index b999b957a2..f1dbe5948a 100644 --- a/nym-connect/desktop/src/context/main.tsx +++ b/nym-connect/desktop/src/context/main.tsx @@ -9,9 +9,11 @@ import { getItemFromStorage, setItemInStorage } from 'src/utils'; import { ConnectionStatusKind, GatewayPerformance } from '../types'; import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ServiceProvider } from '../types/directory'; +import initSentry from '../sentry'; const FORAGE_GATEWAY_KEY = 'nym-connect-user-gateway'; const FORAGE_SP_KEY = 'nym-connect-user-sp'; +const FORAGE_MONITORING_ENABLED = 'nym-connect-monitoring-enabled'; type ModeType = 'light' | 'dark'; @@ -30,6 +32,7 @@ export type TClientContext = { serviceProviders?: ServiceProvider[]; setMode: (mode: ModeType) => void; clearError: () => void; + monitoringEnabled: boolean; setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void; setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void; setConnectedSince: (connectedSince: DateTime | undefined) => void; @@ -39,6 +42,7 @@ export type TClientContext = { startDisconnecting: () => Promise; setUserDefinedGateway: React.Dispatch>; setUserDefinedSPAddress: React.Dispatch>; + setMonitoring: (value: boolean) => Promise; }; export const ClientContext = createContext({} as TClientContext); @@ -62,12 +66,25 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { isActive: false, address: undefined, }); + const [monitoringEnabled, setMonitoringEnabled] = useState(false); const getAppVersion = async () => { const version = await getVersion(); return version; }; + useEffect(() => { + const initSentryClient = async () => { + const monitoring = await getItemFromStorage({ key: FORAGE_MONITORING_ENABLED }); + setMonitoringEnabled(Boolean(monitoring)); + if (monitoring === true) { + await initSentry(); + } + }; + + initSentryClient(); + }, []); + useEffect(() => { setItemInStorage({ key: FORAGE_GATEWAY_KEY, value: userDefinedGateway }); }, [userDefinedGateway]); @@ -85,8 +102,12 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { setAppVersion(AppVersion); setServiceProviders(services as ServiceProvider[]); - if (storedUserDefinedGateway) setUserDefinedGateway(storedUserDefinedGateway); - if (storedUserDefinedSP) setUserDefinedSPAddress(storedUserDefinedSP); + if (storedUserDefinedGateway) { + setUserDefinedGateway(storedUserDefinedGateway); + } + if (storedUserDefinedSP) { + setUserDefinedSPAddress(storedUserDefinedSP); + } }; useEvents({ @@ -162,6 +183,11 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { const clearError = () => setError(undefined); + const setMonitoring = async (value: boolean) => { + setMonitoringEnabled(value); + await setItemInStorage({ key: FORAGE_MONITORING_ENABLED, value }); + }; + const contextValue = useMemo( () => ({ mode, @@ -177,6 +203,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { selectedProvider, serviceProviders, connectedSince, + monitoringEnabled, setConnectedSince, setSerivceProvider, startConnecting, @@ -187,6 +214,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { userDefinedGateway, setUserDefinedGateway, setUserDefinedSPAddress, + setMonitoring, }), [ mode, @@ -202,6 +230,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { selectedProvider, userDefinedGateway, userDefinedSPAddress, + monitoringEnabled, ], ); diff --git a/nym-connect/desktop/src/context/mocks/main.tsx b/nym-connect/desktop/src/context/mocks/main.tsx index 78028b03f8..8c602596eb 100644 --- a/nym-connect/desktop/src/context/mocks/main.tsx +++ b/nym-connect/desktop/src/context/mocks/main.tsx @@ -11,6 +11,7 @@ const mockValues: TClientContext = { showInfoModal: false, userDefinedGateway: { isActive: false, gateway: '' }, userDefinedSPAddress: { isActive: false, address: '' }, + monitoringEnabled: false, setShowInfoModal: () => {}, setMode: () => {}, clearError: () => {}, @@ -22,6 +23,7 @@ const mockValues: TClientContext = { setSerivceProvider: () => {}, setUserDefinedGateway: () => {}, setUserDefinedSPAddress: () => {}, + setMonitoring: async () => {}, }; export const MockProvider: FCWithChildren<{ diff --git a/nym-connect/desktop/src/pages/menu/settings/MonitoringSettings.tsx b/nym-connect/desktop/src/pages/menu/settings/MonitoringSettings.tsx new file mode 100644 index 0000000000..0bc23a55bd --- /dev/null +++ b/nym-connect/desktop/src/pages/menu/settings/MonitoringSettings.tsx @@ -0,0 +1,52 @@ +import React, { ChangeEvent, useState } from 'react'; +import { Warning as WarningIcon } from '@mui/icons-material'; +import { Box, FormControl, FormControlLabel, FormHelperText, Stack, Switch, Typography } from '@mui/material'; +import { useClientContext } from 'src/context/main'; + +export const MonitoringSettings = () => { + const { monitoringEnabled, setMonitoring } = useClientContext(); + const [enabled, setEnabled] = useState(monitoringEnabled); + const [loading, setLoading] = useState(false); + + const handleChange = async (e: ChangeEvent) => { + setLoading(true); + setEnabled(e.target.checked); + await setMonitoring(e.target.checked); + setLoading(false); + }; + + return ( + + + + + Error reporting and performance monitoring + + + + } + label="Enable" + /> + + Help Nym developers to fix errors, crashes and improve the application by enabling this option. If errors + occur or if the app crashes, it will automatically send a report. Also it tracks various performance + metrics. We use sentry.io service to handle this. + + + Note: A report can include your external IP, this can be useful to catch issues related to IP location. + All recorded data is used by Nym developers and for app development purposes only. + + + + + + You must restart the application for the change to take effect. + + + + + + ); +}; diff --git a/nym-connect/desktop/src/pages/menu/settings/index.tsx b/nym-connect/desktop/src/pages/menu/settings/index.tsx index bc58b69f1b..fc7bd030cd 100644 --- a/nym-connect/desktop/src/pages/menu/settings/index.tsx +++ b/nym-connect/desktop/src/pages/menu/settings/index.tsx @@ -7,6 +7,7 @@ import { toggleLogViewer } from 'src/utils'; const menuSchema = [ { title: 'Select your gateway', path: 'gateway' }, { title: 'Select a service provider', path: 'service-provider' }, + { title: 'Help us improve the app', path: 'monitoring' }, ]; export const SettingsMenu = () => ( diff --git a/nym-connect/desktop/src/routes/index.tsx b/nym-connect/desktop/src/routes/index.tsx index dd3eec678e..e490fec83a 100644 --- a/nym-connect/desktop/src/routes/index.tsx +++ b/nym-connect/desktop/src/routes/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Routes, Route } from 'react-router-dom'; +import * as Sentry from '@sentry/react'; import { ConnectionPage } from 'src/pages/connection'; import { Menu } from 'src/pages/menu'; import { CompatibleApps } from 'src/pages/menu/Apps'; @@ -7,19 +8,30 @@ import { HelpGuide } from 'src/pages/menu/Guide'; import { SettingsMenu } from 'src/pages/menu/settings'; import { GatewaySettings } from 'src/pages/menu/settings/GatewaySettings'; import { ServiceProviderSettings } from 'src/pages/menu/settings/ServiceProviderSettings'; +import { MonitoringSettings } from '../pages/menu/settings/MonitoringSettings'; +import { useClientContext } from '../context/main'; -export const AppRoutes = () => ( - - } /> - - } /> - } /> - } /> - - } /> - } /> - } /> +const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); + +export const AppRoutes = () => { + const { monitoringEnabled } = useClientContext(); + + const RoutesContainer = monitoringEnabled ? SentryRoutes : Routes; + + return ( + + } /> + + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + - - -); + + ); +}; diff --git a/nym-connect/desktop/src/sentry.ts b/nym-connect/desktop/src/sentry.ts new file mode 100644 index 0000000000..835131c2ec --- /dev/null +++ b/nym-connect/desktop/src/sentry.ts @@ -0,0 +1,48 @@ +import React from 'react'; +import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from 'react-router-dom'; +import * as Sentry from '@sentry/react'; +import { CaptureConsole } from '@sentry/integrations'; +import { getVersion } from '@tauri-apps/api/app'; + +const SENTRY_DSN = 'https://625e2658da4945a7a253f3ee04413a31@o967446.ingest.sentry.io/4505306292289536'; + +async function initSentry() { + console.log('⚠ performance monitoring and error reporting enabled'); + console.log('initializing sentry'); + + Sentry.init({ + dsn: SENTRY_DSN, + integrations: [ + new Sentry.BrowserTracing({ + // Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled + tracePropagationTargets: ['localhost'], + routingInstrumentation: Sentry.reactRouterV6Instrumentation( + React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + ), + }), + new Sentry.Replay(), + // captures Console API calls + new CaptureConsole({ levels: ['error', 'warn'] }), + ], + + // TODO adjust this in the future, 100% is not recommended for production + tracesSampleRate: 1.0, + + // Capture Replay for 10% of all sessions, + // plus for 100% of sessions with an error + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + + environment: process.env.NODE_ENV, + }); + + getVersion().then((version) => { + Sentry.setTag('app_version', version); + }); +} + +export default initSentry; diff --git a/nym-connect/native/android/app/build.gradle b/nym-connect/native/android/app/build.gradle index 63e9a51fe1..1afc11b2be 100644 --- a/nym-connect/native/android/app/build.gradle +++ b/nym-connect/native/android/app/build.gradle @@ -121,4 +121,4 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' implementation 'com.github.kittinunf.fuel:fuel:3.0.0-alpha1' -} \ No newline at end of file +} diff --git a/nym-connect/native/android/app/src/main/AndroidManifest.xml b/nym-connect/native/android/app/src/main/AndroidManifest.xml index 311aa8bd42..6321a2ce8c 100644 --- a/nym-connect/native/android/app/src/main/AndroidManifest.xml +++ b/nym-connect/native/android/app/src/main/AndroidManifest.xml @@ -36,4 +36,4 @@ tools:node="remove" /> - \ No newline at end of file + diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 8f410b2eb8..155d677f5b 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -122,4 +122,4 @@ "webpack-favicons": "^1.3.8", "webpack-merge": "^5.8.0" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index f8b88fd2bf..8479008ac8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3042,6 +3042,80 @@ resolved "https://registry.yarnpkg.com/@sapphire/utilities/-/utilities-3.11.1.tgz#75d068fe473d9cf8483156ae731bce92fa39a2ef" integrity sha512-SO7WvlPc0QEB+bCT6Z/NGpYTpOtYAJtxpxKV4YU+agEGlMZ5hM8J/xkco4/yFWH7X2gAcG4Ckoi2shFr7CMS/A== +"@sentry-internal/tracing@7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.54.0.tgz#eeb10ee72426d08669a7706faa4264f1ec02c71d" + integrity sha512-JsyhZ0wWZ+VqbHJg+azqRGdYJDkcI5R9+pnkO6SzbzxrRewqMAIwzkpPee3oI7vG99uhMEkOkMjHu0nQGwkOQw== + dependencies: + "@sentry/core" "7.54.0" + "@sentry/types" "7.54.0" + "@sentry/utils" "7.54.0" + tslib "^1.9.3" + +"@sentry/browser@7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.54.0.tgz#7fe331c776d02b5c902733aa41dcbfac7bef1ae6" + integrity sha512-EvLAw03N9WE2m1CMl2/1YMeIs1icw9IEOVJhWmf3uJEysNJOFWXu6ZzdtHEz1E6DiJYhc1HzDya0ExZeJxNARA== + dependencies: + "@sentry-internal/tracing" "7.54.0" + "@sentry/core" "7.54.0" + "@sentry/replay" "7.54.0" + "@sentry/types" "7.54.0" + "@sentry/utils" "7.54.0" + tslib "^1.9.3" + +"@sentry/core@7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.54.0.tgz#8c4cb8800f8df708b3f3f6483026bb9a02820014" + integrity sha512-MAn0E2EwgNn1pFQn4qxhU+1kz6edullWg6VE5wCmtpXWOVw6sILBUsQpeIG5djBKMcneJCdOlz5jeqcKPrLvZQ== + dependencies: + "@sentry/types" "7.54.0" + "@sentry/utils" "7.54.0" + tslib "^1.9.3" + +"@sentry/integrations@^7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.54.0.tgz#62c73013ca6040d0c9b045809fc5d6ecefda3339" + integrity sha512-RolGsQzJChJzjHTJcCKSZ1HanmY33floc5o13WgU9NoDqJbLGLNcOIrAu+WynqPe8P5VTVrVb8NiwhLqWrKp4g== + dependencies: + "@sentry/types" "7.54.0" + "@sentry/utils" "7.54.0" + localforage "^1.8.1" + tslib "^1.9.3" + +"@sentry/react@^7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.54.0.tgz#0d9e1b902fd9ded713ac46a623f6a490e4aa2c8a" + integrity sha512-qUbwmRRpTh05m2rbC8A2zAFQYsoHhwIpxT5UXxh0P64ZlA3cSg1/DmTTgwnd1l+7gzKrc31UikXQ4y0YDbMNKg== + dependencies: + "@sentry/browser" "7.54.0" + "@sentry/types" "7.54.0" + "@sentry/utils" "7.54.0" + hoist-non-react-statics "^3.3.2" + tslib "^1.9.3" + +"@sentry/replay@7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.54.0.tgz#f0f44f9413ceefd1809bf1665e82315927ae08db" + integrity sha512-C0F0568ybphzGmKGe23duB6n5wJcgM7WLYhoeqW3o2bHeqpj1dGPSka/K3s9KzGaAgzn1zeOUYXJsOs+T/XdsA== + dependencies: + "@sentry/core" "7.54.0" + "@sentry/types" "7.54.0" + "@sentry/utils" "7.54.0" + +"@sentry/types@7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.54.0.tgz#bfee18107a78e290e6c8ad41646e2b9d9dd95234" + integrity sha512-D+i9xogBeawvQi2r0NOrM7zYcUaPuijeME4O9eOTrDF20tj71hWtJLilK+KTGLYFtpGg1h+9bPaz7OHEIyVopg== + +"@sentry/utils@7.54.0": + version "7.54.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.54.0.tgz#a3acb5e25a1409cbca7b46d6356d7417a253ea9a" + integrity sha512-3Yf5KlKjIcYLddOexSt2ovu2TWlR4Fi7M+aCK8yUTzwNzf/xwFSWOstHlD/WiDy9HvfhWAOB/ukNTuAeJmtasw== + dependencies: + "@sentry/types" "7.54.0" + tslib "^1.9.3" + "@sigstore/protobuf-specs@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz#957cb64ea2f5ce527cc9cf02a096baeb0d2b99b4" @@ -12987,7 +13061,7 @@ loader-utils@^2.0.0, loader-utils@^2.0.4: emojis-list "^3.0.0" json5 "^2.1.2" -localforage@^1.7.3: +localforage@^1.7.3, localforage@^1.8.1: version "1.10.0" resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==