├── .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 |
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 | }
--------------------------------------------------------------------------------