add pinia action trail to sentry events

This commit is contained in:
pa
2026-01-07 20:28:16 +09:00
committed by Natsumi
parent acbc0ca0fc
commit 7ab3ba959b
4 changed files with 170 additions and 22 deletions

View 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);
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);