├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── pr-main.yml │ └── server-release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierrc ├── README.md ├── commitlint.config.js ├── docs └── assets │ ├── framework.png │ ├── logo.png │ ├── monitor.png │ ├── report.png │ └── sdk.png ├── package.json ├── packages ├── eslint-config │ ├── CHANGELOG.md │ ├── README.md │ ├── index.js │ └── package.json ├── monitor │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── lib │ │ ├── index.cjs.js │ │ ├── index.esm.js │ │ ├── index.umd.js │ │ └── types │ │ │ ├── common │ │ │ ├── index.d.ts │ │ │ ├── observer.d.ts │ │ │ ├── queue.d.ts │ │ │ ├── reportInfo.d.ts │ │ │ └── store.d.ts │ │ │ ├── config │ │ │ └── index.d.ts │ │ │ ├── core │ │ │ ├── behavior │ │ │ │ ├── getPageDuration.d.ts │ │ │ │ ├── getPv.d.ts │ │ │ │ ├── getVueJump.d.ts │ │ │ │ └── index.d.ts │ │ │ ├── error │ │ │ │ └── index.d.ts │ │ │ ├── index.d.ts │ │ │ └── performance │ │ │ │ ├── getCLS.d.ts │ │ │ │ ├── getDevices.d.ts │ │ │ │ ├── getFID.d.ts │ │ │ │ ├── getFP.d.ts │ │ │ │ ├── getFSP.d.ts │ │ │ │ ├── getLCP.d.ts │ │ │ │ ├── getMemory.d.ts │ │ │ │ ├── getNavConnection.d.ts │ │ │ │ ├── getNavTiming.d.ts │ │ │ │ ├── getWhiteScreen.d.ts │ │ │ │ └── index.d.ts │ │ │ ├── index.d.ts │ │ │ ├── types │ │ │ ├── behavior.d.ts │ │ │ ├── common.d.ts │ │ │ ├── error.d.ts │ │ │ ├── index.d.ts │ │ │ ├── option.d.ts │ │ │ ├── performance.d.ts │ │ │ └── vue.d.ts │ │ │ └── utils │ │ │ ├── event.d.ts │ │ │ ├── helper.d.ts │ │ │ ├── index.d.ts │ │ │ └── window.d.ts │ ├── package.json │ ├── scripts │ │ └── build.js │ ├── src │ │ ├── common │ │ │ ├── index.ts │ │ │ ├── observer.ts │ │ │ ├── queue.ts │ │ │ ├── reportInfo.ts │ │ │ └── store.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── core │ │ │ ├── behavior │ │ │ │ ├── getPageDuration.ts │ │ │ │ ├── getPv.ts │ │ │ │ ├── getVueJump.ts │ │ │ │ └── index.ts │ │ │ ├── error │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── performance │ │ │ │ ├── getCLS.ts │ │ │ │ ├── getDevices.ts │ │ │ │ ├── getFID.ts │ │ │ │ ├── getFP.ts │ │ │ │ ├── getFSP.ts │ │ │ │ ├── getLCP.ts │ │ │ │ ├── getMemory.ts │ │ │ │ ├── getNavConnection.ts │ │ │ │ ├── getNavTiming.ts │ │ │ │ ├── getWhiteScreen.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── types │ │ │ ├── behavior.ts │ │ │ ├── common.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── option.ts │ │ │ ├── performance.ts │ │ │ └── vue.ts │ │ └── utils │ │ │ ├── event.ts │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ └── window.ts │ └── tsconfig.json ├── report │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── lib │ │ ├── index.cjs.js │ │ ├── index.esm.js │ │ └── types │ │ │ └── index.d.ts │ ├── package.json │ ├── rollup │ ├── scripts │ │ └── build.mjs │ ├── src │ │ └── index.ts │ └── tsconfig.json └── server │ ├── .babelrc │ ├── .dockerignore │ ├── .gitignore │ ├── .npmrc │ ├── Dockerfile │ ├── README.md │ ├── app │ ├── db │ │ ├── queueRedis.ts │ │ ├── redis.ts │ │ └── sequelize.ts │ ├── index.ts │ ├── middlewares │ │ ├── errorHandler.ts │ │ └── index.ts │ ├── models │ │ ├── error.ts │ │ ├── index.ts │ │ └── performance.ts │ ├── routes │ │ ├── collect.ts │ │ ├── common.ts │ │ ├── error.ts │ │ └── index.ts │ ├── services │ │ ├── collect.ts │ │ ├── error.ts │ │ └── index.ts │ ├── types │ │ └── index.ts │ ├── typings │ │ ├── connect-multiparty.d.ts │ │ ├── index.d.ts │ │ └── validate.d.ts │ └── utils │ │ ├── helper.ts │ │ ├── resolve.ts │ │ └── uploadFile.ts │ ├── config │ ├── development.ts │ └── production.ts │ ├── docker-compose.yml │ ├── index.ts │ ├── package.json │ ├── static │ └── sourcemap │ │ └── testvue │ │ └── 1.0.2 │ │ ├── main.ec314ab9cd719f2de58c.js │ │ └── main.ec314ab9cd719f2de58c.js.map │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── test ├── error.html └── vue-demo ├── .babelrc ├── index.html ├── package.json ├── postcss.config.js ├── script ├── webpack.base.config.js ├── webpack.dev.config.js └── webpack.prod.config.js └── src ├── App.vue ├── about-child.vue ├── about.vue ├── home.vue ├── index.js └── router.js /.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 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | pnpm-lock.yaml 4 | test 5 | packages/*/node_modules 6 | packages/monitor/lib 7 | packages/report/lib 8 | packages/server/static 9 | 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@apply-monitor/eslint-config"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/pr-main.yml: -------------------------------------------------------------------------------- 1 | name: PR MAIN 2 | on: 3 | pull_request: 4 | branches: main 5 | jobs: 6 | CI: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v2 11 | 12 | - name: Setup pnpm 13 | uses: pnpm/action-setup@v2 14 | 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: "16.x" 19 | # 检查缓存 20 | - name: Cache 21 | id: cache-dependencies 22 | uses: actions/cache@v3 23 | with: 24 | # 缓存文件目录的路径 25 | path: | 26 | **/node_modules 27 | key: ${{runner.OS}}-${{hashFiles('**/pnpm-lock.yaml')}} 28 | 29 | - name: Installing Dependencies 30 | # 判断是否命中缓存,否则跳过该阶段 31 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: Running Lint 35 | run: pnpm run lint 36 | -------------------------------------------------------------------------------- /.github/workflows/server-release.yml: -------------------------------------------------------------------------------- 1 | name: SERVER RELEASE 2 | on: 3 | push: 4 | branches: main 5 | paths: 'packages/server/**' 6 | jobs: 7 | server-build-image: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v3 12 | - name: Use Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: "16.1" 16 | - name: Installing Dependencies 17 | run: cd packages/server&&npm install 18 | - name: Log in to Docker Hub #登陆docker 19 | uses: docker/login-action@v1 20 | with: 21 | registry: registry.cn-guangzhou.aliyuncs.com 22 | username: ${{ secrets.DOCKER_USERNAME }} #docker的用户名 23 | password: ${{ secrets.DOCKER_PASSWORD }} #docker的密码 24 | 25 | - name: Extract metadata (tags, labels) for Docker # 获取元数据包括tag和labels 26 | id: meta 27 | uses: docker/metadata-action@v2 28 | with: 29 | images: registry.cn-guangzhou.aliyuncs.com/lycarrot/monitor-server 30 | tags: latest 31 | 32 | - name: Build and push Docker image #构建和发布 docker镜像 33 | uses: docker/build-push-action@v2 34 | with: 35 | context: ./packages/server 36 | push: true 37 | tags: monitor-server:latest #上一步所拿到的tags,默认是分支名字 38 | labels: ${{ steps.meta.outputs.labels }} # 上一步所拿到的labels 39 | 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目介绍 2 | 3 |
4 | 5 |

6 | 7 |

