mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-23 16:53: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 { router } from './router';
|
||||||
|
import { startRendererMemoryThresholdReport } from './piniaActionTrail';
|
||||||
|
|
||||||
import configRepository from '../service/config';
|
import configRepository from '../service/config';
|
||||||
|
|
||||||
@@ -81,22 +82,7 @@ export async function initSentry(app) {
|
|||||||
}
|
}
|
||||||
return event;
|
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: [
|
integrations: [
|
||||||
Sentry.replayIntegration({
|
Sentry.replayIntegration({
|
||||||
maskAllText: true,
|
maskAllText: true,
|
||||||
@@ -119,6 +105,12 @@ export async function initSentry(app) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
console.log('Sentry initialized');
|
console.log('Sentry initialized');
|
||||||
|
|
||||||
|
startRendererMemoryThresholdReport(Sentry, {
|
||||||
|
thresholdRatio: 0.8,
|
||||||
|
intervalMs: 10_000,
|
||||||
|
cooldownMs: 5 * 60_000
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to initialize Sentry:', e);
|
console.error('Failed to initialize Sentry:', e);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
import { getSentry, isSentryOptedIn } from '../plugin';
|
import { getSentry, isSentryOptedIn } from '../plugin';
|
||||||
|
import { createPiniaActionTrailPlugin } from '../plugin/piniaActionTrail';
|
||||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||||
import { useAuthStore } from './auth';
|
import { useAuthStore } from './auth';
|
||||||
@@ -38,11 +39,17 @@ import { useWristOverlaySettingsStore } from './settings/wristOverlay';
|
|||||||
|
|
||||||
export const pinia = createPinia();
|
export const pinia = createPinia();
|
||||||
|
|
||||||
|
function registerPiniaActionTrailPlugin() {
|
||||||
|
if (!NIGHTLY) return;
|
||||||
|
pinia.use(createPiniaActionTrailPlugin({ maxEntries: 200 }));
|
||||||
|
}
|
||||||
|
|
||||||
async function registerSentryPiniaPlugin() {
|
async function registerSentryPiniaPlugin() {
|
||||||
if (!NIGHTLY) return;
|
if (!NIGHTLY) return;
|
||||||
if (!(await isSentryOptedIn())) return;
|
if (!(await isSentryOptedIn())) return;
|
||||||
|
|
||||||
const Sentry = await getSentry();
|
const Sentry = await getSentry();
|
||||||
|
|
||||||
pinia.use(
|
pinia.use(
|
||||||
Sentry.createSentryPiniaPlugin({
|
Sentry.createSentryPiniaPlugin({
|
||||||
stateTransformer: (state) => ({
|
stateTransformer: (state) => ({
|
||||||
@@ -114,6 +121,7 @@ async function registerSentryPiniaPlugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initPiniaPlugins() {
|
export async function initPiniaPlugins() {
|
||||||
|
registerPiniaActionTrailPlugin();
|
||||||
await registerSentryPiniaPlugin();
|
await registerSentryPiniaPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { debounce, parseLocation } from '../shared/utils';
|
|||||||
import { AppDebug } from '../service/appConfig';
|
import { AppDebug } from '../service/appConfig';
|
||||||
import { database } from '../service/database';
|
import { database } from '../service/database';
|
||||||
import { failedGetRequests } from '../service/request';
|
import { failedGetRequests } from '../service/request';
|
||||||
|
import { getPiniaActionTrail } from '../plugin/piniaActionTrail';
|
||||||
import { refreshCustomScript } from '../shared/utils/base/ui';
|
import { refreshCustomScript } from '../shared/utils/base/ui';
|
||||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||||
import { useAvatarProviderStore } from './avatarProvider';
|
import { useAvatarProviderStore } from './avatarProvider';
|
||||||
@@ -527,12 +528,18 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
|||||||
if (advancedSettingsStore.sentryErrorReporting) {
|
if (advancedSettingsStore.sentryErrorReporting) {
|
||||||
try {
|
try {
|
||||||
import('@sentry/vue').then((Sentry) => {
|
import('@sentry/vue').then((Sentry) => {
|
||||||
Sentry.captureMessage(
|
const trail = getPiniaActionTrail();
|
||||||
`crash message: ${crashMessage}`,
|
Sentry.withScope((scope) => {
|
||||||
{
|
scope.setLevel('fatal');
|
||||||
level: 'fatal'
|
scope.setTag('reason', 'crash-recovery');
|
||||||
}
|
scope.setContext('pinia_actions', {
|
||||||
);
|
trail,
|
||||||
|
count: trail.length
|
||||||
|
});
|
||||||
|
Sentry.captureMessage(
|
||||||
|
`crash message: ${crashMessage}`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up Sentry feedback:', error);
|
console.error('Error setting up Sentry feedback:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user