├── .babelrc.js ├── .github └── workflows │ ├── ci.yml │ └── release-please.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── config │ ├── define.ts │ ├── microspot.config.ts │ └── utils │ │ ├── default.ts │ │ └── merge.ts ├── define.ts ├── entries │ ├── entry.es.ts │ └── entry.global.ts ├── index.ts ├── microspot │ ├── business │ │ ├── define.ts │ │ ├── index.ts │ │ └── tracker │ │ │ └── pageView.ts │ ├── experience │ │ ├── define.ts │ │ ├── index.ts │ │ └── tracker │ │ │ ├── common │ │ │ ├── firstContentfulPaint.ts │ │ │ ├── firstPaint.ts │ │ │ ├── index.ts │ │ │ ├── longTask.ts │ │ │ ├── memory.ts │ │ │ └── timing.ts │ │ │ ├── core │ │ │ ├── cumulativeLayoutShift.ts │ │ │ ├── firstInputDelay.ts │ │ │ ├── index.ts │ │ │ └── largestContentfulPaint.ts │ │ │ └── index.ts │ ├── index.ts │ └── stability │ │ ├── define.ts │ │ ├── index.ts │ │ └── tracker │ │ ├── __tests__ │ │ └── error.spec.ts │ │ ├── blankScreen.ts │ │ ├── error.ts │ │ ├── fetch.ts │ │ ├── index.ts │ │ ├── promiseRejection.ts │ │ └── xhr.ts └── utils │ ├── findLastEvent.ts │ ├── findSelector.ts │ ├── gifReport.ts │ ├── history.ts │ ├── onLoad.ts │ ├── resolveStack.ts │ └── sendHOC │ ├── buffer.ts │ ├── compose.ts │ ├── index.ts │ ├── sampling.ts │ └── userAgent.ts └── tsconfig.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 2 | 3 | module.exports = { 4 | presets: [ 5 | '@babel/preset-env', 6 | '@babel/preset-typescript', 7 | // [ 8 | // '@babel/preset-react', 9 | // { runtime: 'automatic' } 10 | // ] 11 | ] 12 | }; -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | runTSCheck: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-node@v3 9 | with: 10 | node-version: '16' 11 | - run: npm install 12 | - name: Typescript check 13 | run: npm run ci 14 | runTest: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: '16' 21 | - run: npm install 22 | - name: Run tests and collect coverage 23 | run: npm run test 24 | - name: Upload coverage to Codecov 25 | uses: codecov/codecov-action@v3 26 | runBuild: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: '16' 33 | - run: npm install 34 | - name: Build resource 35 | run: npm run build 36 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | name: release-please 9 | jobs: 10 | release-please: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: google-github-actions/release-please-action@v3 14 | with: 15 | release-type: node 16 | package-name: microspot 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | es 3 | types 4 | coverage -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run ci --noEmit 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.2.1](https://github.com/Ruimve/microspot/compare/v1.2.0...v1.2.1) (2023-03-12) 4 | 5 | 6 | ### Miscellaneous Chores 7 | 8 | * release 1.2.1 ([caf7169](https://github.com/Ruimve/microspot/commit/caf7169fbf3754614bd63a8cbb51b98bb326b941)) 9 | 10 | ## [1.2.0](https://github.com/Ruimve/microspot/compare/v1.1.0...v1.2.0) (2023-02-28) 11 | 12 | 13 | ### Features 14 | 15 | * add xhr/fetch whitelist and status list ([c9b3742](https://github.com/Ruimve/microspot/commit/c9b3742a6a3212e7d40225cbc0ad765a52ff090d)) 16 | * env information ([73a78ae](https://github.com/Ruimve/microspot/commit/73a78ae481fa243f16a152a6ed4eb27ce7d9e18f)) 17 | * memory usage ([b132a05](https://github.com/Ruimve/microspot/commit/b132a05cd0ea606aebcb47aaafc5addacf6db902)) 18 | * page view ([bb9df5a](https://github.com/Ruimve/microspot/commit/bb9df5a94385e37a2832f6babd8f14b5bc158405)) 19 | * sampling, buffer ([7fc5f01](https://github.com/Ruimve/microspot/commit/7fc5f015ebbd371339c54df20544f07a9ee57f26)) 20 | * support editing long task time ([e6679de](https://github.com/Ruimve/microspot/commit/e6679de33a101f17e415fbd80614ad5162a89e32)) 21 | * timing tracker ([84594ff](https://github.com/Ruimve/microspot/commit/84594ffc10c58bed58c39ecb828dfa687a0d7a6f)) 22 | * 支持指定监听最后触发事件的类型 ([e0a3e39](https://github.com/Ruimve/microspot/commit/e0a3e397d7ccd31039a685e00b5ab893577db739)) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * stack definition ([8cce7b2](https://github.com/Ruimve/microspot/commit/8cce7b2ef8ebb36fa2f085efb5749d8ac6129b78)) 28 | 29 | 30 | ### Performance Improvements 31 | 32 | * format style ([e115aad](https://github.com/Ruimve/microspot/commit/e115aad6e839dbb2c4b085f6731cbc2e2fcdf552)) 33 | * hide memory ([78848d9](https://github.com/Ruimve/microspot/commit/78848d96d1b7a807772e4f76ae90e150aa7821a5)) 34 | 35 | ## [1.1.0](https://github.com/Ruimve/microspot/compare/v1.0.0...v1.1.0) (2023-02-19) 36 | 37 | 38 | ### Features 39 | 40 | * gif report ([7ffc015](https://github.com/Ruimve/microspot/commit/7ffc015cc5189c8c558b72a56875893ee63d7f0b)) 41 | * performance timing ([92e9685](https://github.com/Ruimve/microspot/commit/92e9685167d75125e8fed043a94e36a274248a64)) 42 | 43 | 44 | ### Performance Improvements 45 | 46 | * ts define ([40f66e9](https://github.com/Ruimve/microspot/commit/40f66e9c220dbf5a926f42e81d1ef1254a84b523)) 47 | 48 | ## 1.0.0 (2023-02-10) 49 | 50 | 51 | ### Features 52 | 53 | * blackscreen, xhr and fetch tracker ([330d167](https://github.com/Ruimve/microspot/commit/330d167fc410e40e69bfd67fc79d7765f0ff9d2f)) 54 | * js tracker ([f0e6c22](https://github.com/Ruimve/microspot/commit/f0e6c2256485fc76bee860b477f7acde15880c3f)) 55 | * 添加性能指标 CLS, FID, LCP, FP, FCP, LT ([4662131](https://github.com/Ruimve/microspot/commit/466213110807f67ea761f653b7ae47b948844c84)) 56 | 57 | ## 1.0.0 (2023-01-23) 58 | 59 | 60 | ### Features 61 | 62 | * template ([d46655a](https://github.com/Ruimve/TEMPALTE-LIB-ROLLUP/commit/d46655aecdb39f5f677c839386202ecbe40dceae)) 63 | * update .gitignore ([2ab8211](https://github.com/Ruimve/TEMPALTE-LIB-ROLLUP/commit/2ab82112d649dbb57be41ce866a907cedee9b614)) 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | ## How should I write my commits? 6 | 7 | Please assumes you are using [Conventional Commit messages][conventional-commit-message]. 8 | 9 | The most important prefixes you should have in mind are: 10 | ``` 11 | fix: which represents bug fixes, and correlates to a SemVer patch. 12 | feat: which represents a new feature, and correlates to a SemVer minor. 13 | feat!:, or fix!:, refactor!:, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major. 14 | ``` 15 | 16 | 17 | 18 | [conventional-commit-message]: https://www.conventionalcommits.org -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ruimve 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

microspot

3 | 4 | 5 | Ruimve 10 | 11 | 12 |

前端监控 SDK

13 |
14 |
15 | 16 | [![Build Status][build-badge]][build] 17 | [![version][version-badge]][package] 18 | [![downloads][downloads-badge]][npmtrends] 19 | [![MIT License][license-badge]][license] 20 | [![PRs Welcome][prs-badge]][prs] 21 | 22 | [![Watch on GitHub][github-watch-badge]][github-watch] 23 | [![Star on GitHub][github-star-badge]][github-star] 24 | 25 | 26 | ## 内容列表 27 | 28 | - [简介](#简介) 29 | - [如何消费](#如何消费) 30 | - [支持 CDN 引入](#支持-cdn-引入) 31 | - [支持 ES Module](#支持-es-module) 32 | - [监控项](#监控项) 33 | - [异常监控](#异常监控) 34 | - [运行时异常](#监控-js-运行时异常) 35 | - [资源加载异常](#监控资源加载异常) 36 | - [Promise 拒绝未处理](#捕获拒绝未处理的-promise) 37 | - [白屏](#白屏) 38 | - [Ajax 请求](#监控-ajax) 39 | - [Fetch 请求](#监控-fetch) 40 | - [性能监控](#性能监控) 41 | - [核心指标](#lcp-largest-contentful-paint-最大内容绘制) 42 | - [最大内容绘制(LCP)](#lcp-largest-contentful-paint-最大内容绘制) 43 | - [首次输入延迟(FID)](#fid-first-input-delay-首次输入延迟) 44 | - [累计布局位移(CLS)](#cls-cumulative-layout-shift-累计布局位移) 45 | - [其他指标](#fp-first-paint-首次绘制时间) 46 | - [首次绘制时间(FP)](#fp-first-paint-首次绘制时间) 47 | - [首次内容绘制时间(FCP)](#fcp-first-contentful-paint-首次内容绘制时间) 48 | - [长任务(LongTask)](#longtask-长任务) 49 | - [阶段耗时(Timing)](#timing-阶段耗时) 50 | - [业务埋点](#业务埋点) 51 | - [页面访问量(PV)](#page-view-页面访问量) 52 | - [自定义埋点](#自定义埋点) 53 | - [指标配置项](#指标配置项) 54 | 55 | ## 简介 56 | 57 | 在前端系统中,通常异常是不可控的,并不能确定什么时候或者什么场景会发生异常,但它却会实实在在的影响用户体现。所以,我们非常有必要去做这样一件事情,就是去监控异常的发生并防范它。 58 | 59 | 不过有时候尽管没有异常发生,功能也正常,但是由于编码的问题导致运行十分缓慢,这就需要监控 web 应用的性能指标。 60 | 61 | 除此之外,去了解用户的行为,以用户数据为基础,来指导我们产品的优化方向,也是前端监控的重要课题之一。 62 | 63 | 所以前端监控主要可以分为三大类:数据监控、性能监控和异常监控。 64 | 65 | ## 如何消费 66 | 67 | `microspot` 支持 CDN 和 ES Module 的方式使用,但是必须确保的是需要在所有资源之前引入。 68 | 69 | ### 支持 CDN 引入 70 | 71 | 打包之后会在 `es` 文件夹中生成 `index.global.js`,我们将它挂到 CDN 上通过路径引入,它会在 `window` 上定义一个 `microspot` 对象,通过对象上的 `set` 方法来配置监听项,通过 `start` 方法启动监听。 72 | 73 | ```html 74 | 75 | 76 | 77 | 78 | 95 | 96 | 97 | ``` 98 | 99 | ### 支持 ES Module 100 | 101 | #### 安装 102 | 103 | 本模块通过 [npm][npm] 分发,需要将其添加到项目的 `dependencies` 中: 104 | 105 | ``` 106 | npm install microspot --save 107 | ``` 108 | 或 109 | 110 | 通过 [yarn][yarn] 安装: 111 | ``` 112 | yarn add microspot 113 | ``` 114 | 115 | #### 使用 116 | 117 | ```ts 118 | import { Microspot } from 'microspot'; 119 | const microspot = new Microspot(); 120 | microspot.set({ 121 | tracker: [ 122 | 'STABILITY', 123 | 'EXPERIENCE', 124 | 'BUSINESS' 125 | ], 126 | lastEvent: true, 127 | send: (spot) => { 128 | console.log('上传', spot) 129 | } 130 | }); 131 | 132 | microspot.start(() => { 133 | console.log('启动监听'); 134 | }); 135 | ``` 136 | 137 | ## 配置项 138 | 139 | 我们通过 `set` 方法可以传入监控配置,下面是具体的配置项: 140 | 141 | ### Config 定义 142 | 143 | |字段|类型|默认值|描述| 144 | |:-:|:-:|:----:|:--| 145 | |**`tracker`**|`{Array}`|`['STABILITY','EXPERIENCE','BUSINESS']`|需要监控的大类有:`STABILITY 稳定性`、`EXPERIENCE 体验`、`BUSINESS 业务`| 146 | |**`lastEvent`**|`{boolean\|Array}`|`true`|是否监听最后操作事件, 也可以通过事件数组指定监听的事件| 147 | |**`send`**|`{(spot: SendSpot\|SendSpot[], options: DefaultIndexOption) => void}`|模块提供了 gif 上传方法|自定义埋点数据上传的方法,当埋点被触发时会调用该方法| 148 | 149 | ## 监控项 150 | 151 | 监控用户数据的上报格式,如下: 152 | 153 | 154 | ### 异常监控 155 | 156 | #### 监控 JS 运行时异常 157 | 158 | ##### 配置 159 | 160 | ```js 161 | { 162 | "tracker": [ 163 | { 164 | "type": "STABILITY", 165 | "index": [ 166 | { 167 | "type": "JS_RUNTIME_ERROR", 168 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 169 | "sampling": 1, 170 | /** 缓存,会收集埋点数据批量上传 */ 171 | // "buffer": 10 172 | }, 173 | ] 174 | } 175 | ], 176 | } 177 | ``` 178 | 179 | ##### 埋点数据 180 | 181 | ```ts 182 | interface Spot { 183 | type: string; 184 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 185 | subType: string; 186 | filename: string; 187 | message: string; 188 | position: string; 189 | stack: { at: string; scope: string; filename: string; lineno: string; colno: string; }[]; 190 | selector: string; 191 | } 192 | ``` 193 | 194 | #### 监控资源加载异常 195 | 196 | ##### 配置 197 | 198 | |资源类型|对应字段| 199 | |:-----:|:-----:| 200 | |**`脚本`**|`SCRIPT_LOAD_ERROR`| 201 | |**`样式`**|`CSS_LOAD_ERROR`| 202 | |**`图像`**|`IMAGE_LOAD_ERROR`| 203 | |**`音频`**|`AUDIO_LOAD_ERROR`| 204 | |**`视频`**|`VIDEO_LOAD_ERROR`| 205 | 206 | ```js 207 | { 208 | "tracker": [ 209 | { 210 | "type": "STABILITY", 211 | "index": [ 212 | { 213 | "type": "SCRIPT_LOAD_ERROR", 214 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 215 | "sampling": 1, 216 | /** 缓存,会收集埋点数据批量上传 */ 217 | // "buffer": 10 218 | }, 219 | ] 220 | } 221 | ], 222 | } 223 | ``` 224 | 225 | ##### 埋点数据 226 | 227 | ```ts 228 | interface Spot { 229 | type: string; 230 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 231 | subType: string; 232 | filename: string; 233 | tagName: string; 234 | selector: string; 235 | } 236 | ``` 237 | 238 | #### 捕获拒绝未处理的 Promise 239 | 240 | ##### 配置 241 | 242 | ```js 243 | { 244 | "tracker": [ 245 | { 246 | "type": "STABILITY", 247 | "index": [ 248 | { 249 | "type": "PROMISE_REJECTION", 250 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 251 | "sampling": 1, 252 | /** 缓存,会收集埋点数据批量上传 */ 253 | // "buffer": 10 254 | }, 255 | ] 256 | } 257 | ], 258 | } 259 | ``` 260 | 261 | ##### 埋点数据 262 | 263 | ```ts 264 | interface Spot { 265 | type: string; 266 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 267 | subType: string; 268 | message: string; 269 | filename: string; 270 | position: string; 271 | stack: { at: string; scope: string; filename: string; lineno: string; colno: string; }[]; 272 | selector: string; 273 | } 274 | ``` 275 | 276 | #### 白屏 277 | 278 | ##### 配置 279 | 280 | ```js 281 | { 282 | "tracker": [ 283 | { 284 | "type": "STABILITY", 285 | "index": [ 286 | { 287 | "type": "BLANK_SCREEN", 288 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 289 | "sampling": 1, 290 | /** 缓存,会收集埋点数据批量上传 */ 291 | // "buffer": 10 292 | }, 293 | ] 294 | } 295 | ], 296 | } 297 | ``` 298 | 299 | ##### 埋点数据 300 | 301 | ```ts 302 | interface Spot { 303 | type: string; 304 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 305 | subType: string; 306 | emptyPoints: string; 307 | screen: string; 308 | viewPoint: string; 309 | selector: string[]; 310 | } 311 | ``` 312 | 313 | #### 监控 Ajax 314 | 315 | ##### 配置 316 | 317 | ```js 318 | { 319 | "tracker": [ 320 | { 321 | "type": "STABILITY", 322 | "index": [ 323 | { 324 | "type": "XHR", 325 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 326 | "sampling": 1, 327 | /** 缓存,会收集埋点数据批量上传 */ 328 | // "buffer": 10, 329 | /** 接口白名单,名单中的接口不会进行打点 **/ 330 | "apiWhiteList": [/^http:\/\/127.0.0.1:5500\/.+/], 331 | /** 监听的状态码列表 **/ 332 | "statusList": [404, 405] 333 | }, 334 | ] 335 | } 336 | ], 337 | } 338 | ``` 339 | 340 | ##### 埋点数据 341 | 342 | ```ts 343 | interface Spot { 344 | type: string; 345 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 346 | subType: string; 347 | eventType: string; 348 | pathname: string; 349 | status: string; 350 | statusText: string; 351 | duration: string; 352 | response: string; 353 | params: string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData; 354 | } 355 | ``` 356 | 357 | #### 监控 Fetch 358 | 359 | ##### 配置 360 | 361 | ```js 362 | { 363 | "tracker": [ 364 | { 365 | "type": "STABILITY", 366 | "index": [ 367 | { 368 | "type": "FETCH", 369 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 370 | "sampling": 1, 371 | /** 缓存,会收集埋点数据批量上传 */ 372 | // "buffer": 10, 373 | /** 接口白名单,名单中的接口不会进行打点 **/ 374 | "apiWhiteList": [/^http:\/\/127.0.0.1:5500\/.+/], 375 | /** 监听的状态码列表 **/ 376 | "statusList": [404, 405] 377 | }, 378 | ] 379 | } 380 | ], 381 | } 382 | ``` 383 | 384 | ##### 埋点数据 385 | 386 | ```ts 387 | interface Spot { 388 | type: string; 389 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 390 | subType: string; 391 | pathname: string; 392 | status: string; 393 | statusText: string; 394 | contentType: string; 395 | duration: string; 396 | } 397 | ``` 398 | 399 | ### 性能监控 400 | 401 | Web 指标是 Google 开创的一项新计划,旨在为网络质量信号提供统一指导,这些信号对于提供出色的网络用户体验至关重要。 402 | 403 | 网站所有者要想了解他们提供给用户的体验质量,并非需要成为性能专家。Web 指标计划为了简化场景,帮助网站专注于最重要的指标,即 [核心 Web 指标(LCP FID CLS)][web-index]。 404 | 405 | 除此核心指标之外还有另外一些指标,如 FP、FCP、longTask、内存使用情况和阶段耗时等也是重要的指标。 406 | 407 | #### LCP (Largest Contentful Paint) 最大内容绘制 408 | 409 | ##### 配置 410 | 411 | ```js 412 | { 413 | "tracker": [ 414 | { 415 | "type": "EXPERIENCE", 416 | "index": [ 417 | { 418 | "type": "LARGEST_CONTENTFUL_PAINT", 419 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 420 | "sampling": 1, 421 | /** 缓存,会收集埋点数据批量上传 */ 422 | // "buffer": 10, 423 | }, 424 | ] 425 | } 426 | ], 427 | } 428 | ``` 429 | 430 | ##### 埋点数据 431 | 432 | ```ts 433 | interface Spot { 434 | type: string; 435 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 436 | subType: string; 437 | startTime: string; 438 | duration: string; 439 | selector: string; 440 | url: string; 441 | } 442 | ``` 443 | 444 | #### FID (First Input Delay) 首次输入延迟 445 | 446 | ##### 配置 447 | 448 | ```js 449 | { 450 | "tracker": [ 451 | { 452 | "type": "EXPERIENCE", 453 | "index": [ 454 | { 455 | "type": "FIRST_INPUT_DELAY", 456 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 457 | "sampling": 1, 458 | /** 缓存,会收集埋点数据批量上传 */ 459 | // "buffer": 10, 460 | }, 461 | ] 462 | } 463 | ], 464 | } 465 | ``` 466 | 467 | ##### 埋点数据 468 | 469 | ```ts 470 | interface Spot { 471 | type: string; 472 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 473 | subType: string; 474 | cancelable: string, 475 | processingStart: string, 476 | processingEnd: string, 477 | startTime: string; 478 | duration: string; 479 | } 480 | ``` 481 | 482 | #### CLS (Cumulative Layout Shift) 累计布局位移 483 | 484 | ##### 配置 485 | 486 | ```js 487 | { 488 | "tracker": [ 489 | { 490 | "type": "EXPERIENCE", 491 | "index": [ 492 | { 493 | "type": "CUMULATIVE_LAYOUT_SHIFT", 494 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 495 | "sampling": 1, 496 | /** 缓存,会收集埋点数据批量上传 */ 497 | // "buffer": 10, 498 | }, 499 | ] 500 | } 501 | ], 502 | } 503 | ``` 504 | 505 | ##### 埋点数据 506 | 507 | ```ts 508 | interface Spot { 509 | type: string; 510 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 511 | subType: string; 512 | value: string; 513 | sources: string[]; 514 | } 515 | ``` 516 | 517 | #### FP (First Paint) 首次绘制时间 518 | 519 | ##### 配置 520 | 521 | ```js 522 | { 523 | "tracker": [ 524 | { 525 | "type": "EXPERIENCE", 526 | "index": [ 527 | { 528 | "type": "FIRST_PAINT", 529 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 530 | "sampling": 1, 531 | /** 缓存,会收集埋点数据批量上传 */ 532 | // "buffer": 10, 533 | }, 534 | ] 535 | } 536 | ], 537 | } 538 | ``` 539 | 540 | ##### 埋点数据 541 | 542 | ```ts 543 | interface Spot { 544 | type: string; 545 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 546 | subType: string; 547 | startTime: string; 548 | duration: string; 549 | } 550 | ``` 551 | 552 | #### FCP (First Contentful Paint) 首次内容绘制时间 553 | 554 | ##### 配置 555 | 556 | ```js 557 | { 558 | "tracker": [ 559 | { 560 | "type": "EXPERIENCE", 561 | "index": [ 562 | { 563 | "type": "FIRST_CONTENTFUL_PAINT", 564 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 565 | "sampling": 1, 566 | /** 缓存,会收集埋点数据批量上传 */ 567 | // "buffer": 10, 568 | }, 569 | ] 570 | } 571 | ], 572 | } 573 | ``` 574 | 575 | ##### 埋点数据 576 | 577 | ```ts 578 | interface Spot { 579 | type: string; 580 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 581 | subType: string; 582 | startTime: string; 583 | duration: string; 584 | } 585 | ``` 586 | 587 | #### LongTask 长任务 588 | 589 | ##### 配置 590 | 591 | ```js 592 | { 593 | "tracker": [ 594 | { 595 | "type": "EXPERIENCE", 596 | "index": [ 597 | { 598 | "type": "LONG_TASK", 599 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 600 | "sampling": 1, 601 | /** 缓存,会收集埋点数据批量上传 */ 602 | // "buffer": 10, 603 | /** 长任务埋点设定时长,超过这个时长会进行打点,默认 200 毫秒 */ 604 | "limitTime": 200 605 | }, 606 | ] 607 | } 608 | ], 609 | } 610 | ``` 611 | 612 | ##### 埋点数据 613 | 614 | ```ts 615 | interface Spot { 616 | type: string; 617 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 618 | subType: string; 619 | eventType: string; 620 | startTime: string; 621 | duration: string; 622 | selector: string; 623 | } 624 | ``` 625 | 626 | #### Timing 阶段耗时 627 | 628 | 在 Web 应用中还有很多耗时的指标计算如下: 629 | 630 | |名称|计算方法|描述| 631 | |:-:|:-----:|:-:| 632 | |**`loadTiming`**|`loadEventEnd - startTime`|`页面加载总耗时`| 633 | |**`dnsTiming`**|`domainLookupEnd - domainLookupStart`|`DNS 解析耗时`| 634 | |**`tcpTiming`**|`connectEnd - connectStart`|`TCP 连接耗时`| 635 | |**`sslTiming`**|`connectEnd - secureConnectionStart`|`SSL 连接耗时`| 636 | |**`requestTiming`**|`responseStart - requestStart`|`网路请求耗时`| 637 | |**`responseTiming`**|`responseEnd - responseStart`|`数据请求耗时`| 638 | |**`domTiming`**|`domContentLoadedEventEnd - responseEnd`|`DOM 解析耗时`| 639 | |**`resourceTiming`**|`loadEventEnd - domContentLoadedEventEnd`|`资源加载耗时`| 640 | |**`firstPacketTiming`**|`responseStart - startTime`|`首包耗时`| 641 | |**`renderTiming`**|`loadEventEnd - responseEnd`|`页面渲染耗时`| 642 | |**`htmlTiming`**|`responseEnd - startTime`|`HTML 加载完时间`| 643 | |**`firstInteractiveTiming`**|`domInteractive - startTime`|`首次可交互时间`| 644 | 645 | ##### 配置 646 | 647 | ```js 648 | { 649 | "tracker": [ 650 | { 651 | "type": "EXPERIENCE", 652 | "index": [ 653 | { 654 | "type": "TIMING", 655 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 656 | "sampling": 1, 657 | /** 缓存,会收集埋点数据批量上传 */ 658 | // "buffer": 10, 659 | }, 660 | ] 661 | } 662 | ], 663 | } 664 | ``` 665 | 666 | ##### 埋点数据 667 | 668 | ```ts 669 | interface Spot { 670 | type: string; 671 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 672 | subType: string; 673 | /** 性能元数据 */ 674 | raw: PerformanceNavigationTiming; 675 | loadTiming: string; 676 | dnsTiming: string; 677 | tcpTiming: string; 678 | sslTiming: string; 679 | requestTiming: string; 680 | responseTiming: string; 681 | domTiming: string; 682 | resourceTiming: string; 683 | firstPacketTiming: string; 684 | renderTiming: string; 685 | htmlTiming: string; 686 | firstInteractiveTiming: string; 687 | } 688 | ``` 689 | 690 | ### 业务埋点 691 | 692 | #### Page View 页面访问量 693 | 694 | ##### 配置 695 | 696 | ```js 697 | { 698 | "tracker": [ 699 | { 700 | "type": "BUSINESS", 701 | "index": [ 702 | { 703 | "type": "PAGE_VIEW", 704 | /** 采样率 0-1, 比如 0.2,只有百分之 20 的埋点会被上传*/ 705 | "sampling": 1, 706 | /** 缓存,会收集埋点数据批量上传 */ 707 | // "buffer": 10, 708 | }, 709 | ] 710 | } 711 | ], 712 | } 713 | ``` 714 | 715 | ##### 埋点数据 716 | 717 | ```ts 718 | interface Spot { 719 | type: string; 720 | env: { title: string; url: string; timestamp: number; userAgent: string; }; 721 | subType: string; 722 | href: string; 723 | } 724 | ``` 725 | 726 | #### 自定义埋点 727 | 728 | 不管是 `CDN` 还是 `ES Module`,`microspot` 对象都会提供一个 send 方法,可以手动发送埋点数据,参数有两个: 729 | 730 | - 拥有任意属性的自定义对象 731 | - [指标配置项](#指标配置项) 732 | 733 | 734 | 735 | ```ts 736 | import { Microspot } from 'microspot'; 737 | const microspot = new Microspot(); 738 | microspot.set(/** ... **/ ); 739 | microspot.start(/** ... **/); 740 | 741 | /** 742 | * 第一个入参数是自定义的对象,不限制属性 743 | * 第二个入参数是指标配置项 744 | */ 745 | microspot.send( 746 | { type: '自定义 type', arg2: '自定义属性' }, 747 | { sampling: 1, buffer: 10 } 748 | ); 749 | ``` 750 | 751 | ## 指标配置项 752 | 753 | 从上面的配置中,应该已经大概了解,下面具体列举下: 754 | 755 | |名称|适用的指标|描述| 756 | |:-:|:-----:|:-:| 757 | |**`type`**|`all`|`指标类型`| 758 | |**`sampling`**|`all`|`采样率 0 - 1`| 759 | |**`buffer`**|`all`|`缓冲发送`| 760 | |**`routerMode`**|`PAGE_VIEW`|`路由模式`| 761 | |**`apiWhiteList`**|`XHR, FETCH`|`请求域名白名单`| 762 | |**`statusList`**|`XHR, FETCH`|`请求监听 status`| 763 | |**`limitTime`**|`LONG_TASK`|`长任务埋点设定时长`| 764 | 765 | [npm]: https://www.npmjs.com/ 766 | [yarn]: https://classic.yarnpkg.com 767 | [build-badge]: https://img.shields.io/github/workflow/status/microspot/validate?logo=github&style=flat-square 768 | [build]: https://github.com/Ruimve/microspot/actions/workflows/ci.yml/badge.svg 769 | [coverage-badge]: https://img.shields.io/codecov/c/github/Ruimve/microspot.svg?style=flat-square 770 | [coverage]: https://codecov.io/github/microspot 771 | [version-badge]: https://img.shields.io/npm/v/microspot.svg?style=flat-square 772 | [package]: https://www.npmjs.com/package/microspot 773 | [downloads-badge]: https://img.shields.io/npm/dm/microspot.svg?style=flat-square 774 | [npmtrends]: http://www.npmtrends.com/microspot 775 | [license-badge]: https://img.shields.io/npm/l/microspot.svg?style=flat-square 776 | [license]: https://github.com/Ruimve/microspot/blob/master/LICENSE 777 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 778 | [prs]: http://makeapullrequest.com 779 | [github-watch-badge]: https://img.shields.io/github/watchers/Ruimve/microspot.svg?style=social 780 | [github-watch]: https://github.com/Ruimve/microspot/watchers 781 | [github-star-badge]: https://img.shields.io/github/stars/Ruimve/microspot.svg?style=social 782 | [github-star]: https://github.com/Ruimve/microspot/stargazers 783 | 784 | [web-index]: https://web.dev/i18n/zh/defining-core-web-vitals-thresholds/ -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'chore', 10 | 'ci', 11 | 'docs', 12 | 'feat', 13 | 'fix', 14 | 'perf', 15 | 'refactor', 16 | 'revert', 17 | 'style', 18 | 'test' 19 | ] 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microspot", 3 | "version": "1.2.1", 4 | "description": "", 5 | "main": "es/index.es.js", 6 | "types": "types/index.d.ts", 7 | "author": "jingyu", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "rollup -c --bundleConfigAsCjs", 11 | "ci": "tsc --noEmit", 12 | "test": "jest", 13 | "prepare": "husky install" 14 | }, 15 | "files": [ 16 | "es", 17 | "types" 18 | ], 19 | "keywords": [], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Ruimve/microspot.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/Ruimve/microspot/issues" 26 | }, 27 | "homepage": "https://github.com/Ruimve/microspot#readme", 28 | "dependencies": { 29 | "typescript": "^4.9.4" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.20.12", 33 | "@babel/preset-env": "^7.20.2", 34 | "@babel/preset-typescript": "^7.18.6", 35 | "@commitlint/cli": "^17.4.2", 36 | "@commitlint/config-conventional": "^17.4.2", 37 | "@rollup/plugin-alias": "^4.0.3", 38 | "@rollup/plugin-commonjs": "^24.0.1", 39 | "@rollup/plugin-node-resolve": "^15.0.1", 40 | "@rollup/plugin-terser": "^0.3.0", 41 | "@rollup/plugin-typescript": "^11.0.0", 42 | "@types/jest": "^29.2.6", 43 | "babel-jest": "^29.3.1", 44 | "husky": "^8.0.3", 45 | "jest": "^29.3.1", 46 | "jest-environment-jsdom": "^29.3.1", 47 | "rollup": "^3.10.1", 48 | "tslib": "^2.4.1" 49 | }, 50 | "jest": { 51 | "testEnvironment": "jsdom", 52 | "testMatch": [ 53 | "**/*.spec.{js,jsx,ts,tsx}" 54 | ], 55 | "collectCoverage": true, 56 | "coverageReporters": [ 57 | "text", 58 | "cobertura" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import terser from '@rollup/plugin-terser'; 5 | import pkg from './package.json'; 6 | 7 | export default [ 8 | { 9 | input: 'src/entries/entry.es.ts', 10 | output: { 11 | file: pkg.main, 12 | format: 'es' 13 | }, 14 | external: [], 15 | plugins: [ 16 | nodeResolve(), 17 | typescript({ 18 | compilerOptions: { 19 | declaration: false 20 | } 21 | }), 22 | commonjs(), 23 | terser() 24 | ] 25 | }, 26 | { 27 | input: 'src/entries/entry.global.ts', 28 | output: { 29 | file: 'es/index.global.js', 30 | format: 'es' 31 | }, 32 | external: [], 33 | plugins: [ 34 | nodeResolve(), 35 | typescript({ 36 | compilerOptions: { 37 | declaration: false 38 | } 39 | }), 40 | commonjs(), 41 | terser() 42 | ] 43 | }, 44 | { 45 | input: 'src/index.ts', 46 | output: { 47 | file: pkg.types, 48 | format: 'es' 49 | }, 50 | external: [], 51 | plugins: [ 52 | typescript() 53 | ] 54 | } 55 | ]; -------------------------------------------------------------------------------- /src/config/define.ts: -------------------------------------------------------------------------------- 1 | 2 | import { SpotType } from '../define'; 3 | import { StabilityType, RuntimeErrorSpot, ResourceLoadErrorSpot, PromiseRejectionSpot, BlankScreenSpot, XHRSpot, FetchSpot } from '../microspot/stability/define'; 4 | import { ExperienceType, FirstPaintSpot, FirstContentfulPaintSpot, LargestContentfulPaintSpot, FirstInputDelaySpot, CumulativeLayoutShiftSpot, LongTaskSpot, TimingSpot } from '../microspot/experience/define'; 5 | import { BusinessType, PageViewSpot } from '../microspot/business/define'; 6 | 7 | /** 上报函数定义 */ 8 | type StabilitySpot = RuntimeErrorSpot | ResourceLoadErrorSpot | PromiseRejectionSpot | BlankScreenSpot | XHRSpot | FetchSpot; 9 | type ExperienceSpot = FirstPaintSpot | FirstContentfulPaintSpot | LargestContentfulPaintSpot | FirstInputDelaySpot | CumulativeLayoutShiftSpot | LongTaskSpot | TimingSpot; 10 | type BusinessSpot = PageViewSpot; 11 | export type SendSpot = StabilitySpot | ExperienceSpot | BusinessSpot; 12 | export type Send = (spot: SendSpot | SendSpot[], options: DefaultIndexOption) => void; 13 | 14 | /** 配置 config 定义 */ 15 | export type IndexType = string | StabilityType | ExperienceType; 16 | export type IndexOption = { 17 | type: IndexType, 18 | /** 采样率 0 - 1 */ 19 | sampling: number, 20 | /** 路由模式 */ 21 | routerMode?: 'history' | 'hash', 22 | /** 缓冲发送 */ 23 | buffer?: number, 24 | /** 请求域名白名单 */ 25 | apiWhiteList?: string[], 26 | /** 请求监听 status */ 27 | statusList?: number[], 28 | /** 长任务埋点设定时长 */ 29 | limitTime?: number 30 | } 31 | export type Index = (IndexType | IndexOption)[] 32 | 33 | export type TrackerOption = { type: string | SpotType, index?: Index } 34 | export type Tracker = (string | TrackerOption)[]; 35 | 36 | export interface Config { 37 | tracker: Tracker; 38 | lastEvent?: boolean | string[]; 39 | send?: Send; 40 | } 41 | 42 | /** 默认配置的定义 */ 43 | export type DefaultIndexType = StabilityType | ExperienceType | BusinessType; 44 | export type DefaultIndexOption = { 45 | type: DefaultIndexType, 46 | /** 采样率 0 - 1 */ 47 | sampling: number, 48 | /** 路由模式 */ 49 | routerMode?: 'history' | 'hash', 50 | /** 缓冲发送 */ 51 | buffer?: number, 52 | /** 请求域名白名单 */ 53 | apiWhiteList?: string[], 54 | /** 请求监听 status */ 55 | statusList?: number[], 56 | /** 长任务埋点设定时长 */ 57 | limitTime?: number 58 | } 59 | export type DefaultIndex = DefaultIndexOption[]; 60 | 61 | export type DefaultTrackerOption = { type: SpotType, index: DefaultIndex } 62 | export type DefaultTracker = DefaultTrackerOption[]; 63 | 64 | export interface DefaultConfig { 65 | tracker: DefaultTracker; 66 | lastEvent: boolean | string[]; 67 | send: Send; 68 | } -------------------------------------------------------------------------------- /src/config/microspot.config.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 默认配置文件 4 | */ 5 | import { DefaultConfig } from './define'; 6 | import { getIndex } from './utils/default'; 7 | import { SpotType } from '../define'; 8 | import { gifReport } from '../utils/gifReport'; 9 | 10 | const config: DefaultConfig = { 11 | tracker: [ 12 | { 13 | type: SpotType.STABILITY, 14 | index: getIndex(SpotType.STABILITY) 15 | }, 16 | { 17 | type: SpotType.EXPERIENCE, 18 | index: getIndex(SpotType.EXPERIENCE) 19 | }, 20 | { 21 | type: SpotType.BUSINESS, 22 | index: getIndex(SpotType.BUSINESS) 23 | } 24 | ], 25 | lastEvent: true, 26 | send: (spot, option) => { 27 | if (Array.isArray(spot)) { 28 | gifReport('http://localhost:3000/dig', { bufferArray: spot }); 29 | } else { 30 | gifReport('http://localhost:3000/dig', spot); 31 | } 32 | } 33 | } 34 | 35 | export { 36 | config 37 | } -------------------------------------------------------------------------------- /src/config/utils/default.ts: -------------------------------------------------------------------------------- 1 | import { Index, DefaultIndexOption } from '../define'; 2 | import { SpotType } from '../../define'; 3 | 4 | import { StabilityType } from '../../microspot/stability/define'; 5 | import { ExperienceType } from '../../microspot/experience/define'; 6 | import { BusinessType } from '../../microspot/business/define'; 7 | 8 | export const StabilityIndex: Index = [ 9 | { type: StabilityType.JS_RUNTIME_ERROR, sampling: 1 }, 10 | { type: StabilityType.SCRIPT_LOAD_ERROR, sampling: 1 }, 11 | { type: StabilityType.CSS_LOAD_ERROR, sampling: 1 }, 12 | { type: StabilityType.IMAGE_LOAD_ERROR, sampling: 1 }, 13 | { type: StabilityType.AUDIO_LOAD_ERROR, sampling: 1 }, 14 | { type: StabilityType.VIDEO_LOAD_ERROR, sampling: 1 }, 15 | { type: StabilityType.PROMISE_REJECTION, sampling: 1 }, 16 | { type: StabilityType.BLANK_SCREEN, sampling: 1 }, 17 | { type: StabilityType.XHR, sampling: 1 }, 18 | { type: StabilityType.FETCH, sampling: 1 }, 19 | ] 20 | 21 | export const ExperienceIndex: Index = [ 22 | { type: ExperienceType.CUMULATIVE_LAYOUT_SHIFT, sampling: 1 }, 23 | { type: ExperienceType.FIRST_INPUT_DELAY, sampling: 1 }, 24 | { type: ExperienceType.LARGEST_CONTENTFUL_PAINT, sampling: 1 }, 25 | { type: ExperienceType.FIRST_PAINT, sampling: 1 }, 26 | { type: ExperienceType.FIRST_CONTENTFUL_PAINT, sampling: 1 }, 27 | { type: ExperienceType.TIMING, sampling: 1 } 28 | ] 29 | 30 | export const BusinessIndex: Index = [ 31 | { type: BusinessType.PAGE_VIEW, sampling: 1, routerMode: 'history' }, 32 | { type: BusinessType.LOG, sampling: 1 }, 33 | ] 34 | 35 | export const getIndex = (spotType: string | SpotType) => { 36 | let index: Index = []; 37 | switch (spotType) { 38 | case SpotType.STABILITY: 39 | index = StabilityIndex; 40 | break; 41 | case SpotType.EXPERIENCE: 42 | index = ExperienceIndex; 43 | break; 44 | case SpotType.BUSINESS: 45 | index = BusinessIndex; 46 | break; 47 | default: 48 | break; 49 | } 50 | return index as DefaultIndexOption[]; 51 | } -------------------------------------------------------------------------------- /src/config/utils/merge.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 合并用户配置和默认配置 3 | */ 4 | import { 5 | Config, 6 | Index, 7 | IndexOption, 8 | Tracker, 9 | TrackerOption, 10 | DefaultConfig, 11 | DefaultIndex, 12 | DefaultIndexOption, 13 | DefaultTracker, 14 | DefaultTrackerOption 15 | } from '../define'; 16 | 17 | import { getIndex } from '../utils/default'; 18 | import { SpotType } from '../../define'; 19 | import { types } from '@babel/core'; 20 | 21 | /** 合并指标 */ 22 | function mergeIndex(userIndex: Index | undefined, defaultIndex: DefaultIndex): DefaultIndex { 23 | /** 如果不传指标,则默认追踪所有指标 */ 24 | if (typeof userIndex === 'undefined') return defaultIndex; 25 | 26 | /** 如果传了,则进行合并 */ 27 | const index = userIndex.map(idx => { 28 | 29 | /** 如果指标为字符串,则转换为 Index 对象 */ 30 | if (typeof idx === 'string') { 31 | return { 32 | type: idx, 33 | sampling: 1 34 | } 35 | } 36 | 37 | /** 返回合并后的 Index 对象 */ 38 | return { 39 | type: idx.type, 40 | sampling: typeof idx.sampling === 'number' ? idx.sampling : 1, 41 | routerMode: idx.routerMode, 42 | buffer: idx.buffer, 43 | apiWhiteList: idx.apiWhiteList, 44 | statusList: idx.statusList, 45 | limitTime: idx.limitTime 46 | } 47 | }); 48 | 49 | return index as DefaultIndex; 50 | } 51 | 52 | /** 合并追踪器 */ 53 | function mergeTracker(userTracker: Tracker | undefined, defaultTracker: DefaultTracker): DefaultTracker { 54 | /** trakcer 未定义时,默认开启所有 trakcer */ 55 | if (typeof userTracker === 'undefined') return defaultTracker; 56 | 57 | /** 遍历用户 tracker,合并指标 */ 58 | const tracker = userTracker.map(tk => { 59 | if (typeof tk === 'string') { 60 | const spotType = tk as SpotType; 61 | return { 62 | type: spotType, 63 | index: getIndex(spotType) 64 | } 65 | } 66 | 67 | /** 返回合并后的 Tracker 对象 */ 68 | return { 69 | type: tk.type, 70 | index: mergeIndex(tk.index, getIndex(tk.type)) 71 | } 72 | }) 73 | 74 | return tracker as DefaultTrackerOption[]; 75 | } 76 | 77 | /** 合并配置项 */ 78 | function mergeConfig(userConfig: Config | undefined, defaultConfig: DefaultConfig): DefaultConfig { 79 | /** 如果用户配置不存在,则读取默认配置 */ 80 | if (typeof userConfig === 'undefined') return defaultConfig; 81 | 82 | /** 合并用户设置 tracker 和默认 tracker */ 83 | const tracker = mergeTracker(userConfig.tracker, defaultConfig.tracker); 84 | 85 | /** 合并发送函数 */ 86 | const send = userConfig.send ?? defaultConfig.send; 87 | 88 | /** 合并是否监听最后操作事件 */ 89 | const lastEvent = typeof userConfig.lastEvent === 'undefined' ? defaultConfig.lastEvent : userConfig.lastEvent 90 | 91 | /** 返回合并后的配置项 */ 92 | return { 93 | tracker, 94 | lastEvent, 95 | send 96 | } 97 | } 98 | 99 | export { 100 | mergeConfig 101 | } -------------------------------------------------------------------------------- /src/define.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTracker, DefaultIndex, Send } from './config/define'; 2 | 3 | /** 指标大类 */ 4 | export enum SpotType { 5 | /** 系统稳定性指标 */ 6 | STABILITY = 'STABILITY', 7 | /** 用户体验指标 */ 8 | EXPERIENCE = 'EXPERIENCE', 9 | /** 业务指标 */ 10 | BUSINESS = 'BUSINESS' 11 | } 12 | 13 | export interface Env { 14 | title: string, 15 | url: string, 16 | timestamp: number, 17 | userAgent: string; 18 | } 19 | 20 | /** 日志类型 */ 21 | export interface Spot { 22 | type: SpotType; 23 | env?: Env 24 | } 25 | 26 | export interface SpotOption { 27 | tracker: DefaultTracker, 28 | index: DefaultIndex, 29 | send: Send 30 | } -------------------------------------------------------------------------------- /src/entries/entry.es.ts: -------------------------------------------------------------------------------- 1 | export { Microspot } from '../index'; -------------------------------------------------------------------------------- /src/entries/entry.global.ts: -------------------------------------------------------------------------------- 1 | import { Microspot } from '../index'; 2 | const microspot = new Microspot(); 3 | (window as any).microspot = microspot; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Config, DefaultConfig, Send } from './config/define'; 2 | import { config as defaultConfig } from './config/microspot.config'; 3 | import { mergeConfig } from './config/utils/merge'; 4 | 5 | import { lastEvent } from './utils/findLastEvent'; 6 | import { sendHOC } from './utils/sendHOC'; 7 | import { injectTracker } from './microspot'; 8 | 9 | type SendParameters = Parameters; 10 | 11 | interface MicrospotIF { 12 | /** 全部配置信息 */ 13 | _config: Config | null; 14 | 15 | /** 配置选项 */ 16 | set(userConfig: Config): void; 17 | 18 | /** 启动监听 */ 19 | start(callback: (config: Config) => void): void; 20 | 21 | /** 用户手动调用埋点方法 */ 22 | send(...args: SendParameters): void; 23 | } 24 | 25 | class Microspot implements MicrospotIF { 26 | _config: DefaultConfig | null = null; 27 | 28 | constructor() { 29 | this._config = defaultConfig; 30 | } 31 | 32 | set(userConfig: Config): void { 33 | /** 合并配置文件 */ 34 | const config = mergeConfig(userConfig, defaultConfig); 35 | /** 包装发送函数 */ 36 | config.send = sendHOC(config.send); 37 | /** 设定全局配置文件 */ 38 | this._config = config; 39 | } 40 | 41 | start(callback?: (config: DefaultConfig) => void): void { 42 | if (!this._config) { 43 | throw new Error('请调用 set 方法设定配置文件'); 44 | } 45 | 46 | const { lastEvent: _lastEvent, tracker: _tracker, send: _send } = this._config; 47 | 48 | if(_lastEvent){ 49 | lastEvent.init(Array.isArray(_lastEvent) ? _lastEvent : undefined); 50 | } 51 | 52 | injectTracker({ 53 | tracker: _tracker, 54 | send: _send 55 | }); 56 | 57 | callback?.(this._config); 58 | } 59 | 60 | send(...args: SendParameters): void { 61 | if (!this._config) { 62 | throw new Error('请调用 set 方法设定配置文件'); 63 | } 64 | 65 | const { send: _send } = this._config; 66 | _send(...args); 67 | } 68 | } 69 | 70 | export { 71 | Microspot 72 | } -------------------------------------------------------------------------------- /src/microspot/business/define.ts: -------------------------------------------------------------------------------- 1 | import { Spot } from '../../define'; 2 | 3 | export enum BusinessType { 4 | PAGE_VIEW = 'PAGE_VIEW', 5 | LOG = 'LOG' 6 | } 7 | 8 | export interface BusinessSpot extends Spot { 9 | subType: BusinessType; 10 | } 11 | 12 | /** PV 上报结构体 */ 13 | export interface PageViewSpot extends BusinessSpot { 14 | href: string; 15 | } 16 | 17 | export interface LogSpot extends BusinessSpot { 18 | [keyword: string]: any; 19 | } -------------------------------------------------------------------------------- /src/microspot/business/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { DefaultIndex, Send } from '../../config/define'; 3 | import { injectPVTracker } from './tracker/pageView'; 4 | 5 | interface Props { 6 | index: DefaultIndex; 7 | send: Send; 8 | } 9 | 10 | function injectBusinessTracker(props: Props) { 11 | injectPVTracker.call(null, props); 12 | } 13 | 14 | export { 15 | injectBusinessTracker 16 | } -------------------------------------------------------------------------------- /src/microspot/business/tracker/pageView.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Page View 页面访问量 3 | */ 4 | 5 | import { SpotType, SpotOption } from '../../../define'; 6 | import { BusinessType, PageViewSpot } from '../define'; 7 | 8 | import { onLoad } from '../../../utils/onLoad'; 9 | import { _history } from '../../../utils/history'; 10 | 11 | function sendlocationSpot(config: Object, callback: Function) { 12 | const spot: PageViewSpot = { 13 | type: SpotType.BUSINESS, 14 | subType: BusinessType.PAGE_VIEW, 15 | href: location.href 16 | } 17 | callback(spot, config); 18 | } 19 | 20 | function injectPVTracker(props: Pick) { 21 | const { index, send } = props; 22 | const idx = index.find(idx => idx.type === BusinessType.PAGE_VIEW); 23 | if (!idx) return; 24 | 25 | onLoad(() => sendlocationSpot(idx, send)); 26 | 27 | if (idx.routerMode === 'hash') { 28 | window.addEventListener('hashchange', event => sendlocationSpot(idx, send)); 29 | } else { 30 | _history.addEventListener(() => sendlocationSpot(idx, send)); 31 | } 32 | } 33 | 34 | export { 35 | injectPVTracker 36 | } -------------------------------------------------------------------------------- /src/microspot/experience/define.ts: -------------------------------------------------------------------------------- 1 | import { Spot } from '../../define'; 2 | 3 | export enum ExperienceType { 4 | FIRST_PAINT = 'FIRST_PAINT', 5 | FIRST_CONTENTFUL_PAINT = 'FIRST_CONTENTFUL_PAINT', 6 | LARGEST_CONTENTFUL_PAINT = 'LARGEST_CONTENTFUL_PAINT', 7 | FIRST_INPUT_DELAY = 'FIRST_INPUT_DELAY', 8 | CUMULATIVE_LAYOUT_SHIFT = 'CUMULATIVE_LAYOUT_SHIFT', 9 | LONG_TASK = 'LONG_TASK', 10 | TIMING = 'TIMING' 11 | } 12 | 13 | export interface ExperienceSpot extends Spot { 14 | subType: ExperienceType; 15 | } 16 | 17 | /** FP 上报 */ 18 | export interface FirstPaintSpot extends ExperienceSpot { 19 | startTime: string; 20 | duration: string; 21 | } 22 | 23 | /** FCP 上报 */ 24 | export interface FirstContentfulPaintSpot extends ExperienceSpot { 25 | startTime: string; 26 | duration: string; 27 | } 28 | 29 | /** LCP 上报 */ 30 | export interface LargestContentfulPaintSpot extends ExperienceSpot { 31 | startTime: string; 32 | duration: string; 33 | selector: string; 34 | url: string; 35 | } 36 | 37 | /** FID 上报 */ 38 | export interface FirstInputDelaySpot extends ExperienceSpot { 39 | cancelable: string, 40 | processingStart: string, 41 | processingEnd: string, 42 | startTime: string; 43 | duration: string; 44 | } 45 | 46 | /** CLS 上报 */ 47 | export interface CumulativeLayoutShiftSpot extends ExperienceSpot { 48 | value: string; 49 | sources: string[]; 50 | } 51 | 52 | /** LT 上报 */ 53 | export interface LongTaskSpot extends ExperienceSpot { 54 | eventType: string; 55 | startTime: string; 56 | duration: string; 57 | selector: string; 58 | } 59 | 60 | 61 | /** 页面耗时上报 */ 62 | export interface TimingSpot extends ExperienceSpot { 63 | /** 性能元数据 */ 64 | raw: PerformanceNavigationTiming; 65 | /** 页面加载总耗时 */ 66 | loadTiming: string; 67 | /** DNS 解析耗时 */ 68 | dnsTiming: string; 69 | /** TCP 连接耗时 */ 70 | tcpTiming: string; 71 | /** SSL 连接耗时 */ 72 | sslTiming: string; 73 | /** 网路请求耗时 */ 74 | requestTiming: string; 75 | /** 数据请求耗时 */ 76 | responseTiming: string; 77 | /** DOM 解析耗时 */ 78 | domTiming: string; 79 | /** 资源加载耗时 */ 80 | resourceTiming: string; 81 | /** 首包耗时 */ 82 | firstPacketTiming: string; 83 | /** 页面渲染耗时 */ 84 | renderTiming: string; 85 | /** HTML 加载完时间 */ 86 | htmlTiming: string; 87 | /** 首次可交互时间 */ 88 | firstInteractiveTiming: string; 89 | } -------------------------------------------------------------------------------- /src/microspot/experience/index.ts: -------------------------------------------------------------------------------- 1 | import { SpotOption } from '../../define'; 2 | import { injectCoreTracker, injectCommonTracker } from './tracker'; 3 | 4 | function injectExperienceTracker(props: Pick) { 5 | /** 核心性能指标 */ 6 | injectCoreTracker.call(null, props); 7 | 8 | /** 其他一些指标 */ 9 | injectCommonTracker.call(null, props); 10 | } 11 | 12 | export { 13 | injectExperienceTracker 14 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/common/firstContentfulPaint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description FCP (First Contentful Paint) 首次内容绘制时间 3 | */ 4 | 5 | import { SpotType, SpotOption } from '../../../../define'; 6 | import { ExperienceType, FirstContentfulPaintSpot } from '../../define'; 7 | 8 | function injectFCPTracker(props: Pick) { 9 | const { index, send } = props; 10 | const idx = index.find(idx => idx.type === ExperienceType.FIRST_CONTENTFUL_PAINT); 11 | if (!idx) return; 12 | 13 | const observer = new PerformanceObserver((entries, observer) => { 14 | const firstContentPaint = entries.getEntriesByName('first-contentful-paint')[0]; 15 | const startTime = firstContentPaint.startTime; 16 | const duration = firstContentPaint.duration; 17 | 18 | const spot: FirstContentfulPaintSpot = { 19 | type: SpotType.EXPERIENCE, 20 | subType: ExperienceType.FIRST_CONTENTFUL_PAINT, 21 | startTime: `${startTime}`, 22 | duration: `${duration}`, 23 | } 24 | 25 | send(spot, idx); 26 | 27 | observer.disconnect(); 28 | }); 29 | 30 | observer.observe({ entryTypes: ['paint'] }); 31 | } 32 | 33 | export { 34 | injectFCPTracker 35 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/common/firstPaint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description FP (First Paint) 首次绘制时间 3 | */ 4 | 5 | import { SpotType, SpotOption } from '../../../../define'; 6 | import { ExperienceType, FirstPaintSpot } from '../../define'; 7 | 8 | function injectFPTracker(props: Pick) { 9 | const { index, send } = props; 10 | const idx = index.find(idx => idx.type === ExperienceType.FIRST_PAINT); 11 | if (!idx) return; 12 | 13 | const observer = new PerformanceObserver((entries, observer) => { 14 | const firstPaint = entries.getEntriesByName('first-paint')[0]; 15 | const startTime = firstPaint.startTime; 16 | const duration = firstPaint.duration; 17 | 18 | const spot: FirstPaintSpot = { 19 | type: SpotType.EXPERIENCE, 20 | subType: ExperienceType.FIRST_PAINT, 21 | startTime: `${startTime}`, 22 | duration: `${duration}`, 23 | } 24 | 25 | send(spot, idx); 26 | 27 | observer.disconnect(); 28 | }); 29 | 30 | observer.observe({ entryTypes: ['paint'] }); 31 | } 32 | 33 | export { 34 | injectFPTracker 35 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/common/index.ts: -------------------------------------------------------------------------------- 1 | import { SpotOption } from '../../../../define'; 2 | 3 | import { injectFPTracker } from './firstPaint'; 4 | import { injectFCPTracker } from './firstContentfulPaint'; 5 | import { injectLTTracker } from './longTask'; 6 | 7 | import { injectTimingTracker } from './timing'; 8 | import { injectMemoryTracker } from './memory'; 9 | 10 | function injectCommonTracker(props: Pick) { 11 | /** FP */ 12 | injectFPTracker.call(null, props); 13 | 14 | /** FCP */ 15 | injectFCPTracker.call(null, props); 16 | 17 | /** 长任务*/ 18 | injectLTTracker.call(null, props); 19 | 20 | /** 各个阶段耗时 */ 21 | injectTimingTracker.call(null, props); 22 | 23 | /** 注入内存追踪器 */ 24 | injectMemoryTracker.call(null, props); 25 | } 26 | 27 | export { 28 | injectCommonTracker 29 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/common/longTask.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description LongTask 长任务 3 | */ 4 | 5 | import { SpotType, SpotOption } from '../../../../define'; 6 | import { ExperienceType, LongTaskSpot } from '../../define'; 7 | 8 | import { lastEvent } from '../../../../utils/findLastEvent'; 9 | import { findSelector } from '../../../../utils/findSelector'; 10 | 11 | function injectLTTracker(props: Pick) { 12 | const { index, send } = props; 13 | const idx = index.find(idx => idx.type === ExperienceType.LONG_TASK); 14 | if (!idx) return; 15 | 16 | const limitTime = idx?.limitTime || 200; 17 | 18 | const observer = new PerformanceObserver((entries) => { 19 | const longTask = entries.getEntries()[0]; 20 | if (longTask.duration >= limitTime) { 21 | const lEvent = lastEvent.findLastEvent(); 22 | const startTime = longTask.startTime; 23 | const duration = longTask.duration; 24 | const spot: LongTaskSpot = { 25 | type: SpotType.EXPERIENCE, 26 | subType: ExperienceType.LONG_TASK, 27 | startTime: `${startTime}`, 28 | duration: `${duration}`, 29 | eventType: lEvent ? lEvent.type : '', 30 | selector: lEvent ? findSelector(lEvent.target as HTMLElement) : '' 31 | } 32 | 33 | send(spot, idx); 34 | } 35 | }); 36 | 37 | observer.observe({ entryTypes: ['longtask'] }) 38 | } 39 | 40 | export { 41 | injectLTTracker 42 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/common/memory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 监控内存使用情况 3 | * TODO 待开发 4 | */ 5 | 6 | import { SpotType, SpotOption } from '../../../../define'; 7 | 8 | function injectMemoryTracker(props: Pick) { 9 | //console.log(performance) 10 | } 11 | 12 | export { 13 | injectMemoryTracker 14 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/common/timing.ts: -------------------------------------------------------------------------------- 1 | import { SpotType, SpotOption } from '../../../../define'; 2 | import { ExperienceType, TimingSpot } from '../../define'; 3 | 4 | function injectTimingTracker(props: Pick) { 5 | const { index, send } = props; 6 | const idx = index.find(idx => idx.type === ExperienceType.TIMING); 7 | if (!idx) return; 8 | 9 | const observer = new PerformanceObserver((entries, observer) => { 10 | const navigationTiming = entries.getEntries()[0] as PerformanceNavigationTiming; 11 | 12 | const spot: TimingSpot = { 13 | type: SpotType.EXPERIENCE, 14 | subType: ExperienceType.TIMING, 15 | raw: navigationTiming, 16 | loadTiming: `${navigationTiming.loadEventEnd - navigationTiming.startTime}`, 17 | dnsTiming: `${navigationTiming.domainLookupEnd - navigationTiming.domainLookupStart}`, 18 | tcpTiming: `${navigationTiming.connectEnd - navigationTiming.connectStart}`, 19 | sslTiming: `${location.protocol === 'https:' ? navigationTiming.connectEnd - navigationTiming.secureConnectionStart : '0'}`, 20 | requestTiming: `${navigationTiming.responseStart - navigationTiming.requestStart}`, 21 | responseTiming: `${navigationTiming.responseEnd - navigationTiming.responseStart}`, 22 | domTiming: `${navigationTiming.domContentLoadedEventEnd - navigationTiming.responseEnd}`, 23 | resourceTiming: `${navigationTiming.loadEventEnd - navigationTiming.domContentLoadedEventEnd}`, 24 | firstPacketTiming: `${navigationTiming.responseStart - navigationTiming.startTime}`, 25 | renderTiming: `${navigationTiming.loadEventEnd - navigationTiming.responseEnd}`, 26 | htmlTiming: `${navigationTiming.responseEnd - navigationTiming.startTime}`, 27 | firstInteractiveTiming: `${navigationTiming.domInteractive - navigationTiming.startTime}`, 28 | } 29 | 30 | send(spot, idx); 31 | }); 32 | 33 | observer.observe({ entryTypes: ['navigation'] }); 34 | } 35 | 36 | export { 37 | injectTimingTracker 38 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/core/cumulativeLayoutShift.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description CLS (Cumulative Layout Shift) 累计布局位移 3 | */ 4 | 5 | import { SpotType, SpotOption } from '../../../../define'; 6 | import { ExperienceType, CumulativeLayoutShiftSpot } from '../../define'; 7 | 8 | import { findSelector } from '../../../../utils/findSelector'; 9 | 10 | interface CumulativeLayoutShift extends PerformanceEntry { 11 | hadRecentInput: boolean; 12 | sources: Array<{ node: Element | null }>; 13 | value: number; 14 | } 15 | 16 | function injectCLSTracker(props: Pick) { 17 | const { index, send } = props; 18 | const idx = index.find(idx => idx.type === ExperienceType.CUMULATIVE_LAYOUT_SHIFT); 19 | if (!idx) return; 20 | 21 | let cls = 0; 22 | const observer = new PerformanceObserver((entries, observer) => { 23 | const cumulativeLayoutShift = entries.getEntries()[0] as CumulativeLayoutShift; 24 | if (!cumulativeLayoutShift.hadRecentInput) { 25 | cls += cumulativeLayoutShift.value; 26 | 27 | const sources = cumulativeLayoutShift.sources 28 | .filter(source => source.node) 29 | .map(source => source.node ? findSelector(source.node as HTMLElement) : ''); 30 | const spot: CumulativeLayoutShiftSpot = { 31 | type: SpotType.EXPERIENCE, 32 | subType: ExperienceType.CUMULATIVE_LAYOUT_SHIFT, 33 | value: `${cls}`, 34 | sources: sources 35 | } 36 | 37 | send(spot, idx); 38 | } 39 | }); 40 | 41 | observer.observe({ type: 'layout-shift', buffered: true }); 42 | } 43 | 44 | export { 45 | injectCLSTracker 46 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/core/firstInputDelay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description FID (First Input Delay) 首次输入延迟 3 | */ 4 | 5 | import { SpotType, SpotOption } from '../../../../define'; 6 | import { ExperienceType, FirstInputDelaySpot } from '../../define'; 7 | 8 | interface FirstInputDelay extends PerformanceEntry { 9 | cancelable: boolean; 10 | processingStart: number; 11 | processingEnd: number; 12 | startTime: number; 13 | } 14 | 15 | function injectFIDTracker(props: Pick) { 16 | const { index, send } = props; 17 | const idx = index.find(idx => idx.type === ExperienceType.FIRST_INPUT_DELAY); 18 | if(!idx) return; 19 | 20 | const observer = new PerformanceObserver((entries, observer) => { 21 | const firstInputDelay = entries.getEntries()[0] as FirstInputDelay; 22 | const cancelable = firstInputDelay.cancelable; 23 | const processingStart = firstInputDelay.processingStart; 24 | const processingEnd = firstInputDelay.processingEnd; 25 | const startTime = firstInputDelay.startTime; 26 | const duration = firstInputDelay.duration; 27 | 28 | const spot: FirstInputDelaySpot = { 29 | type: SpotType.EXPERIENCE, 30 | subType: ExperienceType.FIRST_INPUT_DELAY, 31 | cancelable: `${cancelable}`, 32 | processingStart: `${processingStart}`, 33 | processingEnd: `${processingEnd}`, 34 | startTime: `${startTime}`, 35 | duration: `${duration}`, 36 | } 37 | 38 | send(spot, idx); 39 | 40 | observer.disconnect(); 41 | }); 42 | 43 | observer.observe({ entryTypes: ['first-input'] }); 44 | } 45 | 46 | export { 47 | injectFIDTracker 48 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/core/index.ts: -------------------------------------------------------------------------------- 1 | import { SpotOption } from '../../../../define'; 2 | 3 | import { injectLCPTracker } from './largestContentfulPaint'; 4 | import { injectFIDTracker } from './firstInputDelay'; 5 | import { injectCLSTracker } from './cumulativeLayoutShift'; 6 | 7 | /** 8 | * 核心性能指标追踪器 (LCP, FID, CLS) 9 | */ 10 | 11 | function injectCoreTracker(props: Pick) { 12 | injectLCPTracker.call(null, props); 13 | injectFIDTracker.call(null, props); 14 | injectCLSTracker.call(null, props); 15 | } 16 | 17 | export { 18 | injectCoreTracker 19 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/core/largestContentfulPaint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description LCP (Largest Contentful Paint) 最大内容绘制 3 | * @tips 取代 FMP(First Meaningful Paint) 4 | */ 5 | 6 | import { SpotType, SpotOption } from '../../../../define'; 7 | import { ExperienceType, LargestContentfulPaintSpot } from '../../define'; 8 | import { findSelector } from '../../../../utils/findSelector'; 9 | 10 | interface LargestContentfulPaint extends PerformanceEntry { 11 | element: HTMLElement; 12 | url: string; 13 | } 14 | 15 | function injectLCPTracker(props: Pick) { 16 | const { index, send } = props; 17 | const idx = index.find(idx => idx.type === ExperienceType.LARGEST_CONTENTFUL_PAINT); 18 | if(!idx) return; 19 | 20 | const observer = new PerformanceObserver((entries, observer) => { 21 | const largestContentfulPaint = entries.getEntries()[0] as LargestContentfulPaint; 22 | const element = largestContentfulPaint.element; 23 | const startTime = largestContentfulPaint.startTime; 24 | const url = largestContentfulPaint.url; 25 | const duration = largestContentfulPaint.duration; 26 | 27 | const spot: LargestContentfulPaintSpot = { 28 | type: SpotType.EXPERIENCE, 29 | subType: ExperienceType.LARGEST_CONTENTFUL_PAINT, 30 | startTime: `${startTime}`, 31 | duration: `${duration}`, 32 | selector: element ? findSelector(element) : '', 33 | url, 34 | } 35 | send(spot, idx); 36 | }); 37 | 38 | observer.observe({ entryTypes: ['largest-contentful-paint'] }); 39 | } 40 | 41 | export { 42 | injectLCPTracker 43 | } -------------------------------------------------------------------------------- /src/microspot/experience/tracker/index.ts: -------------------------------------------------------------------------------- 1 | export { injectCoreTracker } from './core'; 2 | export { injectCommonTracker } from './common'; -------------------------------------------------------------------------------- /src/microspot/index.ts: -------------------------------------------------------------------------------- 1 | import { SpotType, SpotOption } from '../define'; 2 | 3 | import { injectStabilityTracker } from './stability'; 4 | import { injectExperienceTracker } from './experience'; 5 | import { injectBusinessTracker } from './business'; 6 | 7 | 8 | function injectTracker(props: Pick) { 9 | const { tracker, send } = props; 10 | 11 | const stabilityTracker = tracker.find(tk => tk.type === SpotType.STABILITY); 12 | const experienceTracker = tracker.find(tk => tk.type === SpotType.EXPERIENCE); 13 | const businessTracker = tracker.find(tk => tk.type === SpotType.BUSINESS); 14 | 15 | /** 稳定性监控 */ 16 | stabilityTracker && injectStabilityTracker({ index: stabilityTracker.index, send }); 17 | /** 体验监控 */ 18 | experienceTracker && injectExperienceTracker({ index: experienceTracker.index, send }); 19 | /** 业务监控 */ 20 | businessTracker && injectBusinessTracker({ index: businessTracker.index, send }); 21 | } 22 | 23 | export { 24 | injectTracker 25 | } -------------------------------------------------------------------------------- /src/microspot/stability/define.ts: -------------------------------------------------------------------------------- 1 | import { Spot } from "../../define"; 2 | import { Stack } from '../../utils/resolveStack'; 3 | 4 | /** error 类型 */ 5 | export enum StabilityType { 6 | /** js 运行时错误 */ 7 | JS_RUNTIME_ERROR = 'JS_RUNTIME_ERROR', 8 | 9 | /** 脚本加载错误 */ 10 | SCRIPT_LOAD_ERROR = 'SCRIPT_LOAD_ERROR', 11 | 12 | /** 样式加载错误 */ 13 | CSS_LOAD_ERROR = 'CSS_LOAD_ERROR', 14 | 15 | /** 图片加载错误 */ 16 | IMAGE_LOAD_ERROR = 'IMAGE_LOAD_ERROR', 17 | 18 | /** 音频加载错误 */ 19 | AUDIO_LOAD_ERROR = 'AUDIO_LOAD_ERROR', 20 | 21 | /** 视频加载错误 */ 22 | VIDEO_LOAD_ERROR = 'VIDEO_LOAD_ERROR', 23 | 24 | /** Promise 拒绝状态未捕获 */ 25 | PROMISE_REJECTION = 'PROMISE_REJECTION', 26 | 27 | /** 白屏 */ 28 | BLANK_SCREEN = 'BLANK_SCREEN', 29 | 30 | /** Ajax */ 31 | XHR = 'XHR', 32 | 33 | /** Fetch */ 34 | FETCH = 'FETCH' 35 | } 36 | 37 | export interface StabilitySpot extends Spot { 38 | subType: StabilityType; 39 | } 40 | 41 | /** 运行时错误上报格式 */ 42 | export interface RuntimeErrorSpot extends StabilitySpot { 43 | filename: string; 44 | message: string; 45 | position: string; 46 | stack: Stack[]; 47 | selector: string; 48 | } 49 | 50 | /** 资源加载错误上报格式 */ 51 | export interface ResourceLoadErrorSpot extends StabilitySpot { 52 | filename: string; 53 | tagName: string; 54 | selector: string; 55 | } 56 | 57 | /** 捕获拒绝未处理的 Promise */ 58 | export interface PromiseRejectionSpot extends StabilitySpot { 59 | message: string; 60 | filename: string; 61 | position: string; 62 | stack: Stack[], 63 | selector: string; 64 | } 65 | 66 | /** 白屏上报格式 */ 67 | export interface BlankScreenSpot extends StabilitySpot { 68 | emptyPoints: string; 69 | screen: string; 70 | viewPoint: string; 71 | selector: string[]; 72 | } 73 | 74 | /** Ajax 请求上报 */ 75 | export interface XHRSpot extends StabilitySpot { 76 | eventType: string; 77 | pathname: string; 78 | status: string; 79 | statusText: string; 80 | duration: string; 81 | response: string; 82 | params: string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData; 83 | } 84 | 85 | /** Fetch 请求上报 */ 86 | export interface FetchSpot extends StabilitySpot { 87 | pathname: string; 88 | status: string; 89 | statusText: string; 90 | contentType: string; 91 | duration: string; 92 | } -------------------------------------------------------------------------------- /src/microspot/stability/index.ts: -------------------------------------------------------------------------------- 1 | import { SpotOption } from '../../define'; 2 | import { StabilityType } from './define'; 3 | import { 4 | injectErrorTracker, 5 | injectPromiseTracker, 6 | injectBlankScreenTracker, 7 | injectXHRTracker, 8 | injectFetchTracker, 9 | } from './tracker'; 10 | 11 | /** 注入稳定性追踪器 */ 12 | function injectStabilityTracker(props: Pick) { 13 | /** 注入 Error 追踪器 */ 14 | injectErrorTracker.call(null, props); 15 | 16 | /** 注入 Promise 追踪器 */ 17 | injectPromiseTracker.call(null, props); 18 | 19 | /** 注入白屏追踪器 */ 20 | injectBlankScreenTracker.call(null, props); 21 | 22 | /** 注入 xhr 追踪器 */ 23 | injectXHRTracker.call(null, props); 24 | 25 | /** 注入 fetch 追踪器 */ 26 | injectFetchTracker.call(null, props); 27 | } 28 | 29 | export { 30 | injectStabilityTracker 31 | } -------------------------------------------------------------------------------- /src/microspot/stability/tracker/__tests__/error.spec.ts: -------------------------------------------------------------------------------- 1 | import { injectErrorTracker } from '../error'; 2 | 3 | describe('测试错误追踪器', () => { 4 | it('抛出一个错误,被错误追踪器捕获', ()=>{ 5 | expect(1 + 1).toEqual(2); 6 | }); 7 | }); -------------------------------------------------------------------------------- /src/microspot/stability/tracker/blankScreen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ruimve 3 | * @description 监听白屏错误 4 | */ 5 | 6 | import { SpotType, SpotOption } from '../../../define'; 7 | import { StabilityType, BlankScreenSpot } from '../define'; 8 | 9 | import { onLoad } from '../../../utils/onLoad'; 10 | 11 | /** 获取元素的所有选择器 */ 12 | function getSelector(element: Element) { 13 | const selectors: string[] = []; 14 | 15 | /** id 选择器 */ 16 | if (element.id) { 17 | selectors.push(`#${element.id}`); 18 | } 19 | 20 | /** 类选择器 */ 21 | if (element.className) { 22 | selectors.push(`.${element.className.split(' ').filter(Boolean).join('.')}`); 23 | } 24 | 25 | /** 标签选择器 */ 26 | if (element.nodeName) { 27 | selectors.push(element.nodeName.toLowerCase()); 28 | } 29 | 30 | return selectors; 31 | } 32 | 33 | /** 判断元素为 wrapper 元素 */ 34 | function isWrapper(element: Element, wrapper: string[]): boolean { 35 | /** 如果元素不存在,判断为 wrapper 元素 */ 36 | if (!element) return true; 37 | 38 | /** 如果元素在 wrapper 名单中,则为 wrapper 元素 */ 39 | const selector = getSelector(element); 40 | if (wrapper.some(w => selector.includes(w))) { 41 | return true; 42 | } 43 | 44 | /** 其他情况不是 wrapper 元素 */ 45 | return false; 46 | } 47 | 48 | function blankScreen(props: Pick) { 49 | const { index, send } = props; 50 | const idx = index.find(idx => idx.type === StabilityType.BLANK_SCREEN); 51 | if (!idx) return; 52 | 53 | const wrapper = ['html', 'body', '.div1.div2.div3']; 54 | let emptyPoints = 0; 55 | 56 | /** 将 window 可视区域平分为 4 个象限, 在边界上取18个点 */ 57 | for (let i = 1; i <= 9; i++) { 58 | const xElement = document.elementsFromPoint(window.innerWidth / 10 * i, window.innerHeight / 2); 59 | const yElement = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 10 * i); 60 | 61 | isWrapper(xElement[0], wrapper) && emptyPoints++; 62 | isWrapper(yElement[0], wrapper) && emptyPoints++; 63 | } 64 | 65 | /** 如果空点数超过 18,则判定为白屏 */ 66 | if (emptyPoints >= 18) { 67 | const centerElements = document.elementsFromPoint( 68 | window.innerWidth / 2, window.innerHeight / 2 69 | ); 70 | const spot: BlankScreenSpot = { 71 | type: SpotType.STABILITY, 72 | subType: StabilityType.BLANK_SCREEN, 73 | emptyPoints: `${emptyPoints}`, 74 | screen: `${window.screen.width}:${window.screen.height}`, 75 | viewPoint: `${window.innerWidth}:${window.innerHeight}`, 76 | selector: getSelector(centerElements[0]) 77 | }; 78 | send(spot, idx); 79 | } 80 | } 81 | 82 | function blankScreenHOC(props: Pick) { 83 | return () => blankScreen(props); 84 | } 85 | 86 | function injectBlankScreenTracker(props: Pick) { 87 | onLoad(blankScreenHOC(props)); 88 | } 89 | 90 | export { 91 | injectBlankScreenTracker 92 | } -------------------------------------------------------------------------------- /src/microspot/stability/tracker/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ruimve 3 | * @description 监听 js 运行时错误和资源加载错误 4 | */ 5 | 6 | import { SpotType, SpotOption } from '../../../define'; 7 | import { StabilityType, RuntimeErrorSpot, ResourceLoadErrorSpot } from '../define'; 8 | 9 | import { findSelector } from '../../../utils/findSelector'; 10 | import { lastEvent } from '../../../utils/findLastEvent'; 11 | import { resolveStack } from '../../../utils/resolveStack'; 12 | 13 | /** 资源类型对应资源加载错误 */ 14 | const ResourceErrorMap = { 15 | SCRIPT: StabilityType.SCRIPT_LOAD_ERROR, 16 | LINK: StabilityType.CSS_LOAD_ERROR, 17 | IMG: StabilityType.IMAGE_LOAD_ERROR, 18 | AUDIO: StabilityType.AUDIO_LOAD_ERROR, 19 | VIDEO: StabilityType.VIDEO_LOAD_ERROR 20 | } 21 | 22 | /** 资源标签 */ 23 | type ResourceType = keyof (typeof ResourceErrorMap); 24 | /** 资源元素 */ 25 | type ResourceElement = HTMLScriptElement & HTMLLinkElement & HTMLImageElement & HTMLAudioElement & HTMLVideoElement; 26 | 27 | /** 生成资源加载错误日志 */ 28 | function formatResourceLoadError(event: Event): ResourceLoadErrorSpot { 29 | const node = event.target as ResourceElement; 30 | const nodeName = node.nodeName.toUpperCase() as ResourceType; 31 | const spot: ResourceLoadErrorSpot = { 32 | type: SpotType.STABILITY, 33 | subType: ResourceErrorMap[nodeName], 34 | filename: node.src || node.href, 35 | tagName: nodeName.toLowerCase(), 36 | selector: event.target ? findSelector(event.target as HTMLElement) : '' 37 | }; 38 | return spot; 39 | } 40 | 41 | /** 生成 JS 运行时错误日志 */ 42 | function formatRuntimeError(event: ErrorEvent): RuntimeErrorSpot { 43 | const lEvent = lastEvent.findLastEvent(); 44 | const spot: RuntimeErrorSpot = { 45 | type: SpotType.STABILITY, 46 | subType: StabilityType.JS_RUNTIME_ERROR, 47 | filename: event.filename, 48 | message: event.message, 49 | position: `${event.lineno}:${event.colno}`, 50 | stack: resolveStack(event.error.stack), 51 | selector: lEvent?.target ? findSelector(lEvent?.target as HTMLElement) : '' 52 | }; 53 | 54 | return spot; 55 | } 56 | 57 | /** 注入错误跟踪器的函数 */ 58 | function injectErrorTracker(props: Pick) { 59 | const { index, send } = props; 60 | 61 | window.addEventListener('error', (event) => { 62 | /** 获取事件的构造函数 */ 63 | const constructor = event.constructor; 64 | 65 | /** 继承自 Event 为资源加载错误 */ 66 | if (constructor === Event) { 67 | const spot = formatResourceLoadError(event); 68 | const idx = index.find(idx => idx.type === spot.subType); 69 | idx && send(spot, idx); 70 | } 71 | 72 | /** 继承自 ErrorEvent 为运行时错误 */ 73 | if (constructor === ErrorEvent) { 74 | const spot = formatRuntimeError(event); 75 | const idx = index.find(idx => idx.type === spot.subType); 76 | idx && send(spot, idx); 77 | } 78 | 79 | }, true); 80 | } 81 | 82 | export { 83 | injectErrorTracker 84 | } -------------------------------------------------------------------------------- /src/microspot/stability/tracker/fetch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ruimve 3 | * @description 监听 fetch 请求 4 | */ 5 | 6 | import { SpotType, SpotOption } from '../../../define'; 7 | import { StabilityType, FetchSpot } from '../define'; 8 | 9 | type FetchParameters = Parameters; 10 | 11 | const isWhiteListMatched = (url: string, whiteList: string[]) => { 12 | return whiteList.some(white => { 13 | const reg = new RegExp(white); 14 | return reg.test(url); 15 | }) 16 | } 17 | 18 | const isStatusMatched = (status: number, statusList: number[]) => { 19 | return statusList.includes(status); 20 | } 21 | 22 | function injectFetchTracker(props: Pick) { 23 | const { index, send } = props; 24 | const idx = index.find(idx => idx.type === StabilityType.FETCH); 25 | if (!idx) return; 26 | 27 | const whiteList = idx?.apiWhiteList || []; 28 | const statusList = idx?.statusList || []; 29 | 30 | const originalFetch = window.fetch; 31 | 32 | window.fetch = function (...args: FetchParameters) { 33 | const startTime = Date.now(); 34 | return originalFetch.apply(this, args).then(res => { 35 | if (isWhiteListMatched(res.url, whiteList)) return res; 36 | if (!isStatusMatched(res.status, statusList)) return res; 37 | 38 | const contentType = res.headers.get('Content-Type'); 39 | 40 | const typeMatched = contentType?.match(/^application\/(.+);.*/); 41 | const type = typeMatched?.[1]; 42 | 43 | const duration = Date.now() - startTime; 44 | const spot: FetchSpot = { 45 | type: SpotType.STABILITY, 46 | subType: StabilityType.FETCH, 47 | pathname: res.url, 48 | status: `${res.status}`, 49 | statusText: res.statusText, 50 | contentType: type || 'text', 51 | duration: `${duration}` 52 | }; 53 | send(spot, idx); 54 | return res; 55 | }) 56 | } 57 | } 58 | 59 | export { 60 | injectFetchTracker 61 | } -------------------------------------------------------------------------------- /src/microspot/stability/tracker/index.ts: -------------------------------------------------------------------------------- 1 | export { injectErrorTracker } from './error'; 2 | export { injectPromiseTracker } from './promiseRejection'; 3 | export { injectBlankScreenTracker } from './blankScreen'; 4 | export { injectXHRTracker } from './xhr'; 5 | export { injectFetchTracker } from './fetch'; -------------------------------------------------------------------------------- /src/microspot/stability/tracker/promiseRejection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ruimve 3 | * @description 监听 Promise 未处理事件 4 | */ 5 | 6 | import { SpotType, SpotOption } from '../../../define'; 7 | import { StabilityType, PromiseRejectionSpot } from '../define'; 8 | 9 | import { findSelector } from '../../../utils/findSelector'; 10 | import { lastEvent } from '../../../utils/findLastEvent'; 11 | import { resolveStack, Stack } from '../../../utils/resolveStack'; 12 | 13 | function formatPromise(event: PromiseRejectionEvent) { 14 | 15 | const lEvent = lastEvent.findLastEvent(); 16 | const reason = event.reason; 17 | 18 | let message: string = ''; 19 | let filename: string = ''; 20 | let position: string = ''; 21 | let stack: Stack[] = []; 22 | 23 | /** 代码报错抛出错误 */ 24 | if (typeof reason === 'object' && reason.message && reason.stack) { 25 | message = reason.message; 26 | stack = resolveStack(reason.stack); 27 | filename = stack?.[0]?.filename || ''; 28 | position = `${stack?.[0]?.lineno}:${stack?.[0]?.colno}`; 29 | } else { /** 手动使用 reject 抛出错误 */ 30 | message = reason; 31 | } 32 | 33 | const spot: PromiseRejectionSpot = { 34 | type: SpotType.STABILITY, 35 | subType: StabilityType.PROMISE_REJECTION, 36 | message, 37 | filename, 38 | position, 39 | stack, 40 | selector: lEvent ? findSelector(lEvent?.target as HTMLElement) : '' 41 | } 42 | 43 | return spot; 44 | } 45 | 46 | function injectPromiseTracker(props: Pick) { 47 | const { index, send } = props; 48 | const idx = index.find(idx => idx.type === StabilityType.PROMISE_REJECTION); 49 | if(!idx) return; 50 | 51 | window.addEventListener('unhandledrejection', event => { 52 | const spot = formatPromise(event); 53 | send(spot, idx); 54 | }, true); 55 | 56 | } 57 | 58 | export { 59 | injectPromiseTracker 60 | } -------------------------------------------------------------------------------- /src/microspot/stability/tracker/xhr.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ruimve 3 | * @description 监听 Ajax 请求 4 | */ 5 | 6 | import { SpotType, SpotOption } from '../../../define'; 7 | import { StabilityType, XHRSpot } from '../define'; 8 | 9 | /** 获取 open 的参数类型 */ 10 | type OpenParameters = Parameters; 11 | type OpenParametersOverload = OpenParameters | [method: string, url: string | URL]; 12 | 13 | /** 获取 send 的参数类型 */ 14 | type SendParameters = Parameters; 15 | 16 | const isWhiteListMatched = (url: string, whiteList: string[]) => { 17 | return whiteList.some(white => { 18 | const reg = new RegExp(white); 19 | return reg.test(url); 20 | }) 21 | } 22 | 23 | const isStatusMatched = (status: number, statusList: number[]) => { 24 | return statusList.includes(status); 25 | } 26 | 27 | function injectXHRTracker(props: Pick) { 28 | const { index, send } = props; 29 | const idx = index.find(idx => idx.type === StabilityType.XHR); 30 | if (!idx) return; 31 | 32 | const XMLHttpRequest = window.XMLHttpRequest; 33 | const originalOpen = XMLHttpRequest.prototype.open; 34 | const originalSend = XMLHttpRequest.prototype.send; 35 | 36 | const whiteList = idx?.apiWhiteList || []; 37 | const statusList = idx?.statusList || []; 38 | 39 | XMLHttpRequest.prototype.open = function (...args: OpenParametersOverload) { 40 | const [method, url, async] = args; 41 | if (typeof url === 'string') { 42 | this.spotData = { method, url, async }; 43 | } 44 | //@ts-ignore 45 | return originalOpen.apply(this, args) 46 | } 47 | 48 | XMLHttpRequest.prototype.send = function (...args: SendParameters) { 49 | const [body] = args; 50 | if (this.spotData) { 51 | const startTime = Date.now(); //在发送之前记录下开始的时间 52 | const hander = (type: 'load' | 'error' | 'abort') => (event: XMLHttpRequest) => { 53 | if (isWhiteListMatched(this.spotData.url, whiteList)) return; 54 | if (!isStatusMatched(this.status, statusList)) return; 55 | 56 | const duration = Date.now() - startTime; 57 | const status = this.status; 58 | const statusText = this.statusText; 59 | 60 | const spot: XHRSpot = { 61 | type: SpotType.STABILITY, 62 | subType: StabilityType.XHR, 63 | eventType: type, 64 | pathname: this.spotData.url, 65 | status: `${status}`, 66 | statusText: statusText, 67 | duration: `${duration}`, 68 | response: this.response ? JSON.stringify(this.response) : '', 69 | params: body || '' 70 | }; 71 | 72 | send(spot, idx); 73 | } 74 | 75 | this.addEventListener('load', hander('load'), false); 76 | this.addEventListener('error', hander('error'), false); 77 | this.addEventListener('abort', hander('abort'), false); 78 | } 79 | 80 | //@ts-ignore 81 | return originalSend.apply(this, args); 82 | } 83 | } 84 | 85 | export { 86 | injectXHRTracker 87 | } -------------------------------------------------------------------------------- /src/utils/findLastEvent.ts: -------------------------------------------------------------------------------- 1 | 2 | const DEFAULT_EVENT = [ 3 | 'click', 4 | 'touchcancel', 'touchend', 'touchmove', 'touchstart', 5 | 'keydown', 'keypress', 'keyup', 6 | 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup' 7 | ] 8 | 9 | interface LastEventInterface { 10 | /** 最后触发事件 */ 11 | lastEvent: Event | null; 12 | /** 是否进行初始化 */ 13 | isInitialization: boolean; 14 | /** 初始化函数 */ 15 | init(event: string[]): void; 16 | /** 获取最后触发事件的函数 */ 17 | findLastEvent(): void; 18 | } 19 | 20 | class LastEvent implements LastEventInterface { 21 | 22 | lastEvent: Event | null; 23 | 24 | isInitialization: boolean; 25 | 26 | constructor() { 27 | this.lastEvent = null; 28 | this.isInitialization = false; 29 | } 30 | 31 | init(event: string[] = DEFAULT_EVENT): void { 32 | this.isInitialization = true; 33 | event.forEach(eventType => { 34 | window.addEventListener(eventType, event => { 35 | this.lastEvent = event; 36 | }, { 37 | capture: true, 38 | passive: true 39 | }); 40 | }); 41 | } 42 | 43 | findLastEvent(): Event | null { 44 | if (!this.isInitialization) { 45 | console.warn('无法获取 selector,可能是由于配置 lastEvent 为 false'); 46 | } 47 | return this.lastEvent; 48 | } 49 | } 50 | 51 | /** 返回一个获取最后事件的函数 */ 52 | const lastEvent = new LastEvent(); 53 | 54 | export { 55 | lastEvent 56 | } -------------------------------------------------------------------------------- /src/utils/findSelector.ts: -------------------------------------------------------------------------------- 1 | /** 生成选择器字符串 */ 2 | function generateSelector(path: Array) { 3 | /** 使元素路径从 html 到 目标元素排序 */ 4 | const pathSort = path.reverse(); 5 | 6 | /** 过滤掉 window 和 document */ 7 | const pathFilter = pathSort.filter(element => element !== window && element !== document) as Array; 8 | 9 | /** 选择器 */ 10 | const selector = pathFilter.reduce((prv, cur) => { 11 | const nodeName = cur.nodeName.toLowerCase(); 12 | if (cur.id) { 13 | return `${prv} ${nodeName}#${cur.id}`; 14 | } 15 | 16 | if (cur.className && typeof cur.className === 'string') { 17 | return `${prv} ${nodeName}.${cur.id}`; 18 | } 19 | 20 | return `${prv} ${nodeName}`; 21 | }, ''); 22 | 23 | /** 修剪掉头尾的空白符 */ 24 | return selector.trim(); 25 | } 26 | 27 | /** 向上记录父元素 */ 28 | function findSelector(target: HTMLElement | null) { 29 | const path = []; 30 | while (target) { 31 | path.push(target); 32 | target = target.parentElement; 33 | } 34 | return generateSelector(path); 35 | } 36 | 37 | export { 38 | findSelector 39 | } -------------------------------------------------------------------------------- /src/utils/gifReport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 使用 1 * 1 像素的 gif 实现埋点上传 3 | */ 4 | 5 | type Init = Record; 6 | 7 | function querystring(params: Init) { 8 | if (!params) return ''; 9 | const qs = Object.keys(params).reduce( 10 | (prv, cur) => prv 11 | ? `${prv}&${cur}=${encodeURIComponent(JSON.stringify(params[cur]))}` 12 | : `${cur}=${encodeURIComponent(JSON.stringify(params[cur]))}` 13 | , ''); 14 | return qs; 15 | } 16 | 17 | /** 18 | * 19 | * @param input http://localhost:3000/dig 20 | * @param option 21 | */ 22 | function gifReport(input: string | URL, init: Init) { 23 | const qs = querystring(init); 24 | const image = new Image(); 25 | if (typeof input === 'string') { 26 | image.src = `${input}?${qs}`; 27 | } else { 28 | image.src = `${input.href}?${qs}`; 29 | } 30 | } 31 | 32 | /** 33 | * @description 优化上传时机 34 | * @param callback 回调函数 35 | * @returns 36 | */ 37 | function gifReportHOC(callback: typeof gifReport) { 38 | return (input: string | URL, init: Init) => { 39 | requestIdleCallback(() => callback(input, init)) 40 | } 41 | } 42 | 43 | const HOC = gifReportHOC(gifReport); 44 | 45 | export { 46 | HOC as gifReport 47 | } -------------------------------------------------------------------------------- /src/utils/history.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * history 路由监听 3 | */ 4 | type Listener = () => void; 5 | type Method = 'back' | 'forward' | 'go' | 'pushState' | 'replaceState'; 6 | 7 | class HistoryListener { 8 | events: Set = new Set(); 9 | 10 | constructor() { 11 | const methods: Method[] = [/* 'back', 'forward', 'go', */ 'pushState', 'replaceState']; 12 | methods.forEach((name) => { 13 | this.registListener(name); 14 | }); 15 | } 16 | 17 | emit() { 18 | this.events.forEach((listener: Listener) => listener()); 19 | } 20 | 21 | registListener(name: Method) { 22 | const originalMethod = history[name]; 23 | const _this = this; 24 | 25 | history[name] = function (...args: any[]) { 26 | //@ts-ignore 27 | originalMethod.apply(history, args); 28 | _this.emit(); 29 | } 30 | } 31 | 32 | addEventListener(listener: Listener) { 33 | this.events.add(listener); 34 | window.addEventListener('popstate', listener, false); 35 | } 36 | 37 | removeEventListener(listener: Listener) { 38 | if (this.events.has(listener)) { 39 | this.events.delete(listener); 40 | window.removeEventListener('popstate', listener); 41 | } 42 | } 43 | } 44 | 45 | export const _history = (() => { 46 | let history: HistoryListener; 47 | return () => history = history ? history : new HistoryListener 48 | })()(); -------------------------------------------------------------------------------- /src/utils/onLoad.ts: -------------------------------------------------------------------------------- 1 | function onLoad(callback: Function) { 2 | /** document 和 资源全部加载完毕 readyState 为 complete,直接执行回调 */ 3 | if (document.readyState === 'complete') { 4 | callback(); 5 | } else { /** 等全部加载完毕,再执行回调 */ 6 | window.addEventListener('load', (...args) => callback.call(null, ...args)); 7 | } 8 | } 9 | 10 | export { 11 | onLoad 12 | } -------------------------------------------------------------------------------- /src/utils/resolveStack.ts: -------------------------------------------------------------------------------- 1 | export interface Stack { 2 | at: string; 3 | scope: string; 4 | filename: string; 5 | lineno: string; 6 | colno: string; 7 | } 8 | 9 | /** 匹配栈数据 */ 10 | function matchStack(at: string) { 11 | /** 报错在函数作用域中, 如 HTMLButtonElement.onclick (http://127.0.0.1:5500/public/index.html:11:75) */ 12 | const regFunc = /(.+)\s+\((.+):(\d+):(\d+)\)/; 13 | /** 报错在全局作用域中, 如 http://127.0.0.1:5500/public/index.html:11:75*/ 14 | const regGlobal = /(.+):(\d+):(\d+)/; 15 | 16 | const regFuncResult = at.match(regFunc); 17 | if (Array.isArray(regFuncResult)) { 18 | const [, scope, filename, lineno, colno] = regFuncResult; 19 | return { 20 | scope, 21 | filename, 22 | lineno, 23 | colno 24 | } 25 | } else { 26 | const regGlobalResult = at.match(regGlobal) || []; 27 | const [, filename, lineno, colno] = regGlobalResult; 28 | return { 29 | scope: 'global', 30 | filename, 31 | lineno, 32 | colno 33 | } 34 | } 35 | } 36 | 37 | function resolveStack(stack: string) { 38 | const ats = stack.split('\n').slice(1).map(at => at.replace(/^\s+at\s+/g, '')); 39 | const lines = ats.map(at => { 40 | const line = matchStack(at); 41 | return { 42 | at, 43 | ...line 44 | }; 45 | }); 46 | return lines; 47 | } 48 | 49 | export { 50 | resolveStack 51 | } -------------------------------------------------------------------------------- /src/utils/sendHOC/buffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 缓冲发送 3 | */ 4 | import { Send, SendSpot, DefaultIndexType } from '../../config/define'; 5 | 6 | function buffer(send: Send): Send { 7 | /** 暂存埋点信息 */ 8 | const bufferMap = new Map(); 9 | /** 返回发送函数 */ 10 | return (spot, options) => { 11 | const { type, buffer } = options; 12 | /** 任意配置大于 0 的 buffer,表示开启 */ 13 | if (typeof buffer === 'number' && buffer > 0) { 14 | /** 如果缓存已存在 */ 15 | if (bufferMap.has(type)) { 16 | /** 获取缓存值 */ 17 | const bu = bufferMap.get(type) || []; 18 | /** 如果缓存没满则继续插入 */ 19 | if (bu.length < buffer) { 20 | bufferMap.set(type, bu.concat(spot)); 21 | /** 如果达到最大值,则上传数据 */ 22 | } else if (bu.length === buffer) { 23 | send(bu as any, options); 24 | bufferMap.delete(type); 25 | /** 如果超出设定值,清空缓存 */ 26 | } else { 27 | bufferMap.delete(type); 28 | } 29 | /** 初始一个缓存 */ 30 | } else { 31 | bufferMap.set(type, ([] as SendSpot[]).concat(spot)) 32 | } 33 | /** 不开启缓存 */ 34 | } else { 35 | send(spot, options); 36 | } 37 | } 38 | } 39 | 40 | export { 41 | buffer 42 | } -------------------------------------------------------------------------------- /src/utils/sendHOC/compose.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 合并函数 3 | */ 4 | 5 | const compose = (...fns: Function[]) => fns.reduce((prv, cur) => (...args: any[]) => prv(cur(...args))); 6 | 7 | export { 8 | compose 9 | } -------------------------------------------------------------------------------- /src/utils/sendHOC/index.ts: -------------------------------------------------------------------------------- 1 | import { Send } from '../../config/define'; 2 | import { compose } from './compose'; 3 | import { sampling } from './sampling'; 4 | import { buffer } from './buffer'; 5 | import { userAgent } from './userAgent'; 6 | 7 | function sendHOC(send: Send): Send { 8 | return compose(sampling, userAgent, buffer)(send); 9 | } 10 | 11 | export { 12 | sendHOC 13 | } -------------------------------------------------------------------------------- /src/utils/sendHOC/sampling.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 采样率 3 | */ 4 | import { Send } from '../../config/define'; 5 | 6 | function sampling(send: Send): Send { 7 | /** 返回发送函数 */ 8 | return (spot, options) => { 9 | const { sampling } = options; 10 | const random = Math.random(); 11 | if (random <= sampling) { 12 | send(spot, options); 13 | } 14 | } 15 | } 16 | 17 | export { 18 | sampling 19 | } -------------------------------------------------------------------------------- /src/utils/sendHOC/userAgent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 使用者环境信息 3 | */ 4 | import { Send } from '../../config/define'; 5 | 6 | function userAgent(send: Send): Send { 7 | /** 返回发送函数 */ 8 | return (spot, options) => { 9 | const env = { 10 | title: document.title, 11 | url: location.href, 12 | timestamp: Date.now(), 13 | userAgent: navigator.userAgent 14 | } 15 | send({ ...spot, env }, options); 16 | } 17 | } 18 | 19 | export { 20 | userAgent 21 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "ES2015", /* Specify what module code is generated. */ 26 | // "rootDir": "./", /* Specify the root folder within your source files. */ 27 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | // "resolveJsonModule": true, /* Enable importing .json files. */ 36 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 37 | /* JavaScript Support */ 38 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 39 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 40 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 41 | /* Emit */ 42 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 43 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 44 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 45 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 46 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 47 | "outDir": "./types", /* Specify an output folder for all emitted files. */ 48 | // "removeComments": true, /* Disable emitting comments. */ 49 | // "noEmit": true, /* Disable emitting files from a compilation. */ 50 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 51 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 52 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 53 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 56 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 57 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 58 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 59 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 60 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 61 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 62 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 63 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 64 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 65 | /* Interop Constraints */ 66 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 67 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 68 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 69 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 70 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 71 | /* Type Checking */ 72 | "strict": true, /* Enable all strict type-checking options. */ 73 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 74 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 75 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 76 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 77 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 78 | "noImplicitThis": false, /* Enable error reporting when 'this' is given the type 'any'. */ 79 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 80 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 81 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 82 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 83 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 84 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 85 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 86 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 87 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 88 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 89 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 90 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 91 | /* Completeness */ 92 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 93 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 94 | }, 95 | "include": [ 96 | "./src/**/*" 97 | ], 98 | "exclude": [ 99 | "**/__tests__/**/*" 100 | ] 101 | } --------------------------------------------------------------------------------