├── .babelrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierrc.json ├── .vscode ├── launch.json └── settings.json ├── ChangeLog.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── package.json ├── src ├── bury.filter.ts ├── bury.ts ├── bury.vue.ts ├── common.ts ├── config.ts ├── index.interface.ts ├── index.ts └── map.config.ts └── tsconfig.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }]], 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | package-lock.json 4 | dist 5 | pnpm-lock.yaml 6 | *.log -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .husky 4 | coverage 5 | test 6 | .babelrc.js 7 | .eslint* 8 | .prettierrc* 9 | commitlint* 10 | jest* 11 | /src 12 | pnpm* 13 | tsconfig.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/src/index.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["bupoint", "fingerprintjs"] 3 | } 4 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## 0.5.0 2 | 3 | 1. 删除返回的 ip 地址和 cityName,因为原 sohu 获取 ip 地址的 api 已经关闭,如有需求,请自行创造后台接口,并在 init 方法的参数中添加(实际上请求本身就会带 ip 地址,前端没有必要再获取一次 ip 地址); 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Xmo 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 | # xmon-bury [![npm license](https://img.shields.io/npm/l/@xmon/bury.svg?sanitize=true)](https://github.com/darkXmo/bury/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/@xmon/bury.svg?sanitize=true)](https://www.npmjs.com/package/@xmon/bury) 2 | 3 | **如果你的项目埋点会严重影响到业务代码,是时候考虑使用 @xmon/bury 了** 4 | 5 | - 无痕埋点 6 | 7 | - 访问调查 8 | 9 | - 行为监测 10 | 11 | 除了必要的添加 eventId,即为项目添加 ID 标识的行为以外,`@xmon/bury` 不会影响到你的业务代码,你只需要添加配置就够了! 12 | 13 | 页面行为埋点,通过事件监听来进行行为监测,目前可以监控事件包括 14 | 15 | 1. 点击事件(Click) 16 | 2. 页面加载(Load & Unload) 17 | 3. 特定行为(Action) 18 | 4. Axios 请求(Api) 19 | 20 | 5. 路由跳转(Router) 21 | 22 | ## 安装 23 | 24 | ```bash 25 | # yarn 26 | yarn add @xmon/bury 27 | 28 | # npm 29 | npm install @xmon/bury 30 | 31 | # pnpm 32 | pnpm install @xmon/bury 33 | ``` 34 | 35 | ## Examples 36 | 37 | ### 监听一般事件(开启监听点击、页面加载,特定行为) 38 | 39 | ```javascript 40 | // main.js 41 | import { init } from "@xmon/bury"; 42 | import config from "./config.js"; 43 | 44 | const bury = init(config); 45 | ``` 46 | 47 | #### 配置 48 | 49 | 你需要在 `config` 中指定你要监听的路由,路由对应事件(进入和离开)的 `eventId`。 50 | 51 | 同时你需要指定埋点的**基础参数**,他们通常是环境,埋点版本以及系统版本,这些**参数都是可选的**。 52 | 53 | ```javascript 54 | // config.js 55 | import { initUrlMap } from "@xmon/bury"; 56 | 57 | // 用initUrlMap配置你想要监听的1页面路径和2加载页面,3离开页面的事件ID 58 | initUrlMap([ 59 | { 60 | path: "/user/:id", 61 | leave: "eventIdLeavePage", // Leave EventId 62 | enter: "eventIdEnterPage", // Enter EventId 63 | }, 64 | ]); 65 | 66 | // 这里填写埋点事件返回值中的额外字段,通常你需要添加以下几个配置信息 67 | const config = { 68 | environment: process.env.NODE_ENV, 69 | version: "1.0.0", 70 | }; 71 | export default config; 72 | ``` 73 | 74 | ### 监听 Router 75 | 76 | 如果你使用的是 `Vue` 单页面应用,则还需要监听 `Vue-Router` 跳转,因此你还需要传入 `router` 实例作为第 2 个参数 77 | 78 | ```javascript 79 | import router from "@/router"; 80 | // 把router实例注册到bury中 81 | const bury = init(config, router); 82 | ``` 83 | 84 | ### 监听 Api 85 | 86 | 如果你需要监听 `Axios Api` ,则需要封装 `Axios` 实例。 87 | 88 | ```javascript 89 | import axios from "axios"; 90 | import { trackApi } from "@xmon/bury"; 91 | 92 | const axiosInstance = axios.create({ 93 | ... 94 | }); 95 | // 提醒 @xmon/bury 监听 axios示例 发出请求的行为 96 | trackApi(axiosInstance); 97 | ``` 98 | 99 | #### 配置 100 | 101 | 和页面监听类似,你也需要指定你要监听的 `api` 路径以及对应的 `eventId`。 102 | 103 | ```javascript 104 | import { initUrlMap, initApiMap } from "@xmon/bury"; 105 | 106 | initUrlMap([ 107 | ... 108 | ]); 109 | // 利用 initApiMap 来配置需要监听的url 110 | initApiMap([ 111 | { 112 | url: "/v3/search/anime", 113 | eventId: "eventIdApi", 114 | }, 115 | ]); 116 | 117 | const config = { 118 | ... 119 | }; 120 | export default config; 121 | ``` 122 | 123 | > 值得注意的是,无论是监听页面加载还是监听 `api`,都会忽略 `query` 参数。 124 | 125 | ### 监听点击事件 126 | 127 | ```vue 128 | 129 | ``` 130 | 131 | 对于需要监听点击事件的元素,添加 `data-bupoint` 属性,并注入 `eventId` 即可。 132 | 133 | ### 监听特定行为 134 | 135 | ```javascript 136 | import { track } from "@xmon/bury"; 137 | // 对于特定的行为,你需要将行为包装成函数,并配置好eventId,然后再使用track来监听这个函数行为 138 | const increase = track(() => { 139 | console.log("I am tracked"); 140 | }, "eventId"); 141 | 142 | // track的返回值是你传入的函数,原封不动。 143 | // increase = () => { console.log('I am tracked') } 144 | ``` 145 | 146 | 对于行为,你应当使用 `track` 进行封装,第一个参数是要封装的函数,第二个参数是 `eventId` 。 147 | 148 | `track` 会在封装后返回被封装的函数。 149 | 150 | 埋点行为发生在特定行为执行之前。 151 | 152 | ### 立即触发 153 | 154 | 当调用 `tracked` 方法时,会立即触发 `Action` 类型埋点回调。 155 | 156 | ```javascript 157 | import { tracked } from "@xmon/bury"; 158 | 159 | ... 160 | tracked("eventId"); 161 | ... 162 | ``` 163 | 164 | ## 触发埋点事件回调 165 | 166 | 触发监听行为会同时触发埋点行为,通过 `onBury` 我们可以获取到埋点行为的回调。 167 | 168 | **初始化(`init`)之后才能访问到 `instance`、`track`、`trackApi`、`onBury` 等方法,否则会抛出未定义错误** 169 | 170 | ```javascript 171 | import { onBury } from "@xmon/bury"; 172 | 173 | // 做好配置之后,你可以使用 onBury 来监听事件 174 | // 一旦 你配置过的url加载或关闭了 OR 你监听的api请求发送了 OR 你监听的事件被调用了 OR 你观察的Dom被点击了 => 就会触发在 onBury 中注册的回调函数 175 | onBury((value) => { 176 | // 下文中 BuryConfig 中会说明 payload 中包含哪些值 177 | const buryInfo = value.payload; 178 | // 下面是我的埋点回调示例行为,你应当用你的行为代替示例 179 | const queries = Object.entries(buryInfo) 180 | .map(([key, value]) => { 181 | return key + "=" + encodeURI(value); 182 | }) 183 | .join("&"); 184 | let img = new Image(1, 1); 185 | // 请将url改成你的后端埋点系统的API 186 | img.src = `http://exmapleApi.com/bury?` + queries; 187 | // 3000ms超时处理 188 | setTimeout(() => { 189 | if (img && (!img.complete || !img.naturalWidth)) { 190 | img.src = ""; 191 | } 192 | }, 3000); 193 | }); 194 | ``` 195 | 196 | 每当被监听的事件发生的时候,都会注册在 `onBury` 事件中的回调函数。 197 | 198 | 在这个例子中,它将会取出回调参数中的 `payload` ,并将它封装并发出 `img` 的 `Get` 请求。 199 | 200 | > 由于 `onBeforeUnload` 方法在页面即将关闭时执行,此时无法使用 `Axios` 来发起异步请求。但 `img` 和 `XMLHttpRequest` 同步请求仍然可以执行。 201 | 202 | ## API 203 | 204 | #### init 205 | 206 | ```typescript 207 | export const init = (config: BuryConfig, router?: VueRouter) => Bury; 208 | 209 | // 预配置中的一些配置并没有默认值,可以通过 config 手动添加预设 210 | // 这些参数是 payload 中预定义的。 211 | // 你也可以自定义参数 212 | export interface BuryConfig { 213 | eventId?: string; 214 | timestamp?: string; 215 | ua?: string; 216 | browser?: "MSIE" | "Firefox" | "Safari" | "Chrome" | "Opera"; 217 | referrer?: string; 218 | width?: string; 219 | height?: string; 220 | ip?: string; 221 | cityName?: string; 222 | isPhone?: "phone" | "pc"; 223 | userId?: string; 224 | pageUrl?: string; 225 | pageStayTime?: string; 226 | apiUrl?: string; // 仅在 type === Api 中 227 | } 228 | ``` 229 | 230 | `BuryConfig` 中通过 `"@fingerprintjs/fingerprintjs"` 模块 以及 `"http://pv.sohu.com/cityjson?ie=utf-8"` `Api` 接口获取了一些预设值,它们分别是 231 | 232 | 1. `timestamp` - 时间戳 - `new Date().getTime()` 233 | 2. `ua` - 客户端信息(navigator.userAgent),详情请查看 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/navigator 234 | 3. `browser` - 浏览器类型 235 | 4. `referrer` - 引用来源 - http://www.ruanyifeng.com/blog/2019/06/http-referer.html 236 | 5. `width` - 窗口宽度 237 | 6. `height` - 窗口高度 238 | 7. `ip` - 客户端 ip 地址 239 | 8. `cityName` - 客户端省市名 - 如 “江苏省南京市” 240 | 9. `isPhone` - 是否是移动端,如果是,则值为 `phone` 241 | 10. `userId` - 客户端设备的唯一标识符 详情请查阅 https://github.com/fingerprintjs/fingerprintjs 242 | 243 | 例如说,你可以传入 `project`、`version` 和 `environment`。 244 | 245 | ```javascript 246 | const bury = init({ 247 | project: "projectName", 248 | version: "v1", 249 | environment: process.env.NODE_ENV, 250 | }); 251 | ``` 252 | 253 | ```javascript 254 | const bury = init(config, router); 255 | ``` 256 | 257 | - `config` 预定义参数 258 | - `router` 可选参数,如果要监听 `VueRouter` 跳转的话 259 | 260 | ### Bury.spy 261 | 262 | 开启监听模式(生产模式下请不要打开),可以在 `devtools` 的控制台中查看每次触发埋点事件的返回值 263 | 264 | ```javascript 265 | bury.spy(); 266 | ``` 267 | 268 | ### initUrlMap 269 | 270 | 初始化监听的 `url` 的页面路径及其 `eventId` 的数组。 271 | 272 | > 所有的 eventId 都会被包含在回调函数参数中的 payload 中 273 | 274 | ```typescript 275 | interface UrlMap: { 276 | path: string; // 页面url地址,同VueRouter中的path的定义方式 277 | enter?: string; // 进入该页面的 EventId 278 | leave?: string; // 离开该页面的 EventId 279 | }[] 280 | ``` 281 | 282 | - `url` 页面的 `url` 地址,定义遵循 https://github.com/pillarjs/path-to-regexp/tree/v1.7.0 ,通常可以和 `VueRouter` 中的 `path` 参数对照着填。 283 | - `enter` 进入该埋点页面的 `eventId` 284 | - `leave` 离开该埋点页面的 `eventId` 285 | 286 | > 关于 `path` https://github.com/pillarjs/path-to-regexp/tree/v1.7.0 287 | 288 | ### initApiMap 289 | 290 | ```typescript 291 | interface ApiMap: { 292 | url: string; // 接口的url地址 293 | method?: Method; // Method ,例如 `GET` `POST` ,如果不定义,则监听该url下的所有 Method 294 | eventId: string; // 接口被触发时的 EventId 295 | }[] = []; 296 | ``` 297 | 298 | - `url` 接口的 `url` 地址 299 | - `method` 可选参数 `Method` ,例如 `GET` `POST` ,如果未定义,则监听该 `url` 下的所有 `Method` 300 | - `eventId` 该埋点接口的 `eventId` 301 | 302 | ### track 303 | 304 | 对事件进行埋点监听 305 | 306 | ```typescript 307 | function track any>(fn: T, eventId: string): T; 308 | ``` 309 | 310 | - `fn` 被埋点的方法 311 | - `eventId` 该埋点方法的 `eventId` 312 | 313 | 为传入的方法埋点,并返回埋完点的方法。 314 | 315 | ### tracked 316 | 317 | 立即触发 `Action` 埋点事件 318 | 319 | ```typescript 320 | function tracked(eventId: string): void; 321 | ``` 322 | 323 | - `eventId` 该埋点事件 ID 324 | 325 | ### trackApi 326 | 327 | 对 `Axios` 进行埋点监听 328 | 329 | ```typescript 330 | function trackApi(axiosInstance: AxiosInstance): void; 331 | ``` 332 | 333 | ### onBury 334 | 335 | 当被埋点的时间触发时的回调函数 336 | 337 | ```typescript 338 | function onBury(callback: (value: BuryCallBackPayload) => void): void; 339 | ``` 340 | 341 | - `callback` 回调函数 342 | 343 | ### BuryCallBackPayload 344 | 345 | ```typescript 346 | interface BuryCallBackPayload { 347 | type: "Action" | "Click" | "Leave" | "Enter" | "Api"; 348 | payload: BuryConfig; 349 | extra?: 350 | | Payload.ActionPayload 351 | | Payload.ApiPayload 352 | | Payload.ClickPayload 353 | | Payload.LoadPayload 354 | | Payload.RoutePayload; 355 | } 356 | ``` 357 | 358 | - `type` 类型 分别是埋点事件、点击、离开页面、载入页面和接口 359 | - `payload` 负载,除了预定义的配置以外 360 | - `Enter` 361 | - `pageUrl` 进入的页面 `url` 362 | - `Leave` 363 | - `PageStayTime` 在当前页面停留的时间 364 | - `pageUrl` 离开的页面 `url` 365 | - `Api` 366 | - `apiUrl` 接口的 `url` 367 | - `extra` 监听事件的负载,详情请查看 https://github.com/darkXmo/monitor 368 | 369 | ## help 370 | 371 | ### 如何在 Nuxt2 项目中使用 372 | 373 | #### plugins 374 | 375 | 在 `plugins` 文件夹中创建文件 `bury.js` 或 `bury.ts` ; 376 | 377 | ```typescript 378 | // bury.js 379 | import { init, initUrlMap } from '@xmon/bury'; 380 | 381 | const bury = init({ 382 | version: 'projectVersion', 383 | dataPointVersion: 'v1', 384 | project: 'projectName' 385 | }); 386 | 387 | initUrlMap([{ 388 | path: "/", 389 | enter: "EnterEventPoint", 390 | leave: "LeaveEventPoint" 391 | }, ...]) 392 | 393 | bury.spy(); 394 | 395 | bury.onBury((value) => { 396 | // do something with value 397 | }) 398 | ``` 399 | 400 | 在 `nuxt.config.js` 或 `nuxt.config.ts` 中添加插件配置 401 | 402 | ```javascript 403 | { 404 | plugins: [ 405 | ... 406 | { src: "@/plugins/bury.ts", mode: 'client' }, 407 | ... 408 | ], 409 | } 410 | ``` 411 | 412 | ## TODO 413 | 414 | 🚀 已完成 415 | 416 | | 事项 | 状态 | 417 | | ---------------------------------------------------------------------------------- | ---- | 418 | | 添加 `tracked` 语法糖,当运行`tracked()`时,就会触发埋点事件,而不需要单独封装行为 | 🚀 | 419 | | 添加配置可选择关闭全局点击事件监听 | 📝 | 420 | | 添加配置可根据页面开启全局点击事件监听 | 📝 | 421 | | 如果未检测到需要监听的页面路由,则不开启路由监听 | 📝 | 422 | | 可开启监听页面 Web 指标,详情请参考 web-vitals | 📝 | 423 | | 可开启监听页面 Error 事件 | 📝 | 424 | | 可以配置项中的部分默认内容,以加速埋点实例的创建(例如 ip + cityName, userId) | 📝 | 425 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmon/bury", 3 | "version": "0.4.0", 4 | "description": "monitor most action by mitt, 一个不入侵你业务代码的埋点系统", 5 | "main": "dist/index.js", 6 | "repository": "git@github.com:darkXmo/bury.git", 7 | "author": "Xmo <18851989097@163.com>", 8 | "keywords": [ 9 | "event", 10 | "vue", 11 | "monitor", 12 | "aegis", 13 | "xmon", 14 | "listener", 15 | "bury" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "@fingerprintjs/fingerprintjs": "^3.3.0", 20 | "@xmon/monitor": "0.2.4", 21 | "mitt": "^3.0.0", 22 | "path-to-regexp": "^6.2.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/preset-env": "^7.15.6", 26 | "@babel/preset-typescript": "^7.15.0", 27 | "@commitlint/cli": "^13.1.0", 28 | "@commitlint/config-conventional": "^13.1.0", 29 | "cz-conventional-changelog": "^3.3.0", 30 | "git-cz": "^4.8.0", 31 | "husky": "^7.0.0", 32 | "lint-staged": "^11.1.2", 33 | "prettier": "2.4.1", 34 | "typescript": "^4.4.3" 35 | }, 36 | "lint-staged": { 37 | "src/**/*.vue": [ 38 | "prettier --write --ignore-unknown" 39 | ], 40 | "src/**/*.js": [ 41 | "prettier --write --ignore-unknown" 42 | ], 43 | "src/**/*.ts": [ 44 | "prettier --write --ignore-unknown" 45 | ], 46 | "*.{js,ts,css,md}": "prettier --write --ignore-unknown" 47 | }, 48 | "scripts": { 49 | "commit": "git add . && git status && git-cz", 50 | "prepare": "husky install", 51 | "build": "yarn tsc", 52 | "clean": "rm -rf dist pnpm* yarn.* node_modules package-*" 53 | }, 54 | "config": { 55 | "commitizen": { 56 | "path": "node_modules/cz-conventional-changelog" 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/bury.filter.ts: -------------------------------------------------------------------------------- 1 | import { Method } from "@xmon/monitor/dist/index.interface"; 2 | import { apiMap, urlMap } from "./map.config"; 3 | import { pathToRegexp } from "path-to-regexp"; 4 | 5 | const filters = { 6 | clickFilter: (ele: HTMLElement) => !!ele.dataset["bupoint"], 7 | /** 8 | * 判断路径是否在需要监听 9 | * @param path 要判断的路径 10 | * @returns 如果在监听的列表中,对应 path + enter + leave 对象,否则返回为undefined 11 | */ 12 | urlFilter: (path: string) => { 13 | return urlMap.find((item) => pathToRegexp(item.path).test(path)); 14 | }, 15 | /** 16 | * 判断api是否在需要监听 17 | * @param url 要判断的apiUrl 18 | * @param method 要判断的请求的Method,默认为 GET ,如果没有设置该api的Method限制,则该参数无意义 19 | * @returns 如果在监听的列表中,对应 url + eventId 对象,否则返回为undefined 20 | */ 21 | apiFilter: (url: string, method: Method = "GET") => { 22 | const ans = apiMap.find((item) => { 23 | if (item.method && method !== item.method) { 24 | return false; 25 | } 26 | return ( 27 | pathToRegexp(item.url).test(url) || 28 | pathToRegexp(item.url).test(url.split("?")[0]) 29 | ); 30 | }); 31 | return ans; 32 | }, 33 | }; 34 | 35 | export default filters; 36 | -------------------------------------------------------------------------------- /src/bury.ts: -------------------------------------------------------------------------------- 1 | import { Monitor } from "@xmon/monitor"; 2 | import initConfig, { BuryConfig } from "./config"; 3 | import { AxiosInstance } from "@xmon/monitor/dist/index.interface"; 4 | import filters from "./bury.filter"; 5 | import { BuryCallBackPayload } from "./index.interface"; 6 | import { buryEmit, buryEmitter } from "./common"; 7 | 8 | export default class Bury { 9 | on = (callback: (value: BuryCallBackPayload) => void) => 10 | buryEmitter.on("bury", callback); 11 | 12 | config: BuryConfig = {}; 13 | private monitor: Monitor; 14 | private isSpy: boolean = false; 15 | protected todo: ((config: BuryConfig) => void)[] = []; 16 | 17 | protected ready = false; 18 | 19 | constructor(monitor: Monitor, config: BuryConfig) { 20 | this.monitor = monitor; 21 | this.init(monitor, config); 22 | } 23 | 24 | private init(monitor: Monitor, defaultConfig: BuryConfig) { 25 | initConfig(defaultConfig).then((res) => { 26 | Object.assign(this.config, res); 27 | this.ready = true; 28 | const to = filters.urlFilter(window.location.pathname); 29 | if (to?.enter) { 30 | const eventId = to.enter; 31 | buryEmit("Enter", this.config, eventId); 32 | } 33 | this.todo.map((item) => item(res)); 34 | this.todo.length = 0; 35 | }); 36 | monitor.monitorPage(); 37 | monitor.monitorClick(filters.clickFilter); 38 | this.onAction(); 39 | this.onClick(); 40 | this.onApi(); 41 | this.onUnload(); 42 | return buryEmitter; 43 | } 44 | 45 | private onAction() { 46 | this.monitor.on("Action", (payload) => { 47 | const eventId = payload.eventId; 48 | if (!this.ready) { 49 | this.todo.push((config: BuryConfig) => { 50 | buryEmit("Action", config, eventId, payload); 51 | }); 52 | } else { 53 | buryEmit("Action", this.config, eventId, payload); 54 | } 55 | }); 56 | } 57 | 58 | private onClick() { 59 | this.monitor.on("Click", (payload) => { 60 | const eventId = payload.target.dataset["bupoint"] as string; 61 | if (!this.ready) { 62 | this.todo.push((config: BuryConfig) => { 63 | buryEmit("Click", config, eventId, payload); 64 | }); 65 | } else { 66 | buryEmit("Click", this.config, eventId, payload); 67 | } 68 | }); 69 | } 70 | 71 | private onApi() { 72 | this.monitor.on("Api", (payload) => { 73 | const api = filters.apiFilter(payload.url, payload.method); 74 | if (api) { 75 | const eventId = api.eventId; 76 | if (!this.ready) { 77 | this.todo.push((config: BuryConfig) => { 78 | buryEmit("Api", config, eventId, payload); 79 | }); 80 | } else { 81 | buryEmit("Api", this.config, eventId, payload); 82 | } 83 | } 84 | }); 85 | } 86 | 87 | private onUnload() { 88 | this.monitor.on("Unload", (payload) => { 89 | const from = filters.urlFilter(window.location.pathname); 90 | if (from?.leave) { 91 | const eventId = from.leave; 92 | if (!this.ready) { 93 | this.todo.push((config: BuryConfig) => { 94 | buryEmit("Leave", config, eventId, payload); 95 | }); 96 | } else { 97 | buryEmit("Leave", this.config, eventId, payload); 98 | } 99 | } 100 | }); 101 | } 102 | 103 | spy() { 104 | if (this.isSpy) return; 105 | buryEmitter.on("bury", (payload) => { 106 | console.log( 107 | "%c" + payload.type, 108 | "color: blue; background: #bfcf5f;", 109 | "|payload =>", 110 | payload.payload, 111 | "|extra =>", 112 | payload.extra, 113 | "|" 114 | ); 115 | }); 116 | this.isSpy = true; 117 | } 118 | 119 | /** 120 | * 当运行tracked的时候,会触发一次埋点事件onBury 121 | * @param eventId 事件ID 122 | */ 123 | tracked(eventId: string) { 124 | return this.monitor.emit({ eventId }); 125 | } 126 | 127 | track any>(fn: T, eventId: string): T { 128 | return this.monitor.monitorEvent(fn, { 129 | eventId, 130 | }) as T; 131 | } 132 | 133 | trackApi(axiosInstance: AxiosInstance) { 134 | this.monitor.monitorAxios(axiosInstance); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/bury.vue.ts: -------------------------------------------------------------------------------- 1 | import { MonitorVue } from "@xmon/monitor"; 2 | import Bury from "./bury"; 3 | import { BuryConfig } from "./config"; 4 | import filters from "./bury.filter"; 5 | import { buryEmit } from "./common"; 6 | 7 | export default class BuryVue extends Bury { 8 | constructor(monitor: MonitorVue, config: BuryConfig) { 9 | super(monitor, config); 10 | this.initBuriedVue(monitor); 11 | } 12 | 13 | private initBuriedVue(monitor: MonitorVue) { 14 | monitor.monitorRouter(); 15 | monitor.on("Route", (payload) => { 16 | const from = filters.urlFilter(payload.from.path); 17 | const to = filters.urlFilter(payload.to.path); 18 | if (from?.leave) { 19 | const eventId = from.leave; 20 | if (!this.ready) { 21 | this.todo.push((config: BuryConfig) => { 22 | buryEmit("Leave", config, eventId, payload); 23 | }); 24 | } else { 25 | buryEmit("Leave", this.config, eventId, payload); 26 | } 27 | } 28 | if (to?.enter) { 29 | const eventId = to.enter; 30 | if (!this.ready) { 31 | this.todo.push((config: BuryConfig) => { 32 | buryEmit("Enter", config, eventId, payload); 33 | }); 34 | } else { 35 | buryEmit("Enter", this.config, eventId, payload); 36 | } 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import { Payload as monitorPayload } from "@xmon/monitor/dist/index.interface"; 2 | import mitt from "mitt"; 3 | import { BuryConfig } from "./config"; 4 | import { BuryCallBackPayload } from "./index.interface"; 5 | 6 | type Payload = 7 | | monitorPayload.ActionPayload 8 | | monitorPayload.ApiPayload 9 | | monitorPayload.ClickPayload 10 | | monitorPayload.LoadPayload 11 | | monitorPayload.RoutePayload; 12 | 13 | export const initBuryCallbackPayload = ( 14 | type: "Action" | "Click" | "Leave" | "Enter" | "Api", 15 | config: BuryConfig, 16 | eventId: string, 17 | payload?: Payload 18 | ): BuryCallBackPayload => { 19 | const defaultValue = { 20 | type, 21 | payload: { 22 | ...config, 23 | eventId, 24 | pageUrl: window.location.pathname, 25 | timestamp: 26 | payload?.time.getTime().toString() ?? new Date().getTime().toString(), 27 | }, 28 | extra: payload, 29 | }; 30 | if (type === "Action") { 31 | return defaultValue; 32 | } else if (type === "Click") { 33 | return defaultValue; 34 | } else if (type === "Api") { 35 | const p = payload as monitorPayload.ApiPayload; 36 | defaultValue.payload.apiUrl = p.url; 37 | return defaultValue; 38 | } else if (type === "Enter") { 39 | return defaultValue; 40 | } else if (type === "Leave") { 41 | const p = payload as 42 | | monitorPayload.RoutePayload 43 | | monitorPayload.LoadPayload; 44 | defaultValue.payload.pageStayTime = p.duration.toString(); 45 | return defaultValue; 46 | } 47 | }; 48 | 49 | export const buryEmitter = mitt<{ 50 | bury: BuryCallBackPayload; 51 | }>(); 52 | 53 | export const buryEmit = ( 54 | type: "Action" | "Click" | "Leave" | "Enter" | "Api", 55 | config: BuryConfig, 56 | eventId: string, 57 | payload?: Payload 58 | ) => { 59 | buryEmitter.emit( 60 | "bury", 61 | initBuryCallbackPayload(type, config, eventId, payload) 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import FingerprintJS from "@fingerprintjs/fingerprintjs"; 2 | 3 | export interface BuryConfig { 4 | eventId?: string; 5 | timestamp?: string; 6 | ua?: string; 7 | browser?: "MSIE" | "Firefox" | "Safari" | "Chrome" | "Opera"; 8 | referrer?: string; 9 | width?: string; 10 | height?: string; 11 | isPhone?: "phone" | "pc"; 12 | userId?: string; 13 | apiUrl?: string; 14 | pageUrl?: string; 15 | pageStayTime?: string; 16 | [K: string]: string; 17 | } 18 | 19 | let config: BuryConfig = {}; 20 | 21 | async function initBuryConfig(loadConfig: BuryConfig) { 22 | const fpPromise = FingerprintJS.load(); 23 | 24 | const fp = await fpPromise; 25 | const result = await fp.get(); 26 | const userId = result.visitorId; 27 | config.userId = userId; 28 | config.ua = navigator.userAgent; 29 | config.referrer = document.referrer; 30 | config.width = document.documentElement.clientWidth.toString(); 31 | config.height = document.documentElement.clientHeight.toString(); 32 | config.browser = (() => { 33 | let aKeys: ("MSIE" | "Firefox" | "Safari" | "Chrome" | "Opera")[] = [ 34 | "MSIE", 35 | "Firefox", 36 | "Safari", 37 | "Chrome", 38 | "Opera", 39 | ], 40 | sUsrAg = navigator.userAgent, 41 | nIdx = aKeys.length - 1; 42 | for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx--); 43 | return aKeys[nIdx]; 44 | })(); 45 | if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) { 46 | config.isPhone = "phone"; 47 | } else { 48 | config.isPhone = "pc"; 49 | } 50 | Object.assign(config, loadConfig); 51 | return config; 52 | } 53 | 54 | export default initBuryConfig; 55 | -------------------------------------------------------------------------------- /src/index.interface.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from "@xmon/monitor/dist/index.interface"; 2 | import { BuryConfig } from "./config"; 3 | 4 | export interface BuryCallBackPayload { 5 | type: "Action" | "Click" | "Leave" | "Enter" | "Api"; 6 | payload: BuryConfig; 7 | extra?: 8 | | Payload.ActionPayload 9 | | Payload.ApiPayload 10 | | Payload.ClickPayload 11 | | Payload.LoadPayload 12 | | Payload.RoutePayload; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | initMonitor, 3 | initMonitorVue, 4 | Monitor, 5 | MonitorVue, 6 | } from "@xmon/monitor"; 7 | import { AxiosInstance } from "@xmon/monitor/dist/index.interface"; 8 | import Bury from "./bury"; 9 | import BuryVue from "./bury.vue"; 10 | import { BuryConfig } from "./config"; 11 | import { BuryCallBackPayload } from "./index.interface"; 12 | export { initApiMap, initUrlMap } from "./map.config"; 13 | 14 | const ex: { instance: Bury | null } = { 15 | instance: null, 16 | }; 17 | 18 | export const init = ( 19 | config: BuryConfig, 20 | router?: { 21 | beforeEach: (guard: any) => () => void; 22 | push: any; 23 | afterEach: (guard: any) => () => void; 24 | [K: string]: any; 25 | } 26 | ) => { 27 | if (router) { 28 | const monitor: MonitorVue = initMonitorVue(router); 29 | return (ex.instance = new BuryVue(monitor, config)); 30 | } 31 | const monitor: Monitor = initMonitor(); 32 | return (ex.instance = new Bury(monitor, config)); 33 | }; 34 | 35 | export function track any>(fn: T, eventId: string): T { 36 | if (ex.instance) { 37 | return ex.instance.track(fn, eventId); 38 | } else { 39 | throw new Error("Monitor should be init first | 你可能没有初始化Bury实例"); 40 | } 41 | } 42 | 43 | export const trackApi = (axiosInstance: AxiosInstance) => { 44 | if (ex.instance) { 45 | return ex.instance.trackApi(axiosInstance); 46 | } else { 47 | throw new Error("Monitor should be init first | 你可能没有初始化Bury实例"); 48 | } 49 | }; 50 | 51 | /** 52 | * 当运行tracked的时候,会触发一次埋点事件onBury 53 | * @param eventId 事件ID 54 | */ 55 | export const tracked = (eventId: string) => { 56 | if (ex.instance) { 57 | return ex.instance.tracked(eventId); 58 | } else { 59 | throw new Error("Monitor should be init first | 你可能没有初始化Bury实例"); 60 | } 61 | }; 62 | 63 | export const onBury = (callback: (value: BuryCallBackPayload) => void) => { 64 | if (ex.instance) { 65 | return ex.instance.on(callback); 66 | } else { 67 | throw new Error("Monitor should be init first | 你可能没有初始化Bury实例"); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/map.config.ts: -------------------------------------------------------------------------------- 1 | import { Method } from "@xmon/monitor/dist/index.interface"; 2 | 3 | export const urlMap: { 4 | path: string; 5 | enter?: string; 6 | leave?: string; 7 | }[] = []; 8 | 9 | export function initUrlMap( 10 | map: { 11 | path: string; 12 | enter?: string; 13 | leave?: string; 14 | }[] 15 | ) { 16 | urlMap.push(...map); 17 | } 18 | 19 | export const apiMap: { 20 | url: string; 21 | eventId: string; 22 | method?: Method; 23 | }[] = []; 24 | 25 | export function initApiMap( 26 | map: { 27 | url: string; 28 | eventId: string; 29 | method?: Method; 30 | }[] 31 | ) { 32 | apiMap.push(...map); 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "test/**/*"], 3 | "compilerOptions": { 4 | // "target": "es2015", 5 | // "module": "es2015", 6 | "lib": ["es2015", "dom"], 7 | "module": "es6", 8 | "target": "es5", 9 | "outDir": "dist", 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | "declaration": true, 14 | "allowJs": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------