├── .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}${tagName}>`;
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 |
--------------------------------------------------------------------------------