├── .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 | logo 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 | logo 25 | 26 | ### 错误监听 27 | logo 28 | 29 | ### 资源监听 30 | logo 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 | logo 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 | logo 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 | 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 | 71 | 72 | 113 | 114 | 134 | -------------------------------------------------------------------------------- /examples/vue2/src/components/MenuList.vue: -------------------------------------------------------------------------------- 1 | 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 | 62 | 63 | 122 | 123 | 148 | -------------------------------------------------------------------------------- /examples/vue2/src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/vue2/src/views/pv/index.vue: -------------------------------------------------------------------------------- 1 | 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 | logo 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 | 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 | 71 | 72 | 77 | 78 | 115 | 116 | 136 | -------------------------------------------------------------------------------- /examples/vue3/src/components/MenuList.vue: -------------------------------------------------------------------------------- 1 | 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(' 106 | item.replace(/\/.\//g, '/') 107 | ) 108 | index = copySources.indexOf(result.source) 109 | } 110 | console.log('index', index) 111 | if (index === -1) { 112 | return '源码解析失败' 113 | } 114 | const code = sourcesContent[index] 115 | codeList = code.split('\n') 116 | } 117 | 118 | const row = result.line 119 | const len = codeList.length - 1 120 | const start = row - 5 >= 0 ? row - 5 : 0 // 将报错代码显示在中间位置 121 | const end = start + 9 >= len ? len : start + 9 // 最多展示10行 122 | const newLines = [] 123 | let j = 0 124 | for (let i = start; i <= end; i++) { 125 | j++ 126 | newLines.push( 127 | `
${j}. ${repalceAll(codeList[i])}
` 130 | ) 131 | } 132 | 133 | const innerHTML = `
${ 134 | result.source 135 | } at line ${result.column}:${row}
${newLines.join( 136 | '' 137 | )}
` 138 | 139 | callback(innerHTML) 140 | } 141 | -------------------------------------------------------------------------------- /examples/vue3/src/views/event/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 109 | 110 | 135 | -------------------------------------------------------------------------------- /examples/vue3/src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/vue3/src/views/pv/index.vue: -------------------------------------------------------------------------------- 1 | 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 |
20 |
21 | 22 |
23 |
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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 | '', 88 | ` 89 | 92 | 93 | ` 94 | ) 95 | } 96 | 97 | export function delay(timeout: number) { 98 | return new Promise(resolve => setTimeout(resolve, timeout)) 99 | } 100 | -------------------------------------------------------------------------------- /packages/core/__test__/utils/pollify.ts: -------------------------------------------------------------------------------- 1 | export type PromiseRejectionEventInit = { 2 | promise: Promise 3 | reason: any 4 | } 5 | 6 | export class PromiseRejectionEvent extends Event { 7 | public readonly reason: any 8 | public readonly promise: Promise 9 | constructor(type: string, eventInitDict: PromiseRejectionEventInit) { 10 | super(type) 11 | this.promise = eventInitDict.promise 12 | this.reason = eventInitDict.reason 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | import type { InitOptions } from './src/types' 2 | import { initReplace } from './src/lib/replace' 3 | import { initOptions, options as _options } from './src/lib/options' 4 | import { initBase } from './src/lib/base' 5 | import { initSendData } from './src/lib/sendData' 6 | import { initLineStatus } from './src/lib/line-status' 7 | import { initError, parseError } from './src/lib/err' 8 | import { initEvent } from './src/lib/event' 9 | import { initHttp } from './src/lib/http' 10 | import { initPerformance } from './src/lib/performance' 11 | import { initPv } from './src/lib/pv' 12 | import { initIntersection } from './src/lib/intersectionObserver' 13 | import { _global } from './src/utils/global' 14 | import { SENDID } from './src/common' 15 | import { logError } from './src/utils/debug' 16 | import { initRecordScreen } from './src/lib/recordscreen' 17 | import * as exportMethods from './src/lib/exportMethods' 18 | import './src/observer/index' 19 | 20 | function init(options: InitOptions): void { 21 | if (_global.__webTracingInit__) return 22 | if (!initOptions(options)) return 23 | 24 | // 注册全局 25 | initReplace() 26 | initBase() 27 | initSendData() 28 | initLineStatus() 29 | 30 | // 注册各个业务 31 | initError() 32 | initEvent() 33 | initHttp() 34 | initPerformance() 35 | initPv() 36 | initIntersection() 37 | 38 | if (_options.value.recordScreen) initRecordScreen() 39 | 40 | _global.__webTracingInit__ = true 41 | } 42 | 43 | export { 44 | init, 45 | InitOptions, 46 | logError, 47 | parseError, 48 | SENDID, 49 | exportMethods, 50 | _options as options 51 | } 52 | export * from './src/lib/exportMethods' 53 | export default { init, ...exportMethods, options: _options } 54 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@web-tracing/core", 3 | "version": "2.0.9", 4 | "description": "基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段", 5 | "main": "./dist/index.cjs", 6 | "module": "./dist/index.mjs", 7 | "jsdelivr": "./dist/index.iife.min.js", 8 | "types": "./dist/index.d.ts", 9 | "sideEffects": false, 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.cjs", 14 | "types": "./dist/index.d.ts" 15 | }, 16 | "./*": "./*" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/M-cheng-web/web-tracing.git", 22 | "directory": "packages/core" 23 | }, 24 | "author": "M-cheng-web ", 25 | "keywords": [ 26 | "埋点", 27 | "性能", 28 | "异常", 29 | "性能采集", 30 | "异常采集", 31 | "前端埋点", 32 | "前端性能采集" 33 | ], 34 | "dependencies": { 35 | "ua-parser-js": "2.0.0-alpha.1", 36 | "@types/pako": "^2.0.0", 37 | "pako": "^2.1.0", 38 | "js-base64": "^3.7.5", 39 | "rrweb": "2.0.0-alpha.5" 40 | }, 41 | "devDependencies": { 42 | "@types/ua-parser-js": "^0.7.36" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/M-cheng-web/web-tracing/issues" 46 | }, 47 | "homepage": "https://github.com/M-cheng-web/web-tracing#readme", 48 | "unpkg": "./dist/index.iife.min.js" 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/common/config.ts: -------------------------------------------------------------------------------- 1 | import { name, version } from '../../package.json' 2 | 3 | export const DEVICE_KEY = '_webtracing_device_id' // 设备ID Key - 私有属性 4 | 5 | export const SESSION_KEY = '_webtracing_session_id' // 会话ID Key(一个站点只允许运行一个埋点程序) - 私有属性 6 | 7 | export const SURVIVIE_MILLI_SECONDS = 1800000 // 会话 session存活时长(30minutes) - 私有属性 8 | 9 | export const SDK_LOCAL_KEY = '_webtracing_localization_key' // 事件本地化的key 10 | 11 | export const SDK_VERSION = version 12 | 13 | export const SDK_NAME = name 14 | -------------------------------------------------------------------------------- /packages/core/src/common/constant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 事件类型 3 | */ 4 | export enum EVENTTYPES { 5 | ERROR = 'error', 6 | CONSOLEERROR = 'consoleError', 7 | UNHANDLEDREJECTION = 'unhandledrejection', 8 | CLICK = 'click', 9 | LOAD = 'load', 10 | BEFOREUNLOAD = 'beforeunload', 11 | FETCH = 'fetch', 12 | XHROPEN = 'xhr-open', 13 | XHRSEND = 'xhr-send', 14 | HASHCHANGE = 'hashchange', 15 | HISTORYPUSHSTATE = 'history-pushState', 16 | HISTORYREPLACESTATE = 'history-replaceState', 17 | POPSTATE = 'popstate', 18 | READYSTATECHANGE = 'readystatechange', 19 | ONLINE = 'online', 20 | OFFLINE = 'offline' 21 | } 22 | 23 | /** 24 | * 触发的事件是什么类型 - eventType 25 | */ 26 | export enum SEDNEVENTTYPES { 27 | PV = 'pv', // 路由跳转 28 | PVDURATION = 'pv-duration', // 页面停留事件 29 | ERROR = 'error', // 错误 30 | PERFORMANCE = 'performance', // 资源 31 | CLICK = 'click', // 点击 32 | DWELL = 'dwell', // 页面卸载 33 | CUSTOM = 'custom', // 手动触发事件 34 | INTERSECTION = 'intersection' // 曝光采集 35 | } 36 | 37 | /** 38 | * 触发的事件id - eventID 39 | */ 40 | export enum SENDID { 41 | PAGE = 'page', // 页面 42 | RESOURCE = 'resource', // 资源 43 | SERVER = 'server', // 请求 44 | CODE = 'code', // code 45 | REJECT = 'reject', // reject 46 | CONSOLEERROR = 'console.error' // console.error 47 | } 48 | 49 | /** 50 | * 网页的几种加载方式 51 | */ 52 | export const WEBPAGELOAD: Record = { 53 | 0: 'navigate', // 网页通过点击链接,地址栏输入,表单提交,脚本操作等方式加载 54 | 1: 'reload', // 网页通过“重新加载”按钮或者location.reload()方法加载 55 | 2: 'back_forward', // 网页通过“前进”或“后退”按钮加载 56 | 255: 'reserved' // 任何其他来源的加载 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './constant' 3 | -------------------------------------------------------------------------------- /packages/core/src/lib/base.ts: -------------------------------------------------------------------------------- 1 | import { DEVICE_KEY, SDK_VERSION } from '../common' 2 | import { _support, getGlobal, isTestEnv } from '../utils/global' 3 | import { load } from '../utils/fingerprintjs' 4 | import { getCookieByName, uuid } from '../utils' 5 | import { getSessionId } from '../utils/session' 6 | import { options } from './options' 7 | import { getIPs } from '../utils/getIps' 8 | import { AnyObj } from '../types' 9 | import { computed } from '../observer' 10 | import type { ObserverValue } from '../observer/types' 11 | import { sendData } from './sendData' 12 | 13 | interface Device { 14 | clientHeight: number 15 | clientWidth: number 16 | colorDepth: number 17 | pixelDepth: number 18 | screenWidth: number 19 | screenHeight: number 20 | deviceId: string 21 | vendor: string 22 | platform: string 23 | } 24 | interface Base extends Device { 25 | userUuid: string 26 | sdkUserUuid: string 27 | ext: AnyObj 28 | appName: string 29 | appCode: string 30 | pageId: string 31 | sessionId: string 32 | sdkVersion: string 33 | ip: string 34 | } 35 | 36 | export class BaseInfo { 37 | public base: ObserverValue | undefined 38 | public pageId: string 39 | private sdkUserUuid = '' 40 | private device: Device | undefined 41 | // 基础信息是否初始化成功 42 | public _initSuccess = false 43 | 44 | constructor() { 45 | this.pageId = uuid() // 当前应用ID,在整个页面生命周期内不变,单页应用路由变化也不会改变;加载SDK时创建且只创建一次 46 | 47 | this.initSdkUserUuid() 48 | .then(() => { 49 | this.initDevice() 50 | this.initBase() 51 | }) 52 | .finally(() => { 53 | this._initSuccess = true 54 | sendData.emit([]) 55 | }) 56 | } 57 | private initDevice() { 58 | const { screen } = getGlobal() 59 | const { clientWidth, clientHeight } = document.documentElement 60 | const { width, height, colorDepth, pixelDepth } = screen 61 | let deviceId = getCookieByName(DEVICE_KEY) 62 | if (!deviceId) { 63 | deviceId = `t_${uuid()}` 64 | document.cookie = `${DEVICE_KEY}=${deviceId};path=/;` 65 | } 66 | this.device = { 67 | clientHeight, // 网页可见区高度 68 | clientWidth, // 网页可见区宽度 69 | colorDepth, // 显示屏幕调色板的比特深度 70 | pixelDepth, // 显示屏幕的颜色分辨率 71 | deviceId, // id 72 | screenWidth: width, // 显示屏幕的宽度 73 | screenHeight: height, // 显示屏幕的高度 74 | vendor: navigator.vendor, // 浏览器名称 75 | platform: navigator.platform // 浏览器平台的环境,不是电脑系统的x64这样的(浏览器平台的环境可能是x32) 76 | } 77 | } 78 | /** 79 | * 初始化 base 数据 80 | */ 81 | private initBase() { 82 | // 与一般业务上理解的sessionId做区分,此session与业务无关,单纯就是浏览器端和后端直接的联系 83 | const sessionId = getSessionId() 84 | let ip = '' 85 | 86 | this.base = computed(() => ({ 87 | ...this.device!, 88 | userUuid: options.value.userUuid, 89 | sdkUserUuid: this.sdkUserUuid, 90 | ext: options.value.ext, 91 | appName: options.value.appName, 92 | appCode: options.value.appCode, 93 | pageId: this.pageId, 94 | sessionId, 95 | sdkVersion: SDK_VERSION, 96 | ip 97 | })) 98 | 99 | !isTestEnv && 100 | getIPs().then((res: any) => { 101 | this.base!.value.ip = res[0] 102 | ip = res[0] 103 | }) 104 | } 105 | /** 106 | * 初始化sdk中给用户的唯一标识 107 | */ 108 | private initSdkUserUuid() { 109 | return isTestEnv 110 | ? Promise.resolve().then(() => { 111 | this.sdkUserUuid = 'unit-test-id' 112 | options.value.sdkUserUuid = 'unit-test-id' 113 | }) 114 | : load({}) 115 | .then((fp: any) => fp.get()) 116 | .then((result: any) => { 117 | const visitorId = result.visitorId 118 | this.sdkUserUuid = visitorId 119 | options.value.sdkUserUuid = visitorId 120 | }) 121 | } 122 | } 123 | 124 | export let baseInfo: BaseInfo 125 | 126 | export function initBase() { 127 | baseInfo = new BaseInfo() 128 | _support.baseInfo = baseInfo 129 | } 130 | -------------------------------------------------------------------------------- /packages/core/src/lib/err-batch.ts: -------------------------------------------------------------------------------- 1 | import { sendData } from './sendData' 2 | import { AnyFun } from '../types' 3 | import { debounce, throttle, groupArray } from '../utils' 4 | 5 | const SETTIMEA = 2000 6 | const SETTIMEB = 20000 7 | const MAXLENGTHA = 5 8 | const GROUPARRAYKEY = ['errMessage', 'eventId', 'requestUrl'] 9 | 10 | /** 11 | * 判断是否为批量错误 12 | * 判断流程: 13 | * 1. 先把所有错误都放入 a栈 14 | * 2. 每次发生错误后防抖 2s查 a栈是否有批量错误(批量错误: errMessage、errType相同且发生个数大于等于5) 15 | * 1. 如果为批量错误则合并这些错误并加入[时间区间参数、发生个数参数]后放入 b栈 16 | * 2. 不为批量错误则发送这些错误 17 | * 3. 每次推入错误到b栈后延迟 20s查 b栈并发送这些错误 18 | * 4. 在这个过程中,如果用户关闭了网页,会统一把 a栈、b栈内的数据发送 19 | * 5. 在这个过程中,a栈每满50个错误也会强制触发a栈和b栈的错误处理(处理结果为直接发送批量错误) 20 | */ 21 | class BatchError { 22 | cacheErrorA: any[] 23 | cacheErrorB: any[] 24 | throttleProxyAddCacheErrorA: AnyFun 25 | throttleProxyAddCacheErrorB: AnyFun 26 | constructor() { 27 | this.cacheErrorA = [] 28 | this.cacheErrorB = [] 29 | this.throttleProxyAddCacheErrorA = debounce( 30 | this.proxyAddCacheErrorA, 31 | SETTIMEA 32 | ) 33 | this.throttleProxyAddCacheErrorB = throttle( 34 | this.proxyAddCacheErrorB, 35 | SETTIMEB 36 | ) 37 | } 38 | proxyAddCacheErrorA() { 39 | let len = this.cacheErrorA.length 40 | if (!len) return 41 | const arr = groupArray(this.cacheErrorA, ...GROUPARRAYKEY) 42 | const arrA = arr.filter(item => item.length < MAXLENGTHA) 43 | const arrB = arr.filter(item => item.length >= MAXLENGTHA) 44 | 45 | if (arrA.length) { 46 | sendData.emit(arrA.flat(Infinity)) 47 | } 48 | if (arrB.length) { 49 | const arrBsum: any[] = [] 50 | arrB.forEach(item => { 51 | const sumItem = item[0] 52 | sumItem.batchError = true 53 | sumItem.batchErrorLength = item.length 54 | sumItem.batchErrorLastHappenTime = item[item.length - 1].triggerTime 55 | arrBsum.push(sumItem) 56 | }) 57 | this.cacheErrorB.push(...arrBsum) 58 | this.throttleProxyAddCacheErrorB() 59 | } 60 | 61 | while (len--) { 62 | this.cacheErrorA.shift() 63 | } 64 | } 65 | proxyAddCacheErrorB() { 66 | let len = this.cacheErrorB.length 67 | if (!len) return 68 | const arr = groupArray(this.cacheErrorB, ...GROUPARRAYKEY) 69 | 70 | while (len--) { 71 | this.cacheErrorB.shift() 72 | } 73 | 74 | // 将区间报错合并 75 | const emitList: any[] = [] 76 | arr.forEach((itemList: any[]) => { 77 | const sumItem = itemList[0] 78 | if (itemList.length > 1) { 79 | sumItem.batchErrorLength = itemList.reduce( 80 | (p, item) => (p += item.batchErrorLength), 81 | 0 82 | ) 83 | sumItem.batchErrorLastHappenTime = 84 | itemList[itemList.length - 1].triggerTime 85 | } 86 | emitList.push(sumItem) 87 | }) 88 | sendData.emit(emitList) 89 | } 90 | /** 91 | * 获取所有的错误 92 | * 用户突然关闭页面时调用此方法集成错误 93 | */ 94 | sendAllCacheError() { 95 | const errInfoList = this.cacheErrorA.concat(this.cacheErrorB) 96 | const arr = groupArray(errInfoList, ...GROUPARRAYKEY) 97 | const arrA = arr.filter(item => item.length < MAXLENGTHA) 98 | const arrB = arr.filter(item => item.length >= MAXLENGTHA) 99 | 100 | if (arrA.length) { 101 | sendData.emit(arrA.flat(Infinity), true) 102 | } 103 | if (arrB.length) { 104 | const arrBsum: any[] = [] 105 | arrB.forEach(item => { 106 | const sumItem = item[0] 107 | sumItem.batchError = true 108 | sumItem.batchErrorLength = item.length 109 | sumItem.batchErrorLastHappenTime = item[item.length - 1].triggerTime 110 | arrBsum.push(sumItem) 111 | }) 112 | sendData.emit(arrBsum, true) 113 | } 114 | } 115 | pushCacheErrorA(errorInfo: any) { 116 | this.cacheErrorA.push(errorInfo) 117 | this.throttleProxyAddCacheErrorA() 118 | 119 | // 每 50 个触发一次强制发送事件 120 | if (this.cacheErrorA.length >= 50) { 121 | this.proxyAddCacheErrorA() 122 | this.proxyAddCacheErrorB() 123 | } 124 | } 125 | } 126 | 127 | export let batchError: BatchError 128 | 129 | /** 130 | * 初始化错误缓存 131 | */ 132 | export function initBatchError() { 133 | batchError = new BatchError() 134 | } 135 | -------------------------------------------------------------------------------- /packages/core/src/lib/event-dwell.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 这部分功能移植到 pv 中,并且默认开启 3 | */ 4 | import { EVENTTYPES, SEDNEVENTTYPES, WEBPAGELOAD } from '../common' 5 | import { uuid, isValidKey, getTimestamp, getLocationHref } from '../utils' 6 | import { eventBus } from './eventBus' 7 | import { sendData } from './sendData' 8 | // import { options } from './options' 9 | 10 | class DwellRequestTemplate { 11 | eventId = '' // 事件ID 12 | eventType = '' // 事件类型 13 | triggerPageUrl = '' // 当前页面URL 14 | referer = '' // 上级页面URL 15 | entryTime = -1 // 加载完成时间 16 | triggerTime = -1 // 卸载时间 17 | millisecond = -1 // 页面停留时间 18 | operateAction = '' // 页面加载来源 19 | constructor(config = {}) { 20 | Object.keys(config).forEach(key => { 21 | if (isValidKey(key, config)) { 22 | this[key] = config[key] || null 23 | } 24 | }) 25 | } 26 | } 27 | 28 | /** 29 | * 加载 & 卸载事件 30 | */ 31 | function dwellCollector() { 32 | const _config = new DwellRequestTemplate({ eventType: SEDNEVENTTYPES.DWELL }) 33 | 34 | // 加载完成事件 35 | eventBus.addEvent({ 36 | type: EVENTTYPES.LOAD, 37 | callback: () => { 38 | _config.entryTime = getTimestamp() 39 | } 40 | }) 41 | 42 | // 卸载事件 43 | eventBus.addEvent({ 44 | type: EVENTTYPES.BEFOREUNLOAD, 45 | callback: () => { 46 | _config.eventId = uuid() 47 | _config.triggerPageUrl = getLocationHref() // 当前页面 url 48 | _config.referer = document.referrer // 上级页面 url(从哪个页面跳过来的就是上级页面) 49 | _config.triggerTime = getTimestamp() // 卸载时间 50 | _config.millisecond = getTimestamp() - _config.entryTime // 停留多久 51 | const { type } = performance.navigation // 表示加载来源, type为 0,1,2,255 52 | _config.operateAction = WEBPAGELOAD[type] || '' 53 | sendData.emit(_config, true) 54 | } 55 | }) 56 | } 57 | 58 | function initEventDwell() { 59 | // options.value.event.unload && dwellCollector() // 放弃此方法 60 | dwellCollector() 61 | } 62 | 63 | export { initEventDwell } 64 | -------------------------------------------------------------------------------- /packages/core/src/lib/eventBus.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFun } from '../types' 2 | import { EVENTTYPES } from '../common' 3 | import { _support } from '../utils/global' 4 | 5 | interface EventHandler { 6 | type: EVENTTYPES 7 | callback: AnyFun 8 | } 9 | 10 | type Handlers = { 11 | [key in EVENTTYPES]?: AnyFun[] 12 | } 13 | 14 | export class EventBus { 15 | private handlers: Handlers 16 | constructor() { 17 | this.handlers = {} 18 | } 19 | /** 20 | * 为目标类型事件添加回调 21 | * @param handler 需要被添加的类型以及回调函数 22 | */ 23 | addEvent(handler: EventHandler) { 24 | !this.handlers[handler.type] && (this.handlers[handler.type] = []) 25 | const funIndex = this._getCallbackIndex(handler) 26 | if (funIndex === -1) { 27 | this.handlers[handler.type]?.push(handler.callback) 28 | } 29 | } 30 | /** 31 | * 为目标类型事件删除回调 32 | * @param handler 需要被删除的类型以及回调函数 33 | */ 34 | delEvent(handler: EventHandler) { 35 | const funIndex = this._getCallbackIndex(handler) 36 | if (funIndex !== -1) { 37 | this.handlers[handler.type]?.splice(funIndex, 1) 38 | } 39 | } 40 | /** 41 | * 为目标类型事件更改回调 42 | * @param handler 需要被更改的类型以及回调函数 43 | * @param newCallback 新的回调函数 44 | */ 45 | changeEvent(handler: EventHandler, newCallback: AnyFun) { 46 | const funIndex = this._getCallbackIndex(handler) 47 | if (funIndex !== -1) { 48 | this.handlers[handler.type]?.splice(funIndex, 1, newCallback) 49 | } 50 | } 51 | /** 52 | * 获取目标类型事件所有的回调 53 | * @param type 事件类型 54 | */ 55 | getEvent(type: EVENTTYPES): AnyFun[] { 56 | return this.handlers[type] || [] 57 | } 58 | /** 59 | * 执行目标类型事件所有的回调 60 | * @param type 事件类型 61 | * @param args 额外参数 62 | */ 63 | runEvent(type: EVENTTYPES, ...args: any[]): void { 64 | const allEvent = this.getEvent(type) 65 | allEvent.forEach(fun => { 66 | fun(...args) 67 | }) 68 | } 69 | /** 70 | * 获取函数在 callback 列表中的位置 71 | */ 72 | private _getCallbackIndex(handler: EventHandler): number { 73 | if (this.handlers[handler.type]) { 74 | const callbackList = this.handlers[handler.type] 75 | if (callbackList) { 76 | return callbackList.findIndex(fun => fun === handler.callback) 77 | } else { 78 | return -1 79 | } 80 | } else { 81 | return -1 82 | } 83 | } 84 | } 85 | 86 | const eventBus = _support.eventBus || (_support.eventBus = new EventBus()) 87 | 88 | export { eventBus } 89 | -------------------------------------------------------------------------------- /packages/core/src/lib/line-status.ts: -------------------------------------------------------------------------------- 1 | import { _support } from '../utils/global' 2 | import { EVENTTYPES } from '../common' 3 | import { eventBus } from './eventBus' 4 | import { debug } from '../utils/debug' 5 | 6 | /** 7 | * 监听网络状态 8 | * 当处于断网状态下的所有埋点事件都无效(认为此时采集的数据大部分是无效的) 9 | */ 10 | export class LineStatus { 11 | onLine = true 12 | constructor() { 13 | this.init() 14 | } 15 | init() { 16 | eventBus.addEvent({ 17 | type: EVENTTYPES.OFFLINE, 18 | callback: e => { 19 | if (e.type === 'offline') { 20 | debug('网络断开') 21 | this.onLine = false 22 | } 23 | } 24 | }) 25 | eventBus.addEvent({ 26 | type: EVENTTYPES.ONLINE, 27 | callback: e => { 28 | if (e.type === 'online') { 29 | debug('网络连接') 30 | this.onLine = true 31 | } 32 | } 33 | }) 34 | } 35 | } 36 | 37 | export let lineStatus: LineStatus 38 | 39 | /** 40 | * 初始化网络监听 41 | */ 42 | export function initLineStatus() { 43 | _support.lineStatus = new LineStatus() 44 | lineStatus = _support.lineStatus 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/observer/computed.ts: -------------------------------------------------------------------------------- 1 | import { Watcher } from './watcher' 2 | import { ObserverValue, AnyFun } from './types' 3 | import { OBSERVERSIGNBOARD } from './config' 4 | 5 | /** 6 | * 计算属性响应式 7 | */ 8 | export class Computed { 9 | target: ObserverValue 10 | constructor(target: ObserverValue) { 11 | this.target = target 12 | } 13 | defineReactive() { 14 | const computedWatcher = new Watcher(this, { computed: true }) 15 | 16 | // const proxyCache = new WeakMap, any>() 17 | const handlers: ProxyHandler> = { 18 | get() { 19 | if (computedWatcher.proxy.dirty) { 20 | // 代表这个属性已经脏了,需要更新(重新运算) 21 | // console.log('计算属性:取新值') 22 | computedWatcher.depend() // 添加上下文与此属性绑定 23 | return computedWatcher.get() 24 | } else { 25 | // 代表这个属性不需要重新运算 26 | // console.log('计算属性:取旧值') 27 | 28 | // 取旧值的时候也要添加上下文绑定 29 | // 因为其他值在依赖这个计算属性的时候,有可能会依赖到旧的值 30 | // 所以在依赖到旧值时也要添加上下文绑定,从而当这个计算属性被改变时也能通知到对方改变 31 | // 一开始我就是没进行这一步,从而导致莫名bug 32 | computedWatcher.depend() 33 | return computedWatcher.proxy.value 34 | } 35 | } 36 | } 37 | return new Proxy>(this.target, handlers) 38 | } 39 | } 40 | 41 | export const computedMap = new WeakMap, AnyFun>() 42 | 43 | export function computed(fun: AnyFun) { 44 | const target: any = { value: 0 } 45 | target[OBSERVERSIGNBOARD] = true 46 | 47 | const ob = new Computed(target) 48 | const proxy = ob.defineReactive() 49 | 50 | computedMap.set(ob, fun) 51 | return proxy 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/observer/config.ts: -------------------------------------------------------------------------------- 1 | export const OBSERVERSIGNBOARD = '__webtracingobserver__' 2 | -------------------------------------------------------------------------------- /packages/core/src/observer/dep.ts: -------------------------------------------------------------------------------- 1 | import { Watcher } from './watcher' 2 | 3 | export class Dep { 4 | // set结构可以自动去重,因为不可避免有些依赖会被重复添加 5 | // 例如有两个计算属性是依赖于dataA,第一遍计算出那两个计算属性时,dataA的dep是收集了他俩的watcher 6 | // 但是当其中一个计算属性重新计算时(比如另外一个依赖项改动了会影响此计算属性重新计算),会再次调取dataA 7 | // 的get拦截,也就是会再次触发 dep.addSub(),如果不加重复过滤这样的场景会一直递增下去,然后当dataA发生 8 | // 更改时遍历其subs,届时有太多不需要遍历的watcher,很大概率卡死 9 | subs = new Set() 10 | static target: Watcher | undefined // 全局唯一收集容器 11 | addSub() { 12 | if (Dep.target) this.subs.add(Dep.target) 13 | } 14 | notify(...params: any[]) { 15 | // 在某个属性发生变化时会执行其 dep.notify(),用来通知依赖这个属性的所有 watcher 16 | this.subs.forEach(function (watcher: any) { 17 | watcher.proxy.dirty = true // 标明数据脏了,当再次使用到这个值会重新计算 18 | watcher.update(...params) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/observer/index.ts: -------------------------------------------------------------------------------- 1 | import { ref as _ref } from './ref' 2 | import { computed as _computed } from './computed' 3 | import { watch as _watch } from './watch' 4 | import { ObserverValue, AnyFun, voidFun } from './types' 5 | 6 | /** 7 | * 响应式 8 | * 说明:与vue用法相似,但不提供多样的写法,只完成了基础用法,observer文件并不引用其他文件,为了方便移植 9 | * 完成功能:ref computed watch 10 | * 兼容性:需要支持proxy,如不支持则响应式无效 11 | * 12 | * 不支持proxy时各个函数表现: 13 | * ref:返回 { value: target } 对象 14 | * computed:返回 { value: fun() } 对象 15 | * watch:返回空函数 16 | */ 17 | 18 | function hasProxy(): boolean { 19 | return !!window.Proxy 20 | } 21 | 22 | function ref(target: T) { 23 | return hasProxy() ? _ref(target) : { value: target } 24 | } 25 | 26 | function computed(fun: AnyFun) { 27 | return hasProxy() ? _computed(fun) : { value: fun() } 28 | } 29 | 30 | function watch(target: ObserverValue, fun: voidFun) { 31 | return hasProxy() ? _watch(target, fun) : () => ({}) 32 | } 33 | 34 | export { ref, computed, watch } 35 | 36 | // ---------------- demo 1 ---------------- 37 | // const data = { 38 | // name: 'aaa', 39 | // age: 1, 40 | // cheng: { 41 | // a: 1, 42 | // b: 1, 43 | // c: 1 44 | // } 45 | // } 46 | // const a = ref(data) 47 | // const b = ref({ 48 | // name: 'bbb', 49 | // age: 2 50 | // }) 51 | // const c = ref({ 52 | // name: 'ccc', 53 | // age: 3 54 | // }) 55 | // const d = computed(() => a.value.age + b.value.age + c.value.age) 56 | 57 | // watch(d, val => { 58 | // console.log('val', val) 59 | // }) 60 | 61 | // setTimeout(() => { 62 | // a.value.age = 11 63 | // console.log('d', d.value) 64 | // }, 1000) 65 | 66 | // ---------------- demo 2 ---------------- 67 | // const a = ref(1) 68 | // const b = ref(2) 69 | // const c = ref(3) 70 | 71 | // const d = computed(() => a.value + b.value) // 3 72 | // const e = computed(() => d.value + c.value) // 6 73 | // const f = computed(() => e.value + d.value) // 9 74 | 75 | // c.value = 6 76 | 77 | // setTimeout(() => { 78 | // console.log('f', f.value) // 12 79 | // }, 1000) 80 | 81 | // ---------------- demo 3 ---------------- 82 | // const a = ref(1) 83 | // const b = ref(2) 84 | // const c = 3 85 | 86 | // const d = computed(() => a.value + b.value) // 3 87 | // const e = computed(() => d.value + c) // 6 88 | 89 | // setTimeout(() => { 90 | // console.log('e', e.value) // 6 91 | // }, 1000) 92 | -------------------------------------------------------------------------------- /packages/core/src/observer/ref.ts: -------------------------------------------------------------------------------- 1 | import { Dep } from './dep' 2 | import { ObserverValue, AnyFun } from './types' 3 | import { OBSERVERSIGNBOARD } from './config' 4 | 5 | function isRegExp(value: any) { 6 | return Object.prototype.toString.call(value) === `[object RegExp]` 7 | } 8 | 9 | class Observer { 10 | target: ObserverValue 11 | constructor(target: ObserverValue) { 12 | this.target = target 13 | } 14 | defineReactive() { 15 | const dep = new Dep() 16 | const handlers = getHandlers( 17 | () => { 18 | dep.addSub() 19 | }, 20 | (oldValue: any) => { 21 | dep.notify(oldValue) 22 | } 23 | ) 24 | return new Proxy>(this.target, handlers) 25 | } 26 | } 27 | 28 | function getHandlers( 29 | getCallBack?: AnyFun, 30 | setCallBack?: AnyFun 31 | ): ProxyHandler> { 32 | const proxyCache = new WeakMap, any>() 33 | const handlers: ProxyHandler> = { 34 | get(target, key: string, receiver) { 35 | // console.log(`读取属性:${key}`) 36 | const value = Reflect.get(target, key, receiver) 37 | getCallBack && getCallBack() 38 | if (typeof value === 'object' && value !== null && !isRegExp(value)) { 39 | let proxy = proxyCache.get(value) 40 | if (!proxy) { 41 | proxy = new Proxy(value, handlers) 42 | proxyCache.set(value, proxy) 43 | } 44 | return proxy 45 | } 46 | return value 47 | }, 48 | set(target, key: string, value, receiver) { 49 | const oldValue = Reflect.get(target, key, receiver) 50 | if (oldValue === value) return oldValue 51 | // console.log(`设置属性:${key}=${value}, oldValue:${oldValue}`) 52 | const beforeTarget = JSON.parse(JSON.stringify(target)) 53 | const result = Reflect.set(target, key, value, receiver) 54 | setCallBack && setCallBack(beforeTarget) 55 | return result 56 | } 57 | } 58 | return handlers 59 | } 60 | 61 | export const refMap = new WeakMap>() 62 | 63 | export function ref(target: T) { 64 | const newObj: any = { value: target } 65 | newObj[OBSERVERSIGNBOARD] = true 66 | 67 | const ob = new Observer(newObj) 68 | const proxy = ob.defineReactive() 69 | 70 | refMap.set(ob, proxy) 71 | return proxy 72 | } 73 | 74 | export function isRef(ref: any) { 75 | return !!ref[OBSERVERSIGNBOARD] 76 | } 77 | 78 | export function unRef(ref: any) { 79 | return isRef(ref) ? ref.value : ref 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/src/observer/types.ts: -------------------------------------------------------------------------------- 1 | export interface ObserverValue { 2 | value: T 3 | } 4 | 5 | export type AnyFun = { 6 | (...args: any[]): any 7 | } 8 | 9 | export type voidFun = { 10 | (...args: T[]): void 11 | } 12 | 13 | export type Options = { 14 | computed?: boolean 15 | watch?: boolean 16 | callback?: AnyFun 17 | } 18 | 19 | export type Proxy = { 20 | value: any 21 | dirty: boolean 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/observer/watch.ts: -------------------------------------------------------------------------------- 1 | import { Watcher } from './watcher' 2 | import { isRef } from './ref' 3 | import { ObserverValue, AnyFun, voidFun } from './types' 4 | 5 | function watchInit(callback: AnyFun, getter: AnyFun) { 6 | new Watcher('', { watch: true, callback }, getter) 7 | } 8 | 9 | export function watch(target: ObserverValue, fun: voidFun) { 10 | if (!isRef(target)) return 11 | watchInit( 12 | (newValue: T, oldValue: T) => { 13 | fun(newValue, oldValue) 14 | }, 15 | function () { 16 | return target.value 17 | } 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/observer/watcher.ts: -------------------------------------------------------------------------------- 1 | import { Dep } from './dep' 2 | import { computedMap } from './computed' 3 | import { AnyFun, Options, Proxy } from './types' 4 | 5 | const targetStack: Watcher[] = [] 6 | function pushTarget(_target: Watcher) { 7 | if (Dep.target) targetStack.push(Dep.target) 8 | Dep.target = _target 9 | } 10 | function popTarget() { 11 | Dep.target = targetStack.pop() 12 | } 13 | 14 | export class Watcher { 15 | vm: any 16 | computed: boolean 17 | watch: boolean 18 | proxy: Proxy 19 | dep: Dep | undefined 20 | getter: AnyFun | undefined 21 | callback: AnyFun | undefined 22 | constructor(vm: any, options: Options, getter?: AnyFun) { 23 | const { computed, watch, callback } = options 24 | this.getter = getter // 获取值函数 25 | this.computed = computed || false // 是否为计算属性 26 | this.watch = watch || false // 是否为监听属性 27 | this.callback = callback // 回调函数,专门给watch用的 28 | this.proxy = { 29 | value: '', // 存储这个属性的值,在不需要更新的时候会直接取这个值 30 | dirty: true // 表示这个属性是否脏了(脏了代表需要重新运算更新这个值) 31 | } 32 | this.vm = vm 33 | 34 | if (computed) { 35 | this.dep = new Dep() 36 | } else if (watch) { 37 | this.watchGet() 38 | } else { 39 | this.get() 40 | } 41 | } 42 | update(oldValue: any) { 43 | if (this.computed) { 44 | // 更新计算属性(不涉及渲染) 45 | this.dep!.notify() 46 | } else if (this.watch) { 47 | // 触发watch 48 | // this.watchGet() 49 | if (oldValue !== this.proxy.value) { 50 | this.callback && this.callback(this.proxy.value, oldValue) 51 | } 52 | } else { 53 | // 更新data, 触发依赖其的属性更新 54 | this.get() 55 | } 56 | } 57 | get() { 58 | // 存入当前上下文到依赖(表示当前是哪个属性在依赖其他属性,这样在其他属性发生变化时就知道应该通知谁了) 59 | pushTarget(this) 60 | 61 | // 目前只有计算属性才会调用 get 方法 62 | const value = this.computed ? computedMap.get(this.vm)!.call(this.vm) : '' 63 | if (value !== this.proxy.value) { 64 | this.proxy.dirty = false // 标记为不是脏的数据 65 | this.proxy.value = value // 缓存数据,在数据不脏的时候直接拿这个缓存值 66 | } 67 | popTarget() // 取出依赖 68 | return value 69 | } 70 | /** 71 | * 监听属性专用 - 拿到最新值并添加依赖 72 | */ 73 | watchGet() { 74 | pushTarget(this) // 将当前上下文放入 Dep.target 75 | this.proxy.dirty = false // 设定不为脏数据 76 | if (this.getter) { 77 | this.proxy.value = this.getter() // 设定值(在这个过程中就给上了依赖) 78 | } 79 | popTarget() // 取出上面放入 Dep.target 的上下文 80 | } 81 | /** 82 | * 计算属性专用 - 添加依赖 83 | * 其他值用到了这个计算属性就会被记录添加到依赖中 84 | */ 85 | depend() { 86 | this.dep!.addSub() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { EventBus } from '../lib/eventBus' 2 | import type { BaseInfo } from '../lib/base' 3 | import type { ObserverValue } from '../observer/types' 4 | 5 | export type WebTracing = { 6 | eventBus: EventBus 7 | baseInfo: BaseInfo 8 | sendData: any 9 | lineStatus: any 10 | options: ObserverValue // 配置信息 11 | firstScreen: any // 首屏信息 12 | intersection: any // 曝光采集 13 | } 14 | 15 | interface Pv { 16 | core?: boolean // 是否发送页面跳转相关数据 17 | } 18 | interface Performance { 19 | core?: boolean // 是否采集静态资源、接口的相关数据 20 | firstResource?: boolean // 是否采集首次进入页面的数据 21 | server?: boolean // 是否采集接口请求 22 | } 23 | interface Error { 24 | core?: boolean // 是否采集异常数据 25 | server?: boolean // 是否采集报错接口数据 26 | } 27 | interface Event { 28 | core?: boolean // 是否采集点击事件 29 | } 30 | 31 | /** 32 | * sdk内部配置 33 | */ 34 | export type InternalOptions = { 35 | dsn: string // 上报地址 36 | appName: string // 应用名称 37 | appCode: string // 应用code 38 | appVersion: string // 应用版本 39 | userUuid: string // 用户id(外部填充进来的id) 40 | sdkUserUuid: string // 用户id(sdk内部生成的id) 41 | debug: boolean // 是否开启调试模式(控制台会输出sdk动作) 42 | pv: Pv 43 | performance: Performance 44 | error: Error 45 | event: Event 46 | ext: AnyObj // 自定义全局附加参数(放在baseInfo中) 47 | tracesSampleRate: number // 抽样发送 48 | cacheMaxLength: number // 上报数据最大缓存数 49 | cacheWatingTime: number // 上报数据最大等待时间 50 | ignoreErrors: Array // 错误类型事件过滤 51 | ignoreRequest: Array // 请求类型事件过滤 52 | scopeError: boolean // 当某个时间段报错时,会将此类错误转为特殊错误类型,会新增错误持续时间范围 53 | localization: boolean // 是否本地化:sdk不再主动发送事件,事件都存储在本地,由用户手动调用方法发送 54 | sendTypeByXmlBody?: boolean // 是否强制指定发送形式为xml,body请求方式 55 | // whiteScreen: boolean // 开启白屏检测 56 | beforePushEventList: AnyFun[] // 添加到行为列表前的 hook (在这里面可以给出错误类型,然后就能达到用户想拿到是何种事件类型的触发) 57 | beforeSendData: AnyFun[] // 数据上报前的 hook 58 | afterSendData: AnyFun[] // 数据上报后的 hook 59 | localizationOverFlow: VoidFun // 本地化存储溢出后的回调 60 | recordScreen: boolean // 是否启动录屏 61 | } 62 | 63 | /** 64 | * sdk初始化入参配置 65 | */ 66 | export type InitOptions = { 67 | dsn: string // 上报地址 68 | appName: string // 应用名称 69 | appCode?: string // 应用code 70 | appVersion?: string // 应用版本 71 | userUuid?: string // 用户id(外部填充进来的id) 72 | debug?: boolean // 是否开启调试模式(控制台会输出sdk动作) 73 | pv?: Pv | boolean 74 | performance?: Performance | boolean 75 | error?: Error | boolean 76 | event?: Event | boolean 77 | ext?: { [key: string]: any } // 自定义全局附加参数(放在baseInfo中) 78 | tracesSampleRate?: number // 抽样发送 79 | cacheMaxLength?: number // 上报数据最大缓存数 80 | cacheWatingTime?: number // 上报数据最大等待时间 81 | ignoreErrors?: Array // 错误类型事件过滤 82 | ignoreRequest?: Array // 请求类型事件过滤 83 | scopeError?: boolean // 当某个时间段报错时,会将此类错误转为特殊错误类型,会新增错误持续时间范围 84 | localization?: boolean // 是否本地化:sdk不再主动发送事件,事件都存储在本地,由用户手动调用方法发送 85 | sendTypeByXmlBody?: boolean // 是否强制指定发送形式为xml,body请求方式 86 | // whiteScreen?: boolean // 开启白屏检测 87 | beforePushEventList?: (data: any) => any // 添加到行为列表前的 hook (在这里面可以给出错误类型,然后就能达到用户想拿到是何种事件类型的触发) 88 | beforeSendData?: (data: any) => any // 数据上报前的 hook 89 | afterSendData?: (data: any) => void // 数据上报后的 hook 90 | recordScreen?: boolean // 是否启动录屏 91 | } 92 | 93 | export type ElementOrList = Element | Element[] 94 | export interface TargetGather { 95 | target: ElementOrList 96 | threshold: number 97 | params?: AnyObj 98 | } 99 | 100 | export interface RecordEventScope { 101 | scope: string 102 | eventList: any[] 103 | } 104 | 105 | export type VoidFun = { 106 | (...args: any[]): void 107 | } 108 | 109 | export type AnyFun = { 110 | (...args: any[]): any 111 | } 112 | 113 | export type AnyObj = { 114 | [key: string]: T 115 | } 116 | 117 | export interface SendData { 118 | baseInfo: object 119 | eventInfo: unknown[] 120 | } 121 | -------------------------------------------------------------------------------- /packages/core/src/utils/debug.ts: -------------------------------------------------------------------------------- 1 | import { options } from '../lib/options' 2 | 3 | /** 4 | * 控制台输出信息 5 | * @param args 输出信息 6 | */ 7 | export function debug(...args: any[]): void { 8 | if (options.value.debug) console.log('@web-tracing: ', ...args) 9 | } 10 | 11 | /** 12 | * 控制台输出错误信息 13 | * @param args 错误信息 14 | */ 15 | export function logError(...args: any[]): void { 16 | console.error('@web-tracing: ', ...args) 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/element.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断元素是否含有目标属性 3 | */ 4 | export function getElByAttr(list: Element[], key: string): Element | undefined { 5 | return list.find(item => item.hasAttribute && item.hasAttribute(key)) 6 | } 7 | 8 | /** 9 | * 是否为简单的标签 10 | * 简单标签数组:['em', 'b', 'strong', 'span', 'img', 'i', 'code'] 11 | */ 12 | export function isSimpleEl(children: Element[]): boolean { 13 | if (children.length > 0) { 14 | const arr = ['em', 'b', 'strong', 'span', 'img', 'i', 'code'] 15 | const a = children.filter( 16 | ({ tagName }) => arr.indexOf(tagName.toLowerCase()) >= 0 17 | ) 18 | return a.length === children.length 19 | } 20 | return true 21 | } 22 | 23 | /** 24 | * 获取元素的关系字符串(从子级一直递归到最外层) 25 | * 例如两层div的关系会得到字符串: div>div 26 | */ 27 | export function getNodeXPath(node: Element, curPath = ''): string { 28 | if (!node) return curPath 29 | const parent = node.parentElement 30 | const { id } = node 31 | const tagName = node.tagName.toLowerCase() 32 | const path = curPath ? `>${curPath}` : '' 33 | 34 | if ( 35 | !parent || 36 | parent === document.documentElement || 37 | parent === document.body 38 | ) { 39 | return `${tagName}${path}` 40 | } 41 | 42 | if (id) { 43 | return `#${id}${path}` // 知道了id 就不需要获取上下级关系了(id是唯一的) 44 | } 45 | 46 | return getNodeXPath(parent, `${tagName}${path}`) 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/utils/global.ts: -------------------------------------------------------------------------------- 1 | import { isWindow } from './is' 2 | import { WebTracing } from '../types' 3 | 4 | /** 5 | * 是否为浏览器环境 6 | */ 7 | export const isBrowserEnv = isWindow(typeof window !== 'undefined' ? window : 0) 8 | 9 | /** 10 | * 是否为 electron 环境 11 | */ 12 | export const isElectronEnv = !!window?.process?.versions?.electron 13 | 14 | /** 15 | * 是否为测试环境 16 | */ 17 | export const isTestEnv = 18 | (typeof navigator !== 'undefined' && navigator.userAgent.includes('jsdom')) || 19 | // @ts-expect-error: jsdom 20 | (typeof window !== 'undefined' && window.jsdom) 21 | 22 | /** 23 | * 获取全局变量 24 | */ 25 | export function getGlobal(): Window { 26 | if (isBrowserEnv || isElectronEnv || isTestEnv) return window 27 | return {} as Window 28 | } 29 | 30 | /** 31 | * 获取全部变量 __webTracing__ 的引用地址 32 | */ 33 | export function getGlobalSupport(): WebTracing { 34 | _global.__webTracing__ = _global.__webTracing__ || ({} as WebTracing) 35 | return _global.__webTracing__ 36 | } 37 | 38 | /** 39 | * 判断sdk是否初始化 40 | * @returns sdk是否初始化 41 | */ 42 | export function isInit(): boolean { 43 | return !!_global.__webTracingInit__ 44 | } 45 | 46 | const _global = getGlobal() 47 | const _support = getGlobalSupport() 48 | 49 | export { _global, _support } 50 | -------------------------------------------------------------------------------- /packages/core/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | function isType(type: any) { 2 | return function (value: any): boolean { 3 | return Object.prototype.toString.call(value) === `[object ${type}]` 4 | } 5 | } 6 | 7 | export const isRegExp = isType('RegExp') 8 | export const isNumber = isType('Number') 9 | export const isString = isType('String') 10 | export const isBoolean = isType('Boolean') 11 | export const isNull = isType('Null') 12 | export const isUndefined = isType('Undefined') 13 | export const isSymbol = isType('Symbol') 14 | export const isFunction = isType('Function') 15 | export const isObject = isType('Object') 16 | export const isArray = isType('Array') 17 | export const isProcess = isType('process') 18 | export const isWindow = isType('Window') 19 | export const isFlase = (val: any) => { 20 | return isBoolean(val) && String(val) === 'false' 21 | } 22 | 23 | /** 24 | * 检测变量类型 25 | * @param type 26 | */ 27 | export const variableTypeDetection = { 28 | isNumber: isType('Number'), 29 | isString: isType('String'), 30 | isBoolean: isType('Boolean'), 31 | isNull: isType('Null'), 32 | isUndefined: isType('Undefined'), 33 | isSymbol: isType('Symbol'), 34 | isFunction: isType('Function'), 35 | isObject: isType('Object'), 36 | isArray: isType('Array'), 37 | isProcess: isType('process'), 38 | isWindow: isType('Window') 39 | } 40 | 41 | /** 42 | * 判断值是否为错误对象 43 | */ 44 | export function isError(error: Error): boolean { 45 | switch (Object.prototype.toString.call(error)) { 46 | case '[object Error]': 47 | return true 48 | case '[object Exception]': 49 | return true 50 | case '[object DOMException]': 51 | return true 52 | default: 53 | return false 54 | } 55 | } 56 | 57 | /** 58 | * 判断值是否为空对象 59 | */ 60 | export function isEmptyObject(obj: object): boolean { 61 | return isObject(obj) && Object.keys(obj).length === 0 62 | } 63 | 64 | /** 65 | * 判断值是否为空 ['', undefined, null] 66 | */ 67 | export function isEmpty(wat: any): boolean { 68 | return ( 69 | (isString(wat) && wat.trim() === '') || wat === undefined || wat === null 70 | ) 71 | } 72 | 73 | /** 74 | * 判断值与目标对象关系 75 | */ 76 | export function isExistProperty(obj: object, key: string): boolean { 77 | return Object.prototype.hasOwnProperty.call(obj, key) 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { deepAssign } from '../utils' 2 | import { SendData } from '../types' 3 | 4 | /** 5 | * 操作 localstorage 的工具类 6 | */ 7 | export class LocalStorageUtil { 8 | static maxSize = 5 * 1024 * 1000 // 5Mb 9 | 10 | static getItem(key: string): any { 11 | const value = localStorage.getItem(key) 12 | if (value) { 13 | return JSON.parse(value) 14 | } 15 | return null 16 | } 17 | 18 | static setItem(key: string, value: any): void { 19 | localStorage.setItem(key, JSON.stringify(value)) 20 | } 21 | 22 | static removeItem(key: string): void { 23 | localStorage.removeItem(key) 24 | } 25 | 26 | static getSize(): number { 27 | let size = 0 28 | for (let i = 0; i < localStorage.length; i++) { 29 | const key = localStorage.key(i) 30 | if (key) { 31 | const value = localStorage.getItem(key) 32 | if (value) { 33 | size += this.getBytes(value) 34 | } 35 | } 36 | } 37 | return size 38 | } 39 | 40 | /** 41 | * sendData专属存储 42 | * 特殊性: 43 | * 1. 每次存储检查最大容量(5M),如超过则不再继续存并通知外部 44 | * 2. 按照特定结构去拼接 45 | * 46 | * 注意:刷新页面测试会加入卸载事件,这在控制台是看不到的 47 | */ 48 | static setSendDataItem(key: string, value: SendData) { 49 | if (this.getSize() >= this.maxSize) return false 50 | 51 | const localItem = (this.getItem(key) || { 52 | baseInfo: {}, 53 | eventInfo: [] 54 | }) as SendData 55 | 56 | const newItem: SendData = { 57 | baseInfo: deepAssign(localItem.baseInfo, value.baseInfo), 58 | eventInfo: localItem.eventInfo.concat(value.eventInfo) 59 | } 60 | 61 | this.setItem(key, newItem) 62 | 63 | return true 64 | } 65 | 66 | private static getBytes(str: string): number { 67 | const blob = new Blob([str]) 68 | return blob.size 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/src/utils/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 会话控制,此会话只和具体的浏览器相关,与业务无关,和业务意义上的登录态没有任何关联,只是用于追踪同一个浏览器上访问页面的动作 3 | */ 4 | import { getCookieByName, uuid } from './index' 5 | import { SURVIVIE_MILLI_SECONDS, SESSION_KEY } from '../common' 6 | import { getTimestamp } from '../utils' 7 | 8 | /** 9 | * 刷新会话存续期 10 | */ 11 | function refreshSession() { 12 | const id = getCookieByName(SESSION_KEY) || `s_${uuid()}` 13 | const expires = new Date(getTimestamp() + SURVIVIE_MILLI_SECONDS) 14 | document.cookie = `${SESSION_KEY}=${id};path=/;max-age=1800;expires=${expires.toUTCString()}` 15 | return id 16 | } 17 | 18 | /** 19 | * 获取sessionid 20 | */ 21 | function getSessionId() { 22 | return getCookieByName(SESSION_KEY) || refreshSession() 23 | } 24 | 25 | refreshSession() // 初始化 26 | 27 | export { getSessionId, refreshSession } 28 | -------------------------------------------------------------------------------- /packages/vue2/README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |

