mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-11 02:43:50 +02:00
add pinia action trail to sentry events
This commit is contained in:
141
src/plugin/piniaActionTrail.js
Normal file
141
src/plugin/piniaActionTrail.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const STORAGE_KEY = 'vrcx:sentry:piniaActions';
|
||||
|
||||
function getStorage() {
|
||||
try {
|
||||
return localStorage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse(value) {
|
||||
if (!value) return null;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonStringify(value, fallback = '[]') {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPiniaActionTrail() {
|
||||
const storage = getStorage();
|
||||
if (!storage) return [];
|
||||
|
||||
const data = safeJsonParse(storage.getItem(STORAGE_KEY));
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
export function clearPiniaActionTrail() {
|
||||
const storage = getStorage();
|
||||
if (!storage) return;
|
||||
storage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function appendPiniaActionTrail(entry, maxEntries = 200) {
|
||||
const storage = getStorage();
|
||||
if (!storage) return;
|
||||
|
||||
const existing = getPiniaActionTrail();
|
||||
existing.push(entry);
|
||||
|
||||
if (existing.length > maxEntries) {
|
||||
existing.splice(0, existing.length - maxEntries);
|
||||
}
|
||||
|
||||
try {
|
||||
storage.setItem(STORAGE_KEY, safeJsonStringify(existing));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function createPiniaActionTrailPlugin({ maxEntries = 200 } = {}) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
if (!window.__VRCX_PINIA_ACTION_TRAIL__) {
|
||||
// @ts-ignore
|
||||
window.__VRCX_PINIA_ACTION_TRAIL__ = true;
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
clearPiniaActionTrail();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ({ store }) => {
|
||||
store.$onAction(({ name }) => {
|
||||
appendPiniaActionTrail(
|
||||
{
|
||||
ts: Date.now(),
|
||||
storeId: store.$id,
|
||||
action: name
|
||||
},
|
||||
maxEntries
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function readPerformanceMemory() {
|
||||
// @ts-ignore
|
||||
const memory = window?.performance?.memory;
|
||||
if (!memory) return null;
|
||||
const { usedJSHeapSize, jsHeapSizeLimit } = memory;
|
||||
if (
|
||||
typeof usedJSHeapSize !== 'number' ||
|
||||
typeof jsHeapSizeLimit !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { usedJSHeapSize, jsHeapSizeLimit };
|
||||
}
|
||||
|
||||
export function startRendererMemoryThresholdReport(
|
||||
Sentry,
|
||||
{ intervalMs = 10_000, thresholdRatio = 0.8, cooldownMs = 5 * 60_000 } = {}
|
||||
) {
|
||||
const initial = readPerformanceMemory();
|
||||
if (!initial) return null;
|
||||
|
||||
if (!Sentry?.withScope) return null;
|
||||
|
||||
let lastSent = 0;
|
||||
|
||||
return setInterval(() => {
|
||||
const m = readPerformanceMemory();
|
||||
if (!m) return;
|
||||
|
||||
const ratio = m.usedJSHeapSize / m.jsHeapSizeLimit;
|
||||
if (!Number.isFinite(ratio) || ratio < thresholdRatio) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastSent < cooldownMs) return;
|
||||
lastSent = now;
|
||||
|
||||
const trail = getPiniaActionTrail();
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setLevel('warning');
|
||||
scope.setTag('reason', 'high-js-heap');
|
||||
scope.setContext('memory', {
|
||||
usedMB: m.usedJSHeapSize / 1024 / 1024,
|
||||
limitMB: m.jsHeapSizeLimit / 1024 / 1024,
|
||||
ratio
|
||||
});
|
||||
scope.setContext('pinia_actions', {
|
||||
trail,
|
||||
count: trail.length
|
||||
});
|
||||
Sentry.captureMessage(
|
||||
'Memory usage critical: nearing JS heap limit'
|
||||
);
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { router } from './router';
|
||||
import { startRendererMemoryThresholdReport } from './piniaActionTrail';
|
||||
|
||||
import configRepository from '../service/config';
|
||||
|
||||
@@ -81,22 +82,7 @@ export async function initSentry(app) {
|
||||
}
|
||||
return event;
|
||||
},
|
||||
beforeSendSpan(span) {
|
||||
span.data = {
|
||||
...span.data,
|
||||
usedJSHeapSize:
|
||||
// @ts-ignore
|
||||
window.performance.memory.usedJSHeapSize / 1024 / 1024,
|
||||
totalJSHeapSize:
|
||||
// @ts-ignore
|
||||
window.performance.memory.totalJSHeapSize / 1024 / 1024,
|
||||
jsHeapSizeLimit:
|
||||
// @ts-ignore
|
||||
window.performance.memory.jsHeapSizeLimit / 1024 / 1024,
|
||||
vrcxId: vrcxId
|
||||
};
|
||||
return span;
|
||||
},
|
||||
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: true,
|
||||
@@ -119,6 +105,12 @@ export async function initSentry(app) {
|
||||
]
|
||||
});
|
||||
console.log('Sentry initialized');
|
||||
|
||||
startRendererMemoryThresholdReport(Sentry, {
|
||||
thresholdRatio: 0.8,
|
||||
intervalMs: 10_000,
|
||||
cooldownMs: 5 * 60_000
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize Sentry:', e);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import { getSentry, isSentryOptedIn } from '../plugin';
|
||||
import { createPiniaActionTrailPlugin } from '../plugin/piniaActionTrail';
|
||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||
import { useAuthStore } from './auth';
|
||||
@@ -38,11 +39,17 @@ import { useWristOverlaySettingsStore } from './settings/wristOverlay';
|
||||
|
||||
export const pinia = createPinia();
|
||||
|
||||
function registerPiniaActionTrailPlugin() {
|
||||
if (!NIGHTLY) return;
|
||||
pinia.use(createPiniaActionTrailPlugin({ maxEntries: 200 }));
|
||||
}
|
||||
|
||||
async function registerSentryPiniaPlugin() {
|
||||
if (!NIGHTLY) return;
|
||||
if (!(await isSentryOptedIn())) return;
|
||||
|
||||
const Sentry = await getSentry();
|
||||
|
||||
pinia.use(
|
||||
Sentry.createSentryPiniaPlugin({
|
||||
stateTransformer: (state) => ({
|
||||
@@ -114,6 +121,7 @@ async function registerSentryPiniaPlugin() {
|
||||
}
|
||||
|
||||
export async function initPiniaPlugins() {
|
||||
registerPiniaActionTrailPlugin();
|
||||
await registerSentryPiniaPlugin();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { debounce, parseLocation } from '../shared/utils';
|
||||
import { AppDebug } from '../service/appConfig';
|
||||
import { database } from '../service/database';
|
||||
import { failedGetRequests } from '../service/request';
|
||||
import { getPiniaActionTrail } from '../plugin/piniaActionTrail';
|
||||
import { refreshCustomScript } from '../shared/utils/base/ui';
|
||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||
import { useAvatarProviderStore } from './avatarProvider';
|
||||
@@ -527,12 +528,18 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
||||
if (advancedSettingsStore.sentryErrorReporting) {
|
||||
try {
|
||||
import('@sentry/vue').then((Sentry) => {
|
||||
Sentry.captureMessage(
|
||||
`crash message: ${crashMessage}`,
|
||||
{
|
||||
level: 'fatal'
|
||||
}
|
||||
);
|
||||
const trail = getPiniaActionTrail();
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setLevel('fatal');
|
||||
scope.setTag('reason', 'crash-recovery');
|
||||
scope.setContext('pinia_actions', {
|
||||
trail,
|
||||
count: trail.length
|
||||
});
|
||||
Sentry.captureMessage(
|
||||
`crash message: ${crashMessage}`
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting up Sentry feedback:', error);
|
||||
|
||||
Reference in New Issue
Block a user