From a9affcfdc2ea10fc7d2ee96471e1b4f112ff06d6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:34:35 +0000
Subject: [PATCH 1/6] Initial plan
From 74bd99080737a9faeee75cf74121bb25ca7ecae7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:36:33 +0000
Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=E5=88=B6=E5=AE=9A=20monitor-test?=
=?UTF-8?q?=20=E8=A1=A5=E5=BC=BA=E8=AE=A1=E5=88=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/EzStars/EzMonitor/sessions/34f3cb4c-9d92-472f-b3c6-4f5e9d11522f
---
pnpm-workspace.yaml | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 85b5bc0..c87b188 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -135,3 +135,17 @@ catalogs:
typescript: ^5.7.3
conflicts_typescript-eslint_h8_20_0:
typescript-eslint: ^8.20.0
+ conflicts_react-router-dom_h6_28_0:
+ react-router-dom: ^6.28.0
+ conflicts_@eslint/js_h9_18_0:
+ '@eslint/js': ^9.18.0
+ conflicts_conflicts_@types/node_h24_12_0_h22_10_7:
+ '@types/node': ^22.10.7
+ conflicts_conflicts_eslint_h9_39_4_h9_18_0:
+ eslint: ^9.18.0
+ conflicts_conflicts_globals_h17_4_0_h16_0_0:
+ globals: ^16.0.0
+ conflicts_conflicts_typescript_t5_9_3_h5_7_3:
+ typescript: ^5.7.3
+ conflicts_conflicts_typescript-eslint_h8_57_0_h8_20_0:
+ typescript-eslint: ^8.20.0
From 1042500b282d84d5a85d342fb7f2c40203b0cf95 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:42:20 +0000
Subject: [PATCH 3/6] =?UTF-8?q?feat(monitor-test):=20=E8=A1=A5=E9=BD=90?=
=?UTF-8?q?=E7=A6=BB=E7=BA=BF=E6=81=A2=E5=A4=8D=E4=B8=8A=E4=BC=A0=E4=B8=8E?=
=?UTF-8?q?=E7=99=BD=E5=B1=8F=E6=A3=80=E6=B5=8B=E9=AA=8C=E8=AF=81=E8=83=BD?=
=?UTF-8?q?=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/EzStars/EzMonitor/sessions/34f3cb4c-9d92-472f-b3c6-4f5e9d11522f
---
app/monitor-test/README.md | 16 +-
.../build/autoSourcemapUploadPlugin.ts | 90 ++++++++
app/monitor-test/src/hooks/useMonitorSDK.ts | 6 +
app/monitor-test/src/pages/ErrorPage.tsx | 201 +++++++++++++++++-
app/monitor-test/src/pages/HomePage.tsx | 9 +-
.../src/pages/PerformancePage.tsx | 22 +-
.../src/services/reliability.spec.ts | 47 ++++
app/monitor-test/src/services/reliability.ts | 92 ++++++++
app/monitor-test/src/services/sdkRuntime.ts | 62 ++++++
9 files changed, 531 insertions(+), 14 deletions(-)
create mode 100644 app/monitor-test/build/autoSourcemapUploadPlugin.ts
create mode 100644 app/monitor-test/src/services/reliability.spec.ts
create mode 100644 app/monitor-test/src/services/reliability.ts
diff --git a/app/monitor-test/README.md b/app/monitor-test/README.md
index f62e812..bbd641d 100644
--- a/app/monitor-test/README.md
+++ b/app/monitor-test/README.md
@@ -12,8 +12,8 @@ monitor-test 是 EzMonitor SDK 的测试门户应用,不是单一 demo 页面
- `/`:测试主页
- `/tracking`:TrackingPlugin(track / trackPage / trackUser)
-- `/performance`:性能测试(long task、Navigation/Resource/User Timing,含 Core Web Vitals 自动采集)
-- `/error`:错误测试(同步错误、Promise rejection、资源错误)
+- `/performance`:性能测试(long task、Navigation/Resource/User Timing、交互延迟,含 Core Web Vitals 自动采集)
+- `/error`:错误测试(同步错误、Promise rejection、资源错误、离线恢复上传、白屏检测)
- `/data-generator`:批量数据生成器
## 运行
@@ -37,9 +37,11 @@ pnpm --filter monitor-test run dev
1. 打开主页,确认可跳转到所有专题页。
2. 在 tracking 页分别触发 track、trackPage、trackUser,确认页面日志有回显。
-3. 在 performance 页触发 long task,确认耗时显示并触发事件上报。
+3. 在 performance 页触发 long task/交互延迟采样,确认耗时显示并触发事件上报。
4. 在 error 页触发三类错误,确认错误回显区域可见记录。
-5. 运行构建与 lint:
+5. 在 error 页断网后触发“发送离线探针事件”,再恢复网络,确认本地队列可被刷新重传。
+6. 在 error 页启动白屏检测并触发“显示 7 秒白屏遮罩”,确认出现 `error_white_screen` 上报日志。
+7. 运行构建与 lint:
```bash
pnpm --filter monitor-test run build
@@ -94,6 +96,12 @@ pnpm --filter monitor-test run preview
- **跨域失败**:确认前端端口在后端 CORS 白名单中。
- **端口冲突**:如果 `monitor-app` 同时运行,Vite 可能自动切换到 `5174`。
+## 对标补齐说明(Sentry / New Relic / OpenTelemetry Browser)
+
+- **离线恢复上传**:支持本地队列可视化、online 事件自动 flush、手动 flush。
+- **白屏检测**:支持 18 点采样 + 持续时间阈值触发 `error_white_screen`。
+- **交互延迟验证**:补充 `performance_interaction_delay` 用于辅助 INP 场景回归。
+
## 目录结构
```text
diff --git a/app/monitor-test/build/autoSourcemapUploadPlugin.ts b/app/monitor-test/build/autoSourcemapUploadPlugin.ts
new file mode 100644
index 0000000..e203a1f
--- /dev/null
+++ b/app/monitor-test/build/autoSourcemapUploadPlugin.ts
@@ -0,0 +1,90 @@
+import type { Plugin } from 'vite'
+import { promises as fs } from 'node:fs'
+import path from 'node:path'
+import process from 'node:process'
+
+interface AutoSourcemapUploadPluginOptions {
+ appId: string
+ release: string
+ uploadKey?: string
+ baseUrl: string
+ strict?: boolean
+}
+
+interface SourceMapPayload {
+ appId: string
+ release: string
+ file: string
+ map: string
+}
+
+async function collectSourcemapPayloads(
+ assetsDir: string,
+ options: AutoSourcemapUploadPluginOptions,
+): Promise {
+ const files = await fs.readdir(assetsDir)
+ const payloads: SourceMapPayload[] = []
+
+ for (const file of files) {
+ if (!file.endsWith('.js.map')) {
+ continue
+ }
+ const mapPath = path.join(assetsDir, file)
+ const map = await fs.readFile(mapPath, 'utf8')
+ payloads.push({
+ appId: options.appId,
+ release: options.release,
+ file: `assets/${file.slice(0, -4)}`,
+ map,
+ })
+ }
+
+ return payloads
+}
+
+export function createAutoSourcemapUploadPlugin(
+ options: AutoSourcemapUploadPluginOptions,
+): Plugin {
+ return {
+ name: 'monitor-test-auto-sourcemap-upload',
+ apply: 'build',
+ async closeBundle() {
+ if (!options.uploadKey) {
+ return
+ }
+
+ const assetsDir = path.resolve(process.cwd(), 'dist/assets')
+ let payloads: SourceMapPayload[] = []
+ try {
+ payloads = await collectSourcemapPayloads(assetsDir, options)
+ }
+ catch (error) {
+ if (options.strict) {
+ throw error
+ }
+ console.warn('[monitor-test] skip sourcemap upload:', error)
+ return
+ }
+
+ for (const payload of payloads) {
+ const response = await fetch(`${options.baseUrl}/api/monitor/sourcemap`, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ 'x-monitor-upload-key': options.uploadKey,
+ },
+ body: JSON.stringify(payload),
+ })
+
+ if (!response.ok) {
+ const body = await response.text()
+ const error = new Error(`[monitor-test] sourcemap upload failed (${response.status}): ${body}`)
+ if (options.strict) {
+ throw error
+ }
+ console.warn(error.message)
+ }
+ }
+ },
+ }
+}
diff --git a/app/monitor-test/src/hooks/useMonitorSDK.ts b/app/monitor-test/src/hooks/useMonitorSDK.ts
index db7b9b5..50bcc8f 100644
--- a/app/monitor-test/src/hooks/useMonitorSDK.ts
+++ b/app/monitor-test/src/hooks/useMonitorSDK.ts
@@ -2,7 +2,10 @@ import { useEffect, useMemo, useState } from 'react'
import {
ensureSDKStarted,
flushReplay,
+ flushReportQueue,
+ getReportQueueStorageKey,
getSDKStatus,
+ readPersistedReportQueue,
reportError,
trackEvent,
trackPage,
@@ -26,7 +29,10 @@ export function useMonitorSDK() {
return useMemo(() => ({
status,
+ flushReportQueue,
flushReplay,
+ getReportQueueStorageKey,
+ readPersistedReportQueue,
reportError,
trackEvent,
trackPage,
diff --git a/app/monitor-test/src/pages/ErrorPage.tsx b/app/monitor-test/src/pages/ErrorPage.tsx
index 07c0dc0..eb07966 100644
--- a/app/monitor-test/src/pages/ErrorPage.tsx
+++ b/app/monitor-test/src/pages/ErrorPage.tsx
@@ -1,19 +1,38 @@
-import { useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
import { useMonitorSDK } from '../hooks/useMonitorSDK'
+import { collectWhiteScreenSnapshot } from '../services/reliability'
interface ErrorLog {
id: number
- kind: 'sync' | 'promise' | 'resource' | 'network' | 'listener'
+ kind: 'sync' | 'promise' | 'resource' | 'network' | 'listener' | 'offline' | 'white-screen'
title: string
detail: string
payload: unknown
}
+const WHITE_SCREEN_WINDOW_MS = 6000
+const WHITE_SCREEN_INTERVAL_MS = 1000
+const WHITE_SCREEN_THRESHOLD = 0.95
+const ROOT_SELECTORS = ['#root', '.portal-shell']
+const SKELETON_SELECTORS = ['.skeleton', '.loading', '[data-skeleton]']
+
export default function ErrorPage() {
- const { status } = useMonitorSDK()
+ const {
+ status,
+ flushReportQueue,
+ getReportQueueStorageKey,
+ readPersistedReportQueue,
+ reportError,
+ trackEvent,
+ } = useMonitorSDK()
const [logs, setLogs] = useState([])
+ const [queueInfo, setQueueInfo] = useState(() => readPersistedReportQueue())
+ const [isWhiteMaskVisible, setIsWhiteMaskVisible] = useState(false)
+ const [whiteScreenMonitoring, setWhiteScreenMonitoring] = useState(false)
+ const whiteScreenTimerRef = useRef(null)
+ const whiteScreenStartedAtRef = useRef(null)
- const pushLog = (kind: ErrorLog['kind'], title: string, detail: string, payload: unknown) => {
+ const pushLog = useCallback((kind: ErrorLog['kind'], title: string, detail: string, payload: unknown) => {
setLogs(prev => [
{
id: Date.now() + Math.floor(Math.random() * 1000),
@@ -24,7 +43,11 @@ export default function ErrorPage() {
},
...prev,
])
- }
+ }, [])
+
+ const refreshQueueInfo = useCallback(() => {
+ setQueueInfo(readPersistedReportQueue())
+ }, [readPersistedReportQueue])
const triggerCaughtSyncError = () => {
try {
@@ -88,6 +111,119 @@ export default function ErrorPage() {
}
}
+ const enqueueOfflineProbe = async () => {
+ const payload = await trackEvent('reliability_offline_probe', {
+ page: '/error',
+ hint: 'disconnect-network-and-click',
+ online: typeof navigator !== 'undefined' ? navigator.onLine : undefined,
+ happenedAt: new Date().toISOString(),
+ })
+ refreshQueueInfo()
+ pushLog('offline', '离线恢复-写入队列', '已发送离线探针事件,断网状态下将进入本地队列', payload)
+ }
+
+ const flushOfflineQueue = async () => {
+ await flushReportQueue()
+ refreshQueueInfo()
+ pushLog('offline', '离线恢复-手动刷新', '已执行 Reporter.flush(),联网后应看到队列减少', {
+ online: typeof navigator !== 'undefined' ? navigator.onLine : undefined,
+ queue: readPersistedReportQueue(),
+ })
+ }
+
+ const runWhiteScreenCheck = async () => {
+ const snapshot = collectWhiteScreenSnapshot(ROOT_SELECTORS, SKELETON_SELECTORS)
+ const isPotentialWhiteScreen = snapshot.ratio >= WHITE_SCREEN_THRESHOLD
+ if (!isPotentialWhiteScreen) {
+ whiteScreenStartedAtRef.current = null
+ pushLog('white-screen', '白屏检测采样', '当前页面存在内容,未命中白屏阈值', snapshot)
+ return
+ }
+
+ if (whiteScreenStartedAtRef.current === null) {
+ whiteScreenStartedAtRef.current = Date.now()
+ pushLog('white-screen', '白屏检测采样', '首次命中潜在白屏阈值,进入持续观察', snapshot)
+ return
+ }
+
+ const duration = Date.now() - whiteScreenStartedAtRef.current
+ if (duration < WHITE_SCREEN_WINDOW_MS) {
+ pushLog('white-screen', '白屏检测采样', `持续命中 ${duration}ms,尚未达到 ${WHITE_SCREEN_WINDOW_MS}ms 上报阈值`, snapshot)
+ return
+ }
+
+ await reportError('white_screen', {
+ message: 'Potential white screen detected in monitor-test',
+ detail: {
+ duration,
+ thresholdRatio: WHITE_SCREEN_THRESHOLD,
+ sample: snapshot,
+ },
+ url: typeof window !== 'undefined' ? window.location.href : undefined,
+ })
+ whiteScreenStartedAtRef.current = null
+ pushLog('white-screen', '白屏检测上报', `已达到 ${WHITE_SCREEN_WINDOW_MS}ms 阈值并完成 error_white_screen 上报`, snapshot)
+ }
+
+ const startWhiteScreenMonitoring = () => {
+ if (whiteScreenTimerRef.current !== null) {
+ return
+ }
+
+ setWhiteScreenMonitoring(true)
+ whiteScreenTimerRef.current = window.setInterval(() => {
+ void runWhiteScreenCheck()
+ }, WHITE_SCREEN_INTERVAL_MS)
+ pushLog('white-screen', '白屏检测启动', '每 1s 采样一次,连续 6s 命中才会上报', {
+ intervalMs: WHITE_SCREEN_INTERVAL_MS,
+ thresholdMs: WHITE_SCREEN_WINDOW_MS,
+ thresholdRatio: WHITE_SCREEN_THRESHOLD,
+ })
+ }
+
+ const stopWhiteScreenMonitoring = () => {
+ if (whiteScreenTimerRef.current !== null) {
+ window.clearInterval(whiteScreenTimerRef.current)
+ whiteScreenTimerRef.current = null
+ }
+ whiteScreenStartedAtRef.current = null
+ setWhiteScreenMonitoring(false)
+ pushLog('white-screen', '白屏检测停止', '已停止周期采样', {})
+ }
+
+ const showWhiteMask = async () => {
+ setIsWhiteMaskVisible(true)
+ pushLog('white-screen', '白屏模拟开始', '已显示 7 秒白屏遮罩,可用于触发自动检测', { durationMs: 7000 })
+ await new Promise(resolve => window.setTimeout(resolve, 7000))
+ setIsWhiteMaskVisible(false)
+ pushLog('white-screen', '白屏模拟结束', '白屏遮罩已移除', {})
+ }
+
+ useEffect(() => {
+ const handleOnline = () => {
+ void flushReportQueue().then(() => {
+ refreshQueueInfo()
+ pushLog('offline', '网络恢复自动刷新', '检测到 online 事件,已自动尝试重发本地队列', {
+ queue: readPersistedReportQueue(),
+ })
+ })
+ }
+ const handleOffline = () => {
+ refreshQueueInfo()
+ pushLog('offline', '网络断开', '检测到 offline 事件,可先触发离线探针再恢复联网验证重传', {})
+ }
+
+ window.addEventListener('online', handleOnline)
+ window.addEventListener('offline', handleOffline)
+ return () => {
+ window.removeEventListener('online', handleOnline)
+ window.removeEventListener('offline', handleOffline)
+ if (whiteScreenTimerRef.current !== null) {
+ window.clearInterval(whiteScreenTimerRef.current)
+ }
+ }
+ }, [flushReportQueue, pushLog, readPersistedReportQueue, refreshQueueInfo])
+
return (
错误测试页
@@ -95,7 +231,7 @@ export default function ErrorPage() {
SDK 状态:
{status}
- 用于触发同步错误、Promise rejection、网络失败、资源错误;错误监听与上报由 SDK ErrorPlugin 自动处理。
+ 用于触发同步错误、Promise rejection、网络失败、资源错误;同时补充离线恢复上传与白屏检测验证。
@@ -106,6 +242,59 @@ export default function ErrorPage() {
+ 离线恢复上传验证
+
+ 本地队列 Key:
+ {getReportQueueStorageKey()}
+
+
+ 当前网络:
+ {typeof navigator !== 'undefined' && navigator.onLine ? 'online' : 'offline'}
+ {' '}
+ |本地队列条数:
+ {queueInfo.itemCount}
+
+
+ 最近持久化时间:
+ {queueInfo.savedAt ? new Date(queueInfo.savedAt).toLocaleTimeString() : '无'}
+ {' '}
+ |样本类型:
+ {queueInfo.sampleTypes.length > 0 ? queueInfo.sampleTypes.join(', ') : '无'}
+
+
+
+
+
+
+
+ 白屏检测验证
+
+ 规则:每 1 秒采样 18 个点,容器命中率 ≥
+ {' '}
+ {WHITE_SCREEN_THRESHOLD}
+ {' '}
+ 且持续 6 秒,上报 error_white_screen。
+
+
+
+
+
+
+
+
+ {isWhiteMaskVisible
+ ? (
+
+ )
+ : null}
+
{logs.length === 0 ?
还没有触发错误
: null}
{logs.map(item => (
diff --git a/app/monitor-test/src/pages/HomePage.tsx b/app/monitor-test/src/pages/HomePage.tsx
index 2d4d21f..b5ec2a9 100644
--- a/app/monitor-test/src/pages/HomePage.tsx
+++ b/app/monitor-test/src/pages/HomePage.tsx
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom'
-import { getReportUrl } from '../services/sdkRuntime'
import { useMonitorSDK } from '../hooks/useMonitorSDK'
+import { getReportUrl } from '../services/sdkRuntime'
export default function HomePage() {
const { status } = useMonitorSDK()
@@ -16,7 +16,10 @@ export default function HomePage() {
{status}
-
上报地址:{getReportUrl()}
+
+ 上报地址:
+ {getReportUrl()}
+
Tracking
@@ -30,7 +33,7 @@ export default function HomePage() {
错误监控
- 验证同步错误、Promise rejection、资源加载错误的捕获与回显。
+ 验证同步错误、Promise rejection、资源加载错误,并覆盖离线恢复上传与白屏检测。
进入测试页
diff --git a/app/monitor-test/src/pages/PerformancePage.tsx b/app/monitor-test/src/pages/PerformancePage.tsx
index fc80147..9bc2443 100644
--- a/app/monitor-test/src/pages/PerformancePage.tsx
+++ b/app/monitor-test/src/pages/PerformancePage.tsx
@@ -3,7 +3,7 @@ import { useMonitorSDK } from '../hooks/useMonitorSDK'
interface PerfLog {
id: number
- kind: 'long-task' | 'metrics' | 'observer' | 'cls'
+ kind: 'long-task' | 'metrics' | 'observer' | 'cls' | 'interaction'
title: string
detail: string
payload: unknown
@@ -226,11 +226,31 @@ export default function PerformancePage() {
}
}
+ const collectInteractionLatency = async () => {
+ const startedAt = performance.now()
+ await new Promise((resolve) => {
+ window.requestAnimationFrame(() => {
+ resolve()
+ })
+ })
+ const afterRaf = performance.now()
+ const delay = Number((afterRaf - startedAt).toFixed(2))
+ const metrics = {
+ page: '/performance',
+ interactionDelay: delay,
+ hint: '对标 Sentry/NewRelic 的交互延迟观测,补充 INP 场景验证',
+ timestamp: Date.now(),
+ }
+ const payload = await trackEvent('performance_interaction_delay', metrics)
+ pushLog('interaction', '交互延迟采样', '已采样一次 requestAnimationFrame 延迟,可辅助验证 INP 场景', payload ?? metrics)
+ }
+
const metricCases = [
{ title: '轻量 Long Task (80ms)', run: () => emitLongTask('轻量 Long Task', 80, 'light-blocking-work') },
{ title: '标准 Long Task (180ms)', run: () => emitLongTask('标准 Long Task', 180, 'standard-blocking-work') },
{ title: '观测 Long Task', run: observeLongTask },
{ title: '触发 CLS 变化', run: triggerClsShift },
+ { title: '交互延迟采样', run: collectInteractionLatency },
{ title: '导航指标采集', run: collectNavigationMetrics },
{ title: '资源概览采集', run: collectResourceSummary },
{ title: '用户计时采集', run: collectUserTiming },
diff --git a/app/monitor-test/src/services/reliability.spec.ts b/app/monitor-test/src/services/reliability.spec.ts
new file mode 100644
index 0000000..c502159
--- /dev/null
+++ b/app/monitor-test/src/services/reliability.spec.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from 'vitest'
+import {
+ createWhiteScreenSamplePoints,
+ isContainerElement,
+} from './reliability'
+
+function createElementStub(
+ input: {
+ tagName: string
+ id?: string
+ className?: string
+ matchResult?: boolean
+ },
+) {
+ return {
+ tagName: input.tagName,
+ id: input.id ?? '',
+ className: input.className ?? '',
+ matches: () => input.matchResult ?? false,
+ } as unknown as Element
+}
+
+describe('reliability helpers', () => {
+ it('creates 18 sample points for white-screen detection', () => {
+ const points = createWhiteScreenSamplePoints(1200, 900)
+ expect(points).toHaveLength(18)
+ expect(points[0]).toEqual({ x: 120, y: 90 })
+ expect(points.at(-1)).toEqual({ x: 1080, y: 810 })
+ })
+
+ it('treats html/body/root as container elements', () => {
+ const roots = ['#root', '.portal-shell']
+ const skeletons = ['.skeleton']
+
+ expect(isContainerElement(createElementStub({ tagName: 'HTML' }), roots, skeletons)).toBe(true)
+ expect(isContainerElement(createElementStub({ tagName: 'BODY' }), roots, skeletons)).toBe(true)
+ expect(isContainerElement(createElementStub({ tagName: 'DIV', id: 'root' }), roots, skeletons)).toBe(true)
+ expect(isContainerElement(createElementStub({ tagName: 'DIV', className: 'portal-shell' }), roots, skeletons)).toBe(true)
+ })
+
+ it('treats non-container element as content', () => {
+ const roots = ['#root', '.portal-shell']
+ const skeletons = ['.skeleton']
+ const content = createElementStub({ tagName: 'SECTION', className: 'content-area' })
+ expect(isContainerElement(content, roots, skeletons)).toBe(false)
+ })
+})
diff --git a/app/monitor-test/src/services/reliability.ts b/app/monitor-test/src/services/reliability.ts
new file mode 100644
index 0000000..0da5373
--- /dev/null
+++ b/app/monitor-test/src/services/reliability.ts
@@ -0,0 +1,92 @@
+export interface Point {
+ x: number
+ y: number
+}
+
+function normalizeClassName(value: string): string[] {
+ return value
+ .trim()
+ .split(/\s+/)
+ .filter(Boolean)
+}
+
+export function createWhiteScreenSamplePoints(width: number, height: number): Point[] {
+ const xRatios = [0.1, 0.5, 0.9]
+ const yRatios = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9]
+ const points: Point[] = []
+
+ for (const yRatio of yRatios) {
+ for (const xRatio of xRatios) {
+ points.push({
+ x: Math.max(0, Math.floor(width * xRatio)),
+ y: Math.max(0, Math.floor(height * yRatio)),
+ })
+ }
+ }
+
+ return points
+}
+
+export function isContainerElement(
+ element: Element,
+ rootSelectors: string[],
+ skeletonSelectors: string[],
+): boolean {
+ const tagName = element.tagName.toLowerCase()
+ if (tagName === 'html' || tagName === 'body') {
+ return true
+ }
+
+ const id = element.id ? `#${element.id}` : ''
+ if (id && rootSelectors.includes(id)) {
+ return true
+ }
+
+ const className = typeof element.className === 'string' ? normalizeClassName(element.className) : []
+ for (const classToken of className) {
+ if (rootSelectors.includes(`.${classToken}`)) {
+ return true
+ }
+ }
+
+ return skeletonSelectors.some((selector) => {
+ try {
+ return typeof element.matches === 'function' && element.matches(selector)
+ }
+ catch {
+ return false
+ }
+ })
+}
+
+export interface WhiteScreenSnapshot {
+ total: number
+ containerHits: number
+ ratio: number
+ points: Point[]
+}
+
+export function collectWhiteScreenSnapshot(
+ rootSelectors: string[],
+ skeletonSelectors: string[],
+): WhiteScreenSnapshot {
+ const width = window.innerWidth
+ const height = window.innerHeight
+ const points = createWhiteScreenSamplePoints(width, height)
+ let containerHits = 0
+
+ for (const point of points) {
+ const element = document.elementFromPoint(point.x, point.y)
+ if (element && isContainerElement(element, rootSelectors, skeletonSelectors)) {
+ containerHits++
+ }
+ }
+
+ const total = points.length
+ return {
+ total,
+ containerHits,
+ ratio: total === 0 ? 0 : Number((containerHits / total).toFixed(3)),
+ points,
+ }
+}
diff --git a/app/monitor-test/src/services/sdkRuntime.ts b/app/monitor-test/src/services/sdkRuntime.ts
index d6bfaf4..1d0991e 100644
--- a/app/monitor-test/src/services/sdkRuntime.ts
+++ b/app/monitor-test/src/services/sdkRuntime.ts
@@ -60,6 +60,11 @@ type TrackPageResult = Awaited>
type TrackUserResult = Awaited>
type TrackUvResult = Awaited>
+interface PersistedReportQueue {
+ savedAt?: number
+ items?: Array<{ type?: string, timestamp?: number }>
+}
+
let startPromise: Promise | null = null
export async function ensureSDKStarted() {
@@ -175,3 +180,60 @@ export async function flushReplay(reason = 'manual_debug') {
replayPlugin.flushForError(reason)
await sdk.getReporter().flush()
}
+
+export function getReportQueueStorageKey() {
+ return String(sdk.getConfig().localStorageKey ?? 'ez_monitor_report_queue')
+}
+
+export function readPersistedReportQueue() {
+ const key = getReportQueueStorageKey()
+ if (typeof localStorage === 'undefined') {
+ return {
+ key,
+ supported: false,
+ itemCount: 0,
+ savedAt: undefined,
+ sampleTypes: [] as string[],
+ }
+ }
+
+ const raw = localStorage.getItem(key)
+ if (!raw) {
+ return {
+ key,
+ supported: true,
+ itemCount: 0,
+ savedAt: undefined,
+ sampleTypes: [] as string[],
+ }
+ }
+
+ try {
+ const parsed = JSON.parse(raw) as PersistedReportQueue
+ const items = Array.isArray(parsed.items) ? parsed.items : []
+ return {
+ key,
+ supported: true,
+ itemCount: items.length,
+ savedAt: parsed.savedAt,
+ sampleTypes: items
+ .slice(0, 8)
+ .map(item => item.type)
+ .filter((item): item is string => typeof item === 'string' && item.length > 0),
+ }
+ }
+ catch {
+ return {
+ key,
+ supported: true,
+ itemCount: 0,
+ savedAt: undefined,
+ sampleTypes: [''],
+ }
+ }
+}
+
+export async function flushReportQueue() {
+ await ensureSDKStarted()
+ await sdk.getReporter().flush()
+}
From d1e71e6fb91e46dff6bae65369a62be8599f8573 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:44:04 +0000
Subject: [PATCH 4/6] =?UTF-8?q?fix(monitor-test):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E7=99=BD=E5=B1=8F=E6=A3=80=E6=B5=8B=E5=B9=B6=E5=8F=91=E9=87=87?=
=?UTF-8?q?=E6=A0=B7=E4=B8=8E=E9=87=87=E6=A0=B7=E8=AE=A1=E7=AE=97=E7=BB=86?=
=?UTF-8?q?=E8=8A=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/EzStars/EzMonitor/sessions/34f3cb4c-9d92-472f-b3c6-4f5e9d11522f
---
app/monitor-test/src/pages/ErrorPage.tsx | 63 ++++++++++++--------
app/monitor-test/src/services/reliability.ts | 2 +-
2 files changed, 38 insertions(+), 27 deletions(-)
diff --git a/app/monitor-test/src/pages/ErrorPage.tsx b/app/monitor-test/src/pages/ErrorPage.tsx
index eb07966..692ef4d 100644
--- a/app/monitor-test/src/pages/ErrorPage.tsx
+++ b/app/monitor-test/src/pages/ErrorPage.tsx
@@ -31,6 +31,7 @@ export default function ErrorPage() {
const [whiteScreenMonitoring, setWhiteScreenMonitoring] = useState(false)
const whiteScreenTimerRef = useRef(null)
const whiteScreenStartedAtRef = useRef(null)
+ const whiteScreenCheckingRef = useRef(false)
const pushLog = useCallback((kind: ErrorLog['kind'], title: string, detail: string, payload: unknown) => {
setLogs(prev => [
@@ -132,37 +133,47 @@ export default function ErrorPage() {
}
const runWhiteScreenCheck = async () => {
- const snapshot = collectWhiteScreenSnapshot(ROOT_SELECTORS, SKELETON_SELECTORS)
- const isPotentialWhiteScreen = snapshot.ratio >= WHITE_SCREEN_THRESHOLD
- if (!isPotentialWhiteScreen) {
- whiteScreenStartedAtRef.current = null
- pushLog('white-screen', '白屏检测采样', '当前页面存在内容,未命中白屏阈值', snapshot)
+ if (whiteScreenCheckingRef.current) {
return
}
+ whiteScreenCheckingRef.current = true
- if (whiteScreenStartedAtRef.current === null) {
- whiteScreenStartedAtRef.current = Date.now()
- pushLog('white-screen', '白屏检测采样', '首次命中潜在白屏阈值,进入持续观察', snapshot)
- return
- }
+ try {
+ const snapshot = collectWhiteScreenSnapshot(ROOT_SELECTORS, SKELETON_SELECTORS)
+ const isPotentialWhiteScreen = snapshot.ratio >= WHITE_SCREEN_THRESHOLD
+ if (!isPotentialWhiteScreen) {
+ whiteScreenStartedAtRef.current = null
+ pushLog('white-screen', '白屏检测采样', '当前页面存在内容,未命中白屏阈值', snapshot)
+ return
+ }
- const duration = Date.now() - whiteScreenStartedAtRef.current
- if (duration < WHITE_SCREEN_WINDOW_MS) {
- pushLog('white-screen', '白屏检测采样', `持续命中 ${duration}ms,尚未达到 ${WHITE_SCREEN_WINDOW_MS}ms 上报阈值`, snapshot)
- return
- }
+ if (whiteScreenStartedAtRef.current === null) {
+ whiteScreenStartedAtRef.current = Date.now()
+ pushLog('white-screen', '白屏检测采样', '首次命中潜在白屏阈值,进入持续观察', snapshot)
+ return
+ }
- await reportError('white_screen', {
- message: 'Potential white screen detected in monitor-test',
- detail: {
- duration,
- thresholdRatio: WHITE_SCREEN_THRESHOLD,
- sample: snapshot,
- },
- url: typeof window !== 'undefined' ? window.location.href : undefined,
- })
- whiteScreenStartedAtRef.current = null
- pushLog('white-screen', '白屏检测上报', `已达到 ${WHITE_SCREEN_WINDOW_MS}ms 阈值并完成 error_white_screen 上报`, snapshot)
+ const duration = Date.now() - whiteScreenStartedAtRef.current
+ if (duration < WHITE_SCREEN_WINDOW_MS) {
+ pushLog('white-screen', '白屏检测采样', `持续命中 ${duration}ms,尚未达到 ${WHITE_SCREEN_WINDOW_MS}ms 上报阈值`, snapshot)
+ return
+ }
+
+ await reportError('white_screen', {
+ message: 'Potential white screen detected in monitor-test',
+ detail: {
+ duration,
+ thresholdRatio: WHITE_SCREEN_THRESHOLD,
+ sample: snapshot,
+ },
+ url: typeof window !== 'undefined' ? window.location.href : undefined,
+ })
+ whiteScreenStartedAtRef.current = null
+ pushLog('white-screen', '白屏检测上报', `已达到 ${WHITE_SCREEN_WINDOW_MS}ms 阈值并完成 error_white_screen 上报`, snapshot)
+ }
+ finally {
+ whiteScreenCheckingRef.current = false
+ }
}
const startWhiteScreenMonitoring = () => {
diff --git a/app/monitor-test/src/services/reliability.ts b/app/monitor-test/src/services/reliability.ts
index 0da5373..37babd0 100644
--- a/app/monitor-test/src/services/reliability.ts
+++ b/app/monitor-test/src/services/reliability.ts
@@ -86,7 +86,7 @@ export function collectWhiteScreenSnapshot(
return {
total,
containerHits,
- ratio: total === 0 ? 0 : Number((containerHits / total).toFixed(3)),
+ ratio: total === 0 ? 0 : Math.round((containerHits * 1000) / total) / 1000,
points,
}
}
From 2dc25255ae1ee29a830f5ccfed96d90ad29b1709 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:45:38 +0000
Subject: [PATCH 5/6] =?UTF-8?q?refactor(monitor-test):=20=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E7=99=BD=E5=B1=8F=E9=87=87=E6=A0=B7=E5=AE=9E=E7=8E=B0?=
=?UTF-8?q?=E7=BB=86=E8=8A=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/EzStars/EzMonitor/sessions/34f3cb4c-9d92-472f-b3c6-4f5e9d11522f
---
app/monitor-test/src/pages/ErrorPage.tsx | 6 ++++--
app/monitor-test/src/services/reliability.ts | 4 +++-
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/app/monitor-test/src/pages/ErrorPage.tsx b/app/monitor-test/src/pages/ErrorPage.tsx
index 692ef4d..9412f6d 100644
--- a/app/monitor-test/src/pages/ErrorPage.tsx
+++ b/app/monitor-test/src/pages/ErrorPage.tsx
@@ -212,12 +212,14 @@ export default function ErrorPage() {
useEffect(() => {
const handleOnline = () => {
- void flushReportQueue().then(() => {
+ const flushWhenOnline = async () => {
+ await flushReportQueue()
refreshQueueInfo()
pushLog('offline', '网络恢复自动刷新', '检测到 online 事件,已自动尝试重发本地队列', {
queue: readPersistedReportQueue(),
})
- })
+ }
+ void flushWhenOnline()
}
const handleOffline = () => {
refreshQueueInfo()
diff --git a/app/monitor-test/src/services/reliability.ts b/app/monitor-test/src/services/reliability.ts
index 37babd0..e5cc777 100644
--- a/app/monitor-test/src/services/reliability.ts
+++ b/app/monitor-test/src/services/reliability.ts
@@ -3,6 +3,8 @@ export interface Point {
y: number
}
+const RATIO_PRECISION_MULTIPLIER = 1000
+
function normalizeClassName(value: string): string[] {
return value
.trim()
@@ -86,7 +88,7 @@ export function collectWhiteScreenSnapshot(
return {
total,
containerHits,
- ratio: total === 0 ? 0 : Math.round((containerHits * 1000) / total) / 1000,
+ ratio: total === 0 ? 0 : Math.round((containerHits * RATIO_PRECISION_MULTIPLIER) / total) / RATIO_PRECISION_MULTIPLIER,
points,
}
}
From 44721b09d9f85a72ca26f257b6ff3d58cb15e902 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:46:56 +0000
Subject: [PATCH 6/6] =?UTF-8?q?chore(monitor-test):=20=E7=BB=9F=E4=B8=80?=
=?UTF-8?q?=E7=99=BD=E5=B1=8F=E6=A8=A1=E6=8B=9F=E6=97=B6=E9=95=BF=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E4=B8=8E=E6=97=A5=E5=BF=97=E6=96=87=E6=A1=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/EzStars/EzMonitor/sessions/34f3cb4c-9d92-472f-b3c6-4f5e9d11522f
---
app/monitor-test/src/pages/ErrorPage.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/monitor-test/src/pages/ErrorPage.tsx b/app/monitor-test/src/pages/ErrorPage.tsx
index 9412f6d..1708241 100644
--- a/app/monitor-test/src/pages/ErrorPage.tsx
+++ b/app/monitor-test/src/pages/ErrorPage.tsx
@@ -13,6 +13,7 @@ interface ErrorLog {
const WHITE_SCREEN_WINDOW_MS = 6000
const WHITE_SCREEN_INTERVAL_MS = 1000
const WHITE_SCREEN_THRESHOLD = 0.95
+const WHITE_SCREEN_MASK_DURATION_MS = 7000
const ROOT_SELECTORS = ['#root', '.portal-shell']
const SKELETON_SELECTORS = ['.skeleton', '.loading', '[data-skeleton]']
@@ -169,7 +170,7 @@ export default function ErrorPage() {
url: typeof window !== 'undefined' ? window.location.href : undefined,
})
whiteScreenStartedAtRef.current = null
- pushLog('white-screen', '白屏检测上报', `已达到 ${WHITE_SCREEN_WINDOW_MS}ms 阈值并完成 error_white_screen 上报`, snapshot)
+ pushLog('white-screen', '白屏检测上报', `已达到 ${WHITE_SCREEN_WINDOW_MS}ms 阈值并完成 white_screen(error_white_screen)上报`, snapshot)
}
finally {
whiteScreenCheckingRef.current = false
@@ -204,8 +205,8 @@ export default function ErrorPage() {
const showWhiteMask = async () => {
setIsWhiteMaskVisible(true)
- pushLog('white-screen', '白屏模拟开始', '已显示 7 秒白屏遮罩,可用于触发自动检测', { durationMs: 7000 })
- await new Promise(resolve => window.setTimeout(resolve, 7000))
+ pushLog('white-screen', '白屏模拟开始', `已显示 ${WHITE_SCREEN_MASK_DURATION_MS / 1000} 秒白屏遮罩,可用于触发自动检测`, { durationMs: WHITE_SCREEN_MASK_DURATION_MS })
+ await new Promise(resolve => window.setTimeout(resolve, WHITE_SCREEN_MASK_DURATION_MS))
setIsWhiteMaskVisible(false)
pushLog('white-screen', '白屏模拟结束', '白屏遮罩已移除', {})
}