8 | 9 | 本项目是一个完整的监控平台体系,项目信息收集 sdk、服务端接口 API 信息处理均已实现,监控后台信息平台管理还在实现中,采用 monorepo+pnpm 方式开发。 10 | 11 | ## 项目结构 12 | 13 | ``` 14 | ├── .changeset 包版本维护 15 | ├── .github 16 | ├── .workflows github-actions配置 17 | ├── .husky 发布到github校验 18 | ├── docs 静态资源 19 | ├── _tests sdk测试 20 | ├── packages *项目核心 21 | ├── eslint-config eslint通用规范 22 | ├── monitor 信息收集sdk 23 | ├── report 上传sourcemap插件 24 | ├── server 服务端处理平台 25 | ├── .eslintignore 26 | ├── .eslintrc 27 | ├── .npmrc 28 | ├── .nvmrc 29 | ├──.prettierrc 30 | ├── commitlint.config 31 | ├── pnpm-workspace.yaml 32 | |── pnpm-lock.yaml 33 | ├── package.json 34 | └── README.md 35 | ``` 36 | 37 | ## 项目整体架构 38 | 39 | 整个监控体系核心部分主要分成三部分,首先是 sdk 植入到各个需要监控到的项目中,然后监控信息会上报到统一的的服务器进行处理存储,最后是统一的监控平台对监控信息进行展示 40 | 41 | 42 | ### 核心部分 43 | 44 | 信息收集 sdk:monitor
45 | 信息处理分析:server
46 | 信息管理平台(实现中):admin
47 | 48 | #### monitor(信息收集 sdk) 49 | 50 | 整个 sdk 其实是做了俩部分工作,信息采集+信息上报 51 | 52 | #### 信息采集 53 | 54 | 信息采集包括三个部分:错误监控、行为监控、性能监控三个部分 55 | 56 | ![信息采集](https://img-1301800639.cos.ap-guangzhou.myqcloud.com/sdk%281%29.png) 57 | 58 | #### 信息上报 59 | 60 | ##### 上报方法 61 | 62 | 关于信息上报的方式有三种:sendBeacon>image>XMLHttpRequest,默认采用的 sendBeacon,如果不兼容这个 api,会选择 image 上报,最后才会选择 XMLHttpRequest 方式进行上传。当然也可以通过配置自行选择上报方式。 63 | 64 | - sendBeacon:该方法会将少量异步数据发送到后台,所以用这种方式传输时,不会影响页面本身加载的请求,同时这种是异步方式的。 65 | - image:采用的是 1\*1 像素的透明 gif 进行上报,因为 gif 图片格式体积小,可以避免阻塞页面加载,影响用户体验,该方式也是支持跨域方式得。 66 | - XMLHttpRequest:该方式的问题主要是可能会占用页面本身请求,阻塞页面加载,同时也需要考虑跨域的问题 67 | 68 | #### 上报时机 69 | 70 | - 错误监控:触发时会直接上报 71 | - 其它:会把相应的信息上报加人一个队列里面进行缓存,然后在页面加载完成或页面隐藏时这些时机进行上报,这样可以避免监控 sdk 的请求阻塞页面加载。 72 | 73 | #### server(存储和分析) 74 | 75 | 服务端采用的 koa2+mysql+redis+typescript 方式开发,整个服务端可以分为俩部分,信息收集处理存储、提取存储数据分析。 76 | 77 | #### 存储部分 78 | 79 | 存储主要是指 sdk 上报的性能、行为、错误这三部分信息收集,拿到上报的信息之后,会首先对数据做个清洗,然后假如同时监控了多个项目,所以可能存在一下子发送过多的数据,导致服务端信息处理过载崩溃,所以在加入 mysql 存储之前,会对数据做层队列缓存,缓存方式采用的是 kue+redis。 80 | 81 | 82 | 83 | #### 数据分析 84 | 85 | 主要对项目上报的数据进行处理,然后展示在后台分析平台。 86 | 87 | #### report(webpack 上报插件) 88 | 89 | 因为 sdk 上报的错误代码信息都是经过压缩处理的,是无法直观分析到代码具体报错位置的。可以通过匹配到相应的 source-map 文件,通过错误信息的行列数与对应的 source-map 文件,处理后得到源文件的具体错误信息。所以这里编写了个 webpack 插件,在代码打包完成后,会把相应的 source-map 文件上传到服务端。 90 | 91 | - webpack 打包生成文件触发 afterEmit 钩子,拿到相应的 source-map 文件资源,然后把它们以 form-data 的形式发送到服务端。 92 | - 然后再把相应的 source-map 文件资源删除,在 webpack 的 done 钩子阶段执行 93 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycarrot/apply-monitor/00573248f6a1acb6577864283c8a2da4ed74842f/docs/assets/framework.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycarrot/apply-monitor/00573248f6a1acb6577864283c8a2da4ed74842f/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/assets/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycarrot/apply-monitor/00573248f6a1acb6577864283c8a2da4ed74842f/docs/assets/monitor.png -------------------------------------------------------------------------------- /docs/assets/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycarrot/apply-monitor/00573248f6a1acb6577864283c8a2da4ed74842f/docs/assets/report.png -------------------------------------------------------------------------------- /docs/assets/sdk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycarrot/apply-monitor/00573248f6a1acb6577864283c8a2da4ed74842f/docs/assets/sdk.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apply-monitor", 3 | "packageManager": "pnpm@8.3.1", 4 | "private": "true", 5 | "version": "1.0.3", 6 | "description": "完整监控体系", 7 | "main": "index.js", 8 | "scripts": { 9 | "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx,.md --max-warnings 0 --cache", 10 | "lint:fix": "pnpm run lint --fix", 11 | "prepare": "husky install", 12 | "commit": "cz", 13 | "changeset": "changeset", 14 | "update:version": "changeset version", 15 | "release": "changeset publish", 16 | "monitor:build": "pnpm run -C ./packages/monitor build", 17 | "monitor:build:watch": "pnpm run -C ./packages/monitor build:watch", 18 | "server": "pnpm run -C ./packages/server start" 19 | }, 20 | "keywords": ["monitor"], 21 | "author": "luoying", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@apply-monitor/eslint-config": "workspace:*" 25 | }, 26 | "devDependencies": { 27 | "@commitlint/cli": "^17.6.3", 28 | "@commitlint/config-conventional": "^17.6.3", 29 | "commitizen": "^4.3.0", 30 | "cz-conventional-changelog": "^3.3.0", 31 | "@changesets/cli": "^2.26.1", 32 | "eslint": "^8.32.0", 33 | "husky": "^8.0.3", 34 | "lint-staged": "^13.2.2" 35 | }, 36 | "config": { 37 | "commitizen": { 38 | "path": "./node_modules/cz-conventional-changelog" 39 | } 40 | }, 41 | "engines": { 42 | "node": ">= 16" 43 | }, 44 | "lint-staged": { 45 | "*.{js,jsx,ts,tsx,vue,md}": [ 46 | "prettier --write", 47 | "eslint --fix" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/eslint-config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @apply-monitor/eslint-config 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - 内容版本更新 8 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | ## apply-monitor/eslint-config 2 | 3 | ### 安装方式 4 | 5 | 使用 npm: 6 | 7 | ``` 8 | $ npm install @apply-monitor/eslint-config --save-dev 9 | ``` 10 | 11 | 使用 yarn: 12 | 13 | ``` 14 | $ yarn add @apply-monitor/eslint-config --dev 15 | ``` 16 | 17 | ### 使用方式 18 | 19 | ```json 20 | //.eslintrc.json 21 | { 22 | "root": true, 23 | "extends": ["@apply-monitor/eslint-config"] 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/eslint-config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:vue/recommended', 10 | 'plugin:prettier/recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'prettier', 13 | 'plugin:jsonc/recommended-with-jsonc', 14 | 'plugin:markdown/recommended', 15 | ], 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 'latest', 22 | sourceType: 'module', 23 | }, 24 | plugins: ['@typescript-eslint', 'prettier'], 25 | rules: { 26 | 'no-useless-escape': 0, 27 | 'no-async-promise-executor': 'off', 28 | // ts 29 | '@typescript-eslint/explicit-module-boundary-types': 'off', 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-non-null-assertion': 'off', 32 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', 33 | '@typescript-eslint/ban-ts-comment': ['off', { 'ts-ignore': false }], 34 | // prettier 35 | 'prettier/prettier': 'error', 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apply-monitor/eslint-config", 3 | "version": "1.0.0", 4 | "description": "eslint通用配置", 5 | "main": "index.js", 6 | "publishConfig": { 7 | "access": "public", 8 | "registry": "https://registry.npmjs.org/" 9 | }, 10 | "keywords": [ 11 | "eslint" 12 | ], 13 | "author": "lycarrot", 14 | "license": "ISC", 15 | "peerDependencies": { 16 | "eslint": "^8.0.0" 17 | }, 18 | "dependencies": { 19 | "@typescript-eslint/eslint-plugin": "^5.51.0", 20 | "@typescript-eslint/parser": "^5.51.0", 21 | "eslint": "^8.32.0", 22 | "eslint-config-prettier": "^8.6.0", 23 | "eslint-plugin-jsonc": "^2.6.0", 24 | "eslint-plugin-markdown": "^3.0.0", 25 | "eslint-plugin-prettier": "^4.2.1", 26 | "eslint-plugin-vue": "^9.11.0", 27 | "prettier": "^2.8.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/monitor/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/monitor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @apply-monitor/monitor 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - 版本内容更新 8 | -------------------------------------------------------------------------------- /packages/monitor/README.md: -------------------------------------------------------------------------------- 1 | ## apply-monitor/monitor 2 | 3 | ### 安装方式 4 | 5 | 使用 npm: 6 | 7 | ``` 8 | npm install @apply-monitor/monitor --save-dev 9 | ``` 10 | 11 | 使用 yarn: 12 | 13 | ``` 14 | yarn add @apply-monitor/monitor --dev 15 | ``` 16 | 17 | ### 使用方式 18 | 19 | ```js 20 | import Monitor from '@apply-monitor/monitor' 21 | 22 | new Monitor({ 23 | url: '项目上报地址', 24 | project: '项目名称', 25 | version: '版本', 26 | }) 27 | ``` 28 | 29 | ### 配置项 30 | 31 | | Name | Description | tyep | default | isRequired | 32 | | ----------------- | ----------------------------- | ------- | ------------ | ---------- | 33 | | url | 上传 url | string | '' | true | 34 | | project | 项目名称 | string | '' | true | 35 | | proSub | 子项目名称 | string | '' | false | 36 | | version | 项目版本 | string | '' | true | 37 | | isCollectErr | 是否收集错误 | boolean | true | false | 38 | | isCollectPer | 是否收集用户行为 | boolean | true | false | 39 | | isCollectBehavior | 是否收集性能 | boolean | true | false | 40 | | sendWay | 上传方式 | string | 'sendBeacon' | false | 41 | | isVue | 开启 vue 路由跳转传入 Vue | boolean | false | false | 42 | | vue | 是否开启 vue 错误收集 | boolean | false | false | 43 | | isVueJump | 是否开启监听 vue 路由跳转收集 | boolean | false | false | 44 | | router | 开启 vue 路由跳转传入路由 | router | null | false | 45 | -------------------------------------------------------------------------------- /packages/monitor/lib/index.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var defaultOptions = { 4 | url: '//127.0.0.1:8000/api/collect/info/detail', 5 | sendWay: 'sendBeacon', 6 | isCollectErr: true, 7 | isCollectPer: true, 8 | isCollectBehavior: true, 9 | } 10 | 11 | var MonitorType 12 | ;(function (MonitorType) { 13 | MonitorType['ERROR'] = 'error' 14 | MonitorType['PERFORMANCE'] = 'performance' 15 | MonitorType['BEHAVIOR'] = 'behavior' 16 | })(MonitorType || (MonitorType = {})) 17 | var Level 18 | ;(function (Level) { 19 | Level['ERROR'] = 'error' 20 | Level['WARN'] = 'warn' 21 | Level['INFO'] = 'info' 22 | })(Level || (Level = {})) 23 | 24 | var ErrorType 25 | ;(function (ErrorType) { 26 | ErrorType['JS'] = 'js_error' 27 | ErrorType['RESOURCE'] = 'resource_error' 28 | ErrorType['VUE'] = 'vue_error' 29 | ErrorType['PROMISE'] = 'promise_error' 30 | ErrorType['AJAX'] = 'ajax_error' 31 | })(ErrorType || (ErrorType = {})) 32 | 33 | var PerformanceType 34 | ;(function (PerformanceType) { 35 | PerformanceType['FP'] = 'first-paint' 36 | PerformanceType['FCP'] = 'first-contentful-paint' 37 | PerformanceType['LCP'] = 'largest-contentful-paint' 38 | PerformanceType['CLS'] = 'layout-shift' 39 | PerformanceType['FID'] = 'first-input' 40 | PerformanceType['FSP'] = 'first-screen-paint' 41 | PerformanceType['NC'] = 'nav-connecttion' 42 | PerformanceType['NAV'] = 'navigation' 43 | PerformanceType['MRY'] = 'memory' 44 | PerformanceType['DICE'] = 'devices' 45 | PerformanceType['WHITE'] = 'white-screen' 46 | })(PerformanceType || (PerformanceType = {})) 47 | 48 | var BehaviorType 49 | ;(function (BehaviorType) { 50 | BehaviorType['PV'] = 'pv' 51 | BehaviorType['VJ'] = 'vue-jump' 52 | BehaviorType['PD'] = 'page-duration' 53 | })(BehaviorType || (BehaviorType = {})) 54 | 55 | // Unique ID creation requires a high quality random # generator. In the browser we therefore 56 | // require the crypto API and do not support built-in fallback to lower quality random number 57 | // generators (like Math.random()). 58 | let getRandomValues 59 | const rnds8 = new Uint8Array(16) 60 | function rng() { 61 | // lazy load so that environments that need to polyfill have a chance to do so 62 | if (!getRandomValues) { 63 | // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. 64 | getRandomValues = 65 | typeof crypto !== 'undefined' && 66 | crypto.getRandomValues && 67 | crypto.getRandomValues.bind(crypto) 68 | 69 | if (!getRandomValues) { 70 | throw new Error( 71 | 'crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported' 72 | ) 73 | } 74 | } 75 | 76 | return getRandomValues(rnds8) 77 | } 78 | 79 | /** 80 | * Convert array of 16 byte values to UUID string format of the form: 81 | * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 82 | */ 83 | 84 | const byteToHex = [] 85 | 86 | for (let i = 0; i < 256; ++i) { 87 | byteToHex.push((i + 0x100).toString(16).slice(1)) 88 | } 89 | 90 | function unsafeStringify(arr, offset = 0) { 91 | // Note: Be careful editing this code! It's been tuned for performance 92 | // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 93 | return ( 94 | byteToHex[arr[offset + 0]] + 95 | byteToHex[arr[offset + 1]] + 96 | byteToHex[arr[offset + 2]] + 97 | byteToHex[arr[offset + 3]] + 98 | '-' + 99 | byteToHex[arr[offset + 4]] + 100 | byteToHex[arr[offset + 5]] + 101 | '-' + 102 | byteToHex[arr[offset + 6]] + 103 | byteToHex[arr[offset + 7]] + 104 | '-' + 105 | byteToHex[arr[offset + 8]] + 106 | byteToHex[arr[offset + 9]] + 107 | '-' + 108 | byteToHex[arr[offset + 10]] + 109 | byteToHex[arr[offset + 11]] + 110 | byteToHex[arr[offset + 12]] + 111 | byteToHex[arr[offset + 13]] + 112 | byteToHex[arr[offset + 14]] + 113 | byteToHex[arr[offset + 15]] 114 | ).toLowerCase() 115 | } 116 | 117 | const randomUUID = 118 | typeof crypto !== 'undefined' && 119 | crypto.randomUUID && 120 | crypto.randomUUID.bind(crypto) 121 | var native = { 122 | randomUUID, 123 | } 124 | 125 | function v4(options, buf, offset) { 126 | if (native.randomUUID && !buf && !options) { 127 | return native.randomUUID() 128 | } 129 | 130 | options = options || {} 131 | const rnds = options.random || (options.rng || rng)() // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` 132 | 133 | rnds[6] = (rnds[6] & 0x0f) | 0x40 134 | rnds[8] = (rnds[8] & 0x3f) | 0x80 // Copy bytes to buffer, if provided 135 | 136 | if (buf) { 137 | offset = offset || 0 138 | 139 | for (let i = 0; i < 16; ++i) { 140 | buf[offset + i] = rnds[i] 141 | } 142 | 143 | return buf 144 | } 145 | 146 | return unsafeStringify(rnds) 147 | } 148 | 149 | function formatParams(obj) { 150 | var strArr = [] 151 | Object.keys(obj).forEach(function (key) { 152 | strArr.push( 153 | '' 154 | .concat(encodeURIComponent(key), '=') 155 | .concat(encodeURIComponent(obj[key])) 156 | ) 157 | }) 158 | return strArr.join('&') 159 | } 160 | function getLines(stack) { 161 | return stack 162 | .split('\n') 163 | .slice(1) 164 | .map(function (item) { 165 | return item.replace(/^\s+at\s+/g, '') 166 | }) 167 | .join('^') 168 | } 169 | function getNowTime() { 170 | return Date.now() 171 | } 172 | function switchToMB(bytes) { 173 | if (typeof bytes !== 'number') { 174 | return null 175 | } 176 | return parseFloat((bytes / Math.pow(1024, 2)).toFixed(2)) 177 | } 178 | function generateId() { 179 | return v4() 180 | } 181 | function getIdentity() { 182 | var key = 'identity' 183 | var identity = sessionStorage.getItem(key) 184 | if (!identity) { 185 | // 生成标识 186 | identity = generateId() 187 | sessionStorage.setItem(key, identity) 188 | } 189 | return identity 190 | } 191 | function getReferer() { 192 | if (typeof document === 'undefined' || document.location == null) return '' 193 | return document.location.href 194 | } 195 | 196 | var isPerformance = function () { 197 | return ( 198 | !!window.performance && 199 | !!window.performance.getEntriesByType && 200 | !!window.performance.mark 201 | ) 202 | } 203 | var isPerformanceObserver = function () { 204 | return !!window.PerformanceObserver 205 | } 206 | var isNavigator = function () { 207 | return !!window.navigator 208 | } 209 | // dom 对象是否在屏幕内 210 | var isInScreen = function (dom) { 211 | var viewportWidth = window.innerWidth 212 | var viewportHeight = window.innerHeight 213 | var rectInfo = dom.getBoundingClientRect() 214 | if ( 215 | rectInfo.left >= 0 && 216 | rectInfo.left < viewportWidth && 217 | rectInfo.top >= 0 && 218 | rectInfo.top < viewportHeight 219 | ) { 220 | return true 221 | } 222 | } 223 | var isIncludeEle = function (node, arr) { 224 | if (!node || node === document.documentElement) { 225 | return false 226 | } 227 | if (arr.includes(node)) { 228 | return true 229 | } 230 | return isIncludeEle(node.parentElement, arr) 231 | } 232 | 233 | // 页面加载完成 234 | var onLoaded = function (callback) { 235 | if (document.readyState === 'complete') { 236 | setTimeout(callback) 237 | } else { 238 | addEventListener('pageshow', callback) 239 | } 240 | } 241 | // 页面隐藏 242 | function onHidden(callback, once) { 243 | var onHiddenOrPageHide = function (event) { 244 | if (event.type === 'pagehide' || document.visibilityState === 'hidden') { 245 | callback(event) 246 | if (once) { 247 | window.removeEventListener('visibilitychange', onHiddenOrPageHide, true) 248 | window.removeEventListener('pagehide', onHiddenOrPageHide, true) 249 | } 250 | } 251 | } 252 | window.addEventListener('visibilitychange', onHiddenOrPageHide, true) 253 | window.addEventListener('pagehide', onHiddenOrPageHide, true) 254 | } 255 | // 页面卸载(关闭)或刷新时调用 256 | var beforeUnload = function (callback) { 257 | window.addEventListener('beforeunload', callback) 258 | } 259 | 260 | var Queue = /** @class */ (function () { 261 | function Queue() { 262 | this.pending = false 263 | this.callbacks = [] 264 | } 265 | Queue.prototype.push = function (fn) { 266 | var _this = this 267 | if (typeof fn !== 'function') return 268 | this.callbacks.push(fn) 269 | if (!this.pending) { 270 | this.pending = true 271 | Promise.resolve().then(function () { 272 | _this.flushCallbacks() 273 | }) 274 | } 275 | } 276 | Queue.prototype.flushCallbacks = function () { 277 | this.pending = false 278 | var copies = this.callbacks.slice() 279 | this.callbacks.length = 0 280 | for (var i = 0; i < copies.length; i++) { 281 | copies[i]() 282 | } 283 | } 284 | Queue.prototype.getCallbacks = function () { 285 | return this.callbacks 286 | } 287 | Queue.prototype.clear = function () { 288 | this.callbacks = [] 289 | } 290 | return Queue 291 | })() 292 | 293 | var ReportInfo = /** @class */ (function () { 294 | function ReportInfo(options) { 295 | this.options = options 296 | this.sendWay = options.sendWay 297 | this.queue = new Queue() 298 | } 299 | ReportInfo.prototype.beforeSend = function (data) { 300 | var commonInfo = { 301 | project: this.options.project, 302 | version: this.options.version, 303 | projectSub: this.options.proSub, 304 | referer: getReferer(), 305 | identity: getIdentity(), 306 | } 307 | return Object.assign(data, commonInfo) 308 | } 309 | ReportInfo.prototype.send = function (data, isImmediate) { 310 | var sendData = this.beforeSend(data) 311 | var fn = 312 | this.sendWay == 'sendBeacon' 313 | ? this.useBeacon(sendData) 314 | : this.sendWay == 'img' 315 | ? this.useImg(sendData) 316 | : this.useAjax(sendData) 317 | isImmediate ? fn() : this.queue.push(fn) 318 | } 319 | ReportInfo.prototype.useImg = function (data) { 320 | var _this = this 321 | var fn = function () { 322 | var img = new Image() 323 | var spliceStr = _this.options.url.indexOf('?') === -1 ? '?' : '&' 324 | img.src = '' 325 | .concat(_this.options.url) 326 | .concat(spliceStr) 327 | .concat(formatParams(data)) 328 | img = null 329 | } 330 | return fn 331 | } 332 | ReportInfo.prototype.useAjax = function (data) { 333 | var _this = this 334 | var fn = function () { 335 | var xhr = new XMLHttpRequest() 336 | xhr.open('POST', _this.options.url) 337 | xhr.withCredentials 338 | xhr.setRequestHeader('Content-Type', 'application/json') 339 | xhr.send(JSON.stringify(data)) 340 | xhr.onreadystatechange = function () { 341 | console.log(xhr.readyState, xhr.status) 342 | } 343 | } 344 | return fn 345 | } 346 | ReportInfo.prototype.useBeacon = function (data) { 347 | var _this = this 348 | if (navigator.sendBeacon) { 349 | var fn = function () { 350 | navigator.sendBeacon(_this.options.url, JSON.stringify(data)) 351 | } 352 | return fn 353 | } else { 354 | return this.useAjax(data) 355 | } 356 | } 357 | return ReportInfo 358 | })() 359 | 360 | /****************************************************************************** 361 | Copyright (c) Microsoft Corporation. 362 | 363 | Permission to use, copy, modify, and/or distribute this software for any 364 | purpose with or without fee is hereby granted. 365 | 366 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 367 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 368 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 369 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 370 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 371 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 372 | PERFORMANCE OF THIS SOFTWARE. 373 | ***************************************************************************** */ 374 | 375 | function __values(o) { 376 | var s = typeof Symbol === 'function' && Symbol.iterator, 377 | m = s && o[s], 378 | i = 0 379 | if (m) return m.call(o) 380 | if (o && typeof o.length === 'number') 381 | return { 382 | next: function () { 383 | if (o && i >= o.length) o = void 0 384 | return { value: o && o[i++], done: !o } 385 | }, 386 | } 387 | throw new TypeError( 388 | s ? 'Object is not iterable.' : 'Symbol.iterator is not defined.' 389 | ) 390 | } 391 | 392 | function __read(o, n) { 393 | var m = typeof Symbol === 'function' && o[Symbol.iterator] 394 | if (!m) return o 395 | var i = m.call(o), 396 | r, 397 | ar = [], 398 | e 399 | try { 400 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value) 401 | } catch (error) { 402 | e = { error: error } 403 | } finally { 404 | try { 405 | if (r && !r.done && (m = i['return'])) m.call(i) 406 | } finally { 407 | if (e) throw e.error 408 | } 409 | } 410 | return ar 411 | } 412 | 413 | var Store = /** @class */ (function () { 414 | function Store() { 415 | this.store = new Map() 416 | } 417 | Store.prototype.get = function (key) { 418 | return this.store.get(key) 419 | } 420 | Store.prototype.set = function (key, val) { 421 | this.store.set(key, val) 422 | } 423 | Store.prototype.clear = function () { 424 | this.store.clear() 425 | } 426 | Store.prototype.getValues = function () { 427 | return Array.from(this.store).reduce(function (obj, _a) { 428 | var _b = __read(_a, 2), 429 | key = _b[0], 430 | value = _b[1] 431 | obj[key] = value 432 | return obj 433 | }, {}) 434 | } 435 | return Store 436 | })() 437 | 438 | function observe(type, handler) { 439 | if (PerformanceObserver.supportedEntryTypes.includes(type)) { 440 | var ob = new PerformanceObserver(function (item) { 441 | return item.getEntries().map(handler) 442 | }) 443 | ob.observe({ type: type, buffered: true }) 444 | return ob 445 | } 446 | } 447 | 448 | var Error$1 = /** @class */ (function () { 449 | function Error(options) { 450 | this.init(options) 451 | this.reportInfo = new ReportInfo(options) 452 | } 453 | Error.prototype.init = function (options) { 454 | this.handleJS() 455 | this.handleAajxError() 456 | if (options.isVue) { 457 | this.handleVue(options.vue) 458 | } 459 | } 460 | Error.prototype.report = function (secondType, value) { 461 | console.log('value', value) 462 | this.reportInfo.send( 463 | { 464 | type: MonitorType.ERROR, 465 | secondType: secondType, 466 | level: Level.ERROR, 467 | time: getNowTime(), 468 | value: value, 469 | }, 470 | true 471 | ) 472 | } 473 | // js错误监控 474 | Error.prototype.handleJS = function () { 475 | var _this = this 476 | window.addEventListener( 477 | 'error', 478 | function (event) { 479 | var target = event.target 480 | if (target && (target.src || target.href)) { 481 | _this.report(ErrorType.RESOURCE, { 482 | message: '资源加载异常了', 483 | filename: target.src || target.href, 484 | tagName: target.tagName, 485 | }) 486 | } else { 487 | var message = event.message, 488 | filename = event.filename, 489 | lineno = event.lineno, 490 | colno = event.colno 491 | _this.report(ErrorType.JS, { 492 | message: message, 493 | filename: filename, 494 | row: lineno, 495 | col: colno, 496 | stack: event.error && getLines(event.error.stack), 497 | }) 498 | } 499 | }, 500 | true 501 | ) 502 | // promise错误捕捉 503 | window.addEventListener( 504 | 'unhandledrejection', 505 | function (event) { 506 | var message 507 | var filename 508 | var line = 0 509 | var column = 0 510 | var stack = '' 511 | var reason = event.reason 512 | if (typeof reason === 'string') { 513 | message = reason 514 | } else if (typeof reason === 'object') { 515 | message = reason.message 516 | if (reason.stack) { 517 | var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/) 518 | filename = matchResult[1] 519 | line = matchResult[2] 520 | column = matchResult[3] 521 | } 522 | stack = getLines(reason.stack) 523 | } 524 | _this.report(ErrorType.PROMISE, { 525 | message: message, 526 | filename: filename, 527 | row: line, 528 | col: column, 529 | stack: stack, 530 | }) 531 | }, 532 | true 533 | ) 534 | } 535 | // vue错误监控; 536 | Error.prototype.handleVue = function (Vue) { 537 | var _this = this 538 | Vue.config.errorHandler = function (error, vm, info) { 539 | var componentName 540 | if (Object.prototype.toString.call(vm) === '[object Object]') { 541 | componentName = vm._isVue 542 | ? vm.$options.name || vm.$options._componentTag 543 | : vm.name 544 | } 545 | var value = { 546 | message: error.message, 547 | info: info, 548 | componentName: componentName, 549 | stack: error.stack, 550 | } 551 | // 匹配到代码报错出现位置 552 | var reg = /.js\:(\d+)\:(\d+)/i 553 | var codePos = error.stack.match(reg) 554 | if (codePos.length) { 555 | value.row = parseInt(codePos[1]) 556 | value.col = parseInt(codePos[2]) 557 | } 558 | _this.report(ErrorType.VUE, value) 559 | } 560 | } 561 | //ajax请求错误 562 | Error.prototype.handleAajxError = function () { 563 | var _this = this 564 | if (!window.XMLHttpRequest) { 565 | return 566 | } 567 | var xhrSend = XMLHttpRequest.prototype.send 568 | XMLHttpRequest.prototype.send = function () { 569 | var args = [] 570 | for (var _i = 0; _i < arguments.length; _i++) { 571 | args[_i] = arguments[_i] 572 | } 573 | if (this.addEventListener) { 574 | this.addEventListener('error', _handleEvent) 575 | this.addEventListener('load', _handleEvent) 576 | this.addEventListener('abort', _handleEvent) 577 | } else { 578 | var tempStateChange_1 = this.onreadystatechange 579 | this.onreadystatechange = function (event) { 580 | tempStateChange_1.apply(this, args) 581 | if (this.readyState === 4) { 582 | _handleEvent(event) 583 | } 584 | } 585 | } 586 | return xhrSend.apply(this, args) 587 | } 588 | var _handleEvent = function (event) { 589 | try { 590 | if (!event) return 591 | var target = event.currentTarget 592 | if (target && target.status !== 200) { 593 | _this.report(ErrorType.AJAX, { 594 | message: target.response, 595 | status: target.status, 596 | statusText: target.statusText, 597 | url: target.responseURL, 598 | }) 599 | } 600 | } catch (error) { 601 | console.log(error) 602 | } 603 | } 604 | } 605 | return Error 606 | })() 607 | 608 | // first-paint 从页面加载开始到第一个像素绘制到屏幕上的时间 609 | // first-contentful-paint 从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间 610 | function getEntriesByFP(setStore) { 611 | var entryFP = performance.getEntriesByName('first-paint')[0] 612 | var entryFCP = performance.getEntriesByName('first-contentful-paint')[0] 613 | setStore(PerformanceType.FP, entryFP.startTime.toFixed(2)) 614 | setStore(PerformanceType.FCP, entryFCP.startTime.toFixed(2)) 615 | } 616 | function getFP(setStore) { 617 | if (!isPerformanceObserver()) { 618 | if (!isPerformance()) { 619 | throw new Error('浏览器不支持performance') 620 | } else { 621 | onLoaded(function () { 622 | getEntriesByFP(setStore) 623 | }) 624 | } 625 | } else { 626 | var entryHandler = function (entry) { 627 | if (ob_1) { 628 | ob_1.disconnect() 629 | } 630 | setStore(PerformanceType.FP, entry.startTime.toFixed(2)) 631 | } 632 | var ob_1 = observe('paint', entryHandler) 633 | } 634 | } 635 | 636 | // largest-contentful-paint 从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间 637 | var lcpDone = false 638 | function isLCPDone() { 639 | return lcpDone 640 | } 641 | function getLCP(setStore) { 642 | if (!isPerformanceObserver()) { 643 | lcpDone = true 644 | throw new Error('浏览器不支持PerformanceObserver') 645 | } else { 646 | var entryHandler = function (entry) { 647 | lcpDone = true 648 | if (ob_1) { 649 | ob_1.disconnect() 650 | } 651 | setStore(PerformanceType.LCP, entry.startTime.toFixed(2)) 652 | } 653 | var ob_1 = observe('largest-contentful-paint', entryHandler) 654 | } 655 | } 656 | 657 | // layout-shift 从页面加载开始和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数 658 | var value = 0 659 | function getCLS(setStore) { 660 | if (!isPerformanceObserver()) { 661 | throw new Error('浏览器不支持PerformanceObserver') 662 | } else { 663 | var entryHandler = function (entry) { 664 | if (!entry.hadRecentInput) { 665 | value += entry.value 666 | } 667 | } 668 | var ob_1 = observe(PerformanceType.CLS, entryHandler) 669 | var stopListening = function () { 670 | if (ob_1 === null || ob_1 === void 0 ? void 0 : ob_1.takeRecords) { 671 | ob_1.takeRecords().map(function (entry) { 672 | if (!entry.hadRecentInput) { 673 | value += entry.value 674 | } 675 | }) 676 | } 677 | ob_1 === null || ob_1 === void 0 ? void 0 : ob_1.disconnect() 678 | setStore(PerformanceType.CLS, value.toFixed(2)) 679 | } 680 | onHidden(stopListening, true) 681 | } 682 | } 683 | 684 | // first-input 测量用户首次与您的站点交互时的时间(即,当他们单击链接,点击按钮或使用自定义的JavaScript驱动控件时)到浏览器实际能够的时间回应这种互动。 685 | function getFID(setStore) { 686 | if (!isPerformanceObserver()) { 687 | throw new Error('浏览器不支持PerformanceObserver') 688 | } else { 689 | var entryHandler = function (entry) { 690 | if (ob_1) { 691 | ob_1.disconnect() 692 | } 693 | setStore(PerformanceType.FID, { 694 | value: entry.startTime.toFixed(2), 695 | event: entry.name, 696 | }) 697 | } 698 | var ob_1 = observe(PerformanceType.FID, entryHandler) 699 | } 700 | } 701 | 702 | //first-screen-paint 首屏渲染时间 703 | var entries = [] 704 | var isOnLoaded = false 705 | onLoaded(function () { 706 | isOnLoaded = true 707 | }) 708 | var timer 709 | function checkDOMChange(setStore) { 710 | clearTimeout(timer) 711 | timer = setTimeout(function () { 712 | // 等 load、lcp 事件触发后并且 DOM 树不再变化时,计算首屏渲染时间 713 | if (isOnLoaded && isLCPDone()) { 714 | setStore(PerformanceType.FSP, getRenderTime()) 715 | entries = null 716 | } else { 717 | checkDOMChange(setStore) 718 | } 719 | }, 500) 720 | } 721 | function getRenderTime() { 722 | var startTime = 0 723 | entries.forEach(function (entry) { 724 | var e_1, _a 725 | try { 726 | for ( 727 | var _b = __values(entry.children), _c = _b.next(); 728 | !_c.done; 729 | _c = _b.next() 730 | ) { 731 | var node = _c.value 732 | if ( 733 | isInScreen(node) && 734 | entry.startTime > startTime && 735 | needToCalculate(node) 736 | ) { 737 | startTime = entry.startTime 738 | break 739 | } 740 | } 741 | } catch (e_1_1) { 742 | e_1 = { error: e_1_1 } 743 | } finally { 744 | try { 745 | if (_c && !_c.done && (_a = _b.return)) _a.call(_b) 746 | } finally { 747 | if (e_1) throw e_1.error 748 | } 749 | } 750 | }) 751 | // 需要和当前页面所有加载图片的时间做对比,取最大值 752 | // 图片请求时间要小于 startTime,响应结束时间要大于 startTime 753 | performance.getEntriesByType('resource').forEach(function (item) { 754 | if ( 755 | item.initiatorType === 'img' && 756 | item.fetchStart < startTime && 757 | item.responseEnd > startTime 758 | ) { 759 | startTime = item.responseEnd 760 | } 761 | }) 762 | return startTime 763 | } 764 | function needToCalculate(node) { 765 | // 隐藏的元素不用计算 766 | if (window.getComputedStyle(node).display === 'none') return false 767 | // 用于统计的图片不用计算 768 | if (node.tagName === 'IMG' && node.width < 2 && node.height < 2) { 769 | return false 770 | } 771 | return true 772 | } 773 | function getFSP(setStore) { 774 | if (!MutationObserver) { 775 | throw new Error('浏览器不支持MutationObserver') 776 | } 777 | var next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout 778 | var ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK', 'META'] 779 | var ob = new MutationObserver(function (mutationList) { 780 | var e_2, _a, e_3, _b 781 | checkDOMChange(setStore) 782 | next(function () { 783 | entry.startTime = performance.now() 784 | }) 785 | var entry = { 786 | startTime: 0, 787 | children: [], 788 | } 789 | try { 790 | for ( 791 | var mutationList_1 = __values(mutationList), 792 | mutationList_1_1 = mutationList_1.next(); 793 | !mutationList_1_1.done; 794 | mutationList_1_1 = mutationList_1.next() 795 | ) { 796 | var mutation = mutationList_1_1.value 797 | if (mutation.addedNodes.length) { 798 | var nodeLists = Array.from(mutation.addedNodes) 799 | try { 800 | for ( 801 | var nodeLists_1 = ((e_3 = void 0), __values(nodeLists)), 802 | nodeLists_1_1 = nodeLists_1.next(); 803 | !nodeLists_1_1.done; 804 | nodeLists_1_1 = nodeLists_1.next() 805 | ) { 806 | var node = nodeLists_1_1.value 807 | if ( 808 | node.nodeType === 1 && 809 | !ignoreDOMList.includes( 810 | node === null || node === void 0 ? void 0 : node.tagName 811 | ) && 812 | !isIncludeEle(node, entry.children) 813 | ) { 814 | entry.children.push(node) 815 | } 816 | } 817 | } catch (e_3_1) { 818 | e_3 = { error: e_3_1 } 819 | } finally { 820 | try { 821 | if ( 822 | nodeLists_1_1 && 823 | !nodeLists_1_1.done && 824 | (_b = nodeLists_1.return) 825 | ) 826 | _b.call(nodeLists_1) 827 | } finally { 828 | if (e_3) throw e_3.error 829 | } 830 | } 831 | } 832 | } 833 | } catch (e_2_1) { 834 | e_2 = { error: e_2_1 } 835 | } finally { 836 | try { 837 | if ( 838 | mutationList_1_1 && 839 | !mutationList_1_1.done && 840 | (_a = mutationList_1.return) 841 | ) 842 | _a.call(mutationList_1) 843 | } finally { 844 | if (e_2) throw e_2.error 845 | } 846 | } 847 | if (entry.children.length) { 848 | entries.push(entry) 849 | } 850 | }) 851 | ob.observe(document, { 852 | childList: true, 853 | subtree: true, 854 | }) 855 | } 856 | 857 | // navigation 可以获取到用户访问一个页面的每个阶段的精确时间 858 | function setPerformanceData(setStore, entry) { 859 | var domainLookupStart = entry.domainLookupStart, 860 | domainLookupEnd = entry.domainLookupEnd, 861 | connectStart = entry.connectStart, 862 | connectEnd = entry.connectEnd, 863 | secureConnectionStart = entry.secureConnectionStart, 864 | requestStart = entry.requestStart, 865 | responseStart = entry.responseStart, 866 | responseEnd = entry.responseEnd, 867 | domInteractive = entry.domInteractive, 868 | domContentLoadedEventStart = entry.domContentLoadedEventStart, 869 | domContentLoadedEventEnd = entry.domContentLoadedEventEnd, 870 | loadEventStart = entry.loadEventStart, 871 | fetchStart = entry.fetchStart 872 | var timing = { 873 | // DNS解析时间 874 | dnsLookup: (domainLookupEnd - domainLookupStart).toFixed(2), 875 | // TCP完成握手时间 876 | initialConnection: (connectEnd - connectStart).toFixed(2), 877 | // ssl连接时间 878 | ssl: secureConnectionStart 879 | ? (connectEnd - secureConnectionStart).toFixed(2) 880 | : 0, 881 | // HTTP请求响应完成时间 882 | ttfb: (responseStart - requestStart).toFixed(2), 883 | // 读取页面第一个字节的时间 884 | contentDownload: (responseEnd - responseStart).toFixed(2), 885 | // dom解析时间 886 | domParse: (domInteractive - responseEnd).toFixed(2), 887 | // DOM 准备就绪时间 888 | deferExecuteDuration: (domContentLoadedEventStart - domInteractive).toFixed( 889 | 2 890 | ), 891 | // 脚本加载时间 892 | domContentLoadedCallback: ( 893 | domContentLoadedEventEnd - domContentLoadedEventStart 894 | ).toFixed(2), 895 | // onload事件时间 896 | resourceLoad: (loadEventStart - domContentLoadedEventEnd).toFixed(2), 897 | // DOM阶段渲染耗时 898 | domReady: (domContentLoadedEventEnd - fetchStart).toFixed(2), 899 | } 900 | setStore(PerformanceType.NAV, timing) 901 | } 902 | function getPerformanceentryTim() { 903 | var entryTim = 904 | performance.getEntriesByType('navigation').length > 0 905 | ? performance.getEntriesByType('navigation')[0] 906 | : performance.timing 907 | return entryTim 908 | } 909 | function getNavTiming(setStore) { 910 | if (!isPerformanceObserver()) { 911 | if (!isPerformance()) { 912 | throw new Error('浏览器不支持performance') 913 | } else { 914 | onLoaded(function () { 915 | setPerformanceData(setStore, getPerformanceentryTim()) 916 | }) 917 | } 918 | } else { 919 | var entryHandler = function (entry) { 920 | if (ob_1) { 921 | ob_1.disconnect() 922 | } 923 | setPerformanceData(setStore, entry) 924 | } 925 | var ob_1 = observe(PerformanceType.NAV, entryHandler) 926 | } 927 | } 928 | 929 | // 获取内存占用空间 930 | function getMemory(setStore) { 931 | if (!isPerformance()) { 932 | throw new Error('浏览器不支持Performance') 933 | } 934 | if (!isNavigator()) { 935 | throw new Error('浏览器不支持Navigator') 936 | } 937 | var value = { 938 | deviceMemory: 'deviceMemory' in navigator ? navigator['deviceMemory'] : 0, 939 | hardwareConcurrency: 940 | 'hardwareConcurrency' in navigator ? navigator['hardwareConcurrency'] : 0, 941 | // 内存大小限制 942 | jsHeapSizeLimit: 943 | 'memory' in performance 944 | ? switchToMB(performance['memory']['jsHeapSizeLimit']) 945 | : 0, 946 | // 可使用的内存大小 947 | totalJSHeapSize: 948 | 'memory' in performance 949 | ? switchToMB(performance['memory']['totalJSHeapSize']) 950 | : 0, 951 | // JS 对象占用的内存数 952 | usedJSHeapSize: 953 | 'memory' in performance 954 | ? switchToMB(performance['memory']['usedJSHeapSize']) 955 | : 0, 956 | } 957 | setStore(PerformanceType.MRY, value) 958 | } 959 | 960 | // 获取网络环境信息 961 | function getNavConnection(setStore) { 962 | if (!isNavigator()) { 963 | throw new Error('浏览器不支持Navigator') 964 | } else { 965 | var connection = 'connection' in navigator ? navigator['connection'] : {} 966 | var value = { 967 | downlink: connection.downlink, 968 | effectiveType: connection.effectiveType, 969 | rtt: connection.rtt, 970 | } 971 | setStore(PerformanceType.NC, value) 972 | } 973 | } 974 | 975 | function getDevices(setStore) { 976 | if (!window.location) { 977 | throw new Error('浏览器不支持location') 978 | } 979 | var host = location.host, 980 | hostname = location.hostname, 981 | href = location.href, 982 | protocol = location.protocol, 983 | origin = location.origin, 984 | port = location.port, 985 | pathname = location.pathname, 986 | search = location.search, 987 | hash = location.hash 988 | var _a = window.screen, 989 | width = _a.width, 990 | height = _a.height 991 | var info = { 992 | host: host, 993 | hostname: hostname, 994 | href: href, 995 | protocol: protocol, 996 | origin: origin, 997 | port: port, 998 | pathname: pathname, 999 | search: search, 1000 | hash: hash, 1001 | userAgent: 'userAgent' in navigator ? navigator.userAgent : '', 1002 | screenResolution: ''.concat(width, '*').concat(height), 1003 | } 1004 | setStore(PerformanceType.DICE, info) 1005 | } 1006 | 1007 | function getWhiteScreen(setStore, options) { 1008 | var isSkeletonScreen = options.isSkeletonScreen 1009 | var pooCount = 0 1010 | var startSampLists = [] 1011 | var nowSampLists = [] 1012 | var containerLists = ['html', 'body', '#app', '#root'] 1013 | var timer = null 1014 | if (options.isSkeletonScreen) { 1015 | if (document.readyState != 'complete') { 1016 | onSamp() 1017 | } 1018 | } else { 1019 | if (document.readyState === 'complete') { 1020 | onSamp() 1021 | } else { 1022 | window.addEventListener('load', onSamp) 1023 | } 1024 | } 1025 | function getSelector(element) { 1026 | if (element.id) { 1027 | return '#' + element.id 1028 | } else if (element.className) { 1029 | return ( 1030 | '.' + 1031 | element.className 1032 | .split(' ') 1033 | .filter(function (item) { 1034 | return !!item 1035 | }) 1036 | .join('.') 1037 | ) 1038 | } else { 1039 | return element.nodeName.toLowerCase() 1040 | } 1041 | } 1042 | function isContainer(element) { 1043 | var selector = getSelector(element) 1044 | if (isSkeletonScreen) { 1045 | pooCount ? nowSampLists.push(selector) : startSampLists.push(selector) 1046 | } 1047 | return containerLists === null || containerLists === void 0 1048 | ? void 0 1049 | : containerLists.includes(selector) 1050 | } 1051 | // 采样对比 1052 | function onSamp() { 1053 | var points = 0 1054 | for (var i = 1; i <= 9; i++) { 1055 | var xElements = document.elementsFromPoint( 1056 | (window.innerWidth * i) / 10, 1057 | window.innerHeight / 2 1058 | ) 1059 | var yElements = document.elementsFromPoint( 1060 | window.innerWidth / 2, 1061 | (window.innerHeight * i) / 10 1062 | ) 1063 | if (isContainer(xElements[0])) points++ 1064 | //避免中心点计算多次 1065 | if (i != 5) { 1066 | if (isContainer(yElements[0])) points++ 1067 | } 1068 | } 1069 | console.log('ds', points) 1070 | if (points != 17) { 1071 | if (isSkeletonScreen) { 1072 | if (!pooCount) return onLoop() 1073 | if (nowSampLists.join() == startSampLists.join()) 1074 | setStore(PerformanceType.WHITE, { isWhite: true }) 1075 | return 1076 | } 1077 | if (timer) { 1078 | clearTimeout(timer) 1079 | timer = null 1080 | } 1081 | } else { 1082 | if (!timer) { 1083 | onLoop() 1084 | } 1085 | } 1086 | setStore(PerformanceType.WHITE, { isWhite: points == 17 ? true : false }) 1087 | } 1088 | //白屏轮训检测 1089 | function onLoop() { 1090 | if (timer) return 1091 | timer = setInterval(function () { 1092 | if (isSkeletonScreen) { 1093 | pooCount++ 1094 | nowSampLists = [] 1095 | } 1096 | onSamp() 1097 | }, 1000) 1098 | } 1099 | } 1100 | 1101 | var Performance = /** @class */ (function () { 1102 | function Performance(options) { 1103 | this.newStore = new Store() 1104 | this.reportInfo = new ReportInfo(options) 1105 | this.init(options) 1106 | } 1107 | Performance.prototype.init = function (options) { 1108 | getFP(this.setStore.bind(this)) 1109 | getLCP(this.setStore.bind(this)) 1110 | getCLS(this.setStore.bind(this)) 1111 | getFID(this.setStore.bind(this)) 1112 | getNavConnection(this.setStore.bind(this)) 1113 | getNavTiming(this.setStore.bind(this)) 1114 | getFSP(this.setStore.bind(this)) 1115 | getMemory(this.setStore.bind(this)) 1116 | getDevices(this.setStore.bind(this)) 1117 | getWhiteScreen(this.setStore.bind(this), options) 1118 | this.report() 1119 | } 1120 | Performance.prototype.report = function () { 1121 | var _this = this 1122 | ;[onLoaded, onHidden, beforeUnload].forEach(function (event) { 1123 | event(function () { 1124 | var storeData = _this.newStore.getValues() 1125 | Object.keys(storeData).forEach(function (key) { 1126 | _this.reportInfo.send(storeData[key]) 1127 | }) 1128 | _this.newStore.clear() 1129 | }) 1130 | }) 1131 | } 1132 | Performance.prototype.setStore = function (secondType, value) { 1133 | var data = { 1134 | type: MonitorType.PERFORMANCE, 1135 | secondType: secondType, 1136 | level: Level.INFO, 1137 | time: getNowTime(), 1138 | value: value, 1139 | } 1140 | this.newStore.set(secondType, data) 1141 | } 1142 | return Performance 1143 | })() 1144 | 1145 | var Monitor = /** @class */ (function () { 1146 | function Monitor(options) { 1147 | this.init(options) 1148 | } 1149 | Monitor.prototype.init = function (options) { 1150 | if (!this.isSetCondition(options)) return 1151 | this.setDefault(options) 1152 | if (options.isCollectPer) { 1153 | new Performance(options) 1154 | } 1155 | if (options.isCollectErr) { 1156 | new Error$1(options) 1157 | } 1158 | } 1159 | Monitor.prototype.isSetCondition = function (options) { 1160 | if (!options.url) { 1161 | console.error('上报url必传') 1162 | return false 1163 | } 1164 | if (!options.project) { 1165 | console.error('项目project必传') 1166 | return false 1167 | } 1168 | if (!options.version) { 1169 | console.error('项目版本号必传') 1170 | return false 1171 | } 1172 | if (options.isVue && !options.vue) { 1173 | console.log('如果isVue为true时,请在vue字段上传入Vue') 1174 | return false 1175 | } 1176 | return true 1177 | } 1178 | Monitor.prototype.setDefault = function (options) { 1179 | Object.keys(defaultOptions).forEach(function (key) { 1180 | if (options[key] == null) { 1181 | options[key] = defaultOptions[key] 1182 | } 1183 | }) 1184 | } 1185 | return Monitor 1186 | })() 1187 | 1188 | module.exports = Monitor 1189 | -------------------------------------------------------------------------------- /packages/monitor/lib/index.esm.js: -------------------------------------------------------------------------------- 1 | var defaultOptions = { 2 | url: '//127.0.0.1:8000/api/collect/info/detail', 3 | sendWay: 'sendBeacon', 4 | isCollectErr: true, 5 | isCollectPer: true, 6 | isCollectBehavior: true, 7 | } 8 | 9 | var MonitorType 10 | ;(function (MonitorType) { 11 | MonitorType['ERROR'] = 'error' 12 | MonitorType['PERFORMANCE'] = 'performance' 13 | MonitorType['BEHAVIOR'] = 'behavior' 14 | })(MonitorType || (MonitorType = {})) 15 | var Level 16 | ;(function (Level) { 17 | Level['ERROR'] = 'error' 18 | Level['WARN'] = 'warn' 19 | Level['INFO'] = 'info' 20 | })(Level || (Level = {})) 21 | 22 | var ErrorType 23 | ;(function (ErrorType) { 24 | ErrorType['JS'] = 'js_error' 25 | ErrorType['RESOURCE'] = 'resource_error' 26 | ErrorType['VUE'] = 'vue_error' 27 | ErrorType['PROMISE'] = 'promise_error' 28 | ErrorType['AJAX'] = 'ajax_error' 29 | })(ErrorType || (ErrorType = {})) 30 | 31 | var PerformanceType 32 | ;(function (PerformanceType) { 33 | PerformanceType['FP'] = 'first-paint' 34 | PerformanceType['FCP'] = 'first-contentful-paint' 35 | PerformanceType['LCP'] = 'largest-contentful-paint' 36 | PerformanceType['CLS'] = 'layout-shift' 37 | PerformanceType['FID'] = 'first-input' 38 | PerformanceType['FSP'] = 'first-screen-paint' 39 | PerformanceType['NC'] = 'nav-connecttion' 40 | PerformanceType['NAV'] = 'navigation' 41 | PerformanceType['MRY'] = 'memory' 42 | PerformanceType['DICE'] = 'devices' 43 | PerformanceType['WHITE'] = 'white-screen' 44 | })(PerformanceType || (PerformanceType = {})) 45 | 46 | var BehaviorType 47 | ;(function (BehaviorType) { 48 | BehaviorType['PV'] = 'pv' 49 | BehaviorType['VJ'] = 'vue-jump' 50 | BehaviorType['PD'] = 'page-duration' 51 | })(BehaviorType || (BehaviorType = {})) 52 | 53 | // Unique ID creation requires a high quality random # generator. In the browser we therefore 54 | // require the crypto API and do not support built-in fallback to lower quality random number 55 | // generators (like Math.random()). 56 | let getRandomValues 57 | const rnds8 = new Uint8Array(16) 58 | function rng() { 59 | // lazy load so that environments that need to polyfill have a chance to do so 60 | if (!getRandomValues) { 61 | // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. 62 | getRandomValues = 63 | typeof crypto !== 'undefined' && 64 | crypto.getRandomValues && 65 | crypto.getRandomValues.bind(crypto) 66 | 67 | if (!getRandomValues) { 68 | throw new Error( 69 | 'crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported' 70 | ) 71 | } 72 | } 73 | 74 | return getRandomValues(rnds8) 75 | } 76 | 77 | /** 78 | * Convert array of 16 byte values to UUID string format of the form: 79 | * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 80 | */ 81 | 82 | const byteToHex = [] 83 | 84 | for (let i = 0; i < 256; ++i) { 85 | byteToHex.push((i + 0x100).toString(16).slice(1)) 86 | } 87 | 88 | function unsafeStringify(arr, offset = 0) { 89 | // Note: Be careful editing this code! It's been tuned for performance 90 | // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 91 | return ( 92 | byteToHex[arr[offset + 0]] + 93 | byteToHex[arr[offset + 1]] + 94 | byteToHex[arr[offset + 2]] + 95 | byteToHex[arr[offset + 3]] + 96 | '-' + 97 | byteToHex[arr[offset + 4]] + 98 | byteToHex[arr[offset + 5]] + 99 | '-' + 100 | byteToHex[arr[offset + 6]] + 101 | byteToHex[arr[offset + 7]] + 102 | '-' + 103 | byteToHex[arr[offset + 8]] + 104 | byteToHex[arr[offset + 9]] + 105 | '-' + 106 | byteToHex[arr[offset + 10]] + 107 | byteToHex[arr[offset + 11]] + 108 | byteToHex[arr[offset + 12]] + 109 | byteToHex[arr[offset + 13]] + 110 | byteToHex[arr[offset + 14]] + 111 | byteToHex[arr[offset + 15]] 112 | ).toLowerCase() 113 | } 114 | 115 | const randomUUID = 116 | typeof crypto !== 'undefined' && 117 | crypto.randomUUID && 118 | crypto.randomUUID.bind(crypto) 119 | var native = { 120 | randomUUID, 121 | } 122 | 123 | function v4(options, buf, offset) { 124 | if (native.randomUUID && !buf && !options) { 125 | return native.randomUUID() 126 | } 127 | 128 | options = options || {} 129 | const rnds = options.random || (options.rng || rng)() // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` 130 | 131 | rnds[6] = (rnds[6] & 0x0f) | 0x40 132 | rnds[8] = (rnds[8] & 0x3f) | 0x80 // Copy bytes to buffer, if provided 133 | 134 | if (buf) { 135 | offset = offset || 0 136 | 137 | for (let i = 0; i < 16; ++i) { 138 | buf[offset + i] = rnds[i] 139 | } 140 | 141 | return buf 142 | } 143 | 144 | return unsafeStringify(rnds) 145 | } 146 | 147 | function formatParams(obj) { 148 | var strArr = [] 149 | Object.keys(obj).forEach(function (key) { 150 | strArr.push( 151 | '' 152 | .concat(encodeURIComponent(key), '=') 153 | .concat(encodeURIComponent(obj[key])) 154 | ) 155 | }) 156 | return strArr.join('&') 157 | } 158 | function getLines(stack) { 159 | return stack 160 | .split('\n') 161 | .slice(1) 162 | .map(function (item) { 163 | return item.replace(/^\s+at\s+/g, '') 164 | }) 165 | .join('^') 166 | } 167 | function getNowTime() { 168 | return Date.now() 169 | } 170 | function switchToMB(bytes) { 171 | if (typeof bytes !== 'number') { 172 | return null 173 | } 174 | return parseFloat((bytes / Math.pow(1024, 2)).toFixed(2)) 175 | } 176 | function generateId() { 177 | return v4() 178 | } 179 | function getIdentity() { 180 | var key = 'identity' 181 | var identity = sessionStorage.getItem(key) 182 | if (!identity) { 183 | // 生成标识 184 | identity = generateId() 185 | sessionStorage.setItem(key, identity) 186 | } 187 | return identity 188 | } 189 | function getReferer() { 190 | if (typeof document === 'undefined' || document.location == null) return '' 191 | return document.location.href 192 | } 193 | 194 | var isPerformance = function () { 195 | return ( 196 | !!window.performance && 197 | !!window.performance.getEntriesByType && 198 | !!window.performance.mark 199 | ) 200 | } 201 | var isPerformanceObserver = function () { 202 | return !!window.PerformanceObserver 203 | } 204 | var isNavigator = function () { 205 | return !!window.navigator 206 | } 207 | // dom 对象是否在屏幕内 208 | var isInScreen = function (dom) { 209 | var viewportWidth = window.innerWidth 210 | var viewportHeight = window.innerHeight 211 | var rectInfo = dom.getBoundingClientRect() 212 | if ( 213 | rectInfo.left >= 0 && 214 | rectInfo.left < viewportWidth && 215 | rectInfo.top >= 0 && 216 | rectInfo.top < viewportHeight 217 | ) { 218 | return true 219 | } 220 | } 221 | var isIncludeEle = function (node, arr) { 222 | if (!node || node === document.documentElement) { 223 | return false 224 | } 225 | if (arr.includes(node)) { 226 | return true 227 | } 228 | return isIncludeEle(node.parentElement, arr) 229 | } 230 | 231 | // 页面加载完成 232 | var onLoaded = function (callback) { 233 | if (document.readyState === 'complete') { 234 | setTimeout(callback) 235 | } else { 236 | addEventListener('pageshow', callback) 237 | } 238 | } 239 | // 页面隐藏 240 | function onHidden(callback, once) { 241 | var onHiddenOrPageHide = function (event) { 242 | if (event.type === 'pagehide' || document.visibilityState === 'hidden') { 243 | callback(event) 244 | if (once) { 245 | window.removeEventListener('visibilitychange', onHiddenOrPageHide, true) 246 | window.removeEventListener('pagehide', onHiddenOrPageHide, true) 247 | } 248 | } 249 | } 250 | window.addEventListener('visibilitychange', onHiddenOrPageHide, true) 251 | window.addEventListener('pagehide', onHiddenOrPageHide, true) 252 | } 253 | // 页面卸载(关闭)或刷新时调用 254 | var beforeUnload = function (callback) { 255 | window.addEventListener('beforeunload', callback) 256 | } 257 | 258 | var Queue = /** @class */ (function () { 259 | function Queue() { 260 | this.pending = false 261 | this.callbacks = [] 262 | } 263 | Queue.prototype.push = function (fn) { 264 | var _this = this 265 | if (typeof fn !== 'function') return 266 | this.callbacks.push(fn) 267 | if (!this.pending) { 268 | this.pending = true 269 | Promise.resolve().then(function () { 270 | _this.flushCallbacks() 271 | }) 272 | } 273 | } 274 | Queue.prototype.flushCallbacks = function () { 275 | this.pending = false 276 | var copies = this.callbacks.slice() 277 | this.callbacks.length = 0 278 | for (var i = 0; i < copies.length; i++) { 279 | copies[i]() 280 | } 281 | } 282 | Queue.prototype.getCallbacks = function () { 283 | return this.callbacks 284 | } 285 | Queue.prototype.clear = function () { 286 | this.callbacks = [] 287 | } 288 | return Queue 289 | })() 290 | 291 | var ReportInfo = /** @class */ (function () { 292 | function ReportInfo(options) { 293 | this.options = options 294 | this.sendWay = options.sendWay 295 | this.queue = new Queue() 296 | } 297 | ReportInfo.prototype.beforeSend = function (data) { 298 | var commonInfo = { 299 | project: this.options.project, 300 | version: this.options.version, 301 | projectSub: this.options.proSub, 302 | referer: getReferer(), 303 | identity: getIdentity(), 304 | } 305 | return Object.assign(data, commonInfo) 306 | } 307 | ReportInfo.prototype.send = function (data, isImmediate) { 308 | var sendData = this.beforeSend(data) 309 | var fn = 310 | this.sendWay == 'sendBeacon' 311 | ? this.useBeacon(sendData) 312 | : this.sendWay == 'img' 313 | ? this.useImg(sendData) 314 | : this.useAjax(sendData) 315 | isImmediate ? fn() : this.queue.push(fn) 316 | } 317 | ReportInfo.prototype.useImg = function (data) { 318 | var _this = this 319 | var fn = function () { 320 | var img = new Image() 321 | var spliceStr = _this.options.url.indexOf('?') === -1 ? '?' : '&' 322 | img.src = '' 323 | .concat(_this.options.url) 324 | .concat(spliceStr) 325 | .concat(formatParams(data)) 326 | img = null 327 | } 328 | return fn 329 | } 330 | ReportInfo.prototype.useAjax = function (data) { 331 | var _this = this 332 | var fn = function () { 333 | var xhr = new XMLHttpRequest() 334 | xhr.open('POST', _this.options.url) 335 | xhr.withCredentials 336 | xhr.setRequestHeader('Content-Type', 'application/json') 337 | xhr.send(JSON.stringify(data)) 338 | xhr.onreadystatechange = function () { 339 | console.log(xhr.readyState, xhr.status) 340 | } 341 | } 342 | return fn 343 | } 344 | ReportInfo.prototype.useBeacon = function (data) { 345 | var _this = this 346 | if (navigator.sendBeacon) { 347 | var fn = function () { 348 | navigator.sendBeacon(_this.options.url, JSON.stringify(data)) 349 | } 350 | return fn 351 | } else { 352 | return this.useAjax(data) 353 | } 354 | } 355 | return ReportInfo 356 | })() 357 | 358 | /****************************************************************************** 359 | Copyright (c) Microsoft Corporation. 360 | 361 | Permission to use, copy, modify, and/or distribute this software for any 362 | purpose with or without fee is hereby granted. 363 | 364 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 365 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 366 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 367 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 368 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 369 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 370 | PERFORMANCE OF THIS SOFTWARE. 371 | ***************************************************************************** */ 372 | 373 | function __values(o) { 374 | var s = typeof Symbol === 'function' && Symbol.iterator, 375 | m = s && o[s], 376 | i = 0 377 | if (m) return m.call(o) 378 | if (o && typeof o.length === 'number') 379 | return { 380 | next: function () { 381 | if (o && i >= o.length) o = void 0 382 | return { value: o && o[i++], done: !o } 383 | }, 384 | } 385 | throw new TypeError( 386 | s ? 'Object is not iterable.' : 'Symbol.iterator is not defined.' 387 | ) 388 | } 389 | 390 | function __read(o, n) { 391 | var m = typeof Symbol === 'function' && o[Symbol.iterator] 392 | if (!m) return o 393 | var i = m.call(o), 394 | r, 395 | ar = [], 396 | e 397 | try { 398 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value) 399 | } catch (error) { 400 | e = { error: error } 401 | } finally { 402 | try { 403 | if (r && !r.done && (m = i['return'])) m.call(i) 404 | } finally { 405 | if (e) throw e.error 406 | } 407 | } 408 | return ar 409 | } 410 | 411 | var Store = /** @class */ (function () { 412 | function Store() { 413 | this.store = new Map() 414 | } 415 | Store.prototype.get = function (key) { 416 | return this.store.get(key) 417 | } 418 | Store.prototype.set = function (key, val) { 419 | this.store.set(key, val) 420 | } 421 | Store.prototype.clear = function () { 422 | this.store.clear() 423 | } 424 | Store.prototype.getValues = function () { 425 | return Array.from(this.store).reduce(function (obj, _a) { 426 | var _b = __read(_a, 2), 427 | key = _b[0], 428 | value = _b[1] 429 | obj[key] = value 430 | return obj 431 | }, {}) 432 | } 433 | return Store 434 | })() 435 | 436 | function observe(type, handler) { 437 | if (PerformanceObserver.supportedEntryTypes.includes(type)) { 438 | var ob = new PerformanceObserver(function (item) { 439 | return item.getEntries().map(handler) 440 | }) 441 | ob.observe({ type: type, buffered: true }) 442 | return ob 443 | } 444 | } 445 | 446 | var Error$1 = /** @class */ (function () { 447 | function Error(options) { 448 | this.init(options) 449 | this.reportInfo = new ReportInfo(options) 450 | } 451 | Error.prototype.init = function (options) { 452 | this.handleJS() 453 | this.handleAajxError() 454 | if (options.isVue) { 455 | this.handleVue(options.vue) 456 | } 457 | } 458 | Error.prototype.report = function (secondType, value) { 459 | console.log('value', value) 460 | this.reportInfo.send( 461 | { 462 | type: MonitorType.ERROR, 463 | secondType: secondType, 464 | level: Level.ERROR, 465 | time: getNowTime(), 466 | value: value, 467 | }, 468 | true 469 | ) 470 | } 471 | // js错误监控 472 | Error.prototype.handleJS = function () { 473 | var _this = this 474 | window.addEventListener( 475 | 'error', 476 | function (event) { 477 | var target = event.target 478 | if (target && (target.src || target.href)) { 479 | _this.report(ErrorType.RESOURCE, { 480 | message: '资源加载异常了', 481 | filename: target.src || target.href, 482 | tagName: target.tagName, 483 | }) 484 | } else { 485 | var message = event.message, 486 | filename = event.filename, 487 | lineno = event.lineno, 488 | colno = event.colno 489 | _this.report(ErrorType.JS, { 490 | message: message, 491 | filename: filename, 492 | row: lineno, 493 | col: colno, 494 | stack: event.error && getLines(event.error.stack), 495 | }) 496 | } 497 | }, 498 | true 499 | ) 500 | // promise错误捕捉 501 | window.addEventListener( 502 | 'unhandledrejection', 503 | function (event) { 504 | var message 505 | var filename 506 | var line = 0 507 | var column = 0 508 | var stack = '' 509 | var reason = event.reason 510 | if (typeof reason === 'string') { 511 | message = reason 512 | } else if (typeof reason === 'object') { 513 | message = reason.message 514 | if (reason.stack) { 515 | var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/) 516 | filename = matchResult[1] 517 | line = matchResult[2] 518 | column = matchResult[3] 519 | } 520 | stack = getLines(reason.stack) 521 | } 522 | _this.report(ErrorType.PROMISE, { 523 | message: message, 524 | filename: filename, 525 | row: line, 526 | col: column, 527 | stack: stack, 528 | }) 529 | }, 530 | true 531 | ) 532 | } 533 | // vue错误监控; 534 | Error.prototype.handleVue = function (Vue) { 535 | var _this = this 536 | Vue.config.errorHandler = function (error, vm, info) { 537 | var componentName 538 | if (Object.prototype.toString.call(vm) === '[object Object]') { 539 | componentName = vm._isVue 540 | ? vm.$options.name || vm.$options._componentTag 541 | : vm.name 542 | } 543 | var value = { 544 | message: error.message, 545 | info: info, 546 | componentName: componentName, 547 | stack: error.stack, 548 | } 549 | // 匹配到代码报错出现位置 550 | var reg = /.js\:(\d+)\:(\d+)/i 551 | var codePos = error.stack.match(reg) 552 | if (codePos.length) { 553 | value.row = parseInt(codePos[1]) 554 | value.col = parseInt(codePos[2]) 555 | } 556 | _this.report(ErrorType.VUE, value) 557 | } 558 | } 559 | //ajax请求错误 560 | Error.prototype.handleAajxError = function () { 561 | var _this = this 562 | if (!window.XMLHttpRequest) { 563 | return 564 | } 565 | var xhrSend = XMLHttpRequest.prototype.send 566 | XMLHttpRequest.prototype.send = function () { 567 | var args = [] 568 | for (var _i = 0; _i < arguments.length; _i++) { 569 | args[_i] = arguments[_i] 570 | } 571 | if (this.addEventListener) { 572 | this.addEventListener('error', _handleEvent) 573 | this.addEventListener('load', _handleEvent) 574 | this.addEventListener('abort', _handleEvent) 575 | } else { 576 | var tempStateChange_1 = this.onreadystatechange 577 | this.onreadystatechange = function (event) { 578 | tempStateChange_1.apply(this, args) 579 | if (this.readyState === 4) { 580 | _handleEvent(event) 581 | } 582 | } 583 | } 584 | return xhrSend.apply(this, args) 585 | } 586 | var _handleEvent = function (event) { 587 | try { 588 | if (!event) return 589 | var target = event.currentTarget 590 | if (target && target.status !== 200) { 591 | _this.report(ErrorType.AJAX, { 592 | message: target.response, 593 | status: target.status, 594 | statusText: target.statusText, 595 | url: target.responseURL, 596 | }) 597 | } 598 | } catch (error) { 599 | console.log(error) 600 | } 601 | } 602 | } 603 | return Error 604 | })() 605 | 606 | // first-paint 从页面加载开始到第一个像素绘制到屏幕上的时间 607 | // first-contentful-paint 从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间 608 | function getEntriesByFP(setStore) { 609 | var entryFP = performance.getEntriesByName('first-paint')[0] 610 | var entryFCP = performance.getEntriesByName('first-contentful-paint')[0] 611 | setStore(PerformanceType.FP, entryFP.startTime.toFixed(2)) 612 | setStore(PerformanceType.FCP, entryFCP.startTime.toFixed(2)) 613 | } 614 | function getFP(setStore) { 615 | if (!isPerformanceObserver()) { 616 | if (!isPerformance()) { 617 | throw new Error('浏览器不支持performance') 618 | } else { 619 | onLoaded(function () { 620 | getEntriesByFP(setStore) 621 | }) 622 | } 623 | } else { 624 | var entryHandler = function (entry) { 625 | if (ob_1) { 626 | ob_1.disconnect() 627 | } 628 | setStore(PerformanceType.FP, entry.startTime.toFixed(2)) 629 | } 630 | var ob_1 = observe('paint', entryHandler) 631 | } 632 | } 633 | 634 | // largest-contentful-paint 从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间 635 | var lcpDone = false 636 | function isLCPDone() { 637 | return lcpDone 638 | } 639 | function getLCP(setStore) { 640 | if (!isPerformanceObserver()) { 641 | lcpDone = true 642 | throw new Error('浏览器不支持PerformanceObserver') 643 | } else { 644 | var entryHandler = function (entry) { 645 | lcpDone = true 646 | if (ob_1) { 647 | ob_1.disconnect() 648 | } 649 | setStore(PerformanceType.LCP, entry.startTime.toFixed(2)) 650 | } 651 | var ob_1 = observe('largest-contentful-paint', entryHandler) 652 | } 653 | } 654 | 655 | // layout-shift 从页面加载开始和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数 656 | var value = 0 657 | function getCLS(setStore) { 658 | if (!isPerformanceObserver()) { 659 | throw new Error('浏览器不支持PerformanceObserver') 660 | } else { 661 | var entryHandler = function (entry) { 662 | if (!entry.hadRecentInput) { 663 | value += entry.value 664 | } 665 | } 666 | var ob_1 = observe(PerformanceType.CLS, entryHandler) 667 | var stopListening = function () { 668 | if (ob_1 === null || ob_1 === void 0 ? void 0 : ob_1.takeRecords) { 669 | ob_1.takeRecords().map(function (entry) { 670 | if (!entry.hadRecentInput) { 671 | value += entry.value 672 | } 673 | }) 674 | } 675 | ob_1 === null || ob_1 === void 0 ? void 0 : ob_1.disconnect() 676 | setStore(PerformanceType.CLS, value.toFixed(2)) 677 | } 678 | onHidden(stopListening, true) 679 | } 680 | } 681 | 682 | // first-input 测量用户首次与您的站点交互时的时间(即,当他们单击链接,点击按钮或使用自定义的JavaScript驱动控件时)到浏览器实际能够的时间回应这种互动。 683 | function getFID(setStore) { 684 | if (!isPerformanceObserver()) { 685 | throw new Error('浏览器不支持PerformanceObserver') 686 | } else { 687 | var entryHandler = function (entry) { 688 | if (ob_1) { 689 | ob_1.disconnect() 690 | } 691 | setStore(PerformanceType.FID, { 692 | value: entry.startTime.toFixed(2), 693 | event: entry.name, 694 | }) 695 | } 696 | var ob_1 = observe(PerformanceType.FID, entryHandler) 697 | } 698 | } 699 | 700 | //first-screen-paint 首屏渲染时间 701 | var entries = [] 702 | var isOnLoaded = false 703 | onLoaded(function () { 704 | isOnLoaded = true 705 | }) 706 | var timer 707 | function checkDOMChange(setStore) { 708 | clearTimeout(timer) 709 | timer = setTimeout(function () { 710 | // 等 load、lcp 事件触发后并且 DOM 树不再变化时,计算首屏渲染时间 711 | if (isOnLoaded && isLCPDone()) { 712 | setStore(PerformanceType.FSP, getRenderTime()) 713 | entries = null 714 | } else { 715 | checkDOMChange(setStore) 716 | } 717 | }, 500) 718 | } 719 | function getRenderTime() { 720 | var startTime = 0 721 | entries.forEach(function (entry) { 722 | var e_1, _a 723 | try { 724 | for ( 725 | var _b = __values(entry.children), _c = _b.next(); 726 | !_c.done; 727 | _c = _b.next() 728 | ) { 729 | var node = _c.value 730 | if ( 731 | isInScreen(node) && 732 | entry.startTime > startTime && 733 | needToCalculate(node) 734 | ) { 735 | startTime = entry.startTime 736 | break 737 | } 738 | } 739 | } catch (e_1_1) { 740 | e_1 = { error: e_1_1 } 741 | } finally { 742 | try { 743 | if (_c && !_c.done && (_a = _b.return)) _a.call(_b) 744 | } finally { 745 | if (e_1) throw e_1.error 746 | } 747 | } 748 | }) 749 | // 需要和当前页面所有加载图片的时间做对比,取最大值 750 | // 图片请求时间要小于 startTime,响应结束时间要大于 startTime 751 | performance.getEntriesByType('resource').forEach(function (item) { 752 | if ( 753 | item.initiatorType === 'img' && 754 | item.fetchStart < startTime && 755 | item.responseEnd > startTime 756 | ) { 757 | startTime = item.responseEnd 758 | } 759 | }) 760 | return startTime 761 | } 762 | function needToCalculate(node) { 763 | // 隐藏的元素不用计算 764 | if (window.getComputedStyle(node).display === 'none') return false 765 | // 用于统计的图片不用计算 766 | if (node.tagName === 'IMG' && node.width < 2 && node.height < 2) { 767 | return false 768 | } 769 | return true 770 | } 771 | function getFSP(setStore) { 772 | if (!MutationObserver) { 773 | throw new Error('浏览器不支持MutationObserver') 774 | } 775 | var next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout 776 | var ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK', 'META'] 777 | var ob = new MutationObserver(function (mutationList) { 778 | var e_2, _a, e_3, _b 779 | checkDOMChange(setStore) 780 | next(function () { 781 | entry.startTime = performance.now() 782 | }) 783 | var entry = { 784 | startTime: 0, 785 | children: [], 786 | } 787 | try { 788 | for ( 789 | var mutationList_1 = __values(mutationList), 790 | mutationList_1_1 = mutationList_1.next(); 791 | !mutationList_1_1.done; 792 | mutationList_1_1 = mutationList_1.next() 793 | ) { 794 | var mutation = mutationList_1_1.value 795 | if (mutation.addedNodes.length) { 796 | var nodeLists = Array.from(mutation.addedNodes) 797 | try { 798 | for ( 799 | var nodeLists_1 = ((e_3 = void 0), __values(nodeLists)), 800 | nodeLists_1_1 = nodeLists_1.next(); 801 | !nodeLists_1_1.done; 802 | nodeLists_1_1 = nodeLists_1.next() 803 | ) { 804 | var node = nodeLists_1_1.value 805 | if ( 806 | node.nodeType === 1 && 807 | !ignoreDOMList.includes( 808 | node === null || node === void 0 ? void 0 : node.tagName 809 | ) && 810 | !isIncludeEle(node, entry.children) 811 | ) { 812 | entry.children.push(node) 813 | } 814 | } 815 | } catch (e_3_1) { 816 | e_3 = { error: e_3_1 } 817 | } finally { 818 | try { 819 | if ( 820 | nodeLists_1_1 && 821 | !nodeLists_1_1.done && 822 | (_b = nodeLists_1.return) 823 | ) 824 | _b.call(nodeLists_1) 825 | } finally { 826 | if (e_3) throw e_3.error 827 | } 828 | } 829 | } 830 | } 831 | } catch (e_2_1) { 832 | e_2 = { error: e_2_1 } 833 | } finally { 834 | try { 835 | if ( 836 | mutationList_1_1 && 837 | !mutationList_1_1.done && 838 | (_a = mutationList_1.return) 839 | ) 840 | _a.call(mutationList_1) 841 | } finally { 842 | if (e_2) throw e_2.error 843 | } 844 | } 845 | if (entry.children.length) { 846 | entries.push(entry) 847 | } 848 | }) 849 | ob.observe(document, { 850 | childList: true, 851 | subtree: true, 852 | }) 853 | } 854 | 855 | // navigation 可以获取到用户访问一个页面的每个阶段的精确时间 856 | function setPerformanceData(setStore, entry) { 857 | var domainLookupStart = entry.domainLookupStart, 858 | domainLookupEnd = entry.domainLookupEnd, 859 | connectStart = entry.connectStart, 860 | connectEnd = entry.connectEnd, 861 | secureConnectionStart = entry.secureConnectionStart, 862 | requestStart = entry.requestStart, 863 | responseStart = entry.responseStart, 864 | responseEnd = entry.responseEnd, 865 | domInteractive = entry.domInteractive, 866 | domContentLoadedEventStart = entry.domContentLoadedEventStart, 867 | domContentLoadedEventEnd = entry.domContentLoadedEventEnd, 868 | loadEventStart = entry.loadEventStart, 869 | fetchStart = entry.fetchStart 870 | var timing = { 871 | // DNS解析时间 872 | dnsLookup: (domainLookupEnd - domainLookupStart).toFixed(2), 873 | // TCP完成握手时间 874 | initialConnection: (connectEnd - connectStart).toFixed(2), 875 | // ssl连接时间 876 | ssl: secureConnectionStart 877 | ? (connectEnd - secureConnectionStart).toFixed(2) 878 | : 0, 879 | // HTTP请求响应完成时间 880 | ttfb: (responseStart - requestStart).toFixed(2), 881 | // 读取页面第一个字节的时间 882 | contentDownload: (responseEnd - responseStart).toFixed(2), 883 | // dom解析时间 884 | domParse: (domInteractive - responseEnd).toFixed(2), 885 | // DOM 准备就绪时间 886 | deferExecuteDuration: (domContentLoadedEventStart - domInteractive).toFixed( 887 | 2 888 | ), 889 | // 脚本加载时间 890 | domContentLoadedCallback: ( 891 | domContentLoadedEventEnd - domContentLoadedEventStart 892 | ).toFixed(2), 893 | // onload事件时间 894 | resourceLoad: (loadEventStart - domContentLoadedEventEnd).toFixed(2), 895 | // DOM阶段渲染耗时 896 | domReady: (domContentLoadedEventEnd - fetchStart).toFixed(2), 897 | } 898 | setStore(PerformanceType.NAV, timing) 899 | } 900 | function getPerformanceentryTim() { 901 | var entryTim = 902 | performance.getEntriesByType('navigation').length > 0 903 | ? performance.getEntriesByType('navigation')[0] 904 | : performance.timing 905 | return entryTim 906 | } 907 | function getNavTiming(setStore) { 908 | if (!isPerformanceObserver()) { 909 | if (!isPerformance()) { 910 | throw new Error('浏览器不支持performance') 911 | } else { 912 | onLoaded(function () { 913 | setPerformanceData(setStore, getPerformanceentryTim()) 914 | }) 915 | } 916 | } else { 917 | var entryHandler = function (entry) { 918 | if (ob_1) { 919 | ob_1.disconnect() 920 | } 921 | setPerformanceData(setStore, entry) 922 | } 923 | var ob_1 = observe(PerformanceType.NAV, entryHandler) 924 | } 925 | } 926 | 927 | // 获取内存占用空间 928 | function getMemory(setStore) { 929 | if (!isPerformance()) { 930 | throw new Error('浏览器不支持Performance') 931 | } 932 | if (!isNavigator()) { 933 | throw new Error('浏览器不支持Navigator') 934 | } 935 | var value = { 936 | deviceMemory: 'deviceMemory' in navigator ? navigator['deviceMemory'] : 0, 937 | hardwareConcurrency: 938 | 'hardwareConcurrency' in navigator ? navigator['hardwareConcurrency'] : 0, 939 | // 内存大小限制 940 | jsHeapSizeLimit: 941 | 'memory' in performance 942 | ? switchToMB(performance['memory']['jsHeapSizeLimit']) 943 | : 0, 944 | // 可使用的内存大小 945 | totalJSHeapSize: 946 | 'memory' in performance 947 | ? switchToMB(performance['memory']['totalJSHeapSize']) 948 | : 0, 949 | // JS 对象占用的内存数 950 | usedJSHeapSize: 951 | 'memory' in performance 952 | ? switchToMB(performance['memory']['usedJSHeapSize']) 953 | : 0, 954 | } 955 | setStore(PerformanceType.MRY, value) 956 | } 957 | 958 | // 获取网络环境信息 959 | function getNavConnection(setStore) { 960 | if (!isNavigator()) { 961 | throw new Error('浏览器不支持Navigator') 962 | } else { 963 | var connection = 'connection' in navigator ? navigator['connection'] : {} 964 | var value = { 965 | downlink: connection.downlink, 966 | effectiveType: connection.effectiveType, 967 | rtt: connection.rtt, 968 | } 969 | setStore(PerformanceType.NC, value) 970 | } 971 | } 972 | 973 | function getDevices(setStore) { 974 | if (!window.location) { 975 | throw new Error('浏览器不支持location') 976 | } 977 | var host = location.host, 978 | hostname = location.hostname, 979 | href = location.href, 980 | protocol = location.protocol, 981 | origin = location.origin, 982 | port = location.port, 983 | pathname = location.pathname, 984 | search = location.search, 985 | hash = location.hash 986 | var _a = window.screen, 987 | width = _a.width, 988 | height = _a.height 989 | var info = { 990 | host: host, 991 | hostname: hostname, 992 | href: href, 993 | protocol: protocol, 994 | origin: origin, 995 | port: port, 996 | pathname: pathname, 997 | search: search, 998 | hash: hash, 999 | userAgent: 'userAgent' in navigator ? navigator.userAgent : '', 1000 | screenResolution: ''.concat(width, '*').concat(height), 1001 | } 1002 | setStore(PerformanceType.DICE, info) 1003 | } 1004 | 1005 | function getWhiteScreen(setStore, options) { 1006 | var isSkeletonScreen = options.isSkeletonScreen 1007 | var pooCount = 0 1008 | var startSampLists = [] 1009 | var nowSampLists = [] 1010 | var containerLists = ['html', 'body', '#app', '#root'] 1011 | var timer = null 1012 | if (options.isSkeletonScreen) { 1013 | if (document.readyState != 'complete') { 1014 | onSamp() 1015 | } 1016 | } else { 1017 | if (document.readyState === 'complete') { 1018 | onSamp() 1019 | } else { 1020 | window.addEventListener('load', onSamp) 1021 | } 1022 | } 1023 | function getSelector(element) { 1024 | if (element.id) { 1025 | return '#' + element.id 1026 | } else if (element.className) { 1027 | return ( 1028 | '.' + 1029 | element.className 1030 | .split(' ') 1031 | .filter(function (item) { 1032 | return !!item 1033 | }) 1034 | .join('.') 1035 | ) 1036 | } else { 1037 | return element.nodeName.toLowerCase() 1038 | } 1039 | } 1040 | function isContainer(element) { 1041 | var selector = getSelector(element) 1042 | if (isSkeletonScreen) { 1043 | pooCount ? nowSampLists.push(selector) : startSampLists.push(selector) 1044 | } 1045 | return containerLists === null || containerLists === void 0 1046 | ? void 0 1047 | : containerLists.includes(selector) 1048 | } 1049 | // 采样对比 1050 | function onSamp() { 1051 | var points = 0 1052 | for (var i = 1; i <= 9; i++) { 1053 | var xElements = document.elementsFromPoint( 1054 | (window.innerWidth * i) / 10, 1055 | window.innerHeight / 2 1056 | ) 1057 | var yElements = document.elementsFromPoint( 1058 | window.innerWidth / 2, 1059 | (window.innerHeight * i) / 10 1060 | ) 1061 | if (isContainer(xElements[0])) points++ 1062 | //避免中心点计算多次 1063 | if (i != 5) { 1064 | if (isContainer(yElements[0])) points++ 1065 | } 1066 | } 1067 | console.log('ds', points) 1068 | if (points != 17) { 1069 | if (isSkeletonScreen) { 1070 | if (!pooCount) return onLoop() 1071 | if (nowSampLists.join() == startSampLists.join()) 1072 | setStore(PerformanceType.WHITE, { isWhite: true }) 1073 | return 1074 | } 1075 | if (timer) { 1076 | clearTimeout(timer) 1077 | timer = null 1078 | } 1079 | } else { 1080 | if (!timer) { 1081 | onLoop() 1082 | } 1083 | } 1084 | setStore(PerformanceType.WHITE, { isWhite: points == 17 ? true : false }) 1085 | } 1086 | //白屏轮训检测 1087 | function onLoop() { 1088 | if (timer) return 1089 | timer = setInterval(function () { 1090 | if (isSkeletonScreen) { 1091 | pooCount++ 1092 | nowSampLists = [] 1093 | } 1094 | onSamp() 1095 | }, 1000) 1096 | } 1097 | } 1098 | 1099 | var Performance = /** @class */ (function () { 1100 | function Performance(options) { 1101 | this.newStore = new Store() 1102 | this.reportInfo = new ReportInfo(options) 1103 | this.init(options) 1104 | } 1105 | Performance.prototype.init = function (options) { 1106 | getFP(this.setStore.bind(this)) 1107 | getLCP(this.setStore.bind(this)) 1108 | getCLS(this.setStore.bind(this)) 1109 | getFID(this.setStore.bind(this)) 1110 | getNavConnection(this.setStore.bind(this)) 1111 | getNavTiming(this.setStore.bind(this)) 1112 | getFSP(this.setStore.bind(this)) 1113 | getMemory(this.setStore.bind(this)) 1114 | getDevices(this.setStore.bind(this)) 1115 | getWhiteScreen(this.setStore.bind(this), options) 1116 | this.report() 1117 | } 1118 | Performance.prototype.report = function () { 1119 | var _this = this 1120 | ;[onLoaded, onHidden, beforeUnload].forEach(function (event) { 1121 | event(function () { 1122 | var storeData = _this.newStore.getValues() 1123 | Object.keys(storeData).forEach(function (key) { 1124 | _this.reportInfo.send(storeData[key]) 1125 | }) 1126 | _this.newStore.clear() 1127 | }) 1128 | }) 1129 | } 1130 | Performance.prototype.setStore = function (secondType, value) { 1131 | var data = { 1132 | type: MonitorType.PERFORMANCE, 1133 | secondType: secondType, 1134 | level: Level.INFO, 1135 | time: getNowTime(), 1136 | value: value, 1137 | } 1138 | this.newStore.set(secondType, data) 1139 | } 1140 | return Performance 1141 | })() 1142 | 1143 | var Monitor = /** @class */ (function () { 1144 | function Monitor(options) { 1145 | this.init(options) 1146 | } 1147 | Monitor.prototype.init = function (options) { 1148 | if (!this.isSetCondition(options)) return 1149 | this.setDefault(options) 1150 | if (options.isCollectPer) { 1151 | new Performance(options) 1152 | } 1153 | if (options.isCollectErr) { 1154 | new Error$1(options) 1155 | } 1156 | } 1157 | Monitor.prototype.isSetCondition = function (options) { 1158 | if (!options.url) { 1159 | console.error('上报url必传') 1160 | return false 1161 | } 1162 | if (!options.project) { 1163 | console.error('项目project必传') 1164 | return false 1165 | } 1166 | if (!options.version) { 1167 | console.error('项目版本号必传') 1168 | return false 1169 | } 1170 | if (options.isVue && !options.vue) { 1171 | console.log('如果isVue为true时,请在vue字段上传入Vue') 1172 | return false 1173 | } 1174 | return true 1175 | } 1176 | Monitor.prototype.setDefault = function (options) { 1177 | Object.keys(defaultOptions).forEach(function (key) { 1178 | if (options[key] == null) { 1179 | options[key] = defaultOptions[key] 1180 | } 1181 | }) 1182 | } 1183 | return Monitor 1184 | })() 1185 | 1186 | export { Monitor as default } 1187 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/common/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './reportInfo'; 2 | export * from './queue'; 3 | export * from './store'; 4 | export * from './observer'; 5 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/common/observer.d.ts: -------------------------------------------------------------------------------- 1 | type handler = { 2 | (entry: PerformanceEntry): void; 3 | }; 4 | export declare function observe(type: string, handler: handler): PerformanceObserver | undefined; 5 | export {}; 6 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/common/queue.d.ts: -------------------------------------------------------------------------------- 1 | type Fn = () => void 2 | declare class Queue { 3 | pending: boolean 4 | callbacks: Fn[] 5 | constructor() 6 | push(fn: Fn): void 7 | flushCallbacks(): void 8 | getCallbacks(): Fn[] 9 | clear(): void 10 | } 11 | export { Queue } 12 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/common/reportInfo.d.ts: -------------------------------------------------------------------------------- 1 | import { PerReportData, ErrorReportData, CommonData, InitOptions } from '../types'; 2 | import { Queue } from './queue'; 3 | type Data = PerReportData | ErrorReportData; 4 | type SendData = CommonData & Data; 5 | declare class ReportInfo { 6 | queue: Queue; 7 | options: InitOptions; 8 | sendWay: string; 9 | constructor(options: InitOptions); 10 | beforeSend(data: Data): SendData; 11 | send(data: Data, isImmediate?: boolean): void; 12 | useImg(data: Data): () => void; 13 | useAjax(data: Data): () => void; 14 | useBeacon(data: Data): () => void; 15 | } 16 | export { ReportInfo }; 17 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/common/store.d.ts: -------------------------------------------------------------------------------- 1 | import { PerformanceReportData, PerformanceType, StoreData } from '../types'; 2 | declare class Store { 3 | store: Map; 4 | constructor(); 5 | get(key: PerformanceType): PerformanceReportData; 6 | set(key: PerformanceType, val: PerformanceReportData): void; 7 | clear(): void; 8 | getValues(): StoreData; 9 | } 10 | export { Store }; 11 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/config/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare const defaultOptions: { 2 | url: string; 3 | sendWay: string; 4 | isCollectErr: boolean; 5 | isCollectPer: boolean; 6 | isCollectBehavior: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/behavior/getPageDuration.d.ts: -------------------------------------------------------------------------------- 1 | import { Report } from '../../types' 2 | export declare function getPageDuration(report: Report): void 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/behavior/getPv.d.ts: -------------------------------------------------------------------------------- 1 | import { Report } from '../../types' 2 | export declare function getPv(report: Report): void 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/behavior/getVueJump.d.ts: -------------------------------------------------------------------------------- 1 | import { Report } from '../../types' 2 | export declare function getVueJump(report: Report, router: any): void 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/behavior/index.d.ts: -------------------------------------------------------------------------------- 1 | import { InitOptions, ReportValue, BehaviorType } from '../../types' 2 | import { ReportInfo } from '../../common' 3 | declare class Behivor { 4 | reportInfo: ReportInfo 5 | constructor(options: InitOptions) 6 | init(options: InitOptions): void 7 | report(secondType: BehaviorType, value: ReportValue): void 8 | } 9 | export default Behivor 10 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/error/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { InitOptions, ReportValue } from '../../types' 2 | import { ErrorType } from '../../types' 3 | import { ReportInfo } from '../../common' 4 | declare class Error { 5 | reportInfo: ReportInfo 6 | constructor(options: InitOptions) 7 | init(options: InitOptions): void 8 | report(secondType: ErrorType, value: ReportValue): void 9 | handleJS(): void 10 | handleVue(Vue: any): void 11 | handleAajxError(): void 12 | } 13 | export default Error 14 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { InitOptions } from '../types'; 2 | declare class Monitor { 3 | constructor(options: InitOptions); 4 | init(options: InitOptions): void; 5 | isSetCondition(options: InitOptions): boolean; 6 | setDefault(options: InitOptions): void; 7 | } 8 | export default Monitor; 9 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getCLS.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function getCLS(setStore: SetStore): void; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getDevices.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types' 2 | export declare function getDevices(setStore: SetStore): void 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getFID.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function getFID(setStore: SetStore): void; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getFP.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function getFP(setStore: SetStore): void; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getFSP.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function getFSP(setStore: SetStore): void; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getLCP.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function isLCPDone(): boolean; 3 | export declare function getLCP(setStore: SetStore): void; 4 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getMemory.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function getMemory(setStore: SetStore): void; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getNavConnection.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function getNavConnection(setStore: SetStore): void; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getNavTiming.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore } from '../../types'; 2 | export declare function getNavTiming(setStore: SetStore): void; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/getWhiteScreen.d.ts: -------------------------------------------------------------------------------- 1 | import { SetStore, InitOptions } from '../../types' 2 | export declare function getWhiteScreen( 3 | setStore: SetStore, 4 | options: InitOptions 5 | ): void 6 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/core/performance/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Store, ReportInfo } from '../../common' 2 | import { InitOptions, PerformanceType, ReportValue } from '../../types' 3 | declare class Performance { 4 | newStore: InstanceType 5 | reportInfo: ReportInfo 6 | constructor(options: InitOptions) 7 | init(options: InitOptions): void 8 | report(): void 9 | setStore(secondType: PerformanceType, value: ReportValue): void 10 | } 11 | export default Performance 12 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import monitor from './core'; 2 | export default monitor; 3 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/types/behavior.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseReportData, ReportValue } from './common' 2 | export declare enum BehaviorType { 3 | PV = 'pv', 4 | VJ = 'vue-jump', 5 | PD = 'page-duration', 6 | } 7 | export interface BehaviorReportData extends BaseReportData { 8 | value?: ReportValue 9 | } 10 | export interface Report { 11 | (secondType: BehaviorType, value: ReportValue): void 12 | } 13 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/types/common.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum MonitorType { 2 | ERROR = "error", 3 | PERFORMANCE = "performance", 4 | BEHAVIOR = "behavior" 5 | } 6 | export declare enum Level { 7 | ERROR = "error", 8 | WARN = "warn", 9 | INFO = "info" 10 | } 11 | export interface BaseReportData { 12 | type: MonitorType; 13 | secondType?: string; 14 | time: number; 15 | level?: Level; 16 | } 17 | export interface ObjAnyAttr { 18 | [key: string]: any; 19 | } 20 | export type ReportValue = string | number | ObjAnyAttr; 21 | export interface CommonData { 22 | project: string; 23 | projectSub: string; 24 | referer: string; 25 | } 26 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/types/error.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseReportData, ReportValue } from './common'; 2 | export declare enum ErrorType { 3 | JS = "js_error", 4 | RESOURCE = "resource_error", 5 | VUE = "vue_error", 6 | PROMISE = "promise_error", 7 | AJAX = "ajax_error" 8 | } 9 | export interface JsEventTarget { 10 | src?: string; 11 | href?: string; 12 | tagName?: string; 13 | } 14 | export interface AjaxEventTarget { 15 | status?: number; 16 | response?: string; 17 | statusText?: string; 18 | responseURL?: string; 19 | } 20 | export interface ErrorReportData extends BaseReportData { 21 | value?: ReportValue; 22 | } 23 | export interface RejectReason { 24 | message: string; 25 | stack: string; 26 | } 27 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './error'; 3 | export * from './option'; 4 | export * from './vue'; 5 | export * from './performance'; 6 | export * from './behavior'; 7 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/types/option.d.ts: -------------------------------------------------------------------------------- 1 | import { VueInstance } from './vue' 2 | export interface InitOptions { 3 | url: string 4 | project: string 5 | version: string | number 6 | proSub?: string 7 | isCollectErr?: boolean 8 | isCollectPer?: boolean 9 | isCollectBehavior?: boolean 10 | sendWay?: 'sendBeacon' | 'img' | 'ajax' 11 | isVue?: boolean 12 | vue?: VueInstance 13 | isVueJump: boolean 14 | router?: any 15 | isSkeletonScreen: boolean 16 | } 17 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/types/performance.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseReportData, ReportValue } from './common' 2 | export declare enum PerformanceType { 3 | FP = 'first-paint', 4 | FCP = 'first-contentful-paint', 5 | LCP = 'largest-contentful-paint', 6 | CLS = 'layout-shift', 7 | FID = 'first-input', 8 | FSP = 'first-screen-paint', 9 | NC = 'nav-connecttion', 10 | NAV = 'navigation', 11 | MRY = 'memory', 12 | DICE = 'devices', 13 | WHITE = 'white-screen', 14 | } 15 | export interface PerformanceReportData extends BaseReportData { 16 | value?: ReportValue 17 | } 18 | export interface StoreData { 19 | [prop: string]: PerformanceReportData 20 | } 21 | export interface LayoutShift extends PerformanceEntry { 22 | value: number 23 | hadRecentInput: boolean 24 | } 25 | export interface SetStore { 26 | (secondType: PerformanceType, value: ReportValue): void 27 | } 28 | export interface NodeItem extends Node { 29 | tagName?: string 30 | } 31 | export interface SourceItem extends PerformanceEntry { 32 | initiatorType?: string 33 | fetchStart?: number 34 | responseEnd?: number 35 | } 36 | export interface NavConnection { 37 | downlink?: number 38 | effectiveType?: string 39 | rtt?: number 40 | } 41 | export interface DevicesInfo { 42 | host: string 43 | hostname: string 44 | href: string 45 | protocol: string 46 | origin: string 47 | port: string 48 | pathname: string 49 | search: string 50 | hash: string 51 | userAgent?: string 52 | screenResolution: string 53 | } 54 | export interface PerReportData extends BaseReportData { 55 | value?: ReportValue 56 | } 57 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/types/vue.d.ts: -------------------------------------------------------------------------------- 1 | import { ObjAnyAttr } from './common'; 2 | export interface VueInstance { 3 | config?: VueConfiguration; 4 | mixin(hooks: { 5 | [key: string]: () => void; 6 | }): void; 7 | util: { 8 | warn(...input: any): void; 9 | }; 10 | version: string; 11 | } 12 | export interface VueConfiguration { 13 | silent: boolean; 14 | errorHandler(error: Error, vm: ViewModel, info: string): void; 15 | warnHandler(msg: string, vm: ViewModel, trace: string): void; 16 | ignoredElements: (string | RegExp)[]; 17 | keyCodes: { 18 | [key: string]: number | number[]; 19 | }; 20 | async: boolean; 21 | } 22 | export interface ViewModel { 23 | [key: string]: any; 24 | $root: Record; 25 | $options: { 26 | [key: string]: any; 27 | name?: string; 28 | propsData?: ObjAnyAttr; 29 | _componentTag?: string; 30 | __file?: string; 31 | props?: ObjAnyAttr; 32 | }; 33 | $props: Record; 34 | } 35 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/utils/event.d.ts: -------------------------------------------------------------------------------- 1 | export declare function onBeforeunload(callback: any): void; 2 | export declare const onLoaded: (callback: any) => void; 3 | export declare function onHidden(callback: any, once?: boolean): void; 4 | export declare const beforeUnload: (callback: any) => void; 5 | export declare const unload: (callback: any) => void; 6 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/utils/helper.d.ts: -------------------------------------------------------------------------------- 1 | export declare function formatParams(obj: any): string; 2 | export declare function getLines(stack: string): string; 3 | export declare function getNowTime(): number; 4 | export declare function switchToMB(bytes: number): number | null; 5 | export declare function generateId(): string; 6 | export declare function getUid(): string; 7 | export declare function getIdentity(): string; 8 | export declare function getReferer(): string; 9 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/utils/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './helper'; 2 | export * from './window'; 3 | export * from './event'; 4 | -------------------------------------------------------------------------------- /packages/monitor/lib/types/utils/window.d.ts: -------------------------------------------------------------------------------- 1 | export declare const isPerformance: () => boolean; 2 | export declare const isPerformanceObserver: () => boolean; 3 | export declare const isNavigator: () => boolean; 4 | export declare const isInScreen: (dom: any) => boolean; 5 | export declare const isIncludeEle: (node: any, arr: any) => any; 6 | export declare const getUrl: () => string; 7 | -------------------------------------------------------------------------------- /packages/monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apply-monitor/monitor", 3 | "version": "0.0.0", 4 | "description": "监控工具", 5 | "main": "lib/index.cjs.js", 6 | "module": "lib/index.esm.js", 7 | "browser": "lib/index.umd.js", 8 | "typings": "lib/types/index.d.ts", 9 | "type": "module", 10 | "scripts": { 11 | "build": "rollup -c ./scripts/build.js ", 12 | "build:watch": "rollup -w -c ./scripts/build.js" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "registry": "https://registry.npmjs.org/" 17 | }, 18 | "author": "lycarrot", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@rollup/plugin-commonjs": "^24.0.1", 22 | "@rollup/plugin-json": "^6.0.0", 23 | "@rollup/plugin-node-resolve": "^15.0.1", 24 | "@rollup/plugin-typescript": "^11.0.0", 25 | "rollup": "^3.11.0", 26 | "tslib": "^2.5.0", 27 | "typescript": "^4.9.4", 28 | "uuid": "^9.0.0" 29 | }, 30 | "keywords": [ 31 | "monitor" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/lycarrot/apply-monitor/tree/main/packages/monitor" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/monitor/scripts/build.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import typescript from '@rollup/plugin-typescript' 4 | import eslint from '@rollup/plugin-eslint' 5 | import json from '@rollup/plugin-json' 6 | import { readFileSync } from 'fs' 7 | 8 | const pkg = JSON.parse(readFileSync('./package.json', 'utf8')) 9 | 10 | export default [ 11 | { 12 | input: './src/index.ts', 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', 17 | }, 18 | { 19 | file: pkg.module, 20 | format: 'es', 21 | }, 22 | { 23 | file: pkg.browser, 24 | name: 'Monitor', 25 | format: 'umd', 26 | }, 27 | ], 28 | plugins: [resolve(), commonjs(), json(), typescript(), eslint()], 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /packages/monitor/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reportInfo' 2 | export * from './queue' 3 | export * from './store' 4 | export * from './observer' 5 | -------------------------------------------------------------------------------- /packages/monitor/src/common/observer.ts: -------------------------------------------------------------------------------- 1 | type handler = { 2 | (entry: PerformanceEntry): void 3 | } 4 | 5 | export function observe( 6 | type: string, 7 | handler: handler 8 | ): PerformanceObserver | undefined { 9 | if (PerformanceObserver.supportedEntryTypes.includes(type)) { 10 | const ob: PerformanceObserver = new PerformanceObserver((item) => 11 | item.getEntries().map(handler) 12 | ) 13 | ob.observe({ type: type, buffered: true }) 14 | return ob 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/monitor/src/common/queue.ts: -------------------------------------------------------------------------------- 1 | type Fn = () => void 2 | class Queue { 3 | pending: boolean 4 | callbacks: Fn[] 5 | constructor() { 6 | this.pending = false 7 | this.callbacks = [] 8 | } 9 | push(fn: Fn): void { 10 | if (typeof fn !== 'function') return 11 | this.callbacks.push(fn) 12 | if (!this.pending) { 13 | this.pending = true 14 | Promise.resolve().then(() => { 15 | this.flushCallbacks() 16 | }) 17 | } 18 | } 19 | flushCallbacks(): void { 20 | this.pending = false 21 | const copies = this.callbacks.slice() 22 | this.callbacks.length = 0 23 | for (let i = 0; i < copies.length; i++) { 24 | copies[i]() 25 | } 26 | } 27 | getCallbacks() { 28 | return this.callbacks 29 | } 30 | clear() { 31 | this.callbacks = [] 32 | } 33 | } 34 | 35 | export { Queue } 36 | -------------------------------------------------------------------------------- /packages/monitor/src/common/reportInfo.ts: -------------------------------------------------------------------------------- 1 | import { formatParams, getReferer, getIdentity } from '../utils' 2 | import { 3 | PerReportData, 4 | ErrorReportData, 5 | CommonData, 6 | InitOptions, 7 | } from '../types' 8 | import { Queue } from './queue' 9 | 10 | type Data = PerReportData | ErrorReportData 11 | type SendData = CommonData & Data 12 | 13 | class ReportInfo { 14 | queue: Queue 15 | options: InitOptions 16 | sendWay: string 17 | 18 | constructor(options: InitOptions) { 19 | this.options = options 20 | this.sendWay = options.sendWay 21 | this.queue = new Queue() 22 | } 23 | beforeSend(data: Data): SendData { 24 | const commonInfo = { 25 | project: this.options.project, 26 | version: this.options.version, 27 | projectSub: this.options.proSub, 28 | referer: getReferer(), 29 | identity: getIdentity(), 30 | } 31 | return Object.assign(data, commonInfo) 32 | } 33 | send(data: Data, isImmediate?: boolean): void { 34 | const sendData = this.beforeSend(data) 35 | const fn = 36 | this.sendWay == 'sendBeacon' 37 | ? this.useBeacon(sendData) 38 | : this.sendWay == 'img' 39 | ? this.useImg(sendData) 40 | : this.useAjax(sendData) 41 | isImmediate ? fn() : this.queue.push(fn) 42 | } 43 | useImg(data: Data): () => void { 44 | const fn = () => { 45 | let img = new Image() 46 | const spliceStr = this.options.url.indexOf('?') === -1 ? '?' : '&' 47 | img.src = `${this.options.url}${spliceStr}${formatParams(data)}` 48 | img = null 49 | } 50 | return fn 51 | } 52 | useAjax(data: Data): () => void { 53 | const fn = () => { 54 | const xhr = new XMLHttpRequest() 55 | xhr.open('POST', this.options.url) 56 | xhr.withCredentials 57 | xhr.setRequestHeader('Content-Type', 'application/json') 58 | xhr.send(JSON.stringify(data)) 59 | xhr.onreadystatechange = function () { 60 | console.log(xhr.readyState, xhr.status) 61 | } 62 | } 63 | return fn 64 | } 65 | useBeacon(data: Data): () => void { 66 | if (navigator.sendBeacon) { 67 | const fn = () => { 68 | navigator.sendBeacon(this.options.url, JSON.stringify(data)) 69 | } 70 | return fn 71 | } else { 72 | return this.useAjax(data) 73 | } 74 | } 75 | } 76 | 77 | export { ReportInfo } 78 | -------------------------------------------------------------------------------- /packages/monitor/src/common/store.ts: -------------------------------------------------------------------------------- 1 | import { PerformanceReportData, PerformanceType, StoreData } from '../types' 2 | class Store { 3 | store: Map 4 | constructor() { 5 | this.store = new Map() 6 | } 7 | get(key: PerformanceType): PerformanceReportData { 8 | return this.store.get(key) 9 | } 10 | set(key: PerformanceType, val: PerformanceReportData) { 11 | this.store.set(key, val) 12 | } 13 | clear() { 14 | this.store.clear() 15 | } 16 | getValues(): StoreData { 17 | return Array.from(this.store).reduce((obj, [key, value]) => { 18 | obj[key] = value 19 | return obj 20 | }, {}) 21 | } 22 | } 23 | 24 | export { Store } 25 | -------------------------------------------------------------------------------- /packages/monitor/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const defaultOptions = { 2 | url: '//127.0.0.1:8000/api/collect/info/detail', 3 | sendWay: 'sendBeacon', 4 | isCollectErr: true, 5 | isCollectPer: true, 6 | isCollectBehavior: true, 7 | } 8 | -------------------------------------------------------------------------------- /packages/monitor/src/core/behavior/getPageDuration.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeunload, getUrl } from '../../utils' 2 | import { Report, BehaviorType } from '../../types' 3 | export function getPageDuration(report: Report) { 4 | const start = performance.now() 5 | onBeforeunload(() => { 6 | report(BehaviorType.PD, { 7 | pageURL: getUrl(), 8 | duraion: performance.now() - start, 9 | }) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/monitor/src/core/behavior/getPv.ts: -------------------------------------------------------------------------------- 1 | import { getUid, onLoaded, getUrl } from '../../utils' 2 | import { Report, BehaviorType } from '../../types' 3 | export function getPv(report: Report) { 4 | onLoaded(() => { 5 | report(BehaviorType.PV, { 6 | uuid: getUid(), 7 | pageURL: getUrl(), 8 | referrer: document.referrer, 9 | }) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/monitor/src/core/behavior/getVueJump.ts: -------------------------------------------------------------------------------- 1 | import { Report, BehaviorType } from '../../types' 2 | type Value = { 3 | from: string 4 | to: string 5 | name: string 6 | } 7 | export function getVueJump(report: Report, router) { 8 | router.beforeEach((to, from, next) => { 9 | if (!from) { 10 | next() 11 | return 12 | } 13 | const value: Value = { 14 | from: from.fullPath, 15 | to: to.fullPath, 16 | name: to.name || to.path, 17 | } 18 | report(BehaviorType.VJ, value) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /packages/monitor/src/core/behavior/index.ts: -------------------------------------------------------------------------------- 1 | import { getPv } from './getPv' 2 | import { getVueJump } from './getVueJump' 3 | import { 4 | InitOptions, 5 | ReportValue, 6 | MonitorType, 7 | BehaviorType, 8 | Level, 9 | } from '../../types' 10 | import { ReportInfo } from '../../common' 11 | import { getNowTime } from '../../utils' 12 | 13 | class Behivor { 14 | reportInfo: ReportInfo 15 | constructor(options: InitOptions) { 16 | this.init(options) 17 | this.reportInfo = new ReportInfo(options) 18 | } 19 | init(options: InitOptions) { 20 | getPv(this.report) 21 | if (options.isVueJump) { 22 | getVueJump(this.report, options.router) 23 | } 24 | } 25 | report(secondType: BehaviorType, value: ReportValue) { 26 | this.reportInfo.send({ 27 | type: MonitorType.BEHAVIOR, 28 | secondType: secondType, 29 | level: Level.INFO, 30 | time: getNowTime(), 31 | value: value, 32 | }) 33 | } 34 | } 35 | 36 | export default Behivor 37 | -------------------------------------------------------------------------------- /packages/monitor/src/core/error/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | InitOptions, 3 | JsEventTarget, 4 | AjaxEventTarget, 5 | // VueInstance, 6 | // ViewModel, 7 | RejectReason, 8 | ReportValue, 9 | } from '../../types' 10 | import { ErrorType, MonitorType, Level } from '../../types' 11 | import { ReportInfo } from '../../common' 12 | import { getLines, getNowTime } from '../../utils' 13 | class Error { 14 | reportInfo: ReportInfo 15 | constructor(options: InitOptions) { 16 | this.init(options) 17 | this.reportInfo = new ReportInfo(options) 18 | } 19 | init(options: InitOptions) { 20 | this.handleJS() 21 | this.handleAajxError() 22 | if (options.isVue) { 23 | this.handleVue(options.vue) 24 | } 25 | } 26 | report(secondType: ErrorType, value: ReportValue) { 27 | console.log('value', value) 28 | this.reportInfo.send( 29 | { 30 | type: MonitorType.ERROR, 31 | secondType: secondType, 32 | level: Level.ERROR, 33 | time: getNowTime(), 34 | value: value, 35 | }, 36 | true 37 | ) 38 | } 39 | // js错误监控 40 | handleJS() { 41 | window.addEventListener( 42 | 'error', 43 | (event: ErrorEvent) => { 44 | const target = event.target as JsEventTarget 45 | if (target && (target.src || target.href)) { 46 | this.report(ErrorType.RESOURCE, { 47 | message: '资源加载异常了', 48 | filename: target.src || target.href, 49 | tagName: target.tagName, 50 | }) 51 | } else { 52 | const { message, filename, lineno, colno } = event 53 | this.report(ErrorType.JS, { 54 | message: message, 55 | filename: filename, 56 | row: lineno, 57 | col: colno, 58 | stack: event.error && getLines(event.error.stack), 59 | }) 60 | } 61 | }, 62 | true 63 | ) 64 | // promise错误捕捉 65 | window.addEventListener( 66 | 'unhandledrejection', 67 | (event: PromiseRejectionEvent) => { 68 | let message: string 69 | let filename: string 70 | let line: number | string = 0 71 | let column: number | string = 0 72 | let stack = '' 73 | const reason: string | RejectReason = event.reason 74 | if (typeof reason === 'string') { 75 | message = reason 76 | } else if (typeof reason === 'object') { 77 | message = reason.message 78 | if (reason.stack) { 79 | const matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/) 80 | filename = matchResult[1] 81 | line = matchResult[2] 82 | column = matchResult[3] 83 | } 84 | stack = getLines(reason.stack) 85 | } 86 | this.report(ErrorType.PROMISE, { 87 | message, 88 | filename, 89 | row: line, 90 | col: column, 91 | stack, 92 | }) 93 | }, 94 | true 95 | ) 96 | } 97 | // vue错误监控; 98 | handleVue(Vue) { 99 | Vue.config.errorHandler = (error, vm, info) => { 100 | let componentName: string 101 | if (Object.prototype.toString.call(vm) === '[object Object]') { 102 | componentName = vm._isVue 103 | ? vm.$options.name || vm.$options._componentTag 104 | : vm.name 105 | } 106 | const value: ReportValue = { 107 | message: error.message, 108 | info: info, 109 | componentName: componentName, 110 | stack: error.stack, 111 | } 112 | // 匹配到代码报错出现位置 113 | const reg = /.js\:(\d+)\:(\d+)/i 114 | const codePos = error.stack.match(reg) 115 | if (codePos.length) { 116 | value.row = parseInt(codePos[1]) 117 | value.col = parseInt(codePos[2]) 118 | } 119 | this.report(ErrorType.VUE, value) 120 | } 121 | } 122 | //ajax请求错误 123 | handleAajxError() { 124 | if (!window.XMLHttpRequest) { 125 | return 126 | } 127 | const xhrSend = XMLHttpRequest.prototype.send 128 | XMLHttpRequest.prototype.send = function (...args): void { 129 | if (this.addEventListener) { 130 | this.addEventListener('error', _handleEvent) 131 | this.addEventListener('load', _handleEvent) 132 | this.addEventListener('abort', _handleEvent) 133 | } else { 134 | const tempStateChange = this.onreadystatechange 135 | this.onreadystatechange = function (event: Event) { 136 | tempStateChange.apply(this, args) 137 | if (this.readyState === 4) { 138 | _handleEvent(event) 139 | } 140 | } 141 | } 142 | return xhrSend.apply(this, args) 143 | } 144 | 145 | const _handleEvent = (event: Event) => { 146 | try { 147 | if (!event) return 148 | const target = event.currentTarget as AjaxEventTarget 149 | if (target && target.status !== 200) { 150 | this.report(ErrorType.AJAX, { 151 | message: target.response, 152 | status: target.status, 153 | statusText: target.statusText, 154 | url: target.responseURL, 155 | }) 156 | } 157 | } catch (error) { 158 | console.log(error) 159 | } 160 | } 161 | } 162 | } 163 | 164 | export default Error 165 | -------------------------------------------------------------------------------- /packages/monitor/src/core/index.ts: -------------------------------------------------------------------------------- 1 | import type { InitOptions } from '../types' 2 | import { defaultOptions } from '../config' 3 | import Error from './error' 4 | import Performance from './performance' 5 | 6 | class Monitor { 7 | constructor(options: InitOptions) { 8 | this.init(options) 9 | } 10 | init(options: InitOptions): void { 11 | if (!this.isSetCondition(options)) return 12 | this.setDefault(options) 13 | if (options.isCollectPer) { 14 | new Performance(options) 15 | } 16 | if (options.isCollectErr) { 17 | new Error(options) 18 | } 19 | } 20 | isSetCondition(options: InitOptions) { 21 | if (!options.url) { 22 | console.error('上报url必传') 23 | return false 24 | } 25 | if (!options.project) { 26 | console.error('项目project必传') 27 | return false 28 | } 29 | if (!options.version) { 30 | console.error('项目版本号必传') 31 | return false 32 | } 33 | if (options.isVue && !options.vue) { 34 | console.log('如果isVue为true时,请在vue字段上传入Vue') 35 | return false 36 | } 37 | return true 38 | } 39 | setDefault(options: InitOptions) { 40 | Object.keys(defaultOptions).forEach((key: string) => { 41 | if (options[key] == null) { 42 | options[key] = defaultOptions[key] 43 | } 44 | }) 45 | } 46 | } 47 | 48 | export default Monitor 49 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getCLS.ts: -------------------------------------------------------------------------------- 1 | import { isPerformanceObserver, onHidden } from '../../utils' 2 | import { observe } from '../../common' 3 | import { PerformanceType, LayoutShift, SetStore } from '../../types' 4 | 5 | // layout-shift 从页面加载开始和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数 6 | 7 | let value = 0 8 | 9 | export function getCLS(setStore: SetStore) { 10 | if (!isPerformanceObserver()) { 11 | throw new Error('浏览器不支持PerformanceObserver') 12 | } else { 13 | const entryHandler = (entry: LayoutShift) => { 14 | if (!entry.hadRecentInput) { 15 | value += entry.value 16 | } 17 | } 18 | 19 | const ob: PerformanceObserver = observe(PerformanceType.CLS, entryHandler) 20 | 21 | const stopListening = () => { 22 | if (ob?.takeRecords) { 23 | ob.takeRecords().map((entry: LayoutShift) => { 24 | if (!entry.hadRecentInput) { 25 | value += entry.value 26 | } 27 | }) 28 | } 29 | ob?.disconnect() 30 | 31 | setStore(PerformanceType.CLS, value.toFixed(2)) 32 | } 33 | 34 | onHidden(stopListening, true) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getDevices.ts: -------------------------------------------------------------------------------- 1 | import { PerformanceType, SetStore, DevicesInfo } from '../../types' 2 | 3 | export function getDevices(setStore: SetStore) { 4 | if (!window.location) { 5 | throw new Error('浏览器不支持location') 6 | } 7 | const { 8 | host, 9 | hostname, 10 | href, 11 | protocol, 12 | origin, 13 | port, 14 | pathname, 15 | search, 16 | hash, 17 | } = location 18 | const { width, height } = window.screen 19 | const info: DevicesInfo = { 20 | host, 21 | hostname, 22 | href, 23 | protocol, 24 | origin, 25 | port, 26 | pathname, 27 | search, 28 | hash, 29 | userAgent: 'userAgent' in navigator ? navigator.userAgent : '', 30 | screenResolution: `${width}*${height}`, 31 | } 32 | setStore(PerformanceType.DICE, info) 33 | } 34 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getFID.ts: -------------------------------------------------------------------------------- 1 | import { isPerformanceObserver } from '../../utils' 2 | import { observe } from '../../common' 3 | import { PerformanceType, SetStore } from '../../types' 4 | // first-input 测量用户首次与您的站点交互时的时间(即,当他们单击链接,点击按钮或使用自定义的JavaScript驱动控件时)到浏览器实际能够的时间回应这种互动。 5 | 6 | export function getFID(setStore: SetStore) { 7 | if (!isPerformanceObserver()) { 8 | throw new Error('浏览器不支持PerformanceObserver') 9 | } else { 10 | const entryHandler = (entry: PerformanceEventTiming) => { 11 | if (ob) { 12 | ob.disconnect() 13 | } 14 | setStore(PerformanceType.FID, { 15 | value: entry.startTime.toFixed(2), 16 | event: entry.name, 17 | }) 18 | } 19 | const ob: PerformanceObserver = observe(PerformanceType.FID, entryHandler) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getFP.ts: -------------------------------------------------------------------------------- 1 | import { isPerformanceObserver, isPerformance, onLoaded } from '../../utils' 2 | import { observe } from '../../common' 3 | import { PerformanceType, SetStore } from '../../types' 4 | 5 | // first-paint 从页面加载开始到第一个像素绘制到屏幕上的时间 6 | // first-contentful-paint 从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间 7 | 8 | function getEntriesByFP(setStore: SetStore) { 9 | const entryFP: PerformanceEntry = 10 | performance.getEntriesByName('first-paint')[0] 11 | const entryFCP: PerformanceEntry = performance.getEntriesByName( 12 | 'first-contentful-paint' 13 | )[0] 14 | setStore(PerformanceType.FP, entryFP.startTime.toFixed(2)) 15 | setStore(PerformanceType.FCP, entryFCP.startTime.toFixed(2)) 16 | } 17 | 18 | export function getFP(setStore: SetStore) { 19 | if (!isPerformanceObserver()) { 20 | if (!isPerformance()) { 21 | throw new Error('浏览器不支持performance') 22 | } else { 23 | onLoaded(() => { 24 | getEntriesByFP(setStore) 25 | }) 26 | } 27 | } else { 28 | const entryHandler = (entry: PerformanceEntry): void => { 29 | if (ob) { 30 | ob.disconnect() 31 | } 32 | setStore(PerformanceType.FP, entry.startTime.toFixed(2)) 33 | } 34 | const ob: PerformanceObserver = observe('paint', entryHandler) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getFSP.ts: -------------------------------------------------------------------------------- 1 | import { onLoaded, isIncludeEle, isInScreen } from '../../utils' 2 | import { PerformanceType, NodeItem, SourceItem, SetStore } from '../../types' 3 | import { isLCPDone } from './getLCP' 4 | 5 | //first-screen-paint 首屏渲染时间 6 | 7 | let entries: { 8 | startTime: number 9 | children: NodeItem[] 10 | }[] = [] 11 | 12 | let isOnLoaded = false 13 | 14 | onLoaded(() => { 15 | isOnLoaded = true 16 | }) 17 | 18 | let timer 19 | function checkDOMChange(setStore: SetStore) { 20 | clearTimeout(timer) 21 | timer = setTimeout(() => { 22 | // 等 load、lcp 事件触发后并且 DOM 树不再变化时,计算首屏渲染时间 23 | if (isOnLoaded && isLCPDone()) { 24 | setStore(PerformanceType.FSP, getRenderTime()) 25 | entries = null 26 | } else { 27 | checkDOMChange(setStore) 28 | } 29 | }, 500) 30 | } 31 | 32 | function getRenderTime() { 33 | let startTime = 0 34 | entries.forEach((entry) => { 35 | for (const node of entry.children) { 36 | if ( 37 | isInScreen(node) && 38 | entry.startTime > startTime && 39 | needToCalculate(node) 40 | ) { 41 | startTime = entry.startTime 42 | break 43 | } 44 | } 45 | }) 46 | 47 | // 需要和当前页面所有加载图片的时间做对比,取最大值 48 | // 图片请求时间要小于 startTime,响应结束时间要大于 startTime 49 | performance.getEntriesByType('resource').forEach((item: SourceItem) => { 50 | if ( 51 | item.initiatorType === 'img' && 52 | item.fetchStart < startTime && 53 | item.responseEnd > startTime 54 | ) { 55 | startTime = item.responseEnd 56 | } 57 | }) 58 | 59 | return startTime 60 | } 61 | 62 | function needToCalculate(node) { 63 | // 隐藏的元素不用计算 64 | if (window.getComputedStyle(node).display === 'none') return false 65 | 66 | // 用于统计的图片不用计算 67 | if (node.tagName === 'IMG' && node.width < 2 && node.height < 2) { 68 | return false 69 | } 70 | 71 | return true 72 | } 73 | 74 | export function getFSP(setStore: SetStore) { 75 | if (!MutationObserver) { 76 | throw new Error('浏览器不支持MutationObserver') 77 | } 78 | 79 | const next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout 80 | const ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK', 'META'] 81 | const ob = new MutationObserver((mutationList) => { 82 | checkDOMChange(setStore) 83 | next(() => { 84 | entry.startTime = performance.now() 85 | }) 86 | const entry = { 87 | startTime: 0, 88 | children: [], 89 | } 90 | for (const mutation of mutationList) { 91 | if (mutation.addedNodes.length) { 92 | const nodeLists: NodeItem[] = Array.from( 93 | mutation.addedNodes as NodeList 94 | ) 95 | for (const node of nodeLists) { 96 | if ( 97 | node.nodeType === 1 && 98 | !ignoreDOMList.includes(node?.tagName) && 99 | !isIncludeEle(node, entry.children) 100 | ) { 101 | entry.children.push(node) 102 | } 103 | } 104 | } 105 | } 106 | if (entry.children.length) { 107 | entries.push(entry) 108 | } 109 | }) 110 | 111 | ob.observe(document, { 112 | childList: true, 113 | subtree: true, 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getLCP.ts: -------------------------------------------------------------------------------- 1 | import { isPerformanceObserver } from '../../utils' 2 | import { observe } from '../../common' 3 | import { PerformanceType, SetStore } from '../../types' 4 | 5 | // largest-contentful-paint 从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间 6 | 7 | let lcpDone = false 8 | export function isLCPDone() { 9 | return lcpDone 10 | } 11 | 12 | export function getLCP(setStore: SetStore) { 13 | if (!isPerformanceObserver()) { 14 | lcpDone = true 15 | throw new Error('浏览器不支持PerformanceObserver') 16 | } else { 17 | const entryHandler = (entry: PerformanceEntry): void => { 18 | lcpDone = true 19 | if (ob) { 20 | ob.disconnect() 21 | } 22 | setStore(PerformanceType.LCP, entry.startTime.toFixed(2)) 23 | } 24 | const ob: PerformanceObserver = observe( 25 | 'largest-contentful-paint', 26 | entryHandler 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getMemory.ts: -------------------------------------------------------------------------------- 1 | import { isPerformance, isNavigator, switchToMB } from '../../utils' 2 | import { PerformanceType, SetStore } from '../../types' 3 | 4 | // 获取内存占用空间 5 | 6 | export function getMemory(setStore: SetStore) { 7 | if (!isPerformance()) { 8 | throw new Error('浏览器不支持Performance') 9 | } 10 | if (!isNavigator()) { 11 | throw new Error('浏览器不支持Navigator') 12 | } 13 | const value = { 14 | deviceMemory: 'deviceMemory' in navigator ? navigator['deviceMemory'] : 0, 15 | hardwareConcurrency: 16 | 'hardwareConcurrency' in navigator ? navigator['hardwareConcurrency'] : 0, 17 | // 内存大小限制 18 | jsHeapSizeLimit: 19 | 'memory' in performance 20 | ? switchToMB(performance['memory']['jsHeapSizeLimit']) 21 | : 0, 22 | // 可使用的内存大小 23 | totalJSHeapSize: 24 | 'memory' in performance 25 | ? switchToMB(performance['memory']['totalJSHeapSize']) 26 | : 0, 27 | // JS 对象占用的内存数 28 | usedJSHeapSize: 29 | 'memory' in performance 30 | ? switchToMB(performance['memory']['usedJSHeapSize']) 31 | : 0, 32 | } 33 | setStore(PerformanceType.MRY, value) 34 | } 35 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getNavConnection.ts: -------------------------------------------------------------------------------- 1 | import { isNavigator } from '../../utils' 2 | import { PerformanceType, NavConnection, SetStore } from '../../types' 3 | 4 | // 获取网络环境信息 5 | 6 | export function getNavConnection(setStore: SetStore) { 7 | if (!isNavigator()) { 8 | throw new Error('浏览器不支持Navigator') 9 | } else { 10 | const connection: NavConnection = 11 | 'connection' in navigator ? navigator['connection'] : {} 12 | const value = { 13 | downlink: connection.downlink, 14 | effectiveType: connection.effectiveType, 15 | rtt: connection.rtt, 16 | } 17 | setStore(PerformanceType.NC, value) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getNavTiming.ts: -------------------------------------------------------------------------------- 1 | import { isPerformanceObserver, isPerformance, onLoaded } from '../../utils' 2 | import { observe } from '../../common' 3 | import { PerformanceType, ObjAnyAttr, SetStore } from '../../types' 4 | 5 | // navigation 可以获取到用户访问一个页面的每个阶段的精确时间 6 | function setPerformanceData( 7 | setStore: SetStore, 8 | entry: PerformanceNavigationTiming 9 | ) { 10 | const { 11 | domainLookupStart, 12 | domainLookupEnd, 13 | connectStart, 14 | connectEnd, 15 | secureConnectionStart, 16 | requestStart, 17 | responseStart, 18 | responseEnd, 19 | domInteractive, 20 | domContentLoadedEventStart, 21 | domContentLoadedEventEnd, 22 | loadEventStart, 23 | fetchStart, 24 | } = entry 25 | const timing: ObjAnyAttr = { 26 | // DNS解析时间 27 | dnsLookup: (domainLookupEnd - domainLookupStart).toFixed(2), 28 | // TCP完成握手时间 29 | initialConnection: (connectEnd - connectStart).toFixed(2), 30 | // ssl连接时间 31 | ssl: secureConnectionStart 32 | ? (connectEnd - secureConnectionStart).toFixed(2) 33 | : 0, 34 | // HTTP请求响应完成时间 35 | ttfb: (responseStart - requestStart).toFixed(2), 36 | // 读取页面第一个字节的时间 37 | contentDownload: (responseEnd - responseStart).toFixed(2), 38 | // dom解析时间 39 | domParse: (domInteractive - responseEnd).toFixed(2), 40 | // DOM 准备就绪时间 41 | deferExecuteDuration: (domContentLoadedEventStart - domInteractive).toFixed( 42 | 2 43 | ), 44 | // 脚本加载时间 45 | domContentLoadedCallback: ( 46 | domContentLoadedEventEnd - domContentLoadedEventStart 47 | ).toFixed(2), 48 | // onload事件时间 49 | resourceLoad: (loadEventStart - domContentLoadedEventEnd).toFixed(2), 50 | // DOM阶段渲染耗时 51 | domReady: (domContentLoadedEventEnd - fetchStart).toFixed(2), 52 | } 53 | 54 | setStore(PerformanceType.NAV, timing) 55 | } 56 | 57 | function getPerformanceentryTim(): PerformanceNavigationTiming { 58 | const entryTim = ( 59 | performance.getEntriesByType('navigation').length > 0 60 | ? performance.getEntriesByType('navigation')[0] 61 | : performance.timing 62 | ) as PerformanceNavigationTiming 63 | return entryTim 64 | } 65 | 66 | export function getNavTiming(setStore: SetStore) { 67 | if (!isPerformanceObserver()) { 68 | if (!isPerformance()) { 69 | throw new Error('浏览器不支持performance') 70 | } else { 71 | onLoaded(() => { 72 | setPerformanceData(setStore, getPerformanceentryTim()) 73 | }) 74 | } 75 | } else { 76 | const entryHandler = (entry: PerformanceNavigationTiming): void => { 77 | if (ob) { 78 | ob.disconnect() 79 | } 80 | setPerformanceData(setStore, entry) 81 | } 82 | const ob: PerformanceObserver = observe(PerformanceType.NAV, entryHandler) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/getWhiteScreen.ts: -------------------------------------------------------------------------------- 1 | import { SetStore, InitOptions, PerformanceType } from '../../types' 2 | export function getWhiteScreen(setStore: SetStore, options: InitOptions) { 3 | const isSkeletonScreen = options.isSkeletonScreen 4 | let pooCount = 0 5 | const startSampLists: string[] = [] 6 | let nowSampLists: string[] = [] 7 | const containerLists = ['html', 'body', '#app', '#root'] 8 | let timer = null 9 | if (options.isSkeletonScreen) { 10 | if (document.readyState != 'complete') { 11 | onSamp() 12 | } 13 | } else { 14 | if (document.readyState === 'complete') { 15 | onSamp() 16 | } else { 17 | window.addEventListener('load', onSamp) 18 | } 19 | } 20 | 21 | function getSelector(element: any) { 22 | if (element.id) { 23 | return '#' + element.id 24 | } else if (element.className) { 25 | return ( 26 | '.' + 27 | element.className 28 | .split(' ') 29 | .filter((item: any) => !!item) 30 | .join('.') 31 | ) 32 | } else { 33 | return element.nodeName.toLowerCase() 34 | } 35 | } 36 | 37 | function isContainer(element: HTMLElement) { 38 | const selector = getSelector(element) 39 | if (isSkeletonScreen) { 40 | pooCount ? nowSampLists.push(selector) : startSampLists.push(selector) 41 | } 42 | return containerLists?.includes(selector) 43 | } 44 | // 采样对比 45 | function onSamp() { 46 | let points = 0 47 | for (let i = 1; i <= 9; i++) { 48 | const xElements = document.elementsFromPoint( 49 | (window.innerWidth * i) / 10, 50 | window.innerHeight / 2 51 | ) 52 | const yElements = document.elementsFromPoint( 53 | window.innerWidth / 2, 54 | (window.innerHeight * i) / 10 55 | ) 56 | if (isContainer(xElements[0] as HTMLElement)) points++ 57 | //避免中心点计算多次 58 | if (i != 5) { 59 | if (isContainer(yElements[0] as HTMLElement)) points++ 60 | } 61 | } 62 | if (points != 17) { 63 | if (isSkeletonScreen) { 64 | if (!pooCount) return onLoop() 65 | if (nowSampLists.join() == startSampLists.join()) 66 | setStore(PerformanceType.WHITE, { isWhite: true }) 67 | return 68 | } 69 | if (timer) { 70 | clearTimeout(timer) 71 | timer = null 72 | } 73 | } else { 74 | if (!timer) { 75 | onLoop() 76 | } 77 | } 78 | setStore(PerformanceType.WHITE, { isWhite: points == 17 ? true : false }) 79 | } 80 | //白屏轮训检测 81 | function onLoop(): void { 82 | if (timer) return 83 | timer = setInterval(() => { 84 | if (isSkeletonScreen) { 85 | pooCount++ 86 | nowSampLists = [] 87 | } 88 | onSamp() 89 | }, 1000) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/monitor/src/core/performance/index.ts: -------------------------------------------------------------------------------- 1 | import { getFP } from './getFP' 2 | import { getLCP } from './getLCP' 3 | import { getCLS } from './getCLS' 4 | import { getFID } from './getFID' 5 | import { getFSP } from './getFSP' 6 | import { getNavTiming } from './getNavTiming' 7 | import { getMemory } from './getMemory' 8 | import { getNavConnection } from './getNavConnection' 9 | import { getDevices } from './getDevices' 10 | import { getWhiteScreen } from './getWhiteScreen' 11 | 12 | import { Store, ReportInfo } from '../../common' 13 | import { onLoaded, onHidden, beforeUnload, getNowTime } from '../../utils' 14 | import { 15 | InitOptions, 16 | PerformanceReportData, 17 | MonitorType, 18 | Level, 19 | PerformanceType, 20 | ReportValue, 21 | } from '../../types' 22 | 23 | class Performance { 24 | newStore: InstanceType 25 | reportInfo: ReportInfo 26 | constructor(options: InitOptions) { 27 | this.newStore = new Store() 28 | this.reportInfo = new ReportInfo(options) 29 | this.init(options) 30 | } 31 | init(options: InitOptions) { 32 | getFP(this.setStore.bind(this)) 33 | getLCP(this.setStore.bind(this)) 34 | getCLS(this.setStore.bind(this)) 35 | getFID(this.setStore.bind(this)) 36 | getNavConnection(this.setStore.bind(this)) 37 | getNavTiming(this.setStore.bind(this)) 38 | getFSP(this.setStore.bind(this)) 39 | getMemory(this.setStore.bind(this)) 40 | getDevices(this.setStore.bind(this)) 41 | getWhiteScreen(this.setStore.bind(this), options) 42 | this.report() 43 | } 44 | report() { 45 | ;[onLoaded, onHidden, beforeUnload].forEach((event) => { 46 | event(() => { 47 | const storeData = this.newStore.getValues() 48 | Object.keys(storeData).forEach((key) => { 49 | this.reportInfo.send(storeData[key]) 50 | }) 51 | this.newStore.clear() 52 | }) 53 | }) 54 | } 55 | setStore(secondType: PerformanceType, value: ReportValue) { 56 | const data: PerformanceReportData = { 57 | type: MonitorType.PERFORMANCE, 58 | secondType: secondType, 59 | level: Level.INFO, 60 | time: getNowTime(), 61 | value: value, 62 | } 63 | this.newStore.set(secondType, data) 64 | } 65 | } 66 | 67 | export default Performance 68 | -------------------------------------------------------------------------------- /packages/monitor/src/index.ts: -------------------------------------------------------------------------------- 1 | import monitor from './core' 2 | export default monitor 3 | -------------------------------------------------------------------------------- /packages/monitor/src/types/behavior.ts: -------------------------------------------------------------------------------- 1 | import { BaseReportData, ReportValue } from './common' 2 | export enum BehaviorType { 3 | PV = 'pv', 4 | VJ = 'vue-jump', 5 | PD = 'page-duration', 6 | } 7 | 8 | export interface BehaviorReportData extends BaseReportData { 9 | value?: ReportValue 10 | } 11 | 12 | export interface Report { 13 | (secondType: BehaviorType, value: ReportValue): void 14 | } 15 | -------------------------------------------------------------------------------- /packages/monitor/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export enum MonitorType { 2 | ERROR = 'error', 3 | PERFORMANCE = 'performance', 4 | BEHAVIOR = 'behavior', 5 | } 6 | 7 | export enum Level { 8 | ERROR = 'error', 9 | WARN = 'warn', 10 | INFO = 'info', 11 | } 12 | 13 | export interface BaseReportData { 14 | type: MonitorType 15 | secondType?: string 16 | time: number 17 | level?: Level 18 | } 19 | 20 | export interface ObjAnyAttr { 21 | [key: string]: any 22 | } 23 | 24 | export type ReportValue = string | number | ObjAnyAttr 25 | 26 | export interface CommonData { 27 | project: string 28 | projectSub: string 29 | referer: string 30 | } 31 | -------------------------------------------------------------------------------- /packages/monitor/src/types/error.ts: -------------------------------------------------------------------------------- 1 | import { BaseReportData, ReportValue } from './common' 2 | export enum ErrorType { 3 | JS = 'js_error', 4 | RESOURCE = 'resource_error', 5 | VUE = 'vue_error', 6 | PROMISE = 'promise_error', 7 | AJAX = 'ajax_error', 8 | } 9 | 10 | export interface JsEventTarget { 11 | src?: string 12 | href?: string 13 | tagName?: string 14 | } 15 | 16 | export interface AjaxEventTarget { 17 | status?: number 18 | response?: string 19 | statusText?: string 20 | responseURL?: string 21 | } 22 | 23 | export interface ErrorReportData extends BaseReportData { 24 | value?: ReportValue 25 | } 26 | 27 | export interface RejectReason { 28 | message: string 29 | stack: string 30 | } 31 | -------------------------------------------------------------------------------- /packages/monitor/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | export * from './error' 3 | export * from './option' 4 | export * from './vue' 5 | export * from './performance' 6 | export * from './behavior' 7 | -------------------------------------------------------------------------------- /packages/monitor/src/types/option.ts: -------------------------------------------------------------------------------- 1 | import { VueInstance } from './vue' 2 | export interface InitOptions { 3 | // 上传url 4 | url: string 5 | //项目名称 6 | project: string 7 | version: string | number 8 | //子项目名称 9 | proSub?: string 10 | // 是否收集错误 11 | isCollectErr?: boolean 12 | // 是否收集性能 13 | isCollectPer?: boolean 14 | // 是否收集用户行为 15 | isCollectBehavior?: boolean 16 | // 上传方式 17 | sendWay?: 'sendBeacon' | 'img' | 'ajax' 18 | isVue?: boolean 19 | // 如果是vue类型需要传入Vue; 20 | vue?: VueInstance 21 | // 监听vue路由跳转 22 | isVueJump: boolean 23 | router?: any 24 | //是否有骨架屏 25 | isSkeletonScreen: boolean 26 | } 27 | -------------------------------------------------------------------------------- /packages/monitor/src/types/performance.ts: -------------------------------------------------------------------------------- 1 | import { BaseReportData, ReportValue } from './common' 2 | export enum PerformanceType { 3 | FP = 'first-paint', 4 | FCP = 'first-contentful-paint', 5 | LCP = 'largest-contentful-paint', 6 | CLS = 'layout-shift', 7 | FID = 'first-input', 8 | FSP = 'first-screen-paint', 9 | NC = 'nav-connecttion', 10 | NAV = 'navigation', 11 | MRY = 'memory', 12 | DICE = 'devices', 13 | WHITE = 'white-screen', 14 | } 15 | 16 | export interface PerformanceReportData extends BaseReportData { 17 | value?: ReportValue 18 | } 19 | 20 | export interface StoreData { 21 | [prop: string]: PerformanceReportData 22 | } 23 | 24 | export interface LayoutShift extends PerformanceEntry { 25 | value: number 26 | hadRecentInput: boolean 27 | } 28 | 29 | export interface SetStore { 30 | (secondType: PerformanceType, value: ReportValue): void 31 | } 32 | 33 | export interface NodeItem extends Node { 34 | tagName?: string 35 | } 36 | 37 | export interface SourceItem extends PerformanceEntry { 38 | initiatorType?: string 39 | fetchStart?: number 40 | responseEnd?: number 41 | } 42 | 43 | export interface NavConnection { 44 | downlink?: number 45 | effectiveType?: string 46 | rtt?: number 47 | } 48 | 49 | export interface DevicesInfo { 50 | host: string 51 | hostname: string 52 | href: string 53 | protocol: string 54 | origin: string 55 | port: string 56 | pathname: string 57 | search: string 58 | hash: string 59 | userAgent?: string 60 | screenResolution: string 61 | } 62 | 63 | export interface PerReportData extends BaseReportData { 64 | value?: ReportValue 65 | } 66 | -------------------------------------------------------------------------------- /packages/monitor/src/types/vue.ts: -------------------------------------------------------------------------------- 1 | import { ObjAnyAttr } from './common' 2 | export interface VueInstance { 3 | config?: VueConfiguration 4 | mixin(hooks: { [key: string]: () => void }): void 5 | util: { 6 | warn(...input: any): void 7 | } 8 | version: string 9 | } 10 | export interface VueConfiguration { 11 | silent: boolean 12 | errorHandler(error: Error, vm: ViewModel, info: string): void 13 | warnHandler(msg: string, vm: ViewModel, trace: string): void 14 | ignoredElements: (string | RegExp)[] 15 | keyCodes: { [key: string]: number | number[] } 16 | async: boolean 17 | } 18 | export interface ViewModel { 19 | [key: string]: any 20 | $root: Record 21 | $options: { 22 | [key: string]: any 23 | name?: string 24 | // vue2.6 25 | propsData?: ObjAnyAttr 26 | _componentTag?: string 27 | __file?: string 28 | props?: ObjAnyAttr 29 | } 30 | $props: Record 31 | } 32 | -------------------------------------------------------------------------------- /packages/monitor/src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export function onBeforeunload(callback): void { 2 | window.addEventListener('beforeunload', callback, true) 3 | } 4 | // 页面加载完成 5 | export const onLoaded = (callback): void => { 6 | if (document.readyState === 'complete') { 7 | setTimeout(callback) 8 | } else { 9 | addEventListener('pageshow', callback) 10 | } 11 | } 12 | // 页面隐藏 13 | export function onHidden(callback, once?: boolean): void { 14 | const onHiddenOrPageHide = (event: Event) => { 15 | if (event.type === 'pagehide' || document.visibilityState === 'hidden') { 16 | callback(event) 17 | if (once) { 18 | window.removeEventListener('visibilitychange', onHiddenOrPageHide, true) 19 | window.removeEventListener('pagehide', onHiddenOrPageHide, true) 20 | } 21 | } 22 | } 23 | 24 | window.addEventListener('visibilitychange', onHiddenOrPageHide, true) 25 | window.addEventListener('pagehide', onHiddenOrPageHide, true) 26 | } 27 | 28 | // 页面卸载(关闭)或刷新时调用 29 | export const beforeUnload = (callback): void => { 30 | window.addEventListener('beforeunload', callback) 31 | } 32 | 33 | // 页面卸载(关闭)或刷新时调用 34 | export const unload = (callback): void => { 35 | window.addEventListener('unload', callback) 36 | } 37 | -------------------------------------------------------------------------------- /packages/monitor/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | 3 | export function formatParams(obj: any): string { 4 | const strArr = [] 5 | Object.keys(obj).forEach((key) => { 6 | strArr.push(`${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`) 7 | }) 8 | return strArr.join('&') 9 | } 10 | 11 | export function getLines(stack: string): string { 12 | return stack 13 | .split('\n') 14 | .slice(1) 15 | .map((item) => item.replace(/^\s+at\s+/g, '')) 16 | .join('^') 17 | } 18 | 19 | export function getNowTime(): number { 20 | return Date.now() 21 | } 22 | 23 | export function switchToMB(bytes: number): number | null { 24 | if (typeof bytes !== 'number') { 25 | return null 26 | } 27 | return parseFloat((bytes / Math.pow(1024, 2)).toFixed(2)) 28 | } 29 | 30 | export function generateId(): string { 31 | return uuidv4() 32 | } 33 | 34 | export function getUid(): string { 35 | const key = 'uuid' 36 | let uuid = localStorage.getItem(key) 37 | if (uuid) return uuid 38 | uuid = generateId() 39 | localStorage.setItem(key, uuid) 40 | return uuid 41 | } 42 | 43 | export function getIdentity(): string { 44 | const key = 'identity' 45 | let identity = sessionStorage.getItem(key) 46 | if (!identity) { 47 | // 生成标识 48 | identity = generateId() 49 | sessionStorage.setItem(key, identity) 50 | } 51 | return identity 52 | } 53 | 54 | export function getReferer(): string { 55 | if (typeof document === 'undefined' || document.location == null) return '' 56 | return document.location.href 57 | } 58 | -------------------------------------------------------------------------------- /packages/monitor/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helper' 2 | export * from './window' 3 | export * from './event' 4 | -------------------------------------------------------------------------------- /packages/monitor/src/utils/window.ts: -------------------------------------------------------------------------------- 1 | export const isPerformance = (): boolean => { 2 | return ( 3 | !!window.performance && 4 | !!window.performance.getEntriesByType && 5 | !!window.performance.mark 6 | ) 7 | } 8 | 9 | export const isPerformanceObserver = (): boolean => { 10 | return !!window.PerformanceObserver 11 | } 12 | 13 | export const isNavigator = (): boolean => { 14 | return !!window.navigator 15 | } 16 | 17 | // dom 对象是否在屏幕内 18 | export const isInScreen = (dom) => { 19 | const viewportWidth = window.innerWidth 20 | const viewportHeight = window.innerHeight 21 | const rectInfo = dom.getBoundingClientRect() 22 | if ( 23 | rectInfo.left >= 0 && 24 | rectInfo.left < viewportWidth && 25 | rectInfo.top >= 0 && 26 | rectInfo.top < viewportHeight 27 | ) { 28 | return true 29 | } 30 | } 31 | 32 | export const isIncludeEle = function (node, arr) { 33 | if (!node || node === document.documentElement) { 34 | return false 35 | } 36 | 37 | if (arr.includes(node)) { 38 | return true 39 | } 40 | 41 | return isIncludeEle(node.parentElement, arr) 42 | } 43 | 44 | export const getUrl = (): string => { 45 | return window.location.href 46 | } 47 | -------------------------------------------------------------------------------- /packages/monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "lib": [ 6 | "es2015", 7 | "es2016", 8 | "es2017", 9 | "dom" 10 | ], 11 | "downlevelIteration": true, 12 | "declaration": true, 13 | "declarationDir": "types", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "moduleResolution": "node", 18 | "esModuleInterop": true 19 | }, 20 | "include": [ 21 | "src" 22 | ] 23 | } -------------------------------------------------------------------------------- /packages/report/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode -------------------------------------------------------------------------------- /packages/report/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @apply-monitor/report 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - 版本内容更新 8 | 9 | ## 0.0.2 10 | 11 | ### Patch Changes 12 | 13 | - 14 | -------------------------------------------------------------------------------- /packages/report/README.md: -------------------------------------------------------------------------------- 1 | ## apply-monitor/report 2 | 3 | ### 安装方式 4 | 5 | 使用 npm: 6 | 7 | ``` 8 | npm install @apply-monitor/report --save-dev 9 | ``` 10 | 11 | 使用 yarn: 12 | 13 | ``` 14 | yarn add @apply-monitor/report --dev 15 | ``` 16 | 17 | ### 使用方式 18 | 19 | ```js 20 | // webpack.config.js 21 | import ApplyMonitorReport from '@apply-monitor/report' 22 | 23 | const webpackConfig = { 24 | plugins: [ 25 | new ApplyMonitorReport({ 26 | url: 'sourcemap上报地址', 27 | project: '项目名称', 28 | version: '项目版本', 29 | }), 30 | ], 31 | } 32 | 33 | export default webpackConfig 34 | ``` 35 | 36 | ### 配置项 37 | 38 | | Name | Description | tyep | default | isRequired | 39 | | ----------- | ------------------------------------- | ------- | -------- | ---------- | ----- | 40 | | url | 上传 sourmap 的 url | string | '' | true | 41 | | project | 项目名称,必须和 sdk 配置项目名称一致 | string | '' | true | 42 | | version | 项目版本,必须和 sdk 配置版本名称一致 | string | '' | true | 43 | | include | 上传包含的文件 | RegExp | /\.js$ | \.map$/ | false | 44 | | exclude | 上传不包含的文件 | RegExp | null | false | 45 | | afterDelMap | 上传完文件后是否删除 | boolean | false | false | 46 | | delInclude | 上传方式 | RegExp | /\.map$/ | false | 47 | -------------------------------------------------------------------------------- /packages/report/lib/types/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | url: string; 3 | project: string; 4 | version: string | number; 5 | include?: RegExp; 6 | exclude?: RegExp; 7 | afterDelMap?: boolean; 8 | delInclude?: RegExp; 9 | } 10 | interface FileLists { 11 | name: string; 12 | filePath: string; 13 | } 14 | declare class Report { 15 | options: Options; 16 | constructor(options: Options); 17 | apply(compiler: any): void; 18 | isRequiredCondition(): Error | null; 19 | getFiles(compilation: any): FileLists[]; 20 | isIncludeOrExclude(filename: any): boolean; 21 | getAssetPath(compilation: any, name: any): string; 22 | uploadFiles(files: any): PromiseLike; 23 | uploadFile({ filePath, name }: { 24 | filePath: any; 25 | name: any; 26 | }): Promise; 27 | deleteFiles(stats: any): Promise; 28 | } 29 | export default Report; 30 | -------------------------------------------------------------------------------- /packages/report/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apply-monitor/report", 3 | "version": "1.0.0", 4 | "description": "webpack插件:sourcemap上报", 5 | "main": "lib/index.cjs.js", 6 | "module": "lib/index.esm.js", 7 | "typings": "lib/types/index.d.ts", 8 | "scripts": { 9 | "build": "rollup -c ./scripts/build.mjs ", 10 | "build:watch": "rollup -w -c ./scripts/build.mjs" 11 | }, 12 | "publishConfig": { 13 | "access": "public", 14 | "registry": "https://registry.npmjs.org/" 15 | }, 16 | "author": "lycarrot", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@rollup/plugin-commonjs": "^24.0.1", 20 | "@rollup/plugin-eslint": "^9.0.3", 21 | "@rollup/plugin-json": "^6.0.0", 22 | "@rollup/plugin-node-resolve": "^15.0.1", 23 | "@rollup/plugin-typescript": "^11.0.0", 24 | "@types/node": "^18.14.4", 25 | "es6-promise-pool": "^2.5.0", 26 | "prettier": "^2.8.3", 27 | "request-promise": "^4.2.1", 28 | "rollup": "^3.11.0", 29 | "tslib": "^2.5.0", 30 | "typescript": "^4.9.4" 31 | }, 32 | "keywords": [], 33 | "dependencies": { 34 | "@apply-monitor/eslint-config": "*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/report/rollup: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycarrot/apply-monitor/00573248f6a1acb6577864283c8a2da4ed74842f/packages/report/rollup -------------------------------------------------------------------------------- /packages/report/scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import eslint from "@rollup/plugin-eslint"; 5 | import json from "@rollup/plugin-json"; 6 | import { readFileSync } from "fs"; 7 | 8 | const pkg = JSON.parse(readFileSync("./package.json", "utf8")); 9 | 10 | export default [ 11 | { 12 | input: "./src/index.ts", 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: "cjs", 17 | }, 18 | { 19 | file: pkg.module, 20 | format: "es", 21 | }, 22 | ], 23 | plugins: [resolve(), commonjs(), json(), typescript(), eslint()], 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /packages/report/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import request from 'request-promise' 4 | import PromisePool from 'es6-promise-pool' 5 | 6 | interface Options { 7 | url: string 8 | project: string 9 | version: string | number 10 | include?: RegExp 11 | exclude?: RegExp 12 | afterDelMap?: boolean 13 | delInclude?: RegExp 14 | } 15 | interface FileLists { 16 | name: string 17 | filePath: string 18 | } 19 | const DEFAULT_INCLUDE = /\.js$|\.map$/ 20 | const DEFAULT_DELETE = /\.map$/ 21 | 22 | class Report { 23 | options: Options 24 | constructor(options: Options) { 25 | this.options = options 26 | this.options.include = this.options.include || DEFAULT_INCLUDE 27 | this.options.delInclude = this.options.delInclude || DEFAULT_DELETE 28 | } 29 | apply(compiler) { 30 | this.isRequiredCondition() 31 | // afterEmit在生成文件到output目录之后执行 32 | compiler.hooks.afterEmit.tapAsync( 33 | 'report', 34 | async (compilation, callback) => { 35 | const files = this.getFiles(compilation) 36 | await this.uploadFiles(files) 37 | callback(null) 38 | } 39 | ) 40 | compiler.hooks.done.tapPromise('report', async (stats) => { 41 | if (this.options.afterDelMap) { 42 | await this.deleteFiles(stats) 43 | } 44 | }) 45 | } 46 | isRequiredCondition(): Error | null { 47 | if (!this.options.url) { 48 | return new Error('上传sourcemap的url必填') 49 | } else if (!this.options.project) { 50 | return new Error('项目名称必填') 51 | } else if (!this.options.version) { 52 | return new Error('项目source版本必填') 53 | } else { 54 | return null 55 | } 56 | } 57 | // 获取上传的文件名称和路径 58 | getFiles(compilation): FileLists[] { 59 | return Object.keys(compilation.assets) 60 | .map((name: string) => { 61 | if (this.isIncludeOrExclude(name)) { 62 | return { name, filePath: this.getAssetPath(compilation, name) } 63 | } 64 | return null 65 | }) 66 | .filter((i) => i) 67 | } 68 | // 判断包含和排除上传文件 69 | isIncludeOrExclude(filename): boolean { 70 | const isIncluded: boolean = this.options.include 71 | ? this.options.include.test(filename) 72 | : true 73 | const isExcluded: boolean = this.options.exclude 74 | ? this.options.exclude.test(filename) 75 | : false 76 | return isIncluded && !isExcluded 77 | } 78 | // 获取文件的绝对路径 79 | getAssetPath(compilation, name) { 80 | return path.join( 81 | compilation.getPath(compilation.compiler.outputPath), 82 | name.split('?')[0] 83 | ) 84 | } 85 | uploadFiles(files) { 86 | const pool = new PromisePool(() => { 87 | const file = files.pop() 88 | if (!file) { 89 | return null 90 | } 91 | return this.uploadFile(file) 92 | }, 5) 93 | return pool.start() 94 | } 95 | // 文件上传 96 | async uploadFile({ filePath, name }) { 97 | await request({ 98 | uri: `${this.options.url}?project=${this.options.project}&version=${this.options.version}`, 99 | method: 'POST', 100 | contentType: 'multipart/form-data', 101 | formData: { 102 | file: fs.createReadStream(filePath), 103 | name: name, 104 | project: this.options.project, 105 | version: this.options.version, 106 | }, 107 | }) 108 | .then((res) => { 109 | console.log('success', res) 110 | }) 111 | .catch((err) => { 112 | console.error('err', err) 113 | }) 114 | } 115 | // 删除文件 116 | async deleteFiles(stats) { 117 | Object.keys(stats.compilation.assets) 118 | .filter((name) => this.options.delInclude.test(name)) 119 | .forEach((name) => { 120 | const filePath = this.getAssetPath(stats.compilation, name) 121 | if (filePath) { 122 | fs.unlinkSync(filePath) 123 | } else { 124 | console.warn('文件不存在或已经删除完了') 125 | } 126 | }) 127 | } 128 | } 129 | 130 | export default Report 131 | -------------------------------------------------------------------------------- /packages/report/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "lib": [ 6 | "es2015", 7 | "es2016", 8 | "es2017", 9 | "dom" 10 | ], 11 | "declaration": true, 12 | "declarationDir": "types", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "moduleResolution": "node", 17 | "esModuleInterop": true 18 | }, 19 | "include": [ 20 | "src" 21 | ] 22 | } -------------------------------------------------------------------------------- /packages/server/.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets":[ 4 | [ 5 | "@babel/preset-env" 6 | ] 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/server/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true -------------------------------------------------------------------------------- /packages/server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.15.1 as node 2 | 3 | # 指定制作我们的镜像的联系人信息 4 | LABEL CHENGTONG=luoying66 5 | 6 | RUN npm config set registry http://registry.npm.taobao.org/ 7 | RUN npm install pnpm -g 8 | 9 | # 将根目录下的文件都copy到container(运行此镜像的容器) 文件系统的app文件夹下 10 | ADD . /monitor/ 11 | 12 | # cd到app文件夹下 13 | WORKDIR /monitor 14 | 15 | # 安装项目依赖包 16 | RUN pnpm install 17 | 18 | FROM registry.cn-hangzhou.aliyuncs.com/aliyun-node/alinode 19 | 20 | COPY --from=node /monitor . 21 | 22 | EXPOSE 6666 23 | 24 | CMD ["npm", "run", "prod"] 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | ## apply-monitor/server 2 | 3 | 1.安装依赖包 4 | 5 | ``` 6 | pnpm install 7 | ``` 8 | 9 | 2.启动 10 | 11 | ``` 12 | $ pnpm run start 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/server/app/db/queueRedis.ts: -------------------------------------------------------------------------------- 1 | import kue from 'kue' 2 | import type { Job, DoneCallback } from 'kue' 3 | import config from 'config' 4 | import services from '../services' 5 | 6 | const redisConfig = config.get('kueRedis') 7 | const queue = kue.createQueue({ 8 | prefix: 'q', 9 | redis: redisConfig, 10 | }) 11 | 12 | queue.on('error', function (err) { 13 | log.trace(`redis-queue error`, err) 14 | }) 15 | 16 | queue.process('handlePerformance', (job: Job, done: DoneCallback) => { 17 | services.collect 18 | .handlePerformance(job.data.item) 19 | .then(() => done()) 20 | .catch((err: string) => { 21 | log.error(err) 22 | done(err) 23 | }) 24 | }) 25 | 26 | const taskName = 'handleError' 27 | queue.process(taskName, (job: Job, done: DoneCallback) => { 28 | log.trace(`${taskName} job begin`, job.id) 29 | services.collect 30 | .handleError(job.data.item) 31 | .then(() => { 32 | done() 33 | log.trace(`${taskName} job completed`, job.id) 34 | job.remove(function () { 35 | //手动移除 36 | log.trace(`${taskName} job removed`, job.id) 37 | }) 38 | }) 39 | .catch((err: string) => { 40 | log.error(err) 41 | done(err) 42 | }) 43 | }) 44 | 45 | export default queue 46 | -------------------------------------------------------------------------------- /packages/server/app/db/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis' 2 | import config from 'config' 3 | 4 | interface ObjectConfig { 5 | [key: string]: any 6 | } 7 | const connections: ObjectConfig = {} 8 | const redisServers = config.get('redis') as ObjectConfig 9 | 10 | Object.keys(redisServers).forEach((item) => { 11 | const redisConfig = redisServers[item] 12 | let client 13 | if (Array.isArray(redisConfig)) { 14 | client = new Redis.Cluster(redisConfig) 15 | } else { 16 | client = new Redis(redisConfig) 17 | } 18 | client.on('error', (err) => { 19 | log.error(`${item} redis error11: ${err}`) 20 | }) 21 | client.on('ready', () => { 22 | log.info(`${item} redis connected`) 23 | }) 24 | connections[item] = client 25 | }) 26 | 27 | export default connections 28 | -------------------------------------------------------------------------------- /packages/server/app/db/sequelize.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Sequelize } from 'sequelize' 4 | import config from 'config' 5 | 6 | interface MysqlConfig { 7 | database: string 8 | user: string 9 | password: string 10 | host: string 11 | port: number 12 | } 13 | 14 | const mysqlConfig: MysqlConfig = config.get('mysql') 15 | // 数据库链接 16 | const sequelize = new Sequelize( 17 | mysqlConfig.database, 18 | mysqlConfig.user, 19 | mysqlConfig.password, 20 | { 21 | host: mysqlConfig.host, 22 | port: mysqlConfig.port, 23 | dialect: 'mysql', 24 | pool: { 25 | max: 5, 26 | min: 0, 27 | idle: 10000, 28 | }, 29 | timezone: '+08:00', 30 | } 31 | ) 32 | 33 | sequelize.sync() 34 | 35 | sequelize 36 | .authenticate() 37 | .then(() => { 38 | log.info('Connection has been established successfully.') 39 | }) 40 | .catch((err) => { 41 | log.error('Unable to connect to the database:', err) 42 | }) 43 | 44 | // sequelize.sync({ force: false }) 45 | 46 | export default sequelize 47 | -------------------------------------------------------------------------------- /packages/server/app/index.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import router from './routes' 3 | import cors from 'koa2-cors' 4 | import bodyParser from 'koa-bodyparser' 5 | import middlewares from './middlewares' 6 | import helmet from 'koa-helmet' 7 | import config from 'config' 8 | import bunyan from 'bunyan' 9 | import bunyanLogger from 'koa-bunyan-logger' 10 | // import jwt from 'koa-jwt' 11 | import validate from 'koa-validate' 12 | import redis from '../app/db/redis' 13 | 14 | global.redis = redis 15 | 16 | global.log = bunyan.createLogger({ 17 | name: 'monitor-server', 18 | level: 'trace', 19 | }) 20 | 21 | const port = config.get('port') as number 22 | const host = config.get('host') as string 23 | 24 | const app = new Koa() 25 | 26 | // app.use(multiparty({uploadDir:'./temp' })); 27 | app 28 | .use(bunyanLogger()) 29 | .use(bunyanLogger.requestIdContext()) 30 | .use(bunyanLogger.requestLogger()) 31 | .use(helmet()) 32 | .use(middlewares()) 33 | .use( 34 | bodyParser({ 35 | jsonLimit: '10mb', 36 | textLimit: '10mb', 37 | enableTypes: ['json', 'form', 'text'], 38 | }) 39 | ) 40 | .use(cors()) 41 | .use(router.routes()) 42 | .use(router.allowedMethods()) 43 | 44 | validate(app) 45 | 46 | app.listen(port, host, () => { 47 | log.info(`Koa is listening in http://${host}:${port}`) 48 | }) 49 | 50 | export default app 51 | -------------------------------------------------------------------------------- /packages/server/app/middlewares/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Next } from 'koa' 2 | export default (ctx: Context, next: Next) => { 3 | return next().catch((err) => { 4 | if (err.status === 500) { 5 | ctx.status = 500 6 | ctx.body = { 7 | error: err.originalError ? err.originalError.message : err.message, 8 | } 9 | } else { 10 | throw err 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/app/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import compose from 'koa-compose' 4 | import errorHandler from './errorHandler' 5 | 6 | export default () => { 7 | return compose([errorHandler]) 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/app/models/error.ts: -------------------------------------------------------------------------------- 1 | import sequelize from '../db/sequelize' 2 | import { DataTypes } from 'sequelize' 3 | 4 | const errorModel = sequelize.define( 5 | 'error', 6 | { 7 | id: { 8 | type: DataTypes.BIGINT, 9 | field: 'id', 10 | autoIncrement: true, 11 | primaryKey: true, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | field: 'name', 16 | }, 17 | key: { 18 | type: DataTypes.STRING, 19 | field: 'key', 20 | }, 21 | occur: { 22 | type: DataTypes.INTEGER, 23 | field: 'occur', 24 | }, 25 | value: { 26 | type: DataTypes.TEXT, 27 | field: 'value', 28 | }, 29 | source: { 30 | type: DataTypes.STRING, 31 | field: 'source', 32 | }, 33 | project: { 34 | type: DataTypes.STRING, 35 | field: 'project', 36 | }, 37 | version: { 38 | type: DataTypes.STRING, 39 | field: 'version', 40 | }, 41 | projectSub: { 42 | type: DataTypes.STRING, 43 | field: 'projectSub', 44 | }, 45 | identity: { 46 | type: DataTypes.STRING, 47 | field: 'identity', 48 | }, 49 | referer: { 50 | type: DataTypes.STRING, 51 | field: 'referer', 52 | }, 53 | level: { 54 | type: DataTypes.STRING, 55 | field: 'level', 56 | }, 57 | createdAt: { 58 | type: DataTypes.DATE, 59 | field: 'createdAt', 60 | }, 61 | }, 62 | { 63 | tableName: 'error', 64 | // updatedAt:'false' 65 | } 66 | ) 67 | export type ErrorModel = typeof errorModel 68 | export default errorModel 69 | -------------------------------------------------------------------------------- /packages/server/app/models/index.ts: -------------------------------------------------------------------------------- 1 | import performance from './performance' 2 | import error from './error' 3 | 4 | const models = { 5 | performance, 6 | error, 7 | } 8 | 9 | export type Models = typeof models 10 | export default models 11 | -------------------------------------------------------------------------------- /packages/server/app/models/performance.ts: -------------------------------------------------------------------------------- 1 | import sequelize from '../db/sequelize' 2 | import { DataTypes } from 'sequelize' 3 | 4 | const performanceModel = sequelize.define( 5 | 'performance', 6 | { 7 | id: { 8 | type: DataTypes.BIGINT, 9 | field: 'id', 10 | autoIncrement: true, 11 | primaryKey: true, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | field: 'name', 16 | }, 17 | value: { 18 | type: DataTypes.STRING(1000), 19 | field: 'value', 20 | }, 21 | project: { 22 | type: DataTypes.STRING, 23 | field: 'project', 24 | }, 25 | version: { 26 | type: DataTypes.STRING, 27 | field: 'version', 28 | }, 29 | projectSub: { 30 | type: DataTypes.STRING, 31 | field: 'projectSub', 32 | }, 33 | referer: { 34 | type: DataTypes.STRING, 35 | field: 'referer', 36 | }, 37 | identity: { 38 | type: DataTypes.STRING, 39 | field: 'identity', 40 | }, 41 | level: { 42 | type: DataTypes.STRING, 43 | field: 'level', 44 | }, 45 | }, 46 | { 47 | tableName: 'performance', 48 | } 49 | ) 50 | export type PerformanceModel = typeof performanceModel 51 | export default performanceModel 52 | -------------------------------------------------------------------------------- /packages/server/app/routes/collect.ts: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router' 2 | import type { Context, Request } from 'koa' 3 | import type { ReportData, QueueItem } from '../types' 4 | import Resolve from '../utils/resolve' 5 | import queue from '../db/queueRedis' 6 | import crypto from 'crypto' 7 | 8 | const router = new Router() 9 | 10 | router.post('/info/detail', async (ctx: Context) => { 11 | const request: Request = ctx.request 12 | const data = JSON.parse(JSON.stringify(request.body as string)) as ReportData 13 | const { 14 | secondType, 15 | value, 16 | project, 17 | version, 18 | projectSub, 19 | identity, 20 | referer, 21 | level, 22 | } = data 23 | const item: QueueItem = { 24 | name: secondType, 25 | value: JSON.stringify(value), 26 | project, 27 | version, 28 | projectSub, 29 | identity, 30 | referer, 31 | level, 32 | } 33 | let queueName = '' 34 | // 性能收集 35 | if (data.type == 'performance') { 36 | queueName = 'handlePerformance' 37 | } else if (data.type == 'error') { 38 | queueName = 'handleError' 39 | // 项目名称+标签页唯一标识+页面来源+报错信息构成唯一key 40 | const valueStr = Object.keys(value) 41 | .map((key) => `${key}:${value[key]}`) 42 | .join(';') 43 | const key = crypto 44 | .createHash('md5') 45 | .update(project + projectSub + identity + referer + valueStr) 46 | .digest('hex') 47 | item.key = key 48 | // reverseSourceMap(data); 49 | } 50 | try { 51 | // 创建性能redis队列 52 | const job = queue 53 | .create(queueName, { item }) 54 | .ttl(60000) 55 | .removeOnComplete(true) 56 | job.on('failed', function (errorMessage) { 57 | log.trace(`${queueName} job faild`, errorMessage) 58 | }) 59 | job.save((err: any) => { 60 | if (err) { 61 | log.trace(`${queueName} job failed!`) 62 | } 63 | log.trace(`${queueName} job saved!`, job.id) 64 | }) 65 | ctx.body = Resolve.success() 66 | } catch (err) { 67 | ctx.body = Resolve.fail() 68 | } 69 | }) 70 | 71 | export default router 72 | -------------------------------------------------------------------------------- /packages/server/app/routes/common.ts: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router' 2 | import type { Context } from 'koa' 3 | import uploadFile from '../utils/uploadFile' 4 | import Resolve from '../utils/resolve' 5 | 6 | const router = new Router() 7 | 8 | router.post( 9 | '/upload/sourcemap', 10 | uploadFile().single('file'), 11 | async (ctx: Context) => { 12 | ctx.body = Resolve.success('上传成功') 13 | } 14 | ) 15 | 16 | export default router 17 | -------------------------------------------------------------------------------- /packages/server/app/routes/error.ts: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router' 2 | import type { Context, Request } from 'koa' 3 | import Resolve from '../utils/resolve' 4 | import services from '../services' 5 | import config from 'config' 6 | import fs from 'fs' 7 | import path from 'path' 8 | import sourceMap from 'source-map' 9 | 10 | interface Result { 11 | source: string 12 | line: number 13 | column: number 14 | } 15 | interface sourcesPathMap { 16 | [key: string]: string 17 | } 18 | const sourceMapFile = config.get('sourceMap') 19 | 20 | const fixPath = (filepath: string) => { 21 | return filepath.replace(/\.[\.\/]+/g, '') 22 | } 23 | 24 | interface ListsParams { 25 | page: number 26 | pageSize: number 27 | project: string 28 | startTime?: number 29 | endTime?: number 30 | } 31 | const reverseSourceMap = async function (data: any) { 32 | return new Promise(async (resolve, reject) => { 33 | try { 34 | const { project, version, value } = data 35 | const complPath = path.resolve(`${sourceMapFile}/${project}/${version}`) 36 | const files = fs.readdirSync(complPath) 37 | if (!files.length) { 38 | log.warn('没有找到sourcemap文件') 39 | resolve(null) 40 | return 41 | } 42 | const { stack, row, col } = JSON.parse(value) 43 | if (!row || !col) { 44 | log.warn('文件还原失败,没有相应的行和列') 45 | resolve(null) 46 | return 47 | } 48 | const mapFiles = files.filter((file: string) => file.indexOf('.map') > -1) 49 | // 找到错误出现相应的sourmap文件 50 | mapFiles.sort((a: string, b: string) => { 51 | let aIndex: number = stack.indexOf(a.slice(0, a.indexOf('.map'))) 52 | aIndex = aIndex == -1 ? Infinity : aIndex 53 | let bIndex: number = stack.indexOf(b.slice(0, b.indexOf('.map'))) 54 | bIndex = bIndex == -1 ? Infinity : bIndex 55 | return aIndex - bIndex 56 | }) 57 | const matchFile: string | null = mapFiles[0] 58 | 59 | if (matchFile) { 60 | const data = fs.readFileSync(`${complPath}/${matchFile}`) 61 | if (!data) { 62 | log.warn('没有读取到${matchFile}文件内容') 63 | resolve(null) 64 | return 65 | } 66 | const rawSourceMap = JSON.parse(data.toString()) 67 | const sources: string[] = rawSourceMap.sources 68 | const sourcesPathMap: sourcesPathMap = {} 69 | sources.forEach((item) => (sourcesPathMap[fixPath(item)] = item)) 70 | const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap) 71 | const lookup = { 72 | line: parseInt(row), 73 | column: parseInt(col), 74 | } 75 | // 找到报错代码具体位置和文件名 76 | const result = consumer.originalPositionFor(lookup) as Result 77 | const sourceFile = sourcesPathMap[result.source] 78 | // 报错代码详情 79 | if (consumer.sourcesContent) { 80 | const sourcesCode = 81 | consumer.sourcesContent[sources.indexOf(sourceFile)] 82 | resolve({ 83 | code: sourcesCode, 84 | line: result.line, 85 | column: result.column, 86 | source: result.source, 87 | }) 88 | return 89 | } 90 | } 91 | } catch (err) { 92 | reject(err) 93 | } 94 | }) 95 | } 96 | 97 | const router = new Router() 98 | 99 | router.post('/lists', async (ctx: Context) => { 100 | const request: Request = ctx.request 101 | const { page, pageSize, project, startTime, endTime } = 102 | request.body as ListsParams 103 | const { rows, count } = await services.error.getErrorLists({ 104 | page, 105 | pageSize, 106 | project, 107 | startTime, 108 | endTime, 109 | }) 110 | const lists = rows.map(async (item: any) => { 111 | const sourceRes = await reverseSourceMap(item) 112 | return { 113 | id: item.id, 114 | name: item.name, 115 | source: sourceRes, 116 | createdAt: item.createdAt, 117 | } 118 | }) 119 | const resLists = await Promise.all([...lists]) 120 | const resultLists = resLists.map((source, index) => { 121 | const list = rows[index] 122 | return { 123 | id: list.id, 124 | name: list.name, 125 | source, 126 | createdAt: list.createdAt, 127 | } 128 | }) 129 | 130 | ctx.body = Resolve.success({ 131 | lists: resultLists, 132 | count, 133 | page, 134 | pageSize, 135 | }) 136 | }) 137 | export default router 138 | -------------------------------------------------------------------------------- /packages/server/app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router' 2 | import collect from './collect' 3 | import common from './common' 4 | import error from './error' 5 | const router = new Router({ 6 | prefix: '/api', 7 | }) 8 | 9 | router.use('/collect', collect.routes(), collect.allowedMethods()) 10 | router.use('/common', common.routes(), common.allowedMethods()) 11 | router.use('/error', error.routes(), error.allowedMethods()) 12 | 13 | export default router 14 | -------------------------------------------------------------------------------- /packages/server/app/services/collect.ts: -------------------------------------------------------------------------------- 1 | import type { Models } from '../models' 2 | class Collect { 3 | models: Models 4 | constructor(models: Models) { 5 | this.models = models 6 | } 7 | async handlePerformance(data: any) { 8 | this.createPerformance(data) 9 | } 10 | async createPerformance(data: any) { 11 | return this.models.performance.create(data) 12 | } 13 | async handleError(data: any) { 14 | //是否存在监控日志 15 | const exist: any = await this.getErrorByKey(data.key) 16 | if (exist) return this.updateErrorDigit(exist.occur + 1, exist.id) 17 | data.occur = 1 18 | //创建数据 19 | let row = await this.createError(data) 20 | row = row.toJSON() 21 | return row 22 | } 23 | createError(data: any) { 24 | return this.models.error.create(data) 25 | } 26 | getErrorByKey(key: string) { 27 | return this.models.error.findOne({ 28 | raw: true, 29 | where: { 30 | key, 31 | }, 32 | }) 33 | } 34 | async updateErrorDigit(occur: number, id: number) { 35 | return this.models.error.update( 36 | { 37 | occur, 38 | }, 39 | { 40 | where: { id }, 41 | } 42 | ) 43 | } 44 | } 45 | export default Collect 46 | -------------------------------------------------------------------------------- /packages/server/app/services/error.ts: -------------------------------------------------------------------------------- 1 | import type { Models } from '../models' 2 | import { Op } from 'sequelize' 3 | 4 | class Error { 5 | models: Models 6 | constructor(models: Models) { 7 | this.models = models 8 | } 9 | 10 | async getErrorLists(data: any) { 11 | const { project, page = 1, pageSize = 20, startTime, endTime } = data 12 | const where: any = {} 13 | if (project) { 14 | where.project = project 15 | } 16 | if (startTime || endTime) { 17 | where.createdAt = { [Op.between]: [startTime, endTime] } 18 | } 19 | return this.models.error.findAndCountAll({ 20 | where, 21 | order: [['id', 'DESC']], 22 | raw: true, 23 | limit: parseInt(pageSize), 24 | offset: (page - 1) * pageSize, 25 | }) 26 | } 27 | } 28 | export default Error 29 | -------------------------------------------------------------------------------- /packages/server/app/services/index.ts: -------------------------------------------------------------------------------- 1 | import models from '../models' 2 | import Collect from './collect' 3 | import Error from './error' 4 | import { propAny } from '../types' 5 | 6 | const servicesMap: propAny = { 7 | collect: Collect, 8 | error: Error, 9 | } 10 | 11 | const services: propAny = {} 12 | 13 | Object.keys(servicesMap).forEach((name: string) => { 14 | services[name] = new servicesMap[name](models) 15 | }) 16 | 17 | export default services 18 | -------------------------------------------------------------------------------- /packages/server/app/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ReportData { 2 | project: string 3 | projectSub: string 4 | version: string 5 | referer: string 6 | identity: string 7 | type: string 8 | secondType: string 9 | key?: string 10 | time: number 11 | level: string 12 | value: any 13 | } 14 | 15 | export interface QueueItem { 16 | name: string 17 | value: string 18 | project: string 19 | version: string 20 | projectSub: string 21 | identity: string 22 | referer: string 23 | key?: string 24 | level: string 25 | } 26 | 27 | export interface propAny { 28 | [key: string]: any 29 | } 30 | -------------------------------------------------------------------------------- /packages/server/app/typings/connect-multiparty.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'connect-multiparty' { 2 | function Validate(): void 3 | namespace Validate { 4 | class Validator {} 5 | } 6 | 7 | export = Validate 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/app/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | declare global { 3 | var log: any 4 | var redis: any 5 | } 6 | 7 | export {} 8 | -------------------------------------------------------------------------------- /packages/server/app/typings/validate.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'koa-validate' { 2 | import * as Koa from 'koa' 3 | function Validate(app: Koa): void 4 | namespace Validate { 5 | class Validator {} 6 | } 7 | 8 | export = Validate 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/app/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | interface Params { 4 | [key: string]: string 5 | } 6 | 7 | // 递归创建目录 同步方法 8 | export const mkdirsSync = (dirname: string) => { 9 | if (fs.existsSync(dirname)) { 10 | return true 11 | } else { 12 | if (mkdirsSync(path.dirname(dirname))) { 13 | fs.mkdirSync(dirname) 14 | return true 15 | } 16 | } 17 | } 18 | // 递归创建目录 异步方法 19 | export const mkdirs = ( 20 | dirname: string, 21 | callback = () => { 22 | console.log('done') 23 | } 24 | ) => { 25 | fs.access(dirname, (err) => { 26 | if (err) { 27 | return mkdirs(path.dirname(dirname), function () { 28 | fs.mkdir(dirname, callback) 29 | }) 30 | } 31 | callback && callback() 32 | }) 33 | } 34 | 35 | export const queryUrl = (url: string): Params | null => { 36 | if (!url) return null 37 | let name, value 38 | const num = url.indexOf('?') 39 | const arr = url.slice(num + 1).split('&') 40 | const obj: Params = {} 41 | for (let i = 0; i < arr.length; i++) { 42 | const keyArr = arr[i].split('=') 43 | name = keyArr[0] 44 | value = keyArr[1] 45 | obj[name] = value 46 | } 47 | return obj 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/app/utils/resolve.ts: -------------------------------------------------------------------------------- 1 | class Resolve { 2 | fail(err: any = null, code = 0, msg = 'fail') { 3 | return { 4 | msg, 5 | err, 6 | code, 7 | } 8 | } 9 | 10 | success(data: any = null, code = 1, msg = 'success') { 11 | return { 12 | msg, 13 | code, 14 | data, 15 | } 16 | } 17 | } 18 | 19 | export default new Resolve() 20 | -------------------------------------------------------------------------------- /packages/server/app/utils/uploadFile.ts: -------------------------------------------------------------------------------- 1 | import multer from 'koa-multer' 2 | import path from 'path' 3 | import config from 'config' 4 | import { queryUrl, mkdirsSync } from '../utils/helper' 5 | // import fs from "fs"; 6 | 7 | const sourceMapFile = config.get('sourceMap') 8 | 9 | function uploadFile() { 10 | const storage = multer.diskStorage({ 11 | destination: async function (req: any, file: any, cb: any) { 12 | const params = queryUrl(req.url) 13 | const complPath = path.resolve( 14 | `${sourceMapFile}/${params && params.project}/${ 15 | params && params.version 16 | }` 17 | ) 18 | mkdirsSync(complPath) 19 | // const files = await fs.readdirSync(complPath); 20 | // if(files.length){ 21 | // files.forEach(async (file:string) => { 22 | // const childPath = complPath + "/" + file; 23 | // await fs.unlinkSync(childPath); 24 | // }); 25 | // } 26 | cb(null, complPath) 27 | }, 28 | filename: (ctx: any, file: any, cb: any) => { 29 | cb(null, file.originalname) 30 | }, 31 | }) 32 | //过滤上传的后缀为txt的文件 33 | const fileFilter = (ctx: any, file: any, cb: any) => { 34 | if (file.originalname.split('.').splice(-1) == 'txt') { 35 | cb(null, false) 36 | } else { 37 | cb(null, true) 38 | } 39 | } 40 | const upload = multer({ storage, fileFilter }) 41 | return upload 42 | } 43 | 44 | export default uploadFile 45 | -------------------------------------------------------------------------------- /packages/server/config/development.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | port: 8888, 3 | host: '127.0.0.1', 4 | secret: '123456', 5 | mysql: { 6 | database: 'monitor', 7 | user: 'root', 8 | password: '123456', 9 | host: '127.0.0.1', 10 | port: 3306, 11 | }, 12 | redis: { 13 | redis1: { 14 | host: '127.0.0.1', 15 | port: 6379, 16 | }, 17 | }, 18 | kueRedis: { 19 | host: '127.0.0.1', 20 | port: 6379, 21 | }, 22 | sourceMap: 'static/sourcemap', 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/config/production.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | port: 6666, 3 | host: '0.0.0.0', 4 | secret: '123456', 5 | mysql: { 6 | database: 'monitor', 7 | user: 'root', 8 | password: '123456', 9 | host: '8.134.132.121', 10 | port: 3306, 11 | }, 12 | redis: { 13 | redis1: { 14 | host: '8.134.132.121', 15 | port: 6379, 16 | }, 17 | }, 18 | kueRedis: { 19 | host: '8.134.132.121', 20 | port: 6379, 21 | }, 22 | sourceMap: 'static/sourcemap', 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | info: 4 | container_name: monitor-server 5 | image: registry.cn-guangzhou.aliyuncs.com/lycarrot/monitor-server:latest 6 | environment: 7 | - APP_ID=92708 8 | - APP_SECRET=5db96e9f6147654145c106e726eb3fa6514382c6 9 | ports: 10 | - "80:6666" 11 | restart: on-failure -------------------------------------------------------------------------------- /packages/server/index.ts: -------------------------------------------------------------------------------- 1 | import('./app') 2 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apply-monitor/server", 3 | "description": "监控平台服务端", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "cross-env NODE_ENV=development nodemon --watch app --exec ts-node --files index.ts | bunyan", 7 | "prod": "cross-env NODE_ENV=production NODE_LOG_DIR=/tmp ENABLE_NODE_LOG=YES ts-node --files index.ts | bunyan", 8 | "debug":"cross-env NODE_ENV=development nodemon --watch app --exec node -r ts-node/register -P ./tsconfig.json --inspect index.ts | bunyan" 9 | }, 10 | "private": true, 11 | "author": "luoying", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@apply-monitor/eslint-config": "^1.0.0", 15 | "@apply-monitor/report": "^1.0.0", 16 | "@babel/cli": "^7.21.0", 17 | "@babel/core": "^7.21.0", 18 | "@babel/node": "^7.20.7", 19 | "@babel/preset-env": "^7.20.2", 20 | "@types/bunyan": "^1.8.8", 21 | "@types/config": "^3.3.0", 22 | "@types/koa": "^2.13.5", 23 | "@types/koa-bodyparser": "^4.3.10", 24 | "@types/koa-bunyan-logger": "^2.1.4", 25 | "@types/koa-helmet": "^6.0.4", 26 | "@types/koa-logger": "^3.1.2", 27 | "@types/koa-router": "^7.4.4", 28 | "@types/koa2-cors": "^2.0.2", 29 | "@types/kue": "^0.11.14", 30 | "@types/node": "^18.14.4", 31 | "@types/sequelize": "^4.28.14", 32 | "@types/source-map": "^0.5.7", 33 | "@types/validator": "^13.7.15", 34 | "ts-node": "^10.9.1", 35 | "tslib": "^2.5.0", 36 | "typescript": "^4.9.4" 37 | }, 38 | "devDependencies": { 39 | "@types/koa-multer": "^1.0.1", 40 | "agenda": "^5.0.0", 41 | "bunyan": "^1.8.15", 42 | "co": "^4.6.0", 43 | "config": "^3.3.9", 44 | "cross-env": "^7.0.3", 45 | "crypto": "^1.0.1", 46 | "ioredis": "^5.3.2", 47 | "koa": "^2.3.0", 48 | "koa-bodyparser": "^4.2.1", 49 | "koa-bunyan-logger": "^2.1.0", 50 | "koa-compose": "^4.0.0", 51 | "koa-convert": "^1.2.0", 52 | "koa-helmet": "^4.1.0", 53 | "koa-jwt": "^4.0.4", 54 | "koa-logger": "^3.2.1", 55 | "koa-mount": "^3.0.0", 56 | "koa-multer": "^1.0.2", 57 | "koa-router": "^7.2.1", 58 | "koa-session": "^5.3.0", 59 | "koa-validate": "^1.0.7", 60 | "koa2-cors": "^2.0.6", 61 | "kue": "^0.11.6", 62 | "moment": "^2.29.4", 63 | "mysql2": "^3.2.0", 64 | "node-schedule": "^2.1.1", 65 | "nodemon": "^1.11.0", 66 | "redis": "^4.6.5", 67 | "request": "^2.81.0", 68 | "request-promise": "^4.2.1", 69 | "sequelize": "^6.31.0", 70 | "source-map": "^0.7.4" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "lib": ["ES6", "DOM"], 12 | "importHelpers": true, 13 | "allowJs": true, 14 | "composite": true, 15 | "typeRoots": ["./node_modules/@types", "./app/typings"] 16 | }, 17 | "include": [ 18 | "./app/**/*.ts", 19 | "./app/**/*.d.ts", 20 | "./index.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - test/* -------------------------------------------------------------------------------- /test/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 代码异常 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/vue-demo/.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets":[ 4 | [ 5 | "@babel/preset-env" 6 | ] 7 | ] 8 | } -------------------------------------------------------------------------------- /test/vue-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

