来源:得物技术
1
背景
2
用户体验量化标准
P0级:这些问题严重影响页面加载速度或涉及到安全风险,例如页面包含超大的图片/媒体资源、页面中含有个人隐私信息; P1级:这些问题可能对用户体验造成潜在影响,例如页面中存在响应时间超过300ms的接口请求。
确定指标和标准:首先,我们需要确定用于量化体验问题的指标和标准。例如,对于接口请求速度问题,可以使用接口响应时间作为指标,同时设定一定的标准,例如超过特定时间阈值即视为问题。 编写自动化脚本:基于指标和标准,我们可以编写自动化脚本来模拟用户在无头浏览器中执行相关操作,例如加载页面、点击按钮、发送请求等。这些脚本将根据设定的指标进行性能测量和问题检测。 使用无头浏览器执行测试:我们可以在无头浏览器中运行自动化脚本,模拟用户行为并收集相应的性能数据。 结果分析和报告生成:通过收集的性能数据,我们可以进行结果分析,并将问题和相关数据转化为检测报告。该报告可以包括问题的详细描述、问题等级、相关性能指标和数据。 提供给调用方:最后,通过卡口服务,我们可以将生成的检测报告提供给调用方。调用方可以根据报告中的问题和数据进行相应的优化和改进,以提升用户体验。

3
巡检系统基础架构
3.1 巡检器基类
DataProviderBase(数据提供基类): dataSlim(): 简化冗余数据; fetchData():获取远程数据,处理并返回待检测页面url列表; isSkipTime():用来设置条件,在某些特定条件下跳过定时任务; schedule():设置定时任务运行区间;

PageInspectorBase(页面检查器基类): check():检查器入口,用来打开指定的检测页面,并初始化各种资源的监听; injectRequestHeaders():注入页面接口请求需要的cookie、token等; urlCheck():url地址检查; onRequest():监听页面请求; onResponse():监听页面响应; onPageError():监听页面错误;

DataReporterBase(数据报告基类): buildReporter(): 根据采集到的错误信息生成检测报告; feishuNotify():将生成的报告通过飞书发送到指定的通知群; getHTMLReporterUrl():根据ejs模板将报告生成html静态文件并上传,返回在线报告地址;

3.2 巡检器
// data-provider.tsexport class DataProvider extends DataProviderBase {// 实现特定的页面列表获取逻辑async fetchData(args) {return await axios.get('https://xxx.xxx').then(res => res.data.urlList)}// 每隔15分钟获取一次待检测列表async schedule() {return [{cron: '*/15 * * * *',args: {}}]}}// page-inspector.tsexport class PageInspector extends PageInspectorBase {async onPageOpen(page, reporter: PageReporter, data) {const pageTitle = await page.evaluate('window.document.title')console.log('这里可以获取到页面title', pageTitle)}}// data-reporter.tsexport class DataReporter extends DataReporterBase {async beforeFeishuNotify(data: InspectorReportBase) {console.log('在飞书通知前做点什么', data)return data}}
3.3 巡检主程序
Redis结合Bull作为巡检系统的异步任务管理工具。定义任务:使用
Bull创建两个任务队列,page_queue用于存放“页面检测任务”,reporter_queue用于存放“报告生成任务”。生产任务:在巡检系统中,页面检测任务和报告生成任务的生产者(主程序)负责将任务添加到相应的队列中。当巡检器(inspector)需要进行页面检测时,生产者将页面检测任务加入
page_queue;当需要生成报告时,生产者将报告生成任务加入reporter_queue。消费任务:巡检系统中的任务消费者(主程序)负责从任务队列中获取任务并执行,一次检测任务会有>=1个页面检测任务,交由上文介绍的页面检查器
PageInspector执行页面检查,然后将检测报告存储到Redis中,当该次检测任务的所有页面都完成检测后,reporter_queue任务被创建并交由巡检器(inspector)的DataReporter消费。
4
卡口服务
巡检器架构去定制实现一个巡检器。4.1 卡口服务运行时序

