├── example ├── mock │ └── .gitkeep ├── src │ ├── app.ts │ ├── @types │ │ └── index.d.ts │ ├── lib │ │ └── helper.ts │ └── pages │ │ └── index │ │ ├── index.less │ │ └── index.tsx ├── .prettierignore ├── README.md ├── .prettierrc ├── typings.d.ts ├── .editorconfig ├── .gitignore ├── .umirc.ts ├── tsconfig.json └── package.json ├── .commitlintrc.js ├── __test__ └── index.ts ├── .babelrc.json ├── src ├── @types │ └── index.d.ts ├── lib │ ├── store │ │ ├── logs.ts │ │ ├── buffer.ts │ │ └── objects.ts │ ├── heartbeat.ts │ ├── static │ │ ├── const.ts │ │ ├── types.ts │ │ └── helper.ts │ ├── config.ts │ └── core.ts └── index.ts ├── .eslintignore ├── .npmignore ├── jest.config.js ├── .prettierrc.js ├── README.md ├── .eslintrc.js ├── tsconfig.json ├── rollup.config.js ├── .gitignore ├── package.json └── LICENSE /example/mock/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/app.ts: -------------------------------------------------------------------------------- 1 | export const getInitialState = async () => {}; 2 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /example/src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | bridge: any; 3 | NE_DAWN: any; 4 | } 5 | -------------------------------------------------------------------------------- /__test__/index.ts: -------------------------------------------------------------------------------- 1 | describe('EventTracing.init', () => { 2 | it('EventTracing.init', () => {}); 3 | }); 4 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /example/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | document: any; 3 | requestIdleCallback: any; 4 | Element: any; 5 | [key: string]: any; 6 | } 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # d.ts files 5 | src/**/*.d.ts 6 | src/**/*.*.d.ts 7 | 8 | # config files 9 | *.js 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | node_modules/ 3 | .idea 4 | yarn-error.log 5 | .vscode 6 | .DS_Store 7 | coverage 8 | convertExcel 9 | convertToExcel 10 | __tests__ 11 | others 12 | src 13 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # umi project 2 | 3 | ## Getting Started 4 | 5 | Install dependencies, 6 | 7 | ```bash 8 | $ yarn 9 | ``` 10 | 11 | Start the dev server, 12 | 13 | ```bash 14 | $ yarn start 15 | ``` 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: ['/__test__/*'], 3 | collectCoverage: true, 4 | collectCoverageFrom: ['/src/*.ts'], 5 | coveragePathIgnorePatterns: ['/src/*.d.ts'], 6 | } 7 | -------------------------------------------------------------------------------- /example/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [{ 6 | "files": ".prettierrc", 7 | "options": { 8 | "parser": "json" 9 | } 10 | }] 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 4, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | arrowParens: 'always', 11 | } 12 | -------------------------------------------------------------------------------- /example/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.png'; 4 | declare module '*.svg' { 5 | export function ReactComponent( 6 | props: React.SVGProps, 7 | ): React.ReactElement; 8 | const url: string; 9 | export default url; 10 | } 11 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | 16 | # umi 17 | /src/.umi 18 | /src/.umi-production 19 | /src/.umi-test 20 | /.env.local 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @eventtracing/web 2 | 3 | 曙光前端 SDK,适用于前端所有基于 DOM 渲染的技术栈,比如:React、Vue、Regular 等。 4 | 5 | ## 使用及项目背景 6 | 7 | 请查看 [曙光主页](https://eventtracing.github.io) 8 | 9 | ## 本地启动项目 10 | 11 | ### SDK 构建 12 | 13 | 在根目录下执行 `yarn install` 或 `npm install`,执行成功后继续执行 `yarn start` 或 `npm run start`。 14 | 15 | ### 启动 Example 查看效果 16 | 17 | 在 example 目录下执行 `yarn install` 或 `npm install`,执行成功后继续执行 `yarn start` 或 `npm run start`。 -------------------------------------------------------------------------------- /example/src/lib/helper.ts: -------------------------------------------------------------------------------- 1 | export const isObject = (data: any) => Object.prototype.toString.call(data) === '[object Object]'; 2 | export const isArray = (data: any) => Object.prototype.toString.call(data) === '[object Array]'; 3 | export const isString = (data: any) => typeof data === 'string'; 4 | export const isNumber = (data: any) => typeof data === 'number'; 5 | export const isBoolean = (data: any) => typeof data === 'boolean'; 6 | export const isFunction = (data: any) => typeof data === 'function'; 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 6 | parserOptions: { 7 | sourceType: 'module', 8 | }, 9 | // 自定义 10 | rules: { 11 | '@typescript-eslint/explicit-module-boundary-types': 0, 12 | '@typescript-eslint/no-explicit-any': 0, 13 | '@typescript-eslint/no-inferrable-types': 0, 14 | '@typescript-eslint/no-empty-function': 0, 15 | '@typescript-eslint/no-this-alias': 0, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /example/.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | 3 | export default defineConfig({ 4 | nodeModulesTransform: { 5 | type: 'none', 6 | }, 7 | publicPath: '/', 8 | routes: [ 9 | { 10 | path: '/', 11 | component: '@/pages/index', 12 | }, 13 | ], 14 | theme: { 15 | '@primary-color': '#FE5D64', 16 | }, 17 | alias: { 18 | '@sdk': '/eventtracing', 19 | }, 20 | extraPostCSSPlugins: [], 21 | metas: [ 22 | { 23 | name: 'format-detection', 24 | content: 'telephone=no', 25 | }, 26 | ], 27 | fastRefresh: {}, 28 | }); 29 | -------------------------------------------------------------------------------- /src/lib/store/logs.ts: -------------------------------------------------------------------------------- 1 | import { ILog } from '../static/types'; 2 | import { isFunction } from '../static/helper'; 3 | 4 | const logs: Array = []; 5 | 6 | /** 7 | * 入队一条日志 8 | * @param log 9 | * @param callback 10 | */ 11 | export const pushLog = (log: ILog, callback?: any): void => { 12 | logs.push(log); 13 | 14 | if (isFunction(callback)) callback(log); 15 | }; 16 | 17 | /** 18 | * 出队指定数量日志 19 | * @param len 20 | */ 21 | export const shiftLog = (len: number): Array => { 22 | if (typeof len === 'number' && len > 0) return logs.splice(0, len); 23 | 24 | return []; 25 | }; 26 | 27 | /** 28 | * 获取日志队列长度 29 | */ 30 | export const getLogsLength = (): number => logs.length || 0; 31 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "importHelpers": true, 8 | "jsx": "react-jsx", 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "baseUrl": "./", 12 | "strict": true, 13 | "paths": { 14 | "@sdk/*": ["eventtracing/*"], 15 | "@/*": ["src/*"], 16 | "@@/*": ["src/.umi/*"] 17 | }, 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "include": [ 21 | "mock/**/*", 22 | "src/**/*", 23 | "config/**/*", 24 | ".umirc.ts", 25 | "typings.d.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "lib", 30 | "es", 31 | "dist", 32 | "typings", 33 | "**/__test__", 34 | "test", 35 | "docs", 36 | "tests" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/heartbeat.ts: -------------------------------------------------------------------------------- 1 | import { postponeCallback } from './static/helper'; 2 | import { triggerHeartbeatLogger } from './core'; 3 | import { getConfig } from './config'; 4 | 5 | const BEGIN_INTERVAL = 2000; 6 | const BEGIN_COUNT = 5; 7 | let heartbeatStatus: boolean = false; 8 | let count = 0; 9 | 10 | const heartbeat = (isFirst: boolean = false) => { 11 | const { heartbeatInterval } = getConfig(); 12 | 13 | if (!heartbeatStatus) return; 14 | 15 | if (!isFirst) { 16 | triggerHeartbeatLogger(); 17 | } 18 | 19 | count++; 20 | postponeCallback(() => { 21 | heartbeat(); 22 | }, { isIdle: false, timeout: count > BEGIN_COUNT ? heartbeatInterval : BEGIN_INTERVAL }); 23 | }; 24 | 25 | export const startHeartbeat = () => { 26 | const { isUseHeartbeat } = getConfig(); 27 | 28 | if (!isUseHeartbeat) return; 29 | if (heartbeatStatus) return; 30 | 31 | heartbeatStatus = true; 32 | heartbeat(true); 33 | }; 34 | 35 | export const pauseHeartbeat = () => { 36 | heartbeatStatus = false; 37 | triggerHeartbeatLogger(); 38 | }; -------------------------------------------------------------------------------- /src/lib/store/buffer.ts: -------------------------------------------------------------------------------- 1 | import { IHtmlNode } from '../static/types'; 2 | import { isFunction } from '../static/helper'; 3 | 4 | const buffer = new Set([]); 5 | const supportBuffer = buffer?.values?.() && buffer?.add && buffer?.delete; 6 | const array: any[] = []; 7 | 8 | /** 9 | * 给缓冲队列队尾推入一个元素 10 | * @param $object 11 | * @param callback 12 | */ 13 | export const pushBuffer = ($object: IHtmlNode, callback?: any): void => { 14 | if (supportBuffer) { 15 | buffer.add($object); 16 | } else { 17 | array.push($object); 18 | } 19 | 20 | if (isFunction(callback)) callback(); 21 | }; 22 | 23 | /** 24 | * 从缓冲队列对头出队一个缓冲元素 25 | */ 26 | export const shiftBuffer = (): Array => { 27 | if (supportBuffer) { 28 | const value = buffer.values()?.next?.()?.value; 29 | 30 | buffer.delete(value); 31 | 32 | return value; 33 | } 34 | 35 | return array.shift(); 36 | }; 37 | 38 | /** 39 | * 获取缓冲队列长度 40 | */ 41 | export const getBufferSize = () => { 42 | if (supportBuffer) { 43 | return buffer.size; 44 | } 45 | 46 | return array.length; 47 | }; 48 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "umi dev", 5 | "build": "umi build", 6 | "postinstall": "umi generate tmp", 7 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 8 | "test": "umi-test", 9 | "test:coverage": "umi-test --coverage" 10 | }, 11 | "gitHooks": { 12 | "pre-commit": "lint-staged" 13 | }, 14 | "lint-staged": { 15 | "*.{js,jsx,less,md,json}": [ 16 | "prettier --write" 17 | ], 18 | "*.ts?(x)": [ 19 | "prettier --parser=typescript --write" 20 | ] 21 | }, 22 | "dependencies": { 23 | "@ant-design/pro-layout": "^6.5.0", 24 | "@eventtracing/web": "^0.0.2", 25 | "eventemitter3": "^5.0.0", 26 | "react": "17.x", 27 | "react-dom": "17.x", 28 | "umi": "^3.5.35" 29 | }, 30 | "devDependencies": { 31 | "@types/react": "^17.0.0", 32 | "@types/react-dom": "^17.0.0", 33 | "@umijs/preset-react": "1.x", 34 | "@umijs/test": "^3.5.35", 35 | "lint-staged": "^10.0.7", 36 | "prettier": "^2.2.0", 37 | "typescript": "^4.1.2", 38 | "yorkie": "^2.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "importHelpers": true, 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": ".", 11 | "noImplicitThis": false, 12 | "noImplicitAny": false, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "charset": "utf8", 16 | "allowJs": true, 17 | "allowSyntheticDefaultImports": true, 18 | "allowUnreachableCode": false, 19 | "allowUnusedLabels": false, 20 | "removeComments": false, 21 | "preserveConstEnums": true, 22 | "skipLibCheck": true, 23 | "skipDefaultLibCheck": true, 24 | "resolveJsonModule": true, 25 | "typeRoots": [ 26 | "./node_modules/@types/", 27 | "./src/@types/" 28 | ], 29 | "jsx": "react" 30 | }, 31 | "include": [ 32 | "src/**/*", 33 | "config/**/*", 34 | "typings.d.ts" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /example/src/pages/index/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | min-height: 100vh; 4 | padding: 16px; 5 | } 6 | 7 | .button { 8 | position: relative; 9 | width: 100%; 10 | max-height: 100%; 11 | height: 40px; 12 | 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | 17 | border-radius: 6px; 18 | background-color: #ff5777; 19 | font-size: 14px; 20 | color: #fff; 21 | cursor: pointer; 22 | 23 | margin-top: 10px; 24 | 25 | &:first-child { 26 | margin-top: 0; 27 | } 28 | } 29 | 30 | .debugButton { 31 | background-color: cadetblue; 32 | } 33 | 34 | .modal { 35 | position: fixed; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 100%; 40 | 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | 45 | background-color: rgba(0, 0, 0, .4); 46 | } 47 | .modalContent { 48 | width: 300px; 49 | height: 120px; 50 | 51 | display: flex; 52 | justify-content: center; 53 | // align-items: center; 54 | 55 | background-color: #fff; 56 | border-radius: 10px; 57 | 58 | .modalTitle { 59 | font-size: 16px; 60 | font-weight: bold; 61 | margin-top: 20px; 62 | margin-bottom: 15px; 63 | } 64 | } 65 | .modalClose { 66 | width: 260px; 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/static/const.ts: -------------------------------------------------------------------------------- 1 | const sdkName = 'NE_DAWN'; 2 | 3 | // SDK在window上挂载的属性名 4 | export const WINDOW_DAWN = sdkName; 5 | // 自定义埋点方法名 6 | export const WINDOW_DAWN_TRIGGER = 'trigger'; 7 | // 是否已初始化埋点参数名 8 | export const WINDOW_DAWN_INITIALIZED = 'initialized'; 9 | // 是否停止埋点参数名(预留,曙光工具中使用) 10 | export const WINDOW_DAWN_REPORTING_STOPPED = 'reportingStopped'; 11 | // 曙光常量 12 | export const WINDOW_DAWN_CONST = 'CONST'; 13 | // 曙光工具对象名(预留,曙光工具中使用) 14 | export const WINDOW_DAWN_TOOL = 'Tool'; 15 | // 曙光工具对象实例名(预留,曙光工具中使用) 16 | export const WINDOW_DAWN_TOOL_INSTANCE = 'toolInstance'; 17 | 18 | export const ATTRIBUTE_KEY = 'data-log'; 19 | 20 | // 记录数据参数名(节点对象上) 21 | export const OBJ_RECORD_KEY = `${sdkName}_RECORD`; 22 | // JSON.parse的埋点参数名(节点对象上) 23 | export const OBJ_PARAMS_KEY = `${sdkName}_PARAMS`; 24 | // 祖先栈参数名(节点对象上) 25 | export const OBJ_ANCESTORS_KEY = `${sdkName}_ANCESTORS`; 26 | export const NODE_ANCESTORS = '__dawnNodeAncestors'; 27 | export const NODE_SPM = '__dawnNodeSpm'; 28 | export const NODE_GET_SPM = '__dawnNodeGetSpm'; 29 | // 子节点队列参数名(节点对象上) 30 | export const OBJ_CHILDREN_KEY = `${sdkName}_CHILDREN`; 31 | 32 | export const EVENT_NAME_MAP = { 33 | pv: '_pv', // 页面开始曝光 34 | pd: '_pd', // 页面结束曝光 35 | ev: '_ev', // 元素开始曝光 36 | ed: '_ed', // 元素结束曝光 37 | ec: '_ec', // 元素点击 38 | es: '_es', // 元素滑动 39 | plv: '_plv', // 开始播放 40 | pld: '_pld', // 结束播放 41 | }; 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import nodeResolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import {uglify} from 'rollup-plugin-uglify'; 5 | import { visualizer } from 'rollup-plugin-visualizer'; 6 | import packageJson from './package.json'; 7 | import path from 'path'; 8 | 9 | const extensions = ['.ts', '.js', '.json']; 10 | const resolve = (...args) => path.resolve(__dirname, ...args); 11 | 12 | export default [ 13 | { 14 | input: resolve('src/index.ts'), 15 | output: [ 16 | { 17 | file: `dist/public/js/eventtracing-web-jssdk${packageJson?.version ? '-' + packageJson.version : ''}.js`, 18 | format: 'umd', 19 | name: 'eventtracing-web', 20 | }, 21 | ], 22 | plugins: [ 23 | nodeResolve({ 24 | extensions, 25 | jsnext: true, 26 | main: true, 27 | browser: true, 28 | }), 29 | commonjs(), 30 | babel({ 31 | babelHelpers: 'bundled', 32 | exclude: 'node_modules/**', 33 | extensions, 34 | }), 35 | uglify({ 36 | sourcemap: true, 37 | }), 38 | // visualizer({ open: true }), 39 | ], 40 | external: [''], 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /src/lib/store/objects.ts: -------------------------------------------------------------------------------- 1 | import { IHtmlNode, IObject, IObjects } from '../static/types'; 2 | import { OBJ_RECORD_KEY } from '../static/const'; 3 | import { isObject } from '../static/helper'; 4 | 5 | const objects: IObjects = {}; 6 | 7 | /** 8 | * 添加/更新埋点对象 9 | * @param $object 10 | * @param objectInfo 11 | */ 12 | export const setObject = ($object: IHtmlNode, objectInfo?: IObject): void => { 13 | if (!$object) return; 14 | 15 | const { nodeId } = $object?.[OBJ_RECORD_KEY] || {}; 16 | 17 | if (!nodeId) return; 18 | 19 | const oldObjectInfo = objects[nodeId] || {}; 20 | const currentObjectInfo = isObject(objectInfo) ? objectInfo : ($object?.[OBJ_RECORD_KEY] || {}); 21 | const currentObject = { ...oldObjectInfo, ...currentObjectInfo }; 22 | 23 | objects[nodeId] = currentObject; 24 | }; 25 | 26 | export const deleteObject = ($object: IHtmlNode): void => { 27 | if (!$object) return; 28 | 29 | const { nodeId } = $object?.[OBJ_RECORD_KEY] || {}; 30 | 31 | delete objects[nodeId]; 32 | }; 33 | 34 | /** 35 | * 获取指定的埋点对象 36 | * @param $object 37 | */ 38 | export const getObject = ($object?: IHtmlNode): IObject => { 39 | if (!$object) return null; 40 | 41 | const { nodeId } = $object?.[OBJ_RECORD_KEY] || {}; 42 | 43 | return objects?.[nodeId] || {}; 44 | }; 45 | 46 | /** 47 | * 获取指定ID的埋点对象 48 | * @param nodeId 49 | */ 50 | export const getObjectById = (nodeId?: string): IObject => { 51 | if (!nodeId) return null; 52 | 53 | return objects?.[nodeId] || {}; 54 | }; 55 | 56 | /** 57 | * 获取全部埋点对象 58 | */ 59 | export const getObjects = (): IObjects => objects; 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | yarn.lock 10 | package-lock.json 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # webstorm 28 | .idea 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | 92 | 93 | .DS_Store 94 | .vscode 95 | 96 | # Cello 本地配置 97 | cello.config.local.js 98 | 99 | # 构建目标 100 | /public 101 | /dist 102 | /lib 103 | /es 104 | /example/eventtracing 105 | stats.html -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eventtracing/web", 3 | "version": "0.0.2", 4 | "description": "曙光前端通用SDK", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest", 13 | "clean": "rm -rf dist lib es example/eventtracing", 14 | "build:es": "tsc --outDir es --module es6 --declaration --target es5", 15 | "build:lib": "tsc --outDir lib --module commonjs --declaration --target es5", 16 | "build:example": "tsc --outDir example/eventtracing --module es6 --declaration --target es5", 17 | "dist": "rollup -c", 18 | "watch:es": "yarn build:es --watch", 19 | "watch:lib": "yarn build:lib --watch", 20 | "watch:example": "yarn build:example --watch", 21 | "build": "yarn clean && NODE_ENV=production run-p build:*", 22 | "start": "yarn watch:example", 23 | "preversion": "yarn build", 24 | "lint": "eslint --ext .ts,.tsx src/" 25 | }, 26 | "keywords": [ 27 | "library" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/eventtracing/EventTracing-web.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/eventtracing/EventTracing-web/issues" 35 | }, 36 | "license": "MIT", 37 | "author": "zhangtengfei", 38 | "husky-del": { 39 | "hooks": { 40 | "pre-commit": "lint-staged", 41 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 42 | } 43 | }, 44 | "lint-staged": { 45 | "*.{js,jsx,less,md,json}": [ 46 | "prettier --write" 47 | ], 48 | "*.ts?(x)": [ 49 | "prettier --parser=typescript --write", 50 | "yarn run lint" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.12.3", 55 | "@babel/preset-env": "^7.12.1", 56 | "@babel/preset-typescript": "^7.12.1", 57 | "@commitlint/cli": "^11.0.0", 58 | "@commitlint/config-conventional": "^11.0.0", 59 | "@rollup/plugin-babel": "^5.2.1", 60 | "@rollup/plugin-commonjs": "^19.0.0", 61 | "@rollup/plugin-node-resolve": "^10.0.0", 62 | "@types/jest": "^26.0.15", 63 | "@typescript-eslint/eslint-plugin": "^4.7.0", 64 | "@typescript-eslint/parser": "^4.7.0", 65 | "babel-jest": "^26.6.3", 66 | "eslint": "^7.13.0", 67 | "husky": "^4.3.0", 68 | "jest": "^26.6.3", 69 | "lint-staged": "^10.5.1", 70 | "npm-run-all": "^4.1.5", 71 | "prettier": "^2.1.2", 72 | "rimraf": "^3.0.2", 73 | "rollup": "^2.38", 74 | "rollup-plugin-uglify": "^6.0.4", 75 | "rollup-plugin-visualizer": "^5.5.2", 76 | "ts-jest": "^26.4.4", 77 | "typescript": "^4.0.5" 78 | }, 79 | "dependencies": { 80 | "intersection-observer": "^0.12.0", 81 | "tslib": "^2.3.1" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import { IHookConsoleParamType, IHookConsoleParamOptions } from './static/types'; 2 | import { 3 | getBody, 4 | isObject, 5 | isString, 6 | consoleLog, 7 | isInSearch, 8 | assignObject, 9 | consoleError, 10 | consoleWarn, 11 | isFunction, 12 | getWindow, 13 | } from './static/helper'; 14 | 15 | let config: any = { 16 | root: getBody(), // 根节点,可配置 17 | sessId: '', // 不可配置 18 | io: null, 19 | globalParams: { 20 | _url: getWindow('location')?.href || '', 21 | }, // 公参,可配置 22 | 23 | isDefaultReportEd: false, // 是否全部上报元素结束曝光事件,可配置 24 | 25 | isUseHeartbeat: false, // 是否开启心跳,可配置 26 | heartbeatInterval: 10000, // 心跳间隔 27 | 28 | reportLogs: null, // 日志上报通道,可配置 29 | HookConsole: null, 30 | reportLogsCallbackList: [], // 上报日志时,callback队列 31 | }; 32 | 33 | export const callHook = (type: IHookConsoleParamType, message: string, options: IHookConsoleParamOptions): void => { 34 | const { force = false, ...otherOptions }: any = options || {}; 35 | const { code } = options || {}; 36 | const consoleParams = isInSearch('console') ? [otherOptions] : []; 37 | 38 | if (!code) return; 39 | 40 | if (type === 'error') { 41 | consoleError(message, ...consoleParams); 42 | } else if (type === 'warn') { 43 | consoleWarn(message, ...consoleParams); 44 | } else { 45 | if (type === 'log' && force) { 46 | consoleLog(message, ...consoleParams); 47 | } else if (isInSearch('console') && code === 'report') { 48 | consoleLog(message, otherOptions?.reportLogs); 49 | } else if (isInSearch('console', code) && code === 'beforeReport') { 50 | consoleLog(message, otherOptions?.reportLogs); 51 | } else if (isInSearch('console', code)) { 52 | consoleLog(message, otherOptions); 53 | } 54 | } 55 | 56 | const { HookConsole } = getConfig(); 57 | 58 | if (isFunction(HookConsole)) { 59 | try { 60 | HookConsole?.(type, message, otherOptions); 61 | } catch (error) { 62 | consoleWarn('钩子执行:出错', { ...otherOptions, error }); 63 | } 64 | } 65 | }; 66 | 67 | export const setConfig = (keyOrConfig: string | any, value?: any): void => { 68 | if (isObject(keyOrConfig)) config = assignObject(config, keyOrConfig); 69 | if (isString(keyOrConfig)) { 70 | if (keyOrConfig === 'reportLogsCallbackList') { 71 | config.reportLogsCallbackList?.push?.(value); 72 | } else if (keyOrConfig === 'globalParams') { 73 | config.globalParams = assignObject(config.globalParams, value); 74 | } else { 75 | config[keyOrConfig] = value; 76 | } 77 | } 78 | 79 | if (isInSearch('console', 'config')) { 80 | consoleLog('配置更新', { key: keyOrConfig, value, config }); 81 | } 82 | }; 83 | 84 | export const getConfig = (key?: string): any => { 85 | if (typeof key === 'string') return config[key]; 86 | 87 | return config; 88 | }; 89 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'intersection-observer'; 2 | import { startHeartbeat, pauseHeartbeat } from './lib/heartbeat'; 3 | import { setConfig, getConfig, callHook } from './lib/config'; 4 | import { IDawnInitParamsOptions } from './lib/static/types'; 5 | import { 6 | isNumber, 7 | isFunction, 8 | isBoolean, 9 | isWindow, 10 | getWindow, 11 | setWindow, 12 | isObject, 13 | } from './lib/static/helper'; 14 | import * as CONST from './lib/static/const'; 15 | import { 16 | getSessId, 17 | getIntersectionObserver, 18 | startMutationObserver, 19 | triggerBatchExpose, 20 | triggerCustomLogger, 21 | listeningClickEvent, 22 | } from './lib/core'; 23 | 24 | // 监听页面进入前台 25 | const listeningPageShow = (onPageShow): void => { 26 | if (isFunction(onPageShow)) { 27 | try { 28 | onPageShow(() => { 29 | startHeartbeat(); 30 | triggerBatchExpose(true); 31 | }); 32 | } catch (error) { 33 | callHook('error', '注册客户端事件:失败,APP 进入前台事件', { 34 | code: 'init', error, initConfig: getConfig(), 35 | }); 36 | } 37 | } else { 38 | callHook('warn', '注册客户端事件:失败,未设置 APP 进入前台事件,如不需要页面进入前台时自动上报开始曝光日志可忽略', { 39 | code: 'init', error: null, initConfig: getConfig(), 40 | }); 41 | } 42 | }; 43 | 44 | // 监听页面进入后台 45 | const listeningPageHide = (onPageHide): void => { 46 | if (isFunction(onPageHide)) { 47 | try { 48 | onPageHide(() => { 49 | pauseHeartbeat(); 50 | triggerBatchExpose(false); 51 | }); 52 | } catch (error) { 53 | callHook('error', '注册客户端事件:失败,APP 进入后台事件', { 54 | code: 'init', error, initConfig: getConfig(), 55 | }); 56 | } 57 | } else { 58 | callHook('warn', '注册客户端事件:失败,未设置 APP 进入后台事件,如不需要页面进入后台时自动上报结束曝光日志可忽略', { 59 | code: 'init', error: null, initConfig: getConfig(), 60 | }); 61 | } 62 | }; 63 | 64 | // 埋点初始化方法 65 | const init = (options: IDawnInitParamsOptions): boolean => { 66 | const { 67 | reportLogs, 68 | root, 69 | globalParams, 70 | isDefaultReportEd, 71 | isUseHeartbeat, 72 | heartbeatInterval, 73 | onPageShow, 74 | onPageHide, 75 | 76 | HookConsole, 77 | } = options || {}; 78 | const { 79 | WINDOW_DAWN, 80 | WINDOW_DAWN_TRIGGER, 81 | WINDOW_DAWN_INITIALIZED, 82 | WINDOW_DAWN_REPORTING_STOPPED, 83 | WINDOW_DAWN_CONST, 84 | WINDOW_DAWN_TOOL, 85 | WINDOW_DAWN_TOOL_INSTANCE, 86 | } = CONST; 87 | 88 | if (!isWindow()) { 89 | callHook('error', '曙光初始化:失败,不在浏览器环境', { 90 | code: 'init', error: null, initConfig: getConfig(), 91 | }); 92 | return false; 93 | } 94 | if (getWindow(WINDOW_DAWN)?.[WINDOW_DAWN_INITIALIZED] === true) { 95 | callHook('warn', '曙光初始化:重复初始化,本次跳过', { 96 | code: 'init', error: null, initConfig: getConfig(), 97 | }); 98 | return false; 99 | } 100 | 101 | if (isFunction(reportLogs)) setConfig('reportLogs', reportLogs); 102 | if (root) setConfig('root', root); 103 | if (isObject(globalParams)) setConfig('globalParams', globalParams); 104 | if (isBoolean(isDefaultReportEd)) setConfig('isDefaultReportEd', isDefaultReportEd); 105 | if (isBoolean(isUseHeartbeat)) setConfig('isUseHeartbeat', isUseHeartbeat); 106 | if (isNumber(heartbeatInterval) && heartbeatInterval >= 2000) setConfig('heartbeatInterval', heartbeatInterval); 107 | if (isFunction(HookConsole)) setConfig('HookConsole', HookConsole); 108 | 109 | setConfig('sessId', getSessId()); 110 | setConfig('io', getIntersectionObserver()); 111 | 112 | setWindow(WINDOW_DAWN, { 113 | [WINDOW_DAWN_TRIGGER]: triggerCustomLogger, 114 | [WINDOW_DAWN_INITIALIZED]: true, 115 | [WINDOW_DAWN_REPORTING_STOPPED]: false, 116 | [WINDOW_DAWN_CONST]: CONST, 117 | [WINDOW_DAWN_TOOL]: null, 118 | [WINDOW_DAWN_TOOL_INSTANCE]: null, 119 | }); 120 | 121 | try { 122 | startMutationObserver(); 123 | } catch (error) { 124 | callHook('error', '曙光初始化:失败', { 125 | code: 'init', error, initConfig: getConfig(), 126 | }); 127 | return false; 128 | } 129 | 130 | listeningClickEvent(); 131 | listeningPageShow(onPageShow); 132 | listeningPageHide(onPageHide); 133 | startHeartbeat(); 134 | 135 | callHook('log', '曙光初始化:完成', { 136 | code: 'init', force: true, initConfig: getConfig(), 137 | }); 138 | return true; 139 | }; 140 | 141 | export { CONST, init }; 142 | export default { CONST, init }; 143 | -------------------------------------------------------------------------------- /src/lib/static/types.ts: -------------------------------------------------------------------------------- 1 | export interface IHtmlNode { 2 | [key: string]: any; 3 | } 4 | 5 | export interface IObject { 6 | target?: IHtmlNode; 7 | observe?: boolean; 8 | 9 | nodeId?: string; 10 | 11 | // 当前节点的虚拟父节点 12 | virtualParent?: any; 13 | 14 | isExpose?: boolean; 15 | startTime?: number; 16 | endTime?: number; 17 | } 18 | 19 | export interface IObjects { 20 | [objectId: string]: IObject; 21 | } 22 | 23 | export interface IDataLog { 24 | oid: string; 25 | 26 | isPage?: boolean; 27 | events?: string[]; 28 | params?: any; 29 | virtualParentNode?: any; 30 | mountParentSelector?: string; 31 | useForRefer?: string[] | boolean; 32 | 33 | // RN参数兼容 34 | pageId?: string; 35 | elementId?: string; 36 | rootpage?: boolean; 37 | key?: string; 38 | } 39 | 40 | interface IHookConsole { 41 | ( 42 | type: IHookConsoleParamType, 43 | message: string, 44 | options: IHookConsoleParamOptions 45 | ): void; 46 | } 47 | 48 | export interface IDawnInitParamsOptions { 49 | reportLogs?: (options: any) => void; // 日志上报方法 50 | root?: IHtmlNode; // 根节点 51 | globalParams?: any; // 公参 52 | 53 | isDefaultReportEd?: boolean; // 是否默认上报元素结束曝光事件 54 | isUseHeartbeat?: boolean; // 是否开启心跳,默认关闭 55 | 56 | heartbeatInterval?: number; // 心跳间隔 57 | 58 | onPageShow?: (callback: () => void) => void; 59 | onPageHide?: (callback: () => void) => void; 60 | 61 | HookConsole?: IHookConsole; 62 | } 63 | 64 | export interface IReferOptions { 65 | sessId?: string; 66 | type?: string; 67 | actSeq?: number | string; 68 | pgStep?: number | string; 69 | spm?: string; 70 | scm?: string; 71 | } 72 | 73 | export interface IFindKeyNodeParamsOptions { 74 | eventName: string; // 指定事件名,不传则代表支持所有事件 75 | objectType: string; // 指定节点类型,page表示页面、element表示元素、不传或其他表示所有 76 | } 77 | 78 | interface ILogParams { 79 | _eventtime: number; 80 | _sessid: string; 81 | _duration?: number; 82 | g_dprefer: string; 83 | [key: string]: any; 84 | } 85 | 86 | export interface ILog { 87 | event: string; 88 | useForRefer: boolean; 89 | _plist: any[]; 90 | _elist: any[]; 91 | _spm?: string; 92 | _scm?: string; 93 | params: ILogParams; 94 | } 95 | 96 | export interface ICreateAndPushLogParamsOptions { 97 | publicUseForRefer?: boolean | string[]; // 是否追踪refer 98 | publicParams?: any; // 埋点额外参数、事件公参 99 | isForce?: boolean; // 是否强制埋点 100 | isBatch?: boolean; // 是否在下个时间循环批量打点 101 | isHeartbeat?: boolean; // 是否心跳埋点 102 | } 103 | 104 | export interface ICreateAndPushLogResult extends ILog { 105 | isPrevent: boolean; 106 | } 107 | 108 | export interface ITriggerCustomLoggerParamsOptions { 109 | event: string; // 自定义埋点事件名 110 | params?: any; // 自定义埋点公参 111 | useForRefer?: boolean; 112 | isForce?: boolean; // 是否强制埋点 113 | } 114 | 115 | export interface ITriggerCustomLoggerResult { 116 | _plist: any[]; 117 | _elist: any[]; 118 | _spm: string; 119 | _scm: string; 120 | _sessid: string; 121 | g_dprefer: string; 122 | jumprefer: string; 123 | } 124 | 125 | export type IHookConsoleParamType = 'log' | 'warn' | 'error'; 126 | 127 | export type IHookConsoleParamOptions = { 128 | code: 'init' | 'report' | 'beforeReport' | 'plugin' | 'buildLog' | 'refer' | 'trigger' | 'checkLog' 129 | | 'dataLog' | 'io' | 'mo' | 'node' | 'dawnNode', 130 | force?: boolean; 131 | error?: any; // type 为 error、warn 时必传 132 | 133 | // code init 134 | initConfig?: any; 135 | 136 | // code report 137 | reportLogs?: ILog[]; 138 | reportSpm?: string; 139 | reportLog?: any; 140 | reportEvent?: string; 141 | reportUseForRefer?: boolean; 142 | 143 | // code beforeReport 144 | beforeReportLogs?: ILog[]; 145 | 146 | // code plugin 147 | plugin?: any; 148 | pluginDependencies?: any; 149 | 150 | // code buildLog 151 | buildLogPe?: any; 152 | buildLogOptions?: any; 153 | buildLogResult?: any; 154 | 155 | // code refer 156 | refer?: string; 157 | 158 | // code trigger 159 | 160 | // code dataLog 161 | dataLogType?: 'parse' | 'check'; 162 | dataLogTarget?: any; 163 | dataLog?: any; 164 | 165 | // code io 166 | ioObserver?: any; 167 | ioEntries?: any[]; 168 | 169 | // code mo 170 | moObserver?: any; 171 | moList?: any[]; 172 | 173 | // code node 174 | nodeType?: 'add' | 'remove' | 'update'; 175 | nodeList?: any[]; 176 | 177 | // code dawnNode 178 | dawnNodeType?: 'add' | 'remove' | 'update'; 179 | dawnNodeList?: any[]; 180 | 181 | // code checkLog 182 | checkLogMessage?: any; 183 | checkLogAction?: 'connect' | 'basicInfo' | 'log'; 184 | }; 185 | -------------------------------------------------------------------------------- /src/lib/static/helper.ts: -------------------------------------------------------------------------------- 1 | export const isObject = data => Object.prototype.toString.call(data) === '[object Object]'; 2 | export const isArray = data => Object.prototype.toString.call(data) === '[object Array]'; 3 | export const isString = data => typeof data === 'string'; 4 | export const isNumber = data => typeof data === 'number'; 5 | export const isBoolean = data => typeof data === 'boolean'; 6 | export const isFunction = data => typeof data === 'function'; 7 | 8 | // 合并对象 9 | export const assignObject = (...args: any[]): any => { 10 | let object = {}; 11 | 12 | for (let i = 0; i < args.length; i++) { 13 | const item = args[i]; 14 | const currentObject = isObject(item) ? item : {}; 15 | 16 | object = { 17 | ...object, 18 | ...currentObject, 19 | }; 20 | } 21 | 22 | return object; 23 | }; 24 | 25 | // 合并数组 26 | export const assignArray = (...args: any[]): any[] => { 27 | let array = []; 28 | 29 | for (let i = 0; i < args.length; i++) { 30 | const item = args[i]; 31 | const currentArray = isArray(item) ? item : []; 32 | 33 | array = [...array, ...currentArray]; 34 | } 35 | 36 | return array; 37 | }; 38 | 39 | // 是否存在window对象 40 | export const isWindow = () => typeof window !== 'undefined' && window; 41 | 42 | // 获取window或window上的属性和方法 43 | export const getWindow = (name?: string): any => { 44 | if (!isWindow()) return null; 45 | if (name) return window?.[name]; 46 | 47 | return window; 48 | }; 49 | 50 | // 给window上添加属性 51 | export const setWindow = (name: string, value: any): void => { 52 | if (!(isWindow() && name)) return null; 53 | 54 | if (isObject(window[name]) && isObject(value)) { 55 | window[name] = { ...window[name], ...value }; 56 | } else { 57 | window[name] = value; 58 | } 59 | }; 60 | 61 | export const isInSearch = (key: string, value?: string): boolean => { 62 | if (value) { 63 | const currentValue = getQuery()?.[key]; 64 | const currentValueArr = currentValue?.split?.(',') || []; 65 | 66 | return currentValueArr?.includes?.(value); 67 | } 68 | 69 | return new RegExp(key, 'i')?.test?.(getWindow('document')?.location?.search || ''); 70 | }; 71 | 72 | // 浏览器日志打印 73 | export const consoleLog = (title: string, ...args): void => { 74 | if (!isInSearch('console')) return; 75 | 76 | console.log(`[曙光]: ${title}`, ...args); 77 | }; 78 | 79 | // 浏览器警告日志打印 80 | export const consoleWarn = (title: string, ...args): void => { 81 | console.warn(`[曙光]: ${title}`, ...args); 82 | }; 83 | 84 | // 浏览器错误日志打印 85 | export const consoleError = (title: string, ...args): void => { 86 | if (args?.length === 1 && isString(args?.[0])) { 87 | console.error(`[曙光]: ${title}`, new Error(args[0])); 88 | } else { 89 | console.error(`[曙光]: ${title}`, ...args); 90 | } 91 | }; 92 | 93 | // 获取单个DOM对象 94 | export const getElement = (selector: string, node?: any) => { 95 | if (!selector) return null; 96 | 97 | return (node || getWindow('document'))?.querySelector?.(selector); 98 | }; 99 | 100 | // 获取多个DOM对象 101 | export const getElements = (selector: string, node?: any) => { 102 | if (!selector) return null; 103 | 104 | return (node || getWindow('document'))?.querySelectorAll?.(selector) || []; 105 | }; 106 | 107 | // 获取body节点 108 | export const getBody = () => getElement('body'); 109 | 110 | // 获取链接参数 111 | export const getQuery = (): any => { 112 | const searchStr = (getWindow('location')?.search || '').slice(1,); 113 | const searchArr = searchStr ? searchStr.split('&') : []; 114 | const searchArrLen = searchArr?.length || 0; 115 | const query = {}; 116 | 117 | if (searchArrLen) { 118 | for (let i = 0; i < searchArrLen; i++) { 119 | const item = searchArr[i]; 120 | const itemArr = item.split('='); 121 | 122 | if (itemArr?.[0]) { 123 | try { 124 | query[itemArr[0]] = decodeURIComponent(itemArr?.[1] || ''); 125 | } catch (e) { 126 | query[itemArr[0]] = ''; 127 | } 128 | } 129 | } 130 | } 131 | 132 | return query; 133 | }; 134 | 135 | // 延迟或线程空闲时执行 136 | export const postponeCallback = (callback: () => void, options?: any) => { 137 | const { isIdle, timeout } = options || {}; 138 | const requestIdleCallbackFunc = getWindow('requestIdleCallback'); 139 | 140 | if (isFunction(requestIdleCallbackFunc) && isIdle) { 141 | requestIdleCallbackFunc?.(() => { 142 | callback?.(); 143 | }, { timeout: isNumber(timeout) ? timeout : 200 }); 144 | return; 145 | } 146 | 147 | const timer = setTimeout(() => { 148 | clearTimeout(timer); 149 | callback?.(); 150 | }, isNumber(timeout) ? timeout : 0); 151 | }; 152 | 153 | // 获取指定长度的随机字符串 154 | export const getRandomString = (length: number, isNum?: boolean): string => { 155 | let text = ''; 156 | const possibleNumber = '0123456789'; 157 | const possibleAll = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 158 | const possible = isNum ? possibleNumber : possibleAll; 159 | 160 | for (let i = 0; i < length; i++) { 161 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 162 | } 163 | 164 | return text; 165 | }; 166 | -------------------------------------------------------------------------------- /example/src/pages/index/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useEffect, useCallback } from 'react'; 2 | import EventEmitter from 'eventemitter3'; 3 | import EventTracing from '@eventtracing/web'; 4 | import styles from './index.less'; 5 | 6 | const ee = new EventEmitter(); 7 | const isInApp = false; // 是否在客户端内,这里只是个例子,具体根据自己客户端对应方法使用 8 | 9 | const Index = () => { 10 | const [visible, setVisible] = useState(false); 11 | 12 | useEffect(() => { 13 | EventTracing.init({ 14 | globalParams: { __test_global_param: 'test' }, // 全局公参,这里只是个例子,具体根据自己需要传参 15 | // isUseHeartbeat: true, // 心跳 _pd 16 | onPageShow: (exposeStart: any) => { 17 | // 模拟客户端进入到前台事件,这里只是个例子,具体根据自己客户端对应方法使用 18 | ee.on('onPageShow', () => { 19 | exposeStart(); 20 | }); 21 | }, 22 | onPageHide: (exposeEnd: any) => { 23 | // 模拟客户端退出到后台事件,这里只是个例子,具体根据自己客户端对应方法使用 24 | ee.on('onPageHide', () => { 25 | exposeEnd(); 26 | }); 27 | }, 28 | reportLogs: ({ logs }: any) => { 29 | if (isInApp) { 30 | console.log(`[曙光日志上报]:`, '客户端协议', logs); 31 | 32 | // 客户端日志上报方式,这里只是个简单的例子,具体根据自己客户端对应方法使用 33 | try { 34 | window.bridge.call('eventTracing', 'reportBatch', { logs }, function (error: any, result: any, context: any) { 35 | console.log('Call eventTracing reportBatch', error, result, context); 36 | }); 37 | } catch (error) { 38 | console.error(`[曙光]:`, '客户端协议出错,请检查是否在客户端内、客户端是否已接入曙光 SDK', error); 39 | } 40 | } else { 41 | console.log(`[曙光日志上报]:`, '网络请求', logs); 42 | 43 | // 浏览器端日志上报方式,通过发送网络请求上报 44 | // fetch... 45 | } 46 | }, 47 | }); 48 | }, []); 49 | 50 | const getRefers = useCallback(() => { 51 | if (isInApp) { 52 | try { 53 | // 客户端 refers 获取方式,这里只是个简单的例子,具体根据自己客户端对应方法使用 54 | window.bridge.call('eventTracing', 'refers', { key: 'all' }, function (error: any, result: any, context: any) { 55 | console.log('Call eventTracing refers', error, result, context); 56 | }); 57 | } catch (error) { 58 | console.error(`[曙光]:`, '客户端协议出错,请检查是否在客户端内、客户端是否已接入曙光 SDK', error); 59 | } 60 | } else { 61 | console.warn(`[曙光]:`, '当前不在客户端内或客户端没有接入曙光 SDK'); 62 | } 63 | }, []); 64 | 65 | const modalMemo = useMemo(() => { 66 | if (!visible) return null; 67 | 68 | return ( 69 |
70 |
79 |
80 |
逻辑挂载
81 |
setVisible(false)} 93 | >关闭
94 |
95 |
96 |
97 | ); 98 | }, [visible]); 99 | 100 | return ( 101 | <> 102 |
111 |
{ 114 | if (isInApp) { 115 | try { 116 | // 客户端相关协议有效性判断方式,这里只是个简单的例子,具体根据自己客户端对应方法使用 117 | window.bridge.isBridgeAvaiable('eventTracing', 'refers', (avaiable: any, content: any) => { 118 | console.log('JS checkout bridge avaiable', avaiable, content); 119 | }); 120 | window.bridge.isBridgeAvaiable('eventTracing', 'report', (avaiable: any, content: any) => { 121 | console.log('JS checkout bridge avaiable', avaiable, content); 122 | }); 123 | window.bridge.isBridgeAvaiable('eventTracing', 'reportBatch', (avaiable: any, content: any) => { 124 | console.log('JS checkout bridge avaiable', avaiable, content); 125 | }); 126 | } catch (error) { 127 | console.error(`[曙光]:`, '客户端协议出错,请检查是否在客户端内、客户端是否已接入曙光 SDK', error); 128 | } 129 | } else { 130 | console.warn(`[曙光]:`, '当前不在客户端内或客户端没有接入曙光 SDK'); 131 | } 132 | }} 133 | >Check Avaiable
134 | 135 |
{ 138 | ee.emit('onPageShow'); 139 | }} 140 | >模拟 APP 进入到前台
141 | 142 |
{ 145 | ee.emit('onPageHide'); 146 | }} 147 | >模拟 APP 退出到后台
148 | 149 |
获取 Refers
153 | 154 |
{ 166 | window.NE_DAWN.trigger(e.currentTarget, { 167 | event: '_ec', // 事件名 168 | params: { __test_event_param: 'test' }, // 事件公参 169 | }); 170 | }} 171 | >手动触发点击事件
172 | 173 |
setVisible(true)} 185 | >弹窗(逻辑挂载)
186 | 187 |
虚拟父节点1
207 |
虚拟父节点2
227 |
虚拟父节点3
247 |
248 | 249 | {modalMemo} 250 | 251 | ); 252 | }; 253 | 254 | export default Index; 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/lib/core.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { 3 | IHtmlNode, 4 | IDataLog, 5 | ILog, 6 | IObject, 7 | IFindKeyNodeParamsOptions, 8 | IReferOptions, 9 | ICreateAndPushLogParamsOptions, 10 | ICreateAndPushLogResult, 11 | ITriggerCustomLoggerParamsOptions, 12 | ITriggerCustomLoggerResult, 13 | } from './static/types'; 14 | import { getConfig, callHook } from './config'; 15 | import { 16 | OBJ_PARAMS_KEY, 17 | OBJ_RECORD_KEY, 18 | OBJ_ANCESTORS_KEY, 19 | 20 | NODE_ANCESTORS, NODE_SPM, NODE_GET_SPM, 21 | 22 | EVENT_NAME_MAP, 23 | ATTRIBUTE_KEY, 24 | OBJ_CHILDREN_KEY, 25 | WINDOW_DAWN, 26 | WINDOW_DAWN_REPORTING_STOPPED, 27 | } from './static/const'; 28 | import { 29 | isArray, 30 | isNumber, 31 | isObject, 32 | isString, 33 | isBoolean, 34 | isFunction, 35 | getWindow, 36 | getBody, 37 | getElement, 38 | getElements, 39 | postponeCallback, 40 | getQuery, 41 | getRandomString, 42 | assignObject, 43 | } from './static/helper'; 44 | import { getObject, getObjectById, getObjects, setObject } from './store/objects'; 45 | import { getLogsLength, pushLog, shiftLog } from './store/logs'; 46 | import { getBufferSize, pushBuffer, shiftBuffer } from './store/buffer'; 47 | 48 | const DEFAULT_LOG_PARAMS = { 49 | oid: '', 50 | isPage: false, 51 | events: [], 52 | params: {}, 53 | }; 54 | 55 | let objectIndex: number = 100000; 56 | 57 | // 默认日志上报方法 58 | const defaultReportLogs = ({ logs = [] } = { logs: [] }): void => { 59 | if (!logs.length) return; 60 | 61 | callHook('error', '日志上报:失败,未设置日志上报通道', { 62 | code: 'report', error: null, reportLogs: logs, 63 | }); 64 | 65 | logs.map((log) => { 66 | const { event, params, useForRefer } = log || {}; 67 | const { _spm } = params || {}; 68 | 69 | callHook('error', '日志上报:失败,未设置日志上报通道', { 70 | code: 'report', error: null, reportSpm: _spm, reportLog: log, reportEvent: event, reportUseForRefer: useForRefer, 71 | }); 72 | }); 73 | }; 74 | 75 | // 上报日志 76 | const reportLog = (logs: ILog[]): void => { 77 | const { reportLogs, reportLogsCallbackList } = getConfig(); 78 | 79 | if (!isFunction(reportLogs)) { 80 | defaultReportLogs({ logs }); 81 | return; 82 | } 83 | 84 | callHook('log', '日志上报:上报前', { 85 | code: 'beforeReport', beforeReportLogs: logs, 86 | }); 87 | 88 | try { 89 | if (reportLogsCallbackList?.length) { 90 | reportLogsCallbackList.map((reportLogsCallback: any) => { 91 | reportLogsCallback?.({ logs }); 92 | }); 93 | } 94 | } catch (error) { 95 | callHook('error', '日志上报:回调队列执行出错', { 96 | code: 'report', error, reportLogs: logs, 97 | }); 98 | } 99 | 100 | try { 101 | reportLogs({ logs }); 102 | } catch (error) { 103 | callHook('error', '日志上报:失败,执行出错', { 104 | code: 'report', error, reportLogs: logs, 105 | }); 106 | } 107 | }; 108 | 109 | // 上报指定数量日志 110 | const reportAllLogs = (): void => { 111 | if (!getLogsLength()) return; 112 | 113 | const logs = shiftLog(10); 114 | 115 | reportLog(logs); 116 | if (getLogsLength()) reportAllLogs(); 117 | }; 118 | 119 | // 获取埋点事件 120 | const formatEvents = (events: string[], isPage: boolean): string[] => { 121 | const { isDefaultReportEd } = getConfig(); 122 | const defaultEvents = isPage 123 | ? [EVENT_NAME_MAP.pv, EVENT_NAME_MAP.pd] 124 | : (isDefaultReportEd ? [EVENT_NAME_MAP.ev, EVENT_NAME_MAP.ed] : [EVENT_NAME_MAP.ev]); 125 | 126 | return isArray(events) ? events : defaultEvents; 127 | }; 128 | 129 | // 是否已开启心跳、且事件为_pd 130 | const isHeartbeatPd = (eventName: string): boolean => { 131 | const { isUseHeartbeat } = getConfig(); 132 | 133 | return isUseHeartbeat && eventName === EVENT_NAME_MAP.pd; 134 | }; 135 | 136 | // 转义埋点参数 137 | const formatDataLog = (stringParams: string): IDataLog => { 138 | if (!stringParams) return DEFAULT_LOG_PARAMS; 139 | 140 | let objectParams = null; 141 | 142 | if (isObject(stringParams)) { 143 | objectParams = stringParams; 144 | } else { 145 | try { 146 | objectParams = JSON.parse(stringParams); 147 | } catch (error) { 148 | callHook('error', '埋点参数:反序列化失败出错', { 149 | code: 'dataLog', error, dataLogType: 'parse', dataLog: stringParams, 150 | }); 151 | } 152 | } 153 | 154 | if (!isObject(objectParams)) return DEFAULT_LOG_PARAMS; 155 | if (objectParams?.oid) return objectParams; 156 | 157 | const { pageId, elementId } = objectParams || {}; 158 | 159 | if (pageId || elementId) { 160 | return { 161 | oid: pageId || elementId, 162 | isPage: !!pageId, 163 | ...objectParams, 164 | }; 165 | } 166 | 167 | return DEFAULT_LOG_PARAMS; 168 | }; 169 | 170 | // 获取节点上的埋点参数(object) 171 | const getDataLog = (target: IHtmlNode, isUpdate?: boolean): IDataLog => { 172 | if (!target) return DEFAULT_LOG_PARAMS; 173 | 174 | let logParams: IDataLog = target?.[OBJ_PARAMS_KEY]; 175 | 176 | if (isUpdate === true) { 177 | const stringParams: string = target?.attributes?.[ATTRIBUTE_KEY]?.value; 178 | 179 | if (stringParams) { 180 | logParams = formatDataLog(stringParams); 181 | } 182 | } 183 | 184 | if (!logParams?.oid) { 185 | const stringParams: string = target?.attributes?.[ATTRIBUTE_KEY]?.value; 186 | 187 | if (!stringParams) return DEFAULT_LOG_PARAMS; 188 | 189 | logParams = formatDataLog(stringParams); 190 | } 191 | 192 | const { isPage, events, params } = logParams; 193 | const currentIsPage = isBoolean(isPage) ? isPage : false; 194 | 195 | return { 196 | ...logParams, 197 | isPage: currentIsPage, 198 | events: formatEvents(events, currentIsPage), 199 | params: isObject(params) ? params : {}, 200 | }; 201 | }; 202 | 203 | // 当前节点是否关键节点 204 | const isKeyNode = (target: IHtmlNode): boolean => { 205 | const { oid } = getDataLog(target) || {}; 206 | 207 | return isString(oid) && !!oid; 208 | }; 209 | 210 | // 查找父级关键节点 211 | const findParentKeyNode = (target: IHtmlNode): IHtmlNode => { 212 | const parent = target?.parentNode; 213 | 214 | if (parent && isKeyNode(parent)) return parent; 215 | if (!parent) return null; 216 | 217 | return findParentKeyNode(parent); 218 | }; 219 | 220 | // 查找所有祖先关键节点,直到根节点结束 221 | const collectAncestors = (terget: IHtmlNode, ancestors?: Array): any => { 222 | ancestors = isArray(ancestors) ? ancestors : isKeyNode(terget) ? [terget] : []; 223 | 224 | const parent = findParentKeyNode(terget); 225 | 226 | if (parent) { 227 | ancestors.push(parent); 228 | return collectAncestors(parent, ancestors); 229 | } else { 230 | return ancestors; 231 | } 232 | }; 233 | 234 | // 查找支持指定事件、指定类型的关键节点 235 | const findKeyNode = (target: IHtmlNode, options?: IFindKeyNodeParamsOptions): IHtmlNode => { 236 | const { eventName, objectType } = options || {}; 237 | let objectTypeList = [true, false]; // 节点类型,[true]表示仅页面、[false]表示仅元素、[true, false]表示页面及元素 238 | 239 | if (objectType === 'page') { 240 | objectTypeList = [true]; 241 | } else if (objectType === 'element') { 242 | objectTypeList = [false]; 243 | } 244 | 245 | while (target) { 246 | if (isKeyNode(target)) { 247 | const { events, isPage } = getDataLog(target); 248 | 249 | if (objectTypeList.includes(isPage) && (!eventName || events.includes(eventName))) { 250 | return target; 251 | } 252 | } 253 | 254 | target = findParentKeyNode(target); 255 | } 256 | 257 | return null; 258 | }; 259 | 260 | // 获取指定节点的祖先节点栈,根据埋点配置选择 261 | const getAncestors = (target: IHtmlNode): Array => { 262 | if (!isKeyNode(target)) return []; 263 | 264 | return target?.[OBJ_ANCESTORS_KEY] || []; 265 | }; 266 | 267 | // 获取指定节点的子节点队列,根据埋点配置选择 268 | const getChildren = (target: IHtmlNode): Array => { 269 | if (!isKeyNode(target)) return []; 270 | 271 | return target?.[OBJ_CHILDREN_KEY] || []; 272 | }; 273 | 274 | // 获取spm 275 | const getSpm = (spm: string, oid: string, s_position: string | number): string => `${spm}${spm ? '|' : ''}${oid}${s_position ? ':' + s_position : ''}`; 276 | 277 | // 获取scm 278 | const getScm = (scm: string, s_cid: string | number, s_ctype: string, s_ctraceid: string, s_ctrp: string): string => `${scm}${scm ? '|' : ''}${s_cid || ''}:${s_ctype || ''}:${s_ctraceid || ''}:${s_ctrp || ''}`; 279 | 280 | // 获取链接中携带的JumpRefer 281 | const getJumpRefer = (): IReferOptions => { 282 | const { jumprefer: jumpReferStr } = getQuery(); 283 | 284 | if (!jumpReferStr) return {}; 285 | 286 | let jumpReferObj = {}; 287 | 288 | try { 289 | jumpReferObj = JSON.parse(jumpReferStr); 290 | } catch (error) { 291 | callHook('error', 'DpRefer生成:失败,JumpRefer 反序列化出错', { 292 | code: 'refer', error, refer: jumpReferStr, 293 | }); 294 | } 295 | 296 | return jumpReferObj; 297 | }; 298 | 299 | const getReferItem = (val) => (val ? '[' + val + ']' : ''); 300 | 301 | // 获取Refer 302 | const getRefer = (options: IReferOptions = {}): string => { 303 | const { sessId, type, actSeq, pgStep, spm, scm } = options || {}; 304 | const currentSessId = sessId || getConfig()?.sessId; 305 | const currentType = ['e', 'p', 's'].includes(type) ? type : ''; 306 | const currentSpm = spm ? encodeURIComponent(spm) : ''; 307 | const currentScm = scm ? encodeURIComponent(scm) : ''; 308 | const currentActSeq = isNumber(actSeq) || isString(actSeq) ? `${actSeq}` : ''; 309 | const currentPgStep = isNumber(pgStep) || isString(pgStep) ? `${pgStep}` : ''; 310 | let option = 0; 311 | 312 | if (currentSessId) option += 1; 313 | if (currentType) option += 10; 314 | if (currentActSeq) option += 100; 315 | if (currentPgStep) option += 1000; 316 | if (currentSpm) option += 10000; 317 | if (currentScm) option += 100000; 318 | if (currentSpm || currentScm) option += 10000000000; // er 319 | 320 | return [ 321 | `[F:${option}]`, 322 | getReferItem(currentSessId), 323 | getReferItem(currentType), 324 | getReferItem(currentActSeq), 325 | getReferItem(currentPgStep), 326 | getReferItem(currentSpm), 327 | getReferItem(currentScm), 328 | ].join(''); 329 | }; 330 | 331 | // 获取dprefer 332 | const getDpRefer = (): string => getRefer(getJumpRefer()); 333 | 334 | // 获取埋点参数 335 | const getLog = (target: IHtmlNode, options?: any): ILog => { 336 | const { eventName, publicParams, useForRefer } = options || {}; 337 | const globalParams = getConfig('globalParams'); 338 | let ancestors = getAncestors(target); 339 | 340 | if (!ancestors?.length) { 341 | ancestors = collectAncestors(target)?.slice?.(1) || []; 342 | } 343 | 344 | const peList = [target, ...ancestors]; 345 | const peListLen = peList?.length || 0; 346 | const { startTime, endTime }: IObject = getObject(target); 347 | const duration = (endTime - startTime) || 0; 348 | const dpRefer = getDpRefer(); 349 | const { sessId } = getConfig(); 350 | const pList = []; 351 | const eList = []; 352 | let spm = ''; 353 | let scm = ''; 354 | let currentUseForRefer = null; 355 | 356 | for (let i = 0; i < peListLen; i++) { 357 | const item = peList[i]; 358 | const { oid, params, isPage } = getDataLog(item, true); 359 | const { s_position, s_ctraceid, s_ctrp, s_cid, s_ctype } = params || {}; 360 | const commonParams = { 361 | ...(isObject(params) ? params : {}), 362 | }; 363 | 364 | if (oid) { 365 | spm = getSpm(spm, oid, s_position); 366 | scm = getScm(scm, s_cid, s_ctype, s_ctraceid, s_ctrp); 367 | 368 | if (isPage) { 369 | pList.push({ _oid: oid, ...commonParams }); 370 | } else { 371 | eList.push({ _oid: oid, ...commonParams }); 372 | } 373 | } 374 | } 375 | 376 | const spmLength = spm?.split('|')?.length || 0; 377 | 378 | if (isBoolean(useForRefer)) { 379 | currentUseForRefer = useForRefer; 380 | } else if (isArray(useForRefer)) { 381 | currentUseForRefer = useForRefer?.includes?.(eventName); 382 | } 383 | 384 | if ([EVENT_NAME_MAP?.ec].includes(eventName)) { 385 | currentUseForRefer = currentUseForRefer !== false; 386 | } else if ([EVENT_NAME_MAP?.pv].includes(eventName)) { 387 | currentUseForRefer = currentUseForRefer !== false && spmLength <= 1; 388 | } else { 389 | currentUseForRefer = !!currentUseForRefer; 390 | } 391 | 392 | return { 393 | event: eventName || '', 394 | useForRefer: isBoolean(currentUseForRefer) ? currentUseForRefer : false, 395 | _plist: pList, 396 | _elist: eList, 397 | _spm: spm, 398 | _scm: scm, 399 | params: assignObject( 400 | { 401 | _eventtime: Date.now(), 402 | _sessid: sessId, 403 | g_dprefer: dpRefer, 404 | }, 405 | [EVENT_NAME_MAP.pd, EVENT_NAME_MAP.ed].includes(eventName) ? { _duration: duration } : {}, 406 | publicParams, 407 | globalParams 408 | ), 409 | }; 410 | }; 411 | 412 | // 构建并将埋点日志push至日志队列 413 | const createAndPushLog = ($object: any, eventName: string, options?: ICreateAndPushLogParamsOptions): ICreateAndPushLogResult | any => { 414 | const defaultResult = { isPrevent: true }; 415 | 416 | if (!($object && isString(eventName))) return defaultResult; 417 | 418 | const formattedDataLog = getDataLog($object); 419 | const { oid, events, useForRefer: privateUseForRefer } = formattedDataLog || {}; 420 | const { publicUseForRefer, publicParams, isForce, isBatch, isHeartbeat } = options || {}; 421 | const currentUseForRefer: any = isBoolean(publicUseForRefer) ? publicUseForRefer : (isArray(publicUseForRefer) ? publicUseForRefer : privateUseForRefer); 422 | 423 | if (!(isString(oid) && oid)) { 424 | callHook('error', '埋点参数:参数校验不通过', { 425 | code: 'dataLog', error: null, dataLogType: 'check', dataLogTarget: $object, dataLog: formattedDataLog, 426 | }); 427 | return defaultResult; 428 | } 429 | if ([EVENT_NAME_MAP.pv, EVENT_NAME_MAP.pd].includes(eventName)) { 430 | // events中存在pv或pd,或者options.isForce为true才可进行下一步 431 | if (!(events.includes(EVENT_NAME_MAP.pv) || events.includes(EVENT_NAME_MAP.pd) || isForce === true)) return defaultResult; 432 | } else if ([EVENT_NAME_MAP.ed].includes(eventName)) { 433 | // events中存在ev且ed,或者options.isForce为true才可进行下一步 434 | if (!(events.includes(EVENT_NAME_MAP.ev) && events.includes(EVENT_NAME_MAP.ed) || isForce === true)) return defaultResult; 435 | } else { 436 | if (!(events.includes(eventName) || isForce === true)) return defaultResult; 437 | } 438 | 439 | const log = getLog($object, { eventName, publicParams, useForRefer: currentUseForRefer }); 440 | 441 | if (!log?._plist?.length) { 442 | callHook('error', '日志构建:失败,没有页面节点', { 443 | code: 'buildLog', error: null, buildLogPe: $object, buildLogOptions: options, buildLogResult: log, 444 | }); 445 | return defaultResult; 446 | } 447 | 448 | if (!(getWindow(WINDOW_DAWN)?.[WINDOW_DAWN_REPORTING_STOPPED] === true)) { 449 | const { _spm, _scm, ...rest }: any = log || {}; 450 | pushLog(rest, () => { 451 | if (isHeartbeat) { 452 | setObject($object, { startTime: Date.now() }); 453 | } 454 | 455 | if (isBatch === true) { 456 | postponeCallback(() => { 457 | reportAllLogs(); 458 | }, { isIdle: false }); 459 | } else { 460 | reportAllLogs(); 461 | } 462 | }); 463 | } 464 | 465 | return assignObject(log, { isPrevent: false }); 466 | }; 467 | 468 | // 获取事件名 469 | const getExposeEventName = (target: IHtmlNode, isGoExpose: boolean): string => { 470 | const { isPage } = getDataLog(target); 471 | 472 | if (isGoExpose) { 473 | return isPage ? EVENT_NAME_MAP.pv : EVENT_NAME_MAP.ev; 474 | } else { 475 | return isPage ? EVENT_NAME_MAP.pd : EVENT_NAME_MAP.ed; 476 | } 477 | }; 478 | 479 | // 更新埋点对象、准备曝光 480 | const updateObjectAndExpose = (entry: any, isGoExpose: boolean): void => { 481 | const { target: $object } = entry || {}; 482 | const eventName = getExposeEventName($object, isGoExpose); 483 | const { virtualParent } = getObject($object); 484 | 485 | if (isGoExpose) { 486 | const extraData = { startTime: Date.now() }; 487 | 488 | // 虚拟父节点只在所有子节点中的第一个曝光时曝光 489 | if (virtualParent?.[OBJ_RECORD_KEY]) { 490 | const virtualParentObject = getObject(virtualParent); 491 | 492 | if (!virtualParentObject?.isExpose) { 493 | const virtualParentEventName = getExposeEventName(virtualParent, isGoExpose); 494 | 495 | setObject(virtualParent, { isExpose: isGoExpose, target: virtualParent, ...extraData }); 496 | postponeCallback(() => { 497 | createAndPushLog(virtualParent, virtualParentEventName, { isBatch: true }); 498 | }, { isIdle: false }); 499 | } 500 | } 501 | 502 | setObject($object, { isExpose: isGoExpose, target: $object, ...extraData }); 503 | postponeCallback(() => { 504 | createAndPushLog($object, eventName, { isBatch: true }); 505 | }, { isIdle: false }); 506 | } else { 507 | const extraData = { endTime: Date.now() }; 508 | 509 | // 虚拟父节点在所有子节点曝光结束后再曝光结束 510 | if (virtualParent?.[OBJ_RECORD_KEY]) { 511 | const { target: $virtualParent } = getObject(virtualParent); 512 | const $virtualParentChildren = getChildren($virtualParent); 513 | const virtualParentChildrenLen = $virtualParentChildren?.length || 0; 514 | let hasShowChildren = false; 515 | 516 | if (virtualParentChildrenLen) { 517 | for (let i = 0; i < virtualParentChildrenLen; i++) { 518 | const childrenItem = $virtualParentChildren[i]; 519 | const childrenItemObject = getObject(childrenItem); 520 | 521 | if (childrenItemObject?.isExpose) { 522 | hasShowChildren = true; 523 | break; 524 | } 525 | } 526 | 527 | if (!hasShowChildren) { 528 | const virtualParentEventName = getExposeEventName(virtualParent, isGoExpose); 529 | 530 | // 心跳开启时,要屏蔽正常的_pd 531 | if (!isHeartbeatPd(virtualParentEventName)) { 532 | setObject(virtualParent, { isExpose: isGoExpose, target: virtualParent, ...extraData }); 533 | postponeCallback(() => { 534 | createAndPushLog(virtualParent, virtualParentEventName, { isBatch: true }); 535 | }, { isIdle: false }); 536 | } 537 | } 538 | } 539 | } 540 | 541 | setObject($object, { isExpose: isGoExpose, target: $object, ...extraData }); 542 | 543 | if (isHeartbeatPd(eventName)) { 544 | postponeCallback(() => { 545 | createAndPushLog($object, eventName, { 546 | publicParams: { is_beat: 1 }, 547 | isBatch: true, 548 | }); 549 | }, { isIdle: false }); 550 | } else { 551 | postponeCallback(() => { 552 | createAndPushLog($object, eventName, { isBatch: true }); 553 | }, { isIdle: false }); 554 | } 555 | } 556 | }; 557 | 558 | // 更新关键节点树、监听曝光 559 | const updateTreeAndListeningExpose = ($object: IHtmlNode): void => { 560 | if (!$object) return; 561 | if ($object?.[OBJ_RECORD_KEY]) { 562 | $object[OBJ_PARAMS_KEY] = getDataLog($object, true); 563 | return; 564 | } 565 | 566 | const logParams = getDataLog($object, true); 567 | let mountParentNode = null; 568 | 569 | if (!logParams?.oid) return; 570 | if (logParams?.mountParentSelector) { 571 | mountParentNode = getElement(logParams.mountParentSelector); 572 | } 573 | 574 | const $parent = mountParentNode || findParentKeyNode($object); 575 | const $ancestors = $parent ? [$parent, ...getAncestors($parent)] : []; 576 | const virtualParentLogParams = logParams?.virtualParentNode || {}; 577 | const virtualParentKey = virtualParentLogParams?.key; 578 | const { 579 | oid: virtualParentOid, 580 | isPage: virtualParentIsPage, 581 | params: virtualParentParams, 582 | events: virtualParentEvents, 583 | ...otherParams 584 | } = formatDataLog(virtualParentLogParams); 585 | let $currentParent = null; 586 | let $currentAncestors = null; 587 | let currentObjectInfo = {}; 588 | 589 | // 虚拟父节点 590 | if (virtualParentOid && virtualParentKey) { 591 | const virtualParentNodeId = `virtual_node_${virtualParentKey}`; 592 | const virtualParentObject = getObjectById(virtualParentNodeId); 593 | const $virtualParent = virtualParentObject?.target || {}; 594 | 595 | $virtualParent[OBJ_ANCESTORS_KEY] = $ancestors; 596 | $virtualParent[NODE_ANCESTORS] = $ancestors; 597 | $virtualParent[NODE_SPM] =''; 598 | $virtualParent[NODE_GET_SPM] = () => getLog($virtualParent)?._spm; 599 | $virtualParent[OBJ_PARAMS_KEY] = { 600 | oid: virtualParentOid, 601 | isPage: virtualParentIsPage, 602 | params: isObject(virtualParentParams) ? virtualParentParams : {}, 603 | events: formatEvents(virtualParentEvents, virtualParentIsPage), 604 | ...(otherParams || {}), 605 | }; 606 | $virtualParent[OBJ_RECORD_KEY] = { target: $virtualParent, observe: true, nodeId: virtualParentNodeId }; 607 | setObject($virtualParent); 608 | 609 | if ($parent) { 610 | $parent[OBJ_CHILDREN_KEY] = getChildren($parent); 611 | $parent[OBJ_CHILDREN_KEY].push($virtualParent); 612 | } 613 | 614 | $currentParent = $virtualParent; 615 | $currentAncestors = [$virtualParent, ...$ancestors]; 616 | currentObjectInfo = { virtualParent: $virtualParent }; 617 | } 618 | 619 | $currentParent = $currentParent || $parent; 620 | $currentAncestors = $currentAncestors || $ancestors; 621 | 622 | if ($currentParent) { 623 | $currentParent[OBJ_CHILDREN_KEY] = getChildren($currentParent); 624 | $currentParent[OBJ_CHILDREN_KEY].push($object); 625 | } 626 | 627 | $object[OBJ_ANCESTORS_KEY] = $currentAncestors; 628 | $object[NODE_ANCESTORS] = $currentAncestors; 629 | $object[NODE_SPM] =''; 630 | $object[NODE_GET_SPM] = () => getLog($object)?._spm; 631 | $object[OBJ_PARAMS_KEY] = logParams; 632 | $object[OBJ_RECORD_KEY] = { target: $object, observe: true, nodeId: `node_${objectIndex}`, ...currentObjectInfo }; 633 | 634 | setObject($object); 635 | getConfig('io')?.observe?.($object); 636 | 637 | ++objectIndex; 638 | }; 639 | 640 | // 开始观察 641 | const taskProcessing = (): void => { 642 | postponeCallback(() => { 643 | if (!getBufferSize()) return; 644 | 645 | const $target: IHtmlNode = shiftBuffer(); 646 | const $sonObjects: Array = $target ? getElements(`[${ATTRIBUTE_KEY}]`, $target) : []; 647 | const $objects: Array = isKeyNode($target) ? [$target, ...$sonObjects] : $sonObjects; 648 | const objectsLen = $objects?.length || 0; 649 | 650 | if (objectsLen) { 651 | for (let i = 0; i < objectsLen; i++) { 652 | const $object = $objects[i]; 653 | 654 | updateTreeAndListeningExpose($object); 655 | } 656 | } 657 | 658 | if (getBufferSize()) taskProcessing(); 659 | }, { isIdle: false }); 660 | }; 661 | 662 | // 观察根节点的子孙节点新增和删除 663 | export const startMutationObserver = (): void => { 664 | const root = getConfig()?.root || getBody(); 665 | 666 | if (!root) return; 667 | 668 | const mutationObserver: any = new MutationObserver((recordList) => { 669 | const recordListLen = recordList?.length; 670 | 671 | if (!recordListLen) return; 672 | 673 | for (let i = 0; i < recordListLen; i++) { 674 | const { addedNodes = [], attributeName = '', type: moType = '' }: any = recordList?.[i] || {}; 675 | 676 | if (moType === 'attributes') { 677 | // 属性变化时如果埋点属性更新,则格式化 678 | if (attributeName === ATTRIBUTE_KEY) { 679 | pushBuffer(recordList?.[i]?.target); 680 | } 681 | } else { 682 | // 有新增节点时,将发生变化的节点推入缓冲区 683 | if (addedNodes?.length) { 684 | for (let idx = 0; idx < addedNodes.length; idx++) { 685 | pushBuffer(addedNodes[idx]); 686 | } 687 | } 688 | } 689 | } 690 | 691 | taskProcessing(); 692 | }); 693 | 694 | pushBuffer(root, taskProcessing); 695 | mutationObserver.observe(root, { 696 | childList: true, // 观察目标节点的子节点的新增和删除 697 | attributes: true, // 观察目标节点的属性节点(新增或删除了某个属性,以及某个属性的属性值发生了变化) 698 | subtree: true, // 观察目标节点的所有后代节点(观察目标节点所包含的整棵 DOM 树上的上述三种节点变化) 699 | }); 700 | }; 701 | 702 | // 获取当前所有已曝光节点,批量打开始或结束曝光(用于页面被遮盖或进入后台前后的时机) 703 | export const triggerBatchExpose = (isGoExpose: boolean): void => { 704 | const objects = getObjects(); 705 | 706 | for (const key in objects) { 707 | const { isExpose, target: $object } = objects?.[key] || {}; 708 | 709 | if (!(isExpose && $object)) continue; 710 | 711 | const eventName = getExposeEventName($object, isGoExpose); 712 | const nowTime = Date.now(); 713 | 714 | // 心跳开启时,要屏蔽正常的_pd 715 | if (!isGoExpose && isHeartbeatPd(eventName)) continue; 716 | 717 | setObject($object, isGoExpose ? { startTime: nowTime } : { endTime: nowTime }); 718 | postponeCallback(() => { 719 | createAndPushLog($object, eventName, { isBatch: true }); 720 | }, { isIdle: false }); 721 | } 722 | }; 723 | 724 | export const triggerHeartbeatLogger = () => { 725 | const objects = getObjects(); 726 | 727 | for (const key in objects) { 728 | const { isUseHeartbeat } = getConfig(); 729 | const { isExpose, target: $object } = objects?.[key] || {}; 730 | 731 | if (!(isUseHeartbeat && isExpose && $object)) continue; 732 | 733 | const { isPage } = getDataLog($object); 734 | const nowTime = Date.now(); 735 | 736 | if (!isPage) continue; 737 | 738 | setObject($object, { endTime: nowTime }); 739 | postponeCallback(() => { 740 | createAndPushLog($object, EVENT_NAME_MAP.pd, { 741 | publicParams: { is_beat: 1 }, 742 | isBatch: true, 743 | isHeartbeat: true, 744 | }); 745 | }, { isIdle: false }); 746 | } 747 | }; 748 | 749 | // 自定义埋点方法 750 | export const triggerCustomLogger = ($object: IHtmlNode, options: ITriggerCustomLoggerParamsOptions): Promise => { 751 | if (!($object && isKeyNode($object) && options?.event)) return Promise.resolve({}); 752 | 753 | const { event: eventName, params, isForce, useForRefer }: any = isObject(options) ? options : {}; 754 | 755 | // 心跳开启时,要屏蔽正常的_pd 756 | if (isHeartbeatPd(eventName)) return Promise.resolve({}); 757 | 758 | // 手动打开始曝光、结束曝光时要更新时间 759 | if ([EVENT_NAME_MAP.pv, EVENT_NAME_MAP.pd, EVENT_NAME_MAP.ev, EVENT_NAME_MAP.ed].includes(eventName)) { 760 | // 更新开始曝光、结束曝光时间 761 | const isGoExpose = [EVENT_NAME_MAP.pv, EVENT_NAME_MAP.ev].includes(eventName); 762 | const nowTime = Date.now(); 763 | 764 | setObject($object, isGoExpose ? { startTime: nowTime } : { endTime: nowTime }); 765 | } 766 | 767 | const { isPrevent, _plist, _elist, _spm, _scm, params: logParams } = createAndPushLog($object, eventName, { 768 | publicUseForRefer: useForRefer, 769 | publicParams: params, 770 | isForce: isForce !== false, 771 | }); 772 | const { g_dprefer, _sessid } = logParams || {}; 773 | const jumprefer = JSON.stringify({ 774 | spm: isString(_spm) ? _spm : '', 775 | scm: isString(_scm) ? _scm : '', 776 | }); 777 | 778 | if (isBoolean(isPrevent) && isPrevent) return Promise.resolve({}); 779 | 780 | return Promise.resolve({ 781 | _plist, 782 | _elist, 783 | _spm, 784 | _scm, 785 | _sessid, 786 | g_dprefer, 787 | jumprefer, 788 | }); 789 | }; 790 | 791 | // 获取曝光观察者对象 792 | export const getIntersectionObserver = (): any => { 793 | const EXPOSE_THRESHOLD = 0; 794 | 795 | return new IntersectionObserver((entries?: any[]) => { 796 | if (!isArray(entries)) return; 797 | 798 | const entriesLen = entries?.length || 0; 799 | 800 | for (let i = 0; i < entriesLen; i++) { 801 | const entry = entries[i]; 802 | 803 | const { target: $object, isIntersecting, intersectionRatio } = entry || {}; 804 | const { nodeId, isExpose } = getObject($object); 805 | 806 | if (nodeId) { 807 | // 对象记录已曝光,当前未曝光、或当前已曝光但未达曝光阈值,打结束曝光 808 | if (isExpose && ((isIntersecting && intersectionRatio < EXPOSE_THRESHOLD) || !isIntersecting)) { 809 | updateObjectAndExpose(entry, false); 810 | } 811 | 812 | // 对象记录未曝光,当前未已曝光且已达曝光阈值,打开始曝光 813 | if (!isExpose && isIntersecting && intersectionRatio >= EXPOSE_THRESHOLD) { 814 | updateObjectAndExpose(entry, true); 815 | } 816 | } 817 | } 818 | }, { 819 | threshold: EXPOSE_THRESHOLD, 820 | }); 821 | }; 822 | 823 | // 获取sessId 824 | export const getSessId = (): string => `${Date.now()}#${getRandomString(3, true)}`; 825 | 826 | // 监听点击事件 827 | export const listeningClickEvent = (): void => { 828 | try { 829 | getWindow('document')?.addEventListener?.('click', (e: any) => { 830 | const eventName = EVENT_NAME_MAP.ec; 831 | const target = e?.nativeEvent?.target || e?.target; 832 | const $object = findKeyNode(target, { eventName, objectType: 'element' }); 833 | 834 | triggerCustomLogger($object, { event: eventName, isForce: false }).catch(() => {}); 835 | }, false); 836 | } catch (error) { 837 | callHook('error', '代理点击事件:失败', { 838 | code: 'init', error, initConfig: getConfig(), 839 | }); 840 | } 841 | }; 842 | --------------------------------------------------------------------------------