11 | 12 | -------------------------------------------------------------------------------- /test/vue-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "webpack-dev-server --config script/webpack.dev.config.js", 9 | "build": "webpack --config script/webpack.prod.config.js" 10 | }, 11 | "private": true, 12 | "author": "luoying", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@apply-monitor/report": "workspace:*", 16 | "@apply-monitor/monitor": "workspace:*", 17 | "@babel/core": "^7.21.0", 18 | "@babel/preset-env": "^7.20.2", 19 | "autoprefixer": "^10.4.13", 20 | "babel-loader": "^8.0.5", 21 | "clean-webpack-plugin": "^2.0.2", 22 | "css-loader": "^2.1.1", 23 | "cssnano": "^4.1.10", 24 | "file-loader": "^3.0.1", 25 | "friendly-errors-webpack-plugin": "^1.7.0", 26 | "glob": "^7.1.4", 27 | "html-webpack-externals-plugin": "^3.8.0", 28 | "html-webpack-plugin": "^3.2.0", 29 | "less": "^3.9.0", 30 | "less-loader": "^5.0.0", 31 | "mini-css-extract-plugin": "^0.6.0", 32 | "optimize-css-assets-webpack-plugin": "^5.0.1", 33 | "postcss-loader": "^3.0.0", 34 | "postcss-preset-env": "^6.6.0", 35 | "source-map-loader": "^4.0.1", 36 | "vue": "^2.7.1", 37 | "vue-loader": "^15.10.1", 38 | "vue-router": "^3.4.1", 39 | "vue-skeleton-webpack-plugin": "^1.2.2", 40 | "webpack": "^4.31.0", 41 | "webpack-cli": "^3.3.1", 42 | "webpack-dev-server": "^3.3.1", 43 | "webpack-merge": "^4.2.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/vue-demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | loader: 'postcss-loader', 3 | plugins: { 4 | 'postcss-preset-env': { 5 | "browsers": [ 6 | "defaults", 7 | "not ie < 11", 8 | "last 2 versions", 9 | "> 1%", 10 | "iOS 7", 11 | "last 3 iOS versions" 12 | ] 13 | }, 14 | } 15 | } -------------------------------------------------------------------------------- /test/vue-demo/script/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 5 | const HtmlWebpackPlugin = require('html-webpack-plugin') 6 | const cleanWebpackPlugin = require('clean-webpack-plugin') 7 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin') 8 | // 从css文件中提取css代码到单独的文件中,对css代码进行代码压缩等 9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 10 | // import SkeletonWebpackPlugin from 'vue-skeleton-webpack-plugin'; 11 | 12 | module.exports = { 13 | entry: './src/index.js', 14 | output: { 15 | filename: '[name].[hash].js', 16 | path: path.join(__dirname, '../dist'), 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /.js$/, 22 | use: ['babel-loader'], 23 | }, 24 | { 25 | test: /.vue$/, 26 | use: 'vue-loader', 27 | }, 28 | { 29 | test: /.css$/, 30 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 31 | }, 32 | { 33 | test: /.less$/, 34 | use: [ 35 | MiniCssExtractPlugin.loader, 36 | 'css-loader', 37 | 'less-loader', 38 | 'postcss-loader', 39 | ], 40 | }, 41 | { 42 | test: /.(png|jpg|gif|jpeg)$/, 43 | use: [ 44 | { 45 | loader: 'file-loader', 46 | options: { 47 | name: '[name]_[hash:8].[ext]', 48 | }, 49 | }, 50 | ], 51 | }, 52 | { 53 | test: /.(woff|woff2|eot|ttf|otf)$/, 54 | use: [ 55 | { 56 | loader: 'file-loader', 57 | options: { 58 | name: '[name]_[hash:8][ext]', 59 | }, 60 | }, 61 | ], 62 | }, 63 | ], 64 | }, 65 | plugins: [ 66 | new webpack.ProgressPlugin(), 67 | new cleanWebpackPlugin(), 68 | new FriendlyErrorsWebpackPlugin({ 69 | clearConsole: true, 70 | }), 71 | new MiniCssExtractPlugin({ 72 | filename: '[name]_[contenthash].css', 73 | }), 74 | new VueLoaderPlugin(), 75 | new HtmlWebpackPlugin({ 76 | template: path.join(__dirname, '../index.html'), 77 | filename: 'index.html', 78 | // chunks: [name], 79 | inject: true, 80 | }), 81 | ], 82 | } 83 | -------------------------------------------------------------------------------- /test/vue-demo/script/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const path=require('path'); 2 | const webpack=require('webpack'); 3 | const baseConfig = require("./webpack.base.config"); 4 | const merge = require("webpack-merge"); 5 | 6 | module.exports = merge(baseConfig, { 7 | plugins: [new webpack.HotModuleReplacementPlugin()], 8 | devServer: { 9 | contentBase: path.join(process.cwd(),'./dist'), 10 | hot: true, 11 | port:8083, 12 | open:true, 13 | stats: "errors-only" 14 | }, 15 | devtool: "source-map", 16 | mode: "development", 17 | // stats: "errors-only", 18 | }); 19 | -------------------------------------------------------------------------------- /test/vue-demo/script/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | 2 | const cssnano = require('cssnano'); 3 | const merge = require('webpack-merge'); 4 | const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin'); 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const baseConfig = require("./webpack.base.config"); 7 | const ApplyMonitorReport = require("@apply-monitor/report"); 8 | 9 | 10 | module.exports = merge(baseConfig, { 11 | mode: 'production', 12 | devtool:'source-map', 13 | plugins: [ 14 | new ApplyMonitorReport({ 15 | url:'http://127.0.0.1:8080/api/common/upload/sourcemap', 16 | project:'testvue', 17 | version:'1.0.2', 18 | afterDelMap:true 19 | }), 20 | new OptimizeCSSAssetsPlugin({ 21 | assetNameRegExp: /\.css$/g, 22 | cssProcessor: cssnano, 23 | }), 24 | new HtmlWebpackExternalsPlugin({ 25 | externals: [ 26 | { 27 | module: "vue", 28 | entry: "https://lib.baomitu.com/vue/2.6.12/vue.min.js", 29 | global: "Vue", 30 | }, 31 | ], 32 | }), 33 | ], 34 | optimization: { 35 | splitChunks: { 36 | minSize: 0, 37 | cacheGroups: { 38 | commons: { 39 | name: 'vendors', 40 | chunks: 'all', 41 | minChunks: 2, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /test/vue-demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /test/vue-demo/src/about-child.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/vue-demo/src/about.vue: -------------------------------------------------------------------------------- 1 | 7 | 16 | -------------------------------------------------------------------------------- /test/vue-demo/src/home.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/vue-demo/src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import Monitor from '@apply-monitor/monitor' 5 | 6 | new Monitor({ 7 | url: 'http://127.0.0.1:6666/api/collect/info/detail', 8 | project: 'testvue', 9 | version: '1.0.2', 10 | isVue: true, 11 | vue: Vue, 12 | // isCollectPer: false, 13 | }) 14 | 15 | new Vue({ 16 | // el: '#app', 17 | router, 18 | render: (h) => h(App), 19 | }) 20 | -------------------------------------------------------------------------------- /test/vue-demo/src/router.js: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | import home from './home.vue' 3 | import about from './about.vue' 4 | import Vue from 'vue' 5 | Vue.use(Router) 6 | 7 | 8 | const route = new Router({ 9 | routes: [{ 10 | path: '/home', 11 | component: home 12 | }, 13 | { 14 | path: '/about', 15 | component: about 16 | }, 17 | { 18 | path: '/', 19 | redirect: '/home' 20 | }, 21 | 22 | ], 23 | }) 24 | 25 | export default route --------------------------------------------------------------------------------