├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── README_SELF.md
├── docs
├── .vitepress
│ ├── cache
│ │ └── deps
│ │ │ ├── @theme_index.js
│ │ │ ├── @theme_index.js.map
│ │ │ ├── _metadata.json
│ │ │ ├── chunk-NQEDJL6T.js
│ │ │ ├── chunk-NQEDJL6T.js.map
│ │ │ ├── package.json
│ │ │ ├── vitepress___@vue_devtools-api.js
│ │ │ ├── vitepress___@vue_devtools-api.js.map
│ │ │ ├── vue.js
│ │ │ └── vue.js.map
│ └── config.ts
├── guide
│ ├── functions
│ │ ├── error.md
│ │ ├── event.md
│ │ ├── exports.md
│ │ ├── http.md
│ │ ├── intersection.md
│ │ ├── other.md
│ │ ├── performance.md
│ │ └── pv.md
│ ├── idea.md
│ ├── plan.md
│ ├── practice.md
│ ├── spotlight.md
│ ├── starting.md
│ └── use
│ │ ├── declare.md
│ │ ├── demo.md
│ │ ├── options.md
│ │ ├── run.md
│ │ └── structure.md
├── index.md
├── package.json
└── vite.config.ts
├── examples
├── vanilla
│ ├── .DS_Store
│ ├── .gitignore
│ ├── README.md
│ ├── assets
│ │ ├── javascript.svg
│ │ ├── style.css
│ │ └── vite.svg
│ ├── index.html
│ ├── index.ts
│ ├── main.ts
│ ├── package.json
│ └── vite.config.ts
├── vue2
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── server.js
│ ├── src
│ │ ├── App.vue
│ │ ├── assets
│ │ │ ├── global.scss
│ │ │ └── vue.svg
│ │ ├── components
│ │ │ ├── CTable.vue
│ │ │ ├── MenuList.vue
│ │ │ └── index.js
│ │ ├── main.js
│ │ ├── router
│ │ │ ├── index.js
│ │ │ └── router.dynamic.js
│ │ └── views
│ │ │ ├── err
│ │ │ └── index.vue
│ │ │ ├── event
│ │ │ └── index.vue
│ │ │ ├── home
│ │ │ └── index.vue
│ │ │ ├── http
│ │ │ └── index.vue
│ │ │ ├── intersection
│ │ │ └── index.vue
│ │ │ ├── performance
│ │ │ └── index.vue
│ │ │ └── pv
│ │ │ └── index.vue
│ └── vite.config.ts
└── vue3
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── public
│ └── vite.svg
│ ├── server.js
│ ├── src
│ ├── App.vue
│ ├── assets
│ │ └── global.scss
│ ├── components
│ │ ├── CTable.vue
│ │ ├── MenuList.vue
│ │ └── index.ts
│ ├── main.ts
│ ├── router
│ │ ├── index.ts
│ │ └── router.dynamic.ts
│ ├── utils
│ │ └── sourcemap.ts
│ └── views
│ │ ├── err
│ │ └── index.vue
│ │ ├── event
│ │ └── index.vue
│ │ ├── home
│ │ └── index.vue
│ │ ├── http
│ │ └── index.vue
│ │ ├── intersection
│ │ └── index.vue
│ │ ├── performance
│ │ └── index.vue
│ │ └── pv
│ │ └── index.vue
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── global.d.ts
├── meta
└── packages.ts
├── package.json
├── packages
├── core
│ ├── __test__
│ │ ├── css
│ │ │ └── performance.css
│ │ ├── err-batch.spec.ts
│ │ ├── err.spec.ts
│ │ ├── event.spec.ts
│ │ ├── html
│ │ │ ├── performance.html
│ │ │ └── recordscreen.html
│ │ ├── http.spec.ts
│ │ ├── img
│ │ │ └── performance.png
│ │ ├── js
│ │ │ └── performance.js
│ │ ├── performance.spec.ts
│ │ ├── recordscreen.spec.ts
│ │ ├── utils.spec.ts
│ │ └── utils
│ │ │ ├── index.ts
│ │ │ └── pollify.ts
│ ├── index.ts
│ ├── package.json
│ └── src
│ │ ├── common
│ │ ├── config.ts
│ │ ├── constant.ts
│ │ └── index.ts
│ │ ├── lib
│ │ ├── base.ts
│ │ ├── err-batch.ts
│ │ ├── err.ts
│ │ ├── event-dwell.ts
│ │ ├── event.ts
│ │ ├── eventBus.ts
│ │ ├── exportMethods.ts
│ │ ├── http.ts
│ │ ├── intersectionObserver.ts
│ │ ├── line-status.ts
│ │ ├── options.ts
│ │ ├── performance.ts
│ │ ├── pv.ts
│ │ ├── recordscreen.ts
│ │ ├── replace.ts
│ │ └── sendData.ts
│ │ ├── observer
│ │ ├── computed.ts
│ │ ├── config.ts
│ │ ├── dep.ts
│ │ ├── index.ts
│ │ ├── ref.ts
│ │ ├── types.ts
│ │ ├── watch.ts
│ │ └── watcher.ts
│ │ ├── types
│ │ └── index.ts
│ │ └── utils
│ │ ├── debug.ts
│ │ ├── element.ts
│ │ ├── fingerprintjs.ts
│ │ ├── getIps.ts
│ │ ├── global.ts
│ │ ├── index.ts
│ │ ├── is.ts
│ │ ├── localStorage.ts
│ │ └── session.ts
├── vue2
│ ├── README.md
│ ├── index.ts
│ └── package.json
└── vue3
│ ├── README.md
│ ├── index.ts
│ └── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── rollup.config.js
├── scripts
├── build.ts
├── examples
│ ├── js.sh
│ ├── vue2.sh
│ └── vue3.sh
├── fix-types.ts
├── publish-docs.sh
├── publish-examples.ts
├── publish.ts
├── rollup.config.ts
├── test-install.sh
├── update.ts
└── utils.ts
├── tsconfig.json
└── vitest.config.mts
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/assets
2 | src/icons
3 | public
4 | dist
5 | node_modules
6 | index.html
7 | *.sh
8 | *.md
9 | *.woff
10 | *.ttf
11 | .vscode
12 | .idea
13 | /docs
14 | .husky
15 | .local
16 | /bin
17 | Dockerfile
18 | _old
19 | fingerprintjs.js
20 | fingerprintjs.ts
21 | getIps.js
22 | getIps.ts
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | es2021: true,
6 | node: true
7 | },
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended'
12 | ],
13 | overrides: [],
14 | parser: '@typescript-eslint/parser',
15 | parserOptions: {
16 | ecmaVersion: 'latest',
17 | sourceType: 'module',
18 | parser: '@typescript-eslint/parser'
19 | },
20 | plugins: ['@typescript-eslint'],
21 | rules: {
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | 'no-debugger': 'warn',
24 | '@typescript-eslint/no-this-alias': 'off',
25 | '@typescript-eslint/no-non-null-assertion': 'off',
26 | 'prettier/prettier': [
27 | 'error',
28 | {
29 | semi: false,
30 | trailingComma: 'none',
31 | arrowParens: 'avoid',
32 | singleQuote: true,
33 | endOfLine: 'auto'
34 | }
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
5 | coverage
6 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 | side-effects-cache=false
4 | ignore-workspace-root-check=true
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "css.validate": false,
4 | "less.validate": false,
5 | "scss.validate": false,
6 | "html.validate.styles": false,
7 | "stylelint.validate": [
8 | "css",
9 | "less",
10 | "scss",
11 | "vue"
12 | ],
13 | "eslint.validate": [
14 | "javascript",
15 | "javascriptreact",
16 | "typescript",
17 | "typescriptreact",
18 | "vue",
19 | "html",
20 | ],
21 | "editor.codeActionsOnSave": {
22 | "source.fixAll": "explicit",
23 | "source.fixAll.stylelint": "explicit",
24 | "source.fixAll.eslint": "explicit"
25 | },
26 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 版本变更记录
2 |
3 | ## 已完成/提交 & 未发包
4 |
5 | ## 2.0.9
6 | + fix(console.error): 上报error 对象errMessage输出为[Object] #88
7 |
8 | ## 2.0.8
9 | + fix: 错误录屏功能在初始化的时候就判断是否加载 https://github.com/M-cheng-web/web-tracing/issues/78 https://github.com/M-cheng-web/web-tracing/issues/67
10 | + fix: vue 使用axios,发送fetch时,有报错 https://github.com/M-cheng-web/web-tracing/issues/73 https://github.com/M-cheng-web/web-tracing/issues/74 https://github.com/M-cheng-web/web-tracing/issues/72
11 |
12 | ## 2.0.7
13 | + test: 增加 err 模块/ utils 模块的单元测试 #51 https://github.com/M-cheng-web/web-tracing/pull/51
14 |
15 | ## 2.0.6
16 | + fix: 修复baseInfo未初始化完成就上报的问题 https://github.com/M-cheng-web/web-tracing/pull/46
17 |
18 | ## 2.0.5
19 | + event.md 事件流图片缺失
20 | + readme.md 添加赞赏码
21 | + fix issues https://github.com/M-cheng-web/web-tracing/issues/43
22 |
23 | ## 2.0.4
24 | + README.md 中微信名片地址更改
25 | + 兼容electron环境下获取window变量
26 |
27 | ## 2.0.3
28 | + package.json 中 clean 命令优化
29 | + package.json 中 esno 本地化
30 | + 添加 CHANGELOG.md 并对之前版本补充
31 | + README.md 更改介绍语
32 | + 暴露出的主动事件方法添加flush参数代表是否立即发送事件
33 |
34 | ## 2.0.2
35 | + 改进 mvvm-watch(原先有bug)
36 | + 添加 recordScreen 配置项
37 |
38 | ## 2.0.1
39 | + 去除 event.unload 参数(同时此事件也会去除)
40 | + 新增 sendTypeByXmlBody 参数强行控制发送方式是否为 xmlbody 形式
41 | + 新增 pv-duration 事件类型来标明页面停留时间
42 |
43 | ## 2.0.0(二期功能)
44 | + 整体代码结构更改
45 | + 文档系统与sdk核心代码融合 (vuepress -> vitepress)
46 | + ts
47 | + demo完善(vue2、vue3、js)
48 | + issues统一解决
49 | + ignoreErrors - 错误屏蔽
50 | + ignoreRequest - 请求屏蔽
51 | + 支持hook以及自定义hook -【1.放入消息队列的钩子 2.发送时的钩子 3.发送之后的钩子】(这些钩子能放置多个)
52 | + 支持暴露更多变量(例如最大缓存数、延迟上传时间)
53 | + 支持更多上传方式(xml、image、sendbeacon) - 可强制指定xml
54 | + 支持抽样发送(tracesSampleRate全局抽样,具体到模块的抽样可以用beforePushEventList来阻止)
55 | + sdk内部的img发送请求不会记录
56 | + sdk内部的console.error不会记录
57 | + 防止重复init
58 | + 设置断网情况下不采集任何元素(个人认为断网了不需要再去采集用户的操作了,除非是特别需要,所以加入此限制)
59 | + 错误过多自动转区间事件,也就是去重(场景大致描述:如果是批量错误会延长20s再发送,如果是普通单个错误会延长2s发送,如果是无限错误每隔50个会发送一次)
60 | + 支持暴露更多sdk内部方法(例如使用者想要拿到此时的硬件数据,用户更改入参的方法,一些钩子也要加到这里,例如上传之前的的钩子,然后组成一个数组,为什么这里需要是因为用户想要在每个页面细致化控制是否上传,例如只想在用户打开某个页面才开始采集,不是这个页面则结束采集)
61 | + 关于用户信息的重构 - 【1.分为未登录与已登录的场景,登录后进行绑定,机器与用户id进行多对多绑定(更多方案还在确定中) 2.支持动态修改用户信息】- 使用 fingerprintjs 去实现设备id
62 | + 获取公网ip - 放在了base信息中
63 | + 支持区域曝光度采集 - 采用 IntersectionObserver 来实现的,当不兼容时此功能无效(后面会平替为 scroll 来实现,看有木有需求) - 考虑到需要曝光的元素可能在任意页面,所以sdk索性只提供暴露方法供使用者在运行中再去手动调用,目前内部在切换页面时会自动销毁当前页面监听 - 目前需要手动清除监听,后面有需要再根据路由跳转自动清除 - 目前收集契机是必须进入+出去才收集,暂不考虑停留页面直接退出的场景
64 | + 支持数据临时存储本地的形式减少服务端压力(会设定存储的阈值大小,最大5M,先不支持跨域存储,否则内容太大上传慢或者不兼容导致此功能不稳定) - 需要用户手动发送 - navigator.sendBeacon 发送大小在 2M-4M,如果觉得不稳妥可以 beforeSendData 拦截自己发送
65 | + 错误录屏
66 | + 针对资源的异步加载监控,sdk不做是否加载成功判断,但会暴露 responseEnd 和 responseStart,一般失败的资源加载通过这俩个字段能发现端倪 - 只针对支持 PerformanceObserver 的场景下,不支持 PerformanceObserver 会用 MutationObserver 兼容,不同的是 MutationObserver 能判断请求资源是否报错,如果报错则提供 responseStatus: 'error' 字段 (注意:通过标签加载资源如果报错除了资源本身会有错误,错误模块也会暴露一个错误信息,但如果是通过XMR请求则不会额外暴露错误信息)
67 | + 将所有 options 变为响应式 - 也就是所有用到了 options 参数时,当动态去改它能实时更新(可能要将所有的变量都变为响应式了) - 也就是支持用户全局动态去改 options - 采用 proxy,如果不兼容则此功能无效
68 | + performance模块的src url 改名 (requestUrl triggerPageUrl)
69 | + http模块 src 改名为 requestUrl
70 | + err模块 url 改名为 triggerPageUrl
71 | + event模块 url 改名为 triggerPageUrl
72 | + event-dwell模块 url 改名为 triggerPageUrl
73 | + pv模块 url 改名为 triggerPageUrl
74 | + sdk内部所有主动方法都改为只传 options 一个参数,但所有的属性优先级是低于内部定义的
75 | + sdk内部发送方式增加至三种,sendbeacon、xml、img,优先 sendBeacon,其次img,最后为xml(因为增加了错误录屏导致数据量较大,xml不限制请求大小)但xml可能会有跨域问题,使用者应该注意这一问题,且sdk内部会将配置的dsn地址加入请求拦截名单(sendBeacon 有64kb限制 img 有2kb限制)
76 | + 兼容 vue2、vue3(错误处理机制已兼容去重)(react以及小程序优先级靠后一些)(vue3的demo项目的err模块会报警告,分别是:rrweb-player初始加载警告、执行 codeError 方法会报警告;均不影响正常调试)
77 | + 实现参数动态更改,目前所有的都可更改,没加限制,所以得提醒注意改的场景不要啥都改
78 | + error 检测那边,加了一个属性标识错误类型,例如 console.error 还是 Error 或者 reject
79 |
80 | ## 1.0.0 - 2.0.0(一期功能)
81 | + 简单支持 vue2 + vue3
82 | + 自动采集 + 暴露api给用户手动采集上报
83 | + 采集功能:【用户事件采集、页面跳转采集、请求采集、错误采集、资源加载采集】
84 | + 采集上传方法:只提供 sendBeacon(内部降级为image)
85 |
86 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 M-cheng-web
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 |

3 |
web-tracing 监控插件
4 |
5 | 基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
6 |
7 |
8 |
9 | ## 官方文档
10 | [官方文档 https://m-cheng-web.github.io/web-tracing/](https://m-cheng-web.github.io/web-tracing/)
11 |
12 | ## DeepWiki文档
13 | [DeepWiki文档 https://deepwiki.com/M-cheng-web/web-tracing](https://deepwiki.com/M-cheng-web/web-tracing)
14 |
15 | ## 示例项目(本地)
16 | [js版本 https://github.com/M-cheng-web/web-tracing-examples-js](https://github.com/M-cheng-web/web-tracing-examples-js)
17 |
18 | [vue2版本 https://github.com/M-cheng-web/web-tracing-examples-vue2](https://github.com/M-cheng-web/web-tracing-examples-vue2)
19 |
20 | [vue3版本 https://github.com/M-cheng-web/web-tracing-examples-vue3](https://github.com/M-cheng-web/web-tracing-examples-vue3)
21 |
22 | ## 演示
23 | ### 事件监听
24 |
25 |
26 | ### 错误监听
27 |
28 |
29 | ### 资源监听
30 |
31 |
32 | ## 项目初衷
33 | 为了帮助开发们在公司平台上搭建一套前端监控平台
34 |
35 | > 作者心声: 想降低一下前端在这方面耗费的时间与精力,此项目会尽量针对每个场景都提供解决方案;即使最后没用我这套,但从在这里对某些场景方案有了一些了解,我也很开心(为了方便大家移植代码所以没有分包,如果你有分包需求可联系我,干杯!)
36 |
37 | ## 亮点
38 | 提供了多种定制化api最大限度帮助你应付各个场景的业务,例如:
39 | + 提供钩子函数让你对数据精确把握
40 | + 提供本地化选项api,让开发手动控制去发送监控数据 - 节省带宽
41 | + 提供批量错误api,在遇到无限错误时融合批量错误信息 - 节省带宽
42 | + 提供抽样发送api - 节省带宽
43 | + 提供 错误/请求 事件的过滤api
44 | + 等等....
45 |
46 | 站在技术角度,因为明确此项目可能更多的是应用在公司平台上,大概率会二开,所以作者对项目结构以及代码都严格要求
47 | + 架构 - demo、核心sdk代码、文档都在同一个项目中,调试、部署都很方便
48 | + 封装 - sdk存在大量的重写或者监听,对此有统一流程
49 | + 响应式 - 项目内部实现了vue响应式,也应用在 options 对象中,相信你接触会后受益良多
50 | + 多版本 - 针对不同平台提供多个版本(目前只有js、vue2、vue3),受益于monorepo架构可一键发布
51 | + 内聚 - 目前核心功能的所有代码都没有分包,虽然monorepo架构支持,但作者认为目前分包不利于代码阅读以及二开方便
52 | + 文档/注释 - 完善的文档以及非常全的注释,力求帮助你快速了解这一切
53 |
54 | ## 功能列表
55 | 具体参见[CHANGELOG.md](https://github.com/M-cheng-web/web-tracing/blob/main/CHANGELOG.md)
56 |
57 | ## 未来方向
58 | 会写一套服务端(nest) + 后台查看监控数据平台(vue),有以下几点考量
59 | + 提供服务端能力(目前只是在采集端发力)
60 | + 可以在线体验此项目
61 | + 提供更多示例代码给开发们,再次降低这一套代码在公司的推广难度
62 | + 作者也想站在业务的角度多思考还能从哪些方面此项目还缺失哪些功能
63 |
64 | 针对首屏加载的监控做出更多精细化的东西,例如考虑sdk的绝对轻量化
65 |
66 | ## 联系我
67 |
68 |
69 | - 如果对此项目有疑虑或者有优化点,欢迎与我讨论(有沟通群)
70 | - Bug 反馈请直接去 Github 上面提 Issues,我会实时收到邮件提醒前去查看
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | ## 🙏🙏🙏 点个Star
79 |
80 | **如果您觉得这个项目还不错, 可以在 [Github](https://github.com/M-cheng-web/web-tracing) 上面帮我点个`star`, 支持一下作者ヾ(◍°∇°◍)ノ゙**
81 |
82 |
83 |
84 | ## 贡献者
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | ## 特别感谢
93 | + [xy-sea](https://github.com/xy-sea)为我提供了很多好主意,这是他的关于[监控平台文章以及blog](https://github.com/xy-sea/blog/blob/main/markdown/%E4%BB%8E0%E5%88%B01%E6%90%AD%E5%BB%BA%E5%89%8D%E7%AB%AF%E7%9B%91%E6%8E%A7%E5%B9%B3%E5%8F%B0%EF%BC%8C%E9%9D%A2%E8%AF%95%E5%BF%85%E5%A4%87%E7%9A%84%E4%BA%AE%E7%82%B9%E9%A1%B9%E7%9B%AE.md),写的很好受益匪浅
94 | + [wangshitao929@163.com](wangshitao929@163.com) - 特别赞助
95 | + [rrweb](https://github.com/rrweb-io/rrweb) - sdk内部使用其帮助错误录屏
96 | + [fingerprintjs v3.4.1](https://github.com/fingerprintjs/fingerprintjs) - sdk内部采用了其离线版本,用于标识唯一用户
97 | + [webrtc-ip v3.0.1](https://github.com/joeymalvinni/webrtc-ip) - sdk内部采用了其离线版本,用于获取公网ip
98 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/_metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "hash": "a91981c2",
3 | "browserHash": "1c39cb08",
4 | "optimized": {
5 | "vue": {
6 | "src": "../../../../node_modules/.pnpm/vue@3.3.4/node_modules/vue/dist/vue.runtime.esm-bundler.js",
7 | "file": "vue.js",
8 | "fileHash": "28201cb0",
9 | "needsInterop": false
10 | },
11 | "vitepress > @vue/devtools-api": {
12 | "src": "../../../../node_modules/.pnpm/@vue+devtools-api@6.5.0/node_modules/@vue/devtools-api/lib/esm/index.js",
13 | "file": "vitepress___@vue_devtools-api.js",
14 | "fileHash": "f4a51e62",
15 | "needsInterop": false
16 | },
17 | "@theme/index": {
18 | "src": "../../../../node_modules/.pnpm/vitepress@1.0.0-beta.5_gzzappvipf62bx3ilub7dtkyaq/node_modules/vitepress/dist/client/theme-default/index.js",
19 | "file": "@theme_index.js",
20 | "fileHash": "b83e5f90",
21 | "needsInterop": false
22 | }
23 | },
24 | "chunks": {
25 | "chunk-NQEDJL6T": {
26 | "file": "chunk-NQEDJL6T.js"
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vue.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": [],
4 | "sourcesContent": [],
5 | "mappings": "",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, DefaultTheme } from 'vitepress'
2 | import { version } from '../../package.json'
3 |
4 | export default defineConfig({
5 | lang: 'zh-CN',
6 | title: 'web-tracing',
7 | description: '行为埋点 & 性能采集 & 异常采集 & 请求采集 & 路由采集',
8 |
9 | lastUpdated: true,
10 | base: '/web-tracing',
11 | cleanUrls: true,
12 |
13 | themeConfig: {
14 | logo: 'https://github.com/M-cheng-web/image-provider/raw/main/web-tracing/logo.7k1jidnhjr40.svg',
15 |
16 | nav: [
17 | { text: '指南', link: '/guide/starting' },
18 | { text: '关于项目', link: '/guide/spotlight' },
19 | { text: '示例', link: '/guide/use/demo' },
20 | { text: version, link: '' }
21 | ],
22 |
23 | sidebar: {
24 | '/guide/': [
25 | {
26 | text: '指南',
27 | items: [
28 | { text: '起步', link: '/guide/starting' },
29 | { text: '最佳实践', link: '/guide/practice' },
30 | { text: '关于项目', link: '/guide/spotlight' },
31 | { text: '设计理念', link: '/guide/idea' },
32 | { text: '迭代计划', link: '/guide/plan' },
33 | ]
34 | },
35 | {
36 | text: '使用',
37 | items: [
38 | { text: '基础说明', link: '/guide/use/declare' },
39 | { text: '配置项', link: '/guide/use/options' },
40 | { text: '数据结构', link: '/guide/use/structure' },
41 | { text: '示例', link: '/guide/use/demo' },
42 | { text: '本地运行项目', link: '/guide/use/run' },
43 | ]
44 | },
45 | {
46 | text: '功能',
47 | items: [
48 | { text: '事件采集', link: '/guide/functions/event' },
49 | { text: '错误采集', link: '/guide/functions/error' },
50 | { text: '路由采集', link: '/guide/functions/pv' },
51 | { text: '请求采集', link: '/guide/functions/http' },
52 | { text: '资源采集', link: '/guide/functions/performance' },
53 | { text: '曝光采集', link: '/guide/functions/intersection' },
54 | { text: '导出项', link: '/guide/functions/exports' },
55 | // { text: '其他', link: '/guide/functions/other' },
56 | ]
57 | },
58 | ],
59 | // '/analyse/': [
60 | // {
61 | // text: '技术点分析',
62 | // items: [
63 | // { text: '基础说明', link: '/analyse/index' },
64 | // { text: '架构', link: '/analyse/framework' },
65 | // ]
66 | // },
67 | // ],
68 | },
69 |
70 | editLink: {
71 | pattern: 'https://github.com/M-cheng-web/web-tracing/blob/main/docs/:path',
72 | text: 'Suggest changes to this page'
73 | },
74 |
75 | socialLinks: [
76 | { icon: 'github', link: 'https://github.com/M-cheng-web/web-tracing' }
77 | ],
78 |
79 | // 这里后续要去申请
80 | // algolia: {
81 | // appId: '8J64VVRP8K',
82 | // apiKey: 'a18e2f4cc5665f6602c5631fd868adfd',
83 | // indexName: 'vitepress'
84 | // },
85 |
86 | // lastUpdatedText: '最后更新',
87 |
88 | // outlineTitle: 'This',
89 | },
90 | head: [
91 | ['link', { rel: 'icon', href: 'https://github.com/M-cheng-web/image-provider/raw/main/web-tracing/logo.7k1jidnhjr40.svg' }],
92 | ]
93 | })
--------------------------------------------------------------------------------
/docs/guide/functions/http.md:
--------------------------------------------------------------------------------
1 | # Http
2 | 捕获所有的 `xhr & axios & fetch` 请求,主要原理是劫持`XHR-open & XHR-send & fetch`
3 |
4 | 触发事件时给后台的对象
5 | | 属性名称 | 值 | 说明 |
6 | | -------------- | ------------------------------------- | ------------ |
7 | | eventId | server | 事件ID |
8 | | eventType | 请求错误时error,请求正确时performance | 事件类型 |
9 | | requestUrl | | 请求地址 |
10 | | requestMethod | get、post... | 请求方式 |
11 | | requestType | xhr、fetch... | 请求类型 |
12 | | responseStatus | | 请求返回代码 |
13 | | duration | 请求正确时才有此字段 | 请求消耗时间 |
14 | | params | | 请求的参数 |
15 | | triggerTime | | 事件发生时间 |
16 | | triggerPageUrl | | 页面地址 |
17 | | sendTime | | 发送时间 |
18 | | errMessage | 请求错误时才有此字段 | 请求错误信息 |
19 | | recordscreen | 请求错误时才有此字段 | 错误录屏数据 |
20 |
21 | ``` js
22 | // 真实场景产生的事件对象 - 请求正确时
23 | {
24 | eventId: 'server',
25 | eventType: 'performance',
26 | requestUrl: 'http://localhost:6656/getList?test=123',
27 | requestMethod: 'get',
28 | requestType: 'xhr',
29 | responseStatus: 200,
30 | duration: 13,
31 | params: { test: '123' },
32 | triggerTime: 1689729859862,
33 | triggerPageUrl: 'http://localhost:6656/#/http',
34 | sendTime: 1689729860863
35 | }
36 |
37 | // 真实场景产生的事件对象 - 请求错误时
38 | {
39 | eventId: 'server',
40 | eventType: 'error',
41 | requestUrl: 'http://localhost:6656/getList2?test=123',
42 | requestMethod: 'get',
43 | requestType: 'xhr',
44 | responseStatus: 404,
45 | params: { test: '123' },
46 | triggerTime: 1689729859862,
47 | triggerPageUrl: 'http://localhost:6656/#/http',
48 | sendTime: 1689729860863
49 | errMessage: 'Not Found',
50 | recordscreen: 'H4sIAAAAAAAAA+R9V3vqyNLuD9oXh2C8h0sbm7RA3saYoDuChyQwswATfv2p6' // 错误录屏数据
51 | }
52 | ```
--------------------------------------------------------------------------------
/docs/guide/functions/intersection.md:
--------------------------------------------------------------------------------
1 | # Intersection
2 | 捕获目标元素的曝光事件,主要原理是创建 `IntersectionObserver` 实例
3 |
4 | ::: warning
5 | 监听阈值(threshold)解释:阀值默认为0.5,当为0.5时代表滚动超过图片达到一半时即为曝光结束
6 | 同理当为0.5时,代表滚动视图能看到图片一半时即为曝光开始
7 | :::
8 |
9 | 触发事件时给后台的对象
10 | | 属性名称 | 值 | 说明 |
11 | | -------------- | ------------ | ---------------------- |
12 | | eventType | intersection | 事件类型 |
13 | | target | | 被监听的元素(无效参数) |
14 | | threshold | | 监听阈值 |
15 | | params | | 附加参数 |
16 | | observeTime | | 开始监听时间 |
17 | | showTime | | 元素开始被曝光的时间 |
18 | | showEndTime | | 元素结束被曝光的时间 |
19 | | sendTime | | 发送时间 |
20 | | triggerPageUrl | | 页面地址 |
21 |
22 |
23 | ``` js
24 | // 真实场景产生的事件对象
25 | {
26 | eventType: 'intersection',
27 | target: { _prevClass: 'mb' },
28 | threshold: 0.5,
29 | observeTime: 1689734412090,
30 | params: { name: 1111, targetName: 'target' },
31 | showTime: 1689734412098,
32 | showEndTime: 1689734414097,
33 | sendTime: 1689734415104
34 | triggerPageUrl: 'http://localhost:6656/#/intersection',
35 | }
36 | ```
37 |
38 | ## 使用说明
39 | sdk初始化时不提供此功能,只能在页面针对某个元素进行监听
40 |
41 | ::: tip
42 | [vue2完整示例代码](https://github.com/M-cheng-web/web-tracing/blob/main/examples/vue2/src/views/intersection/index.vue)
43 |
44 | [vue3完整示例代码](https://github.com/M-cheng-web/web-tracing/blob/main/examples/vue3/src/views/intersection/index.vue)
45 | :::
46 |
47 |
48 | ``` js
49 | import {
50 | intersectionObserver,
51 | intersectionUnobserve,
52 | intersectionDisconnect
53 | } from '@web-tracing/vue2'
54 |
55 | const target = document.querySelector(`#xxx`)
56 |
57 | // 对元素开始监听
58 | intersectionObserver({
59 | target,
60 | threshold: 0.5, // 曝光的临界点 (0.5表示移入窗口一半算做开始曝光、移出窗口一半算结束曝光)
61 | params: { name: 1111, targetName: str } // 附带的额外参数
62 | })
63 |
64 | // 对元素结束监听
65 | intersectionUnobserve(target)
66 |
67 | // 结束所有的元素监听
68 | intersectionDisconnect()
69 | ```
70 |
71 |
72 |
--------------------------------------------------------------------------------
/docs/guide/functions/other.md:
--------------------------------------------------------------------------------
1 | # 其他事件
2 |
3 | ## 页面卸载事件
4 | 页面卸载时会触发`beforeunload`事件,并由此采集发送页面卸载信息给后台
5 |
6 | | 属性名称 | 值 | 说明 |
7 | | ------------- | ----------------------------------------------- | ------------ |
8 | | eventId | 根据时间戳计算得来的字符 (每次卸载事件都不相同) | 事件ID |
9 | | eventType | dwell | 事件类型 |
10 | | url | | 当前页面URL |
11 | | referer | | 上级页面URL |
12 | | entryTime | | 加载完成时间 |
13 | | triggerTime | | 卸载时间 |
14 | | millisecond | | 页面停留时间 |
15 | | operateAction | navigate / reload / back_forward / reserved | 页面加载来源 |
16 |
--------------------------------------------------------------------------------
/docs/guide/functions/pv.md:
--------------------------------------------------------------------------------
1 | # Pv
2 | 采集页面跳转的数据,主要原理是劫持`history.pushState history.replaceState`,以及监听`popstate hashchange`这两个事件
3 |
4 | 触发事件时生成的对象
5 | | 属性名称 | 值 | 说明 |
6 | | -------------- | ------------------------------------------- | ------------ |
7 | | eventId | 根据时间戳计算得来的字符 (固定为pageId) | 事件ID |
8 | | eventType | pv | 事件类型 |
9 | | triggerPageUrl | | 当前页面URL |
10 | | referer | | 上级页面URL |
11 | | title | document.title | 页面标题 |
12 | | sendTime | | 发送时间 |
13 | | triggerTime | | 事件发生时间 |
14 | | action | navigate / reload / back_forward / reserved | 页面加载来源 |
15 |
16 | ``` js
17 | // 真实场景产生的事件对象
18 | {
19 | eventType: 'pv',
20 | eventId: '134b23f7-56a67609-802eb5fc1a34fde9',
21 | triggerPageUrl: 'http://localhost:6656/#/pv',
22 | referer: 'http://localhost:6656/#/err',
23 | title: 'example-vue2',
24 | action: 'navigation',
25 | triggerTime: 1689728946196,
26 | sendTime: 1689728947199
27 | }
28 | ```
29 | ## action 字段解释
30 | + navigate - 网页通过点击链接,地址栏输入,表单提交,脚本操作等方式加载
31 | + reload - 网页通过“重新加载”按钮或者location.reload()方法加载
32 | + back_forward - 网页通过“前进”或“后退”按钮加载
33 | + reserved - 任何其他来源的加载
34 |
35 | ## 页面停留时间捕获
36 | ::: tip
37 | 在每次跳转到新的页面时都会触发两个事件,分别为 pv 跳转页面事件、pv-duration 页面停留事件
38 |
39 | 上面讲的是pv事件,而这里讲重点讲一下 pv-duration 事件
40 | :::
41 |
42 |
43 | 首先其触发时生成的对象格式为
44 | | 属性名称 | 值 | 说明 |
45 | | -------------- | ------------------------------------------- | --------------------------------------- |
46 | | eventId | 根据时间戳计算得来的字符 (固定为pageId) | 事件ID |
47 | | eventType | pv-duration | 事件类型 |
48 | | triggerPageUrl | | 当前页面URL(也就是在哪个页面发生的停留) |
49 | | referer | | 上级页面URL(停留页面的上一张页面) |
50 | | title | document.title | 页面标题 |
51 | | durationTime | | 页面具体停留的时间 |
52 | | sendTime | | 发送时间 |
53 | | triggerTime | | 事件发生时间 |
54 | | action | navigate / reload / back_forward / reserved | 页面加载来源 |
55 |
--------------------------------------------------------------------------------
/docs/guide/idea.md:
--------------------------------------------------------------------------------
1 | # WWWWH
2 | :star::star::star: 谁(Who) 在什么时候(When) 什么地方(Where) 干了什么(What) 怎么干的(How) :star::star::star:
3 |
4 | ## Who
5 | **什么是用户**
6 | 用户指的是访问这个页面的行为人,对于SDK来说使用同一个账户、同一个设备、同一个浏览器来访问页面的"人"就是同一个用户
7 |
8 | **用户ID**
9 | 每一次访问都有一个唯一的ID,不论什么时候来访问,用户是否登录,都携带有一个唯一的ID
10 | 可以理解为用来标记这个访问设备,有网卡mac地址则使用mac地址(移动端用udid)
11 | mac地址、移动端设备id,SDK生成的ID在库中的字段都为 udid
12 |
13 | **会话ID**
14 | 会话ID用来标记某段时间内的连续访问为一次用户会话,当用户开始一个新的访问时,会创建一个session_id,存于cookie当中
15 | 有效期三十分钟,当有用户交互发生时,会话有效期从交互时刻延长至该交互事件发生时刻的30分钟后,即重置session_id有效期
16 |
17 | ## When
18 | 用户事件发生的时间,这个时间是客户端时间,客户端时间用于对这个客户端上的埋点记录进行排序,来串联用户的交互行为
19 |
20 | 客户端时间是不准确的,比较准确的推算出用户事件发生的真实时刻,需要三个值:
21 | 1. 事件发生时间
22 | 2. 事件记录发送时间,我们是缓存后,批量发送,需要加上发送时间
23 | 3. 后端接收时间
24 |
25 | 推算前提:
26 | + 以后端时间为基准,后端时间是真实可靠的
27 | + 我们认为客户端发送给后端的这个网络开销时间忽略不计
28 |
29 | 后端接收时间和客户端发送时间的差值代表了基准时间和客户端时间的差值
30 | **推算公式: realTime = receiveTime(后端接收时间) - sendTime(事件发送时间) + time(事件发生时间)**
31 |
32 | ## Where
33 | 物理位置: 用户所处的地理位置,通过ip或者app的定位服务计算用户所处在哪个地方
34 |
35 | 逻辑位置: 事件发生时用户当前所在的页面,事件发生时在页面内的位置信息
36 |
37 | 来源位置: 事件发生时当前页面的上一个页面
38 |
39 | ## What
40 | 事件的内容
41 |
42 | 对于页面访问,内容就是页面标题
43 | 对于输入事件,内容就是用户输入的内容
44 | 对于点击事件,内容就是点击事件采集的规则(参考下方)
45 |
46 | ## How
47 | 用户是怎么触发这个事件的
48 |
49 | 内置的几种类型:
50 | + pv: 页面切换时会记录该类型数据,页面切换可以多普通页面切换,也可以是调用HistoryAPI,或是hashchange方式。
51 | + error: 页面发生异常时会记录该类型数据,异常可以是代码异常、接口异常、资源加载异常
52 | + performance: 性能相关的事件发生时会记录该类型数据,性能事件包括: 页面加载性能、请求响应性能、自定义性能条件触发
53 | + click: 用户点击交互事件
54 | + dwell: 页面卸载
55 | + intersection: 某个元素被曝光
56 |
--------------------------------------------------------------------------------
/docs/guide/plan.md:
--------------------------------------------------------------------------------
1 | # 迭代计划
2 |
3 | ## 分支说明
4 | + main: 当前开发主分支
5 | + 0.0: 最初始的demo代码
6 | + 1.0: 一期代码
7 |
8 | > 在二期结束后且三期开始时会对2期代码封版
9 |
10 | ## 一期(已结束)
11 | + 简单支持 vue2 + vue3
12 | + 自动采集 + 暴露api给用户手动采集上报
13 | + 采集功能:【用户事件采集、页面跳转采集、请求采集、错误采集、资源加载采集】
14 | + 采集上传方法:只提供 sendBeacon(内部自动降级为image)
15 |
16 | > 一期代码已封版移入“1.0”分支
17 |
18 | ## 二期(beta)
19 | + 整体代码结构更改
20 | + 文档系统与sdk核心代码融合 (vuepress -> vitepress) 但还未编写文档
21 | + 所有模块已ts化,下一步添加新功能
22 | + 解决目前 issues
23 | + 支持暴露更多变量(例如最大缓存数、延迟上传时间)(dom埋点名称 先不计划自定义,个人认为没多少业务需要)
24 | + sdk内部的img发送请求不会记录
25 | + sdk内部的console.error不会记录
26 | + 支持hook以及自定义hook -【1.放入消息队列的钩子 2.发送时的钩子 3.发送之后的钩子】(1,2返回false会取消放入以及取消发送) (这些钩子目前没有做成动态的,只支持在初始化时挂上去,等做了 exportMethods 后再做)
27 | + ignoreErrors
28 | + ignoreErrors 入参格式进行校验,只能是 (string|number|reg)[]
29 | + ignoreRequest
30 | + ignoreRequest 入参格式进行校验,只能是 (string|number|reg)[]
31 | + 支持更多上传方式(此功能放入 beforeSendData 钩子,当返回false则sdk内部不发送请求)
32 | + 支持抽样发送(tracesSampleRate全局抽样,具体到模块的抽样可以用beforePushEventList来阻止)
33 | + 支持延迟加载sdk(可后期再init初始化)
34 | + 防止重复init sdk
35 | + 设置断网情况下不采集任何元素(个人认为断网了不需要再去采集用户的操作了,除非是特别需要,所以加入此限制)
36 | + 错误过多自动转区间事件,也就是去重(完成,后续再测一下关闭网页是否会及时发送就ok,如果是批量错误会延长20s再发送,如果是普通单个错误会延长2s发送,如果是无限错误每隔50个会发送一次)
37 | + 支持暴露更多sdk内部方法(例如使用者想要拿到此时的硬件数据,用户更改入参的方法,一些钩子也要加到这里,例如上传之前的的钩子,然后组成一个数组,为什么这里需要是因为用户想要在每个页面细致化控制是否上传,例如只想在用户打开某个页面才开始采集,不是这个页面则结束采集)- 钩子内不能有异步拦截
38 | + 关于用户信息的重构 - 【1.分为未登录与已登录的场景,登录后进行绑定,机器与用户id进行多对多绑定(更多方案还在确定中) 2.支持动态修改用户信息】- 使用 fingerprintjs 去实现设备id
39 | + 获取公网ip - 放在了base信息中
40 | + 支持区域曝光度采集 - 采用 IntersectionObserver 来实现的,当不兼容时此功能无效(后面会平替为 scroll 来实现,看有木有需求) - 考虑到需要曝光的元素可能在任意页面,所以sdk索性只提供暴露方法供使用者在运行中再去手动调用,目前内部在切换页面时会自动销毁当前页面监听 - 目前需要手动清除监听,后面有需要再根据路由跳转自动清除 - 目前收集契机是必须进入+出去才收集,暂不考虑停留页面直接退出的场景
41 | + 支持数据临时存储本地的形式减少服务端压力(会设定存储的阈值大小,最大5M,先不支持跨域存储,否则内容太大上传慢或者不兼容导致此功能不稳定) - 需要用户手动发送 - navigator.sendBeacon 发送大小在 2M-4M,如果觉得不稳妥可以 beforeSendData 拦截自己发送
42 | + 错误录屏 - 完成数据采集,但结果需要配合demo案例看
43 | + 对入参和出参的ts再次统一化 - eventInfo列表中取消 type 字段
44 | + 针对资源的异步加载监控,sdk不做是否加载成功判断,但会暴露 responseEnd 和 responseStart,一般失败的资源加载通过这俩个字段能发现端倪 - 只针对支持 PerformanceObserver 的场景下,不支持 PerformanceObserver 会用 MutationObserver 兼容,不同的是 MutationObserver 能判断请求资源是否报错,如果报错则提供 responseStatus: 'error' 字段 (注意:通过标签加载资源如果报错除了资源本身会有错误,错误模块也会暴露一个错误信息,但如果是通过XMR请求则不会额外暴露错误信息)
45 | + 批量错误时应该隔一段时间就发一次,不能一直在b栈等用户关闭网页才发 - 统一完善了下
46 | + 将所有 options 变为响应式 - 也就是所有用到了 options 参数时,当动态去改它能实时更新(可能要将所有的变量都变为响应式了) - 也就是支持用户全局动态去改 options - 采用 proxy,如果不兼容则此功能无效
47 | + performance模块的src url应该改名 (requestUrl triggerPageUrl) - 改文档的时候注意
48 | + http模块 src 改名为 requestUrl - 改文档的时候注意
49 | + err模块 url 改名为 triggerPageUrl - 改文档的时候注意
50 | + event模块 url 改名为 triggerPageUrl - 改文档的时候注意
51 | + event-dwell模块 url 改名为 triggerPageUrl - 改文档的时候注意
52 | + pv模块 url 改名为 triggerPageUrl - 改文档的时候注意
53 | + sdk内部所有主动方法都改为只传 options 一个参数,但所有的属性优先级是低于内部定义的
54 | + sdk内部发送方式增加至三种,sendbeacon、xml、img,优先 sendBeacon,其次img,最后为xml(因为增加了错误录屏导致数据量较大,xml不限制请求大小)但xml可能会有跨域问题,使用者应该注意这一问题,且sdk内部会将配置的dsn地址加入请求拦截名单(sendBeacon 有64kb限制 img 有2kb限制)
55 | + beforePushEventList 中的data参数设置为数组
56 | + 兼容 vue2、vue3(错误处理机制已兼容去重)(react以及小程序优先级靠后一些)(vue3的demo项目的err模块会报警告,分别是:rrweb-player初始加载警告、执行 codeError 方法会报警告;均不影响正常调试)
57 | + 实现参数动态更改,目前所有的都可更改,没加限制,所以得提醒注意改的场景不要啥都改
58 | + error 检测那边,应该再加一个属性标识错误类型,例如 console.error 还是 Error 或者 reject - 完成
59 |
60 | ### 二期重构 - 技术层面关键点
61 | 1. options参数集中管理 - 完成
62 | 2. 事件注册以及事件改写集中管理,其他模块需要的则会去模块存放/获取 - 完成
63 | 3. 后面可以用 proxy 来自动化一些东西 - 完成
64 | 4. 数据结构不够直观,再优化下 - 完成
65 |
66 | ### 二期未完成功能
67 | + 首次首屏数据更精确化 - 这个放在最后研究插件化的时候再搞上去
68 | + 后面可以考虑把错误录屏单独拎出来,毕竟数据量太大,使得数据传输方式不可控(目前是 img -> sendBeason -> xhr)
69 | + demo官网示例更简单化且提供在线编写能力
70 | + 可以用浏览器插件的方式去快速加载demo,无需down项目下来本地运行
71 | + sourcemap 错误跟踪 - 做好服务端再来做这个
72 | + 管理端查看功能:能不能做到用户在管理平台上能看到除表格分析外,在页面截图上能标出点击量这些信息,比如在a页面点了b按钮多少次
73 |
74 | ### 二期暂不考虑功能
75 | + 支持对特定dom监控事件(例如监控页面button按钮的点击事件,这在大批量埋点场景中比较需要,另提供一些属性来标识特定按钮无需采集)(这个先不做,可以自己过滤)
76 | + 支持加密传输(加密方式待确定)(这个给用户来做,毕竟钩子已经暴露了)
77 | + 支持断网续联后发送(这个涉及到数据本地存储,得考虑容量大小,且断网状态下的用户操作是无意义的,暂不考虑做)
78 | + 支持静默,思考除了错误的场景下什么情况需要静默(如果是想在规定时间内上传,完全可以通过钩子来实现)
79 | + sendTime err事件 第一个挂载不上(没有复现了,最后再看看是否正常)
80 | + 支持区间打点,区间采集(记录开始和结束,筛选统计开始和结束之间的事件行为,统计到一个分组中) - 个人认为可以通过钩子来解决,所以暂不处理
81 | + 探索:一些关键性的api能让使用者去替代更改、比如监听网络状态的内部实现支持使用者去重写 - 不需要
82 | + 探索:插件化(核心功能+其他插件的形式)- 有利弊,后面需要再去实现
83 | + 白屏检测 - 需要传入临界点时间,例如在进入页面加载完成后超过这个临界点时间还是白屏则计入白屏事件 - 能不能在load后采集一组数据的色值,随后轮训去看看是否一致(这个得有一定的松紧值来限定,得考虑骨架屏gif的情况),毕竟白屏没准也是黑屏 - 蛮复杂,后面多调研一下
84 | + 不做全埋点,会被滥用
85 | + 页面埋点的前缀不可配置
86 | + 例如只想在用户打开某个页面才开始采集,不是这个页面则结束采集 - 这个通过目前的api能达成
87 | + 为 vue3 提供一系列个性化hooks - 目前已有api不太需要
88 | + 此场景怎么做:用户一直点一个带了埋点的按钮,怎么去重呢,简单的节流去重?- 通过目前已有的拦截去做
89 | + 考虑一些枚举也给用户可以改 - 复杂且效益不高
90 | + 用户行为应该给个id,要不然后台不好快速找特定事件,点击按钮可以自己给id,但是切换事件或者曝光就拿不到id了,得找个办法去定义 - 不做,这个可以额外自己给id
91 |
92 | ## 三期(未开始)
93 | + nest完成服务端
94 | + 做一个后台统计端,对所有的数据进行统筹,更方便运营人员查看
95 | + 将demo项目放入到服务器中,方便使用人员快速体验
96 | + 对采集端进行更多数据层面的优化(例如首屏信息)
97 | + 兼容更多端(ps:小程序、uniapp、react....)
--------------------------------------------------------------------------------
/docs/guide/spotlight.md:
--------------------------------------------------------------------------------
1 | # 关于项目
2 |
3 | ## 初衷
4 | 为了帮助开发们在公司平台上搭建一套前端监控平台
5 |
6 | > 作者心声: 想降低一下前端在这方面耗费的时间与精力,此项目会尽量针对每个场景都提供解决方案;即使最后没用我这套,但从在这里对某些场景方案有了一些了解,我也很开心
7 |
8 | ## 亮点
9 | 提供了多种定制化api最大限度帮助你应付各个场景的业务,例如:
10 | + 提供钩子函数让你对数据精确把握
11 | + 提供本地化选项api,让开发手动控制去发送监控数据 - 节省带宽
12 | + 提供批量错误api,在遇到无限错误时融合批量错误信息 - 节省带宽
13 | + 提供抽样发送api - 节省带宽
14 | + 提供 错误/请求 事件的过滤api
15 | + 等等....
16 |
17 | 站在技术角度,因为明确此项目可能更多的是应用在公司平台上,大概率会二开,所以作者对项目结构以及代码都严格要求
18 | + 架构 - demo、核心sdk代码、文档都在同一个项目中,调试、部署都很方便
19 | + 封装 - sdk存在大量的重写或者监听,对此有统一流程
20 | + 响应式 - 项目内部实现了vue响应式,也应用在 options 对象中,相信你接触会后受益良多
21 | + 多版本 - 针对不同平台提供多个版本(目前只有js、vue2、vue3),受益于monorepo架构可一键发布
22 | + 内聚 - 目前核心功能的所有代码都没有分包,虽然monorepo架构支持,但作者认为目前分包不利于代码阅读以及二开方便
23 | + 文档/注释 - 完善的文档以及非常全的注释,力求帮助你快速了解这一切
24 |
25 | ## 未来方向
26 | 会写一套服务端(nest) + 后台查看监控数据平台(vue),有以下几点考量
27 | + 可以在线体验此项目
28 | + 提供更多示例代码给开发们,再次降低这一套代码在公司的推广难度
29 | + 作者也想站在业务的角度多思考还能从哪些方面此项目还缺失哪些功能
30 |
31 | 针对首屏加载的监控做出更多精细化的东西,例如考虑sdk的绝对轻量化
32 |
33 | ## 联系我吧
34 | 欢迎联系我 `微信号: cxh2604856589`
--------------------------------------------------------------------------------
/docs/guide/starting.md:
--------------------------------------------------------------------------------
1 | # Start
2 | WebTracing是一个基于 JavaScript 的埋点SDK
3 |
4 | 它努力为你的前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
5 |
6 | 下面让我们开始逐步了解它吧,相信它不会让你失望
7 |
8 | ::: tip
9 | 以下只展示了【 js、vue2、vue3 】的安装方式,因为目前作者只创建了这些demo项目;因为此sdk是纯js编写,如果您的项目支持浏览器对象那么理论上都会支持
10 | :::
11 |
12 | ## 包总览
13 | ```
14 | // 核心实现包 - js
15 | pnpm install @web-tracing/core
16 |
17 | // vue2版本
18 | pnpm install @web-tracing/vue2
19 |
20 | // vue3版本
21 | pnpm install @web-tracing/vue3
22 | ```
23 |
24 | ## 安装 - HTML & JS
25 | ``` html
26 |
27 |
28 |
29 |
30 |
31 |
32 | Document
33 |
34 |
35 |
66 |
67 |
68 | ```
69 |
70 | ## 安装 - Vue2
71 | ``` js
72 | import WebTracing from '@web-tracing/vue2'
73 |
74 | Vue.use(WebTracing, {
75 | dsn: '/trackweb',
76 | appName: 'cxh',
77 | debug: true,
78 | pv: true,
79 | performance: true,
80 | error: true,
81 | event: true,
82 | cacheMaxLength: 10,
83 | cacheWatingTime: 1000,
84 | userUuid: 'init_userUuid',
85 |
86 | scopeError: true,
87 |
88 | tracesSampleRate: 0.5,
89 |
90 | ignoreErrors: ['111', /^promise/, /.*split is not .* function/],
91 | ignoreRequest: [/getAllTracingList/, /cleanTracingList/],
92 |
93 | beforePushEventList(data) {
94 | const arr = ['intersection', 'click']
95 | data.forEach(item => {
96 | if (arr.includes(item.eventType)) {
97 | window.vm.sendMessage()
98 | }
99 | })
100 | return data
101 | },
102 | beforeSendData(data) {
103 | // 返回false代表sdk不再发送
104 | return data
105 | },
106 | afterSendData(data) {
107 | }
108 | })
109 | ```
110 |
111 | ## 安装 - Vue3
112 | ``` js
113 | import WebTracing from '@web-tracing/vue3'
114 |
115 | app.use(WebTracing, {
116 | dsn: '/trackweb',
117 | appName: 'cxh',
118 | debug: true,
119 | pv: true,
120 | performance: true,
121 | error: true,
122 | event: true,
123 | cacheMaxLength: 10,
124 | cacheWatingTime: 1000,
125 | ignoreRequest: [
126 | /getAllTracingList/,
127 | /cleanTracingList/,
128 | /getBaseInfo/,
129 | /getSourceMap/
130 | ],
131 | afterSendData(data) {
132 | }
133 | })
134 | ```
135 |
136 |
--------------------------------------------------------------------------------
/docs/guide/use/declare.md:
--------------------------------------------------------------------------------
1 | # 基础说明
2 | 帮助您快速了解本项目
3 |
4 | ## 项目架构
5 | 采用 Monorepo + pnpm 方式构建(会加上一些脚本),针对此项目有以下几项优势
6 | + 利于多包(core、vue2、vue3...)联调、发版
7 | + 利于示例项目实时看到效果(包括后续的批量上线)
8 | + 利于文档项目的编写(虽然现在没有联动)
9 |
10 | ## 基本原理
11 |
12 | ### 采集方式
13 | + 自动采集: 内部对多个浏览器事件进行了劫持或者是监听,自动获取 【 错误、性能、页面跳转... 】信息
14 | + 手动采集: 调用sdk暴露的方法去触发事件采集,见[导出项](../functions/exports.md)
15 |
16 | ### 数据流向
17 | 这里针对自动采集的数据流向进行说明
18 |
19 | 1. 内部对多个浏览器事件进行了劫持或者是监听,例如【 click、beforeunload、hashchange、replaceState、popstate...】
20 | 2. 对 监听/劫持 到的事件进行预处理【 例如监听到 replaceState 被触发会提前记录当前时间搓,这样就拿到了页面跳转时的时间啦 】
21 | 3. 每触发一个事件都会生成一个对象来描述此事件的信息,sdk会将这些对象放入列表中(在这个过程中会塞入一些公共信息),等候统一发送
22 |
23 | ### 发送数据
24 | ::: tip
25 | 这里需要了解两个概念
26 | + 最大缓存数(cacheMaxLength 默认为5)
27 | + 延迟发送事件时长(cacheWatingTime 默认为5s)
28 |
29 | 最大缓存数: 在触发一次事件后会生成一个对象描述此事件,但并不会立即将此信息发送到服务端,而是会缓存起来等达到最大缓存数才会将这些采集到的信息组成列表发送给服务端(如果在 `延迟发送事件时长` 内还没有达到最大缓存数,则会将已记录的数据发送,反之在 `延迟发送事件时长` 内达到最大缓存数则立即将事件列表按照 `最大缓存数` 等份切割、分批发送)
30 |
31 | 延迟发送事件时长: 如果在触发一次后迟迟没有达到最大缓存数,达到 `延迟发送事件时长` 后也会将这一次的采集结果发送给服务端;反之已达到则立即发送给服务端
32 | :::
33 |
34 | sdk内部支持多种发送方式
35 | + navigator.sendBeacon
36 | + image
37 | + xml
38 | + 开启本地化(localization)后,数据会存储在 localStorage 中,需要开发手动去发送与清除
39 | + 通过sdk暴露的发送事件拦截事件,拦截所有的事件然后用自己的方式去发送
40 | + 断网后sdk不再主动发送事件
41 |
42 | 发送方式优先级
43 | 1. 浏览器支持sdk会使用 sendBeacon
44 | 2. 其次 image
45 | 3. 如果发送的数据量过大,超过 sendBeacon (60kb限制) 与 image(2kb限制),则该用xml的方式发送
46 |
47 | ## 导出项
48 | sdk内部导出了大量的钩子方便开发自定义,同时也导出了sdk内部的options,开发可动态更改此对象;具体请查看[导出项](../functions/exports.md)
49 |
50 | ::: tip
51 | 导出的钩子是可以被多页面同时调用的,最后触发的顺序会按照初始化的顺序
52 | :::
53 |
54 | 例如以下场景:
55 | + 加密传输 (beforeSendData 拦截到事件信息后再 return新的被加密过的对象)
56 | + 每次发送事件后需要触发弹窗提醒 (afterSendData)
57 | + 中途需要对配置项中的 dsn 地址更改 (任意一个页面 options.value.dsn = 'www.bx.com')
58 | + 获取基础数据用做前端项目的展示 (getBaseInfo)
59 |
60 | ## 事件类型 & 事件ID
61 | 对于采集到的事件对象,内部会含有 `eventType、eventID` 字段,下面对这两个字段进行解释
62 |
63 | ``` ts
64 | /**
65 | * 触发的事件是什么类型 - eventType
66 | */
67 | export enum SEDNEVENTTYPES {
68 | PV = 'pv', // 路由
69 | PVDURATION = 'pv-duration', // 页面停留事件
70 | ERROR = 'error', // 错误
71 | PERFORMANCE = 'performance', // 资源
72 | CLICK = 'click', // 点击
73 | DWELL = 'dwell', // 页面卸载
74 | CUSTOM = 'custom', // 手动触发事件
75 | INTERSECTION = 'intersection' // 曝光采集
76 | }
77 |
78 | /**
79 | * 触发的事件id - eventID
80 | */
81 | export enum SENDID {
82 | PAGE = 'page', // 页面
83 | RESOURCE = 'resource', // 资源
84 | SERVER = 'server', // 请求
85 | CODE = 'code', // code
86 | REJECT = 'reject', // reject
87 | CONSOLEERROR = 'console.error' // console.error
88 | }
89 | ```
90 |
91 | ## 特殊标识
92 | 为了最大程度标识用户以及细分业务,插件提供了以下几个属性
93 | + pageId (应用ID 自动生成)
94 | + sessionId (会话ID 自动生成)
95 | + deviceId (设备ID 自动生成)
96 | + appName (应用Name 使用者初始化设置)
97 | + appCode (应用Code 使用者初始化设置)
98 | + userUuid (用户ID 使用者调用方法设置)
99 |
100 | `pageId sessionId deviceId` 的生成规则是一样的,最终会各自生成类似于这样的字符串
101 | + `13488cb7-85a62e2a-917f1a1d943f5ae5`
102 | + `s_13488cb7-85a6166f-8c296bb4a6089363`
103 | + `t_13466167-991854d1-da9f0cf52c91fac4`
104 |
105 | 注意点
106 | + `pageId` 在整个页面生命周期不变,只会在首次加载插件才会生成
107 | + `sessionId` 会存入cookie,存活时长为30分钟,每次触发采集事件都会刷新这个ID
108 | + `deviceId` 也会存入cookie,不设置存活时长
109 | + `appName` 以及 `appCode` 可以在 `init` 初始化时进行赋值以及后续更改 `options.value.appName`
110 |
111 |
--------------------------------------------------------------------------------
/docs/guide/use/demo.md:
--------------------------------------------------------------------------------
1 | # 示例项目
2 | 目前sdk支持【 js、vue2、vue3 】,项目内部包含有针对这些支持项目的demo版本
3 |
4 | ::: tip
5 | 讲道理react这些也能支持,但由于没有专门去创建这些的demo项目就暂且不进行说明(后续会专门支持)
6 | :::
7 |
8 | + 示例项目目录:web-tracing > examples
9 | + js示例:web-tracing > examples > vanilla
10 | + vue2示例:web-tracing > examples > vue2
11 | + vue3示例:web-tracing > examples > vue3
12 |
13 | [js示例 https://github.com/M-cheng-web/web-tracing-examples-js](https://github.com/M-cheng-web/web-tracing-examples-js)
14 |
15 | [vue2示例 https://github.com/M-cheng-web/web-tracing-examples-vue2](https://github.com/M-cheng-web/web-tracing-examples-vue2)
16 |
17 | [vue3示例 https://github.com/M-cheng-web/web-tracing-examples-vue3](https://github.com/M-cheng-web/web-tracing-examples-vue3)
18 |
19 | > 上面这几个示例项目,是通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 web-tracing 项目中完成的
20 |
21 | ## 初始化
22 | 先 `pnpm install`
23 | ```
24 | 第一步:初始化所有测试项目仓库
25 | pnpm run test:install
26 |
27 | 第二步:打包并监听各个sdk
28 | pnpm run watch
29 |
30 | 第三步:运行js测试项目
31 | pnpm run test:js
32 |
33 | pnpm run test:vue2 (也可以运行vue2测试项目)
34 | pnpm run test:vue3 (也可以运行vue3测试项目)
35 | ```
36 |
37 | ## 在线demo
38 | 目前没有上线,后面会加;目前只能将就在本地运行啦
--------------------------------------------------------------------------------
/docs/guide/use/run.md:
--------------------------------------------------------------------------------
1 | # 本地运行项目
2 | > 建议在这之前看看 [使用 -> 基础说明](./declare.md) 文档
3 |
4 | 项目结构采用 Monorepo + pnpm 方式构建
5 |
6 | ## 项目结构
7 | + 文档项目:web-tracing > docs
8 | + 示例项目:web-tracing > examples
9 | + js示例:web-tracing > examples > vanilla
10 | + vue2示例:web-tracing > examples > vue2
11 | + vue3示例:web-tracing > examples > vue3
12 | + 埋点项目:web-tracing > packages
13 | + js版本:web-tracing > packages > core
14 | + vue2版本:web-tracing > packages > vue2
15 | + vue3版本:web-tracing > packages > vue3
16 | + 构建脚本:web-tracing > scripts
17 |
18 | > web-tracing > packages 下的其他文件只是测试构建脚本作用,后续会删掉
19 |
20 | ## 初始化
21 | 先 `pnpm install`
22 | ```
23 | 第一步:初始化所有测试项目仓库
24 | pnpm run test:install
25 |
26 | 第二步:打包并监听各个sdk
27 | pnpm run watch
28 |
29 | 第三步:运行js测试项目
30 | pnpm run test:js
31 |
32 | pnpm run test:vue2 (也可以运行vue2测试项目)
33 | pnpm run test:vue3 (也可以运行vue3测试项目)
34 | ```
35 |
36 | > web-tracing > package.json 下的其他命令可以自行研究,大部分都是些构建作用
37 |
38 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | title: WebTracing
5 | titleTemplate: 埋点
6 |
7 | hero:
8 | name: WebTracing
9 | text: 为前端项目提供完善的监控手段
10 | image:
11 | src: https://github.com/M-cheng-web/image-provider/raw/main/web-tracing/logo.7k1jidnhjr40.svg
12 | alt: VitePress
13 | actions:
14 | - theme: brand
15 | text: 起步
16 | link: /guide/starting
17 | - theme: alt
18 | text: 关于项目
19 | link: /guide/spotlight
20 | - theme: alt
21 | text: 示例
22 | link: /guide/use/demo
23 | - theme: alt
24 | text: View on GitHub
25 | link: https://github.com/M-cheng-web/web-tracing
26 |
27 | features:
28 | - title: 功能丰富
29 | details: 足以应对大部分前端项目的监控需求
30 | icon: 🚀
31 | - title: 面面俱到
32 | details: 目前已适配 [ js、vue2、vue3 ]
33 | icon: ⚡
34 | - title: 随机应变
35 | details: 提供多种拦截方法、配置项动态更改
36 | icon: 🛠
37 | - title: 珠联璧合
38 | details: demo、文档、sdk核心功能 于一体
39 | icon: 🎪
40 | ---
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-tracing/docs",
3 | "version": "2.0.0-beta.5",
4 | "description": "基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段",
5 | "private": true,
6 | "scripts": {
7 | "docs": "vitepress dev --open",
8 | "docs:build": "vitepress build",
9 | "docs:serve": "vitepress serve",
10 | "docs:publish": "sh scripts/publish-docs.sh"
11 | },
12 | "keywords": [
13 | "埋点",
14 | "性能",
15 | "异常",
16 | "性能采集",
17 | "异常采集",
18 | "前端埋点",
19 | "前端性能采集"
20 | ],
21 | "author": "M-cheng-web <2604856589@qq.com>",
22 | "license": "MIT",
23 | "devDependencies": {
24 | "vitepress": "1.0.0-beta.5"
25 | },
26 | "dependencies": {}
27 | }
28 |
--------------------------------------------------------------------------------
/docs/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig } from 'vite'
3 |
4 | export default defineConfig(async () => {
5 | return {
6 | server: {
7 | hmr: {
8 | overlay: false
9 | },
10 | fs: {
11 | allow: [resolve(__dirname, '..')]
12 | },
13 | host: '0.0.0.0',
14 | port: 8869
15 | },
16 | esbuild: {},
17 | plugins: [],
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/examples/vanilla/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/M-cheng-web/web-tracing/27e4f5b5239a8898734c28578433291a54a38260/examples/vanilla/.DS_Store
--------------------------------------------------------------------------------
/examples/vanilla/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
--------------------------------------------------------------------------------
/examples/vanilla/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
web-tracing 监控插件
4 |
5 | 基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
6 |
7 |
8 |
9 | ## web-tracing-examples-js
10 | [web-tracing](https://github.com/M-cheng-web/web-tracing)的示例项目(js版本)
11 |
12 | 此项目由 [web-tracing -> examples 目录](https://github.com/M-cheng-web/web-tracing/tree/main/examples) 通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 [web-tracing](https://github.com/M-cheng-web/web-tracing) 项目中完成的
13 |
14 | 因为是直接强推的代码,所以本地更新时需要执行:
15 | ```
16 | git fetch --all
17 | git reset --hard origin/main
18 | ```
19 |
20 | ## 运行
21 | ```
22 | pnpm install
23 | pnpm run start
24 | ```
--------------------------------------------------------------------------------
/examples/vanilla/assets/javascript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vanilla/assets/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 |
17 | a {
18 | font-weight: 500;
19 | color: #646cff;
20 | text-decoration: inherit;
21 | }
22 | a:hover {
23 | color: #535bf2;
24 | }
25 |
26 | body {
27 | margin: 0;
28 | display: flex;
29 | place-items: center;
30 | min-width: 320px;
31 | min-height: 100vh;
32 | }
33 |
34 | h1 {
35 | font-size: 3.2em;
36 | line-height: 1.1;
37 | }
38 |
39 | #app {
40 | max-width: 1280px;
41 | margin: 0 auto;
42 | padding: 2rem;
43 | text-align: center;
44 | }
45 |
46 | .logo {
47 | height: 6em;
48 | padding: 1.5em;
49 | will-change: filter;
50 | transition: filter 300ms;
51 | }
52 | .logo:hover {
53 | filter: drop-shadow(0 0 2em #646cffaa);
54 | }
55 | .logo.vanilla:hover {
56 | filter: drop-shadow(0 0 2em #f7df1eaa);
57 | }
58 |
59 | .card {
60 | padding: 2em;
61 | }
62 |
63 | .read-the-docs {
64 | color: #888;
65 | }
66 |
67 | button {
68 | border-radius: 8px;
69 | border: 1px solid transparent;
70 | padding: 0.6em 1.2em;
71 | font-size: 1em;
72 | font-weight: 500;
73 | font-family: inherit;
74 | background-color: #1a1a1a;
75 | cursor: pointer;
76 | transition: border-color 0.25s;
77 | }
78 | button:hover {
79 | border-color: #646cff;
80 | }
81 | button:focus,
82 | button:focus-visible {
83 | outline: 4px auto -webkit-focus-ring-color;
84 | }
85 |
86 | @media (prefers-color-scheme: light) {
87 | :root {
88 | color: #213547;
89 | background-color: #ffffff;
90 | }
91 | a:hover {
92 | color: #747bff;
93 | }
94 | button {
95 | background-color: #f9f9f9;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/examples/vanilla/assets/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vanilla/index.ts:
--------------------------------------------------------------------------------
1 | // ---------------- Error 捕捉 ----------------
2 | document.getElementById('codeErr')?.addEventListener('click', () => {
3 | codeError()
4 | })
5 |
6 | function codeError() {
7 | const a = {}
8 | a.split('/')
9 | }
10 | function promiseError() {
11 | const promiseWrap = () =>
12 | new Promise((resolve, reject) => {
13 | reject('promise reject')
14 | })
15 | promiseWrap().then(res => {
16 | console.log('res', res)
17 | })
18 | }
19 | function consoleErr() {
20 | console.error('consoleErr1', 'consoleErr1.1', 'consoleErr1.2')
21 | // console.error(111);
22 | // console.error(new Error("谢谢谢谢谢"));
23 | }
24 |
--------------------------------------------------------------------------------
/examples/vanilla/main.ts:
--------------------------------------------------------------------------------
1 | import {
2 | init,
3 | beforePushEventList,
4 | beforeSendData,
5 | afterSendData,
6 | setUserUuid,
7 | getBaseInfo,
8 | getFirstScreen,
9 | getIPs
10 | } from '@web-tracing/core'
11 |
12 | start()
13 |
14 | // beforePushEventList(data => {
15 | // console.log('-----------beforePushEventList--0', data)
16 | // return data
17 | // })
18 |
19 | // setTimeout(() => {
20 | // beforePushEventList(data => {
21 | // console.log('-----------beforePushEventList1', data)
22 | // return data
23 | // })
24 | // beforePushEventList(data => {
25 | // console.log('-----------beforePushEventList2', data)
26 | // return data
27 | // })
28 | // beforeSendData(data => {
29 | // console.log('-----------beforeSendData1', data)
30 | // return data
31 | // })
32 | // beforeSendData(data => {
33 | // console.log('-----------beforeSendData2', data)
34 | // return data
35 | // })
36 | // afterSendData(data => {
37 | // console.log('-----------afterSendData1', data)
38 | // })
39 | // afterSendData(data => {
40 | // console.log('-----------afterSendData2', data)
41 | // })
42 | // setUserUuid('xxxxxxxx1')
43 | // console.log('getBaseInfo', getBaseInfo())
44 | // console.log('getFirstScreen', getFirstScreen())
45 | // }, 2000)
46 |
47 | // // 手动发送错误
48 | // setTimeout(() => {
49 | // traceError('自定义错误ID', '自定义错误message', {
50 | // src: '/interface/order',
51 | // params: {
52 | // id: '12121'
53 | // }
54 | // })
55 | // }, 3000)
56 |
57 | // // 手动发送资源事件
58 | // setTimeout(() => {
59 | // tracePerformance('自定义ID', {
60 | // params: {
61 | // param1: 'param1222',
62 | // param2: 'param2',
63 | // param3: 'param3'
64 | // }
65 | // })
66 | // }, 3000)
67 |
68 | // // 手动发送点击事件
69 | // setTimeout(() => {
70 | // traceCustomEvent('自定义ID', '自定义事件标题', {
71 | // params: {
72 | // params1: 'params1',
73 | // params2: 'params2',
74 | // params3: 'params3'
75 | // }
76 | // })
77 | // }, 3000)
78 |
79 | // // 手动发送pv事件
80 | // setTimeout(() => {
81 | // tracePageView({
82 | // url: '自定义URL',
83 | // referer: '自定义上级URL',
84 | // params: { name: '自定义name' },
85 | // actions: 'reserved'
86 | // })
87 | // }, 3000)
88 |
89 | function start() {
90 | init({
91 | // dsn: 'https://cdn.staticaly.com/gh/M-cheng-web/image-provider@main/blog/Annapurna-Ranges-2560x1440.5r9m9t5vg1g0.webp',
92 | dsn: 'http://1.15.224.10:22/trackweb/tra',
93 | appName: 'cxh',
94 | debug: true,
95 | pv: true,
96 | // performance: true,
97 | // error: true,
98 | event: true,
99 | // localization: true,
100 | cacheMaxLength: 10,
101 | cacheWatingTime: 1000,
102 | userUuid: 'init_userUuid',
103 |
104 | scopeError: true,
105 |
106 | // tracesSampleRate: 0.5,
107 |
108 | // ignoreErrors: ['111', /^promise/, /.*split is not .* function/],
109 | // ignoreRequest: ['111', /normal/],
110 |
111 | beforePushEventList(data) {
112 | // console.log('beforePushEventList-data', data)
113 | return data
114 | },
115 | beforeSendData(data) {
116 | // console.log('beforeSendData-data', data)
117 | // return { xx: 2123 }
118 | // 返回false代表sdk不再发送
119 | // return false
120 | return data
121 | },
122 | afterSendData(data) {
123 | // console.log('afterSendData-data', data)
124 | }
125 | })
126 | }
127 |
--------------------------------------------------------------------------------
/examples/vanilla/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview"
9 | },
10 | "dependencies": {
11 | "@web-tracing/core": "workspace:*"
12 | },
13 | "devDependencies": {
14 | "vite": "^4.2.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/vanilla/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 |
3 | export default defineConfig({
4 | base: './',
5 | plugins: [],
6 | build: {
7 | sourcemap: true
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/examples/vue2/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true
6 | },
7 | extends: ['eslint:recommended', 'plugin:prettier/recommended'],
8 | overrides: [],
9 | parser: 'vue-eslint-parser',
10 | parserOptions: {
11 | ecmaVersion: 'latest',
12 | sourceType: 'module'
13 | },
14 | plugins: ['vue'],
15 | rules: {
16 | 'no-debugger': 'warn',
17 | 'prettier/prettier': [
18 | 'error',
19 | {
20 | semi: false,
21 | trailingComma: 'none',
22 | arrowParens: 'avoid',
23 | singleQuote: true,
24 | endOfLine: 'auto'
25 | }
26 | ],
27 | 'vue/return-in-computed-property': 'off',
28 | 'vue/no-multiple-template-root': 'off',
29 | 'vue/multi-word-component-names': 'off'
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/vue2/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
--------------------------------------------------------------------------------
/examples/vue2/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
web-tracing 监控插件
4 |
5 | 基于 JS 跨平台插件,为前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
6 |
7 |
8 |
9 | ## web-tracing-examples-vue2
10 | [web-tracing](https://github.com/M-cheng-web/web-tracing)的示例项目(vue2版本)
11 |
12 | 此项目由 [web-tracing -> examples 目录](https://github.com/M-cheng-web/web-tracing/tree/main/examples) 通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 [web-tracing](https://github.com/M-cheng-web/web-tracing) 项目中完成的
13 |
14 | 因为是直接强推的代码,所以本地更新时需要执行:
15 | ```
16 | git fetch --all
17 | git reset --hard origin/main
18 | ```
19 |
20 | ## 运行
21 | ```
22 | pnpm install
23 | pnpm run start
24 | ```
--------------------------------------------------------------------------------
/examples/vue2/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | example-vue2
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/vue2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "start": "concurrently \"npm run dev\" \"npm run service\"",
8 | "dev": "vite",
9 | "service": "nodemon ./server.js",
10 | "build": "vue-tsc && vite build",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@web-tracing/vue2": "workspace:*",
15 | "axios": "^1.4.0",
16 | "body-parser": "^1.20.2",
17 | "co-body": "^6.1.0",
18 | "concurrently": "^8.1.0",
19 | "element-ui": "^2.15.13",
20 | "express": "^4.18.2",
21 | "nodemon": "^2.0.22",
22 | "rrweb-player": "1.0.0-alpha.4",
23 | "sass": "^1.62.1",
24 | "sass-loader": "^13.3.1",
25 | "vue": "2.6.10",
26 | "vue-router": "3.0.6",
27 | "vue-template-compiler": "2.6.10",
28 | "vuex": "3.1.0"
29 | },
30 | "devDependencies": {
31 | "eslint": "^8.23.1",
32 | "eslint-config-prettier": "^8.5.0",
33 | "eslint-plugin-prettier": "^4.2.1",
34 | "eslint-plugin-vue": "^9.4.0",
35 | "prettier": "^2.7.1",
36 | "typescript": "^4.9.3",
37 | "vite": "^4.2.0",
38 | "vite-plugin-vue2": "^2.0.3",
39 | "vue-tsc": "^1.2.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/vue2/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vue2/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | const app = express()
3 | // import { join } from 'path'
4 | // import { readFile } from 'fs'
5 | import pkg from 'body-parser'
6 | import coBody from 'co-body'
7 |
8 | const { json, urlencoded } = pkg
9 |
10 | app.use(json({ limit: '100mb' }))
11 | app.use(
12 | urlencoded({
13 | limit: '100mb',
14 | extended: true,
15 | parameterLimit: 50000
16 | })
17 | )
18 |
19 | app.all('*', function (res, req, next) {
20 | req.header('Access-Control-Allow-Origin', '*')
21 | req.header('Access-Control-Allow-Headers', 'Content-Type')
22 | req.header('Access-Control-Allow-Methods', '*')
23 | req.header('Content-Type', 'application/json;charset=utf-8')
24 | next()
25 | })
26 |
27 | app.get('/getList', (req, res) => {
28 | console.log('req.query', req.query)
29 | res.send({
30 | code: 200,
31 | data: [1, 2, 3]
32 | })
33 | })
34 | app.post('/setList', (req, res) => {
35 | res.send({
36 | code: 200,
37 | meaage: '设置成功'
38 | })
39 | })
40 |
41 | let allTracingList = []
42 | let baseInfo = {}
43 |
44 | app.get('/getBaseInfo', (req, res) => {
45 | res.send({
46 | code: 200,
47 | data: baseInfo
48 | })
49 | })
50 | app.post('/cleanTracingList', (req, res) => {
51 | allTracingList = []
52 | res.send({
53 | code: 200,
54 | meaage: '清除成功!'
55 | })
56 | })
57 | app.get('/getAllTracingList', (req, res) => {
58 | const eventType = req.query.eventType
59 | if (eventType) {
60 | // const data = JSON.parse(JSON.stringify(allTracingList)).reverse()
61 | const data = JSON.parse(JSON.stringify(allTracingList))
62 | res.send({
63 | code: 200,
64 | data: data.filter(item => item.eventType === eventType)
65 | })
66 | } else {
67 | res.send({
68 | code: 200,
69 | data: allTracingList
70 | })
71 | }
72 | })
73 | app.post('/trackweb', async (req, res) => {
74 | try {
75 | let length = Object.keys(req.body).length
76 | if (length) {
77 | // 数据量大时不会用 sendbeacon,会用xhr的形式,这里是接收xhr的数据格式
78 | allTracingList.push(...req.body.eventInfo)
79 | } else {
80 | // 兼容 sendbeacon 的传输数据格式
81 | const data = await coBody.json(req)
82 | if (!data) return
83 | allTracingList.push(...data.eventInfo)
84 | baseInfo = data.baseInfo
85 | }
86 | res.send({
87 | code: 200,
88 | meaage: '上报成功!'
89 | })
90 | } catch (err) {
91 | res.send({
92 | code: 203,
93 | meaage: '上报失败!',
94 | err
95 | })
96 | }
97 | })
98 |
99 | // 图片上传的方式
100 | app.get('/trackweb', async (req, res) => {
101 | try {
102 | let data = req.query.v
103 | if (!data) return
104 | data = JSON.parse(data)
105 | allTracingList.push(...data.eventInfo)
106 | baseInfo = data.baseInfo
107 | res.send({
108 | code: 200,
109 | data: '上报成功'
110 | })
111 | } catch (err) {
112 | res.send({
113 | code: 203,
114 | meaage: '上报失败!',
115 | err
116 | })
117 | }
118 | })
119 |
120 | app.listen(3351, () => {
121 | console.log('Server is running at http://localhost:3351')
122 | })
123 |
--------------------------------------------------------------------------------
/examples/vue2/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 | 查看核心基础信息
11 |
12 |
13 |
14 | 清除所有事件信息
15 |
16 |
17 |
18 |
19 |
20 |
84 |
85 |
113 |
114 |
126 |
--------------------------------------------------------------------------------
/examples/vue2/src/assets/global.scss:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100%;
3 | margin: 0;
4 | -moz-osx-font-smoothing: grayscale;
5 | -webkit-font-smoothing: antialiased;
6 | text-rendering: optimizeLegibility;
7 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
8 | }
9 |
10 | label {
11 | font-weight: 700;
12 | }
13 |
14 | html {
15 | height: 100%;
16 | box-sizing: border-box;
17 | }
18 |
19 | .mb {
20 | margin-bottom: 20px;
21 | }
22 |
23 | .event-pop {
24 | .pop-line:first-child {
25 | border-bottom: 1px solid #0984e3;
26 | }
27 | .pop-line:not(:first-child) {
28 | & > div {
29 | color: #0984e3;
30 | }
31 | }
32 | .pop-line {
33 | position: relative;
34 | display: flex;
35 | align-items: center;
36 | & > span {
37 | position: absolute;
38 | left: -16px;
39 | }
40 | & > div {
41 | flex: 1;
42 | white-space: nowrap;
43 | overflow: hidden;
44 | text-overflow: ellipsis;
45 | }
46 | }
47 | .warning-text {
48 | color: red;
49 | }
50 | }
51 |
52 | #app {
53 | height: 100%;
54 | }
55 |
56 | *,
57 | *:before,
58 | *:after {
59 | box-sizing: inherit;
60 | }
61 |
62 | a:focus,
63 | a:active {
64 | outline: none;
65 | }
66 |
67 | a,
68 | a:focus,
69 | a:hover {
70 | cursor: pointer;
71 | color: inherit;
72 | text-decoration: none;
73 | }
74 |
75 | div:focus {
76 | outline: none;
77 | }
78 |
--------------------------------------------------------------------------------
/examples/vue2/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vue2/src/components/CTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
20 |
28 |
29 | {{ scope.row[item.prop] }}
30 |
31 |
32 |
33 |
34 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
69 |
70 |
71 |
72 |
113 |
114 |
134 |
--------------------------------------------------------------------------------
/examples/vue2/src/components/MenuList.vue:
--------------------------------------------------------------------------------
1 |
2 |
40 |
41 |
42 |
69 |
70 |
76 |
--------------------------------------------------------------------------------
/examples/vue2/src/components/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description: 项目级别公共组件注册
3 | */
4 |
5 | import Vue from 'vue'
6 |
7 | export function setupComponent() {
8 | const modulesFiles = import.meta.globEager('./*.vue')
9 |
10 | for (const path in modulesFiles) {
11 | const componentName = modulesFiles[path].default.name
12 | Vue.component(componentName, modulesFiles[path].default)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/vue2/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import { dynamicRouterMap } from './router.dynamic.js'
4 |
5 | Vue.use(Router)
6 |
7 | export const constantRoutes = [...dynamicRouterMap]
8 |
9 | const createRouter = () =>
10 | new Router({
11 | // mode: 'history',
12 | mode: 'hash',
13 | scrollBehavior: () => ({ y: 0 }),
14 | routes: constantRoutes
15 | })
16 |
17 | const router = createRouter()
18 |
19 | export default router
20 |
--------------------------------------------------------------------------------
/examples/vue2/src/router/router.dynamic.js:
--------------------------------------------------------------------------------
1 | export const dynamicRouterMap = [
2 | {
3 | path: '/',
4 | redirect: '/home'
5 | },
6 | {
7 | path: '/home',
8 | name: 'Home',
9 | component: () => import('@/views/home/index.vue'),
10 | meta: {
11 | title: '首页',
12 | icon: 'el-icon-setting'
13 | }
14 | },
15 | {
16 | path: '/err',
17 | name: 'Err',
18 | component: () => import('@/views/err/index.vue'),
19 | meta: {
20 | title: '监控 - 错误',
21 | icon: 'el-icon-setting'
22 | }
23 | },
24 | {
25 | path: '/event',
26 | name: 'Event',
27 | component: () => import('@/views/event/index.vue'),
28 | meta: {
29 | title: '监控 - 点击事件',
30 | icon: 'el-icon-setting'
31 | }
32 | },
33 | {
34 | path: '/http',
35 | name: 'Http',
36 | component: () => import('@/views/http/index.vue'),
37 | meta: {
38 | title: '监控 - 请求',
39 | icon: 'el-icon-setting'
40 | }
41 | },
42 | {
43 | path: '/performance',
44 | name: 'Performance',
45 | component: () => import('@/views/performance/index.vue'),
46 | meta: {
47 | title: '监控 - 资源',
48 | icon: 'el-icon-setting'
49 | }
50 | },
51 | {
52 | path: '/pv',
53 | name: 'Pv',
54 | component: () => import('@/views/pv/index.vue'),
55 | meta: {
56 | title: '监控 - 页面跳转',
57 | icon: 'el-icon-setting'
58 | }
59 | },
60 | {
61 | path: '/intersection',
62 | name: 'intersection',
63 | component: () => import('@/views/intersection/index.vue'),
64 | meta: {
65 | title: '监控 - 曝光采集',
66 | icon: 'el-icon-setting'
67 | }
68 | }
69 | ]
70 |
--------------------------------------------------------------------------------
/examples/vue2/src/views/event/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
21 |
26 | 点我一个试试
27 | 再点我一个试试
28 |
29 |
38 |
39 |
40 |
41 | 获取最新采集数据
42 |
43 |
暂停采集
44 |
恢复采集
45 |
50 |
51 | {{ `${scope.index + 1}` }}
52 |
53 |
54 | {{ `${formatDate(scope.row.sendTime)}` }}
55 |
56 |
57 | {{ `${formatDate(scope.row.triggerTime)}` }}
58 |
59 |
60 |
61 |
62 |
63 |
122 |
123 |
148 |
--------------------------------------------------------------------------------
/examples/vue2/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 所有的事件类型:
5 |
{{ `${key}: ${value}` }}
6 |
7 |
8 | 所有的事件ID(还有一些id是随机字符串的):
9 |
{{ `${key}: ${value}` }}
10 |
11 |
12 |
13 |
14 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/vue2/src/views/pv/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | navigate - 网页通过点击链接,地址栏输入,表单提交,脚本操作等方式加载
12 |
13 | reload - 网页通过“重新加载”按钮或者location.reload()方法加载
14 | back_forward - 网页通过“前进”或“后退”按钮加载
15 | reserved - 任何其他来源的加载
16 |
17 |
18 |
19 |
20 | 获取最新采集数据
21 |
22 |
27 |
28 | {{ `${scope.index + 1}` }}
29 |
30 |
31 | {{ `${formatDate(scope.row.sendTime)}` }}
32 |
33 |
34 | {{ `${formatDate(scope.row.triggerTime)}` }}
35 |
36 |
37 |
38 |
39 |
40 |
94 |
95 |
99 |
--------------------------------------------------------------------------------
/examples/vue2/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { createVuePlugin } from 'vite-plugin-vue2'
3 | import { resolve } from 'path'
4 |
5 | export default defineConfig({
6 | plugins: [createVuePlugin()],
7 | server: {
8 | https: false,
9 | host: '0.0.0.0',
10 | port: 6656,
11 | cors: true,
12 | proxy: {
13 | '/getList': {
14 | target: 'http://localhost:3351/',
15 | changeOrigin: false, // target是域名的话,需要这个参数,
16 | secure: false // 设置支持https协议的代理,
17 | },
18 | '/setList': {
19 | target: 'http://localhost:3351/',
20 | changeOrigin: false, // target是域名的话,需要这个参数,
21 | secure: false // 设置支持https协议的代理,
22 | },
23 | '/cleanTracingList': {
24 | target: 'http://localhost:3351/',
25 | changeOrigin: false,
26 | secure: false
27 | },
28 | '/getBaseInfo': {
29 | target: 'http://localhost:3351'
30 | },
31 | '/getAllTracingList': {
32 | target: 'http://localhost:3351'
33 | },
34 | '/trackweb': {
35 | target: 'http://localhost:3351'
36 | }
37 | }
38 | },
39 | resolve: {
40 | alias: [
41 | {
42 | find: '@',
43 | replacement: resolve(__dirname, 'src')
44 | }
45 | ]
46 | }
47 | })
48 |
--------------------------------------------------------------------------------
/examples/vue3/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true
6 | },
7 | extends: ['eslint:recommended', 'plugin:prettier/recommended'],
8 | overrides: [],
9 | parser: 'vue-eslint-parser',
10 | parserOptions: {
11 | ecmaVersion: 'latest',
12 | sourceType: 'module'
13 | },
14 | plugins: ['vue'],
15 | rules: {
16 | 'no-debugger': 'warn',
17 | 'prettier/prettier': [
18 | 'error',
19 | {
20 | semi: false,
21 | trailingComma: 'none',
22 | arrowParens: 'avoid',
23 | singleQuote: true,
24 | endOfLine: 'auto'
25 | }
26 | ],
27 | 'vue/return-in-computed-property': 'off',
28 | 'vue/no-multiple-template-root': 'off',
29 | 'vue/multi-word-component-names': 'off',
30 | '@typescript-eslint/ban-ts-comment': 'off'
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/vue3/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
--------------------------------------------------------------------------------
/examples/vue3/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
web-tracing 监控插件
4 |
5 | 基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
6 |
7 |
8 |
9 | ## web-tracing-examples-vue3
10 | [web-tracing](https://github.com/M-cheng-web/web-tracing)的示例项目(vue3版本)
11 |
12 | 此项目由 [web-tracing -> examples -> vue3](https://github.com/M-cheng-web/web-tracing/tree/main/examples/vue3) 通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 [web-tracing](https://github.com/M-cheng-web/web-tracing) 项目中完成的
13 |
14 | 因为是直接强推的代码,所以本地更新时需要执行:
15 | ```
16 | git fetch --all
17 | git reset --hard origin/main
18 | ```
19 |
20 | ## 运行
21 | ```
22 | pnpm install
23 | pnpm run start
24 | ```
25 |
--------------------------------------------------------------------------------
/examples/vue3/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | example-vue3
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/vue3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "start": "concurrently \"npm run dev\" \"npm run service\"",
8 | "dev": "vite",
9 | "service": "nodemon ./server.js",
10 | "build": "vite build",
11 | "build-tsc": "vue-tsc && vite build",
12 | "preview": "vite preview"
13 | },
14 | "dependencies": {
15 | "@web-tracing/vue3": "workspace:*",
16 | "axios": "^1.4.0",
17 | "body-parser": "^1.20.2",
18 | "co-body": "^6.1.0",
19 | "concurrently": "^8.2.0",
20 | "element-plus": "^2.3.7",
21 | "nodemon": "^2.0.22",
22 | "rrweb-player": "1.0.0-alpha.4",
23 | "sass": "^1.63.6",
24 | "sass-loader": "^13.3.2",
25 | "source-map-js": "^1.0.2",
26 | "vue": "^3.2.47",
27 | "vue-router": "^4.2.2"
28 | },
29 | "devDependencies": {
30 | "@vitejs/plugin-vue": "^4.1.0",
31 | "eslint": "^8.36.0",
32 | "eslint-config-prettier": "^8.8.0",
33 | "eslint-plugin-prettier": "^4.2.1",
34 | "eslint-plugin-vue": "^9.15.0",
35 | "prettier": "^2.8.7",
36 | "typescript": "^4.9.3",
37 | "vite": "^4.2.0",
38 | "vue-tsc": "^1.2.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/vue3/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vue3/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import path from 'path'
3 | import fs from 'fs'
4 | import pkg from 'body-parser'
5 | import coBody from 'co-body'
6 |
7 | const app = express()
8 | const { json, urlencoded } = pkg
9 |
10 | app.use(json({ limit: '100mb' }))
11 | app.use(
12 | urlencoded({
13 | limit: '100mb',
14 | extended: true,
15 | parameterLimit: 50000
16 | })
17 | )
18 |
19 | app.all('*', function (res, req, next) {
20 | req.header('Access-Control-Allow-Origin', '*')
21 | req.header('Access-Control-Allow-Headers', 'Content-Type')
22 | req.header('Access-Control-Allow-Methods', '*')
23 | req.header('Content-Type', 'application/json;charset=utf-8')
24 | next()
25 | })
26 |
27 | // 获取js.map源码文件
28 | app.get('/getSourceMap', (req, res) => {
29 | const { fileName, env } = req.query
30 | console.log('fileName', fileName)
31 | console.log('env', env)
32 | if (env === 'development') {
33 | // const mapFile = path.join(__filename, '..', fileName)
34 | // console.log('mapFile', mapFile)
35 | fs.readFile(fileName, (err, data) => {
36 | if (err) {
37 | console.error('server-getmap', err)
38 | return
39 | }
40 | res.send(data)
41 | })
42 | } else {
43 | // req.query 获取接口参数
44 | const mapFile = path.join(__filename, '..', 'dist/assets')
45 | // 拿到dist目录下对应map文件的路径
46 | const mapPath = path.join(mapFile, `${fileName}.map`)
47 | fs.readFile(mapPath, (err, data) => {
48 | if (err) {
49 | console.error('server-getmap', err)
50 | return
51 | }
52 | res.send(data)
53 | })
54 | }
55 | })
56 |
57 | app.get('/getList', (req, res) => {
58 | console.log('req.query', req.query)
59 | res.send({
60 | code: 200,
61 | data: [1, 2, 3]
62 | })
63 | })
64 | app.post('/setList', (req, res) => {
65 | res.send({
66 | code: 200,
67 | meaage: '设置成功'
68 | })
69 | })
70 |
71 | let allTracingList = []
72 | let baseInfo = {}
73 |
74 | app.get('/getBaseInfo', (req, res) => {
75 | res.send({
76 | code: 200,
77 | data: baseInfo
78 | })
79 | })
80 | app.post('/cleanTracingList', (req, res) => {
81 | allTracingList = []
82 | res.send({
83 | code: 200,
84 | meaage: '清除成功!'
85 | })
86 | })
87 | app.get('/getAllTracingList', (req, res) => {
88 | const eventType = req.query.eventType
89 | if (eventType) {
90 | // const data = JSON.parse(JSON.stringify(allTracingList)).reverse()
91 | const data = JSON.parse(JSON.stringify(allTracingList))
92 | res.send({
93 | code: 200,
94 | data: data.filter(item => item.eventType === eventType)
95 | })
96 | } else {
97 | res.send({
98 | code: 200,
99 | data: allTracingList
100 | })
101 | }
102 | })
103 | app.post('/trackweb', async (req, res) => {
104 | try {
105 | let length = Object.keys(req.body).length
106 | if (length) {
107 | // 数据量大时不会用 sendbeacon,会用xhr的形式,这里是接收xhr的数据格式
108 | allTracingList.push(...req.body.eventInfo)
109 | baseInfo = req.body.baseInfo
110 | } else {
111 | // 兼容 sendbeacon 的传输数据格式
112 | const data = await coBody.json(req)
113 | if (!data) return
114 | allTracingList.push(...data.eventInfo)
115 | baseInfo = data.baseInfo
116 | }
117 | res.send({
118 | code: 200,
119 | meaage: '上报成功!'
120 | })
121 | } catch (err) {
122 | res.send({
123 | code: 203,
124 | meaage: '上报失败!',
125 | err
126 | })
127 | }
128 | })
129 |
130 | // 图片上传的方式
131 | app.get('/trackweb', async (req, res) => {
132 | try {
133 | let data = req.query.v
134 | if (!data) return
135 | data = JSON.parse(data)
136 | allTracingList.push(...data.eventInfo)
137 | baseInfo = data.baseInfo
138 | res.send({
139 | code: 200,
140 | data: '上报成功'
141 | })
142 | } catch (err) {
143 | res.send({
144 | code: 203,
145 | meaage: '上报失败!',
146 | err
147 | })
148 | }
149 | })
150 |
151 | app.listen(3352, () => {
152 | console.log('Server is running at http://localhost:3352')
153 | })
154 |
--------------------------------------------------------------------------------
/examples/vue3/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 查看核心基础信息
13 |
14 |
15 |
16 | 清除所有事件信息
17 |
18 |
19 |
20 |
21 |
22 |
109 |
110 |
138 |
139 |
151 |
--------------------------------------------------------------------------------
/examples/vue3/src/assets/global.scss:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100%;
3 | margin: 0;
4 | -moz-osx-font-smoothing: grayscale;
5 | -webkit-font-smoothing: antialiased;
6 | text-rendering: optimizeLegibility;
7 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
8 | }
9 |
10 | label {
11 | font-weight: 700;
12 | }
13 |
14 | html {
15 | height: 100%;
16 | box-sizing: border-box;
17 | }
18 |
19 | .mb {
20 | margin-bottom: 20px;
21 | }
22 |
23 | .event-pop {
24 | .pop-line:first-child {
25 | border-bottom: 1px solid #0984e3;
26 | }
27 | .pop-line:not(:first-child) {
28 | & > div {
29 | color: #0984e3;
30 | }
31 | }
32 | .pop-line {
33 | position: relative;
34 | display: flex;
35 | align-items: center;
36 | & > span {
37 | position: absolute;
38 | left: -16px;
39 | }
40 | & > div {
41 | flex: 1;
42 | white-space: nowrap;
43 | overflow: hidden;
44 | text-overflow: ellipsis;
45 | }
46 | }
47 | .warning-text {
48 | color: red;
49 | }
50 | }
51 |
52 | #app {
53 | height: 100%;
54 | }
55 |
56 | *,
57 | *:before,
58 | *:after {
59 | box-sizing: inherit;
60 | }
61 |
62 | a:focus,
63 | a:active {
64 | outline: none;
65 | }
66 |
67 | a,
68 | a:focus,
69 | a:hover {
70 | cursor: pointer;
71 | color: inherit;
72 | text-decoration: none;
73 | }
74 |
75 | div:focus {
76 | outline: none;
77 | }
78 |
--------------------------------------------------------------------------------
/examples/vue3/src/components/CTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
20 |
28 |
29 | {{ scope.row[item.prop] }}
30 |
31 |
32 |
33 |
34 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
69 |
70 |
71 |
72 |
77 |
78 |
115 |
116 |
136 |
--------------------------------------------------------------------------------
/examples/vue3/src/components/MenuList.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
40 |
45 |
46 |
60 |
61 |
67 |
--------------------------------------------------------------------------------
/examples/vue3/src/components/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'vue'
2 |
3 | interface ObjType {
4 | [propName: string]: object
5 | }
6 | interface filesType extends ObjType {
7 | default: {
8 | __name: string
9 | [key: string]: any
10 | }
11 | }
12 |
13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
14 | // @ts-ignore
15 | const files: Record = import.meta.globEager('./*.vue')
16 |
17 | export default (app: App) => {
18 | Object.keys(files).forEach(path => {
19 | const name = files[path].default.name
20 | app.component(name, files[path].default)
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/examples/vue3/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 | import ElementPlus from 'element-plus'
4 | import 'element-plus/dist/index.css'
5 | import WebTracing from '@web-tracing/vue3'
6 | import router from './router'
7 | import './assets/global.scss'
8 | import initComponents from './components/index'
9 | import { ElNotification } from 'element-plus'
10 |
11 | const app = createApp(App)
12 |
13 | const sendEventType: any = {
14 | pv: '路由',
15 | error: '错误',
16 | performance: '资源',
17 | click: '点击',
18 | dwell: '页面卸载',
19 | intersection: '曝光采集'
20 | }
21 |
22 | app.use(WebTracing, {
23 | dsn: '/trackweb',
24 | appName: 'cxh',
25 | debug: true,
26 | pv: true,
27 | performance: true,
28 | error: true,
29 | event: true,
30 | cacheMaxLength: 10,
31 | cacheWatingTime: 1000,
32 |
33 | // 查询埋点信息、清除埋点信息、获取埋点基础信息 不需要进行捕获
34 | ignoreRequest: [
35 | /getAllTracingList/,
36 | /cleanTracingList/,
37 | /getBaseInfo/,
38 | /getSourceMap/
39 | ],
40 |
41 | // 发送埋点数据后,拉起弹窗提示用户已发送
42 | afterSendData(data) {
43 | const { sendType, success, params } = data
44 | const message = `
45 |
46 |
打开控制台可查看更多详细信息
47 |
发送是否成功: ${success}
48 |
发送方式: ${sendType}
49 |
发送内容(只概括 eventType、eventId)
50 | ${params.eventInfo.reduce(
51 | (pre: string, item: any, index: number) => {
52 | pre += `
53 |
54 |
${index + 1}
55 |
${item.eventType}(${sendEventType[item.eventType]})
56 |
${item.eventId}
57 |
`
58 | return pre
59 | },
60 | `
61 |
eventType
62 |
eventId
63 |
`
64 | )}
65 |
66 |
67 | `
68 | ElNotification({
69 | title: '发送一批数据到服务端',
70 | message,
71 | position: 'top-right',
72 | dangerouslyUseHTMLString: true
73 | })
74 | // @ts-ignore
75 | if (window.getAllTracingList) {
76 | // @ts-ignore
77 | window.getAllTracingList()
78 | }
79 | }
80 | })
81 |
82 | app.use(router)
83 | app.use(initComponents)
84 | app.use(ElementPlus)
85 | app.mount('#app')
86 |
--------------------------------------------------------------------------------
/examples/vue3/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createRouter,
3 | createWebHashHistory
4 | // createWebHistory
5 | } from 'vue-router'
6 | import { dynamicRouterMap } from './router.dynamic'
7 |
8 | const router = createRouter({
9 | history: createWebHashHistory(),
10 | routes: dynamicRouterMap,
11 | scrollBehavior() {
12 | return {
13 | top: 0,
14 | behavior: 'smooth'
15 | }
16 | }
17 | })
18 |
19 | export { router as default, dynamicRouterMap }
20 |
--------------------------------------------------------------------------------
/examples/vue3/src/router/router.dynamic.ts:
--------------------------------------------------------------------------------
1 | export const dynamicRouterMap = [
2 | {
3 | path: '/',
4 | redirect: '/home'
5 | },
6 | {
7 | path: '/home',
8 | name: 'Home',
9 | component: () => import('@/views/home/index.vue'),
10 | meta: {
11 | title: '首页',
12 | icon: 'el-icon-setting'
13 | }
14 | },
15 | {
16 | path: '/err',
17 | name: 'Err',
18 | component: () => import('@/views/err/index.vue'),
19 | meta: {
20 | title: '监控 - 错误',
21 | icon: 'el-icon-setting'
22 | }
23 | },
24 | {
25 | path: '/event',
26 | name: 'Event',
27 | component: () => import('@/views/event/index.vue'),
28 | meta: {
29 | title: '监控 - 点击事件',
30 | icon: 'el-icon-setting'
31 | }
32 | },
33 | {
34 | path: '/http',
35 | name: 'Http',
36 | component: () => import('@/views/http/index.vue'),
37 | meta: {
38 | title: '监控 - 请求',
39 | icon: 'el-icon-setting'
40 | }
41 | },
42 | {
43 | path: '/performance',
44 | name: 'Performance',
45 | component: () => import('@/views/performance/index.vue'),
46 | meta: {
47 | title: '监控 - 资源',
48 | icon: 'el-icon-setting'
49 | }
50 | },
51 | {
52 | path: '/pv',
53 | name: 'Pv',
54 | component: () => import('@/views/pv/index.vue'),
55 | meta: {
56 | title: '监控 - 页面跳转',
57 | icon: 'el-icon-setting'
58 | }
59 | },
60 | {
61 | path: '/intersection',
62 | name: 'intersection',
63 | component: () => import('@/views/intersection/index.vue'),
64 | meta: {
65 | title: '监控 - 曝光采集',
66 | icon: 'el-icon-setting'
67 | }
68 | }
69 | ]
70 |
--------------------------------------------------------------------------------
/examples/vue3/src/utils/sourcemap.ts:
--------------------------------------------------------------------------------
1 | import sourceMap from 'source-map-js'
2 |
3 | // 找到以.js结尾的fileName
4 | function matchStr(str: string) {
5 | if (str.endsWith('.js')) return str.substring(str.lastIndexOf('/') + 1)
6 | }
7 |
8 | // 将所有的空格转化为实体字符
9 | function repalceAll(str: string) {
10 | return str.replace(new RegExp(' ', 'gm'), ' ')
11 | }
12 |
13 | // 获取文件路径
14 | function getFileLink(str: string) {
15 | const reg = /vue-loader-options!\.(.*)\?/
16 | const res = str.match(reg)
17 | console.log(res, 'getFileLink')
18 | if (res && Array.isArray(res)) {
19 | return res[1]
20 | }
21 | }
22 |
23 | function loadSourceMap(fileName: string): Promise | string {
24 | const env: string = import.meta.env.MODE
25 | const file = fileName
26 |
27 | // if (env == 'development') {
28 | // file = getFileLink(fileName)
29 | // } else {
30 | // file = matchStr(fileName)
31 | // }
32 |
33 | console.log('file', file)
34 | if (!file) return ''
35 | return new Promise(resolve => {
36 | fetch(
37 | `http://localhost:3352/getSourceMap?fileName=${file}&env=${env}`
38 | ).then(response => {
39 | console.log('response', response)
40 | if (env == 'development') {
41 | resolve(response.text())
42 | } else {
43 | resolve(response.json())
44 | }
45 | })
46 | })
47 | }
48 |
49 | interface mapOptions {
50 | fileName: string
51 | line: number
52 | column: number
53 | }
54 | export const findCodeBySourceMap = async (
55 | { fileName, line, column }: mapOptions,
56 | callback: any
57 | ) => {
58 | const sourceData = await loadSourceMap(fileName)
59 | if (!sourceData) return
60 | let result, codeList
61 | if (import.meta.env.MODE == 'development') {
62 | const source = getFileLink(fileName)
63 | let isStart = false
64 | result = {
65 | source,
66 | line: line + 1, // 具体的报错行数
67 | column, // 具体的报错列数
68 | name: null
69 | }
70 | codeList = (sourceData as string).split('\n').filter(item => {
71 | if (item.indexOf('
109 |
110 |
135 |
--------------------------------------------------------------------------------
/examples/vue3/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 所有的事件类型:
5 |
{{ `${key}: ${value}` }}
6 |
7 |
8 | 所有的事件ID(还有一些id是随机字符串的):
9 |
{{ `${key}: ${value}` }}
10 |
11 |
12 |
13 |
14 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/vue3/src/views/pv/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 | navigate - 网页通过点击链接,地址栏输入,表单提交,脚本操作等方式加载
11 |
12 | reload - 网页通过“重新加载”按钮或者location.reload()方法加载
13 | back_forward - 网页通过“前进”或“后退”按钮加载
14 | reserved - 任何其他来源的加载
15 |
16 |
17 |
18 | 获取最新采集数据
19 |
20 |
25 |
26 | {{ `${scope.index + 1}` }}
27 |
28 |
29 | {{ `${formatDate(scope.row.sendTime)}` }}
30 |
31 |
32 | {{ `${formatDate(scope.row.triggerTime)}` }}
33 |
34 |
35 |
36 |
37 |
38 |
93 |
94 |
98 |
--------------------------------------------------------------------------------
/examples/vue3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "lib": ["ESNext", "DOM"],
13 | "skipLibCheck": true,
14 | "noEmit": true,
15 | "paths": {
16 | "@web-tracing/core": ["../../packages/core/index.ts"],
17 | "@web-tracing/core/*": ["../../packages/core/*"],
18 | // "@web-tracing/component": ["./packages/component/index.ts"],
19 | },
20 | "types": [
21 | "vite/client"
22 | ]
23 | },
24 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
25 | "references": [{ "path": "./tsconfig.node.json" }]
26 | }
27 |
--------------------------------------------------------------------------------
/examples/vue3/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["./vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/examples/vue3/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import { resolve } from 'path'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [vue()],
8 | server: {
9 | https: false,
10 | host: '0.0.0.0',
11 | port: 6657,
12 | cors: true,
13 | proxy: {
14 | '/getList': {
15 | target: 'http://localhost:3352/',
16 | changeOrigin: false, // target是域名的话,需要这个参数,
17 | secure: false // 设置支持https协议的代理,
18 | },
19 | '/setList': {
20 | target: 'http://localhost:3352/',
21 | changeOrigin: false, // target是域名的话,需要这个参数,
22 | secure: false // 设置支持https协议的代理,
23 | },
24 | '/cleanTracingList': {
25 | target: 'http://localhost:3352/',
26 | changeOrigin: false,
27 | secure: false
28 | },
29 | '/getBaseInfo': {
30 | target: 'http://localhost:3352'
31 | },
32 | '/getAllTracingList': {
33 | target: 'http://localhost:3352'
34 | },
35 | '/trackweb': {
36 | target: 'http://localhost:3352'
37 | },
38 | '/getSourceMap': {
39 | target: 'http://localhost:3352/',
40 | changeOrigin: false, // target是域名的话,需要这个参数,
41 | secure: false // 设置支持https协议的代理,
42 | }
43 | }
44 | },
45 | resolve: {
46 | alias: {
47 | '@': resolve(__dirname, 'src')
48 | }
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | __webTracing__: WebTracing
3 | __webTracingInit__: boolean
4 | }
5 |
--------------------------------------------------------------------------------
/meta/packages.ts:
--------------------------------------------------------------------------------
1 | export const packages: any[] = [
2 | // {
3 | // name: 'demo',
4 | // display: 'Demo', // 展示名
5 | // description: 'demo: 项目简介',
6 | // keywords: ['关键词1', '关键词2'],
7 | // external: ['vue', 'vue-router', 'dayjs'], // 外部依赖
8 | // build: false, // 是否打包
9 | // iife: false, // 是否打包 iife 格式
10 | // cjs: false, // 是否打包 cjs 格式
11 | // mjs: false, // 是否打包 mjs/es 格式
12 | // dts: false, // 是否打包 ts声明
13 | // target: 'es2015', // 打包的兼容性
14 | // moduleJs: true, // 是否 main 入口指向 index.mjs
15 | // utils: true // 含义:1.不会在文档中看到此分类 2.此分类只会参与打包到npm以及让库内其他包使用
16 | // globals: {
17 | // // 用到的全局变量名,用于打包
18 | // dayjs: 'Dayjs',
19 | // 'vue-router': 'VueRouter',
20 | // 'js-cookie': 'JsCookie',
21 | // easyqrcodejs: 'Easyqrcodejs'
22 | // }
23 | // },
24 | {
25 | name: 'core',
26 | display: 'WebTracing',
27 | description:
28 | '基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段',
29 | keywords: [
30 | '埋点',
31 | '性能',
32 | '异常',
33 | '性能采集',
34 | '异常采集',
35 | '前端埋点',
36 | '前端性能采集'
37 | ],
38 | exampleName: 'vanilla',
39 | exampleGitHubPath: 'https://github.com/M-cheng-web/web-tracing-examples-js'
40 | },
41 | {
42 | name: 'vue2',
43 | display: 'Vue2',
44 | description:
45 | '基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段 - vue2版本',
46 | keywords: [
47 | '埋点',
48 | '性能',
49 | '异常',
50 | '性能采集',
51 | '异常采集',
52 | '前端埋点',
53 | '前端性能采集'
54 | ],
55 | exampleName: 'vue2',
56 | exampleGitHubPath:
57 | 'https://github.com/M-cheng-web/web-tracing-examples-vue2'
58 | },
59 | {
60 | name: 'vue3',
61 | display: 'Vue3',
62 | description:
63 | '基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段 - vue3版本',
64 | keywords: [
65 | '埋点',
66 | '性能',
67 | '异常',
68 | '性能采集',
69 | '异常采集',
70 | '前端埋点',
71 | '前端性能采集'
72 | ],
73 | exampleName: 'vue3',
74 | exampleGitHubPath:
75 | 'https://github.com/M-cheng-web/web-tracing-examples-vue3'
76 | }
77 | // {
78 | // name: 'utils',
79 | // display: 'Utils',
80 | // description: '@web-tracing/utils',
81 | // keywords: [
82 | // '埋点',
83 | // '性能',
84 | // '异常',
85 | // '性能采集',
86 | // '异常采集',
87 | // '前端埋点',
88 | // '前端性能采集'
89 | // ]
90 | // },
91 | // {
92 | // name: 'types',
93 | // display: 'Types',
94 | // description: '@web-tracing/types',
95 | // keywords: [
96 | // '埋点',
97 | // '性能',
98 | // '异常',
99 | // '性能采集',
100 | // '异常采集',
101 | // '前端埋点',
102 | // '前端性能采集'
103 | // ]
104 | // }
105 | ]
106 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-tracing/monorepo",
3 | "version": "2.0.9",
4 | "description": "基于 JS 跨平台插件,为前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段",
5 | "private": true,
6 | "packageManager": "pnpm@9.0.6",
7 | "scripts": {
8 | "docs": "pnpm run -C docs docs",
9 | "docs:build": "pnpm run -C docs docs:build",
10 | "docs:publish": "sh scripts/publish-docs.sh",
11 | "build": "pnpm run update && esno scripts/build.ts",
12 | "build:rollup": "cross-env NODE_OPTIONS=\"--max-old-space-size=6144\" rollup -c",
13 | "build:rollup-watch": "cross-env NODE_OPTIONS=\"--max-old-space-size=6144\" rollup -c -w",
14 | "build:types": "tsc --emitDeclarationOnly && esno scripts/fix-types.ts",
15 | "update": "esno scripts/update.ts",
16 | "update:full": "pnpm run update && pnpm run build:types",
17 | "clean": "rimraf --glob dist types \"packages/*/dist\"",
18 | "release": "bumpp --execute=\"npm run release:prepare\" --no-push --no-commit --no-tag",
19 | "release:prepare": "npm run build:types && npm run update",
20 | "publish": "esno scripts/publish.ts",
21 | "watch": "esno scripts/build.ts --watch",
22 | "test:install": "sh scripts/test-install.sh",
23 | "test:js": "pnpm run -C examples/vanilla dev",
24 | "test:vue2": "pnpm run -C examples/vue2 start",
25 | "test:vue3": "pnpm run -C examples/vue3 start",
26 | "example:publish": "esno scripts/publish-examples.ts",
27 | "example:publish-js": "sh scripts/examples/js.sh",
28 | "example:publish-vue2": "sh scripts/examples/vue2.sh",
29 | "example:publish-vue3": "sh scripts/examples/vue3.sh",
30 | "test": "vitest",
31 | "test-coverage": "vitest --coverage"
32 | },
33 | "keywords": [
34 | "埋点",
35 | "性能",
36 | "异常",
37 | "性能采集",
38 | "异常采集",
39 | "前端埋点",
40 | "前端性能采集"
41 | ],
42 | "author": "M-cheng-web <2604856589@qq.com>",
43 | "license": "MIT",
44 | "devDependencies": {
45 | "@algolia/client-search": "^4.16.0",
46 | "@rollup/plugin-commonjs": "^24.0.1",
47 | "@rollup/plugin-json": "^6.0.0",
48 | "@rollup/plugin-node-resolve": "^15.0.1",
49 | "@types/express": "^4.17.21",
50 | "@types/fs-extra": "^11.0.1",
51 | "@types/jsdom": "^21.1.7",
52 | "@types/node": "^18.15.10",
53 | "@types/prettier": "^2.7.2",
54 | "@typescript-eslint/eslint-plugin": "^5.56.0",
55 | "@typescript-eslint/parser": "^5.56.0",
56 | "@vitest/coverage-v8": "^1.6.0",
57 | "@vitest/ui": "^1.6.0",
58 | "bumpp": "^9.0.0",
59 | "consola": "^2.15.3",
60 | "cross-env": "^7.0.3",
61 | "esbuild": "0.17.14",
62 | "esbuild-register": "^3.4.2",
63 | "eslint": "^8.36.0",
64 | "eslint-config-prettier": "^8.8.0",
65 | "eslint-plugin-prettier": "^4.2.1",
66 | "esno": "^0.17.0",
67 | "express": "^4.18.2",
68 | "fs-extra": "^11.1.1",
69 | "jsdom": "^24.1.0",
70 | "ohmyfetch": "^0.4.21",
71 | "prettier": "^2.8.7",
72 | "puppeteer": "^22.11.1",
73 | "rimraf": "^4.4.1",
74 | "rollup": "^3.26.0",
75 | "rollup-plugin-dts": "^5.3.0",
76 | "rollup-plugin-esbuild": "^5.0.0",
77 | "typescript": "^5.0.2",
78 | "vitepress": "1.0.0-beta.5",
79 | "vitest": "^1.6.0"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/packages/core/__test__/css/performance.css:
--------------------------------------------------------------------------------
1 | #web-tracing-test-id {
2 | color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/core/__test__/err-batch.spec.ts:
--------------------------------------------------------------------------------
1 | import { init } from '../index'
2 | import { _support } from '../src/utils/global'
3 |
4 | describe('err', () => {
5 | beforeAll(() => {
6 | init({
7 | dsn: 'http://unit-test.com',
8 | appName: 'unit-test',
9 | error: true,
10 | recordScreen: false,
11 | scopeError: true
12 | })
13 | })
14 |
15 | function proxyEmit() {
16 | const testResult = { error: null, spy: vi.fn() }
17 | _support.sendData.emit = (e: any) => {
18 | testResult.spy()
19 | testResult.error = e
20 | }
21 | return testResult
22 | }
23 |
24 | it('batch error should be captured correctly', () => {
25 | const testResult = proxyEmit()
26 | for (let i = 0; i < 50; i++) {
27 | const errorEvent = new window.ErrorEvent('error', {
28 | filename: 'test.js',
29 | lineno: 10,
30 | colno: 20,
31 | error: new Error('code error')
32 | })
33 | window.dispatchEvent(errorEvent)
34 | }
35 | expect(testResult.spy).toHaveBeenCalledTimes(1)
36 | expect(testResult.error).toMatchObject([
37 | {
38 | line: 10,
39 | col: 20,
40 | eventId: 'code',
41 | eventType: 'error',
42 | errMessage: 'code error',
43 | batchError: true
44 | }
45 | ])
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/packages/core/__test__/err.spec.ts:
--------------------------------------------------------------------------------
1 | import { init } from '../index'
2 | import { _support } from '../src/utils/global'
3 | import { PromiseRejectionEvent } from './utils/pollify'
4 |
5 | describe('err', () => {
6 | beforeAll(() => {
7 | init({
8 | dsn: 'http://unit-test.com',
9 | appName: 'unit-test',
10 | error: true,
11 | recordScreen: false,
12 | ignoreErrors: [/^ignore/]
13 | })
14 | })
15 |
16 | function proxyEmit() {
17 | const testResult = { error: null, spy: vi.fn() }
18 | _support.sendData.emit = (e: any) => {
19 | testResult.spy()
20 | testResult.error = e
21 | }
22 | return testResult
23 | }
24 |
25 | it('code error should be captured correctly', () => {
26 | const testResult = proxyEmit()
27 | const errorEvent = new window.ErrorEvent('error', {
28 | filename: 'test.js',
29 | lineno: 10,
30 | colno: 20,
31 | error: new Error('code error')
32 | })
33 | window.dispatchEvent(errorEvent)
34 | expect(testResult.spy).toHaveBeenCalledTimes(1)
35 | expect(testResult.error).toMatchObject({
36 | line: 10,
37 | col: 20,
38 | eventId: 'code',
39 | eventType: 'error',
40 | errMessage: 'code error'
41 | })
42 | })
43 |
44 | it('unhandledrejection error should be captured correctly', () => {
45 | const testResult = proxyEmit()
46 | const errorEvent = new PromiseRejectionEvent('unhandledrejection', {
47 | reason: 'unhandledrejection error',
48 | // eslint-disable-next-line @typescript-eslint/no-empty-function
49 | promise: Promise.reject('unhandledrejection error').catch(() => {})
50 | })
51 | window.dispatchEvent(errorEvent)
52 | expect(testResult.spy).toHaveBeenCalledTimes(1)
53 | expect(testResult.error).toMatchObject({
54 | eventId: 'reject',
55 | eventType: 'error',
56 | errMessage: 'unhandledrejection error'
57 | })
58 | })
59 |
60 | it('console error should be captured correctly', () => {
61 | const testResult = proxyEmit()
62 | console.error('console error')
63 | expect(testResult.spy).toHaveBeenCalledTimes(1)
64 | expect(testResult.error).toMatchObject({
65 | eventId: 'console.error',
66 | eventType: 'error',
67 | errMessage: 'console error'
68 | })
69 | })
70 |
71 | it('option ignoreErrors should work', () => {
72 | const testResult = proxyEmit()
73 | const errorEvent = new window.ErrorEvent('error', {
74 | filename: 'test.js',
75 | lineno: 10,
76 | colno: 20,
77 | error: new Error('ignore error')
78 | })
79 | window.dispatchEvent(errorEvent)
80 | expect(testResult.spy).toHaveBeenCalledTimes(0)
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/packages/core/__test__/event.spec.ts:
--------------------------------------------------------------------------------
1 | import { init } from '../index'
2 | import { _support, _global } from '../src/utils/global'
3 | import { JSDOM } from 'jsdom'
4 |
5 | describe('err', () => {
6 | beforeAll(() => {
7 | const dom = new JSDOM(`
8 |
9 |
10 |
11 |
12 |
13 | Test
14 |
15 |
16 |
24 |
25 |
26 | `)
27 | // @ts-expect-error: expected
28 | _global.document = dom.window.document
29 | init({
30 | dsn: 'http://unit-test.com',
31 | appName: 'unit-test',
32 | event: true,
33 | recordScreen: false
34 | })
35 | })
36 |
37 | function proxyEmit() {
38 | const testResult = { info: null, spy: vi.fn() }
39 | _support.sendData.emit = (e: any) => {
40 | testResult.spy()
41 | testResult.info = e
42 | }
43 | return testResult
44 | }
45 |
46 | it('event should be captured correctly', async () => {
47 | const testResult = proxyEmit()
48 |
49 | const button = document.getElementById('btn')!
50 | button.click()
51 | expect(testResult.spy).toHaveBeenCalledTimes(1)
52 | expect(testResult.info).toMatchObject({
53 | eventId: 'test-event-id',
54 | eventType: 'click',
55 | title: 'test-title',
56 | elementPath: 'div',
57 | params: {
58 | id: 'test-warden-id'
59 | }
60 | })
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/packages/core/__test__/html/performance.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | performance test
8 |
9 |
10 |
11 |
12 |
Performance 异步加载资源
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
57 |
58 |
--------------------------------------------------------------------------------
/packages/core/__test__/html/recordscreen.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | recordscreen test
8 |
9 |
10 |
11 |
12 |
recordscreen
13 |
16 |
17 |
18 |
19 |
39 |
40 |
--------------------------------------------------------------------------------
/packages/core/__test__/img/performance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/M-cheng-web/web-tracing/27e4f5b5239a8898734c28578433291a54a38260/packages/core/__test__/img/performance.png
--------------------------------------------------------------------------------
/packages/core/__test__/js/performance.js:
--------------------------------------------------------------------------------
1 | ;(function WebTracingTest() {
2 | window.WebTracingTestVar = true
3 | })()
4 |
--------------------------------------------------------------------------------
/packages/core/__test__/performance.spec.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import http from 'http'
4 |
5 | import { getServerURL, startServer, launchPuppeteer, getHtml } from './utils'
6 | import { Browser, Page } from 'puppeteer'
7 |
8 | describe('err', () => {
9 | vi.setConfig({ testTimeout: 30_000, hookTimeout: 30_000 })
10 |
11 | let code: string
12 | let serverURL: string
13 | let server: http.Server
14 | let browser: Browser
15 |
16 | beforeAll(async () => {
17 | server = await startServer()
18 | serverURL = getServerURL(server)
19 | browser = await launchPuppeteer()
20 |
21 | const bundlePath = path.resolve(__dirname, '../dist/index.iife.js')
22 | code = fs.readFileSync(bundlePath, 'utf8')
23 | })
24 |
25 | afterAll(async () => {
26 | browser && (await browser.close())
27 | server && server.close()
28 | })
29 |
30 | const baseInfo = {
31 | appCode: '',
32 | appName: 'cxh',
33 | clientHeight: 1080,
34 | clientWidth: 1920,
35 | vendor: 'Google Inc.'
36 | }
37 |
38 | async function loadTestPage() {
39 | const page: Page = await browser.newPage()
40 | const htmlName = 'performance.html'
41 | await page.goto(`${serverURL}/${htmlName}`)
42 | const html = getHtml(`${htmlName}`, code)
43 | await page.setContent(html)
44 | await page.waitForFunction(() => {
45 | return document.readyState === 'complete'
46 | })
47 | return page
48 | }
49 |
50 | function assertInitiatorType(
51 | data: any,
52 | initiatorType: 'script' | 'link' | 'img'
53 | ) {
54 | expect(data).toMatchObject({
55 | baseInfo,
56 | eventInfo: [
57 | {
58 | eventId: 'resource',
59 | eventType: 'performance',
60 | initiatorType: initiatorType
61 | }
62 | ]
63 | })
64 | }
65 |
66 | it('firstResource performance should be captured correctly', async () => {
67 | const page = await loadTestPage()
68 | const webTracingData = (await page.evaluate(
69 | `window.__WebTracingData__`
70 | )) as any[]
71 |
72 | expect(webTracingData).toMatchObject({
73 | baseInfo,
74 | eventInfo: [
75 | {
76 | eventId: 'page',
77 | eventType: 'performance'
78 | }
79 | ]
80 | })
81 | })
82 |
83 | it('async script performance should be captured correctly', async () => {
84 | const page = await loadTestPage()
85 | await page.click('.script-button')
86 | await page.waitForFunction(() => {
87 | return (window as any).WebTracingTestVar !== undefined
88 | })
89 | const webTracingData = await page.evaluate(`window.__WebTracingData__`)
90 | assertInitiatorType(webTracingData, 'script')
91 | })
92 |
93 | it('async link performance should be captured correctly', async () => {
94 | const page = await loadTestPage()
95 | await page.click('.link-button')
96 | await page.waitForFunction(() => {
97 | const element = document.querySelector('#web-tracing-test-id')!
98 | const style = window.getComputedStyle(element)
99 | return style.color === 'rgb(255, 0, 0)'
100 | })
101 | const webTracingData = await page.evaluate(`window.__WebTracingData__`)
102 | assertInitiatorType(webTracingData, 'link')
103 | })
104 |
105 | it('async img performance should be captured correctly', async () => {
106 | const page = await loadTestPage()
107 | await page.click('.img-button')
108 | await page.waitForSelector('#performance-img-div img')
109 | const webTracingData = await page.evaluate(`window.__WebTracingData__`)
110 | assertInitiatorType(webTracingData, 'img')
111 | })
112 | })
113 |
--------------------------------------------------------------------------------
/packages/core/__test__/recordscreen.spec.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import http from 'http'
4 |
5 | import { getServerURL, startServer, launchPuppeteer, getHtml } from './utils'
6 | import { Browser, Page } from 'puppeteer'
7 |
8 | describe('err', () => {
9 | vi.setConfig({ testTimeout: 30_000, hookTimeout: 30_000 })
10 |
11 | let code: string
12 | let serverURL: string
13 | let server: http.Server
14 | let browser: Browser
15 |
16 | beforeAll(async () => {
17 | server = await startServer(3031)
18 | serverURL = getServerURL(server)
19 | browser = await launchPuppeteer()
20 |
21 | const bundlePath = path.resolve(__dirname, '../dist/index.iife.js')
22 | code = fs.readFileSync(bundlePath, 'utf8')
23 | })
24 |
25 | afterAll(async () => {
26 | browser && (await browser.close())
27 | server && server.close()
28 | })
29 |
30 | async function loadTestPage() {
31 | const page: Page = await browser.newPage()
32 | const htmlName = 'recordscreen.html'
33 | await page.goto(`${serverURL}/${htmlName}`)
34 | const html = getHtml(`${htmlName}`, code)
35 | await page.setContent(html)
36 | await page.waitForFunction(() => {
37 | return document.readyState === 'complete'
38 | })
39 | return page
40 | }
41 |
42 | it('error recordscreen should be captured correctly', async () => {
43 | const page = await loadTestPage()
44 | await page.click('.code-error-button')
45 | const webTracingData = await page.evaluate(`window.__WebTracingData__`)
46 | expect(
47 | (webTracingData as any).eventInfo[0].recordscreen
48 | ).not.toBeUndefined()
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/packages/core/__test__/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { getNodeXPath } from '../src/utils/element'
2 |
3 | describe('utils', () => {
4 | it('getNodeXPath should work', () => {
5 | const element = document.createElement('div')
6 | element.innerHTML = `
7 |
14 | `
15 | const target = element.querySelector('.target')!
16 | expect(getNodeXPath(target)).toBe('#wrapper>div>div')
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/packages/core/__test__/utils/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import http from 'http'
3 | import url from 'url'
4 | import fs from 'fs'
5 | import puppeteer from 'puppeteer'
6 |
7 | interface IMimeType {
8 | [key: string]: string
9 | }
10 |
11 | export function startServer(defaultPort = 3030) {
12 | return new Promise((resolve, reject) => {
13 | const mimeType: IMimeType = {
14 | '.html': 'text/html',
15 | '.js': 'text/javascript',
16 | '.css': 'text/css',
17 | '.png': 'image/png'
18 | }
19 |
20 | const s = http.createServer((req, res) => {
21 | const parsedUrl = url.parse(req.url!)
22 | const sanitizePath = path
23 | .normalize(parsedUrl.pathname!)
24 | .replace(/^(\.\.[/\\])+/, '')
25 | const pathname = path.join(__dirname, '../', sanitizePath)
26 |
27 | try {
28 | const data = fs.readFileSync(pathname)
29 | const ext = path.parse(pathname).ext
30 | res.setHeader('Content-type', mimeType[ext] || 'text/plain')
31 | res.setHeader('Access-Control-Allow-Origin', '*')
32 | res.setHeader('Access-Control-Allow-Methods', 'GET')
33 | res.setHeader('Access-Control-Allow-Headers', 'Content-type')
34 | setTimeout(() => {
35 | res.end(data)
36 | }, 100)
37 | } catch (error) {
38 | res.end()
39 | }
40 | })
41 | s.listen(defaultPort)
42 | .on('listening', () => {
43 | resolve(s)
44 | })
45 | .on('error', e => {
46 | reject(e)
47 | })
48 | })
49 | }
50 |
51 | export function getServerURL(server: http.Server): string {
52 | const address = server.address()
53 | if (address && typeof address !== 'string') {
54 | return `http://localhost:${address.port}`
55 | } else {
56 | return `${address}`
57 | }
58 | }
59 |
60 | export function replaceLast(str: string, find: string, replace: string) {
61 | const index = str.lastIndexOf(find)
62 | if (index === -1) {
63 | return str
64 | }
65 | return str.substring(0, index) + replace + str.substring(index + find.length)
66 | }
67 |
68 | export async function launchPuppeteer(
69 | options?: Parameters<(typeof puppeteer)['launch']>[0]
70 | ) {
71 | return await puppeteer.launch({
72 | headless: true,
73 | defaultViewport: {
74 | width: 1920,
75 | height: 1080
76 | },
77 | args: ['--no-sandbox'],
78 | ...options
79 | })
80 | }
81 |
82 | export function getHtml(fileName: string, code: string) {
83 | const filePath = path.resolve(__dirname, `../html/${fileName}`)
84 | const html = fs.readFileSync(filePath, 'utf8')
85 | return replaceLast(
86 | html,
87 | '