微信小程序端监控自建

453 阅读2分钟

背景

传统方式下一个前端项目发到正式环境后,所有报错信息只能通过用户使用时截图、口头描述发送到开发者,然后开发者来根据用户所描述的场景去模拟这个错误的产生,这效率肯定超级低,所以很多开源或收费的前端监控平台就应运而生,我司用的一款webfunny,代码加密,没法二次扩展,而且只能接入web vue版本,本节先说说微信小程序端搭建监控sdk

做之前我也调研过其他很多开源的一些sdk及服务,但都没有满足我司需求:

监控sdk:monitor-sdk

先来讲讲js 错误收集吧

小程序文档中是靠着APP中的onError来收集的,如何扩展到sdk,我这边方法是重写->发布订阅来收集错误,当然方法有很多,大家都可以尝试

发布订阅

const handlers = {}
const appHandles = {}
const pageHandles = {}

/**
 * @param {*} handler 
 * @param {*} handleType 
 */
export function subscribeEvent(handler, handleType = '') {
    if (!handler) {
        return
    }
    switch (handleType) {
        case 'app':
            appHandles[handler.type] = appHandles[handler.type] || []
            appHandles[handler.type].push(handler.callback)
            break
        case 'page':
            pageHandles[handler.type] = appHandles[handler.type] || []
            pageHandles[handler.type].push(handler.callback)
            break
        default:
            handlers[handler.type] = handlers[handler.type] || []
            handlers[handler.type].push(handler.callback)
    }

}

/**
 * @param {*} type 
 * @param {*} data 
 * @param {*} handleType
 */
export function triggerHandlers(type, data, handleType = '') {
    switch (handleType) {
        case 'app':
            if (!type || !appHandles[type]) return
            appHandles[type].forEach((callback) => {
                callback(data)
            })
            break
        case 'page':
            if (!type || !pageHandles[type]) return
            pageHandles[type].forEach((callback) => {
                callback(data)
            })
            break
        default:
            if (!type || !handlers[type]) return
            handlers[type].forEach((callback) => {
                callback(data)
            })
    }
}

重写对象上面某个属性:

/**
 * 重写对象上面的某个属性
 * @param {*} source 需要被重写的对象
 * @param {*} name 需要被重写对象的key
 * @param {*} replacement 以原有的函数作为参数,执行并重写原有函数
 * @param {*} isForced 
 */
export function replaceOld(source, name, replacement, isForced = false) {
    if (name in source || isForced) {
        const original = source[name]
        const wrapped = replacement(original)
        if (typeof wrapped === 'function') {
            source[name] = wrapped
        }
    }
}

重写APP:

const HandleWxPageEvents = {
    onLoad() {
        let vm = this.wxMonitor,
            toUrl = util.getPage()
        let data = {
            simpleUrl: toUrl,
            referrer: vm.referrerPage || "",
        }
        vm.logSave('page_pv', data)
        vm.referrerPage = toUrl
    }
}
export function replaceApp(wxMonitor) {
    if (!App) {
        return
    }
    HandleWxAppEvents.wxMonitor = wxMonitor

    const originApp = App
    App = function (appOptions) {
        let methods = config.APP_CONFIG
        methods.forEach((method) => {
            addReplaceHandler({
                callback: (data) => HandleWxAppEvents[method.replace('AppOn', 'on')](data),
                type: method
            }, 'app')
            replaceOld(
                appOptions,
                method.replace('AppOn', 'on'),
                function (originMethod) {
                    return function (...args) {
                        triggerHandlers.apply(null, [method, ...args, 'app'])
                        if (originMethod) {
                            originMethod.apply(this, args)
                        }
                    }
                },
                true
            )
        })
        return originApp(appOptions)
    }
}

当然Page 页面首页pv和收集错误是一样的,这边不一一贴代码了

如何收集wx.request 来做请求数,请求耗时呢

这块可以发现wx.request 可以使用Object.defineProperty来重新给这个对象附上描述,贴代码吧:

/**
 * 代理请求
 * @param {*} wxMonitor 
 */
export function replaceNetwork(wxMonitor) {
    let vm = wxMonitor
    let WxHookMethods = config.WxHookMethods
    WxHookMethods.forEach(hook => {
        let originRequest = wx[hook];
        Object.defineProperty(wx, hook, {
            writable: true,
            enumerable: true,
            configurable: true,
            value: function () {
                let args = [];
                let startTime = new Date().getTime()
                for (let _i = 0; _i < arguments.length; _i++) {
                    args[_i] = arguments[_i];
                }
                let options$1 = args[0];
                let url = options$1.url || ""

                let reqData;
                if (hook === 'request') {
                    reqData = options$1.data;
                }
                let successHandler = function (res) {
                    try {
                        // 上报接口报警
                        if (!!res && res.statusCode && res.statusCode != 200) {
                            let data = {
                                simpleUrl: util.getPage(),
                                httpUrl: options$1.url || "",
                                httpUploadType: config.HTTP_ERROR,
                                responseText: JSON.stringify(res),
                                httpStatus: res.statusCode
                            }
                            if (!!url && url != `${vm.queue.baseUrl}${vm.queue.api}`) {
                                vm.logSave('http_log', data)
                            }
                        } else {
                            let endTime = new Date().getTime()
                            let consumeData = {
                                simpleUrl: util.getPage(),
                                loadTime: endTime - startTime,
                                httpUrl: options$1.url || "",
                                httpUploadType: config.HTTP_SUCCESS,
                                responseText: JSON.stringify(res),
                                httpStatus: res.statusCode || 200
                            }
                            if (!!url && url != `${vm.queue.baseUrl}${vm.queue.api}`) {
                                vm.logSave('http_log', consumeData)
                            }
                        }
                    } catch (e) {
                        util.warn('[cloudMonitor] http error')
                    }
                    if (typeof options$1.success === 'function') {
                        return options$1.success(res);
                    }
                };
                let failHandler = function (err) {
                    try {
                        let data = {
                            simpleUrl: util.getPage(),
                            httpUrl: options$1.url || "",
                            httpUploadType: config.HTTP_ERROR,
                            responseText: JSON.stringify(err),
                            httpStatus: '0'
                        }
                        if (!!url && url != `${vm.queue.baseUrl}${vm.queue.api}`) {
                            vm.logSave('http_log', data)
                        }
                    } catch (e) {
                        util.warn('[cloudMonitor] http error')
                    }
                    if (typeof options$1.fail === 'function') {
                        return options$1.fail(err);
                    }
                };
                let actOptions = util.__assign(util.__assign({}, options$1), { success: successHandler, fail: failHandler });
                return originRequest.call(this, actOptions);
            }
        })
    })
}

收集信息平台展示

后面文章我会讲下服务端搭建

服务端web:github.com/fonitor/web… 服务端server:github.com/fonitor/web…