web-tracing 监控插件 - vue2版本

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 | ## 示例项目(本地) 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 | ## 演示 20 | ### 事件监听 21 | logo 22 | 23 | ### 错误监听 24 | logo 25 | 26 | ### 资源监听 27 | logo 28 | 29 | 30 | ## 项目初衷 31 | 为了帮助开发们在公司平台上搭建一套前端监控平台 32 | 33 | > 作者心声: 想降低一下前端在这方面耗费的时间与精力,此项目会尽量针对每个场景都提供解决方案;即使最后没用我这套,但从在这里对某些场景方案有了一些了解,我也很开心 34 | 35 | ## 亮点 36 | 提供了多种定制化api最大限度帮助你应付各个场景的业务,例如: 37 | + 提供钩子函数让你对数据精确把握 38 | + 提供本地化选项api,让开发手动控制去发送监控数据 - 节省带宽 39 | + 提供批量错误api,在遇到无限错误时融合批量错误信息 - 节省带宽 40 | + 提供抽样发送api - 节省带宽 41 | + 提供 错误/请求 事件的过滤api 42 | + 等等.... 43 | 44 | 站在技术角度,因为明确此项目可能更多的是应用在公司平台上,大概率会二开,所以作者对项目结构以及代码都严格要求 45 | + 架构 - demo、核心sdk代码、文档都在同一个项目中,调试、部署都很方便 46 | + 封装 - sdk存在大量的重写或者监听,对此有统一流程 47 | + 响应式 - 项目内部实现了vue响应式,也应用在 options 对象中,相信你接触会后受益良多 48 | + 多版本 - 针对不同平台提供多个版本(目前只有js、vue2、vue3),受益于monorepo架构可一键发布 49 | + 内聚 - 目前核心功能的所有代码都没有分包,虽然monorepo架构支持,但作者认为目前分包不利于代码阅读以及二开方便 50 | + 文档/注释 - 完善的文档以及非常全的注释,力求帮助你快速了解这一切 51 | 52 | ## 未来方向 53 | 会写一套服务端(nest) + 后台查看监控数据平台(vue),有以下几点考量 54 | + 提供服务端能力(目前只是在采集端发力) 55 | + 可以在线体验此项目 56 | + 提供更多示例代码给开发们,再次降低这一套代码在公司的推广难度 57 | + 作者也想站在业务的角度多思考还能从哪些方面此项目还缺失哪些功能 58 | 59 | 针对首屏加载的监控做出更多精细化的东西,例如考虑sdk的绝对轻量化 60 | 61 | ## 加入我们 62 | 63 | 64 | - 如果对此项目有疑虑或者有优化点,欢迎进群讨论 65 | - Bug 反馈请直接去 Github 上面提 Issues,我会实时收到邮件提醒前去查看 66 | 67 |
68 |
69 |
70 |
71 |
72 | 73 | ## 🙏🙏🙏 点个Star 74 | 75 | **如果您觉得这个项目还不错, 可以在 [Github](https://github.com/M-cheng-web/web-tracing) 上面帮我点个`star`, 支持一下作者ヾ(◍°∇°◍)ノ゙** 76 | 77 |
78 | 79 | ## 特别感谢 80 | + [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),写的很好受益匪浅 81 | + [wangshitao929@163.com](wangshitao929@163.com) -------------------------------------------------------------------------------- /packages/vue2/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | init, 3 | InitOptions, 4 | traceError, 5 | logError, 6 | parseError, 7 | SENDID 8 | } from '@web-tracing/core' 9 | 10 | function install(Vue: any, options: InitOptions) { 11 | const handler = Vue.config.errorHandler 12 | Vue.config.errorHandler = function (err: Error, vm: any, info: string): void { 13 | // const match = err.stack!.match(/(?<=http:\/\/.*:\d+\/).*:\d+:\d+/) 14 | // const position = match ? match[0] : '' 15 | // const line = position.split(':')[1] // 行 16 | // const col = position.split(':')[2] // 列 17 | // traceError({ 18 | // eventId: err.name, 19 | // errMessage: err.message, 20 | // line, 21 | // col 22 | // }) 23 | 24 | logError(err) 25 | const errorInfo = { eventId: SENDID.CODE, ...parseError(err) } 26 | traceError(errorInfo) 27 | if (handler) handler.apply(null, [err, vm, info]) 28 | } 29 | init(options) 30 | } 31 | 32 | export default { install } 33 | export * from '@web-tracing/core' 34 | -------------------------------------------------------------------------------- /packages/vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@web-tracing/vue2", 3 | "version": "2.0.9", 4 | "description": "基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段 - vue2版本", 5 | "main": "./dist/index.cjs", 6 | "module": "./dist/index.mjs", 7 | "jsdelivr": "./dist/index.iife.min.js", 8 | "types": "./dist/index.d.ts", 9 | "sideEffects": false, 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.cjs", 14 | "types": "./dist/index.d.ts" 15 | }, 16 | "./*": "./*" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/M-cheng-web/web-tracing.git", 22 | "directory": "packages/vue2" 23 | }, 24 | "author": "M-cheng-web ", 25 | "keywords": [ 26 | "埋点", 27 | "性能", 28 | "异常", 29 | "性能采集", 30 | "异常采集", 31 | "前端埋点", 32 | "前端性能采集" 33 | ], 34 | "peerDependencies": {}, 35 | "dependencies": { 36 | "@web-tracing/core": "workspace:*" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/M-cheng-web/web-tracing/issues" 40 | }, 41 | "homepage": "https://github.com/M-cheng-web/web-tracing#readme", 42 | "unpkg": "./dist/index.iife.min.js" 43 | } 44 | -------------------------------------------------------------------------------- /packages/vue3/README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |

