├── examples
├── vue2
│ ├── .gitignore
│ ├── src
│ │ ├── components
│ │ │ ├── index.js
│ │ │ ├── MenuList.vue
│ │ │ └── CTable.vue
│ │ ├── router
│ │ │ ├── index.js
│ │ │ └── router.dynamic.js
│ │ ├── assets
│ │ │ ├── vue.svg
│ │ │ └── global.scss
│ │ ├── views
│ │ │ ├── home
│ │ │ │ └── index.vue
│ │ │ ├── pv
│ │ │ │ └── index.vue
│ │ │ └── event
│ │ │ │ └── index.vue
│ │ └── App.vue
│ ├── index.html
│ ├── README.md
│ ├── .eslintrc.cjs
│ ├── package.json
│ ├── vite.config.ts
│ ├── public
│ │ └── vite.svg
│ └── server.js
├── vue3
│ ├── .gitignore
│ ├── tsconfig.node.json
│ ├── index.html
│ ├── src
│ │ ├── router
│ │ │ ├── index.ts
│ │ │ └── router.dynamic.ts
│ │ ├── components
│ │ │ ├── index.ts
│ │ │ ├── MenuList.vue
│ │ │ └── CTable.vue
│ │ ├── views
│ │ │ ├── home
│ │ │ │ └── index.vue
│ │ │ ├── pv
│ │ │ │ └── index.vue
│ │ │ └── event
│ │ │ │ └── index.vue
│ │ ├── assets
│ │ │ └── global.scss
│ │ ├── main.ts
│ │ ├── App.vue
│ │ └── utils
│ │ │ └── sourcemap.ts
│ ├── README.md
│ ├── tsconfig.json
│ ├── .eslintrc.cjs
│ ├── package.json
│ ├── vite.config.ts
│ ├── public
│ │ └── vite.svg
│ └── server.js
└── vanilla
│ ├── .gitignore
│ ├── .DS_Store
│ ├── vite.config.ts
│ ├── package.json
│ ├── index.ts
│ ├── README.md
│ ├── assets
│ ├── javascript.svg
│ ├── vite.svg
│ └── style.css
│ └── main.ts
├── pnpm-workspace.yaml
├── .gitignore
├── docs
├── .vitepress
│ ├── cache
│ │ └── deps
│ │ │ ├── package.json
│ │ │ ├── vue.js.map
│ │ │ └── _metadata.json
│ └── config.ts
├── vite.config.ts
├── package.json
├── guide
│ ├── functions
│ │ ├── other.md
│ │ ├── intersection.md
│ │ ├── http.md
│ │ └── pv.md
│ ├── use
│ │ ├── run.md
│ │ ├── demo.md
│ │ └── declare.md
│ ├── spotlight.md
│ ├── idea.md
│ ├── starting.md
│ └── plan.md
└── index.md
├── packages
├── core
│ ├── __test__
│ │ ├── css
│ │ │ └── performance.css
│ │ ├── js
│ │ │ └── performance.js
│ │ ├── img
│ │ │ └── performance.png
│ │ ├── utils
│ │ │ ├── pollify.ts
│ │ │ └── index.ts
│ │ ├── utils.spec.ts
│ │ ├── html
│ │ │ ├── recordscreen.html
│ │ │ └── performance.html
│ │ ├── err-batch.spec.ts
│ │ ├── recordscreen.spec.ts
│ │ ├── event.spec.ts
│ │ ├── err.spec.ts
│ │ └── performance.spec.ts
│ ├── src
│ │ ├── common
│ │ │ ├── index.ts
│ │ │ ├── config.ts
│ │ │ └── constant.ts
│ │ ├── observer
│ │ │ ├── config.ts
│ │ │ ├── types.ts
│ │ │ ├── watch.ts
│ │ │ ├── dep.ts
│ │ │ ├── computed.ts
│ │ │ ├── index.ts
│ │ │ ├── ref.ts
│ │ │ └── watcher.ts
│ │ ├── utils
│ │ │ ├── debug.ts
│ │ │ ├── session.ts
│ │ │ ├── global.ts
│ │ │ ├── element.ts
│ │ │ ├── localStorage.ts
│ │ │ └── is.ts
│ │ ├── lib
│ │ │ ├── line-status.ts
│ │ │ ├── event-dwell.ts
│ │ │ ├── eventBus.ts
│ │ │ ├── base.ts
│ │ │ └── err-batch.ts
│ │ └── types
│ │ │ └── index.ts
│ ├── package.json
│ └── index.ts
├── vue3
│ ├── index.ts
│ ├── package.json
│ └── README.md
└── vue2
│ ├── index.ts
│ ├── package.json
│ └── README.md
├── rollup.config.js
├── global.d.ts
├── .npmrc
├── scripts
├── update.ts
├── examples
│ ├── js.sh
│ ├── vue2.sh
│ └── vue3.sh
├── test-install.sh
├── publish-docs.sh
├── publish.ts
├── fix-types.ts
├── publish-examples.ts
├── rollup.config.ts
└── build.ts
├── vitest.config.mts
├── .eslintignore
├── .vscode
└── settings.json
├── .eslintrc.cjs
├── LICENSE
├── tsconfig.json
├── meta
└── packages.ts
├── package.json
├── README.md
└── CHANGELOG.md
/examples/vue2/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
--------------------------------------------------------------------------------
/examples/vue3/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 | - examples/*
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
5 | coverage
6 |
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/vanilla/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
4 | examples-copy
--------------------------------------------------------------------------------
/packages/core/__test__/css/performance.css:
--------------------------------------------------------------------------------
1 | #web-tracing-test-id {
2 | color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/core/src/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config'
2 | export * from './constant'
3 |
--------------------------------------------------------------------------------
/packages/core/src/observer/config.ts:
--------------------------------------------------------------------------------
1 | export const OBSERVERSIGNBOARD = '__webtracingobserver__'
2 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | require('esbuild-register')
2 | module.exports = require('./scripts/rollup.config.ts')
3 |
--------------------------------------------------------------------------------
/examples/vanilla/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/M-cheng-web/web-tracing/HEAD/examples/vanilla/.DS_Store
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | __webTracing__: WebTracing
3 | __webTracingInit__: boolean
4 | }
5 |
--------------------------------------------------------------------------------
/packages/core/__test__/js/performance.js:
--------------------------------------------------------------------------------
1 | ;(function WebTracingTest() {
2 | window.WebTracingTestVar = true
3 | })()
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 | side-effects-cache=false
4 | ignore-workspace-root-check=true
--------------------------------------------------------------------------------
/packages/core/__test__/img/performance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/M-cheng-web/web-tracing/HEAD/packages/core/__test__/img/performance.png
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vue.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": [],
4 | "sourcesContent": [],
5 | "mappings": "",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/scripts/update.ts:
--------------------------------------------------------------------------------
1 | import { updatePackageJSON } from './utils'
2 |
3 | async function run() {
4 | await Promise.all([updatePackageJSON()])
5 | }
6 |
7 | run()
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 -
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 -
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/vue2/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | example-vue2
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/vue3/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | example-vue3
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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/__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 |
--------------------------------------------------------------------------------
/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/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/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/__test__/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { getNodeXPath } from '../src/utils/element'
2 |
3 | describe('utils', () => {
4 | it('getNodeXPath should work', () => {
5 | const element = document.createElement('div')
6 | element.innerHTML = `
7 |
14 | `
15 | const target = element.querySelector('.target')!
16 | expect(getNodeXPath(target)).toBe('#wrapper>div>div')
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/packages/core/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/vue2/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
web-tracing 监控插件
4 |
5 | 基于 JS 跨平台插件,为前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
6 |
7 |
8 |
9 | ## web-tracing-examples-vue2
10 | [web-tracing](https://github.com/M-cheng-web/web-tracing)的示例项目(vue2版本)
11 |
12 | 此项目由 [web-tracing -> examples 目录](https://github.com/M-cheng-web/web-tracing/tree/main/examples) 通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 [web-tracing](https://github.com/M-cheng-web/web-tracing) 项目中完成的
13 |
14 | 因为是直接强推的代码,所以本地更新时需要执行:
15 | ```
16 | git fetch --all
17 | git reset --hard origin/main
18 | ```
19 |
20 | ## 运行
21 | ```
22 | pnpm install
23 | pnpm run start
24 | ```
--------------------------------------------------------------------------------
/examples/vanilla/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
web-tracing 监控插件
4 |
5 | 基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
6 |
7 |
8 |
9 | ## web-tracing-examples-js
10 | [web-tracing](https://github.com/M-cheng-web/web-tracing)的示例项目(js版本)
11 |
12 | 此项目由 [web-tracing -> examples 目录](https://github.com/M-cheng-web/web-tracing/tree/main/examples) 通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 [web-tracing](https://github.com/M-cheng-web/web-tracing) 项目中完成的
13 |
14 | 因为是直接强推的代码,所以本地更新时需要执行:
15 | ```
16 | git fetch --all
17 | git reset --hard origin/main
18 | ```
19 |
20 | ## 运行
21 | ```
22 | pnpm install
23 | pnpm run start
24 | ```
--------------------------------------------------------------------------------
/examples/vue3/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
web-tracing 监控插件
4 |
5 | 基于 JS 跨平台插件,为前端项目提供【 埋点、行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
6 |
7 |
8 |
9 | ## web-tracing-examples-vue3
10 | [web-tracing](https://github.com/M-cheng-web/web-tracing)的示例项目(vue3版本)
11 |
12 | 此项目由 [web-tracing -> examples -> vue3](https://github.com/M-cheng-web/web-tracing/tree/main/examples/vue3) 通过脚本直接覆盖迁移过来的,目的是为了拟真测试,本地联调还是在 [web-tracing](https://github.com/M-cheng-web/web-tracing) 项目中完成的
13 |
14 | 因为是直接强推的代码,所以本地更新时需要执行:
15 | ```
16 | git fetch --all
17 | git reset --hard origin/main
18 | ```
19 |
20 | ## 运行
21 | ```
22 | pnpm install
23 | pnpm run start
24 | ```
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/.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/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 所有的事件类型:
5 |
{{ `${key}: ${value}` }}
6 |
7 |
8 | 所有的事件ID(还有一些id是随机字符串的):
9 |
{{ `${key}: ${value}` }}
10 |
11 |
12 |
13 |
14 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/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/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 | ---
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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`
--------------------------------------------------------------------------------
/examples/vanilla/assets/javascript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 目前没有上线,后面会加;目前只能将就在本地运行啦
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/packages/vue2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-tracing/vue2",
3 | "version": "2.1.0",
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/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-tracing/vue3",
3 | "version": "2.1.0",
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-tracing/core",
3 | "version": "2.1.0",
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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/examples/vue2/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vue3/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vanilla/assets/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/vue2/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 所有的事件类型:
5 |
{{ `${key}: ${value}` }}
6 |
7 |
8 | 所有的事件ID(还有一些id是随机字符串的):
9 |
{{ `${key}: ${value}` }}
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/packages/core/__test__/event.spec.ts:
--------------------------------------------------------------------------------
1 | import { init } from '../index'
2 | import { _support, _global } from '../src/utils/global'
3 | import { JSDOM } from 'jsdom'
4 |
5 | describe('err', () => {
6 | beforeAll(() => {
7 | const dom = new JSDOM(`
8 |
9 |
10 |
11 |
12 |
13 | Test
14 |
15 |
16 |
24 |
25 |
26 | `)
27 | // @ts-expect-error: expected
28 | _global.document = dom.window.document
29 | init({
30 | dsn: 'http://unit-test.com',
31 | appName: 'unit-test',
32 | event: true,
33 | recordScreen: false
34 | })
35 | })
36 |
37 | function proxyEmit() {
38 | const testResult = { info: null, spy: vi.fn() }
39 | _support.sendData.emit = (e: any) => {
40 | testResult.spy()
41 | testResult.info = e
42 | }
43 | return testResult
44 | }
45 |
46 | it('event should be captured correctly', async () => {
47 | const testResult = proxyEmit()
48 |
49 | const button = document.getElementById('btn')!
50 | button.click()
51 | expect(testResult.spy).toHaveBeenCalledTimes(1)
52 | expect(testResult.info).toMatchObject({
53 | eventId: 'test-event-id',
54 | eventType: 'click',
55 | title: 'test-title',
56 | elementPath: 'div',
57 | params: {
58 | id: 'test-warden-id'
59 | }
60 | })
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/examples/vue3/src/components/MenuList.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
40 |
45 |
46 |
60 |
61 |
67 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/examples/vue2/src/components/MenuList.vue:
--------------------------------------------------------------------------------
1 |
2 |
40 |
41 |
42 |
69 |
70 |
76 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | ```
--------------------------------------------------------------------------------
/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/index.ts:
--------------------------------------------------------------------------------
1 | import type { InitOptions } from './src/types'
2 | import { initReplace, destroyReplace } from './src/lib/replace'
3 | import { initOptions, options as _options } from './src/lib/options'
4 | import { initBase } from './src/lib/base'
5 | import { initSendData, sendData } from './src/lib/sendData'
6 | import { initLineStatus } from './src/lib/line-status'
7 | import { initError, parseError, destroyError } from './src/lib/err'
8 | import { initEvent, destroyEvent } from './src/lib/event'
9 | import { initHttp, destroyHttp } from './src/lib/http'
10 | import { initPerformance, destroyPerformance } from './src/lib/performance'
11 | import { initPv, destroyPv } from './src/lib/pv'
12 | import {
13 | initIntersection,
14 | destroyIntersection
15 | } from './src/lib/intersectionObserver'
16 | import { _global } from './src/utils/global'
17 | import { SENDID } from './src/common'
18 | import { logError } from './src/utils/debug'
19 | import { initRecordScreen, destroyRecordScreen } from './src/lib/recordscreen'
20 | import * as exportMethods from './src/lib/exportMethods'
21 | import './src/observer/index'
22 |
23 | function init(options: InitOptions): void {
24 | if (_global.__webTracingInit__) return
25 | if (!initOptions(options)) return
26 |
27 | // 注册全局
28 | initReplace()
29 | initBase()
30 | initSendData()
31 | initLineStatus()
32 |
33 | // 注册各个业务
34 | initError()
35 | initEvent()
36 | initHttp()
37 | initPerformance()
38 | initPv()
39 | initIntersection()
40 |
41 | if (_options.value.recordScreen) initRecordScreen()
42 |
43 | _global.__webTracingInit__ = true
44 | }
45 |
46 | /**
47 | * 销毁SDK添加的事件监听器,不会影响用户手动添加的监听器
48 | */
49 | function destroyTracing(): void {
50 | destroyEvent()
51 | destroyError()
52 | destroyHttp()
53 | destroyPerformance()
54 | destroyIntersection()
55 | destroyRecordScreen()
56 | destroyPv()
57 | destroyReplace()
58 | if (sendData) sendData.destroy()
59 |
60 | // 重置全局状态,确保重新初始化时能正常工作
61 | _global.__webTracingInit__ = false
62 | }
63 |
64 | export {
65 | init,
66 | destroyTracing,
67 | InitOptions,
68 | logError,
69 | parseError,
70 | SENDID,
71 | exportMethods,
72 | _options as options
73 | }
74 | export * from './src/lib/exportMethods'
75 | export default { init, destroyTracing, ...exportMethods, options: _options }
76 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | * @param types 事件类型数组
87 | */
88 | removeEvents(types: EVENTTYPES[]) {
89 | types.forEach(type => {
90 | delete this.handlers[type]
91 | })
92 | }
93 | }
94 |
95 | const eventBus = _support.eventBus || (_support.eventBus = new EventBus())
96 |
97 | export { eventBus }
98 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/vue3/src/views/pv/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 | navigate - 网页通过点击链接,地址栏输入,表单提交,脚本操作等方式加载
11 |
12 | reload - 网页通过“重新加载”按钮或者location.reload()方法加载
13 | back_forward - 网页通过“前进”或“后退”按钮加载
14 | reserved - 任何其他来源的加载
15 |
16 |
17 |
18 | 获取最新采集数据
19 |
20 |
25 |
26 | {{ `${scope.index + 1}` }}
27 |
28 |
29 | {{ `${formatDate(scope.row.sendTime)}` }}
30 |
31 |
32 | {{ `${formatDate(scope.row.triggerTime)}` }}
33 |
34 |
35 |
36 |
37 |
38 |
93 |
94 |
98 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/vue2/src/views/pv/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | navigate - 网页通过点击链接,地址栏输入,表单提交,脚本操作等方式加载
12 |
13 | reload - 网页通过“重新加载”按钮或者location.reload()方法加载
14 | back_forward - 网页通过“前进”或“后退”按钮加载
15 | reserved - 任何其他来源的加载
16 |
17 |
18 |
19 |
20 | 获取最新采集数据
21 |
22 |
27 |
28 | {{ `${scope.index + 1}` }}
29 |
30 |
31 | {{ `${formatDate(scope.row.sendTime)}` }}
32 |
33 |
34 | {{ `${formatDate(scope.row.triggerTime)}` }}
35 |
36 |
37 |
38 |
39 |
40 |
94 |
95 |
99 |
--------------------------------------------------------------------------------
/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/vue2/README.md:
--------------------------------------------------------------------------------
1 |
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 |
22 |
23 | ### 错误监听
24 |
25 |
26 | ### 资源监听
27 |
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/README.md:
--------------------------------------------------------------------------------
1 |
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 |
22 |
23 | ### 错误监听
24 |
25 |
26 | ### 资源监听
27 |
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)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-tracing/monorepo",
3 | "version": "2.1.0",
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |