本文作者:cjinhuo,未经受权禁止转载。
概要
已开源的前端监控SDK:mitojs,有趣味的小伙伴能够去瞅瞅~(SDK在线Demo)
来到注释,本文分成四个局部
- 背景
- 前端监控的原理
- 结尾
背景
上一篇前端监控:监控SDK手摸手Teach-架构篇(已开源)讲的是SDK的整体架构,这篇讲的是监控代码实现,也就是插件外面代码的实现
前端监控的原理
监控原生事件,如果不反对addEventListener
,那么就是重写原生函数拿到入参,再将原函数返回。
replaceOld
咱们须要重写很多原生函数,事后定义一个公共函数便于缩小冗余代码
/**
* 重写对象下面的某个属性
*
* @export
* @param {IAnyObject} source 须要被重写的对象
* @param {string} name 须要被重写对象的key
* @param {(...args: any[]) => any} replacement 以原有的函数作为参数,执行并重写原有函数
* @param {boolean} [isForced=false] 是否强制重写(可能原先没有该属性)
*/
export function replaceOld(source: IAnyObject, name: string, replacement: (...args: any[]) => any, isForced = false): void {
if (source === undefined) return
if (name in source || isForced) {
const original = source[name]
const wrapped = replacement(original)
if (typeof wrapped === 'function') {
source[name] = wrapped
}
}
}
fetch
所有的申请第三方库都是基于xhr
、fetch
二次封装的,只须要重写这两个事件就能够拿到所有的接口申请的信息。举个例子,重写fetch
的代码操作:
replaceOld(_global, BrowserEventTypes.FETCH, (originalFetch: voidFun) => {
return function (url: string, config: Partial<Request> = {}): void {
const sTime = getTimestamp()
const method = (config && config.method) || 'GET'
// 收集fetch的根本信息
const httpCollect: HttpCollectedType = {
request: {
httpType: HttpTypes.FETCH,
url,
method,
data: config && config.body
},
time: sTime,
response: {}
}
return originalFetch.apply(_global, [url, config]).then(
(res: Response) => {
// 须要克隆一下对象,不然会被标记该对象曾经被应用过
const resClone = res.clone()
const eTime = getTimestamp()
httpCollect.elapsedTime = eTime - sTime
httpCollect.response.status = resClone.status
resClone.text().then((data) => {
// 收集响应体
httpCollect.response.data = data
// 收集到须要的数据 notify函数用来告诉订阅核心
notify(BrowserEventTypes.FETCH, httpCollect)
})
return res
},
(err: Error) => {
const eTime = getTimestamp()
httpCollect.elapsedTime = eTime - sTime
httpCollect.response.status = 0
// 收集到须要的数据 notify函数用来告诉订阅核心
notify(BrowserEventTypes.FETCH, httpCollect)
throw err
}
)
}
})
对于接口跨域、超时的问题:这两种状况产生的时候,接口返回的响应体和响应头外面都是空的,status
等于0,所以很难辨别两者,然而失常状况下,个别我的项目中都的申请都是简单申请,所以在正式申请会先进行option
进行预申请,如果是跨域的话根本几十毫秒就会返回来,能够以此作为临界值来判断跨域与超时的问题(如果是接口不存在也会被判断成接口跨域)
下面代码就是重写fetch
的基本操作,拿到收集到数据后就能够做一步数据处理,数据上面再讲。同理可得以下列表的重写形式都是如此,重写的过程中拿到入参并收集到你想要的数据,具体代码实现点击上面的链接
- console
- xhr
- onpopstate、pushState、replaceState
onerror
onerror
是能够通过addEventListener
来监听的,当呈现资源谬误或代码谬误时会触发该回调函数
/**
* 增加事件监听器
*
* @export
* @param {{ addEventListener: Function }} target 指标对象
* @param {TotalEventName} eventName 指标对象上的事件名
* @param {Function} handler 回调函数
* @param {(boolean | unknown)} [opitons=false] useCapture默认为false
*/
function on(
target: { addEventListener: Function },
eventName: TotalEventName,
handler: Function,
opitons: boolean | unknown = false
): void {
target.addEventListener(eventName, handler, opitons)
}
on(
_global,
'error',
function (e: ErrorEvent) {
// 收集到须要的数据 notify函数用来告诉订阅核心
notify(BrowserEventTypes.ERROR, e)
},
true
)
同理可得以下列表的监听形式都是如此:
- click
- hashchange
- unhandlerejecttion
Vue2 和 Vue3的谬误
Vue
提供了一个函数errorHandler
供开发者来获取框架层面的谬误,所以间接重写该办法并拿到入参即可
const originErrorHandle = Vue.config.errorHandler
Vue.config.errorHandler = function (err: Error, vm: ViewModel, info: string): void {
const data: ReportDataType = {
type: ErrorTypes.VUE,
message: `${err.message}(${info})`,
level: Severity.Normal,
url: getUrlWithEnv(),
name: err.name,
stack: err.stack || [],
time: getTimestamp()
}
notify(BaseEventTypes.VUE, { data, vm })
const hasConsole = typeof console !== 'undefined'
// vue源码会判断Vue.config.silent,为true时则不会在控制台打印,false时则会打印
if (hasConsole && !Vue.config.silent) {
silentConsoleScope(() => {
console.error('Error in ' + info + ': "' + err.toString() + '"', vm)
console.error(err)
})
}
return originErrorHandle?.(err, vm, info)
}
当然Vue2和Vue3拿到的数据格式是不一样的,具体的解决逻辑能够点击这里
react的render谬误捕获
React16.13中提供了componentDidCatch钩子函数来回调错误信息,所以咱们能够新建一个类ErrorBoundary
来继承React,而后而后申明componentDidCatch
钩子函数,能够拿到错误信息
interface ErrorBoundaryProps {
fallback?: ReactNode
onError?: (error: Error, componentStack: string) => void
}
interface ErrorBoundaryState {
hasError?: boolean
}
class ErrorBoundaryWrapped extends PureComponent<ErrorBoundaryProps, ErrorBoundaryState> {
readonly state: ErrorBoundaryState
constructor(props: any) {
super(props)
this.state = {
hasError: false
}
}
componentDidCatch(error: Error, { componentStack }: ErrorInfo) {
// error 和 componentStack就是咱们须要的错误信息
const { onError } = this.props
const reactError = extractErrorStack(error, Severity.Normal)
reactError.type = ErrorTypes.REACT
onError?.(error, componentStack)
this.setState({
hasError: true
})
}
render() {
return (this.state.hasError ? this.props.fallback : this.props.children) ?? null
}
}
而后将组件抛出来,具体的代码实现
留神:如果是在react中呈现代码谬误,然而不在render函数中,将会被全局的onerror
捕捉到
插件
实现差不多就这了,具体代码能够去仓库外面看看,上一篇前端监控:监控SDK手摸手Teach-架构篇(已开源))中有讲过插件这个概念,插件是用来标准代码分层的一个思维,在指定的区域编写指定性能的代码,可读性和可迭代性会大大提高
export interface BasePluginType<T extends EventTypes = EventTypes, C extends BaseClientType = BaseClientType> {
// 事件枚举
name: T
// 监控事件,并在该事件中用notify告诉订阅核心
monitor: (this: C, notify: (eventName: T, data: any) => void) => void
// 在monitor中触发数据并将数据传入以后函数,拿到数据做数据格式转换(会将tranform放入Subscrib的handers)
transform?: (this: C, collectedData: any) => any
// 拿到转换后的数据进行breadcrumb、report等等操作
consumer?: (this: C, transformedData: any) => void
}
那么下面的重写逻辑就放在monitor
层,能够看进去有个入参notify
,它是用告诉订阅核心的,让咱们看个简略且残缺的例子(具体代码点击这里):
const domPlugin: BasePluginType<BrowserEventTypes, BrowserClient> = {
name: BrowserEventTypes.DOM,
// 监听事件
monitor(notify) {
if (!('document' in _global)) return
// 增加全局click事件
on(
_global.document,
'click',
function () {
notify(BrowserEventTypes.DOM, {
category: 'click',
data: this
})
},
true
)
},
// 转换数据
transform(collectedData: DomCollectedType) {
/**
* 返回蕴含id、class、innerTextde字符串的标签
* @param target html节点
*/
function htmlElementAsString(target: HTMLElement): string {
const tagName = target.tagName.toLowerCase()
let classNames = target.classList.value
classNames = classNames !== '' ? ` class="${classNames}"` : ''
const id = target.id ? ` id="${target.id}"` : ''
const innerText = target.innerText
return `<${tagName}${id}${classNames !== '' ? classNames : ''}>${innerText}</${tagName}>`
}
// 将拿到的数据activeElement转换成相似<button class="btn-one">click me</button>
const htmlString = htmlElementAsString(collectedData.data.activeElement as HTMLElement)
return htmlString
},
// 生产已转换的数据
consumer(transformedData: string) {
// 转换后的数据增加到用户行为栈 breadcrumb中
addBreadcrumbInBrowser.call(this, transformedData, BrowserBreadcrumbTypes.CLICK)
}
}
应用插件
定义完插件后,须要在browserClient初始化的时候应用这些插件(具体代码点击这里):
const browserClient = new BrowserClient(options)
const browserPlugins = [
fetchPlugin,
xhrPlugin,
domPlugin,
errorPlugin,
hashRoutePlugin,
historyRoutePlugin,
consolePlugin,
unhandlerejectionPlugin
]
browserClient.use([...browserPlugins, ...plugins])
browserClient.use
browserClient
是继承与BaseClient
,BaseClient
中有个use
的办法,用来构建插件的hooks程序具体代码实现
/**
* 援用插件
*
* @param {BasePluginType<E>[]} plugins
* @memberof BaseClient
*/
use(plugins: BasePluginType<E>[]) {
if (this.options.disabled) return
// 新建公布订阅实例
const subscrib = new Subscrib<E>()
plugins.forEach((item) => {
if (!this.isPluginEnable(item.name)) return
// 调用插件中的monitor并将公布函数传入
item.monitor.call(this, subscrib.notify.bind(subscrib))
const wrapperTranform = (...args: any[]) => {
// 先执行transform
const res = item.transform?.apply(this, args)
// 拿到transform返回的数据并传入
item.consumer?.call(this, res)
// 如果须要新增hook,可在这里增加逻辑
}
// 订阅插件中的名字,并传入回调函数
subscrib.watch(item.name, wrapperTranform)
})
}
插件运行流程
那么整体的流程大略如下图所示:
结尾
🤔 小结
监控的原理无非就是重写(将原有的根底上包裹一层)或者增加事件监听器,例如小程序的监控也是如此。
下一篇「监控SDK手摸手Teach-微信小程序篇」会讲在微信小程序中怎么实现事件埋点、谬误监控,敬请期待~
🧐 开源
监控SDKmitojs文档,目前有局部人在用mitojs在做本人的监控平台或者埋点相干业务,如果你感兴趣能够,无妨过去瞅瞅 😘
📞 分割&内推
字节前端大量招人,内推可帮忙批改简历和实时查问面试进度,欢送砸简历到我的邮箱:chenjinhuo@bytedance.com
如果你对字节前端、谬误监控、埋点感兴趣、也间接分割我的微信:cjinhuo
Have A Good Day!!!
发表回复