web-tracing 监控插件 - vue3版本

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 | ## 示例项目(本地) 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 | ## 演示 20 | ### 事件监听 21 | logo 22 | 23 | ### 错误监听 24 | logo 25 | 26 | ### 资源监听 27 | logo 28 | 29 | 30 | ## 项目初衷 31 | 为了帮助开发们在公司平台上搭建一套前端监控平台 32 | 33 | > 作者心声: 想降低一下前端在这方面耗费的时间与精力,此项目会尽量针对每个场景都提供解决方案;即使最后没用我这套,但从在这里对某些场景方案有了一些了解,我也很开心 34 | 35 | ## 亮点 36 | 提供了多种定制化api最大限度帮助你应付各个场景的业务,例如: 37 | + 提供钩子函数让你对数据精确把握 38 | + 提供本地化选项api,让开发手动控制去发送监控数据 - 节省带宽 39 | + 提供批量错误api,在遇到无限错误时融合批量错误信息 - 节省带宽 40 | + 提供抽样发送api - 节省带宽 41 | + 提供 错误/请求 事件的过滤api 42 | + 等等.... 43 | 44 | 站在技术角度,因为明确此项目可能更多的是应用在公司平台上,大概率会二开,所以作者对项目结构以及代码都严格要求 45 | + 架构 - demo、核心sdk代码、文档都在同一个项目中,调试、部署都很方便 46 | + 封装 - sdk存在大量的重写或者监听,对此有统一流程 47 | + 响应式 - 项目内部实现了vue响应式,也应用在 options 对象中,相信你接触会后受益良多 48 | + 多版本 - 针对不同平台提供多个版本(目前只有js、vue2、vue3),受益于monorepo架构可一键发布 49 | + 内聚 - 目前核心功能的所有代码都没有分包,虽然monorepo架构支持,但作者认为目前分包不利于代码阅读以及二开方便 50 | + 文档/注释 - 完善的文档以及非常全的注释,力求帮助你快速了解这一切 51 | 52 | ## 未来方向 53 | 会写一套服务端(nest) + 后台查看监控数据平台(vue),有以下几点考量 54 | + 提供服务端能力(目前只是在采集端发力) 55 | + 可以在线体验此项目 56 | + 提供更多示例代码给开发们,再次降低这一套代码在公司的推广难度 57 | + 作者也想站在业务的角度多思考还能从哪些方面此项目还缺失哪些功能 58 | 59 | 针对首屏加载的监控做出更多精细化的东西,例如考虑sdk的绝对轻量化 60 | 61 | ## 加入我们 62 | 63 | 64 | - 如果对此项目有疑虑或者有优化点,欢迎进群讨论 65 | - Bug 反馈请直接去 Github 上面提 Issues,我会实时收到邮件提醒前去查看 66 | 67 |
68 |
69 |
70 |
71 |
72 | 73 | ## 🙏🙏🙏 点个Star 74 | 75 | **如果您觉得这个项目还不错, 可以在 [Github](https://github.com/M-cheng-web/web-tracing) 上面帮我点个`star`, 支持一下作者ヾ(◍°∇°◍)ノ゙** 76 | 77 |
78 | 79 | ## 特别感谢 80 | + [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),写的很好受益匪浅 81 | + [wangshitao929@163.com](wangshitao929@163.com) -------------------------------------------------------------------------------- /packages/vue3/index.ts: -------------------------------------------------------------------------------- 1 | import { init, InitOptions } from '@web-tracing/core' 2 | 3 | function install(app: any, options: InitOptions) { 4 | init(options) 5 | } 6 | 7 | export default { install } 8 | export * from '@web-tracing/core' 9 | -------------------------------------------------------------------------------- /packages/vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@web-tracing/vue3", 3 | "version": "2.0.9", 4 | "description": "基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段 - vue3版本", 5 | "main": "./dist/index.cjs", 6 | "module": "./dist/index.mjs", 7 | "jsdelivr": "./dist/index.iife.min.js", 8 | "types": "./dist/index.d.ts", 9 | "sideEffects": false, 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.cjs", 14 | "types": "./dist/index.d.ts" 15 | }, 16 | "./*": "./*" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/M-cheng-web/web-tracing.git", 22 | "directory": "packages/vue3" 23 | }, 24 | "author": "M-cheng-web ", 25 | "keywords": [ 26 | "埋点", 27 | "性能", 28 | "异常", 29 | "性能采集", 30 | "异常采集", 31 | "前端埋点", 32 | "前端性能采集" 33 | ], 34 | "peerDependencies": {}, 35 | "dependencies": { 36 | "@web-tracing/core": "workspace:*" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/M-cheng-web/web-tracing/issues" 40 | }, 41 | "homepage": "https://github.com/M-cheng-web/web-tracing#readme", 42 | "unpkg": "./dist/index.iife.min.js" 43 | } 44 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - examples/* 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | require('esbuild-register') 2 | module.exports = require('./scripts/rollup.config.ts') 3 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import assert from 'assert' 3 | import { execSync as exec } from 'child_process' 4 | import fs from 'fs-extra' 5 | import fg from 'fast-glob' 6 | import consola from 'consola' 7 | import { packages } from '../meta/packages' 8 | import { version } from '../package.json' 9 | 10 | const rootDir = path.resolve(__dirname, '..') 11 | const watch = process.argv.includes('--watch') 12 | 13 | const FILES_COPY_ROOT = ['LICENSE'] 14 | const FILES_COPY_LOCAL = ['README.md', 'index.json', '*.cjs', '*.mjs', '*.d.ts'] 15 | 16 | assert(process.cwd() !== __dirname) 17 | 18 | /** 19 | * 将打包后的 dist 文件进行二次处理 (复制一些公共文件进 dist 包以及文件的移动) 20 | */ 21 | async function buildMetaFiles() { 22 | for (const { name, moduleJs, iife } of packages) { 23 | const packageRoot = path.resolve(__dirname, '..', 'packages', name) 24 | const packageDist = path.resolve(packageRoot, 'dist') 25 | 26 | // ----- 复制 core README ----- 27 | if (name === 'core') { 28 | await fs.copyFile( 29 | path.join(rootDir, 'README.md'), 30 | path.join(packageDist, 'README.md') 31 | ) 32 | } 33 | 34 | // ----- 复制 LICENSE 证书 ----- 35 | for (const file of FILES_COPY_ROOT) { 36 | await fs.copyFile(path.join(rootDir, file), path.join(packageDist, file)) 37 | } 38 | 39 | // ----- 将部分文件放入dist包 ----- 40 | const files = await fg(FILES_COPY_LOCAL, { cwd: packageRoot }) 41 | for (const file of files) { 42 | await fs.copyFile( 43 | path.join(packageRoot, file), 44 | path.join(packageDist, file) 45 | ) 46 | } 47 | 48 | // ----- 更改本地引用版本 ----- 49 | const packageJSON = await fs.readJSON( 50 | path.join(packageRoot, 'package.json') 51 | ) 52 | // 当子类包互相引用时,要手动更改其版本(不改的话则是 workspace) 53 | for (const key of Object.keys(packageJSON.dependencies || {})) { 54 | if (key.startsWith('@web-tracing/')) { 55 | packageJSON.dependencies[key] = version 56 | } 57 | } 58 | // 因为只有在打包的时候才用这些,开发时需要 ./dist/index.mjs 才能正常引用 59 | // 更改 exports、types、main、module、unpkg、jsdelivr(./dist/index.mjs => ./index.mjs) 60 | packageJSON.types = './index.d.ts' 61 | packageJSON.main = moduleJs ? './index.mjs' : './index.cjs' 62 | packageJSON.module = './index.mjs' 63 | if (iife !== false) { 64 | packageJSON.unpkg = './index.iife.min.js' 65 | packageJSON.jsdelivr = './index.iife.min.js' 66 | } 67 | packageJSON.exports = { 68 | ...packageJSON.exports, 69 | '.': { 70 | import: './index.mjs', 71 | require: './index.cjs', 72 | types: './index.d.ts' 73 | }, 74 | './*': './*' 75 | } 76 | 77 | await fs.writeJSON(path.join(packageDist, 'package.json'), packageJSON, { 78 | spaces: 2 79 | }) 80 | } 81 | } 82 | 83 | async function build() { 84 | consola.info('Clean up') 85 | 86 | exec('pnpm run clean', { stdio: 'inherit' }) 87 | 88 | exec(`pnpm run build:rollup${watch ? '-watch' : ''}`, { 89 | stdio: 'inherit' 90 | }) 91 | 92 | consola.info('build:types') 93 | 94 | exec('pnpm run build:types', { stdio: 'inherit' }) 95 | 96 | await buildMetaFiles() 97 | } 98 | 99 | async function cli() { 100 | try { 101 | await build() 102 | } catch (e) { 103 | process.exit(1) 104 | } 105 | } 106 | 107 | export { build } 108 | 109 | if (require.main === module) cli() 110 | -------------------------------------------------------------------------------- /scripts/examples/js.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | # 进入生成的文件夹 6 | cd examples-copy/vanilla 7 | 8 | git init 9 | git add -A 10 | git commit -m 'deploy' 11 | 12 | git push -f git@github.com:M-cheng-web/web-tracing-examples-js.git main 13 | 14 | cd - -------------------------------------------------------------------------------- /scripts/examples/vue2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | # 进入生成的文件夹 6 | cd examples-copy/vue2 7 | 8 | git init 9 | git add -A 10 | git commit -m 'deploy' 11 | 12 | git push -f git@github.com:M-cheng-web/web-tracing-examples-vue2.git main 13 | 14 | cd - -------------------------------------------------------------------------------- /scripts/examples/vue3.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | # 进入生成的文件夹 6 | cd examples-copy/vue3 7 | 8 | git init 9 | git add -A 10 | git commit -m 'deploy' 11 | 12 | git push -f git@github.com:M-cheng-web/web-tracing-examples-vue3.git main 13 | 14 | cd - -------------------------------------------------------------------------------- /scripts/fix-types.ts: -------------------------------------------------------------------------------- 1 | // import fg from 'fast-glob' 2 | // import fs from 'fs-extra' 3 | 4 | // tsc --emitDeclarationOnly 会生成 .type 文件,在这里需要对那些声明文件进行修改 5 | /** 6 | * 修正自动生成的 type 文件 7 | * 这里主要是为了兼容 vue-demi(目前监控项目用不上,先保留在这) 8 | */ 9 | export async function fixTypes() { 10 | // const files = await fg(['types/**/*.d.ts', 'packages/*/dist/*.d.ts'], { 11 | // onlyFiles: true 12 | // }) 13 | // for (const f of files) { 14 | // const raw = await fs.readFile(f, 'utf-8') 15 | // const changed = raw 16 | // .replace(/"@vue\/composition-api"/g, "'vue-demi'") 17 | // .replace(/"vue"/g, "'vue-demi'") 18 | // .replace(/'vue'/g, "'vue-demi'") 19 | // await fs.writeFile(f, changed, 'utf-8') 20 | // } 21 | } 22 | 23 | fixTypes() 24 | -------------------------------------------------------------------------------- /scripts/publish-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 发布文档 4 | 5 | # 确保脚本抛出遇到的错误 6 | set -e 7 | 8 | # 生成静态文件 9 | pnpm run docs:build 10 | 11 | # 进入生成的文件夹 12 | cd docs/.vitepress/dist 13 | 14 | git init 15 | git add -A 16 | git commit -m 'deploy' 17 | 18 | # 如果发布到 https://.github.io/ 19 | git push -f git@github.com:M-cheng-web/web-tracing.git main:gh-pages 20 | 21 | cd - -------------------------------------------------------------------------------- /scripts/publish-examples.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将 examples 下的所有项目移植到 3 | */ 4 | import path from 'path' 5 | import fs from 'fs-extra' 6 | import { packages } from '../meta/packages' 7 | import { version } from '../package.json' 8 | import { execSync as exec } from 'child_process' 9 | 10 | const rootDir = path.resolve(__dirname, '..') 11 | const examplesDir = path.resolve(rootDir, 'examples') 12 | const newExamplesDir = path.resolve(rootDir, 'examples-copy') 13 | 14 | async function copyFolder(source: string, destination: string) { 15 | try { 16 | await fs.remove(destination) 17 | await fs.ensureDir(destination) // 确保目标文件夹存在,不存在则新建 18 | 19 | // 过滤某些文件夹不复制 20 | const filterRootFile = ['dist', 'node_modules'] // 这里只支持根目录的过滤 21 | const filterList: string[] = [] 22 | for (const { exampleName } of packages) { 23 | for (const rootFileName of filterRootFile) { 24 | filterList.push(`${exampleName}/${rootFileName}`) 25 | } 26 | } 27 | 28 | await fs.copy(source, destination, { 29 | overwrite: true, // 是否覆盖已存在的文件 30 | filter: (src: string) => { 31 | return filterList.every(item => !src.includes(item)) 32 | } 33 | }) 34 | } catch (error) { 35 | console.error('文件夹复制失败', error) 36 | } 37 | } 38 | 39 | async function changeFile() { 40 | for (const { exampleName } of packages) { 41 | const packageJSON = await fs.readJSON( 42 | path.join(newExamplesDir, exampleName, 'package.json') 43 | ) 44 | 45 | // 当子类包互相引用时,要手动更改其版本(不改的话则是 workspace) 46 | for (const key of Object.keys(packageJSON.dependencies || {})) { 47 | if (key.startsWith('@web-tracing/')) { 48 | packageJSON.dependencies[key] = version 49 | } 50 | } 51 | 52 | await fs.writeJSON( 53 | path.join(newExamplesDir, exampleName, 'package.json'), 54 | packageJSON, 55 | { 56 | spaces: 2 57 | } 58 | ) 59 | } 60 | } 61 | 62 | // async function publish() { 63 | // for (const { exampleName, exampleGitHubPath } of packages) { 64 | // const cmd = `cd ${'examples-copy'}/${exampleName} && git init && git add -A && git commit -m 'deploy' && git push -f ${exampleGitHubPath} main` 65 | // exec(cmd, { stdio: 'inherit' }) 66 | // } 67 | // } 68 | 69 | async function start() { 70 | await copyFolder(examplesDir, newExamplesDir) 71 | await changeFile() 72 | // await publish() 73 | exec('pnpm run example:publish-js', { stdio: 'inherit' }) 74 | exec('pnpm run example:publish-vue2', { stdio: 'inherit' }) 75 | exec('pnpm run example:publish-vue3', { stdio: 'inherit' }) 76 | } 77 | 78 | start() 79 | -------------------------------------------------------------------------------- /scripts/publish.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import path from 'path' 3 | import consola from 'consola' 4 | import { version } from '../package.json' 5 | import { packages } from '../meta/packages' 6 | 7 | execSync('npm run build', { stdio: 'inherit' }) 8 | 9 | let command = 'npm publish --access public' 10 | 11 | if (version.includes('beta')) command += ' --tag beta' 12 | 13 | for (const { name } of packages) { 14 | execSync(command, { 15 | stdio: 'inherit', 16 | cwd: path.join('packages', name, 'dist') 17 | }) 18 | consola.success(`Published @web-tracing/${name}`) 19 | } 20 | -------------------------------------------------------------------------------- /scripts/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import esbuild from 'rollup-plugin-esbuild' 2 | import dts from 'rollup-plugin-dts' 3 | import json from '@rollup/plugin-json' 4 | import nodeResolve from '@rollup/plugin-node-resolve' 5 | import commonjs from '@rollup/plugin-commonjs' 6 | import type { Options as ESBuildOptions } from 'rollup-plugin-esbuild' 7 | import type { OutputOptions, RollupOptions } from 'rollup' 8 | import { packages } from '../meta/packages' 9 | 10 | const configs: RollupOptions[] = [] 11 | 12 | const esbuildPlugin = esbuild({ target: 'esnext' }) 13 | const dtsPlugin = [dts()] 14 | 15 | const externals = [] 16 | // const externals = ['@web-tracing/core'] 17 | 18 | const esbuildMinifer = (options: ESBuildOptions) => { 19 | const { renderChunk } = esbuild(options) 20 | return { name: 'esbuild-minifer', renderChunk } 21 | } 22 | 23 | for (const { 24 | globals, 25 | name, 26 | external, 27 | iife, 28 | build, 29 | cjs, 30 | mjs, 31 | dts, 32 | target 33 | } of packages) { 34 | if (build === false) continue 35 | 36 | const iifeGlobals = { 37 | '@web-tracing/core': 'WebTracing', 38 | ...(globals || {}) 39 | } 40 | const iifeName = 'WebTracing' 41 | 42 | // 打包 hooks & utils 43 | const fn = 'index' 44 | const input = `packages/${name}/index.ts` 45 | const output: OutputOptions[] = [] 46 | 47 | if (mjs !== false) { 48 | output.push({ 49 | file: `packages/${name}/dist/${fn}.mjs`, 50 | format: 'es' 51 | }) 52 | } 53 | 54 | if (cjs !== false) { 55 | output.push({ 56 | file: `packages/${name}/dist/${fn}.cjs`, 57 | format: 'cjs' 58 | }) 59 | } 60 | 61 | if (iife !== false) { 62 | output.push( 63 | { 64 | file: `packages/${name}/dist/${fn}.iife.js`, 65 | format: 'iife', 66 | name: iifeName, 67 | extend: true, 68 | globals: iifeGlobals 69 | }, 70 | { 71 | file: `packages/${name}/dist/${fn}.iife.min.js`, 72 | format: 'iife', 73 | name: iifeName, 74 | extend: true, 75 | globals: iifeGlobals, 76 | plugins: [esbuildMinifer({ minify: true })] 77 | } 78 | ) 79 | } 80 | 81 | configs.push({ 82 | input, 83 | output, 84 | plugins: [ 85 | commonjs(), 86 | nodeResolve(), 87 | json(), 88 | target ? esbuild({ target }) : esbuildPlugin 89 | ], 90 | external: [...externals, ...(external || [])] 91 | }) 92 | 93 | if (dts !== false) { 94 | configs.push({ 95 | input, 96 | output: { 97 | file: `packages/${name}/dist/${fn}.d.ts`, 98 | format: 'es' 99 | }, 100 | plugins: dtsPlugin, 101 | external: [...externals, ...(external || [])] 102 | }) 103 | } 104 | } 105 | 106 | export default configs 107 | -------------------------------------------------------------------------------- /scripts/test-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 测试仓库初始化 4 | 5 | # 确保脚本抛出遇到的错误 6 | set -e 7 | 8 | cd examples/vanilla/ 9 | pnpm install 10 | 11 | cd - 12 | 13 | cd examples/vue2/ 14 | pnpm install 15 | 16 | cd - 17 | 18 | cd examples/vue3/ 19 | pnpm install 20 | 21 | cd - -------------------------------------------------------------------------------- /scripts/update.ts: -------------------------------------------------------------------------------- 1 | import { updatePackageJSON } from './utils' 2 | 3 | async function run() { 4 | await Promise.all([updatePackageJSON()]) 5 | } 6 | 7 | run() 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "declaration": true, 12 | "declarationDir": "./types", 13 | "resolveJsonModule": true, 14 | "rootDir": ".", 15 | "baseUrl": ".", 16 | "jsx": "preserve", 17 | "skipLibCheck": true, 18 | "skipDefaultLibCheck": true, 19 | "noUnusedLocals": true, 20 | "paths": { 21 | "@web-tracing/core": ["./packages/core/index.ts"], 22 | // "@web-tracing/core/*": ["./packages/core/*"], 23 | // "@web-tracing/component": ["./packages/component/index.ts"], 24 | }, 25 | "types": [ 26 | // "vitest", 27 | "vitest/globals", 28 | "vitest/jsdom" 29 | // "@types/web-bluetooth" 30 | ] 31 | }, 32 | "include": [ 33 | "global.d.ts", 34 | "packages", 35 | "packages/.vitepress/components/*.vue", 36 | "packages/.vitepress/*.ts", 37 | "meta", 38 | "vitest.config.mts" 39 | ], 40 | "exclude": [ 41 | "node_modules", 42 | "**/**/*.md", 43 | "**/dist", 44 | "packages/.test", 45 | "packages/_docs" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | include: ['packages/core/__test__/**.spec.ts'], 7 | environment: 'jsdom' 8 | } 9 | }) 10 | --------------------------------------------------------------------------------