diff --git a/package-lock.json b/package-lock.json index d70e0825..b8e9f5a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,11 @@ "@eslint/js": "^9.35.0", "@fontsource/noto-sans-jp": "^5.2.7", "@fontsource/noto-sans-kr": "^5.2.7", - "@fontsource/noto-sans-sc": "^5.2.6", + "@fontsource/noto-sans-sc": "^5.2.7", "@fontsource/noto-sans-tc": "^5.2.7", + "@sentry/vue": "^10.11.0", "@types/jest": "^30.0.0", - "@types/node": "^24.3.1", + "@types/node": "^24.3.3", "@vitejs/plugin-vue": "^6.0.1", "animate.css": "^4.1.1", "babel-runtime": "^6.26.0", @@ -26,21 +27,21 @@ "cross-env": "^10.0.0", "dayjs": "^1.11.18", "echarts": "^6.0.0", - "electron": "^37.4.0", + "electron": "^37.5.0", "electron-builder": "^26.0.12", "element-plus": "^2.11.2", "esbuild-jest": "^0.4.0", "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-vue": "^9.33.0", - "globals": "^16.3.0", + "globals": "^16.4.0", "jest": "^30.1.3", "noty": "^3.2.0-beta-deprecated", "pinia": "^3.0.3", "prettier": "^3.6.2", "remixicon": "^4.6.0", "sass-embedded": "^1.92.1", - "vite": "^7.1.4", + "vite": "^7.1.5", "vue": "^3.5.21", "vue-i18n": "^11.1.12", "vue-marquee-text-component": "^2.0.1", @@ -3903,6 +3904,110 @@ "win32" ] }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.11.0.tgz", + "integrity": "sha512-fnMlz5ntap6x4vRsLOHwPqXh7t82StgAiRt+EaqcMX0t9l8C0w0df8qwrONKXvE5GdHWTNFJj5qR15FERSkg3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "10.11.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.11.0.tgz", + "integrity": "sha512-ADey51IIaa29kepb8B7aSgSGSrcyT7QZdRsN1rhitefzrruHzpSUci5c2EPIvmWfKJq8Wnvukm9BHXZXAAIOzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "10.11.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.11.0.tgz", + "integrity": "sha512-t4M2bxMp2rKGK/l7bkVWjN+xVw9H9V12jAeXmO/Fskz2RcG1ZNLQnKSx/W/zCRMk8k7xOQFsfiApq+zDN+ziKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.11.0", + "@sentry/core": "10.11.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.11.0.tgz", + "integrity": "sha512-brWQ90IYQyZr44IpTprlmvbtz4l2ABzLdpP94Egh12Onf/q6n4CjLKaA25N5kX0uggHqX1Rs7dNaG0mP3ETHhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.11.0", + "@sentry/core": "10.11.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.11.0.tgz", + "integrity": "sha512-qemaKCJKJHHCyGBpdLq23xL5u9Xvir20XN7YFTnHcEq4Jvj0GoWsslxKi5cQB2JvpYn62WxTiDgVLeQlleZhSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.11.0", + "@sentry-internal/feedback": "10.11.0", + "@sentry-internal/replay": "10.11.0", + "@sentry-internal/replay-canvas": "10.11.0", + "@sentry/core": "10.11.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/core": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.11.0.tgz", + "integrity": "sha512-39Rxn8cDXConx3+SKOCAhW+/hklM7UDaz+U1OFzFMDlT59vXSpfI6bcXtNiFDrbOxlQ2hX8yAqx8YRltgSftoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/vue": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-10.11.0.tgz", + "integrity": "sha512-uXPXce4QCEuutG3b7FEcA+fhvXCgVPA/iFyeBMdagtjKGRLWgM0nRgVww/WGrltq8414aq7dAiBTWmPKAoaslw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.11.0", + "@sentry/core": "10.11.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "pinia": "2.x || 3.x", + "vue": "2.x || 3.x" + }, + "peerDependenciesMeta": { + "pinia": { + "optional": true + } + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -4157,9 +4262,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", - "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "version": "24.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz", + "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9ec254d1..d8c813b5 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,11 @@ "@eslint/js": "^9.35.0", "@fontsource/noto-sans-jp": "^5.2.7", "@fontsource/noto-sans-kr": "^5.2.7", - "@fontsource/noto-sans-sc": "^5.2.6", + "@fontsource/noto-sans-sc": "^5.2.7", "@fontsource/noto-sans-tc": "^5.2.7", + "@sentry/vue": "^10.11.0", "@types/jest": "^30.0.0", - "@types/node": "^24.3.1", + "@types/node": "^24.3.3", "@vitejs/plugin-vue": "^6.0.1", "animate.css": "^4.1.1", "babel-runtime": "^6.26.0", @@ -47,21 +48,21 @@ "cross-env": "^10.0.0", "dayjs": "^1.11.18", "echarts": "^6.0.0", - "electron": "^37.4.0", + "electron": "^37.5.0", "electron-builder": "^26.0.12", "element-plus": "^2.11.2", "esbuild-jest": "^0.4.0", "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-vue": "^9.33.0", - "globals": "^16.3.0", + "globals": "^16.4.0", "jest": "^30.1.3", "noty": "^3.2.0-beta-deprecated", "pinia": "^3.0.3", "prettier": "^3.6.2", "remixicon": "^4.6.0", "sass-embedded": "^1.92.1", - "vite": "^7.1.4", + "vite": "^7.1.5", "vue": "^3.5.21", "vue-i18n": "^11.1.12", "vue-marquee-text-component": "^2.0.1", diff --git a/src/app.js b/src/app.js index 737c6365..9f7fd4c1 100644 --- a/src/app.js +++ b/src/app.js @@ -6,9 +6,7 @@ import { createApp } from 'vue'; import { pinia } from './stores'; -import { initPlugins } from './plugin'; -import { i18n } from './plugin/i18n'; -import { initComponents } from './plugin/components'; +import { initPlugins, i18n, initComponents, initSentry } from './plugin'; import ElementPlus from 'element-plus'; import App from './App.vue'; @@ -22,6 +20,7 @@ app.use(pinia); app.use(i18n); app.use(ElementPlus); initComponents(app); +initSentry(app); app.mount('#root'); diff --git a/src/app.scss b/src/app.scss index 3b521e30..d275d292 100644 --- a/src/app.scss +++ b/src/app.scss @@ -8,10 +8,6 @@ // For a copy, see . // -@use './assets/scss/flags.scss'; -@use './assets/scss/animated-emoji.scss'; -@use 'element-plus/theme-chalk/src/index.scss' as *; - @use '@fontsource/noto-sans-kr/korean.css'; @use '@fontsource/noto-sans-jp/japanese.css'; @use '@fontsource/noto-sans-sc/chinese-simplified.css'; @@ -21,6 +17,10 @@ @use '@fontsource/noto-sans-sc'; @use '@fontsource/noto-sans-tc'; +@use './assets/scss/flags.scss'; +@use './assets/scss/animated-emoji.scss'; +@use 'element-plus/theme-chalk/src/index.scss' as *; + @use 'element-plus/theme-chalk/src/dark/css-vars.scss'; @use 'animate.css/animate.min.css'; @use 'noty/lib/noty.css'; diff --git a/src/plugin/index.js b/src/plugin/index.js index fa6695e1..86181600 100644 --- a/src/plugin/index.js +++ b/src/plugin/index.js @@ -11,3 +11,7 @@ export async function initPlugins(isVrOverlay = false) { initDayjs(); initNoty(isVrOverlay); } + +export * from './i18n'; +export * from './components'; +export * from './sentry'; diff --git a/src/plugin/sentry.js b/src/plugin/sentry.js new file mode 100644 index 00000000..c01926a9 --- /dev/null +++ b/src/plugin/sentry.js @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/vue'; +import configRepository from '../service/config'; + +export function initSentry(app) { + configRepository + .getString('VRCX_SentryEnabled', 'false') + .then((enabled) => { + let isNightly = false; + AppApi.GetVersion().then( + (v) => (isNightly = v.includes('Nightly')) + ); + if (enabled === 'true' && isNightly) { + Sentry.init({ + app, + dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', + environment: 'nightly', + sampleRate: 0.1, + beforeSend(event) { + if ( + event.message && + (event.message.toLowerCase().includes('password') || + event.message.toLowerCase().includes('token')) + ) { + return null; + } + return event; + }, + integrations: [] + }); + console.log('Sentry initialized'); + } + }); +} diff --git a/src/stores/index.js b/src/stores/index.js index 5850d3d3..d1b30d16 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -32,7 +32,10 @@ import { useVrcxStore } from './vrcx'; import { useVRCXUpdaterStore } from './vrcxUpdater'; import { useWorldStore } from './world'; +import { createSentryPiniaPlugin } from '@sentry/vue'; + export const pinia = createPinia(); +pinia.use(createSentryPiniaPlugin()); export function createGlobalStores() { return { diff --git a/src/stores/settings/advanced.js b/src/stores/settings/advanced.js index 9e19f996..4cb373ed 100644 --- a/src/stores/settings/advanced.js +++ b/src/stores/settings/advanced.js @@ -8,11 +8,13 @@ import webApiService from '../../service/webapi'; import { watchState } from '../../service/watchState'; import { useGameStore } from '../game'; import { useVrcxStore } from '../vrcx'; +import { useVRCXUpdaterStore } from '../vrcxUpdater'; import { AppDebug } from '../../service/appConfig'; export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { const gameStore = useGameStore(); const vrcxStore = useVrcxStore(); + const VRCXUpdaterStore = useVRCXUpdaterStore(); const { t } = useI18n(); @@ -47,7 +49,8 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { isVRChatConfigDialogVisible: false, saveInstanceEmoji: false, vrcRegistryAutoBackup: true, - vrcRegistryAskRestore: true + vrcRegistryAskRestore: true, + sentryErrorReporting: false }); async function initAdvancedSettings() { @@ -78,7 +81,8 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { notificationOpacity, saveInstanceEmoji, vrcRegistryAutoBackup, - vrcRegistryAskRestore + vrcRegistryAskRestore, + sentryErrorReporting ] = await Promise.all([ configRepository.getBool('enablePrimaryPassword', false), configRepository.getBool('VRCX_relaunchVRChatAfterCrash', false), @@ -118,7 +122,8 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { configRepository.getFloat('VRCX_notificationOpacity', 100), configRepository.getBool('VRCX_saveInstanceEmoji', false), configRepository.getBool('VRCX_vrcRegistryAutoBackup', true), - configRepository.getBool('VRCX_vrcRegistryAskRestore', true) + configRepository.getBool('VRCX_vrcRegistryAskRestore', true), + configRepository.getString('VRCX_SentryEnabled', 'false') ]); state.enablePrimaryPassword = enablePrimaryPassword; @@ -148,8 +153,18 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { state.saveInstanceEmoji = saveInstanceEmoji; state.vrcRegistryAutoBackup = vrcRegistryAutoBackup; state.vrcRegistryAskRestore = vrcRegistryAskRestore; + state.sentryErrorReporting = sentryErrorReporting === 'true'; handleSetAppLauncherSettings(); + + setTimeout(() => { + if ( + VRCXUpdaterStore.branch === 'Nightly' && + sentryErrorReporting === '' + ) { + checkSentryConsent(); + } + }, 2000); } initAdvancedSettings(); @@ -225,6 +240,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { }); const vrcRegistryAutoBackup = computed(() => state.vrcRegistryAutoBackup); const vrcRegistryAskRestore = computed(() => state.vrcRegistryAskRestore); + const sentryErrorReporting = computed(() => state.sentryErrorReporting); /** * @param {boolean} value @@ -422,6 +438,69 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { ); } + async function checkSentryConsent() { + ElMessageBox.confirm( + 'Help improve VRCX by allowing anonymous error reporting?\n\n' + + '• Only collects crash and error information\n' + + '• No personal data or VRChat information is collected\n' + + '• Only enabled in nightly builds\n' + + '• Can be disabled anytime in Advanced Settings', + 'Anonymous Error Reporting', + { + type: 'info', + center: true + } + ) + .then(() => { + state.sentryErrorReporting = true; + configRepository.setString('VRCX_SentryEnabled', 'true'); + + ElMessageBox.confirm( + 'Error reporting setting has been enabled. Would you like to restart VRCX now for the change to take effect?', + 'Restart Required', + { + confirmButtonText: 'Restart Now', + cancelButtonText: 'Later', + type: 'info', + center: true + } + ).then(() => { + VRCXUpdaterStore.restartVRCX(false); + }); + }) + .catch(() => { + state.sentryErrorReporting = false; + configRepository.setString('VRCX_SentryEnabled', 'false'); + }); + } + + async function setSentryErrorReporting() { + if (VRCXUpdaterStore.branch !== 'Nightly') { + return; + } + + state.sentryErrorReporting = !state.sentryErrorReporting; + await configRepository.setString( + 'VRCX_SentryEnabled', + state.sentryErrorReporting ? 'true' : 'false' + ); + + ElMessageBox.confirm( + 'Error reporting setting has been disabled. Would you like to restart VRCX now for the change to take effect?', + 'Restart Required', + { + confirmButtonText: 'Restart Now', + cancelButtonText: 'Later', + type: 'info', + center: true + } + ) + .then(() => { + VRCXUpdaterStore.restartVRCX(false); + }) + .catch(() => {}); + } + async function getSqliteTableSizes() { const [ gps, @@ -726,6 +805,7 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { saveInstanceEmoji, vrcRegistryAutoBackup, vrcRegistryAskRestore, + sentryErrorReporting, setEnablePrimaryPasswordConfigRepository, setRelaunchVRChatAfterCrash, @@ -764,6 +844,8 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => { setSaveInstanceEmoji, setVrcRegistryAutoBackup, setVrcRegistryAskRestore, + setSentryErrorReporting, + checkSentryConsent, askDeleteAllScreenshotMetadata }; }); diff --git a/src/views/Settings/Settings.vue b/src/views/Settings/Settings.vue index ed40a05b..3f112f48 100644 --- a/src/views/Settings/Settings.vue +++ b/src/views/Settings/Settings.vue @@ -1486,6 +1486,17 @@ setSelfInviteOverride(); saveOpenVROption(); " /> + + +
+ Anonymous Error Reporting (Nightly Only) + +
+ @@ -2164,7 +2175,9 @@ ugcFolderPath, notificationOpacity, autoDeleteOldPrints, - saveInstanceEmoji + saveInstanceEmoji, + sentryErrorReporting, + isNightlyBuild } = storeToRefs(advancedSettingsStore); const { @@ -2192,6 +2205,7 @@ showVRChatConfig, promptAutoClearVRCXCacheFrequency, setSaveInstanceEmoji, + setSentryErrorReporting, askDeleteAllScreenshotMetadata } = advancedSettingsStore; diff --git a/src/vr/vr.scss b/src/vr/vr.scss index fcd038db..627a0d40 100644 --- a/src/vr/vr.scss +++ b/src/vr/vr.scss @@ -8,8 +8,6 @@ // For a copy, see . // -@use '../assets/scss/flags.scss'; - @use '@fontsource/noto-sans-kr/korean.css'; @use '@fontsource/noto-sans-jp/japanese.css'; @use '@fontsource/noto-sans-sc/chinese-simplified.css'; @@ -19,6 +17,8 @@ @use '@fontsource/noto-sans-sc'; @use '@fontsource/noto-sans-tc'; +@use '../assets/scss/flags.scss'; + @use 'animate.css/animate.min.css'; @use 'noty/lib/noty.css'; @use 'remixicon/fonts/remixicon.css';