4.2 创建任务接口
DataProviderBase(数据提供基类)主要能力是:“定时轮询接收外部提供的待检测页面列表”。DataProviderBase的实现,而是要启动一个api服务,负责创建检测任务,示例代码如下:app.post('/xxx.xxx', async (req, res) => { const urls = req.body?.urls // 待检测url列表 const callBack = req.body?.callBack // 调用方接收报告的回调接口地址 const transData = req.body?.transData // 调用方需要在回调中拿到的透传数据 // 巡检系统检测任务创建函数 newApp.createJob(urls.map(url => ({ url, // 在redis任务队列中传递的信息 pos: { callBack, transData }, })), jobId => { // 返回任务id给调用方 res.json({ taskId: jobId }) } )})4.3 页面检测
PageInspectorBase(页面检查器基类)是卡口服务的改造重点,在这个基类的子类实现方面,我们需要去做前文提到的具体待实现的检测case,主要有两类检测case:请求资源型检测case:在子类中覆写 onResponse方法,针对不同的资源类型执行不同的检测逻辑;运行时检测case:在子类中覆写 onPageOpen方法,通过基类传入的Page对象,注入js脚本,执行页面运行时检测;
// 页面检测类class PageInspector extends PageInspectorBase { // ... // 针对不同资源类型检测方法配置Map checkResponseMethodsMap = new Map([['image', this.checkImageResponse]]) // 请求资源型检测入口 针对请求资源进行检测 async onResponse(response: Response, reporter: PageReporter, data: IJobItem) { const resourceType = response.request().resourceType() const checkMethod = this.checkResponseMethodsMap.get(resourceType) await checkMethod(response, reporter, data) } // 检测图片资源 async checkImageResponse(response: Response, reporter: PageReporter, data: IJobItem) { // ... if (imageCdnList.includes(url)) {reporter.add({ errorType: "图片类型错误.非cdn资源" })} // ... } // 运行时检测入口 在页面打开时执行注入的js脚本进行运行时检测 async onPageOpen(page, reporter: PageReporter, data) { // ... const htmlText = await page.evaluate('window.document.documentElement.innerHTML') const phoneRegex = /\b((?:\+?86)?1(?:3\d{3}|5[^4\D]\d{2}|8\d{3}|7(?:[35678]\d{2}|4(?:0\d|1[0-2]|9\d))|9[189]\d{2}|66\d{2})\d{6})\b/g; let phoneMatch: RegExpExecArray let collectMessage = [] while ((phoneMatch = phoneRegex.exec(html)) !== null) { const phone = phoneMatch[1];collectMessage.push(`手机号码:${phone}`); } collectMessage.forEach(val => {reporter.add({ errorMessage: `敏感信息:${val}`})}) // ... } // ...}4.4 报告与回调
reporter_queue中会被创建一个新的“报告生成任务”,主程序调用继承了DataReporterBase的子类进行以下操作:对检测项逐一整理,将搜集到的错误进行等级分类,整理出报告源数据; 根据报告源数据结合ejs模板生成静态html并上传,得到在线检测报告地址; 向调用方回调检测报告。
4.4.1 利用ChatGPT生成报告模板
Q: 我需要你写一个ejs模板,这个模板用来展示一个统计报告,为了样式和交互方便,模板会通过内置bootstrap的css样式和Jquery,你可以用到他们来实现html元素的组合和设置交互,具体的数据结构如下:interface CheckResult {name: string;status: 0 | 1;message: string;severityLevel: 0 | 1;}interface DetectionResult {url: string;status: 0 | 1;checkResults: CheckResult[];}interface InspectorReport {taskId: string;reporterUrl?: string;detectionResults: DetectionResult[];startTimeStr: string;endTimeStr: string;duration: number;pageTotal: number;taskStat: {completed: number,waiting: number,active: number,delayed: number,paused: number,failed: number,}}export interface InspectorReportBase {manifest: IManifest,data: InspectorReport;}export interface IManifest {name: stringdesc?: stringnotifyTo?: string[]}
A: 好的,以下是基于提供的数据结构设计的ejs模板:<html lang="en"><head><meta charset="UTF-8"><link href="https://xxx.xxx.css" rel="stylesheet"><script src="https://xxx.xxx.js">script><title><%=data.manifest.desc%>title>head><body><div class="container"><h2 class="text-center"><%= data.manifest.desc %> 统计报告h2><div class="row"><div class="col-md-4"><div class="card"><div class="card-body"><h5 class="card-title">任务信息h5><p class="card-text">任务ID: <%= data.data.taskId %>p><p class="card-text">检测开始时间: <%= data.data.startTimeStr %>p><p class="card-text">检测结束时间: <%= data.data.endTimeStr %>p><p class="card-text">检测时长: <%= data.data.duration %> 秒p><p class="card-text">检测页面数: <%= data.data.pageTotal %>p>body>html>

4.4.2 与调用方解耦
callBack和transData字段:callBack:检测任务结束时,卡口服务将调用该地址回传检测报告;transData:检测任务结束时,需要透传给callBack的数据;
页面检测任务完成后,在回调测试报告环节,卡口服务将从redis队列任务的缓存中中取出这两个值,使用POST请求将报告和transData发送给callBack。
// 卡口服务回调示例代码axios.post(callBack, { data: { msg: "本次检测检测报告如下:xxxxx", transData: `透传的数据如下:${transData}` }})
5
总结