前端SDK监控开发 | 青训营笔记

83 阅读4分钟
  • 这是我参与第五届青训营伴学笔记创作活动的第14天
  • 本文主要了解前端监控的概念和意义,包括前端监控的一些重要指标,包含了一些实战,实战部分写的可能不是很好。因为我自己也是云里雾里。主要还是了解一下前端监控的知识,以及各部分有什么作用。

前端监控

是什么

image.png

  • 页面卡顿
  • 打开慢
  • 资源加载失败
  • 页面白屏

为什么

  • 提高用户的流量
  • 提高用户的留存率

做什么

  • 性能指标
  • 异常事件
  • 用户行为

性能指标

  • 早期Web就是纯静态 通过文档发起请求到各阶段的耗时作为性能指标
    • 侧重于技术细节,难以反应用户的关心指标 image.png
  • 以用户为中心的性能指标(FP FCP FMP LCP TTI FID CLS) image.png image.png image.png image.png image.png image.png image.png image.png

异常监控

静态资源错误

  • 404 网络异常 等 image.png

请求异常

  • Http请求码 image.png
  • 请求成功率 = 请求成功 / (请求成功数 + 请求失败数)

Js错误

  • 监控重点,严重JS错误错误可能映像交互和渲染

白屏异常

  • 没有标准化监听方法
  • 通常可以提供过判断DOM树的结构来粗略判断白屏是否发生。
    • JS错误导致关键资源渲染失败
    • 请求异常或静态资源加载失败
    • 长时间JS线程阻塞

监控实战

性能(FP FCP LCP FID)

const entryTypes = ['paint', 'largest-contentful-panit', 'first-input']

const p = new PerformanceObserver(list => {
	for (const entry of list.getEntries()) {
		console.log(entry)
	}
})

p.observe({ entryTypes });

// 利用performance对象读取性能指标
// 必须在渲染之后性能条目才能获取
window.performance.getEntriesByType('paint')
window.performance.getEntriesByType('largest-contentful-panit')
  • 封装为一个monitor
// 封装monitor

// 1. 起名字
// 2. 监听能力
// 3. 主动开启,不是被动开启
// 4. 上报能力
function createPerfMOnitor(report:({name:string, data:any})=>void) {
	// 起名字
	const name = 'performance'
	const entryTypes = ['paint', 'largest-contentful-panit', 'first-input']
	// 主动开启
	function start() {
		const p = new PerformanceObserver(list => {
			for (const entry of list.getEntries()) {
				console.log(entry)
				// 上报
				report({name, data:entry})
			}
		})
		p.observe({ entryTypes });
	}
	return {name, start}
}

JS错误监控

// 监听JS错误
window.addEventListener("error", (e)=> {
  if (e.error) {
    console.log('capture an error', e.error)
  }
});
throw new Error("1111")

// promise rejection错误
window.addEventListener("unhandlerejection", (e)=> {
  console.log('capture unhandlerejection', e)
})
Promise.reject("2222")

  • 监听器
// monitor
function createJSErrorMOnitor(report:({name:string, data:any})=> void) {
  const name = 'js-error'
  function start() {
    window.addEventListener("error", (e)=> {
      if (e.error) {
        console.log('capture an error', e.error)
        report({name, data:{type:e.type, message:e.message}})
      }
    });
    window.addEventListener("unhandlerejection", (e)=> {
      console.log('capture unhandlerejection', e)
      report({name, data:{type:e.type, reason:e.reason}})
    })
  }
  return {name, start}
}

静态资源错误

window.addEventListener("error", (e)=> {
  const target = e.target || e.srcElement;
  if (!target) {
    return
  }
  if (target instanceof HTMLElement) {
    // link, script
    let url
    if (target.tagName.toLocaleLowerCase() === 'link') {
      url = target.getAttribute("href")
    } else {
      url = target.getAttribute("src")
    }
    console.log('异常', url)
  }
}, true); 

const link = document.createElement("link")
link.href = "1.css"
link.rel = "stylesheet"
document.head.append(link)
  • 创建一个monitor
