├── .husky └── .gitignore ├── pnpm-workspace.yaml ├── .markdownlint.json ├── .stylelintrc.js ├── packages ├── core │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── subscribe.ts │ │ ├── external.ts │ │ ├── transformData.ts │ │ ├── breadcrumb.ts │ │ ├── errorId.ts │ │ ├── options.ts │ │ └── transportData.ts │ └── package.json ├── vue │ ├── README.md │ ├── package.json │ └── src │ │ ├── types.ts │ │ ├── index.ts │ │ └── helper.ts ├── web │ ├── README.md │ ├── src │ │ └── index.ts │ └── package.json ├── browser │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── load.ts │ │ └── handleEvents.ts │ └── package.json ├── react │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── shared │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── config.ts │ │ └── constant.ts │ └── package.json ├── types │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── replace.ts │ │ ├── breadcrumb.ts │ │ ├── track.ts │ │ ├── common.ts │ │ ├── transportData.ts │ │ └── options.ts │ └── package.json ├── utils │ ├── README.md │ ├── src │ │ ├── mini.ts │ │ ├── index.ts │ │ ├── exception.ts │ │ ├── Severity.ts │ │ ├── queue.ts │ │ ├── logger.ts │ │ ├── is.ts │ │ ├── httpStatus.ts │ │ ├── global.ts │ │ ├── browser.ts │ │ └── helpers.ts │ └── package.json ├── web-performance │ ├── README.md │ ├── src │ │ ├── utils │ │ │ ├── isSupported.ts │ │ │ ├── generateUniqueID.ts │ │ │ ├── getPath.ts │ │ │ ├── index.ts │ │ │ └── math.ts │ │ ├── lib │ │ │ ├── getFirstVisitedState.ts │ │ │ ├── getFirstHiddenTime.ts │ │ │ ├── measureCustomMetrics.ts │ │ │ ├── observe.ts │ │ │ ├── calculateScore.ts │ │ │ ├── onHidden.ts │ │ │ ├── onPageChange.ts │ │ │ ├── createReporter.ts │ │ │ ├── store.ts │ │ │ ├── markHandler.ts │ │ │ ├── calculateFps.ts │ │ │ └── proxyHandler.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── config │ │ │ └── scoreDefaultConfig.ts │ │ ├── metrics │ │ │ ├── getFPS.ts │ │ │ ├── getPageInfo.ts │ │ │ ├── getNetworkInfo.ts │ │ │ ├── getCLS.ts │ │ │ ├── getDeviceInfo.ts │ │ │ ├── getFP.ts │ │ │ ├── getFCP.ts │ │ │ ├── getFID.ts │ │ │ ├── getLCP.ts │ │ │ └── getNavigationTiming.ts │ │ ├── types │ │ │ └── index.ts │ │ └── index.ts │ └── package.json ├── wx-miniprogram │ ├── README.md │ ├── src │ │ ├── constant.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── initiative.ts │ │ ├── load.ts │ │ └── utils.ts │ └── package.json └── wx-miniprogram-performance │ ├── README.md │ ├── src │ ├── utils │ │ └── index.ts │ ├── core │ │ ├── event.ts │ │ └── store.ts │ ├── index.ts │ ├── constant │ │ └── index.ts │ ├── wx │ │ ├── handleEvents.ts │ │ ├── index.ts │ │ └── replace.ts │ └── types │ │ └── index.ts │ └── package.json ├── .eslintrc.js ├── .markdownlintignore ├── encode-fe-lint.config.js ├── .eslintignore ├── .stylelintignore ├── .vscode ├── extensions.json └── settings.json ├── examples ├── server │ ├── mocks │ │ ├── node.ts │ │ ├── server.ts │ │ └── handlers.ts │ ├── index.ts │ └── config.ts ├── React │ ├── initMonitor.js │ ├── index.html │ └── reactInit.js ├── Vue │ ├── initMonitor.js │ └── index.html ├── Vue3 │ ├── initMonitor.js │ └── index.html ├── JS │ ├── initMonitor.js │ └── index.html └── WebPerformance │ ├── index.css │ └── index.html ├── .prettierrc.js ├── .gitignore ├── .editorconfig ├── .changeset ├── config.json └── README.md ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' 3 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { "extends": "markdownlint-config-encode" } 2 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: 'stylelint-config-encode' }; 2 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-core 2 | 3 | 印客学院--前端稳定性监控 核心功能 4 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-vue 2 | 3 | 印客学院--前端稳定性监控 Vue 监控 4 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-web 2 | 3 | 印客学院--前端稳定性监控 Web 监控 4 | -------------------------------------------------------------------------------- /packages/browser/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-browser 2 | 3 | 印客学院--前端稳定性监控 页面监控 4 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-react 2 | 3 | 印客学院--前端稳定性监控 React 监控 4 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-shared 2 | 3 | 印客学院--前端稳定性监控 共享参数 4 | -------------------------------------------------------------------------------- /packages/types/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-types 2 | 3 | 印客学院--前端稳定性监控 通用类型声明 4 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-utils 2 | 3 | 印客学院--前端稳定性监控 通用函数 4 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './constant'; 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['eslint-config-encode/typescript', 'prettier'] }; 2 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | coverage/ 5 | es/ 6 | lib/ 7 | packages/ -------------------------------------------------------------------------------- /packages/web-performance/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-web-performance 2 | 3 | 印客学院--前端稳定性监控 Web 性能监控 4 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-wx-mini-program 2 | 3 | 印客学院--前端稳定性监控 小程序监控 4 | -------------------------------------------------------------------------------- /packages/utils/src/mini.ts: -------------------------------------------------------------------------------- 1 | export function getAppId() { 2 | return wx.getAccountInfoSync().miniProgram.appId; 3 | } 4 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor-wx-mini-program-performance 2 | 3 | 印客学院--前端稳定性监控 小程序性能监控 4 | -------------------------------------------------------------------------------- /encode-fe-lint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | enableESLint: true, 3 | enableStylelint: true, 4 | enablePrettier: true, 5 | }; 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | coverage/ 3 | dist/ 4 | es/ 5 | lib/ 6 | node_modules/ 7 | **/\*.min.js 8 | **/_-min.js 9 | \*\*/_.bundle.js 10 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | coverage/ 5 | es/ 6 | lib/ 7 | **/*.min.css 8 | **/*-min.css 9 | **/*.bundle.css 10 | -------------------------------------------------------------------------------- /packages/shared/src/config.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json'; 2 | 3 | export const SDK_NAME = 'encode-monitor'; 4 | export const SDK_VERSION = version; 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "stylelint.vscode-stylelint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/server/mocks/node.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | export const mswServer = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /examples/server/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | singleQuote: true, 5 | semi: true, 6 | trailingComma: 'all', 7 | arrowParens: 'always', 8 | }; 9 | -------------------------------------------------------------------------------- /examples/React/initMonitor.js: -------------------------------------------------------------------------------- 1 | encodeMonitor.init({ 2 | debug: true, 3 | silentConsole: true, 4 | maxBreadcrumbs: 10, 5 | dsn: 'http://localhost:2021/errors/upload', 6 | }); 7 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/src/constant.ts: -------------------------------------------------------------------------------- 1 | export enum EListenerTypes { 2 | Touchmove = 'touchmove', 3 | Tap = 'tap', 4 | Longtap = 'longtap', 5 | Longpress = 'longpress', 6 | } 7 | -------------------------------------------------------------------------------- /packages/types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './breadcrumb' 2 | export * from './options' 3 | export * from './replace' 4 | export * from './transportData' 5 | export * from './common' 6 | export * from './track' 7 | -------------------------------------------------------------------------------- /examples/Vue/initMonitor.js: -------------------------------------------------------------------------------- 1 | Vue.use(encodeMonitor.MonitorVue); 2 | encodeMonitor.init({ 3 | debug: true, 4 | silentConsole: true, 5 | maxBreadcrumbs: 10, 6 | dsn: 'http://localhost:2021/errors/upload', 7 | }); 8 | -------------------------------------------------------------------------------- /examples/Vue3/initMonitor.js: -------------------------------------------------------------------------------- 1 | RootVue.use(encodeMonitor.MonitorVue); 2 | encodeMonitor.init({ 3 | debug: true, 4 | silentConsole: true, 5 | maxBreadcrumbs: 10, 6 | dsn: 'http://localhost:2021/errors/upload', 7 | }); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | yarn-error.log 4 | # test 5 | coverage 6 | .idea 7 | # api-extractor 8 | temp 9 | dist 10 | # analyzer 11 | analyzer.html 12 | 13 | packages/*/dist 14 | packages/*/node_modules 15 | -------------------------------------------------------------------------------- /packages/types/src/replace.ts: -------------------------------------------------------------------------------- 1 | export namespace Replace { 2 | export interface TriggerConsole { 3 | args: any[]; 4 | level: string; 5 | } 6 | export interface IRouter { 7 | from: string; 8 | to: string; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | quote_type = single 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './breadcrumb' 2 | export * from './external' 3 | export * from './transformData' 4 | export * from './transportData' 5 | export * from './external' 6 | export * from './options' 7 | export * from './errorId' 8 | export * from './subscribe' 9 | -------------------------------------------------------------------------------- /examples/JS/initMonitor.js: -------------------------------------------------------------------------------- 1 | window.encodeMonitor.init({ 2 | silentConsole: true, 3 | maxBreadcrumbs: 10, 4 | dsn: 'http://localhost:2021/errors/upload', 5 | throttleDelayTime: 0, 6 | onRouteChange(from, to) { 7 | console.log('onRouteChange: _', from, to); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/web/src/index.ts: -------------------------------------------------------------------------------- 1 | import { init, SDK_VERSION, SDK_NAME, log } from 'encode-monitor-browser'; 2 | import { MonitorVue } from 'encode-monitor-vue'; 3 | import { errorBoundaryReport } from 'encode-monitor-react'; 4 | export { init, SDK_VERSION, SDK_NAME, MonitorVue, log, errorBoundaryReport }; 5 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser' 2 | export * from './exception' 3 | export * from './helpers' 4 | export * from './is' 5 | export * from './global' 6 | export * from './logger' 7 | export * from './queue' 8 | export * from './Severity' 9 | export * from './mini' 10 | export * from './httpStatus' 11 | -------------------------------------------------------------------------------- /packages/utils/src/exception.ts: -------------------------------------------------------------------------------- 1 | import { voidFun } from 'encode-monitor-shared'; 2 | 3 | export function nativeTryCatch(fn: voidFun, errorFn?: (err: any) => void): void { 4 | try { 5 | fn(); 6 | } catch (err) { 7 | console.log('err', err); 8 | if (errorFn) { 9 | errorFn(err); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /examples/WebPerformance/index.css: -------------------------------------------------------------------------------- 1 | img { 2 | display: block; 3 | width: 400px; 4 | } 5 | 6 | .title { 7 | font-size: 30px; 8 | font-weight: 600; 9 | } 10 | 11 | .move-box { 12 | margin-bottom: 20px; 13 | width: 100px; 14 | height: 100px; 15 | border: 1px solid #000; 16 | } 17 | 18 | .move{ 19 | transform: translateY(30px); 20 | } 21 | -------------------------------------------------------------------------------- /packages/web-performance/src/utils/isSupported.ts: -------------------------------------------------------------------------------- 1 | export const isPerformanceSupported = (): boolean => { 2 | return !!window.performance && !!window.performance.getEntriesByType && !!window.performance.mark; 3 | }; 4 | 5 | export const isPerformanceObserverSupported = (): boolean => { 6 | return !!window.PerformanceObserver; 7 | }; 8 | 9 | export const isNavigatorSupported = (): boolean => { 10 | return !!window.navigator; 11 | }; 12 | -------------------------------------------------------------------------------- /examples/server/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | export const handlers = [ 4 | rest.post('/error/upload', (req, res, ctx) => { 5 | return res( 6 | ctx.json({ 7 | message: '上传成功', 8 | }), 9 | ); 10 | }), 11 | rest.get('/normal', (req, res, ctx) => { 12 | return res( 13 | ctx.json({ 14 | code: 200, 15 | message: '这是正常接口', 16 | }), 17 | ); 18 | }), 19 | ]; 20 | -------------------------------------------------------------------------------- /packages/web-performance/src/utils/generateUniqueID.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @return {string} The current session ID for data cleansing 3 | * */ 4 | const generateUniqueID = (): string => { 5 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 6 | const r = (Math.random() * 16) | 0, 7 | v = c == 'x' ? r : (r & 0x3) | 0x8; 8 | return v.toString(16); 9 | }); 10 | }; 11 | 12 | export default generateUniqueID; 13 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/getFirstVisitedState.ts: -------------------------------------------------------------------------------- 1 | import { onPageChange } from './onPageChange'; 2 | 3 | let firstVisitedState = true; 4 | 5 | onPageChange(() => { 6 | firstVisitedState = false; 7 | }); 8 | 9 | /** 10 | * get state which page is visited 11 | */ 12 | const getFirstVisitedState = () => { 13 | return { 14 | get state() { 15 | return firstVisitedState; 16 | }, 17 | }; 18 | }; 19 | 20 | export default getFirstVisitedState; 21 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/getFirstHiddenTime.ts: -------------------------------------------------------------------------------- 1 | import { onHidden } from './onHidden'; 2 | 3 | let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity; 4 | 5 | const getFirstHiddenTime = () => { 6 | onHidden((e: Event) => { 7 | firstHiddenTime = Math.min(firstHiddenTime, e.timeStamp); 8 | }, true); 9 | 10 | return { 11 | get timeStamp() { 12 | return firstHiddenTime; 13 | }, 14 | }; 15 | }; 16 | 17 | export default getFirstHiddenTime; 18 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/measureCustomMetrics.ts: -------------------------------------------------------------------------------- 1 | import { isPerformanceSupported } from '../utils/isSupported'; 2 | 3 | export const measure = (customMetrics: string, markName): PerformanceEntry | undefined => { 4 | if (!isPerformanceSupported()) { 5 | console.error('browser do not support performance'); 6 | return; 7 | } 8 | 9 | performance.measure(customMetrics, `${markName}_start`, `${markName}_end`); 10 | 11 | return performance.getEntriesByName(customMetrics).pop(); 12 | }; 13 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/types/src/breadcrumb.ts: -------------------------------------------------------------------------------- 1 | import { Severity } from 'encode-monitor-utils'; 2 | import { BreadCrumbTypes } from 'encode-monitor-shared'; 3 | import { ReportDataType } from './transportData'; 4 | import { Replace } from './replace'; 5 | import { TNumStrObj } from './common'; 6 | 7 | export interface BreadcrumbPushData { 8 | /** 9 | * 事件类型 10 | */ 11 | type: BreadCrumbTypes; 12 | data: ReportDataType | Replace.IRouter | Replace.TriggerConsole | TNumStrObj; 13 | category?: string; 14 | time?: number; 15 | level: Severity; 16 | } 17 | -------------------------------------------------------------------------------- /packages/web-performance/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export enum metricsName { 2 | /* performance metrics */ 3 | NT = 'navigation-timing', 4 | FP = 'first-paint', 5 | FCP = 'first-contentful-paint', 6 | LCP = 'largest-contentful-paint', 7 | CCP = 'custom-contentful-paint', 8 | FID = 'first-input-delay', 9 | RL = 'resource-flow', 10 | CLS = 'cumulative-layout-shift', 11 | FPS = 'fps', 12 | ACT = 'api-complete-time', 13 | /* information */ 14 | DI = 'device-information', 15 | NI = 'network-information', 16 | PI = 'page-information', 17 | } 18 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/observe.ts: -------------------------------------------------------------------------------- 1 | import { PerformanceEntryHandler } from '../types'; 2 | 3 | const observe = ( 4 | type: string, 5 | callback: PerformanceEntryHandler, 6 | ): PerformanceObserver | undefined => { 7 | try { 8 | if (PerformanceObserver.supportedEntryTypes?.includes(type)) { 9 | const po: PerformanceObserver = new PerformanceObserver((l) => l.getEntries().map(callback)); 10 | 11 | po.observe({ type, buffered: true }); 12 | return po; 13 | } 14 | } catch (e) { 15 | throw e; 16 | } 17 | }; 18 | 19 | export default observe; 20 | -------------------------------------------------------------------------------- /packages/web-performance/src/utils/getPath.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param location {Location} 3 | * @param isHash {boolean} 4 | * @return {string} the page path 5 | */ 6 | const getPath = (location: Location, isHash: boolean) => { 7 | if (!isHash) { 8 | return location.pathname.replace(/\/$/, ''); 9 | } else { 10 | const index = location.href.indexOf('#'); 11 | if (index < 0) return ''; 12 | const hash = location.href.slice(index + 1); 13 | const searchIndex = hash.indexOf('?'); 14 | if (searchIndex < 0) return hash; 15 | return hash.slice(0, searchIndex); 16 | } 17 | }; 18 | 19 | export default getPath; 20 | -------------------------------------------------------------------------------- /packages/types/src/track.ts: -------------------------------------------------------------------------------- 1 | export enum EActionType { 2 | // 页面曝光 3 | PAGE = 'PAGE', 4 | // 事件埋点 5 | EVENT = 'EVENT', 6 | // 区域曝光 7 | VIEW = 'VIEW', 8 | // 时长埋点 9 | DURATION = 'DURATION', 10 | // 区域曝光的时长埋点 11 | DURATION_VIEW = 'DURATION_VIEW', 12 | // 其他埋点类型 13 | OTHER = 'OTHER', 14 | } 15 | 16 | export interface DeviceInfo { 17 | //网络类型: 4g,3g,5g,wifi 18 | netType: string; 19 | clientWidth: number; 20 | clientHeight: number; 21 | ratio: number; 22 | } 23 | 24 | export interface ITrackBaseParam { 25 | trackId?: string; 26 | custom?: string | { [prop: string]: string | number | boolean }; 27 | [key: string]: any; 28 | } 29 | -------------------------------------------------------------------------------- /packages/browser/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handleEvents'; 2 | export * from './load'; 3 | export * from './replace'; 4 | import { setupReplace } from './load'; 5 | import { initOptions, log } from 'encode-monitor-core'; 6 | import { _global } from 'encode-monitor-utils'; 7 | import { SDK_VERSION, SDK_NAME } from 'encode-monitor-shared'; 8 | import { InitOptions } from 'encode-monitor-types'; 9 | function webInit(options: InitOptions = {}): void { 10 | if (!('XMLHttpRequest' in _global) || options.disabled) return; 11 | initOptions(options); 12 | setupReplace(); 13 | } 14 | 15 | function init(options: InitOptions = {}): void { 16 | webInit(options); 17 | } 18 | 19 | export { SDK_VERSION, SDK_NAME, init, log }; 20 | -------------------------------------------------------------------------------- /examples/React/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React 监控测试 7 | 8 | 9 | 10 |

点击数字,点击3下就抛出异常

