本文作者: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!!!