UNPKG

@lx-frontend/taro-plugin-monitor

Version:

taro 小程序监控插件,收集生命周期、事件、错误等数据

433 lines (424 loc) 17.6 kB
import { Events, mergeReconciler, hooks } from '@tarojs/shared'; import { monitorSetting } from './monitorSetting'; import { generateBaseEvent, generateHttpEvent, generateApiCallEvent, generateLifecycleEvent, generateCustomErrorEvent, generateClickEvent } from '@lx-frontend/multi-track-shared'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; // ===========================================以上不同类型事件数据生成辅佐函数====================================================== // 监控 event,订阅与触发 var monitorEvent = new Events(); var triggerEvent = function (trackingEvent) { monitorEvent.trigger('all', trackingEvent); }; // 监控 event,订阅与触发 var networkProps = ['request', 'downloadFile', 'uploadFile']; /** * 处理 API 事件 * @param prop 事件类型 * @param apiBaseInfo 基础信息 * @param otherOptions 其他选项 * @param res 响应 * @param apiState */ var handleApiEvent = function (prop, apiBaseInfo, otherOptions, res, apiState) { if (otherOptions === void 0) { otherOptions = {}; } try { if (networkProps.includes(prop)) { // 上报网络请求事件 triggerEvent(generateHttpEvent({ eventLevel: 'info', httpBaseInfo: apiBaseInfo, otherOptions: otherOptions, response: res, })); } else { // 上报 API 事件 triggerEvent(generateApiCallEvent({ eventLevel: 'info', apiBaseInfo: apiBaseInfo, apiName: prop, apiArgs: otherOptions, apiStatus: apiState, apiResponse: res, })); } } catch (_error) { console.error('handleApiEvent', _error); } }; /** * wx 对象 和 taro 对象构造监控函数 * @param target * @param prop * @param originalMethod */ var creatorMonitorHandler = function (target, prop, originalMethod) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var options = args[0], restArgs = args.slice(1); // 传统 API 和网络 API 有不同数据结构 var eventType = networkProps.includes(prop) ? 'http' : 'api'; var apiBaseInfo = generateBaseEvent(eventType, 'info'); var _a = options || {}, success = _a.success, fail = _a.fail, complete = _a.complete, otherOptions = __rest(_a, ["success", "fail", "complete"]); // 处理有 success 和 fail 、complete 的情况 if (typeof options === 'object' && (success || fail || complete)) { var obj = Object.assign({}, options); if (typeof success === 'function') { obj.success = function () { var _a; var callbackArgs = []; for (var _i = 0; _i < arguments.length; _i++) { callbackArgs[_i] = arguments[_i]; } handleApiEvent(prop, apiBaseInfo, otherOptions, callbackArgs === null || callbackArgs === void 0 ? void 0 : callbackArgs[0], 'success'); (_a = success.apply) === null || _a === void 0 ? void 0 : _a.call(success, target, callbackArgs); }; } if (typeof fail === 'function') { obj.fail = function () { var _a; var callbackArgs = []; for (var _i = 0; _i < arguments.length; _i++) { callbackArgs[_i] = arguments[_i]; } handleApiEvent(prop, apiBaseInfo, otherOptions, callbackArgs === null || callbackArgs === void 0 ? void 0 : callbackArgs[0], 'fail'); (_a = fail.apply) === null || _a === void 0 ? void 0 : _a.call(fail, target, callbackArgs); }; } if (typeof complete === 'function') { obj.complete = function () { var _a; var callbackArgs = []; for (var _i = 0; _i < arguments.length; _i++) { callbackArgs[_i] = arguments[_i]; } handleApiEvent(prop, apiBaseInfo, otherOptions, callbackArgs === null || callbackArgs === void 0 ? void 0 : callbackArgs[0], 'complete'); (_a = complete.apply) === null || _a === void 0 ? void 0 : _a.call(complete, target, callbackArgs); }; } return originalMethod.apply(target, __spreadArray([obj], restArgs, true)); } else { // 传统 wx.xxx() 或者 await wx.xxx(options) 的情况 // 传统 Taro.xxx() 或者 await Taro.xxx(options) 的情况 var maybePromiseOrRes = originalMethod.apply(target, args); if (maybePromiseOrRes === null || maybePromiseOrRes === void 0 ? void 0 : maybePromiseOrRes.then) { return maybePromiseOrRes .then(function (res) { handleApiEvent(prop, apiBaseInfo, otherOptions, res, 'success'); return res; }) .catch(function (err) { handleApiEvent(prop, apiBaseInfo, otherOptions, err, 'fail'); throw err; }); } else { handleApiEvent(prop, apiBaseInfo, args, {}, 'success'); return maybePromiseOrRes; } } }; }; // 有限控制劫持 API var allowProxyApi = new Set([ 'addPhoneContact', 'authorize', 'canvasGetImageData', 'canvasPutImageData', 'canvasToTempFilePath', 'checkSession', 'chooseAddress', 'chooseImage', 'chooseInvoiceTitle', 'chooseLocation', 'chooseVideo', 'clearStorage', 'closeBLEConnection', 'closeBluetoothAdapter', 'closeSocket', 'compressImage', 'connectSocket', 'createBLEConnection', 'request', 'downloadFile', 'exitMiniProgram', 'getAvailableAudioSources', 'getBLEDeviceCharacteristics', 'getBLEDeviceServices', 'getBatteryInfo', 'getBeacons', 'getBluetoothAdapterState', 'getBluetoothDevices', 'getClipboardData', 'getConnectedBluetoothDevices', 'getConnectedWifi', 'getExtConfig', 'getFileInfo', 'getImageInfo', 'getLocation', 'getNetworkType', 'getSavedFileInfo', 'getSavedFileList', 'getScreenBrightness', 'getSetting', 'getStorage', 'getStorageInfo', 'getSystemInfo', 'getUserInfo', 'getWifiList', 'hideHomeButton', 'hideShareMenu', 'hideTabBar', 'hideTabBarRedDot', 'loadFontFace', 'login', 'makePhoneCall', 'navigateBack', 'navigateBackMiniProgram', 'navigateTo', 'navigateToBookshelf', 'navigateToMiniProgram', 'notifyBLECharacteristicValueChange', 'hideKeyboard', 'hideLoading', 'hideNavigationBarLoading', 'hideToast', 'openBluetoothAdapter', 'openDocument', 'openLocation', 'openSetting', 'pageScrollTo', 'previewImage', 'queryBookshelf', 'reLaunch', 'readBLECharacteristicValue', 'redirectTo', 'removeSavedFile', 'removeStorage', 'removeTabBarBadge', 'requestSubscribeMessage', 'saveFile', 'saveImageToPhotosAlbum', 'saveVideoToPhotosAlbum', 'scanCode', 'sendSocketMessage', 'setBackgroundColor', 'setBackgroundTextStyle', 'setClipboardData', 'setEnableDebug', 'setInnerAudioOption', 'setKeepScreenOn', 'setNavigationBarColor', 'setNavigationBarTitle', 'setScreenBrightness', 'setStorage', 'setTabBarBadge', 'setTabBarItem', 'setTabBarStyle', 'showActionSheet', 'showFavoriteGuide', 'showLoading', 'showModal', 'showShareMenu', 'showTabBar', 'showTabBarRedDot', 'showToast', 'startBeaconDiscovery', 'startBluetoothDevicesDiscovery', 'startDeviceMotionListening', 'startPullDownRefresh', 'stopBeaconDiscovery', 'stopBluetoothDevicesDiscovery', 'stopCompass', 'startCompass', 'startAccelerometer', 'stopAccelerometer', 'showNavigationBarLoading', 'stopDeviceMotionListening', 'stopPullDownRefresh', 'switchTab', 'uploadFile', 'vibrateLong', 'vibrateShort', 'writeBLECharacteristicValue', ]); /** * 监控 taro 对象 * 注意,只能采用 taro['xxx'] = function() {} 的方式,才能劫持到。 * @param target */ var monitorTaroCalls = function (target) { Object.keys(target).forEach(function (prop) { if (!allowProxyApi.has(prop)) return; var originalMethod = target[prop]; if (typeof originalMethod === 'function') { target[prop] = creatorMonitorHandler(target, prop, originalMethod); } }); }; /** * 监控微信对象 * 注意,只能通过 proxy 处理后,才能劫持到。 * @param target */ var monitorWxCalls = function (target) { return new Proxy(target, { get: function (target, prop, receiver) { var originalMethod = Reflect.get(target, prop, receiver); if (typeof originalMethod === 'function' && allowProxyApi.has(prop)) { return creatorMonitorHandler(target, prop, originalMethod); } return originalMethod; }, }); }; var enableLifecycleMonitor = monitorSetting.enableLifecycleMonitor, enableErrorMonitor = monitorSetting.enableErrorMonitor, enableNodeMonitor = monitorSetting.enableNodeMonitor, enableApiMonitor = monitorSetting.enableApiMonitor, _a = monitorSetting.allowNodeNames, allowNodeNames = _a === void 0 ? [] : _a; /** * 监控 Reconciler 配置 * taro 的 hostConfig * @doc https://docs.taro.zone/docs/platform-plugin/how * @doc https://docs.taro.zone/docs/platform-plugin/reconciler */ var hostConfig = { initNativeApi: function (taro) { // 控制是否劫持 wx if (enableApiMonitor) { // 分别处理 wx 和 taro 对象 // 这里有点看不懂,wx 对象只能通过 proxy 处理后,才能劫持到。taro 对象则是 taro['xxx'] = function() {} 方式。 wx = monitorWxCalls(wx); monitorTaroCalls(taro); } // 拓展 taro 对象,增加 monitorEvent 属性 taro.monitorEvent = monitorEvent; taro.triggerMonitorEvent = triggerEvent; }, modifyRecursiveComponentConfig: function (defaultConfig) { hooks.tap('getLifecycle', function (instance, lifecycle) { var rawLifecycle = lifecycle; lifecycle = lifecycle.replace(/^on(Show|Hide)$/, 'componentDid$1'); // 特别注意:对 instance 操作只能使用 Object.assign,不能使用 ... 展开运算符 // 原因:instance 是组件实例,展开运算符会丢失组件实例的属性 // 比如:instance.componentDidShow = function () {,简单说就是 页面生命周期部分方法没办法触发。 // 这样会导致 componentDidShow 被覆盖,无法正常触发 // 所以只能使用 Object.assign 来添加新的属性 if (enableLifecycleMonitor) { // 上报生命周期事件 triggerEvent(generateLifecycleEvent({ eventLevel: 'info', lifecycleStage: rawLifecycle, lifecycleOption: '', })); Object.assign(instance, { onLaunch: [ function (res) { triggerEvent(generateLifecycleEvent({ eventLevel: 'info', lifecycleStage: 'onLaunch', lifecycleOption: JSON.stringify(res), })); }, ], }); } // 默认情况下,只有 生命周期相关的事件触发。 // 这里需要把 异常事件也注册进来。 // packages/taro-plugin-react/src/runtime/connect.ts:422 triggerAppHook if (enableErrorMonitor) { Object.assign(instance, { onError: [ function (err) { // 这里 err 是 string // 上报普通异常事件 triggerEvent(generateCustomErrorEvent({ eventLevel: 'error', errType: 'error', errMsg: JSON.stringify(err), stackTrace: JSON.stringify(err === null || err === void 0 ? void 0 : err.stack), })); }, ], onUnhandledRejection: [ function (err) { var _a, _b; // 上报未处理 promise 异常事件 // 这里 err 是 object triggerEvent(generateCustomErrorEvent({ eventLevel: 'error', errType: 'onUnhandledRejection', errMsg: JSON.stringify((_a = err === null || err === void 0 ? void 0 : err.reason) === null || _a === void 0 ? void 0 : _a.message), stackTrace: JSON.stringify((_b = err === null || err === void 0 ? void 0 : err.reason) === null || _b === void 0 ? void 0 : _b.stack), })); }, ], }); } return instance[lifecycle]; }); return defaultConfig; }, // 所有事件都要经过此方法 // taro 把所有的事件都会经过 eventHandler 处理。一个点击事件中,除触发 tap、touchstart 、touchend 等等一大堆事件。 // packages/taro-runtime/src/dsl/common.ts:282 行 // config.eh = eventHandler // 这就是为什么 taro 渲染后,所有节点都有 eh 原因 modifyTaroEvent: function (event, element) { var _a, _b; if (!enableNodeMonitor) return; // event 这个 event 就是 dsl 转换成给小程序结构的 事件回调 event // 只允许 tap 事件 if ((event === null || event === void 0 ? void 0 : event.type) === 'tap' && (allowNodeNames === null || allowNodeNames === void 0 ? void 0 : allowNodeNames.includes(element === null || element === void 0 ? void 0 : element.nodeName))) { // 可以通过 element._root 得到当前事件触发页面数据,比如 页面 url 等等 // 可以通过 element 得到当前点击元素的信息,拿到节点的 props 完整数据。 triggerEvent(generateClickEvent({ eventLevel: 'info', elementText: "".concat(((_a = event === null || event === void 0 ? void 0 : event._relatedInfo) === null || _a === void 0 ? void 0 : _a.anchorTargetText) || (element === null || element === void 0 ? void 0 : element.textContent)), elementName: (_b = element === null || element === void 0 ? void 0 : element.nodeName) !== null && _b !== void 0 ? _b : '', elementProps: JSON.stringify(element === null || element === void 0 ? void 0 : element.props), elementDetail: JSON.stringify(event === null || event === void 0 ? void 0 : event.detail), })); } }, }; mergeReconciler(hostConfig); //# sourceMappingURL=runtime.js.map