11 |
12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/web-performance/src/config/scoreDefaultConfig.ts: -------------------------------------------------------------------------------- 1 | import { metricsName } from '../constants'; 2 | 3 | const config: Record = { 4 | [metricsName.FP]: { 5 | median: 3000, 6 | p10: 1800, 7 | }, 8 | [metricsName.FCP]: { 9 | median: 3000, 10 | p10: 1800, 11 | }, 12 | [metricsName.ACT]: { 13 | median: 3500, 14 | p10: 2300, 15 | }, 16 | [metricsName.LCP]: { 17 | median: 4000, 18 | p10: 2500, 19 | }, 20 | [metricsName.CCP]: { 21 | median: 4000, 22 | p10: 2500, 23 | }, 24 | [metricsName.FID]: { 25 | median: 300, 26 | p10: 100, 27 | }, 28 | [metricsName.CLS]: { 29 | median: 0.25, 30 | p10: 0.1, 31 | }, 32 | }; 33 | 34 | export default config; 35 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-shared", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 共享参数", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist", 15 | "README.md" 16 | ], 17 | "keywords": [ 18 | "encode", 19 | "monitor", 20 | "shared" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/src/index.ts: -------------------------------------------------------------------------------- 1 | import { InitOptions } from 'encode-monitor-types'; 2 | import { isWxMiniEnv } from 'encode-monitor-utils'; 3 | import { setupReplace } from './load'; 4 | import { initOptions, log } from 'encode-monitor-core'; 5 | import { sendTrackData, track } from './initiative'; 6 | import { SDK_NAME, SDK_VERSION } from 'encode-monitor-shared'; 7 | import { MonitorVue } from 'encode-monitor-vue'; 8 | import { errorBoundaryReport } from 'encode-monitor-react'; 9 | 10 | export function init(options: InitOptions = {}) { 11 | if (!isWxMiniEnv) return; 12 | initOptions(options); 13 | setupReplace(); 14 | Object.assign(wx, { monitorLog: log, SDK_NAME, SDK_VERSION }); 15 | } 16 | export { log, sendTrackData, track, MonitorVue, errorBoundaryReport }; 17 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/calculateScore.ts: -------------------------------------------------------------------------------- 1 | import { QUANTILE_AT_VALUE } from '../utils/math'; 2 | import scoreDefaultConfig from '../config/scoreDefaultConfig'; 3 | import { IScoreConfig } from '../types'; 4 | 5 | /** 6 | * @param metricsName string 7 | * @param value number 8 | * @param config IScoreConfig 9 | * @return the metrics score 10 | **/ 11 | const calcScore = ( 12 | metricsName: string, 13 | value: number, 14 | config: IScoreConfig = {}, 15 | ): number | null => { 16 | const mergeConfig = { ...scoreDefaultConfig, ...config }; 17 | 18 | const metricsConfig = mergeConfig[metricsName]; 19 | 20 | if (metricsConfig) { 21 | return QUANTILE_AT_VALUE(metricsConfig, value); 22 | } 23 | 24 | return null; 25 | }; 26 | 27 | export default calcScore; 28 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/onHidden.ts: -------------------------------------------------------------------------------- 1 | import { OnHiddenCallback } from '../types'; 2 | 3 | export const onHidden = (cb: OnHiddenCallback, once?: boolean) => { 4 | const onHiddenOrPageHide = (event: Event) => { 5 | if (event.type === 'pagehide' || document.visibilityState === 'hidden') { 6 | cb(event); 7 | if (once) { 8 | removeEventListener('visibilitychange', onHiddenOrPageHide, true); 9 | removeEventListener('pagehide', onHiddenOrPageHide, true); 10 | } 11 | } 12 | }; 13 | addEventListener('visibilitychange', onHiddenOrPageHide, true); 14 | // Some browsers have buggy implementations of visibilitychange, 15 | // so we use pagehide in addition, just to be safe. 16 | addEventListener('pagehide', onHiddenOrPageHide, true); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/src/types.ts: -------------------------------------------------------------------------------- 1 | import { IAnyObject } from 'encode-monitor-types'; 2 | 3 | export interface MiniRoute { 4 | from: string; 5 | to: string; 6 | isFail?: boolean; 7 | message?: string; 8 | } 9 | 10 | export interface WxOnShareAppMessageBreadcrumb { 11 | path: string; 12 | query: IAnyObject; 13 | options: WechatMiniprogram.Page.IShareAppMessageOption; 14 | } 15 | 16 | export interface WxOnTabItemTapBreadcrumb { 17 | path: string; 18 | query: IAnyObject; 19 | options: WechatMiniprogram.Page.ITabItemTapOption; 20 | } 21 | 22 | export interface WxRequestErrorBreadcrumb { 23 | requestOptions: WechatMiniprogram.RequestOption; 24 | errMsg: string; 25 | } 26 | 27 | export interface WxLifeCycleBreadcrumb { 28 | path: string; 29 | query: IAnyObject; 30 | } 31 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/src/initiative.ts: -------------------------------------------------------------------------------- 1 | import { EActionType, ITrackBaseParam, TrackReportData } from 'encode-monitor-types'; 2 | import { transportData } from 'encode-monitor-core'; 3 | import { generateUUID, getTimestamp } from 'encode-monitor-utils'; 4 | 5 | export function track(actionType: EActionType, param: ITrackBaseParam) { 6 | const data = { 7 | ...param, 8 | // rewrite actionType 9 | actionType, 10 | }; 11 | sendTrackData(data); 12 | return data; 13 | } 14 | 15 | /** 16 | * 手动发送埋点数据到服务端 17 | * @param data 埋点上报的数据,必须含有actionType属性 18 | */ 19 | export function sendTrackData(data: TrackReportData) { 20 | const id = generateUUID(); 21 | const trackTime = getTimestamp(); 22 | transportData.send({ 23 | id, 24 | trackTime, 25 | ...data, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/onPageChange.ts: -------------------------------------------------------------------------------- 1 | import { OnPageChangeCallback } from '../types'; 2 | import { proxyHistory } from './proxyHandler'; 3 | 4 | const unifiedHref = (href) => { 5 | return decodeURIComponent(href?.replace(`${location?.protocol}//${location?.host}`, '')); 6 | }; 7 | 8 | const lastHref = unifiedHref(location.href); 9 | 10 | /** 11 | * when page is loaded, listen page change 12 | */ 13 | export const onPageChange = (cb: OnPageChangeCallback) => { 14 | window.addEventListener('hashchange', function (e) { 15 | cb(e); 16 | }); 17 | 18 | window.addEventListener('popstate', function (e) { 19 | cb(e); 20 | }); 21 | 22 | proxyHistory((...args) => { 23 | const currentHref = unifiedHref(args?.[2]); 24 | if (lastHref !== currentHref) { 25 | cb(); 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-web", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 Web 监控", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "keywords": [ 17 | "encode", 18 | "monitor", 19 | "web" 20 | ], 21 | "author": "chenghuai", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "encode-bundle": "^1.4.1" 25 | }, 26 | "dependencies": { 27 | "encode-monitor-browser": "workspace:^", 28 | "encode-monitor-react": "workspace:^", 29 | "encode-monitor-vue": "workspace:^" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-core", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 核心功能", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist", 15 | "README.md" 16 | ], 17 | "keywords": [ 18 | "encode", 19 | "monitor", 20 | "core" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | }, 27 | "dependencies": { 28 | "encode-monitor-shared": "workspace:^", 29 | "encode-monitor-types": "workspace:^", 30 | "encode-monitor-utils": "workspace:^" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-types", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 通用类型", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist", 15 | "README.md" 16 | ], 17 | "keywords": [ 18 | "encode", 19 | "monitor", 20 | "types" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | }, 27 | "dependencies": { 28 | "encode-monitor-core": "workspace:^", 29 | "encode-monitor-shared": "workspace:^", 30 | "encode-monitor-utils": "workspace:^" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-utils", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 通用函数", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist", 15 | "README.md" 16 | ], 17 | "keywords": [ 18 | "encode", 19 | "monitor", 20 | "utils" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | }, 27 | "dependencies": { 28 | "encode-monitor-core": "workspace:^", 29 | "encode-monitor-shared": "workspace:^", 30 | "encode-monitor-types": "workspace:^" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { setUrlQuery, variableTypeDetection, generateUUID } from 'encode-monitor-utils'; 2 | import { STORAGE_KEY } from '../constant'; 3 | 4 | export function noop() {} 5 | 6 | // wx 7 | 8 | export function getDeviceId(): string { 9 | let deviceId: string = wx.getStorageSync(STORAGE_KEY.deviceId); 10 | if (!deviceId) { 11 | const deviceId = generateUUID(); 12 | wx.setStorageSync(STORAGE_KEY.deviceId, deviceId); 13 | } 14 | return deviceId; 15 | } 16 | 17 | export function getPageUrl(setQuery = true) { 18 | if (!variableTypeDetection.isFunction(getCurrentPages)) { 19 | return ''; 20 | } 21 | const pages = getCurrentPages(); // 在App里调用该方法,页面还没有生成,长度为0 22 | if (!pages.length) { 23 | return 'App'; 24 | } 25 | const page = pages[pages.length - 1]; 26 | return setQuery ? setUrlQuery(page.route, page.options) : page.route; 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-react", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 React 监控", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist", 15 | "README.md" 16 | ], 17 | "keywords": [ 18 | "encode", 19 | "monitor", 20 | "react" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | }, 27 | "dependencies": { 28 | "encode-monitor-core": "workspace:^", 29 | "encode-monitor-shared": "workspace:^", 30 | "encode-monitor-types": "workspace:^" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-vue", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 Vue 监控", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "keywords": [ 17 | "encode", 18 | "monitor", 19 | "vue" 20 | ], 21 | "author": "chenghuai", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "encode-bundle": "^1.4.1" 25 | }, 26 | "dependencies": { 27 | "encode-monitor-core": "workspace:^", 28 | "encode-monitor-shared": "workspace:^", 29 | "encode-monitor-utils": "workspace:^", 30 | "encode-monitor-types": "workspace:^" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/utils/src/Severity.ts: -------------------------------------------------------------------------------- 1 | /** 等级程度枚举 */ 2 | export enum Severity { 3 | Else = 'else', 4 | Error = 'error', 5 | Warning = 'warning', 6 | Info = 'info', 7 | Debug = 'debug', 8 | 9 | /** 上报的错误等级 */ 10 | Low = 'low', 11 | Normal = 'normal', 12 | High = 'high', 13 | Critical = 'critical', 14 | } 15 | 16 | export namespace Severity { 17 | export function fromString(level: string): Severity { 18 | switch (level) { 19 | case 'debug': 20 | return Severity.Debug; 21 | case 'info': 22 | case 'log': 23 | case 'assert': 24 | return Severity.Info; 25 | case 'warn': 26 | case 'warning': 27 | return Severity.Warning; 28 | case Severity.Low: 29 | case Severity.Normal: 30 | case Severity.High: 31 | case Severity.Critical: 32 | case 'error': 33 | return Severity.Error; 34 | default: 35 | return Severity.Else; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-wx-mini-program", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 小程序监控", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "keywords": [ 17 | "encode", 18 | "monitor", 19 | "mini" 20 | ], 21 | "author": "chenghuai", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "encode-bundle": "^1.4.1" 25 | }, 26 | "dependencies": { 27 | "encode-monitor-core": "workspace:^", 28 | "encode-monitor-utils": "workspace:^", 29 | "encode-monitor-shared": "workspace:^", 30 | "encode-monitor-types": "workspace:^" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/createReporter.ts: -------------------------------------------------------------------------------- 1 | import { IMetrics, IReportHandler, IReportData, IMetricsObj } from '../types'; 2 | 3 | /** 4 | * @param {string} sessionId 5 | * @param {string} appId 6 | * @param {string} version 7 | * @param {Function} callback 8 | * @returns {IReportHandler} 9 | */ 10 | const createReporter = 11 | (sessionId: string, appId: string, version: string, callback: Function): IReportHandler => 12 | (data: IMetrics | IMetricsObj) => { 13 | const reportData: IReportData = { 14 | sessionId, 15 | appId, 16 | version, 17 | data, 18 | timestamp: +new Date(), 19 | }; 20 | 21 | if ('requestIdleCallback' in window) { 22 | (window as any).requestIdleCallback( 23 | () => { 24 | callback(reportData); 25 | }, 26 | { timeout: 3000 }, 27 | ); 28 | } else { 29 | callback(reportData); 30 | } 31 | }; 32 | 33 | export default createReporter; 34 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-wx-mini-program-performance", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 小程序性能监控", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "keywords": [ 17 | "encode", 18 | "monitor", 19 | "mini", 20 | "performance" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | }, 27 | "dependencies": { 28 | "encode-monitor-utils": "workspace:^", 29 | "encode-monitor-shared": "workspace:^", 30 | "encode-monitor-types": "workspace:^" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-browser", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 页面监控", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist", 15 | "README.md" 16 | ], 17 | "keywords": [ 18 | "encode", 19 | "monitor", 20 | "browser" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | }, 27 | "dependencies": { 28 | "encode-monitor-shared": "workspace:^", 29 | "encode-monitor-types": "workspace:^", 30 | "encode-monitor-utils": "workspace:^", 31 | "encode-monitor-core": "workspace:^" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { IMetrics, IMetricsObj } from '../types'; 2 | import { metricsName } from '../constants'; 3 | 4 | /** 5 | * store metrics 6 | * 7 | * @class 8 | * */ 9 | class metricsStore { 10 | state: Map; 11 | 12 | constructor() { 13 | this.state = new Map(); 14 | } 15 | 16 | set(key: metricsName | string, value: IMetrics) { 17 | this.state.set(key, value); 18 | } 19 | 20 | get(key: metricsName | string): IMetrics { 21 | return this.state.get(key); 22 | } 23 | 24 | has(key: metricsName | string): boolean { 25 | return this.state.has(key); 26 | } 27 | 28 | clear() { 29 | this.state.clear(); 30 | } 31 | 32 | getValues(): IMetricsObj { 33 | return Array.from(this.state).reduce((obj, [key, value]) => { 34 | obj[key] = value; 35 | return obj; 36 | }, {}); 37 | } 38 | } 39 | 40 | export default metricsStore; 41 | -------------------------------------------------------------------------------- /packages/vue/src/types.ts: -------------------------------------------------------------------------------- 1 | import { IAnyObject } from 'encode-monitor-types'; 2 | 3 | export interface VueInstance { 4 | config?: VueConfiguration; 5 | mixin(hooks: { [key: string]: () => void }): void; 6 | util: { 7 | warn(...input: any): void; 8 | }; 9 | version: string; 10 | } 11 | export interface VueConfiguration { 12 | silent: boolean; 13 | errorHandler(err: Error, vm: ViewModel, info: string): void; 14 | warnHandler(msg: string, vm: ViewModel, trace: string): void; 15 | ignoredElements: (string | RegExp)[]; 16 | keyCodes: { [key: string]: number | number[] }; 17 | async: boolean; 18 | } 19 | export interface ViewModel { 20 | [key: string]: any; 21 | $root: Record; 22 | $options: { 23 | [key: string]: any; 24 | name?: string; 25 | // vue2.6 26 | propsData?: IAnyObject; 27 | _componentTag?: string; 28 | __file?: string; 29 | props?: IAnyObject; 30 | }; 31 | $props: Record; 32 | } 33 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | import { BreadCrumbTypes, ErrorTypes } from 'encode-monitor-shared'; 2 | import { isError, extractErrorStack, Severity } from 'encode-monitor-utils'; 3 | import { breadcrumb, transportData } from 'encode-monitor-core'; 4 | import { ReportDataType } from 'encode-monitor-types'; 5 | 6 | /** 7 | * 收集react ErrorBoundary中的错误对象 8 | * 需要用户手动在componentDidCatch中设置 9 | * @param ex ErrorBoundary中的componentDidCatch的一个参数error 10 | */ 11 | export function errorBoundaryReport(ex: any): void { 12 | if (!isError(ex)) { 13 | console.warn('传入的react error不是一个object Error'); 14 | return; 15 | } 16 | const error = extractErrorStack(ex, Severity.Normal) as ReportDataType; 17 | error.type = ErrorTypes.REACT_ERROR; 18 | breadcrumb.push({ 19 | type: BreadCrumbTypes.REACT, 20 | category: breadcrumb.getCategory(BreadCrumbTypes.REACT), 21 | data: `${error.name}: ${error.message}`, 22 | level: Severity.fromString(error.level), 23 | }); 24 | transportData.send(error); 25 | } 26 | -------------------------------------------------------------------------------- /packages/web-performance/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor-web-performance", 3 | "version": "0.0.2", 4 | "description": "印客学院--前端稳定性监控 Web 性能监控", 5 | "main": "dist/index.global.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "unpkg": "dist/index.global.js", 9 | "scripts": { 10 | "build": "encode-bundle src/index.ts --format iife,cjs,esm --dts --minify --global-name encodeMonitor", 11 | "clean": "rimraf dist node_modules" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "keywords": [ 17 | "encode", 18 | "monitor", 19 | "web", 20 | "performance" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "encode-bundle": "^1.4.1" 26 | }, 27 | "dependencies": { 28 | "encode-monitor-browser": "workspace:^", 29 | "encode-monitor-react": "workspace:^", 30 | "encode-monitor-vue": "workspace:^", 31 | "core-js": "^3.19.1", 32 | "path-to-regexp": "^6.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/utils/src/queue.ts: -------------------------------------------------------------------------------- 1 | import { voidFun } from 'encode-monitor-shared'; 2 | import { _global } from './global'; 3 | 4 | export class Queue { 5 | private micro: Promise; 6 | private stack: any[] = []; 7 | private isFlushing = false; 8 | constructor() { 9 | if (!('Promise' in _global)) return; 10 | this.micro = Promise.resolve(); 11 | } 12 | addFn(fn: voidFun): void { 13 | if (typeof fn !== 'function') return; 14 | if (!('Promise' in _global)) { 15 | fn(); 16 | return; 17 | } 18 | this.stack.push(fn); 19 | if (!this.isFlushing) { 20 | this.isFlushing = true; 21 | this.micro.then(() => this.flushStack()); 22 | } 23 | } 24 | clear() { 25 | this.stack = []; 26 | } 27 | getStack() { 28 | return this.stack; 29 | } 30 | flushStack(): void { 31 | const temp = this.stack.slice(0); 32 | this.stack.length = 0; 33 | this.isFlushing = false; 34 | for (let i = 0; i < temp.length; i++) { 35 | temp[i](); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getFlag, setFlag, silentConsoleScope, Severity } from 'encode-monitor-utils'; 2 | import { EventTypes } from 'encode-monitor-shared'; 3 | import { VueInstance, ViewModel } from './types'; 4 | import { handleVueError } from './helper'; 5 | 6 | const hasConsole = typeof console !== 'undefined'; 7 | 8 | const MonitorVue = { 9 | install(Vue: VueInstance): void { 10 | if (getFlag(EventTypes.VUE) || !Vue || !Vue.config) return; 11 | setFlag(EventTypes.VUE, true); 12 | // vue 提供 warnHandler errorHandler报错信息 13 | Vue.config.errorHandler = function (err: Error, vm: ViewModel, info: string): void { 14 | handleVueError.apply(null, [err, vm, info, Severity.Normal, Severity.Error, Vue]); 15 | if (hasConsole && !Vue.config.silent) { 16 | silentConsoleScope(() => { 17 | console.error('Error in ' + info + ': "' + err.toString() + '"', vm); 18 | console.error(err); 19 | }); 20 | } 21 | }; 22 | }, 23 | }; 24 | 25 | export { MonitorVue }; 26 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/markHandler.ts: -------------------------------------------------------------------------------- 1 | import { isPerformanceSupported } from '../utils/isSupported'; 2 | 3 | const hasMark = (markName: string) => { 4 | if (!isPerformanceSupported()) { 5 | console.error('browser do not support performance'); 6 | return; 7 | } 8 | 9 | return performance.getEntriesByName(markName).length > 0; 10 | }; 11 | 12 | const getMark = (markName: string) => { 13 | if (!isPerformanceSupported()) { 14 | console.error('browser do not support performance'); 15 | return; 16 | } 17 | 18 | return performance.getEntriesByName(markName).pop(); 19 | }; 20 | 21 | const setMark = (markName: string): void | undefined => { 22 | if (!isPerformanceSupported()) { 23 | console.error('browser do not support performance'); 24 | return; 25 | } 26 | 27 | performance.mark(markName); 28 | }; 29 | 30 | const clearMark = (markName: string): void | undefined => { 31 | if (!isPerformanceSupported()) { 32 | return; 33 | } 34 | 35 | performance.clearMarks(markName); 36 | }; 37 | 38 | export { hasMark, getMark, setMark, clearMark }; 39 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/src/load.ts: -------------------------------------------------------------------------------- 1 | import { EventTypes } from 'encode-monitor-shared'; 2 | import { 3 | HandleWxConsoleEvents, 4 | HandleNetworkEvents, 5 | HandleWxEvents, 6 | HandleWxPageEvents, 7 | } from './handleWxEvents'; 8 | import { 9 | addReplaceHandler, 10 | replaceApp, 11 | replacePage, 12 | replaceComponent, 13 | replaceBehavior, 14 | } from './replace'; 15 | 16 | export function setupReplace() { 17 | replaceApp(); 18 | replacePage(); 19 | replaceComponent(); 20 | replaceBehavior(); 21 | addReplaceHandler({ 22 | callback: (data) => HandleWxEvents.handleRoute(data), 23 | type: EventTypes.MINI_ROUTE, 24 | }); 25 | addReplaceHandler({ 26 | callback: (data) => { 27 | HandleNetworkEvents.handleRequest(data); 28 | }, 29 | type: EventTypes.XHR, 30 | }); 31 | addReplaceHandler({ 32 | callback: (data) => { 33 | HandleWxConsoleEvents.console(data); 34 | }, 35 | type: EventTypes.CONSOLE, 36 | }); 37 | addReplaceHandler({ 38 | callback: (data) => HandleWxPageEvents.onAction(data), 39 | type: EventTypes.DOM, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getFPS.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FPS 3 | * fps,a frame rate is the speed at which the browser is able to recalculate, layout and paint content to the display. 4 | * */ 5 | import { IReportHandler } from '../types'; 6 | import { metricsName } from '../constants'; 7 | import metricsStore from '../lib/store'; 8 | import calculateFps from '../lib/calculateFps'; 9 | 10 | const getFPS = (logFpsCount: number): Promise => { 11 | return calculateFps(logFpsCount); 12 | }; 13 | 14 | /** 15 | * @param {metricsStore} store 16 | * @param {Function} report 17 | * @param {number} logFpsCount 18 | * @param {boolean} immediately, if immediately is true,data will report immediately 19 | * */ 20 | export const initFPS = ( 21 | store: metricsStore, 22 | report: IReportHandler, 23 | logFpsCount: number, 24 | immediately = true, 25 | ): void => { 26 | getFPS(logFpsCount).then((fps: number) => { 27 | const metrics = { name: metricsName.FPS, value: fps }; 28 | 29 | store.set(metricsName.FPS, metrics); 30 | 31 | if (immediately) { 32 | report(metrics); 33 | } 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /examples/server/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import http from 'http'; 3 | import { port, FilePaths, ServerUrls } from './config'; 4 | import opn from 'open'; 5 | const app = express(); 6 | 7 | const url = `http://localhost:${port}/JS/index.html`; 8 | Object.entries(FilePaths).forEach(([path, resolvePath]) => { 9 | app.use(path, express.static(resolvePath)); 10 | }); 11 | 12 | // mock 13 | app.get(ServerUrls.normalGet, (req, res) => { 14 | res.send('get 正常请求响应体'); 15 | }); 16 | 17 | app.get(ServerUrls.exceptionGet, (req, res) => { 18 | res.status(500).send('get 异常响应体!!!'); 19 | }); 20 | 21 | app.post(ServerUrls.normalPost, (req, res) => { 22 | res.send('post 正常请求响应体'); 23 | }); 24 | 25 | app.post(ServerUrls.exceptionPost, (req, res) => { 26 | res.status(500).send('post 异常响应体!!!'); 27 | }); 28 | 29 | app.post(ServerUrls.errorsUpload, (req, res) => { 30 | res.send('错误上报成功'); 31 | }); 32 | 33 | const server = http.createServer(app); 34 | 35 | server.listen(port, () => {}); 36 | if (process.env.NODE_ENV === 'demo') { 37 | console.log('examples is available at: http://localhost:' + port); 38 | opn(url); 39 | } 40 | -------------------------------------------------------------------------------- /packages/types/src/common.ts: -------------------------------------------------------------------------------- 1 | import { HttpTypes } from 'encode-monitor-shared'; 2 | 3 | export interface IAnyObject { 4 | [key: string]: any; 5 | } 6 | 7 | export interface ResourceErrorTarget { 8 | src?: string; 9 | href?: string; 10 | localName?: string; 11 | } 12 | 13 | export interface MonitorHttp { 14 | type: HttpTypes; 15 | traceId?: string; 16 | method?: string; 17 | url?: string; 18 | status?: number; 19 | reqData?: any; 20 | // statusText?: string 21 | sTime?: number; 22 | elapsedTime?: number; 23 | responseText?: any; 24 | time?: number; 25 | isSdkUrl?: boolean; 26 | // for wx 27 | errMsg?: string; 28 | } 29 | 30 | export interface MonitorXMLHttpRequest extends XMLHttpRequest { 31 | [key: string]: any; 32 | monitor_xhr?: MonitorHttp; 33 | } 34 | 35 | export interface ErrorStack { 36 | args: any[]; 37 | func: string; 38 | column: number; 39 | line: number; 40 | url: string; 41 | } 42 | 43 | export interface IntegrationError { 44 | message: string; 45 | name: string; 46 | stack: ErrorStack[]; 47 | } 48 | 49 | export type TNumStrObj = number | string | object; 50 | 51 | export interface LocalStorageValue { 52 | expireTime?: number; 53 | value: T | string; 54 | } 55 | -------------------------------------------------------------------------------- /examples/server/config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | export const port = 2021; 4 | const resolveDirname = (target: string) => resolve(__dirname, target); 5 | const JsFilePath = resolveDirname('../JS'); 6 | const VueFilePath = resolveDirname('../Vue'); 7 | const ReactFilePath = resolveDirname('../React'); 8 | const Vue3FilePath = resolveDirname('../Vue3'); 9 | const WebPerformancePath = resolveDirname('../WebPerformance'); 10 | const webFilePath = resolve('./packages/web/dist'); 11 | const wxFilePath = resolve('./packages/wx-mini/dist'); 12 | const webPerfFilePath = resolve('./packages/web-performance/dist'); 13 | const browserFilePath = resolve('./packages/browser/dist'); 14 | 15 | export const FilePaths = { 16 | '/JS': JsFilePath, 17 | '/Vue': VueFilePath, 18 | '/React': ReactFilePath, 19 | '/Vue3': Vue3FilePath, 20 | '/WebPerformance': WebPerformancePath, 21 | '/webDist': webFilePath, 22 | '/wxDist': wxFilePath, 23 | '/wpDist': webPerfFilePath, 24 | '/browserDist': browserFilePath, 25 | }; 26 | 27 | export const ServerUrls = { 28 | normalGet: '/normal', 29 | exceptionGet: '/exception', 30 | normalPost: '/normal/post', 31 | exceptionPost: '/exception/post', 32 | errorsUpload: '/errors/upload', 33 | }; 34 | -------------------------------------------------------------------------------- /packages/core/src/subscribe.ts: -------------------------------------------------------------------------------- 1 | import { EventTypes, WxEvents } from 'encode-monitor-shared'; 2 | import { getFlag, getFunctionName, logger, nativeTryCatch, setFlag } from 'encode-monitor-utils'; 3 | export interface ReplaceHandler { 4 | type: EventTypes | WxEvents; 5 | callback: ReplaceCallback; 6 | } 7 | 8 | type ReplaceCallback = (data: any) => void; 9 | 10 | const handlers: { [key in EventTypes]?: ReplaceCallback[] } = {}; 11 | 12 | export function subscribeEvent(handler: ReplaceHandler): boolean { 13 | if (!handler || getFlag(handler.type)) return false; 14 | setFlag(handler.type, true); 15 | handlers[handler.type] = handlers[handler.type] || []; 16 | handlers[handler.type].push(handler.callback); 17 | return true; 18 | } 19 | 20 | export function triggerHandlers(type: EventTypes | WxEvents, data: any): void { 21 | if (!type || !handlers[type]) return; 22 | handlers[type].forEach((callback) => { 23 | nativeTryCatch( 24 | () => { 25 | callback(data); 26 | }, 27 | (e: Error) => { 28 | logger.error( 29 | `重写事件triggerHandlers的回调函数发生错误\nType:${type}\nName: ${getFunctionName( 30 | callback, 31 | )}\nError: ${e}`, 32 | ); 33 | }, 34 | ); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /examples/React/reactInit.js: -------------------------------------------------------------------------------- 1 | class ErrorBoundary extends React.Component { 2 | constructor(props) { 3 | super(props); 4 | this.state = { hasError: false }; 5 | } 6 | 7 | componentDidCatch(error, errorInfo) { 8 | encodeMonitor.errorBoundaryReport(error); 9 | if (error) { 10 | this.setState({ 11 | hasError: true, 12 | }); 13 | } 14 | } 15 | 16 | render() { 17 | if (this.state.hasError) { 18 | return React.createElement('div', null, '子组件抛出异常'); 19 | } 20 | return this.props.children; 21 | } 22 | } 23 | class BuggyCounter extends React.Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = { counter: 0 }; 27 | this.handleClick = this.handleClick.bind(this); 28 | } 29 | 30 | handleClick() { 31 | this.setState(({ counter }) => ({ 32 | counter: counter + 1, 33 | })); 34 | } 35 | 36 | render() { 37 | if (this.state.counter === 3) { 38 | throw new Error('I crashed!'); 39 | } 40 | return React.createElement('h1', { onClick: this.handleClick, id: 'numException' }, this.state.counter); 41 | } 42 | } 43 | ReactDOM.render(React.createElement(ErrorBoundary, { children: React.createElement(BuggyCounter) }), document.getElementById('root')); 44 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/core/event.ts: -------------------------------------------------------------------------------- 1 | import { WxPerformanceItemType, Listener } from '../types/index'; 2 | class Event { 3 | events: Map>; 4 | constructor() { 5 | this.events = new Map(); 6 | } 7 | on(event: WxPerformanceItemType | string, listener: (...args: any[]) => void): this { 8 | let ls = this.events.get(event) || []; 9 | ls.push(listener); 10 | this.events.set(event, ls); 11 | return this; 12 | } 13 | emit(event: WxPerformanceItemType | string, ...args: any[]): boolean { 14 | if (!this.events.has(event)) return false; 15 | let ls = this.events.get(event) || []; 16 | ls.forEach((fn) => fn.apply(this, args)); 17 | return true; 18 | } 19 | remove(event: WxPerformanceItemType | string, listener: (...args: any[]) => void): this { 20 | const ls = this.events.get(event) || []; 21 | const es = ls.filter((f) => f !== listener); 22 | this.events.set(event, es); 23 | return this; 24 | } 25 | removeAll(event: WxPerformanceItemType): this { 26 | this.events.delete(event); 27 | return this; 28 | } 29 | once(event: WxPerformanceItemType | string, listener: (...args: any[]) => void): this { 30 | const fn = (...arg: any[]) => { 31 | listener.apply(this, arg); 32 | this.remove(event, fn); 33 | }; 34 | return this.on(event, fn); 35 | } 36 | } 37 | 38 | export default Event; 39 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/calculateFps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Record FPS and take the average (frame/second) 3 | * Simulate frames 4 | * */ 5 | import { roundByFour } from '../utils'; 6 | 7 | /** 8 | * @params number 9 | * */ 10 | const calculateFps = (count: number): Promise => { 11 | return new Promise((resolve) => { 12 | let frame = 0; 13 | let lastFrameTime = +new Date(); 14 | const fpsQueue = []; 15 | let timerId = null; 16 | 17 | const calculate = () => { 18 | const now = +new Date(); 19 | 20 | frame = frame + 1; 21 | 22 | if (now > 1000 + lastFrameTime) { 23 | const fps = Math.round(frame / ((now - lastFrameTime) / 1000)); 24 | fpsQueue.push(fps); 25 | frame = 0; 26 | lastFrameTime = +new Date(); 27 | 28 | if (fpsQueue.length > count) { 29 | cancelAnimationFrame(timerId); 30 | resolve( 31 | roundByFour( 32 | fpsQueue.reduce((sum, fps) => { 33 | sum = sum + fps; 34 | return sum; 35 | }, 0) / fpsQueue.length, 36 | 2, 37 | ), 38 | ); 39 | } else { 40 | timerId = requestAnimationFrame(calculate); 41 | } 42 | } else { 43 | timerId = requestAnimationFrame(calculate); 44 | } 45 | }; 46 | 47 | calculate(); 48 | }); 49 | }; 50 | 51 | export default calculateFps; 52 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"], 3 | "stylelint.validate": ["css", "scss", "less", "acss"], 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true, 6 | "source.fixAll.stylelint": true, 7 | "source.fixAll.markdownlint": true 8 | }, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[javascriptreact]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[typescript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[typescriptreact]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[vue]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[css]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[less]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[scss]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "[html]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "[json]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | }, 40 | "[jsonc]": { 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | }, 43 | "editor.formatOnSave": true 44 | } 45 | -------------------------------------------------------------------------------- /packages/utils/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { _global, _support } from './global' 2 | const PREFIX = 'Monitor Logger' 3 | 4 | export class Logger { 5 | private enabled = false 6 | private _console: Console = {} as Console 7 | constructor() { 8 | _global.console = console || _global.console 9 | if (console || _global.console) { 10 | const logType = ['log', 'debug', 'info', 'warn', 'error', 'assert'] 11 | logType.forEach((level) => { 12 | if (!(level in _global.console)) return 13 | this._console[level] = _global.console[level] 14 | }) 15 | } 16 | } 17 | disable(): void { 18 | this.enabled = false 19 | } 20 | 21 | bindOptions(debug: boolean): void { 22 | this.enabled = debug ? true : false 23 | } 24 | 25 | enable(): void { 26 | this.enabled = true 27 | } 28 | 29 | getEnableStatus() { 30 | return this.enabled 31 | } 32 | 33 | log(...args: any[]): void { 34 | if (!this.enabled) { 35 | return 36 | } 37 | this._console.log(`${PREFIX}[Log]:`, ...args) 38 | } 39 | warn(...args: any[]): void { 40 | if (!this.enabled) { 41 | return 42 | } 43 | this._console.warn(`${PREFIX}[Warn]:`, ...args) 44 | } 45 | error(...args: any[]): void { 46 | if (!this.enabled) { 47 | return 48 | } 49 | this._console.error(`${PREFIX}[Error]:`, ...args) 50 | } 51 | } 52 | const logger = _support.logger || (_support.logger = new Logger()) 53 | export { logger } 54 | -------------------------------------------------------------------------------- /packages/core/src/external.ts: -------------------------------------------------------------------------------- 1 | import { ErrorTypes, BreadCrumbTypes } from 'encode-monitor-shared'; 2 | import { 3 | isError, 4 | extractErrorStack, 5 | getLocationHref, 6 | getTimestamp, 7 | unknownToString, 8 | isWxMiniEnv, 9 | Severity, 10 | getCurrentRoute, 11 | } from 'encode-monitor-utils'; 12 | import { transportData } from './transportData'; 13 | import { breadcrumb } from './breadcrumb'; 14 | import { TNumStrObj } from 'encode-monitor-types'; 15 | 16 | interface LogTypes { 17 | message: TNumStrObj; 18 | tag?: TNumStrObj; 19 | level?: Severity; 20 | ex?: Error | any; 21 | type?: string; 22 | } 23 | 24 | export function log({ 25 | message = 'emptyMsg', 26 | tag = '', 27 | level = Severity.Critical, 28 | ex = '', 29 | type = ErrorTypes.LOG_ERROR, 30 | }: LogTypes): void { 31 | let errorInfo = {}; 32 | if (isError(ex)) { 33 | errorInfo = extractErrorStack(ex, level); 34 | } 35 | const error = { 36 | type, 37 | level, 38 | message: unknownToString(message), 39 | name: 'Monitor.log', 40 | customTag: unknownToString(tag), 41 | time: getTimestamp(), 42 | url: isWxMiniEnv ? getCurrentRoute() : getLocationHref(), 43 | ...errorInfo, 44 | }; 45 | breadcrumb.push({ 46 | type: BreadCrumbTypes.CUSTOMER, 47 | category: breadcrumb.getCategory(BreadCrumbTypes.CUSTOMER), 48 | data: message, 49 | level: Severity.fromString(level.toString()), 50 | }); 51 | transportData.send(error); 52 | } 53 | -------------------------------------------------------------------------------- /examples/Vue3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue3 监控测试 7 | 8 | 9 |
10 |

vue3 监控测试

11 |
{{testArr}}
12 | 13 |
14 | 15 | 16 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encode-monitor", 3 | "version": "true", 4 | "description": "印客学院--前端稳定性监控", 5 | "scripts": { 6 | "preinstall": "npx only-allow pnpm", 7 | "prepare": "husky install ", 8 | "init": "pnpm install", 9 | "demo": "cross-env NODE_ENV=demo ts-node ./examples/server/index.ts", 10 | "clean": "pnpm -r --filter=./packages/* run clean", 11 | "build": "pnpm run init && pnpm -r --filter=./packages/* run build", 12 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", 13 | "pub": "pnpm run build && pnpm -r --filter=./packages/* publish", 14 | "pub:beta": "pnpm run build && pnpm -r --filter=./packages/* publish --tag beta", 15 | "encode-fe-lint-scan": "encode-fe-lint scan", 16 | "encode-fe-lint-fix": "encode-fe-lint fix" 17 | }, 18 | "keywords": [ 19 | "encode", 20 | "monitor" 21 | ], 22 | "author": "chenghuai", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@changesets/cli": "^2.26.2", 26 | "@swc/core": "^1.3.96", 27 | "@types/express": "^4.17.9", 28 | "@types/node": "^20.9.0", 29 | "@types/wechat-miniprogram": "^3.4.6", 30 | "cross-env": "^7.0.2", 31 | "encode-bundle": "^1.4.1", 32 | "encode-fe-lint": "^1.0.9", 33 | "express": "^4.17.1", 34 | "husky": "^6.0.0", 35 | "msw": "^0.24.3", 36 | "open": "^7.3.0", 37 | "ts-node": "^9.1.1" 38 | }, 39 | "publishConfig": { 40 | "access": "public", 41 | "registry": "https://registry.npmjs.org/" 42 | }, 43 | "dependencies": { 44 | "rimraf": "^5.0.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getPageInfo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Page Info 3 | * host 4 | * hostname 5 | * href 6 | * protocol 7 | * origin 8 | * port 9 | * pathname 10 | * search 11 | * hash 12 | * screen resolution 13 | * */ 14 | import { IMetrics, IPageInformation, IReportHandler } from '../types'; 15 | import { metricsName } from '../constants'; 16 | import metricsStore from '../lib/store'; 17 | 18 | const getPageInfo = (): IPageInformation => { 19 | if (!location) { 20 | console.warn('browser do not support location'); 21 | return; 22 | } 23 | 24 | const { host, hostname, href, protocol, origin, port, pathname, search, hash } = location; 25 | const { width, height } = window.screen; 26 | 27 | return { 28 | host, 29 | hostname, 30 | href, 31 | protocol, 32 | origin, 33 | port, 34 | pathname, 35 | search, 36 | hash, 37 | userAgent: 'userAgent' in navigator ? navigator.userAgent : '', 38 | screenResolution: `${width}x${height}`, 39 | }; 40 | }; 41 | 42 | /** 43 | * @param {metricsStore} store 44 | * @param {Function} report 45 | * @param {boolean} immediately, if immediately is true,data will report immediately 46 | * */ 47 | export const initPageInfo = ( 48 | store: metricsStore, 49 | report: IReportHandler, 50 | immediately = true, 51 | ): void => { 52 | const pageInfo: IPageInformation = getPageInfo(); 53 | 54 | const metrics = { name: metricsName.PI, value: pageInfo } as IMetrics; 55 | 56 | store.set(metricsName.PI, metrics); 57 | 58 | if (immediately) { 59 | report(metrics); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/index.ts: -------------------------------------------------------------------------------- 1 | import { isWxMiniEnv } from 'encode-monitor-utils'; 2 | import { 3 | initBatteryInfo, 4 | initMemoryWarning, 5 | initNetworkInfo, 6 | initWxHideReport, 7 | initWxPerformance, 8 | initWxNetwork, 9 | } from './wx/index'; 10 | import Store from './core/store'; 11 | import { version } from '../package.json'; 12 | import { WxPerformanceInitOptions } from './types/index'; 13 | 14 | class WxPerformance { 15 | appId: string; 16 | version: string; 17 | private store: Store; 18 | 19 | constructor(options: WxPerformanceInitOptions) { 20 | if (!isWxMiniEnv) { 21 | return; 22 | } 23 | const { 24 | appId, 25 | report, 26 | immediately = true, 27 | ignoreUrl, 28 | maxBreadcrumbs = 10, 29 | needNetworkStatus = true, 30 | needBatteryInfo = true, 31 | needMemoryWarning = true, 32 | onAppHideReport = true, 33 | } = options; 34 | 35 | this.appId = appId; 36 | this.version = version; 37 | 38 | const store = new Store({ appId, report, immediately, ignoreUrl, maxBreadcrumbs }); 39 | this.store = store; 40 | 41 | initBatteryInfo(store, needBatteryInfo); 42 | initNetworkInfo(store, needNetworkStatus); 43 | initMemoryWarning(store, needMemoryWarning); 44 | // 如果 immediately为false 会在appHide的时候发送数据 45 | initWxHideReport(store, immediately, onAppHideReport); 46 | initWxPerformance(store); 47 | initWxNetwork(store); 48 | } 49 | 50 | customPaint() { 51 | this.store.customPaint(); 52 | } 53 | } 54 | 55 | export { WxPerformance }; 56 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/constant/index.ts: -------------------------------------------------------------------------------- 1 | export enum EListenerTypes { 2 | Touchmove = 'touchmove', 3 | Tap = 'tap', 4 | LongTap = 'longtap', 5 | LongPress = 'longpress', 6 | } 7 | 8 | export const STORAGE_KEY = { 9 | deviceId: 'monitor--uuid', 10 | }; 11 | 12 | export enum WxPerformanceDataType { 13 | MEMORY_WARNING = 'MEMORY_WARNING', 14 | WX_PERFORMANCE = 'WX_PERFORMANCE', 15 | WX_NETWORK = 'WX_NETWORK', 16 | WX_LIFE_STYLE = 'WX_LIFE_STYLE', 17 | WX_USER_ACTION = 'WX_USER_ACTION', 18 | } 19 | 20 | export enum WxPerformanceItemType { 21 | MemoryWarning = 'WxMemory', 22 | Performance = 'WxPerformance', 23 | Network = 'WxNetwork', 24 | AppOnLaunch = 'AppOnLaunch', 25 | AppOnShow = 'AppOnShow', 26 | AppOnHide = 'AppOnHide', 27 | AppOnError = 'AppOnError', 28 | AppOnPageNotFound = 'AppOnPageNotFound', 29 | AppOnUnhandledRejection = 'AppOnUnhandledRejection', 30 | PageOnLoad = 'PageOnLoad', 31 | PageOnShow = 'PageOnShow', 32 | PageOnHide = 'PageOnHide', 33 | PageOnReady = 'PageOnReady', 34 | PageOnUnload = 'PageOnUnload', 35 | PageOnShareAppMessage = 'PageOnShareAppMessage', 36 | PageOnShareTimeline = 'PageOnShareTimeline', 37 | PageOnTabItemTap = 'PageOnTabItemTap', 38 | WaterFallFinish = 'WaterFallFinish', 39 | UserTap = 'WxUserTap', 40 | UserTouchMove = 'WxUserTouchMove', 41 | WxRequest = 'WxRequest', 42 | WxUploadFile = 'WxUploadFile', 43 | WxDownloadFile = 'WxDownloadFile', 44 | WxCustomPaint = 'WxCustomPaint', 45 | } 46 | 47 | export const WxListenerTypes = { 48 | [EListenerTypes.Tap]: WxPerformanceItemType.UserTap, 49 | [EListenerTypes.Touchmove]: WxPerformanceItemType.UserTouchMove, 50 | }; 51 | -------------------------------------------------------------------------------- /packages/utils/src/is.ts: -------------------------------------------------------------------------------- 1 | export const nativeToString = Object.prototype.toString; 2 | function isType(type: string) { 3 | return function (value: any): boolean { 4 | return nativeToString.call(value) === `[object ${type}]`; 5 | }; 6 | } 7 | 8 | /** 9 | * 检测变量类型 10 | * @param type 11 | */ 12 | export const variableTypeDetection = { 13 | isNumber: isType('Number'), 14 | isString: isType('String'), 15 | isBoolean: isType('Boolean'), 16 | isNull: isType('Null'), 17 | isUndefined: isType('Undefined'), 18 | isSymbol: isType('Symbol'), 19 | isFunction: isType('Function'), 20 | isObject: isType('Object'), 21 | isArray: isType('Array'), 22 | isProcess: isType('process'), 23 | isWindow: isType('Window'), 24 | }; 25 | 26 | export function isError(wat: any): boolean { 27 | switch (nativeToString.call(wat)) { 28 | case '[object Error]': 29 | return true; 30 | case '[object Exception]': 31 | return true; 32 | case '[object DOMException]': 33 | return true; 34 | default: 35 | return isInstanceOf(wat, Error); 36 | } 37 | } 38 | 39 | export function isEmptyObject(obj: Object): boolean { 40 | return variableTypeDetection.isObject(obj) && Object.keys(obj).length === 0; 41 | } 42 | 43 | export function isEmpty(wat: any): boolean { 44 | return ( 45 | (variableTypeDetection.isString(wat) && wat.trim() === '') || wat === undefined || wat === null 46 | ); 47 | } 48 | 49 | export function isInstanceOf(wat: any, base: any): boolean { 50 | try { 51 | return wat instanceof base; 52 | } catch (_e) { 53 | return false; 54 | } 55 | } 56 | 57 | export function isExistProperty(obj: Object, key: string | number | symbol): boolean { 58 | return obj.hasOwnProperty(key); 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # encode-monitor 2 | 3 | > 印客学院 前端监控稳定性 4 | 5 | 如何能够提升前端的稳定性,一直是一个经久不衰、且永不会过时的前端话题。我们可以确保产品的正常运行和用户体验的稳定性,针对前端功能的异常情况可以做到及时发现,为后续的故障排查和复现提供有效的线索,有助于改善系统的性能和稳定性,以及提升前端的可用性和用户体验水平。同时,可以让团队把更多的时间用于改进程序的用户体验,同时节省开发维护成本。 6 | 但就目前的前端行情下,大厂都有自己的稳定性建设方案,对于团队规模不大的同学,却鲜有关注这类问题,或感觉无可下手。因此,本套课程会从 0 开始,手把手的实现一套前端监控系统。 7 | 如果做到在当前团队内推广建设稳定性监控,不仅能够提升前端的团队稳定性的整体水位,也能使得我们可以在稳定性上达到前端技术专家的水准。 8 | 9 | 技术上: 10 | 11 | 1. 学习前端稳定性监控的完整流程; 12 | 2. 学习前端稳定性监控统计指标的思路及无埋点的实现; 13 | 3. 学习前端稳定性监控指标的上传方式的实现; 14 | 4. 掌握 Node 层作为数据清洗层,进行的常见的数据清洗规则及使用; 15 | 5. 掌握如何使用 Node 进行持久化的数据存储,及稳定性指标的展示; 16 | 17 | 学完后对工作/面试的帮助: 18 | 19 | 1. 面试上:在大厂面试中,掌握一套完整的前端稳定性建设,是作为晋升至前端专家的必要条件; 20 | 2. 后续工作上:可以让我们负责团队内的稳定性,提升个人在团队内的影响力; 21 | 22 | ## 产物 23 | 24 | 1. 前端监控 npm 包 25 | 1. [encode-monitor-browser](https://www.npmjs.com/package/encode-monitor-browser):前端稳定性监控 页面监控; 26 | 2. [encode-monitor-core](https://www.npmjs.com/package/encode-monitor-core):前端稳定性监控 核心功能; 27 | 3. [encode-monitor-react](https://www.npmjs.com/package/encode-monitor-react):前端稳定性监控 React 监控; 28 | 4. [encode-monitor-vue](https://www.npmjs.com/package/encode-monitor-vue):前端稳定性监控 Vue 监控; 29 | 5. [encode-monitor-web](https://www.npmjs.com/package/encode-monitor-web):前端稳定性监控 Web 监控; 30 | 6. [encode-monitor-web-performance](https://www.npmjs.com/package/encode-monitor-web-performance):前端稳定性监控 Web 性能监控; 31 | 7. [encode-monitor-wx-mini-program](https://www.npmjs.com/package/encode-monitor-wx-mini-program):前端稳定性监控 小程序监控; 32 | 8. [encode-monitor-wx-mini-program-performance](https://www.npmjs.com/package/encode-monitor-wx-mini-program-performance):前端稳定性监控 小程序性能监控; 33 | 2. 监控异常收集 node 服务; 34 | 3. 前端监控异常告警&界面展示; 35 | 36 | ## 技术选型 37 | 38 | - 包管理工具:pnpm; 39 | - 构建工具:encode-bundle; 40 | - 数据清洗&存储:Node 生态; 41 | -------------------------------------------------------------------------------- /packages/types/src/transportData.ts: -------------------------------------------------------------------------------- 1 | import { BreadcrumbPushData } from './breadcrumb'; 2 | import { DeviceInfo, EActionType } from './track'; 3 | 4 | export interface AuthInfo { 5 | apikey?: string; 6 | trackKey?: string; 7 | sdkVersion: string; 8 | sdkName: string; 9 | trackerId: string; 10 | } 11 | 12 | export interface TransportDataType { 13 | authInfo: AuthInfo; 14 | breadcrumb?: BreadcrumbPushData[]; 15 | data?: FinalReportType; 16 | record?: any[]; 17 | deviceInfo?: DeviceInfo; 18 | } 19 | 20 | export type FinalReportType = ReportDataType | TrackReportData; 21 | 22 | interface ICommonDataType { 23 | isTrackData?: boolean; 24 | } 25 | 26 | export interface ReportDataType extends ICommonDataType { 27 | type?: string; 28 | message?: string; 29 | url: string; 30 | name?: string; 31 | stack?: any; 32 | time?: number; 33 | errorId?: number; 34 | level: string; 35 | // ajax 36 | elapsedTime?: number; 37 | request?: { 38 | httpType?: string; 39 | traceId?: string; 40 | method: string; 41 | url: string; 42 | data: any; 43 | }; 44 | response?: { 45 | status: number; 46 | data: string; 47 | }; 48 | // vue 49 | componentName?: string; 50 | propsData?: any; 51 | customTag?: string; 52 | } 53 | 54 | export interface TrackReportData extends ICommonDataType { 55 | // uuid 56 | id?: string; 57 | // 埋点code 一般由人为传进来,可以自定义规范 58 | trackId?: string; 59 | // 埋点类型 60 | actionType: EActionType; 61 | // 埋点开始时间 62 | startTime?: number; 63 | // 埋点停留时间 64 | durationTime?: number; 65 | // 上报时间 66 | trackTime?: number; 67 | } 68 | 69 | export function isReportDataType(data: ReportDataType | TrackReportData): data is ReportDataType { 70 | return (data).actionType === undefined && !data.isTrackData; 71 | } 72 | -------------------------------------------------------------------------------- /packages/wx-miniprogram/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { setUrlQuery, variableTypeDetection } from 'encode-monitor-utils'; 2 | import { DeviceInfo } from 'encode-monitor-types'; 3 | 4 | /** 5 | * 后退时需要计算当前页面地址 6 | * @param delta 返回的页面数,如果 delta 大于现有页面数,则返回到首页 7 | */ 8 | export function getNavigateBackTargetUrl(delta: number | undefined) { 9 | if (!variableTypeDetection.isFunction(getCurrentPages)) { 10 | return ''; 11 | } 12 | const pages = getCurrentPages(); // 在App里调用该方法,页面还没有生成,长度为0 13 | if (!pages.length) { 14 | return 'App'; 15 | } 16 | delta = delta || 1; 17 | const toPage = pages[pages.length - delta]; 18 | return setUrlQuery(toPage.route, toPage.options); 19 | } 20 | 21 | /** 22 | * 返回包含id、data字符串的标签 23 | * @param e wx BaseEvent 24 | */ 25 | 26 | export function targetAsString(e: WechatMiniprogram.BaseEvent): string { 27 | const id = e.currentTarget?.id ? ` id="${e.currentTarget?.id}"` : ''; 28 | const dataSets = Object.keys(e.currentTarget.dataset).map((key) => { 29 | return `data-${key}=${e.currentTarget.dataset[key]}`; 30 | }); 31 | return ``; 32 | } 33 | 34 | export async function getWxMiniDeviceInfo(): Promise { 35 | const { pixelRatio, screenHeight, screenWidth } = wx.getSystemInfoSync(); 36 | const netType = await getWxMiniNetWrokType(); 37 | return { 38 | ratio: pixelRatio, 39 | clientHeight: screenHeight, 40 | clientWidth: screenWidth, 41 | netType, 42 | }; 43 | } 44 | 45 | export async function getWxMiniNetWrokType(): Promise { 46 | return new Promise((resolve) => { 47 | wx.getNetworkType({ 48 | success(res) { 49 | resolve(res.networkType); 50 | }, 51 | fail(err) { 52 | console.error(`获取微信小程序网络类型失败:${err}`); 53 | resolve('getNetWrokType failed'); 54 | }, 55 | }); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getNetworkInfo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Network Info 3 | * downlink,returns the effective bandwidth estimate in megabits per second, rounded to the nearest multiple of 25 kilobits per seconds. 4 | * effectiveType,returns the effective type of the connection meaning one of 'slow-2g', '2g', '3g', or '4g'. This value is determined using a combination of recently observed round-trip time and downlink values. 5 | * rtt,returns the estimated effective round-trip time of the current connection, rounded to the nearest multiple of 25 milliseconds. 6 | * */ 7 | import { INetworkInformation, IMetrics, IReportHandler } from '../types'; 8 | import { isNavigatorSupported } from '../utils/isSupported'; 9 | import { metricsName } from '../constants'; 10 | import metricsStore from '../lib/store'; 11 | 12 | const getNetworkInfo = (): INetworkInformation | undefined => { 13 | if (!isNavigatorSupported()) { 14 | console.warn('browser do not support performance'); 15 | return; 16 | } 17 | 18 | const connection = ( 19 | 'connection' in navigator ? navigator['connection'] : {} 20 | ) as INetworkInformation; 21 | 22 | const { downlink, effectiveType, rtt } = connection; 23 | 24 | return { 25 | downlink, 26 | effectiveType, 27 | rtt, 28 | }; 29 | }; 30 | 31 | /** 32 | * @param {metricsStore} store 33 | * @param {Function} report 34 | * @param {boolean} immediately, if immediately is true,data will report immediately 35 | * */ 36 | export const initNetworkInfo = ( 37 | store: metricsStore, 38 | report: IReportHandler, 39 | immediately = true, 40 | ): void => { 41 | const networkInfo: INetworkInformation = getNetworkInfo(); 42 | 43 | const metrics = { name: metricsName.NI, value: networkInfo } as IMetrics; 44 | 45 | store.set(metricsName.NI, metrics); 46 | 47 | if (immediately) { 48 | report(metrics); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /packages/browser/src/load.ts: -------------------------------------------------------------------------------- 1 | import { HandleEvents } from './handleEvents'; 2 | import { htmlElementAsString, Severity } from 'encode-monitor-utils'; 3 | import { EventTypes, BreadCrumbTypes } from 'encode-monitor-shared'; 4 | import { breadcrumb, handleConsole } from 'encode-monitor-core'; 5 | import { addReplaceHandler } from './replace'; 6 | export function setupReplace(): void { 7 | addReplaceHandler({ 8 | callback: (data) => { 9 | HandleEvents.handleHttp(data, BreadCrumbTypes.XHR); 10 | }, 11 | type: EventTypes.XHR, 12 | }); 13 | addReplaceHandler({ 14 | callback: (data) => { 15 | HandleEvents.handleHttp(data, BreadCrumbTypes.FETCH); 16 | }, 17 | type: EventTypes.FETCH, 18 | }); 19 | addReplaceHandler({ 20 | callback: (error) => { 21 | HandleEvents.handleError(error); 22 | }, 23 | type: EventTypes.ERROR, 24 | }); 25 | addReplaceHandler({ 26 | callback: (data) => { 27 | handleConsole(data); 28 | }, 29 | type: EventTypes.CONSOLE, 30 | }); 31 | addReplaceHandler({ 32 | callback: (data) => { 33 | HandleEvents.handleHistory(data); 34 | }, 35 | type: EventTypes.HISTORY, 36 | }); 37 | 38 | addReplaceHandler({ 39 | callback: (data) => { 40 | HandleEvents.handleUnhandleRejection(data); 41 | }, 42 | type: EventTypes.UNHANDLEDREJECTION, 43 | }); 44 | addReplaceHandler({ 45 | callback: (data) => { 46 | const htmlString = htmlElementAsString(data.data.activeElement as HTMLElement); 47 | if (htmlString) { 48 | breadcrumb.push({ 49 | type: BreadCrumbTypes.CLICK, 50 | category: breadcrumb.getCategory(BreadCrumbTypes.CLICK), 51 | data: htmlString, 52 | level: Severity.Info, 53 | }); 54 | } 55 | }, 56 | type: EventTypes.DOM, 57 | }); 58 | addReplaceHandler({ 59 | callback: (e: HashChangeEvent) => { 60 | HandleEvents.handleHashchange(e); 61 | }, 62 | type: EventTypes.HASHCHANGE, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /packages/web-performance/src/lib/proxyHandler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param beforeHandler 3 | * @param afterHandler 4 | */ 5 | function proxyXhr( 6 | beforeHandler: (...args: Array) => void, 7 | afterHandler: (...args: Array) => void, 8 | ): void { 9 | if ('XMLHttpRequest' in window && !window.__monitor_xhr__) { 10 | const origin = window.XMLHttpRequest; 11 | const originOpen = origin.prototype.open; 12 | window.__monitor_xhr__ = true; 13 | origin.prototype.open = function (this: XMLHttpRequest, ...args: Array) { 14 | beforeHandler && beforeHandler(args[1]); 15 | originOpen.apply(this, args); 16 | this.addEventListener('loadend', () => { 17 | afterHandler && afterHandler(args[1]); 18 | }); 19 | }; 20 | } 21 | } 22 | 23 | /** 24 | * @param beforeHandler 25 | * @param afterHandler 26 | */ 27 | function proxyFetch( 28 | beforeHandler: (...args: Array) => void, 29 | afterHandler: (...args: Array) => void, 30 | ): void { 31 | if ('fetch' in window && !window.__monitor_fetch__) { 32 | const origin = window.fetch; 33 | window.__monitor_fetch__ = true; 34 | window.fetch = function (resource: string, init: Partial) { 35 | beforeHandler && beforeHandler(resource, init); 36 | return origin.call(window, resource, init).then( 37 | (response: Response) => { 38 | afterHandler && afterHandler(resource, init); 39 | return response; 40 | }, 41 | (err: Error) => { 42 | throw err; 43 | }, 44 | ); 45 | }; 46 | } 47 | } 48 | 49 | /** 50 | * @param handler 51 | */ 52 | function proxyHistory(handler: (...arg: Array) => void) { 53 | if (window.history) { 54 | const originPushState = history.pushState; 55 | const originReplaceState = history.replaceState; 56 | 57 | history.pushState = function (...args: Array) { 58 | handler && handler(...args, 'pushState'); 59 | originPushState.apply(window.history, args); 60 | }; 61 | 62 | history.replaceState = function (...args: Array) { 63 | handler && handler(...args, 'replaceState'); 64 | originReplaceState.apply(window.history, args); 65 | }; 66 | } 67 | } 68 | 69 | export { proxyXhr, proxyFetch, proxyHistory }; 70 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/wx/handleEvents.ts: -------------------------------------------------------------------------------- 1 | import Store from '../core/store' 2 | import { WxPerformanceDataType, WxPerformanceItemType } from '../constant' 3 | import { WxPerformanceItem, WxPerformanceAnyObj } from '../types/index' 4 | 5 | function pushLife(store: Store, itemType: WxPerformanceItemType) { 6 | store.push(WxPerformanceDataType.WX_LIFE_STYLE, { itemType, timestamp: Date.now() }) 7 | } 8 | 9 | function pushAction(store: Store, data: WxPerformanceItem) { 10 | store.push(WxPerformanceDataType.WX_USER_ACTION, { ...data, timestamp: Date.now() }) 11 | } 12 | 13 | function pushNetwork(store: Store, data: WxPerformanceItem) { 14 | store.push(WxPerformanceDataType.WX_NETWORK, { ...data, timestamp: Date.now() }) 15 | } 16 | 17 | const Events = { 18 | [WxPerformanceItemType.AppOnLaunch]: function (args: any[]) { 19 | let _this = this as Store 20 | const now = Date.now() 21 | _this.setLaunchTime(now) 22 | _this.push(WxPerformanceDataType.WX_LIFE_STYLE, { itemType: WxPerformanceItemType.AppOnLaunch, timestamp: now }) 23 | }, 24 | [WxPerformanceItemType.AppOnShow]: function () { 25 | pushLife(this, WxPerformanceItemType.AppOnShow) 26 | }, 27 | [WxPerformanceItemType.PageOnLoad]: function () { 28 | pushLife(this, WxPerformanceItemType.PageOnLoad) 29 | }, 30 | [WxPerformanceItemType.PageOnReady]: function () { 31 | pushLife(this, WxPerformanceItemType.PageOnReady) 32 | }, 33 | [WxPerformanceItemType.PageOnUnload]: function () { 34 | pushLife(this, WxPerformanceItemType.PageOnUnload) 35 | }, 36 | [WxPerformanceItemType.UserTap]: function (event: WxPerformanceAnyObj) { 37 | pushAction(this, { ...event, itemType: WxPerformanceItemType.UserTap }) 38 | }, 39 | [WxPerformanceItemType.UserTouchMove]: function (event: WxPerformanceAnyObj) { 40 | pushAction(this, { ...event, itemType: WxPerformanceItemType.UserTouchMove }) 41 | }, 42 | [WxPerformanceItemType.WxRequest]: function (data: WxPerformanceItem) { 43 | pushNetwork(this, data) 44 | }, 45 | [WxPerformanceItemType.WxDownloadFile]: function (data: WxPerformanceItem) { 46 | pushNetwork(this, data) 47 | }, 48 | [WxPerformanceItemType.WxUploadFile]: function (data: WxPerformanceItem) { 49 | pushNetwork(this, data) 50 | } 51 | } 52 | export default Events 53 | -------------------------------------------------------------------------------- /packages/core/src/transformData.ts: -------------------------------------------------------------------------------- 1 | import { BreadCrumbTypes, ErrorTypes, globalVar } from 'encode-monitor-shared'; 2 | import { 3 | getLocationHref, 4 | getTimestamp, 5 | Severity, 6 | fromHttpStatus, 7 | SpanStatus, 8 | interceptStr, 9 | } from 'encode-monitor-utils'; 10 | import { ReportDataType, MonitorHttp, Replace, ResourceErrorTarget } from 'encode-monitor-types'; 11 | import { getRealPath } from './errorId'; 12 | import { breadcrumb } from './breadcrumb'; 13 | 14 | export function httpTransform(data: MonitorHttp): ReportDataType { 15 | let message = ''; 16 | const { elapsedTime, time, method, traceId, type, status } = data; 17 | const name = `${type}--${method}`; 18 | if (status === 0) { 19 | message = 20 | elapsedTime <= globalVar.crossOriginThreshold 21 | ? 'http请求失败,失败原因:跨域限制或域名不存在' 22 | : 'http请求失败,失败原因:超时'; 23 | } else { 24 | message = fromHttpStatus(status); 25 | } 26 | message = message === SpanStatus.Ok ? message : `${message} ${getRealPath(data.url)}`; 27 | return { 28 | type: ErrorTypes.FETCH_ERROR, 29 | url: getLocationHref(), 30 | time, 31 | elapsedTime, 32 | level: Severity.Low, 33 | message, 34 | name, 35 | request: { 36 | httpType: type, 37 | traceId, 38 | method, 39 | url: data.url, 40 | data: data.reqData || '', 41 | }, 42 | response: { 43 | status, 44 | data: data.responseText, 45 | }, 46 | }; 47 | } 48 | 49 | const resourceMap = { 50 | img: '图片', 51 | script: 'js脚本', 52 | }; 53 | 54 | export function resourceTransform(target: ResourceErrorTarget): ReportDataType { 55 | return { 56 | type: ErrorTypes.RESOURCE_ERROR, 57 | url: getLocationHref(), 58 | message: '资源地址: ' + (interceptStr(target.src, 120) || interceptStr(target.href, 120)), 59 | level: Severity.Low, 60 | time: getTimestamp(), 61 | name: `${resourceMap[target.localName] || target.localName}加载失败`, 62 | }; 63 | } 64 | 65 | export function handleConsole(data: Replace.TriggerConsole): void { 66 | if (globalVar.isLogAddBreadcrumb) { 67 | breadcrumb.push({ 68 | type: BreadCrumbTypes.CONSOLE, 69 | category: breadcrumb.getCategory(BreadCrumbTypes.CONSOLE), 70 | data, 71 | level: Severity.fromString(data.level), 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/wx/index.ts: -------------------------------------------------------------------------------- 1 | import Store from '../core/store' 2 | import { WxPerformanceDataType, WxPerformanceItemType } from '../constant' 3 | import HandleEvents from './handleEvents' 4 | import { replaceApp, replaceComponent, replaceNetwork, replacePage } from './replace' 5 | import { WxPerformanceItem } from '../types/index' 6 | 7 | // 内存警告 8 | export function initMemoryWarning(store: Store, need: boolean) { 9 | if (!need) return 10 | wx.onMemoryWarning((res: WechatMiniprogram.OnMemoryWarningCallbackResult) => { 11 | store.push(WxPerformanceDataType.MEMORY_WARNING, res as WxPerformanceItem) 12 | }) 13 | } 14 | 15 | // 网络状态 16 | export function noNetworkType( 17 | option?: T 18 | ): WechatMiniprogram.PromisifySuccessResult { 19 | return 20 | Promise.resolve({ 21 | networkType: 'unknown', 22 | signalStrength: 0 23 | }) 24 | } 25 | export function initNetworkInfo(store: Store, need: boolean): void { 26 | store.getNetworkType = need ? wx.getNetworkType : noNetworkType 27 | } 28 | 29 | // 电量 30 | function noBatteryInfo(): WechatMiniprogram.GetBatteryInfoSyncResult { 31 | return { 32 | level: '0', 33 | isCharging: false 34 | } 35 | } 36 | export function initBatteryInfo(store: Store, need: boolean): void { 37 | store.getBatteryInfo = need ? wx.getBatteryInfoSync : noBatteryInfo 38 | } 39 | 40 | // 微信性能 41 | export function initWxPerformance(store: Store) { 42 | const performance = wx.getPerformance() 43 | const observer = performance.createObserver((entryList) => { 44 | store.push(WxPerformanceDataType.WX_PERFORMANCE, entryList.getEntries()) 45 | }) 46 | observer.observe({ entryTypes: ['navigation', 'render', 'script'] }) 47 | } 48 | 49 | // appHide 发送 50 | export function initWxHideReport(store: Store, immediately: boolean, onAppHideReport: boolean) { 51 | if (immediately || !onAppHideReport) return 52 | wx.onAppHide(() => { 53 | store.reportLeftData() 54 | }) 55 | } 56 | 57 | // 网络请求性能和点击时间 58 | export function initWxNetwork(store: Store) { 59 | for (let k in HandleEvents) { 60 | store.on(k as WxPerformanceItemType, HandleEvents[k]) 61 | } 62 | replaceApp(store) 63 | replacePage(store) 64 | replaceComponent(store) 65 | replaceNetwork(store) 66 | } 67 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getCLS.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cumulative Layout Shift 3 | * Have you ever been reading an article online when something suddenly changes on the page? 4 | * Without warning, the text moves, and you've lost your place. 5 | * Or even worse: you're about to tap a link or a button, 6 | * but in the instant before your finger lands—BOOM—the link moves, 7 | * and you end up clicking something else! 8 | * */ 9 | import { isPerformanceObserverSupported } from '../utils/isSupported'; 10 | import observe from '../lib/observe'; 11 | import metricsStore from '../lib/store'; 12 | import { IReportHandler, LayoutShift, IMetrics, IScoreConfig } from '../types'; 13 | import { metricsName } from '../constants'; 14 | import { roundByFour } from '../utils'; 15 | import { onHidden } from '../lib/onHidden'; 16 | import calcScore from '../lib/calculateScore'; 17 | 18 | const getCLS = (cls): PerformanceObserver | undefined => { 19 | if (!isPerformanceObserverSupported()) { 20 | console.warn('browser do not support performanceObserver'); 21 | return; 22 | } 23 | 24 | const entryHandler = (entry: LayoutShift) => { 25 | if (!entry.hadRecentInput) { 26 | cls.value += entry.value; 27 | } 28 | }; 29 | 30 | return observe('layout-shift', entryHandler); 31 | }; 32 | 33 | /** 34 | * @param {metricsStore} store 35 | * @param {Function} report 36 | * @param {boolean} immediately, if immediately is true,data will report immediately 37 | * @param {IScoreConfig} scoreConfig 38 | * */ 39 | export const initCLS = ( 40 | store: metricsStore, 41 | report: IReportHandler, 42 | immediately = true, 43 | scoreConfig: IScoreConfig, 44 | ): void => { 45 | const cls = { value: 0 }; 46 | 47 | const po = getCLS(cls); 48 | 49 | const stopListening = () => { 50 | if (po?.takeRecords) { 51 | po.takeRecords().map((entry: LayoutShift) => { 52 | if (!entry.hadRecentInput) { 53 | cls.value += entry.value; 54 | } 55 | }); 56 | } 57 | po?.disconnect(); 58 | 59 | const metrics = { 60 | name: metricsName.CLS, 61 | value: roundByFour(cls.value), 62 | score: calcScore(metricsName.CLS, cls.value, scoreConfig), 63 | } as IMetrics; 64 | 65 | store.set(metricsName.CLS, metrics); 66 | 67 | if (immediately) { 68 | report(metrics); 69 | } 70 | }; 71 | 72 | onHidden(stopListening, true); 73 | }; 74 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getDeviceInfo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Device Info 3 | * deviceMemory,the deviceMemory read-only property of the Navigator interface returns the approximate amount of device memory in gigabytes. 4 | * hardwareConcurrency,the navigator.hardwareConcurrency read-only property returns the number of logical processors available to run threads on the user's computer. 5 | * jsHeapSizeLimit,the maximum size of the heap, in bytes, that is available to the context. 6 | * totalJSHeapSize,the total allocated heap size, in bytes. 7 | * usedJSHeapSize,the currently active segment of JS heap, in bytes. 8 | * userAgent,a user agent is a computer program representing a person, for example, a browser in a Web context. 9 | * */ 10 | import { IDeviceInformation, IMetrics, IReportHandler } from '../types'; 11 | import { isPerformanceSupported, isNavigatorSupported } from '../utils/isSupported'; 12 | import { convertToMB } from '../utils'; 13 | import { metricsName } from '../constants'; 14 | import metricsStore from '../lib/store'; 15 | 16 | const getDeviceInfo = (): IDeviceInformation | undefined => { 17 | if (!isPerformanceSupported()) { 18 | console.warn('browser do not support performance'); 19 | return; 20 | } 21 | 22 | if (!isNavigatorSupported()) { 23 | console.warn('browser do not support navigator'); 24 | return; 25 | } 26 | 27 | return { 28 | // @ts-ignore 29 | deviceMemory: 'deviceMemory' in navigator ? navigator['deviceMemory'] : 0, 30 | hardwareConcurrency: 'hardwareConcurrency' in navigator ? navigator['hardwareConcurrency'] : 0, 31 | jsHeapSizeLimit: 32 | 'memory' in performance ? convertToMB(performance['memory']['jsHeapSizeLimit']) : 0, 33 | totalJSHeapSize: 34 | 'memory' in performance ? convertToMB(performance['memory']['totalJSHeapSize']) : 0, 35 | usedJSHeapSize: 36 | 'memory' in performance ? convertToMB(performance['memory']['usedJSHeapSize']) : 0, 37 | }; 38 | }; 39 | 40 | /** 41 | * @param {metricsStore} store 42 | * @param {Function} report 43 | * @param {boolean} immediately, if immediately is true,data will report immediately 44 | * */ 45 | export const initDeviceInfo = ( 46 | store: metricsStore, 47 | report: IReportHandler, 48 | immediately = true, 49 | ): void => { 50 | const deviceInfo = getDeviceInfo(); 51 | const metrics = { name: metricsName.DI, value: deviceInfo } as IMetrics; 52 | 53 | store.set(metricsName.DI, metrics); 54 | 55 | if (immediately) { 56 | report(metrics); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /packages/vue/src/helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getBigVersion, 3 | getLocationHref, 4 | getTimestamp, 5 | variableTypeDetection, 6 | Severity, 7 | } from 'encode-monitor-utils'; 8 | import { ErrorTypes, BreadCrumbTypes } from 'encode-monitor-shared'; 9 | import { ViewModel, VueInstance } from './types'; 10 | import { breadcrumb, transportData } from 'encode-monitor-core'; 11 | import { ReportDataType } from 'encode-monitor-types'; 12 | 13 | export function handleVueError( 14 | err: Error, 15 | vm: ViewModel, 16 | info: string, 17 | level: Severity, 18 | breadcrumbLevel: Severity, 19 | Vue: VueInstance, 20 | ): void { 21 | const version = Vue?.version; 22 | let data: ReportDataType = { 23 | type: ErrorTypes.VUE_ERROR, 24 | message: `${err.message}(${info})`, 25 | level, 26 | url: getLocationHref(), 27 | name: err.name, 28 | stack: err.stack || [], 29 | time: getTimestamp(), 30 | }; 31 | if (variableTypeDetection.isString(version)) { 32 | switch (getBigVersion(version)) { 33 | case 2: 34 | data = { ...data, ...vue2VmHandler(vm) }; 35 | break; 36 | case 3: 37 | data = { ...data, ...vue3VmHandler(vm) }; 38 | break; 39 | default: 40 | return; 41 | break; 42 | } 43 | } 44 | breadcrumb.push({ 45 | type: BreadCrumbTypes.VUE, 46 | category: breadcrumb.getCategory(BreadCrumbTypes.VUE), 47 | data, 48 | level: breadcrumbLevel, 49 | }); 50 | transportData.send(data); 51 | } 52 | function vue2VmHandler(vm: ViewModel) { 53 | let componentName = ''; 54 | if (vm.$root === vm) { 55 | componentName = 'root'; 56 | } else { 57 | const name = vm._isVue 58 | ? (vm.$options && vm.$options.name) || (vm.$options && vm.$options._componentTag) 59 | : vm.name; 60 | componentName = 61 | (name ? 'component <' + name + '>' : 'anonymous component') + 62 | (vm._isVue && vm.$options && vm.$options.__file 63 | ? ' at ' + (vm.$options && vm.$options.__file) 64 | : ''); 65 | } 66 | return { 67 | componentName, 68 | propsData: vm.$options && vm.$options.propsData, 69 | }; 70 | } 71 | function vue3VmHandler(vm: ViewModel) { 72 | let componentName = ''; 73 | if (vm.$root === vm) { 74 | componentName = 'root'; 75 | } else { 76 | console.log(vm.$options); 77 | const name = vm.$options && vm.$options.name; 78 | componentName = name ? 'component <' + name + '>' : 'anonymous component'; 79 | } 80 | return { 81 | componentName, 82 | propsData: vm.$props, 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getFP.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * First Paint,is the time between navigation and when the browser renders the first pixels to the screen, 3 | * rendering anything that is visually different from what was on the screen prior to navigation.(https://developer.mozilla.org/en-US/docs/Glossary/First_paint) 4 | * */ 5 | import { isPerformanceObserverSupported, isPerformanceSupported } from '../utils/isSupported'; 6 | import { IMetrics, IReportHandler } from '../types'; 7 | import { roundByFour } from '../utils'; 8 | import { metricsName } from '../constants'; 9 | import metricsStore from '../lib/store'; 10 | import observe from '../lib/observe'; 11 | import getFirstHiddenTime from '../lib/getFirstHiddenTime'; 12 | import calcScore from '../lib/calculateScore'; 13 | 14 | const getFP = (): Promise | undefined => { 15 | return new Promise((resolve, reject) => { 16 | if (!isPerformanceObserverSupported()) { 17 | if (!isPerformanceSupported()) { 18 | reject(new Error('browser do not support performance')); 19 | } else { 20 | const [entry] = performance.getEntriesByName('first-paint'); 21 | 22 | if (entry) { 23 | resolve(entry); 24 | } 25 | 26 | reject(new Error('browser has no fp')); 27 | } 28 | } else { 29 | const entryHandler = (entry: PerformanceEntry) => { 30 | if (entry.name === 'first-paint') { 31 | if (po) { 32 | po.disconnect(); 33 | } 34 | 35 | if (entry.startTime < getFirstHiddenTime().timeStamp) { 36 | resolve(entry); 37 | } 38 | } 39 | }; 40 | 41 | const po = observe('paint', entryHandler); 42 | } 43 | }); 44 | }; 45 | 46 | /** 47 | * @param {metricsStore} store 48 | * @param {Function} report 49 | * @param {boolean} immediately, if immediately is true,data will report immediately 50 | * @param scoreConfig 51 | * */ 52 | export const initFP = ( 53 | store: metricsStore, 54 | report: IReportHandler, 55 | immediately = true, 56 | scoreConfig, 57 | ): void => { 58 | getFP() 59 | ?.then((entry: PerformanceEntry) => { 60 | const metrics = { 61 | name: metricsName.FP, 62 | value: roundByFour(entry.startTime, 2), 63 | score: calcScore(metricsName.FP, entry.startTime, scoreConfig), 64 | } as IMetrics; 65 | 66 | store.set(metricsName.FP, metrics); 67 | 68 | if (immediately) { 69 | report(metrics); 70 | } 71 | }) 72 | .catch((error) => { 73 | console.error(error); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getFCP.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * First Contentful Paint (FCP) is when the browser renders the first bit of content from the DOM, 3 | * providing the first feedback to the user that the page is actually loading(https://developer.mozilla.org/en-US/docs/Glossary/First_contentful_paint) 4 | * */ 5 | import { isPerformanceSupported, isPerformanceObserverSupported } from '../utils/isSupported'; 6 | import { IMetrics, IReportHandler, IScoreConfig } from '../types'; 7 | import { roundByFour } from '../utils'; 8 | import { metricsName } from '../constants'; 9 | import metricsStore from '../lib/store'; 10 | import observe from '../lib/observe'; 11 | import getFirstHiddenTime from '../lib/getFirstHiddenTime'; 12 | import calcScore from '../lib/calculateScore'; 13 | 14 | const getFCP = (): Promise => { 15 | return new Promise((resolve, reject) => { 16 | if (!isPerformanceObserverSupported()) { 17 | if (!isPerformanceSupported()) { 18 | reject(new Error('browser do not support performance')); 19 | } else { 20 | const [entry] = performance.getEntriesByName('first-contentful-paint'); 21 | 22 | if (entry) { 23 | resolve(entry); 24 | } 25 | 26 | reject(new Error('browser has no fcp')); 27 | } 28 | } else { 29 | const entryHandler = (entry: PerformanceEntry) => { 30 | if (entry.name === 'first-contentful-paint') { 31 | if (po) { 32 | po.disconnect(); 33 | } 34 | 35 | if (entry.startTime < getFirstHiddenTime().timeStamp) { 36 | resolve(entry); 37 | } 38 | } 39 | }; 40 | 41 | const po = observe('paint', entryHandler); 42 | } 43 | }); 44 | }; 45 | 46 | /** 47 | * @param {metricsStore} store 48 | * @param {Function} report 49 | * @param {boolean} immediately, if immediately is true,data will report immediately 50 | * @param scoreConfig 51 | * */ 52 | export const initFCP = ( 53 | store: metricsStore, 54 | report: IReportHandler, 55 | immediately = true, 56 | scoreConfig: IScoreConfig, 57 | ): void => { 58 | getFCP() 59 | ?.then((entry: PerformanceEntry) => { 60 | const metrics = { 61 | name: metricsName.FCP, 62 | value: roundByFour(entry.startTime, 2), 63 | score: calcScore(metricsName.FCP, entry.startTime, scoreConfig), 64 | } as IMetrics; 65 | 66 | store.set(metricsName.FCP, metrics); 67 | 68 | if (immediately) { 69 | report(metrics); 70 | } 71 | }) 72 | .catch((error) => { 73 | console.error(error); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /examples/Vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue2 监控测试 7 | 8 | 9 | 10 |
11 |

vue2 监控测试

12 | 13 | 14 | 15 |
{{test}}
16 |
17 | 18 | 19 | 20 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /packages/web-performance/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { pathToRegexp } from 'path-to-regexp'; 2 | 3 | export const roundByFour = (num: number, digits = 4) => { 4 | try { 5 | return parseFloat(num.toFixed(digits)); 6 | } catch (err) { 7 | return num; 8 | } 9 | }; 10 | 11 | export const convertToMB = (bytes: number): number | null => { 12 | if (typeof bytes !== 'number') { 13 | return null; 14 | } 15 | return roundByFour(bytes / Math.pow(1024, 2)); 16 | }; 17 | 18 | export const afterLoad = (callback) => { 19 | if (document.readyState === 'complete') { 20 | setTimeout(callback); 21 | } else { 22 | addEventListener('pageshow', callback); 23 | } 24 | }; 25 | 26 | export const beforeUnload = (callback) => { 27 | window.addEventListener('beforeunload', callback); 28 | }; 29 | 30 | export const unload = (callback) => { 31 | window.addEventListener('unload', callback); 32 | }; 33 | 34 | export const validNumber = (nums: number | Array) => { 35 | if (Array.isArray(nums)) { 36 | return nums.every((n) => n >= 0); 37 | } else { 38 | return nums >= 0; 39 | } 40 | }; 41 | 42 | export const isIncludeArr = (arr1: Array, arr2: Array): boolean => { 43 | if (!arr1 || arr1.length === 0) { 44 | return false; 45 | } 46 | 47 | if (!arr2 || arr2.length === 0) { 48 | return false; 49 | } 50 | 51 | if (arr1.length > arr2.length) { 52 | return false; 53 | } 54 | 55 | for (let i = 0; i < arr1.length; i++) { 56 | if (!arr2?.includes(arr1[i])) { 57 | return false; 58 | } 59 | } 60 | 61 | return true; 62 | }; 63 | 64 | export const isEqualArr = (arr1: Array, arr2: Array): boolean => { 65 | if (!arr1 || arr1.length === 0) { 66 | return false; 67 | } 68 | 69 | if (!arr2 || arr2.length === 0) { 70 | return false; 71 | } 72 | 73 | if (arr1.length !== arr2.length) { 74 | return false; 75 | } 76 | 77 | const sortArr1 = arr1.sort(); 78 | const sortArr2 = arr2.sort(); 79 | 80 | return sortArr1.join() === sortArr2.join(); 81 | }; 82 | 83 | export const getApiPath = (url: string): string => { 84 | const reg = /(?:http(?:s|):\/\/[^\/\s]+|)([^#?]+).*/; 85 | 86 | if (url) { 87 | return url.match(reg)?.[1]; 88 | } 89 | return ''; 90 | }; 91 | 92 | export const isExistPath = (paths: Array, target: string) => { 93 | const regArr = paths.map((path) => pathToRegexp(path)); 94 | 95 | for (let i = 0; i < regArr.length; i++) { 96 | if (regArr[i].exec(target)) { 97 | return true; 98 | } 99 | } 100 | 101 | return false; 102 | }; 103 | -------------------------------------------------------------------------------- /packages/utils/src/httpStatus.ts: -------------------------------------------------------------------------------- 1 | /** The status of an Span. */ 2 | export enum SpanStatus { 3 | /** The operation completed successfully. */ 4 | Ok = 'ok', 5 | /** Deadline expired before operation could complete. */ 6 | DeadlineExceeded = 'deadline_exceeded', 7 | /** 401 Unauthorized (actually does mean unauthenticated according to RFC 7235) */ 8 | Unauthenticated = 'unauthenticated', 9 | /** 403 Forbidden */ 10 | PermissionDenied = 'permission_denied', 11 | /** 404 Not Found. Some requested entity (file or directory) was not found. */ 12 | NotFound = 'not_found', 13 | /** 429 Too Many Requests */ 14 | ResourceExhausted = 'resource_exhausted', 15 | /** Client specified an invalid argument. 4xx. */ 16 | InvalidArgument = 'invalid_argument', 17 | /** 501 Not Implemented */ 18 | Unimplemented = 'unimplemented', 19 | /** 503 Service Unavailable */ 20 | Unavailable = 'unavailable', 21 | /** Other/generic 5xx. */ 22 | InternalError = 'internal_error', 23 | /** Unknown. Any non-standard HTTP status code. */ 24 | UnknownError = 'unknown_error', 25 | /** The operation was cancelled (typically by the user). */ 26 | Cancelled = 'cancelled', 27 | /** Already exists (409) */ 28 | AlreadyExists = 'already_exists', 29 | /** Operation was rejected because the system is not in a state required for the operation's */ 30 | FailedPrecondition = 'failed_precondition', 31 | /** The operation was aborted, typically due to a concurrency issue. */ 32 | Aborted = 'aborted', 33 | /** Operation was attempted past the valid range. */ 34 | OutOfRange = 'out_of_range', 35 | /** Unrecoverable data loss or corruption */ 36 | DataLoss = 'data_loss' 37 | } 38 | 39 | export function fromHttpStatus(httpStatus: number): SpanStatus { 40 | if (httpStatus < 400) { 41 | return SpanStatus.Ok 42 | } 43 | 44 | if (httpStatus >= 400 && httpStatus < 500) { 45 | switch (httpStatus) { 46 | case 401: 47 | return SpanStatus.Unauthenticated 48 | case 403: 49 | return SpanStatus.PermissionDenied 50 | case 404: 51 | return SpanStatus.NotFound 52 | case 409: 53 | return SpanStatus.AlreadyExists 54 | case 413: 55 | return SpanStatus.FailedPrecondition 56 | case 429: 57 | return SpanStatus.ResourceExhausted 58 | default: 59 | return SpanStatus.InvalidArgument 60 | } 61 | } 62 | 63 | if (httpStatus >= 500 && httpStatus < 600) { 64 | switch (httpStatus) { 65 | case 501: 66 | return SpanStatus.Unimplemented 67 | case 503: 68 | return SpanStatus.Unavailable 69 | case 504: 70 | return SpanStatus.DeadlineExceeded 71 | default: 72 | return SpanStatus.InternalError 73 | } 74 | } 75 | 76 | return SpanStatus.UnknownError 77 | } 78 | -------------------------------------------------------------------------------- /packages/utils/src/global.ts: -------------------------------------------------------------------------------- 1 | import { EventTypes, WxEvents } from 'encode-monitor-shared'; 2 | import { Breadcrumb, TransportData, Options } from 'encode-monitor-core'; 3 | import { Logger } from './logger'; 4 | import { variableTypeDetection } from './is'; 5 | import { DeviceInfo } from 'encode-monitor-types'; 6 | 7 | // Monitor的全局变量 8 | export interface MonitorSupport { 9 | logger: Logger; 10 | breadcrumb: Breadcrumb; 11 | transportData: TransportData; 12 | replaceFlag: { [key in EventTypes]?: boolean }; 13 | record?: any[]; 14 | deviceInfo?: DeviceInfo; 15 | options?: Options; 16 | track?: any; 17 | } 18 | 19 | interface MonitorGlobal { 20 | console?: Console; 21 | __Monitor__?: MonitorSupport; 22 | } 23 | 24 | export const isNodeEnv = variableTypeDetection.isProcess( 25 | typeof process !== 'undefined' ? process : 0, 26 | ); 27 | 28 | export const isWxMiniEnv = 29 | variableTypeDetection.isObject(typeof wx !== 'undefined' ? wx : 0) && 30 | variableTypeDetection.isFunction(typeof App !== 'undefined' ? App : 0); 31 | 32 | export const isBrowserEnv = variableTypeDetection.isWindow( 33 | typeof window !== 'undefined' ? window : 0, 34 | ); 35 | /** 36 | * 获取全局变量 37 | * 38 | * ../returns Global scope object 39 | */ 40 | export function getGlobal() { 41 | if (isBrowserEnv) return window as unknown as MonitorGlobal & T; 42 | if (isWxMiniEnv) return wx as unknown as MonitorGlobal & T; 43 | if (isNodeEnv) return process as unknown as MonitorGlobal & T; 44 | } 45 | 46 | const _global = getGlobal(); 47 | const _support = getGlobalMonitorSupport(); 48 | 49 | export { _global, _support }; 50 | 51 | _support.replaceFlag = _support.replaceFlag || {}; 52 | const replaceFlag = _support.replaceFlag; 53 | export function setFlag(replaceType: EventTypes | WxEvents, isSet: boolean): void { 54 | if (replaceFlag[replaceType]) return; 55 | replaceFlag[replaceType] = isSet; 56 | } 57 | 58 | export function getFlag(replaceType: EventTypes | WxEvents): boolean { 59 | return replaceFlag[replaceType] ? true : false; 60 | } 61 | 62 | /** 63 | * 获取全部变量__Monitor__的引用地址 64 | * 65 | * ../returns global variable of Monitor 66 | */ 67 | export function getGlobalMonitorSupport(): MonitorSupport { 68 | _global.__Monitor__ = _global.__Monitor__ || ({} as MonitorSupport); 69 | return _global.__Monitor__; 70 | } 71 | 72 | export function supportsHistory(): boolean { 73 | // NOTE: in Chrome App environment, touching history.pushState, *even inside 74 | // a try/catch block*, will cause Chrome to output an error to console.error 75 | // borrowed from: https://github.com/angular/angular.js/pull/13945/files 76 | const chrome = (_global as any).chrome; 77 | // tslint:disable-next-line:no-unsafe-any 78 | const isChromePackagedApp = chrome && chrome.app && chrome.app.runtime; 79 | const hasHistoryApi = 80 | 'history' in _global && !!_global.history.pushState && !!_global.history.replaceState; 81 | 82 | return !isChromePackagedApp && hasHistoryApi; 83 | } 84 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getFID.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FID measures the time from when a user first interacts with a page 3 | * (i.e. when they click a link, tap on a button, or use a custom, JavaScript-powered control) to the time when the browser is actually able to begin processing event handlers in response to that interaction. 4 | * */ 5 | import observe from '../lib/observe'; 6 | import getFirstHiddenTime from '../lib/getFirstHiddenTime'; 7 | import { onHidden } from '../lib/onHidden'; 8 | import { isPerformanceObserverSupported } from '../utils/isSupported'; 9 | import { metricsName } from '../constants'; 10 | import metricsStore from '../lib/store'; 11 | import { IReportHandler, IScoreConfig, PerformanceEventTiming } from '../types'; 12 | import { roundByFour } from '../utils'; 13 | import calcScore from '../lib/calculateScore'; 14 | 15 | const getFID = (): Promise | undefined => { 16 | if (!isPerformanceObserverSupported()) { 17 | console.warn('browser do not support performanceObserver'); 18 | return; 19 | } 20 | 21 | const firstHiddenTime = getFirstHiddenTime(); 22 | 23 | return new Promise((resolve) => { 24 | // Only report FID if the page wasn't hidden prior to 25 | // the entry being dispatched. This typically happens when a 26 | // page is loaded in a background tab. 27 | const eventHandler = (entry: PerformanceEventTiming) => { 28 | if (entry.startTime < firstHiddenTime.timeStamp) { 29 | if (po) { 30 | po.disconnect(); 31 | } 32 | 33 | resolve(entry); 34 | } 35 | }; 36 | 37 | const po = observe('first-input', eventHandler); 38 | 39 | if (po) { 40 | onHidden(() => { 41 | if (po?.takeRecords) { 42 | po.takeRecords().map(eventHandler); 43 | } 44 | po.disconnect(); 45 | }, true); 46 | } 47 | }); 48 | }; 49 | 50 | /** 51 | * @param {metricsStore} store 52 | * @param {Function} report 53 | * @param {boolean} immediately, if immediately is true,data will report immediately 54 | * @param {IScoreConfig} scoreConfig 55 | * */ 56 | export const initFID = ( 57 | store: metricsStore, 58 | report: IReportHandler, 59 | immediately = true, 60 | scoreConfig: IScoreConfig, 61 | ): void => { 62 | getFID()?.then((entry: PerformanceEventTiming) => { 63 | const metrics = { 64 | name: metricsName.FID, 65 | value: { 66 | eventName: entry.name, 67 | targetCls: entry.target?.className, 68 | startTime: roundByFour(entry.startTime, 2), 69 | delay: roundByFour(entry.processingStart - entry.startTime, 2), 70 | eventHandleTime: roundByFour(entry.processingEnd - entry.processingStart, 2), 71 | }, 72 | score: calcScore( 73 | metricsName.FID, 74 | roundByFour(entry.processingStart - entry.startTime, 2), 75 | scoreConfig, 76 | ), 77 | }; 78 | 79 | store.set(metricsName.FID, metrics); 80 | 81 | if (immediately) { 82 | report(metrics); 83 | } 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getLCP.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Why not First Meaningful Paint (FMP) 3 | * In the past we've recommended performance metrics like First Meaningful Paint (FMP) and Speed Index (SI) (both available 4 | * in Lighthouse) to help capture more of the loading experience after the initial paint, 5 | * but these metrics are complex, hard to explain, 6 | * and often wrong—meaning they still do not identify when the main content of the page has loaded. 7 | * (https://web.dev/lcp/) 8 | * 9 | * The Largest Contentful Paint (LCP) metric reports the render time of the largest image or text block visible within the viewport, 10 | * relative to when the page first started loading. 11 | * */ 12 | import { isPerformanceObserverSupported } from '../utils/isSupported'; 13 | import getFirstHiddenTime from '../lib/getFirstHiddenTime'; 14 | import { onHidden } from '../lib/onHidden'; 15 | import { metricsName } from '../constants'; 16 | import { IMetrics, IReportHandler, IScoreConfig } from '../types'; 17 | import metricsStore from '../lib/store'; 18 | import { roundByFour } from '../utils'; 19 | import observe from '../lib/observe'; 20 | import calcScore from '../lib/calculateScore'; 21 | 22 | const getLCP = (lcp): PerformanceObserver | undefined => { 23 | if (!isPerformanceObserverSupported()) { 24 | console.warn('browser do not support performanceObserver'); 25 | return; 26 | } 27 | 28 | const firstHiddenTime = getFirstHiddenTime(); 29 | 30 | const entryHandler = (entry: PerformanceEntry) => { 31 | if (entry.startTime < firstHiddenTime.timeStamp) { 32 | lcp.value = entry; 33 | } 34 | }; 35 | 36 | return observe('largest-contentful-paint', entryHandler); 37 | }; 38 | 39 | /** 40 | * @param {metricsStore} store 41 | * @param {Function} report 42 | * @param {boolean} immediately, if immediately is true,data will report immediately 43 | * @param {IScoreConfig} scoreConfig 44 | * */ 45 | export const initLCP = ( 46 | store: metricsStore, 47 | report: IReportHandler, 48 | immediately = true, 49 | scoreConfig: IScoreConfig, 50 | ): void => { 51 | const lcp = { value: {} as PerformanceEntry }; 52 | const po = getLCP(lcp); 53 | 54 | const stopListening = () => { 55 | if (po) { 56 | if (po.takeRecords) { 57 | po.takeRecords().forEach((entry: PerformanceEntry) => { 58 | const firstHiddenTime = getFirstHiddenTime(); 59 | if (entry.startTime < firstHiddenTime.timeStamp) { 60 | lcp.value = entry; 61 | } 62 | }); 63 | } 64 | po.disconnect(); 65 | 66 | if (!store.has(metricsName.LCP)) { 67 | const value = lcp.value; 68 | const metrics = { 69 | name: metricsName.LCP, 70 | value: roundByFour(value.startTime, 2), 71 | score: calcScore(metricsName.LCP, value.startTime, scoreConfig), 72 | } as IMetrics; 73 | 74 | store.set(metricsName.LCP, metrics); 75 | 76 | if (immediately) { 77 | report(metrics); 78 | } 79 | } 80 | } 81 | }; 82 | 83 | onHidden(stopListening, true); 84 | ['click', 'keydown'].forEach((event: string) => { 85 | addEventListener(event, stopListening, { once: true, capture: true }); 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /packages/core/src/breadcrumb.ts: -------------------------------------------------------------------------------- 1 | import { BreadCrumbTypes, BreadCrumbCategory } from 'encode-monitor-shared'; 2 | import { 3 | logger, 4 | validateOption, 5 | getTimestamp, 6 | silentConsoleScope, 7 | _support, 8 | } from 'encode-monitor-utils'; 9 | import { BreadcrumbPushData, InitOptions } from 'encode-monitor-types'; 10 | 11 | export class Breadcrumb { 12 | maxBreadcrumbs = 10; 13 | beforePushBreadcrumb: unknown = null; 14 | stack: BreadcrumbPushData[] = []; 15 | constructor() {} 16 | 17 | push(data: BreadcrumbPushData): void { 18 | if (typeof this.beforePushBreadcrumb === 'function') { 19 | let result: BreadcrumbPushData = null; 20 | const beforePushBreadcrumb = this.beforePushBreadcrumb; 21 | silentConsoleScope(() => { 22 | result = beforePushBreadcrumb(this, data); 23 | }); 24 | if (!result) return; 25 | this.immediatePush(result); 26 | return; 27 | } 28 | this.immediatePush(data); 29 | } 30 | immediatePush(data: BreadcrumbPushData): void { 31 | data.time || (data.time = getTimestamp()); 32 | if (this.stack.length >= this.maxBreadcrumbs) { 33 | this.shift(); 34 | } 35 | this.stack.push(data); 36 | this.stack.sort((a, b) => a.time - b.time); 37 | logger.log(this.stack); 38 | } 39 | shift(): boolean { 40 | return this.stack.shift() !== undefined; 41 | } 42 | clear(): void { 43 | this.stack = []; 44 | } 45 | getStack(): BreadcrumbPushData[] { 46 | return this.stack; 47 | } 48 | getCategory(type: BreadCrumbTypes) { 49 | switch (type) { 50 | case BreadCrumbTypes.XHR: 51 | case BreadCrumbTypes.FETCH: 52 | return BreadCrumbCategory.HTTP; 53 | case BreadCrumbTypes.CLICK: 54 | case BreadCrumbTypes.ROUTE: 55 | case BreadCrumbTypes.TAP: 56 | case BreadCrumbTypes.TOUCHMOVE: 57 | return BreadCrumbCategory.USER; 58 | case BreadCrumbTypes.CUSTOMER: 59 | case BreadCrumbTypes.CONSOLE: 60 | return BreadCrumbCategory.DEBUG; 61 | case BreadCrumbTypes.APP_ON_LAUNCH: 62 | case BreadCrumbTypes.APP_ON_SHOW: 63 | case BreadCrumbTypes.APP_ON_HIDE: 64 | case BreadCrumbTypes.PAGE_ON_SHOW: 65 | case BreadCrumbTypes.PAGE_ON_HIDE: 66 | case BreadCrumbTypes.PAGE_ON_SHARE_APP_MESSAGE: 67 | case BreadCrumbTypes.PAGE_ON_SHARE_TIMELINE: 68 | case BreadCrumbTypes.PAGE_ON_TAB_ITEM_TAP: 69 | return BreadCrumbCategory.LIFECYCLE; 70 | case BreadCrumbTypes.UNHANDLEDREJECTION: 71 | case BreadCrumbTypes.CODE_ERROR: 72 | case BreadCrumbTypes.RESOURCE: 73 | case BreadCrumbTypes.VUE: 74 | case BreadCrumbTypes.REACT: 75 | default: 76 | return BreadCrumbCategory.EXCEPTION; 77 | } 78 | } 79 | bindOptions(options: InitOptions = {}): void { 80 | const { maxBreadcrumbs, beforePushBreadcrumb } = options; 81 | validateOption(maxBreadcrumbs, 'maxBreadcrumbs', 'number') && 82 | (this.maxBreadcrumbs = maxBreadcrumbs); 83 | validateOption(beforePushBreadcrumb, 'beforePushBreadcrumb', 'function') && 84 | (this.beforePushBreadcrumb = beforePushBreadcrumb); 85 | } 86 | } 87 | const breadcrumb = _support.breadcrumb || (_support.breadcrumb = new Breadcrumb()); 88 | export { breadcrumb }; 89 | -------------------------------------------------------------------------------- /packages/web-performance/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IConfig { 2 | appId?: string; 3 | version?: string; 4 | reportCallback: Function; 5 | immediately: boolean; 6 | isCustomEvent?: boolean; 7 | logFpsCount?: number; 8 | apiConfig?: { 9 | [prop: string]: Array; 10 | }; 11 | hashHistory?: boolean; 12 | excludeRemotePath?: Array; 13 | maxWaitCCPDuration: number; 14 | scoreConfig?: IScoreConfig; 15 | } 16 | 17 | export interface IPerformanceNavigationTiming { 18 | dnsLookup?: number; 19 | initialConnection?: number; 20 | ssl?: number; 21 | ttfb?: number; 22 | contentDownload?: number; 23 | domParse?: number; 24 | deferExecuteDuration?: number; 25 | domContentLoadedCallback?: number; 26 | resourceLoad?: number; 27 | domReady?: number; 28 | pageLoad?: number; 29 | } 30 | 31 | export interface IDeviceInformation { 32 | deviceMemory?: number; 33 | hardwareConcurrency?: number; 34 | jsHeapSizeLimit?: number; 35 | totalJSHeapSize?: number; 36 | usedJSHeapSize?: number; 37 | } 38 | 39 | export interface INetworkInformation { 40 | downlink?: number; 41 | effectiveType?: IEffectiveType; 42 | rtt?: number; 43 | } 44 | 45 | export interface IScoreConfig { 46 | [prop: string]: { median: number; p10: number }; 47 | } 48 | 49 | export interface IEffectiveType { 50 | type: '4g' | '3g' | '2g' | 'slow-2g'; 51 | } 52 | 53 | export interface IPageInformation { 54 | host: string; 55 | hostname: string; 56 | href: string; 57 | protocol: string; 58 | origin: string; 59 | port: string; 60 | pathname: string; 61 | search: string; 62 | hash: string; 63 | userAgent?: string; 64 | screenResolution: string; 65 | } 66 | 67 | export interface IMetrics { 68 | name: string; 69 | value: any; 70 | score?: number; 71 | } 72 | 73 | export interface IWebVitals { 74 | immediately: boolean; 75 | getCurrentMetrics(): IMetricsObj; 76 | setStartMark(markName: string): void; 77 | setEndMark(markName: string): void; 78 | clearMark(markName: string): void; 79 | customContentfulPaint(customMetricName: string): void; 80 | } 81 | 82 | export interface IReportHandler { 83 | (metrics: IMetrics | IMetricsObj): void; 84 | } 85 | 86 | export interface PerformanceEntryHandler { 87 | (entry: PerformanceEntry): void; 88 | } 89 | 90 | export interface PerformanceEventTiming extends PerformanceEntry { 91 | processingStart: DOMHighResTimeStamp; 92 | processingEnd: DOMHighResTimeStamp; 93 | duration: DOMHighResTimeStamp; 94 | cancelable?: boolean; 95 | target?: Element; 96 | } 97 | 98 | export interface OnHiddenCallback { 99 | (event: Event): void; 100 | } 101 | 102 | export interface OnPageChangeCallback { 103 | (event?: Event): void; 104 | } 105 | 106 | export interface IReportData { 107 | sessionId: string; 108 | appId?: string; 109 | version?: string; 110 | data: IMetrics | IMetricsObj; 111 | timestamp: number; 112 | } 113 | 114 | export interface IMetricsObj { 115 | [prop: string]: IMetrics; 116 | } 117 | 118 | export interface LayoutShift extends PerformanceEntry { 119 | value: number; 120 | hadRecentInput: boolean; 121 | } 122 | 123 | export interface Curve { 124 | median: number; 125 | podr?: number; 126 | p10?: number; 127 | } 128 | 129 | declare global { 130 | interface Window { 131 | // Build flags: 132 | __monitor_xhr__: boolean; 133 | __monitor_fetch__: boolean; 134 | __monitor_sessionId__: string; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /examples/JS/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | js 监控测试 7 | 8 | 9 | 10 | 11 | 12 |

js 监控测试

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 跳到Vue页面 23 | 跳到React页面 24 | 跳到Vue3页面 25 | 跳到WebPerformance页面 26 | 27 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /packages/web-performance/src/utils/math.ts: -------------------------------------------------------------------------------- 1 | import { Curve } from '../types'; 2 | 3 | /** 4 | * Approximates the Gauss error function, the probability that a random variable 5 | * from the standard normal distribution lies within [-x, x]. Moved from 6 | * traceviewer.b.math.erf, based on Abramowitz and Stegun, formula 7.1.26. 7 | * @param {number} x 8 | * @return {number} 9 | */ 10 | function internalErf_(x: number): number { 11 | // erf(-x) = -erf(x); 12 | const sign = x < 0 ? -1 : 1; 13 | x = Math.abs(x); 14 | 15 | const a1 = 0.254829592; 16 | const a2 = -0.284496736; 17 | const a3 = 1.421413741; 18 | const a4 = -1.453152027; 19 | const a5 = 1.061405429; 20 | const p = 0.3275911; 21 | const t = 1 / (1 + p * x); 22 | const y = t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5)))); 23 | return sign * (1 - y * Math.exp(-x * x)); 24 | } 25 | 26 | /** 27 | * Creates a log-normal distribution and finds the complementary 28 | * quantile (1-percentile) of that distribution at value. All 29 | * arguments should be in the same units (e.g. milliseconds). 30 | * 31 | * @param curve Curve 32 | * @param {number} value 33 | * @return The complement of the quantile at value. 34 | * @customization 35 | */ 36 | export function QUANTILE_AT_VALUE(curve: Curve, value): number { 37 | const { podr, median, p10 } = curve; 38 | 39 | let _podr = podr; 40 | 41 | if (!podr) { 42 | _podr = derivePodrFromP10(median, p10); 43 | } 44 | 45 | const location = Math.log(median); 46 | 47 | // The "podr" value specified the location of the smaller of the positive 48 | // roots of the third derivative of the log-normal CDF. Calculate the shape 49 | // parameter in terms of that value and the median. 50 | // See https://www.desmos.com/calculator/2t1ugwykrl 51 | const logRatio = Math.log(_podr / median); 52 | const shape = Math.sqrt(1 - 3 * logRatio - Math.sqrt((logRatio - 3) * (logRatio - 3) - 8)) / 2; 53 | 54 | const standardizedX = (Math.log(value) - location) / (Math.SQRT2 * shape); 55 | return (1 - internalErf_(standardizedX)) / 2; 56 | } 57 | 58 | /** 59 | * Approximates the inverse error function. Based on Winitzki, "A handy 60 | * approximation for the error function and its inverse" 61 | * @param {number} x 62 | * @return {number} 63 | */ 64 | function internalErfInv_(x: number): number { 65 | const sign = x < 0 ? -1 : 1; 66 | const a = 0.147; 67 | 68 | const log1x = Math.log(1 - x * x); 69 | const p1 = 2 / (Math.PI * a) + log1x / 2; 70 | const sqrtP1Log = Math.sqrt(p1 * p1 - log1x / a); 71 | return sign * Math.sqrt(sqrtP1Log - p1); 72 | } 73 | 74 | /** 75 | * Calculates the value at the given quantile. Median, podr, and 76 | * expected value should all be in the same units (e.g. milliseconds). 77 | * quantile should be within [0,1]. 78 | * 79 | * @param curve Curve 80 | * @return The value at this quantile. 81 | * @customization 82 | * @param quantile 83 | */ 84 | export function VALUE_AT_QUANTILE(curve: Curve, quantile): number { 85 | const { podr, median, p10 } = curve; 86 | 87 | let _podr = podr; 88 | 89 | if (!podr) { 90 | _podr = derivePodrFromP10(median, p10); 91 | } 92 | 93 | const location = Math.log(median); 94 | const logRatio = Math.log(_podr / median); 95 | const shape = Math.sqrt(1 - 3 * logRatio - Math.sqrt((logRatio - 3) * (logRatio - 3) - 8)) / 2; 96 | 97 | return Math.exp(location + shape * Math.SQRT2 * internalErfInv_(1 - 2 * quantile)); 98 | } 99 | 100 | // https://www.desmos.com/calculator/oqlvmezbze 101 | function derivePodrFromP10(median: number, p10: number): number { 102 | const u = Math.log(median); 103 | const shape = Math.abs(Math.log(p10) - u) / (Math.SQRT2 * 0.9061938024368232); 104 | const inner1 = -3 * shape - Math.sqrt(4 + shape * shape); 105 | return Math.exp(u + (shape / 2) * inner1); 106 | } 107 | -------------------------------------------------------------------------------- /packages/core/src/errorId.ts: -------------------------------------------------------------------------------- 1 | import { getAppId, isWxMiniEnv, variableTypeDetection } from 'encode-monitor-utils'; 2 | import { ErrorTypes, EventTypes } from 'encode-monitor-shared'; 3 | import { ReportDataType } from 'encode-monitor-types'; 4 | import { options } from './options'; 5 | const allErrorNumber: unknown = {}; 6 | /** 7 | * generate error unique Id 8 | * @param data 9 | */ 10 | export function createErrorId(data: ReportDataType, apikey: string): number | null { 11 | let id: any; 12 | switch (data.type) { 13 | case ErrorTypes.FETCH_ERROR: 14 | id = 15 | data.type + 16 | data.request.method + 17 | data.response.status + 18 | getRealPath(data.request.url) + 19 | apikey; 20 | break; 21 | case ErrorTypes.JAVASCRIPT_ERROR: 22 | case ErrorTypes.VUE_ERROR: 23 | case ErrorTypes.REACT_ERROR: 24 | id = data.type + data.name + data.message + apikey; 25 | break; 26 | case ErrorTypes.LOG_ERROR: 27 | id = data.customTag + data.type + data.name + apikey; 28 | break; 29 | case ErrorTypes.PROMISE_ERROR: 30 | id = generatePromiseErrorId(data, apikey); 31 | break; 32 | default: 33 | id = data.type + data.message + apikey; 34 | break; 35 | } 36 | id = hashCode(id); 37 | if (allErrorNumber[id] >= options.maxDuplicateCount) { 38 | return null; 39 | } 40 | if (typeof allErrorNumber[id] === 'number') { 41 | allErrorNumber[id]++; 42 | } else { 43 | allErrorNumber[id] = 1; 44 | } 45 | 46 | return id; 47 | } 48 | 49 | function generatePromiseErrorId(data: ReportDataType, apikey: string) { 50 | const locationUrl = getRealPath(data.url); 51 | if (data.name === EventTypes.UNHANDLEDREJECTION) { 52 | return data.type + objectOrder(data.message) + apikey; 53 | } 54 | return data.type + data.name + objectOrder(data.message) + locationUrl; 55 | } 56 | 57 | function objectOrder(reason: any) { 58 | const sortFn = (obj: any) => { 59 | return Object.keys(obj) 60 | .sort() 61 | .reduce((total, key) => { 62 | if (variableTypeDetection.isObject(obj[key])) { 63 | total[key] = sortFn(obj[key]); 64 | } else { 65 | total[key] = obj[key]; 66 | } 67 | return total; 68 | }, {}); 69 | }; 70 | try { 71 | if (/\{.*\}/.test(reason)) { 72 | let obj = JSON.parse(reason); 73 | obj = sortFn(obj); 74 | return JSON.stringify(obj); 75 | } 76 | } catch (error) { 77 | return reason; 78 | } 79 | } 80 | 81 | /** 82 | * http://.../project?id=1#a => http://.../project 83 | * http://.../id/123=> http://.../id/{param} 84 | * 85 | * @param url 86 | */ 87 | export function getRealPath(url: string): string { 88 | return url.replace(/[\?#].*$/, '').replace(/\/\d+([\/]*$)/, '{param}$1'); 89 | } 90 | 91 | /** 92 | * 93 | * @param url 94 | */ 95 | export function getFlutterRealOrigin(url: string): string { 96 | // for apple 97 | return removeHashPath(getFlutterRealPath(url)); 98 | } 99 | 100 | export function getFlutterRealPath(url: string): string { 101 | // for apple 102 | return url.replace(/(\S+)(\/Documents\/)(\S*)/, `$3`); 103 | } 104 | 105 | export function getRealPageOrigin(url: string): string { 106 | const fileStartReg = /^file:\/\//; 107 | if (fileStartReg.test(url)) { 108 | return getFlutterRealOrigin(url); 109 | } 110 | if (isWxMiniEnv) { 111 | return getAppId(); 112 | } 113 | return getRealPath(removeHashPath(url).replace(/(\S*)(\/\/)(\S+)/, '$3')); 114 | } 115 | 116 | export function removeHashPath(url: string): string { 117 | return url.replace(/(\S+)(\/#\/)(\S*)/, `$1`); 118 | } 119 | 120 | export function hashCode(str: string): number { 121 | let hash = 0; 122 | if (str.length == 0) return hash; 123 | for (let i = 0; i < str.length; i++) { 124 | const char = str.charCodeAt(i); 125 | hash = (hash << 5) - hash + char; 126 | hash = hash & hash; 127 | } 128 | return hash; 129 | } 130 | -------------------------------------------------------------------------------- /packages/shared/src/constant.ts: -------------------------------------------------------------------------------- 1 | export type voidFun = () => void; 2 | 3 | /** 4 | * 上报错误类型 5 | */ 6 | export enum ErrorTypes { 7 | UNKNOWN = 'UNKNOWN', 8 | UNKNOWN_FUNCTION = 'UNKNOWN_FUNCTION', 9 | JAVASCRIPT_ERROR = 'JAVASCRIPT_ERROR', 10 | LOG_ERROR = 'LOG_ERROR', 11 | FETCH_ERROR = 'HTTP_ERROR', 12 | VUE_ERROR = 'VUE_ERROR', 13 | REACT_ERROR = 'REACT_ERROR', 14 | RESOURCE_ERROR = 'RESOURCE_ERROR', 15 | PROMISE_ERROR = 'PROMISE_ERROR', 16 | ROUTE_ERROR = 'ROUTE_ERROR', 17 | } 18 | 19 | /** 20 | * 微信原生APP级事件 21 | */ 22 | export enum WxAppEvents { 23 | AppOnLaunch = 'AppOnLaunch', 24 | AppOnShow = 'AppOnShow', 25 | AppOnHide = 'AppOnHide', 26 | AppOnError = 'AppOnError', 27 | AppOnPageNotFound = 'AppOnPageNotFound', 28 | AppOnUnhandledRejection = 'AppOnUnhandledRejection', 29 | } 30 | 31 | /** 32 | * 微信原生页面级事件 33 | */ 34 | export enum WxPageEvents { 35 | PageOnLoad = 'PageOnLoad', 36 | PageOnShow = 'PageOnShow', 37 | PageOnHide = 'PageOnHide', 38 | PageOnReady = 'PageOnReady', 39 | PageOnUnload = 'PageOnUnload', 40 | PageOnShareAppMessage = 'PageOnShareAppMessage', 41 | PageOnShareTimeline = 'PageOnShareTimeline', 42 | PageOnTabItemTap = 'PageOnTabItemTap', 43 | } 44 | 45 | /** 46 | * 微信原生路由级事件 47 | */ 48 | export enum WxRouterEvents { 49 | SwitchTab = 'switchTab', 50 | ReLaunch = 'reLaunch', 51 | RedirectTo = 'redirectTo', 52 | NavigateTo = 'navigateTo', 53 | NavigateBack = 'navigateBack', 54 | NavigateToMiniProgram = 'navigateToMiniProgram', 55 | RouteFail = 'routeFail', 56 | } 57 | 58 | export type WxEvents = WxAppEvents | WxPageEvents | WxRouterEvents; 59 | 60 | export const CompositeEvents = { 61 | ...WxAppEvents, 62 | ...WxPageEvents, 63 | ...WxRouterEvents, 64 | }; 65 | 66 | export type CompositeEvents = typeof CompositeEvents; 67 | 68 | /** 69 | * 用户行为栈事件类型 70 | */ 71 | export enum BreadCrumbTypes { 72 | ROUTE = 'Route', 73 | CLICK = 'Click', 74 | CONSOLE = 'Console', 75 | XHR = 'Xhr', 76 | FETCH = 'Fetch', 77 | UNHANDLEDREJECTION = 'Unhandledrejection', 78 | VUE = 'Vue', 79 | REACT = 'React', 80 | RESOURCE = 'Resource', 81 | CODE_ERROR = 'Code Error', 82 | CUSTOMER = 'Customer', 83 | 84 | APP_ON_SHOW = 'App On Show', 85 | APP_ON_LAUNCH = 'App On Launch', 86 | APP_ON_HIDE = 'App On Hide', 87 | PAGE_ON_SHOW = 'Page On Show', 88 | PAGE_ON_HIDE = 'Page On Hide', 89 | PAGE_ON_UNLOAD = 'Page On Unload', 90 | PAGE_ON_SHARE_APP_MESSAGE = 'Page On Share App Message', 91 | PAGE_ON_SHARE_TIMELINE = 'Page On Share Timeline', 92 | PAGE_ON_TAB_ITEM_TAP = 'Page On Tab Item Tap', 93 | 94 | TAP = 'Tap', 95 | TOUCHMOVE = 'Touchmove', 96 | } 97 | 98 | /** 99 | * 用户行为整合类型 100 | */ 101 | export enum BreadCrumbCategory { 102 | HTTP = 'http', 103 | USER = 'user', 104 | DEBUG = 'debug', 105 | EXCEPTION = 'exception', 106 | LIFECYCLE = 'lifecycle', 107 | } 108 | 109 | /** 110 | * 重写的事件类型 111 | */ 112 | export enum EventTypes { 113 | XHR = 'xhr', 114 | FETCH = 'fetch', 115 | CONSOLE = 'console', 116 | DOM = 'dom', 117 | HISTORY = 'history', 118 | ERROR = 'error', 119 | HASHCHANGE = 'hashchange', 120 | UNHANDLEDREJECTION = 'unhandledrejection', 121 | MONITOR = 'monitor', 122 | VUE = 'Vue', 123 | MINI_ROUTE = 'miniRoute', 124 | MINI_PERFORMANCE = 'miniPerformance', 125 | MINI_MEMORY_WARNING = 'miniMemoryWarning', 126 | MINI_NETWORK_STATUS_CHANGE = 'miniNetworkStatusChange', 127 | MINI_BATTERY_INFO = 'miniBatteryInfo', 128 | } 129 | 130 | export enum HttpTypes { 131 | XHR = 'xhr', 132 | FETCH = 'fetch', 133 | } 134 | 135 | export enum HttpCodes { 136 | BAD_REQUEST = 400, 137 | UNAUTHORIZED = 401, 138 | INTERNAL_EXCEPTION = 500, 139 | } 140 | 141 | export const ERROR_TYPE_RE = 142 | /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/; 143 | 144 | const globalVar = { 145 | isLogAddBreadcrumb: true, 146 | crossOriginThreshold: 1000, 147 | }; 148 | export { globalVar }; 149 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface WxPerformanceInitOptions { 2 | /** 3 | * 应用标识 4 | */ 5 | appId: string 6 | /** 7 | * 应用版本号 8 | */ 9 | version?: string 10 | /** 11 | * 上报地址 12 | */ 13 | report: () => void 14 | /** 15 | * 是否立即上报 16 | */ 17 | immediately?: boolean 18 | /** 19 | * 可忽略URL正则 20 | */ 21 | ignoreUrl?: RegExp 22 | /** 23 | * 最大数据存储 24 | */ 25 | maxBreadcrumbs?: number 26 | /** 27 | * 是否需要网络状态 28 | */ 29 | needNetworkStatus?: boolean 30 | /** 31 | * 是否携带电池信息 32 | */ 33 | needBatteryInfo?: boolean 34 | /** 35 | * 是否需要内存警告 36 | */ 37 | needMemoryWarning?: boolean 38 | /** 39 | * 当immediately为false起效 是否需要在appHide时发送数据,默认为true 40 | */ 41 | onAppHideReport?: boolean 42 | } 43 | 44 | export type WxNetworkType = 'wifi' | '2g' | '3g' | '4g' | '5g' | 'unknown' | 'none' 45 | 46 | export enum WxPerformanceDataType { 47 | MEMORY_WARNING = 'MEMORY_WARNING', 48 | WX_PERFORMANCE = 'WX_PERFORMANCE', 49 | WX_NETWORK = 'WX_NETWORK', 50 | WX_LIFE_STYLE = 'WX_LIFE_STYLE', 51 | WX_USER_ACTION = 'WX_USER_ACTION' 52 | } 53 | 54 | export enum WxPerformanceItemType { 55 | MemoryWarning = 'WxMemory', 56 | Performance = 'WxPerformance', 57 | Network = 'WxNetwork', 58 | AppOnLaunch = 'AppOnLaunch', 59 | AppOnShow = 'AppOnShow', 60 | AppOnHide = 'AppOnHide', 61 | AppOnError = 'AppOnError', 62 | AppOnPageNotFound = 'AppOnPageNotFound', 63 | AppOnUnhandledRejection = 'AppOnUnhandledRejection', 64 | PageOnLoad = 'PageOnLoad', 65 | PageOnShow = 'PageOnShow', 66 | PageOnHide = 'PageOnHide', 67 | PageOnReady = 'PageOnReady', 68 | PageOnUnload = 'PageOnUnload', 69 | PageOnShareAppMessage = 'PageOnShareAppMessage', 70 | PageOnShareTimeline = 'PageOnShareTimeline', 71 | PageOnTabItemTap = 'PageOnTabItemTap', 72 | WaterFallFinish = 'WaterFallFinish', 73 | UserTap = 'WxUserTap', 74 | UserTouchMove = 'WxUserTouchMove', 75 | WxRequest = 'WxRequest', 76 | WxUploadFile = 'WxUploadFile', 77 | WxDownloadFile = 'WxDownloadFile', 78 | WxCustomPaint = 'WxCustomPaint' 79 | } 80 | 81 | export interface WxPerformanceAnyObj { 82 | [k: string]: any 83 | } 84 | 85 | // 内存警告 86 | export interface WxPerformanceMemoryItem { 87 | /** 内存告警等级,只有 Android 才有,对应系统宏定义 88 | * 89 | * 可选值: 90 | * - 5: TRIM_MEMORY_RUNNING_MODERATE; 91 | * - 10: TRIM_MEMORY_RUNNING_LOW; 92 | * - 15: TRIM_MEMORY_RUNNING_CRITICAL; */ 93 | level?: 5 | 10 | 15 94 | } 95 | 96 | // performance类型 97 | export interface WxPerformanceEntryObj { 98 | entryType?: 'navigation' | 'render' | 'script' // 指标类型 99 | name?: 'route' | 'appLaunch' | 'firstRender' | 'evaluateScript' // 指标名称 100 | startTime?: number // 指标调用开始时间;appLaunch为点击图标的时间 101 | duration?: number // 耗时 102 | path?: string // 路径 103 | navigationStart?: number // 路由真正响应开始时间 104 | navigationType?: 'appLaunch' | 'navigateTo' | 'switchTab' | 'redirectTo' | 'reLaunch' // 路由详细类型 105 | } 106 | 107 | // 网络类型 108 | export interface WxPerformanceNetworkItem { 109 | url?: string 110 | method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT' 111 | header?: WxPerformanceAnyObj 112 | startTime?: number 113 | endTime?: number 114 | duration?: number 115 | status?: number 116 | errMsg?: string 117 | filePath?: string 118 | } 119 | 120 | export interface WxPerformanceItem extends WxPerformanceMemoryItem, WxPerformanceNetworkItem, WxPerformanceAnyObj { 121 | itemType: WxPerformanceItemType 122 | timestamp?: number 123 | } 124 | 125 | export interface WxPerformanceData { 126 | appId: string 127 | uuid: string 128 | deviceId: string 129 | timestamp: number 130 | time: string 131 | networkType: WxNetworkType 132 | batteryLevel: string 133 | systemInfo: WechatMiniprogram.SystemInfo 134 | wxLaunch: number 135 | page: string 136 | type: WxPerformanceDataType 137 | item: null | WxPerformanceItem | Array 138 | } 139 | 140 | export type Listener = (...args: any[]) => void 141 | -------------------------------------------------------------------------------- /packages/web-performance/src/metrics/getNavigationTiming.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Page loads waterfall stream 3 | * dns lookup = domainLookupEnd - domainLookupStart 4 | * initial connection = connectEnd - connectStart 5 | * ssl = connectEnd - secureConnectionStart 6 | * ttfb = responseStart - requestStart 7 | * content download = responseEnd - responseStart 8 | * dom parse = domInteractive - responseEnd 9 | * defer execute duration = domContentLoadedEventStart - domInteractive 10 | * domContentLoadedCallback = domContentLoadedEventEnd - domContentLoadedEventStart 11 | * resource load = loadEventStart - domContentLoadedEventEnd 12 | * dom Ready = domContentLoadedEventEnd - fetchStart 13 | * page load = loadEventStart - fetchStart 14 | * */ 15 | import { IMetrics, IPerformanceNavigationTiming, IReportHandler } from '../types'; 16 | import { isPerformanceSupported, isPerformanceObserverSupported } from '../utils/isSupported'; 17 | import { metricsName } from '../constants'; 18 | import metricsStore from '../lib/store'; 19 | import observe from '../lib/observe'; 20 | import { roundByFour, validNumber } from '../utils'; 21 | 22 | const getNavigationTiming = (): Promise | undefined => { 23 | if (!isPerformanceSupported()) { 24 | console.warn('browser do not support performance'); 25 | return; 26 | } 27 | 28 | const resolveNavigationTiming = (entry: PerformanceNavigationTiming, resolve): void => { 29 | const { 30 | domainLookupStart, 31 | domainLookupEnd, 32 | connectStart, 33 | connectEnd, 34 | secureConnectionStart, 35 | requestStart, 36 | responseStart, 37 | responseEnd, 38 | domInteractive, 39 | domContentLoadedEventStart, 40 | domContentLoadedEventEnd, 41 | loadEventStart, 42 | fetchStart, 43 | } = entry; 44 | 45 | resolve({ 46 | dnsLookup: roundByFour(domainLookupEnd - domainLookupStart), 47 | initialConnection: roundByFour(connectEnd - connectStart), 48 | ssl: secureConnectionStart ? roundByFour(connectEnd - secureConnectionStart) : 0, 49 | ttfb: roundByFour(responseStart - requestStart), 50 | contentDownload: roundByFour(responseEnd - responseStart), 51 | domParse: roundByFour(domInteractive - responseEnd), 52 | deferExecuteDuration: roundByFour(domContentLoadedEventStart - domInteractive), 53 | domContentLoadedCallback: roundByFour(domContentLoadedEventEnd - domContentLoadedEventStart), 54 | resourceLoad: roundByFour(loadEventStart - domContentLoadedEventEnd), 55 | domReady: roundByFour(domContentLoadedEventEnd - fetchStart), 56 | pageLoad: roundByFour(loadEventStart - fetchStart), 57 | }); 58 | }; 59 | 60 | return new Promise((resolve) => { 61 | if ( 62 | isPerformanceObserverSupported() && 63 | PerformanceObserver.supportedEntryTypes?.includes('navigation') 64 | ) { 65 | const entryHandler = (entry: PerformanceNavigationTiming) => { 66 | if (entry.entryType === 'navigation') { 67 | if (po) { 68 | po.disconnect(); 69 | } 70 | 71 | resolveNavigationTiming(entry, resolve); 72 | } 73 | }; 74 | 75 | const po = observe('navigation', entryHandler); 76 | } else { 77 | const navigation = 78 | performance.getEntriesByType('navigation').length > 0 79 | ? performance.getEntriesByType('navigation')[0] 80 | : performance.timing; 81 | resolveNavigationTiming(navigation as PerformanceNavigationTiming, resolve); 82 | } 83 | }); 84 | }; 85 | 86 | /** 87 | * @param {metricsStore} store 88 | * @param {Function} report 89 | * @param {boolean} immediately, if immediately is true,data will report immediately 90 | * */ 91 | export const initNavigationTiming = ( 92 | store: metricsStore, 93 | report: IReportHandler, 94 | immediately = true, 95 | ): void => { 96 | getNavigationTiming()?.then((navigationTiming) => { 97 | const metrics = { name: metricsName.NT, value: navigationTiming } as IMetrics; 98 | 99 | if (validNumber(Object?.values(metrics.value))) { 100 | store.set(metricsName.NT, metrics); 101 | 102 | if (immediately) { 103 | report(metrics); 104 | } 105 | } 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /packages/web-performance/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Performance monitoring entry 3 | * */ 4 | import 'core-js/es/array/includes'; 5 | import 'core-js/es/object/values'; 6 | import { IConfig, IWebVitals, IMetricsObj } from './types'; 7 | import generateUniqueID from './utils/generateUniqueID'; 8 | import { afterLoad, beforeUnload, unload } from './utils'; 9 | import { onHidden } from './lib/onHidden'; 10 | import createReporter from './lib/createReporter'; 11 | import MetricsStore from './lib/store'; 12 | import { measure } from './lib/measureCustomMetrics'; 13 | import { setMark, clearMark, getMark, hasMark } from './lib/markHandler'; 14 | import { initNavigationTiming } from './metrics/getNavigationTiming'; 15 | import { initDeviceInfo } from './metrics/getDeviceInfo'; 16 | import { initNetworkInfo } from './metrics/getNetworkInfo'; 17 | import { initPageInfo } from './metrics/getPageInfo'; 18 | import { initFP } from './metrics/getFP'; 19 | import { initFCP } from './metrics/getFCP'; 20 | import { initFID } from './metrics/getFID'; 21 | import { initLCP } from './metrics/getLCP'; 22 | import { initFPS } from './metrics/getFPS'; 23 | import { initCLS } from './metrics/getCLS'; 24 | import { initCCP } from './metrics/getCCP'; 25 | 26 | let metricsStore: MetricsStore; 27 | let reporter: ReturnType; 28 | 29 | class WebVitals implements IWebVitals { 30 | immediately: boolean; 31 | 32 | constructor(config: IConfig) { 33 | const { 34 | appId, 35 | version, 36 | reportCallback, 37 | immediately = false, 38 | isCustomEvent = false, 39 | logFpsCount = 5, 40 | apiConfig = {}, 41 | hashHistory = true, 42 | excludeRemotePath = [], 43 | maxWaitCCPDuration = 30 * 1000, 44 | scoreConfig = {}, 45 | } = config; 46 | 47 | this.immediately = immediately; 48 | 49 | const sessionId = generateUniqueID(); 50 | window.__monitor_sessionId__ = sessionId; 51 | reporter = createReporter(sessionId, appId, version, reportCallback); 52 | metricsStore = new MetricsStore(); 53 | 54 | initPageInfo(metricsStore, reporter, immediately); 55 | initNetworkInfo(metricsStore, reporter, immediately); 56 | initDeviceInfo(metricsStore, reporter, immediately); 57 | initCLS(metricsStore, reporter, immediately, scoreConfig); 58 | initLCP(metricsStore, reporter, immediately, scoreConfig); 59 | initCCP( 60 | metricsStore, 61 | reporter, 62 | isCustomEvent, 63 | apiConfig, 64 | hashHistory, 65 | excludeRemotePath, 66 | maxWaitCCPDuration, 67 | immediately, 68 | scoreConfig, 69 | ); 70 | 71 | addEventListener( 72 | isCustomEvent ? 'custom-contentful-paint' : 'pageshow', 73 | () => { 74 | initFP(metricsStore, reporter, immediately, scoreConfig); 75 | initFCP(metricsStore, reporter, immediately, scoreConfig); 76 | }, 77 | { once: true, capture: true }, 78 | ); 79 | 80 | afterLoad(() => { 81 | initNavigationTiming(metricsStore, reporter, immediately); 82 | initFID(metricsStore, reporter, immediately, scoreConfig); 83 | initFPS(metricsStore, reporter, logFpsCount, immediately); 84 | }); 85 | 86 | // if immediately is false,report metrics when visibility and unload 87 | [beforeUnload, unload, onHidden].forEach((fn) => { 88 | fn(() => { 89 | const metrics = this.getCurrentMetrics(); 90 | if (Object.keys(metrics).length > 0 && !immediately) { 91 | reporter(metrics); 92 | } 93 | }); 94 | }); 95 | } 96 | 97 | getCurrentMetrics(): IMetricsObj { 98 | return metricsStore.getValues(); 99 | } 100 | 101 | private static dispatchCustomEvent(): void { 102 | const event = document.createEvent('Events'); 103 | event.initEvent('custom-contentful-paint', false, true); 104 | document.dispatchEvent(event); 105 | } 106 | 107 | setStartMark(markName: string) { 108 | setMark(`${markName}_start`); 109 | } 110 | 111 | setEndMark(markName: string) { 112 | setMark(`${markName}_end`); 113 | 114 | if (hasMark(`${markName}_start`)) { 115 | const value = measure(`${markName}Metrics`, markName); 116 | this.clearMark(markName); 117 | 118 | const metrics = { name: `${markName}Metrics`, value }; 119 | 120 | metricsStore.set(`${markName}Metrics`, metrics); 121 | 122 | if (this.immediately) { 123 | reporter(metrics); 124 | } 125 | } else { 126 | const value = getMark(`${markName}_end`)?.startTime; 127 | this.clearMark(markName); 128 | 129 | const metrics = { name: `${markName}Metrics`, value }; 130 | 131 | metricsStore.set(`${markName}Metrics`, metrics); 132 | 133 | if (this.immediately) { 134 | reporter(metrics); 135 | } 136 | } 137 | } 138 | 139 | clearMark(markName: string) { 140 | clearMark(`${markName}_start`); 141 | clearMark(`${markName}_end`); 142 | } 143 | 144 | customContentfulPaint() { 145 | setTimeout(() => { 146 | WebVitals.dispatchCustomEvent(); 147 | }); 148 | } 149 | } 150 | 151 | export { WebVitals }; 152 | -------------------------------------------------------------------------------- /packages/core/src/options.ts: -------------------------------------------------------------------------------- 1 | import { InitOptions } from 'encode-monitor-types'; 2 | import { 3 | generateUUID, 4 | toStringValidateOption, 5 | validateOption, 6 | _support, 7 | setSilentFlag, 8 | logger, 9 | } from 'encode-monitor-utils'; 10 | import { breadcrumb } from './breadcrumb'; 11 | import { transportData } from './transportData'; 12 | export class Options { 13 | beforeAppAjaxSend: Function = () => {}; 14 | enableTraceId: Boolean; 15 | filterXhrUrlRegExp: RegExp; 16 | includeHttpUrlTraceIdRegExp: RegExp; 17 | traceIdFieldName = 'Trace-Id'; 18 | throttleDelayTime = 0; 19 | maxDuplicateCount = 2; 20 | // wx-mini 21 | appOnLaunch: Function = () => {}; 22 | appOnShow: Function = () => {}; 23 | onPageNotFound: Function = () => {}; 24 | appOnHide: Function = () => {}; 25 | pageOnUnload: Function = () => {}; 26 | pageOnShow: Function = () => {}; 27 | pageOnHide: Function = () => {}; 28 | onShareAppMessage: Function = () => {}; 29 | onShareTimeline: Function = () => {}; 30 | onTabItemTap: Function = () => {}; 31 | // need return opitons,so defaul value is undefined 32 | wxNavigateToMiniProgram: Function; 33 | triggerWxEvent: Function = () => {}; 34 | onRouteChange?: Function; 35 | 36 | constructor() { 37 | this.enableTraceId = false; 38 | } 39 | bindOptions(options: InitOptions = {}): void { 40 | const { 41 | beforeAppAjaxSend, 42 | enableTraceId, 43 | filterXhrUrlRegExp, 44 | traceIdFieldName, 45 | throttleDelayTime, 46 | includeHttpUrlTraceIdRegExp, 47 | appOnLaunch, 48 | appOnShow, 49 | appOnHide, 50 | pageOnUnload, 51 | pageOnShow, 52 | pageOnHide, 53 | onPageNotFound, 54 | onShareAppMessage, 55 | onShareTimeline, 56 | onTabItemTap, 57 | wxNavigateToMiniProgram, 58 | triggerWxEvent, 59 | maxDuplicateCount, 60 | onRouteChange, 61 | } = options; 62 | validateOption(beforeAppAjaxSend, 'beforeAppAjaxSend', 'function') && 63 | (this.beforeAppAjaxSend = beforeAppAjaxSend); 64 | // wx-mini hooks 65 | validateOption(appOnLaunch, 'appOnLaunch', 'function') && (this.appOnLaunch = appOnLaunch); 66 | validateOption(appOnShow, 'appOnShow', 'function') && (this.appOnShow = appOnShow); 67 | validateOption(appOnHide, 'appOnHide', 'function') && (this.appOnHide = appOnHide); 68 | validateOption(pageOnUnload, 'pageOnUnload', 'function') && (this.pageOnUnload = pageOnUnload); 69 | validateOption(pageOnShow, 'pageOnShow', 'function') && (this.pageOnShow = pageOnShow); 70 | validateOption(pageOnHide, 'pageOnHide', 'function') && (this.pageOnHide = pageOnHide); 71 | validateOption(onPageNotFound, 'onPageNotFound', 'function') && 72 | (this.onPageNotFound = onPageNotFound); 73 | validateOption(onShareAppMessage, 'onShareAppMessage', 'function') && 74 | (this.onShareAppMessage = onShareAppMessage); 75 | validateOption(onShareTimeline, 'onShareTimeline', 'function') && 76 | (this.onShareTimeline = onShareTimeline); 77 | validateOption(onTabItemTap, 'onTabItemTap', 'function') && (this.onTabItemTap = onTabItemTap); 78 | validateOption(wxNavigateToMiniProgram, 'wxNavigateToMiniProgram', 'function') && 79 | (this.wxNavigateToMiniProgram = wxNavigateToMiniProgram); 80 | validateOption(triggerWxEvent, 'triggerWxEvent', 'function') && 81 | (this.triggerWxEvent = triggerWxEvent); 82 | // browser hooks 83 | validateOption(onRouteChange, 'onRouteChange', 'function') && 84 | (this.onRouteChange = onRouteChange); 85 | 86 | validateOption(enableTraceId, 'enableTraceId', 'boolean') && 87 | (this.enableTraceId = enableTraceId); 88 | validateOption(traceIdFieldName, 'traceIdFieldName', 'string') && 89 | (this.traceIdFieldName = traceIdFieldName); 90 | validateOption(throttleDelayTime, 'throttleDelayTime', 'number') && 91 | (this.throttleDelayTime = throttleDelayTime); 92 | validateOption(maxDuplicateCount, 'maxDuplicateCount', 'number') && 93 | (this.maxDuplicateCount = maxDuplicateCount); 94 | toStringValidateOption(filterXhrUrlRegExp, 'filterXhrUrlRegExp', '[object RegExp]') && 95 | (this.filterXhrUrlRegExp = filterXhrUrlRegExp); 96 | toStringValidateOption( 97 | includeHttpUrlTraceIdRegExp, 98 | 'includeHttpUrlTraceIdRegExp', 99 | '[object RegExp]', 100 | ) && (this.includeHttpUrlTraceIdRegExp = includeHttpUrlTraceIdRegExp); 101 | } 102 | } 103 | 104 | const options = _support.options || (_support.options = new Options()); 105 | 106 | export function setTraceId( 107 | httpUrl: string, 108 | callback: (headerFieldName: string, traceId: string) => void, 109 | ) { 110 | const { includeHttpUrlTraceIdRegExp, enableTraceId } = options; 111 | if (enableTraceId && includeHttpUrlTraceIdRegExp && includeHttpUrlTraceIdRegExp.test(httpUrl)) { 112 | const traceId = generateUUID(); 113 | callback(options.traceIdFieldName, traceId); 114 | } 115 | } 116 | 117 | /** 118 | * init core methods 119 | * @param paramOptions 120 | */ 121 | export function initOptions(paramOptions: InitOptions = {}) { 122 | setSilentFlag(paramOptions); 123 | breadcrumb.bindOptions(paramOptions); 124 | logger.bindOptions(paramOptions.debug); 125 | transportData.bindOptions(paramOptions); 126 | options.bindOptions(paramOptions); 127 | } 128 | 129 | export { options }; 130 | -------------------------------------------------------------------------------- /packages/browser/src/handleEvents.ts: -------------------------------------------------------------------------------- 1 | import { BreadCrumbTypes, ErrorTypes, ERROR_TYPE_RE, HttpCodes } from 'encode-monitor-shared'; 2 | import { 3 | transportData, 4 | breadcrumb, 5 | resourceTransform, 6 | httpTransform, 7 | options, 8 | } from 'encode-monitor-core'; 9 | import { 10 | getLocationHref, 11 | getTimestamp, 12 | isError, 13 | parseUrlToObj, 14 | extractErrorStack, 15 | unknownToString, 16 | Severity, 17 | } from 'encode-monitor-utils'; 18 | import { ReportDataType, Replace, MonitorHttp, ResourceErrorTarget } from 'encode-monitor-types'; 19 | 20 | const HandleEvents = { 21 | /** 22 | * 处理xhr、fetch回调 23 | */ 24 | handleHttp(data: MonitorHttp, type: BreadCrumbTypes): void { 25 | const isError = 26 | data.status === 0 || 27 | data.status === HttpCodes.BAD_REQUEST || 28 | data.status > HttpCodes.UNAUTHORIZED; 29 | const result = httpTransform(data); 30 | breadcrumb.push({ 31 | type, 32 | category: breadcrumb.getCategory(type), 33 | data: { ...result }, 34 | level: Severity.Info, 35 | time: data.time, 36 | }); 37 | if (isError) { 38 | breadcrumb.push({ 39 | type, 40 | category: breadcrumb.getCategory(BreadCrumbTypes.CODE_ERROR), 41 | data: { ...result }, 42 | level: Severity.Error, 43 | time: data.time, 44 | }); 45 | transportData.send(result); 46 | } 47 | }, 48 | /** 49 | * 处理window的error的监听回调 50 | */ 51 | handleError(errorEvent: ErrorEvent) { 52 | const target = errorEvent.target as ResourceErrorTarget; 53 | if (target.localName) { 54 | // 资源加载错误 提取有用数据 55 | const data = resourceTransform(errorEvent.target as ResourceErrorTarget); 56 | breadcrumb.push({ 57 | type: BreadCrumbTypes.RESOURCE, 58 | category: breadcrumb.getCategory(BreadCrumbTypes.RESOURCE), 59 | data, 60 | level: Severity.Error, 61 | }); 62 | return transportData.send(data); 63 | } 64 | // code error 65 | const { message, filename, lineno, colno, error } = errorEvent; 66 | let result: ReportDataType; 67 | if (error && isError(error)) { 68 | result = extractErrorStack(error, Severity.Normal); 69 | } 70 | // 处理SyntaxError,stack没有lineno、colno 71 | result || (result = HandleEvents.handleNotErrorInstance(message, filename, lineno, colno)); 72 | result.type = ErrorTypes.JAVASCRIPT_ERROR; 73 | breadcrumb.push({ 74 | type: BreadCrumbTypes.CODE_ERROR, 75 | category: breadcrumb.getCategory(BreadCrumbTypes.CODE_ERROR), 76 | data: { ...result }, 77 | level: Severity.Error, 78 | }); 79 | transportData.send(result); 80 | }, 81 | 82 | handleNotErrorInstance(message: string, filename: string, lineno: number, colno: number) { 83 | let name: string | ErrorTypes = ErrorTypes.UNKNOWN; 84 | const url = filename || getLocationHref(); 85 | let msg = message; 86 | const matches = message.match(ERROR_TYPE_RE); 87 | if (matches[1]) { 88 | name = matches[1]; 89 | msg = matches[2]; 90 | } 91 | const element = { 92 | url, 93 | func: ErrorTypes.UNKNOWN_FUNCTION, 94 | args: ErrorTypes.UNKNOWN, 95 | line: lineno, 96 | col: colno, 97 | }; 98 | return { 99 | url, 100 | name, 101 | message: msg, 102 | level: Severity.Normal, 103 | time: getTimestamp(), 104 | stack: [element], 105 | }; 106 | }, 107 | handleHistory(data: Replace.IRouter): void { 108 | const { from, to } = data; 109 | const { relative: parsedFrom } = parseUrlToObj(from); 110 | const { relative: parsedTo } = parseUrlToObj(to); 111 | breadcrumb.push({ 112 | type: BreadCrumbTypes.ROUTE, 113 | category: breadcrumb.getCategory(BreadCrumbTypes.ROUTE), 114 | data: { 115 | from: parsedFrom ? parsedFrom : '/', 116 | to: parsedTo ? parsedTo : '/', 117 | }, 118 | level: Severity.Info, 119 | }); 120 | const { onRouteChange } = options; 121 | if (onRouteChange) { 122 | onRouteChange(from, to); 123 | } 124 | }, 125 | handleHashchange(data: HashChangeEvent): void { 126 | const { oldURL, newURL } = data; 127 | const { relative: from } = parseUrlToObj(oldURL); 128 | const { relative: to } = parseUrlToObj(newURL); 129 | breadcrumb.push({ 130 | type: BreadCrumbTypes.ROUTE, 131 | category: breadcrumb.getCategory(BreadCrumbTypes.ROUTE), 132 | data: { 133 | from, 134 | to, 135 | }, 136 | level: Severity.Info, 137 | }); 138 | const { onRouteChange } = options; 139 | if (onRouteChange) { 140 | onRouteChange(from, to); 141 | } 142 | }, 143 | handleUnhandleRejection(ev: PromiseRejectionEvent): void { 144 | let data: ReportDataType = { 145 | type: ErrorTypes.PROMISE_ERROR, 146 | message: unknownToString(ev.reason), 147 | url: getLocationHref(), 148 | name: ev.type, 149 | time: getTimestamp(), 150 | level: Severity.Low, 151 | }; 152 | if (isError(ev.reason)) { 153 | data = { 154 | ...data, 155 | ...extractErrorStack(ev.reason, Severity.Low), 156 | }; 157 | } 158 | breadcrumb.push({ 159 | type: BreadCrumbTypes.UNHANDLEDREJECTION, 160 | category: breadcrumb.getCategory(BreadCrumbTypes.UNHANDLEDREJECTION), 161 | data: { ...data }, 162 | level: Severity.Error, 163 | }); 164 | transportData.send(data); 165 | }, 166 | }; 167 | 168 | export { HandleEvents }; 169 | -------------------------------------------------------------------------------- /packages/utils/src/browser.ts: -------------------------------------------------------------------------------- 1 | import { EventTypes, ErrorTypes, WxAppEvents, WxPageEvents } from 'encode-monitor-shared'; 2 | import { getLocationHref, getTimestamp } from './helpers'; 3 | import { setFlag } from './global'; 4 | import { ReportDataType, InitOptions } from 'encode-monitor-types'; 5 | import { Severity } from './Severity'; 6 | 7 | /** 8 | * 返回包含id、class、innerTextde字符串的标签 9 | * @param target html节点 10 | */ 11 | export function htmlElementAsString(target: HTMLElement): string { 12 | const tagName = target.tagName.toLowerCase(); 13 | if (tagName === 'body') { 14 | return null; 15 | } 16 | let classNames = target.classList.value; 17 | classNames = classNames !== '' ? ` class="${classNames}"` : ''; 18 | const id = target.id ? ` id="${target.id}"` : ''; 19 | const innerText = target.innerText; 20 | return `<${tagName}${id}${classNames !== '' ? classNames : ''}>${innerText}`; 21 | } 22 | 23 | /** 24 | * 将地址字符串转换成对象 25 | * @returns 返回一个对象 26 | */ 27 | export function parseUrlToObj(url: string): { 28 | host?: string; 29 | path?: string; 30 | protocol?: string; 31 | relative?: string; 32 | } { 33 | if (!url) { 34 | return {}; 35 | } 36 | 37 | const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); 38 | 39 | if (!match) { 40 | return {}; 41 | } 42 | 43 | const query = match[6] || ''; 44 | const fragment = match[8] || ''; 45 | return { 46 | host: match[4], 47 | path: match[5], 48 | protocol: match[2], 49 | relative: match[5] + query + fragment, // everything minus origin 50 | }; 51 | } 52 | 53 | export function setSilentFlag(paramOptions: InitOptions = {}): void { 54 | setFlag(EventTypes.XHR, !!paramOptions.silentXhr); 55 | setFlag(EventTypes.FETCH, !!paramOptions.silentFetch); 56 | setFlag(EventTypes.CONSOLE, !!paramOptions.silentConsole); 57 | setFlag(EventTypes.DOM, !!paramOptions.silentDom); 58 | setFlag(EventTypes.HISTORY, !!paramOptions.silentHistory); 59 | setFlag(EventTypes.ERROR, !!paramOptions.silentError); 60 | setFlag(EventTypes.HASHCHANGE, !!paramOptions.silentHashchange); 61 | setFlag(EventTypes.UNHANDLEDREJECTION, !!paramOptions.silentUnhandledrejection); 62 | setFlag(EventTypes.VUE, !!paramOptions.silentVue); 63 | // wx App 64 | setFlag(WxAppEvents.AppOnError, !!paramOptions.silentWxOnError); 65 | setFlag(WxAppEvents.AppOnUnhandledRejection, !!paramOptions.silentUnhandledrejection); 66 | setFlag(WxAppEvents.AppOnPageNotFound, !!paramOptions.silentWxOnPageNotFound); 67 | // wx Page 68 | setFlag(WxPageEvents.PageOnShareAppMessage, !!paramOptions.silentWxOnShareAppMessage); 69 | // mini Route 70 | setFlag(EventTypes.MINI_ROUTE, !!paramOptions.silentMiniRoute); 71 | } 72 | 73 | /** 74 | * 解析error的stack,并返回args、column、line、func、url: 75 | * @param ex 76 | * @param level 77 | */ 78 | export function extractErrorStack(ex: any, level: Severity): ReportDataType { 79 | const normal = { 80 | time: getTimestamp(), 81 | url: getLocationHref(), 82 | name: ex.name, 83 | level, 84 | message: ex.message, 85 | }; 86 | if (typeof ex.stack === 'undefined' || !ex.stack) { 87 | return normal; 88 | } 89 | 90 | const chrome = 91 | /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||[a-z]:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, 92 | gecko = 93 | /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i, 94 | winjs = 95 | /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, 96 | // Used to additionally parse URL/line/column from eval frames 97 | geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i, 98 | chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/, 99 | lines = ex.stack.split('\n'), 100 | stack = []; 101 | 102 | let submatch, parts, element; 103 | // reference = /^(.*) is undefined$/.exec(ex.message) 104 | 105 | for (let i = 0, j = lines.length; i < j; ++i) { 106 | if ((parts = chrome.exec(lines[i]))) { 107 | const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line 108 | const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line 109 | if (isEval && (submatch = chromeEval.exec(parts[2]))) { 110 | // throw out eval line/column and use top-most line/column number 111 | parts[2] = submatch[1]; // url 112 | parts[3] = submatch[2]; // line 113 | parts[4] = submatch[3]; // column 114 | } 115 | element = { 116 | url: !isNative ? parts[2] : null, 117 | func: parts[1] || ErrorTypes.UNKNOWN_FUNCTION, 118 | args: isNative ? [parts[2]] : [], 119 | line: parts[3] ? +parts[3] : null, 120 | column: parts[4] ? +parts[4] : null, 121 | }; 122 | } else if ((parts = winjs.exec(lines[i]))) { 123 | element = { 124 | url: parts[2], 125 | func: parts[1] || ErrorTypes.UNKNOWN_FUNCTION, 126 | args: [], 127 | line: +parts[3], 128 | column: parts[4] ? +parts[4] : null, 129 | }; 130 | } else if ((parts = gecko.exec(lines[i]))) { 131 | const isEval = parts[3] && parts[3].indexOf(' > eval') > -1; 132 | if (isEval && (submatch = geckoEval.exec(parts[3]))) { 133 | parts[3] = submatch[1]; 134 | parts[4] = submatch[2]; 135 | parts[5] = null; // no column when eval 136 | } else if (i === 0 && !parts[5] && typeof ex.columnNumber !== 'undefined') { 137 | // FireFox uses this awesome columnNumber property for its top frame 138 | // Also note, Firefox's column number is 0-based and everything else expects 1-based, 139 | // so adding 1 140 | // NOTE: this hack doesn't work if top-most frame is eval 141 | stack[0].column = ex.columnNumber + 1; 142 | } 143 | element = { 144 | url: parts[3], 145 | func: parts[1] || ErrorTypes.UNKNOWN_FUNCTION, 146 | args: parts[2] ? parts[2].split(',') : [], 147 | line: parts[4] ? +parts[4] : null, 148 | column: parts[5] ? +parts[5] : null, 149 | }; 150 | } else { 151 | continue; 152 | } 153 | 154 | if (!element.func && element.line) { 155 | element.func = ErrorTypes.UNKNOWN_FUNCTION; 156 | } 157 | 158 | stack.push(element); 159 | } 160 | 161 | if (!stack.length) { 162 | return null; 163 | } 164 | return { 165 | ...normal, 166 | stack: stack, 167 | }; 168 | } 169 | -------------------------------------------------------------------------------- /examples/WebPerformance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web 性能测试 6 | 7 | 8 | 9 |
Performance example
10 |
Move
11 | 16 | 17 | 22 |
23 | 24 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/core/store.ts: -------------------------------------------------------------------------------- 1 | import { noop, getPageUrl, getDeviceId } from '../utils'; 2 | import { generateUUID, validateOption, toStringValidateOption } from 'encode-monitor-utils'; 3 | import { WxPerformanceDataType, WxPerformanceItemType } from '../constant'; 4 | import Event from './event'; 5 | import { 6 | WxPerformanceData, 7 | WxPerformanceAnyObj, 8 | WxPerformanceInitOptions, 9 | WxNetworkType, 10 | WxPerformanceItem, 11 | WxPerformanceEntryObj, 12 | } from '../types/index'; 13 | 14 | class Store extends Event { 15 | appId: string; 16 | report: (data: Array) => void; 17 | immediately?: boolean; 18 | ignoreUrl?: RegExp; 19 | maxBreadcrumbs?: number; 20 | 21 | private stack: Array; 22 | 23 | // wx 24 | getBatteryInfo: () => WechatMiniprogram.GetBatteryInfoSyncResult; 25 | getNetworkType: < 26 | T extends WechatMiniprogram.GetNetworkTypeOption = WechatMiniprogram.GetNetworkTypeOption, 27 | >( 28 | option?: T, 29 | ) => WechatMiniprogram.PromisifySuccessResult; 30 | systemInfo: WechatMiniprogram.SystemInfo; 31 | 32 | // 小程序launch时间 33 | wxLaunchTime: number; 34 | 35 | // 首次点击标志位 36 | private firstAction: boolean = false; 37 | 38 | // 路由跳转start时间记录 39 | private navigationMap: WxPerformanceAnyObj = {}; 40 | 41 | constructor(options: WxPerformanceInitOptions) { 42 | super(); 43 | const { appId, report, maxBreadcrumbs, immediately, ignoreUrl } = options; 44 | validateOption(appId, 'appId', 'string') && (this.appId = appId); 45 | validateOption(maxBreadcrumbs, 'maxBreadcrumbs', 'number') && 46 | (this.maxBreadcrumbs = maxBreadcrumbs); 47 | toStringValidateOption(ignoreUrl, 'ignoreUrl', '[object RegExp]') && 48 | (this.ignoreUrl = ignoreUrl); 49 | validateOption(immediately, 'immediately', 'boolean') && (this.immediately = immediately); 50 | 51 | this.report = validateOption(report, 'report', 'function') ? report : noop; 52 | this.stack = []; 53 | } 54 | 55 | async _pushData(data: Array) { 56 | if (this.immediately) { 57 | this.report(data); 58 | return; 59 | } 60 | this.stack = this.stack.concat(data); 61 | if (this.stack.length >= this.maxBreadcrumbs) { 62 | this.reportLeftData(); 63 | } 64 | } 65 | 66 | async reportLeftData() { 67 | this.report([...this.stack]); 68 | this.stack = []; 69 | } 70 | 71 | _getSystemInfo(): WechatMiniprogram.SystemInfo { 72 | !this.systemInfo && (this.systemInfo = wx.getSystemInfoSync()); 73 | return this.systemInfo; 74 | } 75 | async _getNetworkType(): Promise { 76 | let nk = { 77 | networkType: 'none', 78 | errMsg: '', 79 | } as WechatMiniprogram.GetNetworkTypeSuccessCallbackResult; 80 | try { 81 | nk = await this.getNetworkType(); 82 | } catch (err) { 83 | console.warn(`getNetworkType err = `, err); 84 | } 85 | return nk.networkType; 86 | } 87 | 88 | async _createPerformanceData( 89 | type: WxPerformanceDataType, 90 | item: Array, 91 | ): Promise { 92 | const networkType = await this._getNetworkType(); 93 | const date = new Date(); 94 | 95 | return { 96 | appId: this.appId, 97 | timestamp: date.getTime(), 98 | time: date.toLocaleString(), 99 | uuid: generateUUID(), 100 | deviceId: getDeviceId(), 101 | networkType: networkType, 102 | // @ts-ignore 103 | batteryLevel: this.getBatteryInfo().level, 104 | systemInfo: this._getSystemInfo(), 105 | wxLaunch: this.wxLaunchTime, 106 | page: getPageUrl(), 107 | type: type, 108 | item: item, 109 | }; 110 | } 111 | 112 | push(type: WxPerformanceDataType, data: WxPerformanceItem | Array) { 113 | switch (type) { 114 | case WxPerformanceDataType.WX_LIFE_STYLE: 115 | case WxPerformanceDataType.WX_NETWORK: 116 | this.simpleHandle(type, data as WxPerformanceItem); 117 | break; 118 | case WxPerformanceDataType.MEMORY_WARNING: 119 | this.handleMemoryWarning(data as WechatMiniprogram.OnMemoryWarningCallbackResult); 120 | break; 121 | case WxPerformanceDataType.WX_PERFORMANCE: 122 | this.handleWxPerformance(data as Array); 123 | break; 124 | case WxPerformanceDataType.WX_USER_ACTION: 125 | this.handleWxAction(data as WxPerformanceItem); 126 | default: 127 | break; 128 | } 129 | } 130 | 131 | async simpleHandle(type: WxPerformanceDataType, data: WxPerformanceItem) { 132 | let d = await this._createPerformanceData(type as WxPerformanceDataType, [data]); 133 | this._pushData([d]); 134 | } 135 | 136 | // 内存警告会立即上报 137 | async handleMemoryWarning(data: WechatMiniprogram.OnMemoryWarningCallbackResult) { 138 | let d = await this._createPerformanceData(WxPerformanceDataType.MEMORY_WARNING, [ 139 | { ...data, itemType: WxPerformanceItemType.MemoryWarning, timestamp: Date.now() }, 140 | ]); 141 | this.report([d]); 142 | } 143 | 144 | buildNavigationStart(entry: WxPerformanceEntryObj) { 145 | if (entry.entryType === 'navigation') { 146 | // appLaunch时没有navigationStart 147 | this.navigationMap[entry.path] = entry.navigationStart || entry.startTime; 148 | } 149 | } 150 | 151 | async handleWxPerformance(data: Array = []) { 152 | let _data: Array = data.map((d) => { 153 | this.buildNavigationStart(d); 154 | d.itemType = WxPerformanceItemType.Performance; 155 | d.timestamp = Date.now(); 156 | return d; 157 | }); 158 | let item = await this._createPerformanceData(WxPerformanceDataType.WX_PERFORMANCE, _data); 159 | this._pushData([item]); 160 | } 161 | 162 | // 只统计首次点击 163 | async handleWxAction(data: WxPerformanceItem) { 164 | if (!this.firstAction) { 165 | let d = await this._createPerformanceData(WxPerformanceDataType.WX_USER_ACTION, [data]); 166 | this._pushData([d]); 167 | this.firstAction = true; 168 | } 169 | } 170 | 171 | setLaunchTime(now: number) { 172 | this.wxLaunchTime = now; 173 | } 174 | 175 | filterUrl(url: string) { 176 | if (this.ignoreUrl && this.ignoreUrl.test(url)) return true; 177 | return false; 178 | } 179 | 180 | customPaint() { 181 | const now = Date.now(); 182 | const path = getPageUrl(false); 183 | setTimeout(async () => { 184 | if (path && this.navigationMap[path]) { 185 | const navigationStart = this.navigationMap[path]; 186 | const data = await this._createPerformanceData(WxPerformanceDataType.WX_LIFE_STYLE, [ 187 | { 188 | itemType: WxPerformanceItemType.WxCustomPaint, 189 | navigationStart: navigationStart, 190 | timestamp: now, 191 | duration: now - navigationStart, 192 | }, 193 | ]); 194 | this._pushData([data]); 195 | } 196 | }, 1000); 197 | } 198 | } 199 | 200 | export default Store; 201 | -------------------------------------------------------------------------------- /packages/core/src/transportData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | _support, 3 | validateOption, 4 | logger, 5 | isBrowserEnv, 6 | isWxMiniEnv, 7 | variableTypeDetection, 8 | Queue, 9 | isEmpty, 10 | } from 'encode-monitor-utils'; 11 | import { createErrorId } from './errorId'; 12 | import { SDK_NAME, SDK_VERSION } from 'encode-monitor-shared'; 13 | import { breadcrumb } from './breadcrumb'; 14 | import { 15 | AuthInfo, 16 | TransportDataType, 17 | EMethods, 18 | InitOptions, 19 | isReportDataType, 20 | DeviceInfo, 21 | FinalReportType, 22 | } from 'encode-monitor-types'; 23 | 24 | export class TransportData { 25 | queue: Queue; 26 | beforeDataReport: unknown = null; 27 | backTrackerId: unknown = null; 28 | configReportXhr: unknown = null; 29 | configReportUrl: unknown = null; 30 | configReportWxRequest: unknown = null; 31 | useImgUpload = false; 32 | apikey = ''; 33 | trackKey = ''; 34 | errorDsn = ''; 35 | trackDsn = ''; 36 | constructor() { 37 | this.queue = new Queue(); 38 | } 39 | 40 | imgRequest(data: any, url: string): void { 41 | const requestFun = () => { 42 | let img = new Image(); 43 | const spliceStr = url.indexOf('?') === -1 ? '?' : '&'; 44 | img.src = `${url}${spliceStr}data=${encodeURIComponent(JSON.stringify(data))}`; 45 | img = null; 46 | }; 47 | this.queue.addFn(requestFun); 48 | } 49 | 50 | getRecord(): any[] { 51 | const recordData = _support.record; 52 | if (recordData && variableTypeDetection.isArray(recordData) && recordData.length > 2) { 53 | return recordData; 54 | } 55 | return []; 56 | } 57 | 58 | getDeviceInfo(): DeviceInfo | any { 59 | return _support.deviceInfo || {}; 60 | } 61 | 62 | async beforePost(data: FinalReportType) { 63 | if (isReportDataType(data)) { 64 | const errorId = createErrorId(data, this.apikey); 65 | if (!errorId) return false; 66 | data.errorId = errorId; 67 | } 68 | let transportData = this.getTransportData(data); 69 | if (typeof this.beforeDataReport === 'function') { 70 | transportData = await this.beforeDataReport(transportData); 71 | if (!transportData) return false; 72 | } 73 | return transportData; 74 | } 75 | 76 | async xhrPost(data: any, url: string) { 77 | const requestFun = (): void => { 78 | const xhr = new XMLHttpRequest(); 79 | xhr.open(EMethods.Post, url); 80 | xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); 81 | xhr.withCredentials = true; 82 | if (typeof this.configReportXhr === 'function') { 83 | this.configReportXhr(xhr, data); 84 | } 85 | xhr.send(JSON.stringify(data)); 86 | }; 87 | this.queue.addFn(requestFun); 88 | } 89 | 90 | async wxPost(data: any, url: string) { 91 | const requestFun = (): void => { 92 | let requestOptions = { method: 'POST' } as WechatMiniprogram.RequestOption; 93 | if (typeof this.configReportWxRequest === 'function') { 94 | const params = this.configReportWxRequest(data); 95 | requestOptions = { ...requestOptions, ...params }; 96 | } 97 | requestOptions = { 98 | ...requestOptions, 99 | data: JSON.stringify(data), 100 | url, 101 | }; 102 | wx.request(requestOptions); 103 | }; 104 | this.queue.addFn(requestFun); 105 | } 106 | 107 | getAuthInfo(): AuthInfo { 108 | const trackerId = this.getTrackerId(); 109 | const result: AuthInfo = { 110 | trackerId: String(trackerId), 111 | sdkVersion: SDK_VERSION, 112 | sdkName: SDK_NAME, 113 | }; 114 | this.apikey && (result.apikey = this.apikey); 115 | this.trackKey && (result.trackKey = this.trackKey); 116 | return result; 117 | } 118 | 119 | getApikey() { 120 | return this.apikey; 121 | } 122 | 123 | getTrackKey() { 124 | return this.trackKey; 125 | } 126 | 127 | getTrackerId(): string | number { 128 | if (typeof this.backTrackerId === 'function') { 129 | const trackerId = this.backTrackerId(); 130 | if (typeof trackerId === 'string' || typeof trackerId === 'number') { 131 | return trackerId; 132 | } else { 133 | logger.error( 134 | `trackerId:${trackerId} 期望 string 或 number 类型,但是传入类型为 ${typeof trackerId}`, 135 | ); 136 | } 137 | } 138 | return ''; 139 | } 140 | 141 | getTransportData(data: FinalReportType): TransportDataType { 142 | return { 143 | authInfo: this.getAuthInfo(), 144 | breadcrumb: breadcrumb.getStack(), 145 | data, 146 | record: this.getRecord(), 147 | deviceInfo: this.getDeviceInfo(), 148 | }; 149 | } 150 | 151 | isSdkTransportUrl(targetUrl: string): boolean { 152 | let isSdkDsn = false; 153 | if (this.errorDsn && targetUrl.indexOf(this.errorDsn) !== -1) { 154 | isSdkDsn = true; 155 | } 156 | if (this.trackDsn && targetUrl.indexOf(this.trackDsn) !== -1) { 157 | isSdkDsn = true; 158 | } 159 | return isSdkDsn; 160 | } 161 | 162 | bindOptions(options: InitOptions = {}): void { 163 | const { 164 | dsn, 165 | beforeDataReport, 166 | apikey, 167 | configReportXhr, 168 | backTrackerId, 169 | trackDsn, 170 | trackKey, 171 | configReportUrl, 172 | useImgUpload, 173 | configReportWxRequest, 174 | } = options; 175 | validateOption(apikey, 'apikey', 'string') && (this.apikey = apikey); 176 | validateOption(trackKey, 'trackKey', 'string') && (this.trackKey = trackKey); 177 | validateOption(dsn, 'dsn', 'string') && (this.errorDsn = dsn); 178 | validateOption(trackDsn, 'trackDsn', 'string') && (this.trackDsn = trackDsn); 179 | validateOption(useImgUpload, 'useImgUpload', 'boolean') && (this.useImgUpload = useImgUpload); 180 | validateOption(beforeDataReport, 'beforeDataReport', 'function') && 181 | (this.beforeDataReport = beforeDataReport); 182 | validateOption(configReportXhr, 'configReportXhr', 'function') && 183 | (this.configReportXhr = configReportXhr); 184 | validateOption(backTrackerId, 'backTrackerId', 'function') && 185 | (this.backTrackerId = backTrackerId); 186 | validateOption(configReportUrl, 'configReportUrl', 'function') && 187 | (this.configReportUrl = configReportUrl); 188 | validateOption(configReportWxRequest, 'configReportWxRequest', 'function') && 189 | (this.configReportWxRequest = configReportWxRequest); 190 | } 191 | 192 | /** 193 | * 监控错误上报的请求函数 194 | * @param data 错误上报数据格式 195 | * @returns 196 | */ 197 | async send(data: FinalReportType) { 198 | let dsn = ''; 199 | if (isReportDataType(data)) { 200 | dsn = this.errorDsn; 201 | if (isEmpty(dsn)) { 202 | logger.error('dsn为空,没有传入监控错误上报的dsn地址,请在init中传入'); 203 | return; 204 | } 205 | } else { 206 | dsn = this.trackDsn; 207 | if (isEmpty(dsn)) { 208 | logger.error('trackDsn为空,没有传入埋点上报的dsn地址,请在init中传入'); 209 | return; 210 | } 211 | } 212 | const result = await this.beforePost(data); 213 | if (!result) return; 214 | if (typeof this.configReportUrl === 'function') { 215 | dsn = this.configReportUrl(result, dsn); 216 | if (!dsn) return; 217 | } 218 | 219 | if (isBrowserEnv) { 220 | return this.useImgUpload ? this.imgRequest(result, dsn) : this.xhrPost(result, dsn); 221 | } 222 | if (isWxMiniEnv) { 223 | return this.wxPost(result, dsn); 224 | } 225 | } 226 | } 227 | 228 | const transportData = _support.transportData || (_support.transportData = new TransportData()); 229 | 230 | export { transportData }; 231 | -------------------------------------------------------------------------------- /packages/wx-miniprogram-performance/src/wx/replace.ts: -------------------------------------------------------------------------------- 1 | import { replaceOld, isEmptyObject } from 'encode-monitor-utils'; 2 | import Store from '../core/store'; 3 | import HandleEvents from './handleEvents'; 4 | import { WxPerformanceItemType, WxListenerTypes } from '../constant'; 5 | import { WxPerformanceAnyObj } from '../types/index'; 6 | 7 | export function replaceApp(store: Store) { 8 | if (App) { 9 | const originApp = App; 10 | App = function (appOptions: WechatMiniprogram.App.Option) { 11 | const methods = Object.keys(HandleEvents).filter((m) => m.indexOf('App') !== -1); 12 | methods.forEach((method) => { 13 | replaceOld( 14 | appOptions, 15 | method.replace('AppOn', 'on'), 16 | function (originMethod: () => void) { 17 | return function (...args: any): void { 18 | // 让原本的函数比抛出的hooks先执行,便于埋点判断是否重复 19 | if (originMethod) { 20 | originMethod.apply(this, args); 21 | } 22 | store.emit(method as WxPerformanceItemType, args); 23 | }; 24 | }, 25 | true, 26 | ); 27 | }); 28 | return originApp(appOptions); 29 | } as WechatMiniprogram.App.Constructor; 30 | } 31 | } 32 | 33 | function replacePageLifeMethods( 34 | options: 35 | | WechatMiniprogram.Page.Options< 36 | WechatMiniprogram.Page.DataOption, 37 | WechatMiniprogram.Page.CustomOption 38 | > 39 | | WechatMiniprogram.Component.MethodOption, 40 | store: Store, 41 | ) { 42 | const pageLifeMethods = Object.keys(HandleEvents).filter((m) => m.indexOf('Page') !== -1); 43 | pageLifeMethods.forEach((method) => { 44 | replaceOld( 45 | options, 46 | method.replace('PageOn', 'on'), 47 | function (originMethod: (args: any) => void) { 48 | return function (...args: any[]): void { 49 | store.emit(method as WxPerformanceItemType, args); 50 | if (originMethod) { 51 | return originMethod.apply(this, args); 52 | } 53 | }; 54 | }, 55 | true, 56 | ); 57 | }); 58 | } 59 | 60 | /** 61 | * 监听Page, Component下的点击事件 62 | */ 63 | function replaceAction( 64 | options: 65 | | WechatMiniprogram.Page.Options< 66 | WechatMiniprogram.Page.DataOption, 67 | WechatMiniprogram.Page.CustomOption 68 | > 69 | | WechatMiniprogram.Component.MethodOption, 70 | store: Store, 71 | ) { 72 | const ListenerTypes = Object.keys(WxListenerTypes); 73 | if (options) { 74 | Object.keys(options).forEach((m) => { 75 | if ('function' !== typeof options[m]) { 76 | return; 77 | } 78 | replaceOld( 79 | options, 80 | m, 81 | function (originMethod: (args: any) => void) { 82 | return function (...args: any[]): void { 83 | const event = args.find((arg) => arg && arg.type && arg.currentTarget); 84 | if (event && !event.monitorWorked && ListenerTypes.indexOf(event.type) > -1) { 85 | store.emit(WxListenerTypes[event.type] as WxPerformanceItemType, event); 86 | event.monitorWorked = true; 87 | } 88 | return originMethod.apply(this, args); 89 | }; 90 | }, 91 | true, 92 | ); 93 | }); 94 | } 95 | } 96 | 97 | // 重写Page 98 | export function replacePage(store: Store) { 99 | if (!Page) { 100 | return; 101 | } 102 | const originPage = Page; 103 | Page = function (pageOptions): WechatMiniprogram.Page.Constructor { 104 | replacePageLifeMethods(pageOptions, store); 105 | replaceAction(pageOptions, store); 106 | return originPage.call(this, pageOptions); 107 | }; 108 | } 109 | 110 | // 重写Component 111 | export function replaceComponent(store: Store) { 112 | if (!Component) { 113 | return; 114 | } 115 | const originComponent = Component; 116 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 117 | // @ts-ignore 118 | Component = function (componentOptions): WechatMiniprogram.Component.Constructor { 119 | if (!isEmptyObject(componentOptions.methods)) { 120 | replacePageLifeMethods(componentOptions.methods, store); 121 | replaceAction(componentOptions, store); 122 | } 123 | return originComponent.call(this, componentOptions); 124 | }; 125 | } 126 | 127 | // 监听网络性能 128 | export function replaceNetwork(store: Store) { 129 | const HOOKS = { 130 | request: WxPerformanceItemType.WxRequest, 131 | downloadFile: WxPerformanceItemType.WxDownloadFile, 132 | uploadFile: WxPerformanceItemType.WxUploadFile, 133 | }; 134 | Object.keys(HOOKS).forEach((hook) => { 135 | const originRequest = wx[hook]; 136 | Object.defineProperty(wx, hook, { 137 | writable: true, 138 | enumerable: true, 139 | configurable: true, 140 | value: function (...args: any[]) { 141 | const options: 142 | | WechatMiniprogram.RequestOption 143 | | WechatMiniprogram.DownloadFileOption 144 | | WechatMiniprogram.UploadFileOption = args[0]; 145 | const { url } = options; 146 | if (store.filterUrl(url)) { 147 | return originRequest.call(this, options); 148 | } 149 | 150 | let reqData = { 151 | startTime: Date.now(), 152 | header: options.header || {}, 153 | url: options.url, 154 | } as WxPerformanceAnyObj; 155 | switch (hook) { 156 | case 'request': 157 | const { method } = options as WechatMiniprogram.RequestOption; 158 | reqData = { ...reqData, method }; 159 | break; 160 | case 'downloadFile': 161 | case 'uploadFile': 162 | const { filePath } = options as 163 | | WechatMiniprogram.DownloadFileOption 164 | | WechatMiniprogram.UploadFileOption; 165 | reqData = { ...reqData, filePath, method: hook === 'downloadFile' ? 'GET' : 'POST' }; 166 | break; 167 | default: 168 | break; 169 | } 170 | 171 | const originFail = options.fail; 172 | const _fail: 173 | | WechatMiniprogram.RequestFailCallback 174 | | WechatMiniprogram.DownloadFileFailCallback 175 | | WechatMiniprogram.UploadFileFailCallback = function (err) { 176 | // 系统和网络层面的失败 177 | const endTime = Date.now(); 178 | reqData.duration = endTime - reqData.startTime; 179 | reqData.status = 0; 180 | reqData.errMsg = err.errMsg; 181 | reqData.endTime = endTime; 182 | store.emit(HOOKS[hook], reqData); 183 | if (typeof originFail === 'function') { 184 | return originFail(err); 185 | } 186 | }; 187 | 188 | const originSuccess = options.success; 189 | const _success: 190 | | WechatMiniprogram.RequestSuccessCallback 191 | | WechatMiniprogram.DownloadFileSuccessCallback 192 | | WechatMiniprogram.UploadFileFailCallback = function (res) { 193 | const endTime = Date.now(); 194 | reqData.duration = endTime - reqData.startTime; 195 | reqData.status = res.statusCode; 196 | reqData.errMsg = res.errMsg; 197 | reqData.endTime = endTime; 198 | 199 | store.emit(HOOKS[hook], reqData); 200 | if (typeof originSuccess === 'function') { 201 | return originSuccess(res); 202 | } 203 | }; 204 | 205 | return originRequest.call(this, { 206 | ...options, 207 | success: _success, 208 | fail: _fail, 209 | }); 210 | }, 211 | }); 212 | }); 213 | } 214 | -------------------------------------------------------------------------------- /packages/types/src/options.ts: -------------------------------------------------------------------------------- 1 | import { Breadcrumb } from 'encode-monitor-core'; 2 | import { BreadcrumbPushData } from './breadcrumb'; 3 | import { TransportDataType } from './transportData'; 4 | type CANCEL = null | undefined | boolean; 5 | 6 | export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS'; 7 | 8 | export enum EMethods { 9 | Get = 'GET', 10 | Post = 'POST', 11 | Put = 'PUT', 12 | Delete = 'DELETE', 13 | } 14 | 15 | interface IRequestHeaderConfig { 16 | url: HttpMethod; 17 | method: string; 18 | } 19 | 20 | type TSetRequestHeader = (key: string, value: string) => {}; 21 | export interface IBeforeAppAjaxSendConfig { 22 | setRequestHeader: TSetRequestHeader; 23 | } 24 | export interface InitOptions 25 | extends SilentEventTypes, 26 | HooksTypes, 27 | WxSilentEventTypes, 28 | WxMiniHooksTypes, 29 | BrowserHooksTypes { 30 | /** 31 | * 错误监控的dsn服务器地址 32 | */ 33 | dsn?: string; 34 | /** 35 | * 为true时,整个sdk将禁用 36 | */ 37 | disabled?: boolean; 38 | /** 39 | * 每个项目有一个唯一key,给监控的dsn用的 40 | */ 41 | apikey?: string; 42 | /** 43 | * 使用img上报的方式,默认为false,默认是xhr的上报方式 44 | */ 45 | useImgUpload?: boolean; 46 | /** 47 | * 每个项目有一个唯一trackKey,给埋点的dsn用的 48 | */ 49 | trackKey?: string; 50 | /** 51 | * 默认为关闭,为true是会打印一些信息:breadcrumb 52 | */ 53 | debug?: boolean; 54 | /** 55 | * 默认是关闭traceId,开启时,页面的所有请求都会生成一个uuid,放入请求头中 56 | */ 57 | enableTraceId?: boolean; 58 | /** 59 | * 如果开启了enableTraceId,也需要配置该配置项,includeHttpUrlTraceIdRegExp.test(xhr.url)为true时,才会在该请求头中添加traceId 60 | * 由于考虑部分接口如果随便加上多余的请求头会造成跨域,所以这边用的是包含关系的正则 61 | */ 62 | includeHttpUrlTraceIdRegExp?: RegExp; 63 | /** 64 | * traceId放入请求头中的key,默认是Trace-Id 65 | */ 66 | traceIdFieldName?: string; 67 | /** 68 | * 默认为空,所有ajax都会被监听,不为空时,filterXhrUrlRegExp.test(xhr.url)为true时过滤 69 | */ 70 | filterXhrUrlRegExp?: RegExp; 71 | /** 72 | * 忽视某些错误不上传 73 | */ 74 | // ignoreErrors?: Array 75 | /** 76 | * 默认20,最大100,超过100还是设置成100 77 | */ 78 | maxBreadcrumbs?: number; 79 | /** 80 | * 按钮点击和微信触摸事件节流时间,默认是0 81 | */ 82 | throttleDelayTime?: number; 83 | /** 84 | * 在引入wx-mini的情况下,使用该参数用来开启 85 | */ 86 | enableTrack?: boolean; 87 | /** 88 | * 在开启enableBury后,将所有埋点信息上报到该服务端地址,如果该属性有值时才会启动无痕埋点 89 | */ 90 | trackDsn?: string; 91 | /** 92 | * 最多可重复上报同一个错误的次数 93 | */ 94 | maxDuplicateCount?: number; 95 | } 96 | 97 | export interface HooksTypes { 98 | /** 99 | * 钩子函数,配置发送到服务端的xhr 100 | * 可以对当前xhr实例做一些配置:xhr.setRequestHeader()、xhr.withCredentials 101 | * 会在xhr.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8')、 102 | * xhr.withCredentials = true,后面调用该函数 103 | * ../param xhr XMLHttpRequest的实例 104 | */ 105 | configReportXhr?(xhr: XMLHttpRequest, reportData: TransportDataType | any): void; 106 | /** 107 | * 钩子函数,在每次发送事件前会调用 108 | * 109 | * ../param event 有SDK生成的错误事件 110 | * ../returns 如果返回 null | undefined | boolean 时,将忽略本次上传 111 | */ 112 | beforeDataReport?( 113 | event: TransportDataType, 114 | ): Promise | TransportDataType | any | CANCEL | null; 115 | /** 116 | * 117 | * 钩子函数,每次发送前都会调用 118 | * @param {TransportDataType} event 上报的数据格式 119 | * @param {string} url 上报到服务端的地址 120 | * @returns {string} 返回空时不上报 121 | * @memberof HooksTypes 122 | */ 123 | configReportUrl?(event: TransportDataType, url: string): string; 124 | /** 125 | * 钩子函数,在每次添加用户行为事件前都会调用 126 | * 127 | * ../param breadcrumb 由SDK生成的breacrumb事件栈 128 | * ../param hint 当次的生成的breadcrumb数据 129 | * ../returns 如果返回 null | undefined | boolean 时,将忽略本次的push 130 | */ 131 | beforePushBreadcrumb?( 132 | breadcrumb: Breadcrumb, 133 | hint: BreadcrumbPushData, 134 | ): BreadcrumbPushData | CANCEL; 135 | /** 136 | * 在状态小于400并且不等于0的时候回调用当前hook 137 | * ../param data 请求状态为200时返回的响应体 138 | * ../returns 如果返回 null | undefined | boolean 时,将忽略本次的上传 139 | */ 140 | // afterSuccessHttp?(data: T): string | CANCEL 141 | /** 142 | * 钩子函数,拦截用户页面的ajax请求,并在ajax请求发送前执行该hook,可以对用户发送的ajax请求做xhr.setRequestHeader 143 | * ../param config 当前请求的 144 | */ 145 | beforeAppAjaxSend?( 146 | config: IRequestHeaderConfig, 147 | setRequestHeader: IBeforeAppAjaxSendConfig, 148 | ): void; 149 | 150 | /** 151 | * 钩子函数,在beforeDataReport后面调用,在整合上报数据和本身SDK信息数据前调用,当前函数执行完后立即将数据错误信息上报至服务端 152 | * trackerId表示用户唯一键(可以理解成userId),需要trackerId的意义可以区分每个错误影响的用户数量 153 | */ 154 | backTrackerId?(): string | number; 155 | } 156 | 157 | export interface SilentEventTypes { 158 | /** 159 | * 静默监控Xhr事件 160 | */ 161 | silentXhr?: boolean; 162 | /** 163 | * 静默监控fetch事件 164 | */ 165 | silentFetch?: boolean; 166 | /** 167 | * 静默监控console事件 168 | */ 169 | silentConsole?: boolean; 170 | /** 171 | * 静默监控Dom事件 172 | */ 173 | silentDom?: boolean; 174 | /** 175 | * 静默监控history事件 176 | */ 177 | silentHistory?: boolean; 178 | /** 179 | * 静默监控error事件 180 | */ 181 | silentError?: boolean; 182 | /** 183 | * 静默监控unhandledrejection事件 184 | */ 185 | silentUnhandledrejection?: boolean; 186 | /** 187 | * 静默监控hashchange事件 188 | */ 189 | silentHashchange?: boolean; 190 | /** 191 | * 静默监控Vue.warn函数 192 | */ 193 | silentVue?: boolean; 194 | } 195 | 196 | export interface WxSilentEventTypes { 197 | /** 198 | * 静默监控AppOnError 199 | */ 200 | silentWxOnError?: boolean; 201 | /** 202 | * 静默监控AppOnUnhandledRejection 203 | */ 204 | silentWxOnUnhandledRejection?: boolean; 205 | /** 206 | * 静默监控AppOnPageNotFound 207 | */ 208 | silentWxOnPageNotFound?: boolean; 209 | /** 210 | * 静默监控PageOnShareAppMessage 211 | */ 212 | silentWxOnShareAppMessage?: boolean; 213 | /** 214 | * 静默监控小程序路由 215 | */ 216 | silentMiniRoute?: boolean; 217 | } 218 | 219 | export type IWxPageInstance = WechatMiniprogram.Page.Instance< 220 | WechatMiniprogram.IAnyObject, 221 | WechatMiniprogram.IAnyObject 222 | >; 223 | 224 | interface WxMiniHooksTypes { 225 | /** 226 | * wx小程序上报时的wx.request配置 227 | */ 228 | configReportWxRequest?(event: TransportDataType | any): Partial; 229 | /** 230 | * wx小程序的App下的onLaunch执行完后再执行以下hook 231 | */ 232 | appOnLaunch?(options: WechatMiniprogram.App.LaunchShowOption): void; 233 | /** 234 | * wx小程序的App下的OnShow执行完后再执行以下hook 235 | */ 236 | appOnShow?(options: WechatMiniprogram.App.LaunchShowOption): void; 237 | /** 238 | * wx小程序的App下的OnHide执行完后再执行以下hook 239 | */ 240 | appOnHide?(page: IWxPageInstance): void; 241 | /** 242 | * wx小程序的App下的onPageNotFound执行完后再执行以下hook 243 | */ 244 | onPageNotFound?(data: WechatMiniprogram.OnPageNotFoundCallbackResult): void; 245 | /** 246 | * 先执行hook:pageOnShow再执行wx小程序的Page下的onShow 247 | */ 248 | pageOnShow?(page: IWxPageInstance): void; 249 | /** 250 | * wx小程序的App下的pageOnUnload执行完后再执行以下hook 251 | */ 252 | pageOnUnload?(page: IWxPageInstance): void; 253 | /** 254 | * 先执行hook:pageOnHide再执行wx小程序的Page下的onHide 255 | */ 256 | pageOnHide?(page: IWxPageInstance): void; 257 | /** 258 | * 先执行hook:onShareAppMessage再执行wx小程序的Page下的onShareAppMessage 259 | */ 260 | onShareAppMessage?( 261 | options: WechatMiniprogram.Page.IShareAppMessageOption & IWxPageInstance, 262 | ): void; 263 | /** 264 | * 先执行hook:onShareTimeline再执行wx小程序的Page下的onShareTimeline 265 | */ 266 | onShareTimeline?(page: IWxPageInstance): void; 267 | /** 268 | * 先执行hook:onTabItemTap再执行wx小程序的Page下的onTabItemTap 269 | */ 270 | onTabItemTap?(options: WechatMiniprogram.Page.ITabItemTapOption & IWxPageInstance): void; 271 | /** 272 | * 重写wx.NavigateToMiniProgram将里面的参数抛出来,便于在跳转时更改query和extraData 273 | * @param options 274 | */ 275 | wxNavigateToMiniProgram?( 276 | options: WechatMiniprogram.NavigateToMiniProgramOption, 277 | ): WechatMiniprogram.NavigateToMiniProgramOption; 278 | /** 279 | * 代理Action中所有函数,拿到第一个参数并抛出成hook 280 | * @param e 281 | */ 282 | triggerWxEvent?(e: WechatMiniprogram.BaseEvent): void; 283 | } 284 | 285 | export interface BrowserHooksTypes { 286 | onRouteChange?: (from: string, to: string) => unknown; 287 | } 288 | -------------------------------------------------------------------------------- /packages/utils/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { IAnyObject, IntegrationError } from 'encode-monitor-types'; 2 | import { globalVar, HttpCodes, ErrorTypes } from 'encode-monitor-shared'; 3 | import { logger } from './logger'; 4 | import { nativeToString, variableTypeDetection } from './is'; 5 | 6 | export function getLocationHref(): string { 7 | if (typeof document === 'undefined' || document.location == null) return ''; 8 | return document.location.href; 9 | } 10 | 11 | // 用到所有事件名称 12 | type TotalEventName = 13 | | keyof GlobalEventHandlersEventMap 14 | | keyof XMLHttpRequestEventTargetEventMap 15 | | keyof WindowEventMap; 16 | 17 | /** 18 | * 添加事件监听器 19 | * 20 | * ../export 21 | * ../param {{ addEventListener: Function }} target 22 | * ../param {keyof TotalEventName} eventName 23 | * ../param {Function} handler 24 | * ../param {(boolean | Object)} opitons 25 | * ../returns 26 | */ 27 | export function on( 28 | target: { addEventListener: Function }, 29 | eventName: TotalEventName, 30 | handler: Function, 31 | opitons: boolean | unknown = false, 32 | ): void { 33 | target.addEventListener(eventName, handler, opitons); 34 | } 35 | 36 | /** 37 | * 38 | * 重写对象上面的某个属性 39 | * ../param source 需要被重写的对象 40 | * ../param name 需要被重写对象的key 41 | * ../param replacement 以原有的函数作为参数,执行并重写原有函数 42 | * ../param isForced 是否强制重写(可能原先没有该属性) 43 | * ../returns void 44 | */ 45 | export function replaceOld( 46 | source: IAnyObject, 47 | name: string, 48 | replacement: (...args: any[]) => any, 49 | isForced = false, 50 | ): void { 51 | if (source === undefined) return; 52 | if (name in source || isForced) { 53 | const original = source[name]; 54 | const wrapped = replacement(original); 55 | if (typeof wrapped === 'function') { 56 | source[name] = wrapped; 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * 用&分割对象,返回a=1&b=2 63 | * ../param obj 需要拼接的对象 64 | */ 65 | export function splitObjToQuery(obj: Record): string { 66 | return Object.entries(obj).reduce((result, [key, value], index) => { 67 | if (index !== 0) { 68 | result += '&'; 69 | } 70 | const valueStr = 71 | variableTypeDetection.isObject(value) || variableTypeDetection.isArray(value) 72 | ? JSON.stringify(value) 73 | : value; 74 | result += `${key}=${valueStr}`; 75 | return result; 76 | }, ''); 77 | } 78 | 79 | export const defaultFunctionName = ''; 80 | /** 81 | * 需要获取函数名,匿名则返回 82 | * ../param {unknown} fn 需要获取函数名的函数本体 83 | * ../returns 返回传入的函数的函数名 84 | */ 85 | export function getFunctionName(fn: unknown): string { 86 | if (!fn || typeof fn !== 'function') { 87 | return defaultFunctionName; 88 | } 89 | return fn.name || defaultFunctionName; 90 | } 91 | 92 | // 函数防抖 93 | /** 94 | * 95 | * ../param fn 需要防抖的函数 96 | * ../param delay 防抖的时间间隔 97 | * ../param isImmediate 是否需要立即执行,默认为false,第一次是不执行的 98 | * ../returns 返回一个包含防抖功能的函数 99 | */ 100 | // export const debounce = (fn: voidFun, delay: number, isImmediate = false): voidFun => { 101 | // let timer = null 102 | // return function (...args: any) { 103 | // if (isImmediate) { 104 | // fn.apply(this, args) 105 | // isImmediate = false 106 | // return 107 | // } 108 | // clearTimeout(timer) 109 | // timer = setTimeout(() => { 110 | // fn.apply(this, args) 111 | // }, delay) 112 | // } 113 | // } 114 | 115 | // 函数节流 116 | /** 117 | * 118 | * ../param fn 需要节流的函数 119 | * ../param delay 节流的时间间隔 120 | * ../returns 返回一个包含节流功能的函数 121 | */ 122 | export const throttle = (fn: Function, delay: number): Function => { 123 | let canRun = true; 124 | return function (...args: any) { 125 | if (!canRun) return; 126 | fn.apply(this, args); 127 | canRun = false; 128 | setTimeout(() => { 129 | canRun = true; 130 | }, delay); 131 | }; 132 | }; 133 | 134 | /** 135 | * 获取当前的时间戳 136 | * ../returns 返回当前时间戳 137 | */ 138 | export function getTimestamp(): number { 139 | return Date.now(); 140 | } 141 | 142 | export function typeofAny(target: any, type: string): boolean { 143 | return typeof target === type; 144 | } 145 | 146 | export function toStringAny(target: any, type: string): boolean { 147 | return nativeToString.call(target) === type; 148 | } 149 | 150 | export function validateOption(target: any, targetName: string, expectType: string): boolean { 151 | if (typeofAny(target, expectType)) return true; 152 | typeof target !== 'undefined' && 153 | logger.error(`${targetName}期望传入${expectType}类型,目前是${typeof target}类型`); 154 | return false; 155 | } 156 | 157 | export function toStringValidateOption( 158 | target: any, 159 | targetName: string, 160 | expectType: string, 161 | ): boolean { 162 | if (toStringAny(target, expectType)) return true; 163 | typeof target !== 'undefined' && 164 | logger.error( 165 | `${targetName}期望传入${expectType}类型,目前是${nativeToString.call(target)}类型`, 166 | ); 167 | return false; 168 | } 169 | 170 | export function silentConsoleScope(callback: Function) { 171 | globalVar.isLogAddBreadcrumb = false; 172 | callback(); 173 | globalVar.isLogAddBreadcrumb = true; 174 | } 175 | 176 | export function generateUUID(): string { 177 | let d = new Date().getTime(); 178 | const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 179 | const r = (d + Math.random() * 16) % 16 | 0; 180 | d = Math.floor(d / 16); 181 | return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16); 182 | }); 183 | return uuid; 184 | } 185 | 186 | export function unknownToString(target: unknown): string { 187 | if (variableTypeDetection.isString(target)) { 188 | return target as string; 189 | } 190 | if (variableTypeDetection.isUndefined(target)) { 191 | return 'undefined'; 192 | } 193 | return JSON.stringify(target); 194 | } 195 | 196 | export function getBigVersion(version: string) { 197 | return Number(version.split('.')[0]); 198 | } 199 | 200 | export function isHttpFail(code: number) { 201 | return code === 0 || code === HttpCodes.BAD_REQUEST || code > HttpCodes.UNAUTHORIZED; 202 | } 203 | 204 | /** 205 | * 给url添加query 206 | * @param url 207 | * @param query 208 | */ 209 | export function setUrlQuery(url: string, query: object) { 210 | const queryArr = []; 211 | Object.keys(query).forEach((k) => { 212 | queryArr.push(`${k}=${query[k]}`); 213 | }); 214 | if (url.indexOf('?') !== -1) { 215 | url = `${url}&${queryArr.join('&')}`; 216 | } else { 217 | url = `${url}?${queryArr.join('&')}`; 218 | } 219 | return url; 220 | } 221 | 222 | export function interceptStr(str: string, interceptLength: number): string { 223 | if (variableTypeDetection.isString(str)) { 224 | return ( 225 | str.slice(0, interceptLength) + 226 | (str.length > interceptLength ? `:截取前${interceptLength}个字符` : '') 227 | ); 228 | } 229 | return ''; 230 | } 231 | 232 | /** 233 | * 获取wx当前route的方法 234 | * 必须是在进入Page或Component构造函数内部才能够获取到currentPages 235 | * 否则都是在注册Page和Component时执行的代码,此时url默认返回'App' 236 | */ 237 | export function getCurrentRoute() { 238 | if (!variableTypeDetection.isFunction(getCurrentPages)) { 239 | return ''; 240 | } 241 | const pages = getCurrentPages(); // 在App里调用该方法,页面还没有生成,长度为0 242 | if (!pages.length) { 243 | return 'App'; 244 | } 245 | const currentPage = pages.pop(); 246 | return setUrlQuery(currentPage.route, currentPage.options); 247 | } 248 | 249 | /** 250 | * 解析字符串错误信息,返回message、name、stack 251 | * @param str error string 252 | */ 253 | export function parseErrorString(str: string): IntegrationError { 254 | const splitLine: string[] = str.split('\n'); 255 | if (splitLine.length < 2) return null; 256 | if (splitLine[0].indexOf('MiniProgramError') !== -1) { 257 | splitLine.splice(0, 1); 258 | } 259 | const message = splitLine.splice(0, 1)[0]; 260 | const name = splitLine.splice(0, 1)[0].split(':')[0]; 261 | const stack = []; 262 | splitLine.forEach((errorLine: string) => { 263 | const regexpGetFun = /at\s+([\S]+)\s+\(/; // 获取 [ 函数名 ] 264 | const regexGetFile = /\(([^)]+)\)/; // 获取 [ 有括号的文件 , 没括号的文件 ] 265 | const regexGetFileNoParenthese = /\s+at\s+(\S+)/; // 获取 [ 有括号的文件 , 没括号的文件 ] 266 | 267 | const funcExec = regexpGetFun.exec(errorLine); 268 | let fileURLExec = regexGetFile.exec(errorLine); 269 | if (!fileURLExec) { 270 | // 假如为空尝试解析无括号的URL 271 | fileURLExec = regexGetFileNoParenthese.exec(errorLine); 272 | } 273 | 274 | const funcNameMatch = Array.isArray(funcExec) && funcExec.length > 0 ? funcExec[1].trim() : ''; 275 | const fileURLMatch = Array.isArray(fileURLExec) && fileURLExec.length > 0 ? fileURLExec[1] : ''; 276 | const lineInfo = fileURLMatch.split(':'); 277 | stack.push({ 278 | args: [], // 请求参数 279 | func: funcNameMatch || ErrorTypes.UNKNOWN_FUNCTION, // 前端分解后的报错 280 | column: Number(lineInfo.pop()), // 前端分解后的列 281 | line: Number(lineInfo.pop()), // 前端分解后的行 282 | url: lineInfo.join(':'), // 前端分解后的URL 283 | }); 284 | }); 285 | return { 286 | message, 287 | name, 288 | stack, 289 | }; 290 | } 291 | --------------------------------------------------------------------------------