function createResourceMOnitor(report:({name:string, data:any})=> void) {
  const name = 'resource'
  function start() {
    window.addEventListener("error", (e)=> {
      const target = e.target || e.srcElement;
      if (!target) {
        return
      }
      if (target instanceof HTMLElement) {
        // link, script
        let url
        if (target.tagName.toLocaleLowerCase() === 'link') {
          url = target.getAttribute("href")
        } else {
          url = target.getAttribute("src")
        }
        report({name, data: {url}})
      }
    }, true); 
  }
  return {name, start}
}

请求错误

// 简易 hook 函数
function hookMethod(obj: any, key: string, hookFunc: Function) {
	return (...params: any[]) => {
		obj[key] = hookFunc(obj[key], ...params);
	}
}

// hook xhr 对象 open方法
hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
	function (this, method: string, url: string) {
		this.payload = {
			method,
			url,
		}
		// 执行原函数
		origin.apply(this, [method.url]);
	}
)()

// send 方法
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
	function (this, ...params: any[]) {
		this.addEventListener("readystatechange", function () {
			if (this.readState === 4 && this.status >= 400) {
				this.payload.status = this.status
				console.log(this.payload)
			}
		});
		// 执行原函数
		origin.apply(this, ...params);
	}
)()

const xhr = new XMLHttpRequest();
xhr.open("post", "111.cc")
xhr.send()
  • 封装monitor
// monitor
function createJSErrorMOnitor(report:({name:string, data:any})=> void) {
	const name = 'js-error'
	// 简易 hook 函数
	function hookMethod(obj: any, key: string, hookFunc: Function) {
		return (...params: any[]) => {
			obj[key] = hookFunc(obj[key], ...params);
		}
	}
	function start() {
		// hook xhr 对象 open方法
		hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
			function (this, method: string, url: string) {
				this.payload = {
					method,
					url,
				}
				// 执行原函数
				origin.apply(this, [method.url]);
			}
		)()

		// send 方法
		hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
			function (this, ...params: any[]) {
				this.addEventListener("readystatechange", function () {
					if (this.readState === 4 && this.status >= 400) {
						this.payload.status = this.status
						console.log(this.payload)
						report({name, data:this.payload})
					}
				});
				// 执行原函数
				origin.apply(this, ...params);
			}
		)()
	}
	return {name, start}
}

封装通用SDK

// 上报函数
// 创建sdk对象存储
// report函数
// loadMonitor
// start
function createSdk(url: String) {
  const monitors: Array<{name:String, start:Function}> = []
  const sdk = {
    url,
    monitors,
    report,
    loadMonitor,
    start,
  }
  function report({name:String, data:Any}) {
    navigator.sendBeacon(url, JSON.stringify({name:string, data:any}))
  }
  function loadMonitor({name:String, start:Function}) {
    monitors.push({name:String, start:Function})
    // sdk.loadMOnitor(xxx).loadMOnitor(xxx).loadMOnitor(xxx) 链式调用
    return sdk;
  }
  function start() {
    monitors.forEach(m>=m.start())
  }
  return sdk;
}

console.log("开始")
const sdk = createSdk("111.com")
const jsMonitor = createJSErrorMOnitor(sdk.reprot)
const perMonitor = createPerfMOnitor(sdk.reprot)
sdk.loadMonitor(jsMonitor).loadMOnitor(perMonitor).start()
throw Error("test")

function createJSErrorMOnitor(report:({ name: String, data :Any})=> void) {
  const name = 'js-error'
  function start() {
    window.addEventListener("error", (e)=> {
      if (e.error) {
        console.log('capture an error', e.error)
        report({name, data:{ type: e.type, message:e.message}})
      }
    });
    window.addEventListener("unhandlerejection", (e)=> {
      console.log('capture unhandlerejection', e)
      report({name, data:{ type:e.type, reason:e.reason }})
    })
  }
  return {name, start}
}

function createPerfMOnitor(report:({ name: String, data :Any })=>void) {
	// 起名字
	const name = 'performance'
	const entryTypes = ['paint', 'largest-contentful-panit', 'first-input']
	// 主动开启
	function start() {
		const p = new PerformanceObserver(list => {
			for (const entry of list.getEntries()) {
				console.log(entry)
				// 上报
				report({name, data:entry})
			}
		})
		p.observe({ entryTypes });
	}
	return {name, start}
}