├── .eslintignore ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── demo ├── index-plugin.js ├── index.js └── package.json ├── package.json ├── rollup.config.js ├── rollup.config.polyfill.js ├── src ├── collect │ ├── collect.ts │ ├── config.ts │ ├── constant.ts │ ├── event.ts │ ├── hooktype.ts │ ├── session.ts │ └── token.ts ├── entry │ ├── entry-base.ts │ └── entry.ts ├── plugin │ ├── ab │ │ ├── ab.ts │ │ ├── layer.ts │ │ └── load.ts │ ├── check │ │ └── check.ts │ ├── debug │ │ └── debug.ts │ ├── duration │ │ └── duration.ts │ ├── heartbeat │ │ └── heartbeat.ts │ ├── monitor │ │ └── index.ts │ ├── profile │ │ └── profile.ts │ ├── route │ │ └── route.ts │ ├── stay │ │ ├── alive.ts │ │ ├── close.ts │ │ └── stay.ts │ ├── store │ │ └── store.ts │ ├── track │ │ ├── config.ts │ │ ├── dom.ts │ │ ├── element.ts │ │ ├── event.ts │ │ ├── exposure │ │ │ ├── index.ts │ │ │ ├── intersection.ts │ │ │ └── observer.ts │ │ ├── index.ts │ │ ├── listener.ts │ │ ├── load.ts │ │ ├── node.ts │ │ ├── path.ts │ │ ├── request.ts │ │ ├── session.ts │ │ └── type.ts │ └── verify │ │ └── verify_h5.ts └── util │ ├── client.ts │ ├── fetch.ts │ ├── hook.ts │ ├── jsbridge.js │ ├── local.js │ ├── log.ts │ ├── postMessage.ts │ ├── request.ts │ ├── sm2crypto.ts │ ├── storage.ts │ ├── tool.ts │ ├── url-polyfill.js │ └── utm.ts ├── tsconfig.json └── types └── types.d.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | /__tests__/e2e/**/*.js 2 | /__tests__/Extra/**/*.js 3 | /src/autoTrack/autoTrack/**/* 4 | /src/newTrack/autoTrack/* 5 | /rollup.autoTrack.config.js 6 | /example/**/* 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | #最新添加 3 | /core 4 | /es 5 | /lib 6 | package-lock.json 7 | os.json 8 | .DS_Store 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Beijing Volcano Engine Technology Co., Ltd. 2 | 3 | The DataRangers SDK was developed by Beijing Volcanoengine Technology Ltd. (hereinafter “Volcanoengine”). Any copyright or patent right is owned by and proprietary material of the Volcanoengine. 4 | 5 | DataRangers SDK is available under the Volcanoengine and licensed under the commercial license. Customers can contact service@volcengine.com for commercial licensing options. Here is also a link to subscription services agreement: https://www.volcengine.com/docs/6285/69647 6 | 7 | Without Volcanoengine's prior written permission, any use of DataRangers SDK, in particular any use for commercial purposes, is prohibited. This includes, without limitation, incorporation in a commercial product, use in a commercial service, or production of other artefacts for commercial purposes. 8 | 9 | Without Volcanoengine's prior written permission, the DataRangers SDK may not be reproduced, modified and/or made available in any form to any third party. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.zh-CN.md) 2 | 3 | # `DataRangers SDK - javascript` 4 | ## Sample 5 | 6 | ```javascript 7 | npm install 8 | npm run build 9 | ``` 10 | 11 | ## Sample 12 | 13 | ### 1. Initialize the SDK in your javascript file 14 | 15 | ```javascript 16 | const SDK = require('@datarangers/sdk-javascript'); 17 | 18 | SDK.init({ 19 | app_id: 1234, // Replace it with the "APP_ID" 20 | channel: 'cn', // Replace it with your report channel 21 | log: true, // Whether to print the log 22 | }); 23 | 24 | SDK.config({ 25 | username: 'xxx', // when you want report username with event 26 | }); 27 | 28 | SDK.start(); // Setup complete and now events can be sent. 29 | 30 | ``` 31 | 32 | ### 2. Report custom user behavior events 33 | 34 | ```javascript 35 | // Take reporting the "video clicked" behavior of users for example 36 | SDK.event('play_video', { 37 | title: 'Here is the video title', 38 | }); 39 | ``` 40 | 41 | ### 3. Report the unique identifier of the currently logged in user 42 | 43 | ```javascript 44 | // Set "user_unique_id" after a user logs in and the user's unique identifier is retrieved. 45 | SDK.config({ 46 | user_unique_id: 'zhangsan', // Unique user identifier 47 | }); 48 | ``` 49 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](./README.md) 2 | # `DataRangers SDK - Web端` 3 | ## 构建SDK 4 | 5 | ```javascript 6 | npm install 7 | npm run build 8 | ``` 9 | 10 | ## 使用方式 11 | 12 | ### 1. 在你的js文件中初始化SDK 13 | 14 | ```javascript 15 | const SDK = require('@datarangers/sdk-javascript'); 16 | 17 | SDK.init({ 18 | app_id: 1234, // 替换成你申请的 "APP_ID" 19 | channel: 'cn', // 选择你要上报的区域,cn: 国内 sg: 新加坡 va:美东 20 | log: true, // 是否打印日志 21 | }); 22 | 23 | SDK.config({ 24 | username: 'xxx', // 你想要上报一个username的公共属性 25 | }); 26 | 27 | SDK.start(); // 初始化完成,事件开始上报 28 | 29 | ``` 30 | 31 | ### 2. 上报自定义用户事件 32 | 33 | ```javascript 34 | // 比如上报一个'play_video'视频播放的事件 35 | SDK.event('play_video', { 36 | title: 'Here is the video title', 37 | }); 38 | ``` 39 | 40 | ### 3. 使用当前登录用户的信息做为唯一标识来进行上报 41 | 42 | ```javascript 43 | // 可以在用户登录后获取带有唯一性的标识来设置给user_unique_id 44 | SDK.config({ 45 | user_unique_id: 'zhangsan', // 用户唯一标识,可以是你的系统登录用户id 46 | }); 47 | ``` 48 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const presets = [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: [ 8 | 'last 7 versions', 9 | 'ie >= 8', 10 | 'ios >= 8', 11 | 'android >= 4.0', 12 | ].join(','), 13 | useBuiltIns: 'false', 14 | corejs: { version: 3, proposals: true }, 15 | modules: false, // 交给rollup处理模块化 https://babeljs.io/docs/en/babel-preset-env# 16 | loose: true, // 非严格es6 17 | debug: false 18 | }, 19 | ], 20 | '@babel/preset-typescript', 21 | '@babel/preset-flow', 22 | ]; 23 | 24 | const plugins = [ 25 | ['@babel/plugin-proposal-class-properties', { loose: false }], 26 | ]; 27 | 28 | module.exports = { presets, plugins } -------------------------------------------------------------------------------- /demo/index-plugin.js: -------------------------------------------------------------------------------- 1 | // 使用插件 2 | import {Collector} from '@datarangers/sdk-javascript/es/index-base.min.js'; 3 | import Ab from '@datarangers/sdk-javascript/es/plugin/ab.js'; 4 | 5 | const sdk = new Collector('sdk') 6 | sdk.usePlugin(Ab, 'ab') 7 | 8 | sdk.init({ 9 | app_id: 1234, 10 | channel: 'cn', 11 | log: true, 12 | enable_ab_test: true 13 | }) 14 | 15 | sdk.config({ 16 | user_unique_id: 'test_user' 17 | }) 18 | 19 | sdk.start() 20 | 21 | 22 | // 曝光实验 23 | sdk.getVar('abkey', 'defaulyValue', (res) => { 24 | console.log(res) 25 | }) 26 | 27 | sdk.event('test_event', { 28 | name: 'ssss' 29 | }) -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | // 正常使用 2 | import sdk from '@datarangers/sdk-javascript'; 3 | 4 | sdk.init({ 5 | app_id: 1234, 6 | channel: 'cn', 7 | log: true 8 | }) 9 | 10 | sdk.config({ 11 | user_unique_id: 'test_user' 12 | }) 13 | 14 | sdk.start() 15 | 16 | 17 | sdk.event('test_event', { 18 | name: 'ssss' 19 | }) -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@datarangers/sdk-javascript": "^5.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datarangers/sdk-javascript", 3 | "version": "0.0.7", 4 | "scripts": { 5 | "nclean": "rimraf es lib core", 6 | "build": "export NODE_ENV=production && npm run nclean && npx rollup -c rollup.config.js", 7 | "build-core": "export NODE_ENV=production && npx rollup -c rollup.config.polyfill.js" 8 | }, 9 | "main": "lib/index.min.js", 10 | "module": "es/index.min.js", 11 | "types": "./types/types.d.ts", 12 | "license": "ISC", 13 | "dependencies": { 14 | "js-cookie": "^3.0.1", 15 | "sm-crypto": "^0.3.12" 16 | }, 17 | "files": [ 18 | "es", 19 | "lib", 20 | "types", 21 | "core" 22 | ], 23 | "publishConfig": { 24 | "registry": "https://registry.npmjs.org" 25 | }, 26 | "devDependencies": { 27 | "@babel/cli": "^7.17.10", 28 | "@babel/core": "^7.18.0", 29 | "@babel/plugin-proposal-class-properties": "^7.17.12", 30 | "@babel/plugin-transform-arrow-functions": "^7.17.12", 31 | "@babel/plugin-transform-classes": "^7.17.12", 32 | "@babel/plugin-transform-destructuring": "^7.18.0", 33 | "@babel/plugin-transform-parameters": "^7.17.12", 34 | "@babel/plugin-transform-shorthand-properties": "^7.16.7", 35 | "@babel/plugin-transform-spread": "^7.7.4", 36 | "@babel/polyfill": "^7.12.1", 37 | "@babel/preset-env": "^7.6.3", 38 | "@babel/preset-flow": "^7.17.12", 39 | "@babel/preset-typescript": "^7.17.12", 40 | "@babel/types": "^7.18.0", 41 | "@typescript-eslint/eslint-plugin": "^5.25.0", 42 | "@typescript-eslint/parser": "^5.25.0", 43 | "babel-eslint": "^10.1.0", 44 | "babel-loader": "^8.2.5", 45 | "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", 46 | "babel-plugin-transform-remove-strict-mode": "^0.0.2", 47 | "colors": "^1.4.0", 48 | "core-js": "^3.22.6", 49 | "cross-env": "^7.0.3", 50 | "escape-string-regexp": "^5.0.0", 51 | "eslint": "^8.16.0", 52 | "eslint-plugin-import": "^2.26.0", 53 | "eslint-plugin-typescript": "^0.14.0", 54 | "rimraf": "^3.0.2", 55 | "rollup": "^2.74.1", 56 | "rollup-plugin-babel": "^4.4.0", 57 | "rollup-plugin-cleanup": "^3.2.1", 58 | "rollup-plugin-commonjs": "^10.1.0", 59 | "rollup-plugin-filesize": "^9.1.2", 60 | "rollup-plugin-json": "^4.0.0", 61 | "rollup-plugin-node-resolve": "^5.2.0", 62 | "rollup-plugin-progress": "^1.1.2", 63 | "rollup-plugin-replace": "^2.2.0", 64 | "rollup-plugin-strip": "^1.2.2", 65 | "rollup-plugin-terser": "^7.0.2", 66 | "rollup-plugin-typescript2": "^0.31.2", 67 | "ts-lint": "^4.5.1", 68 | "tslib": "^2.1.0", 69 | "typescript": "^3.2.2", 70 | "typescript-eslint-parser": "^22.0.0" 71 | } 72 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import replace from 'rollup-plugin-replace'; 3 | import json from 'rollup-plugin-json'; 4 | import cleanup from 'rollup-plugin-cleanup'; 5 | import filesize from 'rollup-plugin-filesize'; 6 | import progress from 'rollup-plugin-progress'; 7 | import resolve from 'rollup-plugin-node-resolve'; 8 | import commonjs from 'rollup-plugin-commonjs'; 9 | import { terser } from 'rollup-plugin-terser'; 10 | import strip from 'rollup-plugin-strip'; 11 | import ts from 'rollup-plugin-typescript2'; 12 | 13 | const exec = require('child_process').exec; 14 | exec('rm -rf ./output', function(err, stdout, stderr) { 15 | if (err) { 16 | console.log(err.message); 17 | } 18 | console.log(stdout); 19 | console.log(stderr); 20 | }); 21 | 22 | const commonPlugins = [ 23 | strip({ 24 | debugger: true, 25 | functions: ['assert.*', 'debug', 'alert'], 26 | sourceMap: true, 27 | }), 28 | json(), 29 | filesize(), 30 | progress(), 31 | cleanup(), 32 | terser(), 33 | ]; 34 | 35 | const replaceOptions = { 36 | 'process.env.SDK_TYPE': 'npm', 37 | 'process.env.SDK_TARGET': 'tob', 38 | }; 39 | const logSDKCommonPlugins = [ 40 | replace({ 41 | delimiters: ['', ''], 42 | values: replaceOptions, 43 | }), 44 | ts(), 45 | babel({ 46 | exclude: 'node_modules/**', 47 | extensions: ['.js', '.ts'], 48 | }), 49 | resolve(), // 常规配套使用 50 | commonjs(), 51 | ...commonPlugins, 52 | ]; 53 | 54 | const logBaseSDKEntry = [ 55 | { 56 | input: 'src/entry/entry-base.ts', 57 | output: [ 58 | { 59 | file: 'lib/index-base<%insert%>.min.js', 60 | format: 'cjs', 61 | }, 62 | ], 63 | }, 64 | { 65 | input: 'src/entry/entry-base.ts', 66 | output: [ 67 | { 68 | file: 'es/index-base<%insert%>.min.js', 69 | format: 'es', 70 | }, 71 | ], 72 | } 73 | ] 74 | 75 | const logFullSDKEntry = [ 76 | { 77 | input: 'src/entry/entry.ts', 78 | output: [ 79 | { 80 | file: 'lib/index<%insert%>.min.js', 81 | format: 'cjs', 82 | }, 83 | ], 84 | }, 85 | { 86 | input: 'src/entry/entry.ts', 87 | output: [ 88 | { 89 | file: 'es/index<%insert%>.min.js', 90 | format: 'es', 91 | }, 92 | ], 93 | } 94 | ] 95 | 96 | const logPluginSDKEntry = [ 97 | { 98 | input: 'src/plugin/ab/ab.ts', 99 | output: [ 100 | { 101 | file: 'lib/plugin/ab.js', 102 | format: 'cjs', 103 | }, 104 | ], 105 | }, 106 | { 107 | input: 'src/plugin/ab/ab.ts', 108 | output: [ 109 | { 110 | file: 'es/plugin/ab.js', 111 | format: 'es', 112 | }, 113 | ], 114 | }, 115 | { 116 | input: 'src/plugin/stay/stay.ts', 117 | output: [ 118 | { 119 | file: 'lib/plugin/stay.js', 120 | format: 'cjs', 121 | }, 122 | ], 123 | }, 124 | { 125 | input: 'src/plugin/stay/stay.ts', 126 | output: [ 127 | { 128 | file: 'es/plugin/stay.js', 129 | format: 'es', 130 | }, 131 | ], 132 | }, 133 | { 134 | input: 'src/plugin/track/index.ts', 135 | output: [ 136 | { 137 | file: 'lib/plugin/autotrack.js', 138 | format: 'cjs', 139 | }, 140 | ], 141 | }, 142 | { 143 | input: 'src/plugin/track/index.ts', 144 | output: [ 145 | { 146 | file: 'es/plugin/autotrack.js', 147 | format: 'es', 148 | }, 149 | ], 150 | }, 151 | { 152 | input: 'src/plugin/route/route.ts', 153 | output: [ 154 | { 155 | file: 'es/plugin/route.js', 156 | format: 'es', 157 | }, 158 | ], 159 | }, 160 | { 161 | input: 'src/plugin/route/route.ts', 162 | output: [ 163 | { 164 | file: 'lib/plugin/route.js', 165 | format: 'cjs', 166 | }, 167 | ], 168 | }, 169 | { 170 | input: 'src/plugin/store/store.ts', 171 | output: [ 172 | { 173 | file: 'es/plugin/store.js', 174 | format: 'es', 175 | }, 176 | ], 177 | }, 178 | { 179 | input: 'src/plugin/store/store.ts', 180 | output: [ 181 | { 182 | file: 'lib/plugin/store.js', 183 | format: 'cjs', 184 | }, 185 | ], 186 | }, 187 | { 188 | input: 'src/plugin/duration/duration.ts', 189 | output: [ 190 | { 191 | file: 'es/plugin/duration.js', 192 | format: 'es', 193 | }, 194 | ], 195 | }, 196 | { 197 | input: 'src/plugin/duration/duration.ts', 198 | output: [ 199 | { 200 | file: 'lib/plugin/duration.js', 201 | format: 'cjs', 202 | }, 203 | ], 204 | }, 205 | ] 206 | 207 | /** 208 | * 塞入额外的replace。 209 | * 返回 npm 配置对象 210 | * @param {*} replaceObj 211 | */ 212 | function getBaseBundlesWithReplace(replaceObj, outputName) { 213 | const copyLogBaseSDKEntry = JSON.parse(JSON.stringify(logBaseSDKEntry)) 214 | return [ 215 | copyLogBaseSDKEntry 216 | ].map(mainConfig => mainConfig.map((outputConfig) => { 217 | // 替换输出的文件名 218 | outputConfig.output.forEach((item) => { 219 | item.file = item.file.replace('<%insert%>', outputName ? `-${outputName}` : ''); 220 | }); 221 | // 添加replace配置 222 | outputConfig.plugins = [ 223 | replace({ 224 | delimiters: ['', ''], 225 | values: replaceObj, 226 | }), 227 | ...logSDKCommonPlugins, 228 | ]; 229 | return outputConfig; 230 | })); 231 | } 232 | 233 | function getFullBundlesWithReplace(replaceObj, outputName) { 234 | const copyLogFullSDKEntry = JSON.parse(JSON.stringify(logFullSDKEntry)) 235 | return [ 236 | copyLogFullSDKEntry 237 | ].map(mainConfig => mainConfig.map((outputConfig) => { 238 | // 替换输出的文件名 239 | outputConfig.output.forEach((item) => { 240 | item.file = item.file.replace('<%insert%>', outputName ? `-${outputName}` : ''); 241 | }); 242 | // 添加replace配置 243 | outputConfig.plugins = [ 244 | replace({ 245 | delimiters: ['', ''], 246 | values: replaceObj, 247 | }), 248 | ...logSDKCommonPlugins, 249 | ]; 250 | return outputConfig; 251 | })); 252 | } 253 | 254 | function getPluginBundlesWithReplace(replaceObj, outputName) { 255 | const copyLogPluginSDKEntry = JSON.parse(JSON.stringify(logPluginSDKEntry)) 256 | return [ 257 | copyLogPluginSDKEntry 258 | ].map(mainConfig => mainConfig.map((outputConfig) => { 259 | // 替换输出的文件名 260 | outputConfig.output.forEach((item) => { 261 | item.file = item.file.replace('<%insert%>', outputName ? `-${outputName}` : ''); 262 | }); 263 | // 添加replace配置 264 | outputConfig.plugins = [ 265 | replace({ 266 | delimiters: ['', ''], 267 | values: replaceObj, 268 | }), 269 | ...logSDKCommonPlugins, 270 | ]; 271 | return outputConfig; 272 | })); 273 | } 274 | 275 | export default [].concat( 276 | ...getBaseBundlesWithReplace({ 277 | '/**@@SDK': '//', 278 | '@@SDK*/': '//', 279 | }, 280 | '', 281 | ), 282 | 283 | ...getFullBundlesWithReplace({ 284 | '/**@@SDK': '//', 285 | '@@SDK*/': '//', 286 | }, 287 | '', 288 | ), 289 | 290 | ...getPluginBundlesWithReplace({ 291 | '/**@@SDK': '//', 292 | '@@SDK*/': '//', 293 | }, 294 | '', 295 | ), 296 | ); 297 | -------------------------------------------------------------------------------- /rollup.config.polyfill.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import replace from 'rollup-plugin-replace'; 3 | import json from 'rollup-plugin-json'; 4 | import cleanup from 'rollup-plugin-cleanup'; 5 | import filesize from 'rollup-plugin-filesize'; 6 | import progress from 'rollup-plugin-progress'; 7 | import resolve from 'rollup-plugin-node-resolve'; 8 | import commonjs from 'rollup-plugin-commonjs'; 9 | import { terser } from 'rollup-plugin-terser'; 10 | import strip from 'rollup-plugin-strip'; 11 | import ts from 'rollup-plugin-typescript2'; 12 | 13 | import { version, description } from './package.json' 14 | 15 | const exec = require('child_process').exec; 16 | exec('rm -rf ./core', function(err, stdout, stderr) { 17 | if (err) { 18 | console.log(err.message); 19 | } 20 | console.log(stdout); 21 | console.log(stderr); 22 | }); 23 | 24 | const commonPlugins = [ 25 | strip({ 26 | debugger: true, 27 | functions: ['assert.*', 'debug', 'alert'], 28 | sourceMap: true, 29 | }), 30 | json(), 31 | filesize(), 32 | progress(), 33 | cleanup(), 34 | terser(), 35 | ]; 36 | 37 | const replaceOptions = { 38 | 'process.env.SDK_VERSION': JSON.stringify(version), 39 | 'process.env.SDK_DESC': JSON.stringify(description), 40 | }; 41 | const logSDKCommonPlugins = [ 42 | replace({ 43 | delimiters: ['', ''], 44 | values: replaceOptions, 45 | }), 46 | ts(), 47 | babel({ 48 | exclude: ['node_modules/**','src/util/sizzle.js'], 49 | extensions: ['.js', '.ts'], 50 | presets: [ 51 | [ 52 | '@babel/preset-env', 53 | { 54 | targets: [ 55 | 'last 7 versions', 56 | 'ie >= 8', 57 | 'ios >= 8', 58 | 'android >= 4.0', 59 | ].join(','), 60 | useBuiltIns: 'usage', 61 | corejs: { version: 3, proposals: true }, 62 | modules: false, // 交给rollup处理模块化 https://babeljs.io/docs/en/babel-preset-env# 63 | loose: true, // 非严格es6 64 | debug: false 65 | }, 66 | ], 67 | '@babel/preset-typescript', 68 | '@babel/preset-flow', 69 | ] 70 | }), 71 | resolve(), // 常规配套使用 72 | commonjs(), 73 | ...commonPlugins, 74 | ]; 75 | 76 | const logFullSDKEntry = [ 77 | { 78 | input: 'src/entry/entry.ts', 79 | output: [ 80 | { 81 | file: 'core/lib/index<%insert%>.min.js', 82 | format: 'cjs', 83 | }, 84 | ], 85 | }, 86 | { 87 | input: 'src/entry/entry.ts', 88 | output: [ 89 | { 90 | file: 'core/es/index<%insert%>.min.js', 91 | format: 'es', 92 | }, 93 | ], 94 | } 95 | ] 96 | 97 | 98 | function getFullBundlesWithReplace(replaceObj, outputName) { 99 | const copyLogFullSDKEntry = JSON.parse(JSON.stringify(logFullSDKEntry)) 100 | return [ 101 | copyLogFullSDKEntry 102 | ].map(mainConfig => mainConfig.map((outputConfig) => { 103 | // 替换输出的文件名 104 | outputConfig.output.forEach((item) => { 105 | item.file = item.file.replace('<%insert%>', outputName ? `-${outputName}` : ''); 106 | }); 107 | // 添加replace配置 108 | outputConfig.plugins = [ 109 | replace({ 110 | delimiters: ['', ''], 111 | values: replaceObj, 112 | }), 113 | ...logSDKCommonPlugins, 114 | ]; 115 | return outputConfig; 116 | })); 117 | } 118 | 119 | export default [].concat( 120 | ...getFullBundlesWithReplace({ 121 | '/**@@SDK': '//', 122 | '@@SDK*/': '//', 123 | }, 124 | '', 125 | ) 126 | ); 127 | -------------------------------------------------------------------------------- /src/collect/config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Client from '../util/client' 4 | import Storage from '../util/storage' 5 | import { decodeUrl } from '../util/tool' 6 | import { SDK_VERSION, LOG_URL } from './constant' 7 | import EventCheck from '../plugin/check/check' 8 | import { DebuggerMesssge } from './hooktype'; 9 | 10 | 11 | const undef = undefined 12 | const date = new Date() 13 | const timeZoneMin = date.getTimezoneOffset() 14 | const timezone = parseInt(`${-timeZoneMin / 60}`, 10) 15 | const tz_offset = timeZoneMin * 60 16 | 17 | const WEBID_URL = '/webid' 18 | const TOB_URL = '/tobid' 19 | const REPORT_URL = '/list' 20 | const PROFILE_URL = '/profile/list' 21 | const EXPIRE_TIME = 60 * 60 * 1000 * 24 * 90 22 | 23 | export default class ConfigManager { 24 | collect: any 25 | envInfo: any 26 | evtParams: any 27 | filter: any 28 | reportErrorCallback: any 29 | initConfig: any 30 | sessionStorage: any 31 | localStorage: any 32 | storage: any 33 | configKey: string 34 | domain: string 35 | ab_version: any 36 | ab_cache: any 37 | is_first_time: boolean = true 38 | isLast: boolean 39 | configPersist: boolean = false 40 | eventCheck: any 41 | constructor(collect: any, initConfig: any) { 42 | this.initConfig = initConfig 43 | this.collect = collect 44 | const client = new Client(initConfig.app_id, initConfig.cookie_domain || '', initConfig.cookie_expire || EXPIRE_TIME) 45 | const commonInfo = client.init() 46 | this.eventCheck = new EventCheck(collect, initConfig) 47 | const firstKey = `__tea_cache_first_${initConfig.app_id}` 48 | this.configKey = `__tea_cache_config_${initConfig.app_id}` 49 | this.sessionStorage = new Storage(false, 'session') 50 | this.localStorage = new Storage(false, 'local') 51 | if (initConfig.configPersist) { 52 | this.configPersist = true 53 | this.storage = initConfig.configPersist === 1 ? this.sessionStorage : this.localStorage 54 | } 55 | const firstStatus = this.localStorage.getItem(firstKey) 56 | if (firstStatus && firstStatus == 1) { 57 | this.is_first_time = false 58 | } else { 59 | this.is_first_time = true 60 | this.localStorage.setItem(firstKey, '1') 61 | } 62 | this.envInfo = { 63 | user: { 64 | user_unique_id: undef, 65 | user_type: undef, 66 | user_id: undef, 67 | user_is_auth: undef, 68 | user_is_login: undef, 69 | device_id: undef, 70 | web_id: undef, 71 | ip_addr_id: undef, 72 | user_unique_id_type: undef, 73 | anonymous_id: undef, 74 | }, 75 | header: { 76 | app_id: undef, 77 | app_name: undef, 78 | app_install_id: undef, 79 | install_id: undef, 80 | app_package: undef, 81 | app_channel: undef, 82 | app_version: undef, 83 | ab_version: undef, 84 | os_name: commonInfo.os_name, 85 | os_version: commonInfo.os_version, 86 | device_model: commonInfo.device_model, 87 | ab_client: undef, 88 | traffic_type: undef, 89 | 90 | client_ip: undef, 91 | device_brand: undef, 92 | os_api: undef, 93 | access: undef, 94 | language: commonInfo.language, 95 | region: undef, 96 | app_language: undef, 97 | app_region: undef, 98 | creative_id: commonInfo.utm.creative_id, 99 | ad_id: commonInfo.utm.ad_id, 100 | campaign_id: commonInfo.utm.campaign_id, 101 | log_type: undef, 102 | rnd: undef, 103 | platform: commonInfo.platform, 104 | sdk_version: SDK_VERSION, 105 | sdk_lib: 'js', 106 | province: undef, 107 | city: undef, 108 | timezone: timezone, 109 | tz_offset: tz_offset, 110 | tz_name: undef, 111 | sim_region: undef, 112 | carrier: undef, 113 | resolution: `${commonInfo.screen_width}x${commonInfo.screen_height}`, 114 | browser: commonInfo.browser, 115 | browser_version: commonInfo.browser_version, 116 | referrer: commonInfo.referrer, 117 | referrer_host: commonInfo.referrer_host, 118 | 119 | width: commonInfo.screen_width, 120 | height: commonInfo.screen_height, 121 | screen_width: commonInfo.screen_width, 122 | screen_height: commonInfo.screen_height, 123 | 124 | utm_term: commonInfo.utm.utm_term, 125 | utm_content: commonInfo.utm.utm_content, 126 | utm_source: commonInfo.utm.utm_source, 127 | utm_medium: commonInfo.utm.utm_medium, 128 | utm_campaign: commonInfo.utm.utm_campaign, 129 | tracer_data: JSON.stringify(commonInfo.utm.tracer_data), 130 | custom: {}, 131 | 132 | wechat_unionid: undef, 133 | wechat_openid: undef, 134 | }, 135 | } 136 | this.ab_version = ''; 137 | this.evtParams = {}; 138 | // 事件处理函数 139 | this.reportErrorCallback = () => { } 140 | this.isLast = false; 141 | this.setCustom(commonInfo); 142 | this.initDomain(); 143 | this.initABData(); 144 | } 145 | initDomain() { 146 | const channelDomain = this.initConfig['channel_domain']; 147 | if (channelDomain) { 148 | this.domain = channelDomain; 149 | return; 150 | } 151 | let reportChannel = this.initConfig['channel']; 152 | this.domain = decodeUrl(LOG_URL[reportChannel]); 153 | } 154 | setDomain(domain: string) { 155 | this.domain = domain; 156 | } 157 | getDomain() { 158 | return this.domain; 159 | } 160 | initABData() { 161 | const abKey = `__tea_sdk_ab_version_${this.initConfig.app_id}`; 162 | let abCache = null; 163 | if (this.initConfig.ab_cross) { 164 | const ab_cookie = this.localStorage.getCookie(abKey, this.initConfig.ab_cookie_domain); 165 | abCache = ab_cookie ? JSON.parse(ab_cookie) : null; 166 | } else { 167 | abCache = this.localStorage.getItem(abKey); 168 | } 169 | this.setAbCache(abCache); 170 | } 171 | setAbCache(data: any) { 172 | this.ab_cache = data; 173 | } 174 | getAbCache() { 175 | return this.ab_cache; 176 | } 177 | clearAbCache() { 178 | this.ab_cache = {}; 179 | this.ab_version = ''; 180 | } 181 | setAbVersion(vid: string) { 182 | this.ab_version = vid 183 | } 184 | getAbVersion() { 185 | return this.ab_version 186 | } 187 | getUrl(type: string) { 188 | let report = '' 189 | switch (type) { 190 | case 'event': 191 | report = REPORT_URL 192 | break; 193 | case 'webid': 194 | report = WEBID_URL 195 | break; 196 | case 'tobid': 197 | report = TOB_URL 198 | break; 199 | case 'profile': 200 | report = PROFILE_URL 201 | } 202 | let query = '' 203 | if (this.initConfig.caller) { 204 | query = `?sdk_version=${SDK_VERSION}&sdk_name=web&app_id=${this.initConfig.app_id}&caller=${this.initConfig.caller}` 205 | } 206 | if (this.initConfig.enable_encryption && type === 'event' && this.initConfig.encryption_type !== 'sm') { 207 | query = this.initConfig.caller ? `${query}&encryption=1` : `${query}?encryption=1` 208 | } 209 | return this.initConfig.report_url ? `${this.initConfig.report_url}${query}` : `${this.getDomain()}${report}${query}` 210 | } 211 | setCustom(commonInfo) { 212 | if (commonInfo && commonInfo.latest_data && commonInfo.latest_data.isLast) { 213 | delete commonInfo.latest_data['isLast'] 214 | this.isLast = true 215 | for (let key in commonInfo.latest_data) { 216 | this.envInfo.header.custom[key] = commonInfo.latest_data[key] 217 | } 218 | } 219 | } 220 | set(info: any) { 221 | Object.keys(info).forEach((key) => { 222 | if (info[key] === undefined || info[key] === null) { 223 | this.delete(key) 224 | } 225 | try { 226 | this.eventCheck.calculate(key, 'config') 227 | } catch (e) { } 228 | if (key === 'traffic_type' && this.isLast) { 229 | this.envInfo.header.custom['$latest_traffic_source_type'] = info[key] 230 | } 231 | if (key === 'evtParams') { 232 | this.evtParams = { 233 | ...(this.evtParams || {}), 234 | ...(info.evtParams || {}), 235 | }; 236 | } else if (key === '_staging_flag') { 237 | this.evtParams = { 238 | ...(this.evtParams || {}), 239 | _staging_flag: info._staging_flag, 240 | }; 241 | } else if (key === 'reportErrorCallback' && typeof info[key] === 'function') { 242 | this.reportErrorCallback = info[key] 243 | } else { 244 | let scope = '' 245 | let scopeKey = '' 246 | if (key.indexOf('.') > -1) { 247 | const tmp = key.split('.') 248 | scope = tmp[0] 249 | scopeKey = tmp[1] 250 | } 251 | if (scope) { 252 | if (scope === 'user' || scope === 'header') { 253 | this.envInfo[scope][scopeKey] = info[key] 254 | } else { 255 | this.envInfo.header.custom[scopeKey] = info[key] 256 | } 257 | } else if (Object.hasOwnProperty.call(this.envInfo.user, key)) { 258 | if (['user_type', 'ip_addr_id'].indexOf(key) > -1) { 259 | this.envInfo.user[key] = info[key] ? Number(info[key]) : info[key] 260 | } else if (['user_id', 'web_id', 'user_unique_id', 'user_unique_id_type', 'anonymous_id'].indexOf(key) > -1) { 261 | this.envInfo.user[key] = info[key] ? String(info[key]) : info[key] 262 | } else if (['user_is_auth', 'user_is_login'].indexOf(key) > -1) { 263 | this.envInfo.user[key] = Boolean(info[key]) 264 | } else if (key === 'device_id') { 265 | this.envInfo.user[key] = info[key] 266 | } 267 | } else if (Object.hasOwnProperty.call(this.envInfo.header, key)) { 268 | this.envInfo.header[key] = info[key] 269 | } else { 270 | this.envInfo.header.custom[key] = info[key] 271 | } 272 | } 273 | }) 274 | } 275 | 276 | get(key) { 277 | try { 278 | if (key) { 279 | if (key === 'evtParams') { 280 | return this.evtParams 281 | } else if (key === 'reportErrorCallback') { 282 | return this[key] 283 | } else if (Object.hasOwnProperty.call(this.envInfo.user, key)) { 284 | return this.envInfo.user[key] 285 | } else if (Object.hasOwnProperty.call(this.envInfo.header, key)) { 286 | return this.envInfo.header[key] 287 | } else { 288 | return JSON.parse(JSON.stringify(this.envInfo[key])) 289 | } 290 | } else { 291 | return JSON.parse(JSON.stringify(this.envInfo)) 292 | } 293 | } catch (e) { 294 | console.log('get config stringify error ') 295 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 296 | } 297 | } 298 | setStore(config: any) { 299 | try { 300 | if (!this.configPersist) return; 301 | const _cache = this.storage.getItem(this.configKey) || {} 302 | if (_cache && Object.keys(config).length) { 303 | const newCache = Object.assign(config, _cache) 304 | this.storage.setItem(this.configKey, newCache) 305 | } 306 | } catch (e) { 307 | console.log('setStore error') 308 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 309 | } 310 | } 311 | getStore() { 312 | try { 313 | if (!this.configPersist) return null; 314 | const _cache = this.storage.getItem(this.configKey) || {} 315 | if (_cache && Object.keys(_cache).length) { 316 | return _cache 317 | } else { 318 | return null 319 | } 320 | } catch (e) { 321 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 322 | return null 323 | } 324 | } 325 | delete(key: string) { 326 | try { 327 | if (!this.configPersist) return; 328 | const _cache = this.storage.getItem(this.configKey) || {} 329 | if (_cache && Object.hasOwnProperty.call(_cache, key)) { 330 | delete _cache[key] 331 | this.storage.setItem(this.configKey, _cache) 332 | } 333 | } catch (e) { 334 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 335 | console.log('delete error') 336 | } 337 | } 338 | } -------------------------------------------------------------------------------- /src/collect/constant.ts: -------------------------------------------------------------------------------- 1 | 2 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 3 | 4 | interface I_LOG_URL { 5 | cn: string 6 | sg: string; 7 | va: string; 8 | } 9 | let LOG: I_LOG_URL 10 | 11 | LOG = { 12 | cn: '1fz22z22z1nz21z4mz4bz4bz1kz1az21z4az24z1mz1jz1az1cz18z1nz1nz1jz1mz1ez4az1az1mz1k', 13 | va: '1fz22z22z1nz21z4mz4bz4bz1kz1az21z4az1gz22z1mz19z21z1lz21z21z1bz1iz4az1az1mz1k', 14 | sg: '1fz22z22z1nz21z4mz4bz4bz1kz1az21z4az22z1mz19z21z1lz21z21z1bz1iz4az1az1mz1k', 15 | } 16 | 17 | // CN: https://mcs.volceapplog.com VA: https://mcs.itobsnssdk.com SG: https://mcs.tobsnssdk.com 18 | 19 | export const LOG_URL = LOG 20 | 21 | export const SDK_VERSION = '0.0.7' 22 | 23 | let SDK_USE_TYPE = 'npm' 24 | 25 | 26 | export const SDK_TYPE = SDK_USE_TYPE 27 | 28 | export const AB_DOMAINS = { 29 | cn: '1fz22z22z1nz21z4mz4bz4bz22z1mz19z1jz1mz1ez4az1az22z1mz19z21z1lz21z21z1bz1iz4az1az1mz1k', 30 | va: '1fz22z22z1nz21z4mz4bz4bz22z1mz19z1jz1mz1ez4az1gz22z1mz19z21z1lz21z21z1bz1iz4az1az1mz1k', 31 | sg: '1fz22z22z1nz21z4mz4bz4bz22z1mz19z1jz1mz1ez4az22z1mz19z21z1lz21z21z1bz1iz4az1az1mz1k', 32 | } 33 | 34 | 35 | export const VISUAL_EDITOR_RANGERS = 'https://lf3-data.volccdn.com/obj/data-static/log-sdk/collect/visual-editor-rangers.js' 36 | export const VISUAL_AB_CORE = 'https://lf3-data.volccdn.com/obj/data-static/log-sdk/collect/visual-ab-core.js' 37 | export const VISUAL_AB_LOADER = 'https://lf3-data.volccdn.com/obj/data-static/log-sdk/collect/visual-ab-loader.js' 38 | export const HOT_PIC_URL = 'https://lf3-data.volccdn.com/obj/data-static/log-sdk/collect/heatmap-core' 39 | export const VISUAL_URL_INSPECTOR = 'https://lf3-data.volccdn.com/obj/data-static/log-sdk/collect/tester-event-inspector' -------------------------------------------------------------------------------- /src/collect/event.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Types from './hooktype'; 4 | import { IInitParam } from '../../types/types' 5 | import Storage from '../util/storage' 6 | import request from '../util/request' 7 | import { beforePageUnload, encodeBase64, decodeBase64 } from '../util/tool' 8 | import { DebuggerMesssge } from './hooktype'; 9 | import EventCheck from '../plugin/check/check'; 10 | 11 | type TEvent = any; 12 | export default class Event { 13 | collect: any 14 | config: IInitParam 15 | configManager: any 16 | eventKey: string 17 | beconKey: string 18 | abKey: string 19 | cacheStorgae: any 20 | localStorage: any 21 | reportTimeout: any 22 | maxReport: number 23 | reportTime: number 24 | timeout: number 25 | eventLimit: number = 50 26 | reportUrl: string 27 | eventCache: TEvent[] = [] 28 | beconEventCache: TEvent[] = [] 29 | eventCheck: any 30 | refer_key: string 31 | apply(collect: any, config: IInitParam) { 32 | this.collect = collect 33 | this.config = config 34 | this.configManager = collect.configManager 35 | this.cacheStorgae = new Storage(true) 36 | this.localStorage = new Storage(false) 37 | this.eventCheck = new EventCheck(collect, config) 38 | this.maxReport = config.max_report || 20 39 | this.reportTime = config.reportTime || 30 40 | this.timeout = config.timeout || 100000 41 | this.reportUrl = this.configManager.getUrl('event') 42 | this.eventKey = `__tea_cache_events_${this.configManager.get('app_id')}` 43 | this.beconKey = `__tea_cache_events_becon_${this.configManager.get('app_id')}` 44 | this.abKey = `__tea_sdk_ab_version_${this.configManager.get('app_id')}` 45 | this.refer_key = `__tea_cache_refer_${this.configManager.get('app_id')}` 46 | this.collect.on(Types.Ready, () => { 47 | this.reportAll(false) 48 | }) 49 | this.collect.on(Types.ConfigDomain, () => { 50 | this.reportUrl = this.configManager.getUrl('event') 51 | }) 52 | this.collect.on(Types.Event, (events: any) => { 53 | this.event(events) 54 | }); 55 | 56 | this.collect.on(Types.BeconEvent, (events: any) => { 57 | this.beconEvent(events) 58 | }) 59 | this.collect.on(Types.CleanEvents, () => { 60 | // 清除当前的事件 61 | this.reportAll(false) 62 | }) 63 | this.linster() 64 | } 65 | 66 | linster() { 67 | window.addEventListener('unload', () => { 68 | this.reportAll(true) 69 | }, false) 70 | beforePageUnload(() => { 71 | this.reportAll(true) 72 | }) 73 | document.addEventListener('visibilitychange', () => { 74 | if (document.visibilityState === 'hidden') { 75 | this.reportAll(true) 76 | } 77 | }, false) 78 | } 79 | reportAll(becon?: boolean) { 80 | this.report(becon) 81 | this.reportBecon() 82 | } 83 | event(events: any) { 84 | try { 85 | const cache = this.cacheStorgae.getItem(this.eventKey) || [] 86 | const newCache = [...events, ...cache] 87 | this.cacheStorgae.setItem(this.eventKey, newCache) 88 | if (this.reportTimeout) { 89 | clearTimeout(this.reportTimeout) 90 | } 91 | if (newCache.length >= this.maxReport) { 92 | this.report(false) 93 | } else { 94 | const _time = this.reportTime 95 | this.reportTimeout = setTimeout(() => { 96 | this.report(false) 97 | this.reportTimeout = null 98 | }, _time) 99 | } 100 | } catch (e) { 101 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 102 | } 103 | } 104 | beconEvent(events: any) { 105 | const cache = this.cacheStorgae.getItem(this.beconKey) || [] 106 | const newCache = [...events, ...cache] 107 | this.cacheStorgae.setItem(this.beconKey, newCache) 108 | if (this.collect.destroyInstance) return 109 | if (!this.collect.tokenManager.getReady()) return 110 | if (!this.collect.sdkReady) return 111 | this.cacheStorgae.removeItem(this.beconKey) 112 | this.send(this.split(this.merge(newCache)), true) 113 | } 114 | reportBecon() { 115 | const cache = this.cacheStorgae.getItem(this.beconKey) || [] 116 | if (!cache || !cache.length) return 117 | this.cacheStorgae.removeItem(this.beconKey) 118 | this.send(this.split(this.merge(cache)), true) 119 | } 120 | report(becon: boolean) { 121 | if (this.collect.destroyInstance) return 122 | if (!this.collect.tokenManager.getReady()) return 123 | if (!this.collect.sdkReady) return 124 | const eventData = this.cacheStorgae.getItem(this.eventKey) || [] 125 | if (!eventData.length) return 126 | this.cacheStorgae.removeItem(this.eventKey) 127 | this.sliceEvent(eventData, becon); 128 | } 129 | sliceEvent(events: any, becon: boolean) { 130 | if (events.length > this.eventLimit) { 131 | for (let i = 0; i < events.length; i += this.eventLimit) { 132 | let result = [] 133 | result = events.slice(i, i + this.eventLimit); 134 | const mergeData = this.split(this.merge(result)); 135 | this.send(mergeData, becon); 136 | } 137 | } else { 138 | const mergeData = this.split(this.merge(events)); 139 | this.send(mergeData, becon); 140 | } 141 | } 142 | handleRefer() { 143 | let refer = '' 144 | try { 145 | if (this.config.spa || this.config.autotrack) { 146 | const cache_local = this.localStorage.getItem(this.refer_key) || {} 147 | if (cache_local.routeChange) { 148 | // 已经发生路由变化 149 | refer = cache_local.refer_key; 150 | } else { 151 | // 首页,用浏览器的refer 152 | refer = this.configManager.get('referrer'); 153 | } 154 | } else { 155 | refer = this.configManager.get('referrer'); 156 | } 157 | } catch (e) { 158 | refer = document.referrer; 159 | } 160 | return refer 161 | } 162 | merge(events: any, ignoreEvtParams?: boolean) { 163 | const { header, user } = this.configManager.get() 164 | header.referrer = this.handleRefer(); 165 | header.custom = JSON.stringify(header.custom) 166 | const evtParams = this.configManager.get('evtParams') 167 | const type = this.configManager.get('user_unique_id_type') 168 | const mergeEvents = events.map(item => { 169 | try { 170 | if (Object.keys(evtParams).length && !ignoreEvtParams) { 171 | item.params = { ...item.params, ...evtParams } 172 | } 173 | if (this.collect.dynamicParamsFilter) { 174 | const dynamic = this.collect.dynamicParamsFilter(); 175 | if (Object.keys(dynamic).length) { 176 | item.params = { ...item.params, ...dynamic } 177 | } 178 | } 179 | if (type) { 180 | item.params['$user_unique_id_type'] = type 181 | } 182 | const abCache = this.configManager.getAbCache(); 183 | const abVersion = this.configManager.getAbVersion() 184 | if (abVersion && abCache) { 185 | if (this.config.disable_ab_reset) { 186 | // 不校验ab的uuid 187 | item.ab_sdk_version = abVersion 188 | } else if (abCache.uuid === user.user_unique_id) { 189 | item.ab_sdk_version = abVersion 190 | } 191 | } 192 | item.session_id = this.collect.sessionManager.getSessionId() 193 | item.params = JSON.stringify(item.params) 194 | return item; 195 | } catch (e) { 196 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 197 | return item; 198 | } 199 | }) 200 | let mergeData = [] 201 | if (!Object.keys(user).length) { 202 | console.warn('user info error,cant report') 203 | return mergeData 204 | } 205 | if (this.config.enable_anonymousid) { 206 | delete user.web_id; 207 | } 208 | const resultEvent = JSON.parse( 209 | JSON.stringify({ 210 | events: mergeEvents, 211 | user, 212 | header, 213 | }), 214 | ); 215 | resultEvent.local_time = Math.floor(Date.now() / 1000); 216 | resultEvent.verbose = 1; 217 | resultEvent.user_unique_type = this.config.enable_ttwebid ? this.config.user_unique_type : undefined; 218 | mergeData.push(resultEvent) 219 | return mergeData 220 | } 221 | split(eventData: any) { 222 | eventData = eventData.map(item => { 223 | const _item = [] 224 | _item.push(item) 225 | return _item 226 | }) 227 | return eventData 228 | } 229 | send(events: any, becon: boolean) { 230 | if (!events.length) return; 231 | events.forEach(originItem => { 232 | try { 233 | let filterItem = JSON.parse(JSON.stringify(originItem)) 234 | if (this.config.filter) { 235 | filterItem = this.config.filter(filterItem) 236 | if (!filterItem) { 237 | console.warn('filter must return data !!') 238 | } 239 | } 240 | if (this.collect.eventFilter && filterItem) { 241 | filterItem = this.collect.eventFilter(filterItem) 242 | if (!filterItem) { 243 | console.warn('filterEvent api must return data !!') 244 | } 245 | } 246 | const reportItem = filterItem || originItem; 247 | const checkItem = JSON.parse(JSON.stringify(reportItem)); 248 | this.eventCheck.checkVerify(checkItem); 249 | if (!reportItem.length) return; 250 | this.collect.emit(Types.SubmitBefore, reportItem); 251 | const encodeItem = this.collect.cryptoData(reportItem); 252 | request(this.reportUrl, encodeItem, this.timeout, false, 253 | (res, data) => { 254 | if (res && res.e !== 0) { 255 | this.collect.emit(Types.SubmitError, { type: 'f_data', eventData: data, errorCode: res.e, response: res }); 256 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_EVENT, info: '埋点上报失败', time: Date.now(), data: checkItem, code: res.e, failType: '数据异常', status: 'fail' }) 257 | } else { 258 | this.collect.emit(Types.SubmitScuess, { eventData: data, res }); 259 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_EVENT, info: '埋点上报成功', time: Date.now(), data: checkItem, code: 200, status: 'success' }) 260 | } 261 | }, 262 | (eventData, errorCode) => { 263 | this.configManager.get('reportErrorCallback')(eventData, errorCode) 264 | this.collect.emit(Types.SubmitError, { type: 'f_net', eventData, errorCode }) 265 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_EVENT, info: '埋点上报网络异常', time: Date.now(), data: checkItem, code: errorCode, failType: '网络异常', status: 'fail' }) 266 | 267 | }, becon, this.config.enable_encryption, this.config.encryption_header 268 | ) 269 | this.eventCheck.checkVerify(reportItem); 270 | this.collect.emit(Types.SubmitVerifyH, reportItem); 271 | this.collect.emit(Types.SubmitAfter, reportItem); 272 | } catch (e) { 273 | console.warn(`something error, ${JSON.stringify(e.stack)}`) 274 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 275 | } 276 | }) 277 | } 278 | } -------------------------------------------------------------------------------- /src/collect/hooktype.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | enum Types { 4 | Init = 'init', 5 | Config = 'config', 6 | Start = 'start', 7 | Ready = 'ready', 8 | TokenComplete = 'token-complete', 9 | TokenStorage = 'token-storage', 10 | TokenFetch = 'token-fetch', 11 | TokenError = 'token-error', 12 | ConfigUuid = 'config-uuid', 13 | ConfigWebId = 'config-webid', 14 | ConfigDomain = 'config-domain', 15 | CustomWebId = 'custom-webid', 16 | AnonymousId = 'anonymous-id', 17 | TokenChange = 'token-change', 18 | TokenReset = 'token-reset', 19 | ConfigTransform = 'config-transform', 20 | EnvTransform = 'env-transform', 21 | SessionReset = 'session-reset', 22 | SessionResetTime = 'session-reset-time', 23 | Event = 'event', 24 | Events = 'events', 25 | EventNow = 'event-now', 26 | CleanEvents = 'clean-events', 27 | BeconEvent = 'becon-event', 28 | SubmitBefore = 'submit-before', 29 | SubmitScuess = 'submit-scuess', 30 | SubmitAfter = 'submit-after', 31 | SubmitError = 'submit-error', 32 | SubmitVerifyH = 'submit-verify-h5', 33 | 34 | Stay = 'stay', 35 | ResetStay = 'reset-stay', 36 | StayReady = 'stay-ready', 37 | SetStay = 'set-stay', 38 | 39 | RouteChange = 'route-change', 40 | RouteReady = 'route-ready', 41 | 42 | Ab = 'ab', 43 | AbVar = 'ab-var', 44 | AbAllVars = 'ab-all-vars', 45 | AbConfig = 'ab-config', 46 | AbExternalVersion = 'ab-external-version', 47 | AbVersionChangeOn = 'ab-version-change-on', 48 | AbVersionChangeOff = 'ab-version-change-off', 49 | AbOpenLayer = 'ab-open-layer', 50 | AbCloseLayer = 'ab-close-layer', 51 | AbReady = 'ab-ready', 52 | AbComplete = 'ab-complete', 53 | AbTimeout = 'ab-timeout', 54 | 55 | Profile = 'profile', 56 | ProfileSet = 'profile-set', 57 | ProfileSetOnce = 'profile-set-once', 58 | ProfileUnset = 'profile-unset', 59 | ProfileIncrement = 'profile-increment', 60 | ProfileAppend = 'profile-append', 61 | ProfileClear = 'profile-clear', 62 | 63 | TrackDuration = 'track-duration', 64 | TrackDurationStart = 'track-duration-start', 65 | TrackDurationEnd = 'track-duration-end', 66 | TrackDurationPause = 'track-duration-pause', 67 | TrackDurationResume = 'tracl-duration-resume', 68 | 69 | Autotrack = 'autotrack', 70 | AutotrackReady = 'autotrack-ready', 71 | 72 | CepReady = 'cep-ready', 73 | 74 | TracerReady = 'tracer-ready' 75 | } 76 | 77 | export enum DebuggerMesssge { 78 | DEBUGGER_MESSAGE = 'debugger-message', 79 | DEBUGGER_MESSAGE_SDK = 'debugger-message-sdk', 80 | DEBUGGER_MESSAGE_FETCH = 'debugger-message-fetch', 81 | DEBUGGER_MESSAGE_FETCH_RESULT = 'debugger-message-fetch-result', 82 | DEBUGGER_MESSAGE_EVENT = 'debugger-message-event', 83 | DEVTOOL_WEB_READY = 'devtool-web-ready', 84 | } 85 | 86 | export default Types; 87 | -------------------------------------------------------------------------------- /src/collect/session.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Types from './hooktype' 4 | import Storage from '../util/storage' 5 | 6 | interface SessionCacheType { 7 | sessionId: string, 8 | timestamp: number 9 | } 10 | 11 | export const sessionId = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 12 | const r = (Math.random() * 16) | 0; 13 | const v = c === 'x' ? r : (r & 0x3) | 0x8; 14 | return v.toString(16); 15 | }) 16 | 17 | export default class Session { 18 | sessionKey: string 19 | storage: any 20 | sessionExp: any 21 | expireTime: number 22 | disableSession: boolean 23 | collect: any 24 | apply(collect: any, config: any) { 25 | this.collect = collect 26 | this.storage = new Storage(false, 'session') 27 | this.sessionKey = `__tea_session_id_${config.app_id}` 28 | this.expireTime = config.expireTime || 30 * 60 * 1000 29 | this.disableSession = config.disable_session 30 | if (this.disableSession) return 31 | this.setSessionId() 32 | this.collect.on(Types.SessionReset, () => { 33 | this.resetSessionId() 34 | }) 35 | this.collect.on(Types.SessionResetTime, () => { 36 | this.updateSessionIdTime() 37 | }) 38 | } 39 | updateSessionIdTime() { 40 | var sessionCache: SessionCacheType = this.storage.getItem(this.sessionKey) 41 | if (sessionCache && sessionCache.sessionId) { 42 | var _oldTime = sessionCache.timestamp 43 | if ((Date.now() - _oldTime) > this.expireTime) { 44 | // 30分钟超时 45 | sessionCache = { 46 | sessionId: sessionId(), 47 | timestamp: Date.now() 48 | } 49 | } else { 50 | sessionCache.timestamp = Date.now() 51 | } 52 | this.storage.setItem(this.sessionKey, sessionCache) 53 | this.resetExpTime() 54 | } 55 | } 56 | setSessionId() { 57 | var sessionCache: SessionCacheType = this.storage.getItem(this.sessionKey) 58 | if (sessionCache && sessionCache.sessionId) { 59 | sessionCache.timestamp = Date.now() 60 | } else { 61 | sessionCache = { 62 | sessionId: sessionId(), 63 | timestamp: Date.now() 64 | } 65 | } 66 | this.storage.setItem(this.sessionKey, sessionCache) 67 | this.sessionExp = setInterval(() => { 68 | this.checkEXp() 69 | }, this.expireTime) 70 | } 71 | getSessionId() { 72 | var sessionCache: SessionCacheType = this.storage.getItem(this.sessionKey) 73 | if (this.disableSession) { 74 | return '' 75 | } 76 | if (sessionCache && sessionCache.sessionId) { 77 | return sessionCache.sessionId 78 | } else { 79 | return '' 80 | } 81 | } 82 | resetExpTime() { 83 | if (this.sessionExp) { 84 | clearInterval(this.sessionExp) 85 | this.sessionExp = setInterval(() => { 86 | this.checkEXp() 87 | }, this.expireTime) 88 | } 89 | } 90 | resetSessionId() { 91 | var sessionCache = { 92 | sessionId: sessionId(), 93 | timestamp: Date.now() 94 | } 95 | this.storage.setItem(this.sessionKey, sessionCache) 96 | } 97 | checkEXp() { 98 | var sessionCache: SessionCacheType = this.storage.getItem(this.sessionKey) 99 | if (sessionCache && sessionCache.sessionId) { 100 | var _oldTime = Date.now() - sessionCache.timestamp 101 | if (_oldTime + 30 >= this.expireTime) { 102 | // 30分钟超时 103 | sessionCache = { 104 | sessionId: sessionId(), 105 | timestamp: Date.now() 106 | } 107 | this.storage.setItem(this.sessionKey, sessionCache) 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/entry/entry-base.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | import collector from "../collect/collect" 3 | import Profile from '../plugin/profile/profile' 4 | import HeartBeat from '../plugin/heartbeat/heartbeat' 5 | import Monitor from '../plugin/monitor/index' 6 | import VerifyH from "../plugin/verify/verify_h5" 7 | 8 | 9 | collector.usePlugin(VerifyH, 'verifyH') 10 | collector.usePlugin(Profile, 'profile') 11 | collector.usePlugin(HeartBeat, 'heartbeat') 12 | collector.usePlugin(Monitor, 'monitor') 13 | 14 | const SDK = new collector('default') 15 | export const Collector = collector 16 | export default SDK -------------------------------------------------------------------------------- /src/entry/entry.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | import collector from "../collect/collect" 3 | import Ab from '../plugin/ab/ab' 4 | import Stay from '../plugin/stay/stay' 5 | import Profile from '../plugin/profile/profile' 6 | import HeartBeat from '../plugin/heartbeat/heartbeat' 7 | import Monitor from '../plugin/monitor/index' 8 | import Autotrack from '../plugin/track/index' 9 | import RuotePage from '../plugin/route/route' 10 | import VerifyH from "../plugin/verify/verify_h5" 11 | import Store from "../plugin/store/store" 12 | import TrackDuration from "../plugin/duration/duration" 13 | 14 | collector.usePlugin(Ab, 'ab') 15 | collector.usePlugin(Stay, 'stay') 16 | collector.usePlugin(Store, 'store') 17 | collector.usePlugin(Autotrack, 'autotrack') 18 | collector.usePlugin(TrackDuration, 'trackDuration') 19 | collector.usePlugin(VerifyH, 'verify') 20 | collector.usePlugin(Profile, 'profile') 21 | collector.usePlugin(HeartBeat, 'heartbeat') 22 | collector.usePlugin(Monitor, 'monitor') 23 | collector.usePlugin(RuotePage, 'route') 24 | 25 | const Tea = new collector('default') 26 | export const Collector = collector 27 | export default Tea -------------------------------------------------------------------------------- /src/plugin/ab/layer.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | type styleIE8 = { 4 | styleSheet?: { 5 | cssText: string 6 | }; 7 | }; 8 | const STYLE_ID = '__rangers_ab_style__' 9 | function openOverlayer() { 10 | if (document.getElementById(STYLE_ID)) { 11 | return 12 | } 13 | const css = 'body { opacity: 0 !important; }' 14 | const head = document.head || document.getElementsByTagName('head')[0] 15 | const style: HTMLStyleElement & styleIE8 = document.createElement('style') 16 | style.id = STYLE_ID 17 | style.type = 'text/css' 18 | if (style.styleSheet) { 19 | style.styleSheet.cssText = css 20 | } else { 21 | style.appendChild(document.createTextNode(css)) 22 | } 23 | head.appendChild(style) 24 | } 25 | 26 | function closeOverlayer() { 27 | const style = document.getElementById(STYLE_ID) 28 | if (style) { 29 | style.parentElement.removeChild(style) 30 | } 31 | } 32 | 33 | export { 34 | openOverlayer, 35 | closeOverlayer, 36 | } 37 | -------------------------------------------------------------------------------- /src/plugin/ab/load.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { init, addAllowdOrigin, dispatchMsg, receiveMsg, IDataReceive } from '../../util/postMessage' 4 | import { loadScript } from '../../util/tool' 5 | import { VISUAL_AB_CORE, VISUAL_AB_LOADER, SDK_VERSION, VISUAL_URL_INSPECTOR } from '../../collect/constant' 6 | 7 | let VISUAL_URL = '' 8 | 9 | let isLoaded = false; 10 | 11 | function loadEditorScript({ event, editorUrl, collectInstance, fromSession = true }) { 12 | if (isLoaded) { 13 | return 14 | } 15 | isLoaded = true 16 | loadScript(editorUrl, () => { 17 | dispatchMsg(event, 'abEditorScriptloadSuccess') 18 | }, 19 | () => { 20 | if (event) { 21 | dispatchMsg(event, 'abEditorScriptloadError') 22 | } 23 | isLoaded = false; 24 | }); 25 | } 26 | 27 | export default function readyToLoadEditor(collectInstance: any, config: any) { 28 | window.TEAVisualEditor = window.TEAVisualEditor || {} 29 | addAllowdOrigin(['*']) 30 | var _editorUrl = '' 31 | init(config, SDK_VERSION) 32 | var domain 33 | var scriptSrc = '' 34 | try { 35 | var resourceList = window.performance.getEntriesByType('resource') 36 | if (resourceList && resourceList.length) { 37 | resourceList.forEach(item => { 38 | if (item['initiatorType'] === 'script') { 39 | if (item.name && item.name.indexOf('collect') !== -1) { 40 | scriptSrc = item.name 41 | } 42 | } 43 | }) 44 | if (!scriptSrc) { 45 | // if the filename is error 46 | if (document.currentScript) { 47 | // not support in ie 48 | scriptSrc = document.currentScript['src'] 49 | } 50 | } 51 | if (scriptSrc) { 52 | domain = scriptSrc.split('/') 53 | if (domain && domain.length) { 54 | _editorUrl = `https:/` 55 | for (let i = 2; i < domain.length; i++) { 56 | if (i === domain.length - 1) break; 57 | _editorUrl = _editorUrl + `/${domain[i]}` 58 | } 59 | _editorUrl = `${_editorUrl}/visual-ab-core` 60 | } 61 | } 62 | } 63 | } catch (e) { } 64 | receiveMsg('tea:openVisualABEditor', (event) => { 65 | let rawData: IDataReceive = event.data 66 | if (typeof event.data === 'string') { 67 | try { 68 | rawData = JSON.parse(event.data); 69 | } catch (e) { 70 | rawData = undefined; 71 | } 72 | } 73 | if (!rawData) return 74 | const { lang, appId } = rawData 75 | 76 | if (appId !== config.app_id) { 77 | dispatchMsg(event, 'appIdError') 78 | console.error('abtest appid is not belong the page appid please check'); 79 | return; 80 | } 81 | const { version } = rawData 82 | if (version) { 83 | var _version = version ? `.${version}` : '.1.0.1' 84 | if (_editorUrl) { 85 | VISUAL_URL = `${_editorUrl}${_version}.js?query=${Date.now()}` 86 | } else { 87 | VISUAL_URL = `${VISUAL_AB_CORE}?query=${Date.now()}` 88 | } 89 | } else { 90 | VISUAL_URL = `${VISUAL_AB_CORE}?query=${Date.now()}` 91 | } 92 | window.TEAVisualEditor.lang = lang 93 | window.TEAVisualEditor.__ab_domin = config.channel_domain || '' 94 | loadEditorScript({ event, editorUrl: VISUAL_URL, collectInstance }) 95 | }) 96 | } 97 | export const loadMuiltlink = (collectInstance: any, config: any) => { 98 | window.TEAVisualEditor = window.TEAVisualEditor || {} 99 | window.TEAVisualEditor.appId = config.app_id 100 | receiveMsg('tea:openTesterEventInspector', (event) => { 101 | let rawData: IDataReceive = event.data 102 | if (typeof event.data === 'string') { 103 | try { 104 | rawData = JSON.parse(event.data); 105 | } catch (e) { 106 | rawData = undefined; 107 | } 108 | } 109 | if (!rawData) return 110 | const { referrer, lang, appId } = rawData; 111 | window.TEAVisualEditor.__editor_ajax_domain = referrer || ''; 112 | window.TEAVisualEditor.__ab_appId = appId || ''; 113 | window.TEAVisualEditor.lang = lang || '' 114 | let inspectorUrl = VISUAL_URL_INSPECTOR 115 | loadEditorScript({ event, editorUrl: `${inspectorUrl}.js?query=${Date.now()}`, collectInstance }) 116 | }) 117 | } 118 | export const loadVisual = (abconfig: any) => { 119 | window.TEAVisualEditor = window.TEAVisualEditor || {} 120 | window.TEAVisualEditor.__ab_config = abconfig 121 | loadScript(`${VISUAL_AB_LOADER}?query=${Date.now()}`, () => { 122 | console.log('load visual render success') 123 | }, () => { 124 | console.log('load visual render fail') 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /src/plugin/check/check.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | export default class EventCheck { 3 | regStr: RegExp 4 | eventNameWhiteList: string[] 5 | paramsNameWhiteList: string[] 6 | collector: any 7 | config: any 8 | constructor(collect: any, config: any) { 9 | this.collector = collect; 10 | this.config = config; 11 | this.eventNameWhiteList = [ 12 | '__bav_page', 13 | '__bav_beat', 14 | '__bav_page_statistics', 15 | '__bav_click', 16 | '__bav_page_exposure', 17 | '_be_active' 18 | ] 19 | this.paramsNameWhiteList = [ 20 | '$inactive', 21 | '$inline', 22 | '$target_uuid_list', 23 | '$source_uuid', 24 | '$is_spider', 25 | '$source_id', 26 | '$is_first_time', 27 | '$user_unique_id_type', 28 | '_staging_flag' 29 | ] 30 | this.regStr = new RegExp('^[a-zA-Z0-9][a-z0-9A-Z_.-]{1,255}$'); 31 | } 32 | // 事件名校验 33 | checkVerify(eventInfo: any): boolean { 34 | if (!eventInfo || !eventInfo.length) return false; 35 | const arr = eventInfo[0]; 36 | if (!arr) return false; 37 | const events = arr.events; 38 | const headers = arr.header; 39 | if (!events || !events.length) return false; 40 | let checkStatus = true; 41 | events.forEach(event => { 42 | if (!this.checkEventName(event.event)) { 43 | checkStatus = false; 44 | event.checkEvent = `事件名不能以 $ or __开头`; 45 | } 46 | if (!this.checkEventParams(event.params)) { 47 | checkStatus = false; 48 | event.checkParams = `属性名不能以 $ or __开头`; 49 | } 50 | }) 51 | if (!this.checkEventParams(headers)) { 52 | checkStatus = false; 53 | } 54 | return checkStatus; 55 | } 56 | checkEventName(eventName: string): boolean { 57 | if (!eventName) return false; 58 | return this.calculate(eventName, 'event'); 59 | } 60 | checkEventParams(params: any): boolean { 61 | let _params = params 62 | if (typeof params === 'string') { 63 | _params = JSON.parse(_params); 64 | } 65 | let paramStatus = true; 66 | if (!Object.keys(_params).length) return paramStatus; 67 | for (let key in _params) { 68 | if (this.calculate(key, 'params')) { 69 | if (typeof _params[key] === 'string' && _params[key].length > 1024) { 70 | console.warn(`params: ${key} can not over 1024 byte, please check;`); 71 | paramStatus = false; 72 | break; 73 | } 74 | continue; 75 | } 76 | paramStatus = false; 77 | break; 78 | } 79 | return paramStatus; 80 | } 81 | calculate(name: string, type: string): boolean { 82 | const whiteList = type === 'event' ? this.eventNameWhiteList : (type === 'params' ? this.paramsNameWhiteList : []); 83 | if (whiteList.indexOf(name) !== -1) return true; 84 | if (new RegExp('^\\$').test(name) || new RegExp('^__').test(name)) { 85 | console.warn(`${type} name: ${name} can not start with $ or __, pleace check;`); 86 | return false; 87 | } 88 | return true; 89 | } 90 | } -------------------------------------------------------------------------------- /src/plugin/debug/debug.ts: -------------------------------------------------------------------------------- 1 | import { parseUrlQuery, decodeUrl } from "../../util/tool" 2 | import Types, { DebuggerMesssge } from '../../collect/hooktype' 3 | import { SDK_VERSION, SDK_TYPE, AB_DOMAINS } from '../../collect/constant' 4 | 5 | interface MesType { 6 | type: string; 7 | payload: any; 8 | } 9 | export default class Debugger { 10 | collect: any 11 | config: any 12 | devToolReady: boolean = false 13 | devToolOrigin: string = '*' 14 | sendAlready: boolean = false 15 | app_id: number 16 | info: any 17 | log: any 18 | event: any 19 | filterEvent: any 20 | constructor(collect: any, config: any) { 21 | this.collect = collect; 22 | this.config = config; 23 | this.app_id = config.app_id; 24 | this.filterEvent = ['__bav_page', '__bav_beat', '__bav_page_statistics', '__bav_click', '__bav_page_exposure', 'bav2b_page', 25 | 'bav2b_beat', 'bav2b_page_statistics', 'bav2b_click', 'bav2b_page_exposure', '_be_active', 'predefine_pageview', '__profile_set', 26 | '__profile_set_once', '__profile_increment', '__profile_unset', '__profile_append', 'predefine_page_alive', 'predefine_page_close', 'abtest_exposure']; 27 | if (!config.enable_debug) return; 28 | this.load(); 29 | } 30 | loadScript(src: string) { 31 | try { 32 | const script = document.createElement('script'); 33 | script.src = src; 34 | 35 | script.onerror = function () { 36 | console.log('load DevTool render fail'); 37 | }; 38 | 39 | script.onload = function () { 40 | console.log('load DevTool render success'); 41 | }; 42 | 43 | document.getElementsByTagName('body')[0].appendChild(script); 44 | } catch (e) { 45 | console.log(`devTool load fail, ${e.message}`); 46 | } 47 | 48 | } 49 | load() { 50 | try { 51 | this.loadBaseInfo(); 52 | this.loadHook(); 53 | const queryObj = parseUrlQuery(window.location.href); 54 | if (!queryObj['open_devtool_web'] || parseInt(queryObj['app_id']) !== this.app_id) return; 55 | this.addLintener(); 56 | this.loadDebuggerModule(); 57 | this.loadDevTool(); 58 | } catch (e) { 59 | console.log(`debug fail, ${e.message}`); 60 | } 61 | } 62 | loadDevTool() { 63 | this.loadScript(`https://lf3-cdn-tos.bytescm.com/obj/static/log-sdk/collect/devtool/debug-web.js`) 64 | } 65 | loadBaseInfo() { 66 | this.info = [ 67 | { 68 | title: '基本信息', 69 | type: 1, 70 | infoName: { 71 | app_id: this.config.app_id, 72 | channel: this.config.channel, 73 | '上报域名': this.collect.configManager.getDomain(), 74 | 'SDK版本': SDK_VERSION, 75 | 'SDK引入方式': SDK_TYPE, 76 | } 77 | }, 78 | { 79 | title: '用户信息', 80 | type: 2, 81 | infoName: { 82 | uuid: this.collect.configManager.get('user').user_unique_id || '', 83 | web_id: this.collect.configManager.get('user').web_id || '', 84 | ssid: '点击获取SSID', 85 | } 86 | }, 87 | { 88 | title: '公共参数信息', 89 | type: 2, 90 | infoName: { 91 | '浏览器': this.collect.configManager.get('browser'), 92 | '浏览器版本': this.collect.configManager.get('browser_version'), 93 | '平台': this.collect.configManager.get('platform'), 94 | '设备型号': this.collect.configManager.get('device_model'), 95 | '操作系统': this.collect.configManager.get('os_name'), 96 | '操作系统版本': this.collect.configManager.get('os_version'), 97 | '屏幕分辨率': this.collect.configManager.get('resolution'), 98 | '来源': this.collect.configManager.get('referrer'), 99 | '自定义信息': '', 100 | } 101 | }, 102 | { 103 | title: '配置信息', 104 | type: 3, 105 | infoName: { 106 | '全埋点': this.config.autotrack ? true : false, 107 | '停留时长': this.config.enable_stay_duration ? true : false, 108 | } 109 | }, 110 | { 111 | title: 'A/B配置信息', 112 | type: 4, 113 | infoName: { 114 | 'A/B实验': this.config.enable_ab_test ? true : false, 115 | }, 116 | }, 117 | { 118 | title: '客户端信息', 119 | type: 3, 120 | infoName: { 121 | '打通开关': this.config.Native ? true : false, 122 | } 123 | } 124 | ]; 125 | this.log = []; 126 | this.event = []; 127 | this.collect.on(Types.Ready, () => { 128 | this.info[1].infoName.uuid = this.collect.configManager.get('user').user_unique_id; 129 | this.info[1].infoName.web_id = this.collect.configManager.get('user').web_id; 130 | this.info[2].infoName['自定义信息'] = JSON.stringify(this.collect.configManager.get('custom')); 131 | if (this.config.enable_ab_test) { 132 | this.info[4].infoName['已曝光VID'] = this.collect.configManager.getAbVersion(); 133 | this.info[4].infoName['A/B域名'] = this.config.ab_channel_domain || decodeUrl(AB_DOMAINS[this.config.channel]); 134 | this.info[4].infoName['全部配置'] = this.collect.configManager.getAbData(); 135 | } 136 | if (this.config.Native) { 137 | this.info[5].infoName['是否打通'] = this.collect.bridgeReport ? true : false; 138 | } 139 | }) 140 | } 141 | loadHook() { 142 | this.collect.on(DebuggerMesssge.DEBUGGER_MESSAGE, (data => { 143 | switch (data.type) { 144 | case DebuggerMesssge.DEBUGGER_MESSAGE_SDK: 145 | const logObj = { 146 | time: data.time, 147 | type: data.logType || 'sdk', 148 | level: data.level, 149 | name: data.info, 150 | show: true, 151 | levelShow: true, 152 | needDesc: data.data ? true : false, 153 | } 154 | if (data.data) { 155 | logObj['desc'] = { 156 | content: JSON.stringify(data.data) 157 | } 158 | } 159 | this.updateLog(logObj); 160 | if (data.secType && data.secType === 'AB') { 161 | this.info[4].infoName['已曝光VID'] = this.collect.configManager.getAbVersion(); 162 | this.info[4].infoName['全部配置'] = this.collect.configManager.getAbData(); 163 | } else if (data.secType === 'USER') { 164 | this.info[1].infoName['uuid'] = this.collect.configManager.get('user').user_unique_id; 165 | this.info[1].infoName['web_id'] = this.collect.configManager.get('user').web_id; 166 | } 167 | this.updateInfo(); 168 | return; 169 | case DebuggerMesssge.DEBUGGER_MESSAGE_EVENT: 170 | if (data.data && data.data.length) { 171 | const events = data.data[0]; 172 | const event = events.events; 173 | if (!event.length) return; 174 | event.forEach(item => { 175 | item['checkShow'] = true; 176 | item['searchShow'] = true; 177 | item['success'] = data.status; 178 | item['type'] = this.filterEvent.indexOf(item.event) !== -1 ? 'sdk' : 'cus'; 179 | item['type'] = this.collect.bridgeReport ? 'bridge' : item['type']; 180 | item['info'] = ''; 181 | if (data.status === 'fail') { 182 | item['info'] = { 183 | message: `code: ${data.code}, msg: ${data.failType}` 184 | } 185 | } 186 | }) 187 | this.updateEvent(events); 188 | } 189 | return; 190 | } 191 | })) 192 | } 193 | addLintener() { 194 | window.addEventListener('message', (messgae: any) => { 195 | if (messgae && messgae.data && messgae.data.type === 'devtool:web:ready') { 196 | this.devToolOrigin = messgae.origin; 197 | this.devToolReady = true; 198 | if (this.sendAlready) return; 199 | console.log('inittttt') 200 | this.sendData('devtool:web:init', { 201 | info: this.info, 202 | log: this.log, 203 | event: this.event 204 | }); 205 | this.sendAlready = true; 206 | } 207 | if (messgae && messgae.data && messgae.data.type === 'devtool:web:ssid') { 208 | this.collect.getToken(res => { 209 | this.info[1].infoName['ssid'] = res.tobid; 210 | this.updateInfo(); 211 | }) 212 | } 213 | }) 214 | } 215 | sendData(type: string, data: any) { 216 | try { 217 | const postData: MesType = { 218 | type: type, 219 | payload: data 220 | }; 221 | (window.opener || window.parent).postMessage(postData, this.devToolOrigin); 222 | } catch (e) { } 223 | } 224 | updateInfo() { 225 | if (!this.devToolReady) { 226 | return; 227 | } 228 | this.sendData('devtool:web:info', this.info); 229 | } 230 | updateLog(logObj: any) { 231 | if (!this.devToolReady) { 232 | this.log.push(logObj); 233 | return; 234 | } 235 | this.sendData('devtool:web:log', logObj); 236 | } 237 | updateEvent(events: any) { 238 | if (!this.devToolReady) { 239 | this.event.push(events); 240 | return; 241 | } 242 | this.sendData('devtool:web:event', events); 243 | } 244 | loadDebuggerModule() { 245 | const debugCss = `#debugger-applog-web { 246 | position: fixed; 247 | width: 90px; 248 | height: 30px; 249 | background: #23c243; 250 | border-radius: 6px; 251 | color: #fff; 252 | font-size: 12px; 253 | bottom: 5%; 254 | right: 10%; 255 | text-align: center; 256 | line-height: 30px; 257 | cursor: pointer; 258 | z-index:100; 259 | }`; 260 | const head = document.head || document.getElementsByTagName('head')[0] 261 | const style = document.createElement('style') 262 | style.appendChild(document.createTextNode(debugCss)); 263 | head.appendChild(style) 264 | const debuggerHtml = `
AppLog调试
`; 265 | const debugDiv = document.createElement('div'); 266 | debugDiv.innerHTML = debuggerHtml; 267 | const debuggerContainer = `
`; 268 | const debugContainerDiv = document.createElement('div'); 269 | debugContainerDiv.innerHTML = debuggerContainer; 270 | document.getElementsByTagName('body')[0].appendChild(debugDiv); 271 | document.getElementsByTagName('body')[0].appendChild(debugContainerDiv); 272 | const debugTool = document.getElementById('debugger-applog-web'); 273 | debugTool.addEventListener('click', () => { 274 | (window.opener || window.parent).postMessage({ 275 | type: 'devtool:web:open-draw', 276 | }, location.origin); 277 | }) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/plugin/duration/duration.ts: -------------------------------------------------------------------------------- 1 | 2 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 3 | 4 | interface TrackEnd { 5 | eventName: string 6 | params: any 7 | } 8 | export default class TrackDuration { 9 | collector: any 10 | config: any 11 | TrackMap: any 12 | Types: any 13 | apply(collector: any, config: any) { 14 | this.collector = collector; 15 | this.config = config; 16 | const { Types } = collector; 17 | collector.on(Types.TrackDurationStart, (eventName: string) => { 18 | this.trackStart(eventName) 19 | }); 20 | collector.on(Types.TrackDurationEnd, (info: TrackEnd) => { 21 | this.trackEnd(info) 22 | }); 23 | collector.on(Types.TrackDurationPause, (eventName: string) => { 24 | this.trackPause(eventName) 25 | }); 26 | collector.on(Types.TrackDurationResume, (eventName: string) => { 27 | this.trackResume(eventName) 28 | }); 29 | this.Types = Types; 30 | this.TrackMap = new Map(); 31 | this.ready(Types.TrackDuration); 32 | } 33 | ready(name: string) { 34 | this.collector.set(name); 35 | if (this.collector.hook._hooksCache.hasOwnProperty(name)) { 36 | const emits = this.collector.hook._hooksCache[name]; 37 | if (!Object.keys(emits).length) return; 38 | for (let key in emits) { 39 | if (emits[key].length) { 40 | emits[key].forEach(item => { 41 | this.collector.hook.emit(key, item); 42 | }) 43 | } 44 | } 45 | } 46 | } 47 | trackStart(eventName: any) { 48 | this.TrackMap.set(eventName, { 49 | startTime: Date.now(), 50 | isPause: false, 51 | pauseTime: 0, 52 | resumeTime: 0 53 | }); 54 | } 55 | trackEnd(info: TrackEnd) { 56 | const { eventName, params } = info; 57 | if (!this.TrackMap.has(eventName)) return; 58 | const trackData = this.TrackMap.get(eventName); 59 | let event_duration: number = 0; 60 | if (trackData.isPause) { 61 | // 暂停后,未恢复,直接结束 62 | event_duration = trackData.pauseTime - trackData.startTime; 63 | } else { 64 | // 处于未暂停状态,可能是恢复了,可能是从未暂停过 65 | if (trackData.resumeTime) { 66 | // 暂停后,恢复了 67 | event_duration = (trackData.pauseTime - trackData.startTime) + (Date.now() - trackData.resumeTime); 68 | } else { 69 | // 未暂停过 70 | event_duration = Date.now() - trackData.startTime; 71 | } 72 | } 73 | const eventParmas: any = Object.assign(params, { 74 | event_duration 75 | }) 76 | this.collector.event(eventName, eventParmas); 77 | this.cleanTrack(eventName); 78 | } 79 | // 事件暂停计时 80 | trackPause(eventName: string) { 81 | if (!this.TrackMap.has(eventName)) return; 82 | const trackData = this.TrackMap.get(eventName); 83 | if (trackData.isPause) return; 84 | trackData.isPause = true; 85 | trackData.pauseTime = Date.now(); 86 | this.TrackMap.set(eventName, trackData); 87 | } 88 | // 事件恢复计时 89 | trackResume(eventName: string) { 90 | if (!this.TrackMap.has(eventName)) return; 91 | const trackData = this.TrackMap.get(eventName); 92 | if (!trackData.isPause) return; 93 | trackData.isPause = false; 94 | trackData.resumeTime = Date.now(); 95 | this.TrackMap.set(eventName, trackData); 96 | } 97 | cleanTrack(eventName: string) { 98 | this.TrackMap.delete(eventName); 99 | } 100 | } -------------------------------------------------------------------------------- /src/plugin/heartbeat/heartbeat.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { selfAdjust } from '../../util/tool' 4 | export default class HeartBeat { 5 | sessionInterval: number 6 | isSessionhasEvent: boolean 7 | startTime: number 8 | lastTime: number 9 | collect: any 10 | clearIntervalFunc: () => void 11 | apply(collect: any, config: any) { 12 | this.collect = collect 13 | if (config.disable_heartbeat) return 14 | this.sessionInterval = 60 * 1000 15 | this.startTime = 0 16 | this.lastTime = 0 17 | this.setInterval() 18 | const { Types } = this.collect 19 | this.collect.on(Types.SessionReset, () => { 20 | this.process() 21 | }) 22 | } 23 | 24 | endCurrentSession() { 25 | this.collect.event('_be_active', { 26 | start_time: this.startTime, 27 | end_time: this.lastTime, 28 | url: window.location.href, 29 | referrer: window.document.referrer, 30 | title: document.title || location.pathname, 31 | }) 32 | this.isSessionhasEvent = false 33 | this.startTime = 0 34 | } 35 | 36 | setInterval = () => { 37 | this.clearIntervalFunc = selfAdjust(() => { 38 | if (this.isSessionhasEvent) { 39 | this.endCurrentSession() 40 | } 41 | }, this.sessionInterval) 42 | } 43 | 44 | clearInterval = () => { 45 | this.clearIntervalFunc && this.clearIntervalFunc() 46 | } 47 | 48 | process() { 49 | if (!this.isSessionhasEvent) { 50 | this.isSessionhasEvent = true 51 | this.startTime = +new Date() 52 | } 53 | const preLastTime = this.lastTime || +new Date() 54 | this.lastTime = +new Date() 55 | if (this.lastTime - preLastTime > this.sessionInterval) { 56 | this.clearInterval() 57 | this.endCurrentSession() 58 | this.setInterval() 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/plugin/monitor/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { SDK_TYPE } from '../../collect/constant' 4 | export default class Monitor { 5 | sdkReady: boolean 6 | config: any 7 | collect: any 8 | url: string 9 | fetch: any 10 | apply(collect: any, config: any) { 11 | this.collect = collect 12 | this.config = config 13 | if (this.config.channel_domain) return; 14 | if (this.config.disable_track_event || this.config.disable_sdk_monitor) return; 15 | const { fetch } = collect.adapters 16 | this.fetch = fetch 17 | this.url = collect.configManager.getUrl('event') 18 | const { Types } = this.collect 19 | this.collect.on(Types.Ready, () => { 20 | this.sdkOnload() 21 | }) 22 | this.collect.on(Types.SubmitError, ({ type, eventData, errorCode }) => { 23 | if (type !== 'f_data') return; 24 | this.sdkError(eventData, errorCode) 25 | }) 26 | } 27 | sdkOnload() { 28 | try { 29 | const { header, user } = this.collect.configManager.get() 30 | const { app_id, app_name, sdk_version } = header 31 | const { web_id } = user 32 | const event = { 33 | event: 'onload', 34 | params: JSON.stringify({ 35 | app_id, 36 | app_name: app_name || '', 37 | sdk_version, 38 | sdk_type: SDK_TYPE, 39 | sdk_config: this.config, 40 | sdk_desc: 'TOB' 41 | }), 42 | local_time_ms: Date.now(), 43 | } 44 | const loadData = { 45 | events: [event], 46 | user: { 47 | user_unique_id: web_id 48 | }, 49 | header: {}, 50 | } 51 | setTimeout(() => { 52 | this.fetch(this.url, [loadData], 30000, false, () => { }, () => { }, '566f58151b0ed37e') 53 | }, 16) 54 | } catch (e) { 55 | } 56 | } 57 | sdkError(data, code) { 58 | try { 59 | const { user, header } = data[0] 60 | const flatEvents = [] 61 | data.forEach((item) => { 62 | item.events.forEach((event) => { 63 | flatEvents.push(event) 64 | }) 65 | }) 66 | const errEvents = flatEvents.map(event => ({ 67 | event: 'on_error', 68 | params: JSON.stringify({ 69 | error_code: code, 70 | app_id: header.app_id, 71 | app_name: header.app_name || '', 72 | error_event: event.event, 73 | sdk_version: header.sdk_version, 74 | local_time_ms: event.local_time_ms, 75 | tea_event_index: Date.now(), 76 | params: event.params, 77 | header: JSON.stringify(header), 78 | user: JSON.stringify(user), 79 | }), 80 | local_time_ms: Date.now(), 81 | })) 82 | const errData = { 83 | events: errEvents, 84 | user: { 85 | user_unique_id: user.user_unique_id, 86 | }, 87 | header: { 88 | }, 89 | } 90 | setTimeout(() => { 91 | this.fetch(this.url, [errData], 30000, false, () => { }, () => { }, '566f58151b0ed37e') 92 | }, 16) 93 | } catch (e) { 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/plugin/profile/profile.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { DebuggerMesssge } from '../../collect/hooktype' 4 | import EventCheck from '../check/check' 5 | 6 | interface ProfileParams { 7 | [key: string]: string | number | Array; 8 | } 9 | 10 | interface ProfileIncrementParams { 11 | [key: string]: number; 12 | } 13 | export default class Profile { 14 | collect: any 15 | config: any 16 | cache: Record 17 | duration: number 18 | reportUrl: string 19 | fetch: any 20 | eventCheck: any 21 | apply(collect: any, config: any) { 22 | this.collect = collect 23 | this.config = config 24 | this.duration = 60 * 1000 25 | this.reportUrl = `${collect.configManager.getDomain()}/profile/list` 26 | const { Types } = collect 27 | const { fetch } = collect.adapters 28 | this.eventCheck = new EventCheck(collect, config) 29 | this.fetch = fetch 30 | this.cache = {} 31 | this.collect.on(Types.ProfileSet, (params) => { 32 | this.setProfile(params); 33 | }); 34 | this.collect.on(Types.ProfileSetOnce, (params) => { 35 | this.setOnceProfile(params); 36 | }); 37 | this.collect.on(Types.ProfileUnset, (key) => { 38 | this.unsetProfile(key); 39 | }); 40 | this.collect.on(Types.ProfileIncrement, (params) => { 41 | this.incrementProfile(params); 42 | }); 43 | this.collect.on(Types.ProfileAppend, (params) => { 44 | this.appendProfile(params); 45 | }); 46 | this.collect.on(Types.ProfileClear, () => { 47 | this.cache = {}; 48 | }); 49 | this.ready(Types.Profile) 50 | } 51 | ready(name: string) { 52 | this.collect.set(name) 53 | if (this.collect.hook._hooksCache.hasOwnProperty(name)) { 54 | const emits = this.collect.hook._hooksCache[name] 55 | if (!Object.keys(emits).length) return 56 | for (let key in emits) { 57 | if (emits[key].length) { 58 | emits[key].forEach(item => { 59 | this.collect.hook.emit(key, item); 60 | }) 61 | } 62 | } 63 | } 64 | } 65 | report(eventName: string, params: any = {}) { 66 | try { 67 | if (this.config.disable_track_event) return; 68 | let profileEvent = [] 69 | profileEvent.push(this.collect.processEvent(eventName, params)) 70 | let data = this.collect.eventManager.merge(profileEvent, true) 71 | const encodeData = this.collect.cryptoData(data); 72 | const url = this.collect.configManager.getUrl('profile') 73 | this.fetch(url, encodeData, 100000, false, () => { }, () => { }, '', 'POST', this.config.enable_encryption, this.config.encryption_header) 74 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_EVENT, info: '埋点上报成功', time: Date.now(), data: data, code: 200, status: 'success' }) 75 | } catch (e) { 76 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 77 | } 78 | } 79 | setProfile(params: ProfileParams): void { 80 | const result = this.formatParams(params); 81 | if (!result || !Object.keys(result).length) { 82 | return; 83 | } 84 | this.pushCache(result); 85 | this.report('__profile_set', { 86 | ...result, 87 | profile: true 88 | }); 89 | } 90 | setOnceProfile(params: ProfileParams) { 91 | const result = this.formatParams(params, true); 92 | if (!result || !Object.keys(result).length) { 93 | return; 94 | } 95 | this.pushCache(result); 96 | this.report('__profile_set_once', { 97 | ...result, 98 | profile: true 99 | }); 100 | } 101 | incrementProfile(params: ProfileIncrementParams) { 102 | if (!params) { 103 | console.warn('please check the params, must be object!!!') 104 | return; 105 | } 106 | this.report('__profile_increment', { 107 | ...params, 108 | profile: true 109 | }); 110 | } 111 | unsetProfile(key: string) { 112 | if (!key) { 113 | console.warn('please check the key, must be string!!!') 114 | return; 115 | } 116 | let unset = {} 117 | unset[key] = '1' 118 | this.report('__profile_unset', { 119 | ...unset, 120 | profile: true 121 | }); 122 | } 123 | appendProfile(params: ProfileParams) { 124 | if (!params) { 125 | console.warn('please check the params, must be object!!!') 126 | return; 127 | } 128 | let _params = {} 129 | for (let key in params) { 130 | if (typeof params[key] !== 'string' && Object.prototype.toString.call(params[key]).slice(8, -1) !== 'Array') { 131 | console.warn(`please check the value of param: ${key}, must be string or array !!!`) 132 | continue; 133 | } else { 134 | _params[key] = params[key] 135 | } 136 | } 137 | const keys = Object.keys(_params) 138 | if (!keys.length) return 139 | this.report('__profile_append', { 140 | ..._params, 141 | profile: true 142 | }); 143 | } 144 | pushCache(params: ProfileParams) { 145 | Object.keys(params).forEach((key) => { 146 | this.cache[key] = { 147 | val: this.clone(params[key]), 148 | timestamp: Date.now(), 149 | }; 150 | }); 151 | } 152 | formatParams(params: ProfileParams, once: boolean = false): ProfileParams | undefined { 153 | try { 154 | if ( 155 | !params || 156 | Object.prototype.toString.call(params) !== '[object Object]' 157 | ) { 158 | console.warn('please check the params type, must be object !!!') 159 | return; 160 | } 161 | let _params = {} 162 | for (let key in params) { 163 | if (typeof params[key] === 'string' || typeof params[key] === 'number' || Object.prototype.toString.call(params[key]).slice(8, -1) === 'Array') { 164 | _params[key] = params[key] 165 | } else { 166 | console.warn(`please check the value of params:${key}, must be string,number,Array !!!`) 167 | continue; 168 | } 169 | } 170 | const keys = Object.keys(_params); 171 | if (!keys.length) { 172 | return; 173 | } 174 | const now = Date.now(); 175 | return keys.filter((key) => { 176 | const cached = this.cache[key]; 177 | if (once) { 178 | if (cached) return false; 179 | return true; 180 | } else { 181 | if (cached && this.compare(cached.val, params[key]) && (now - cached.timestamp < this.duration)) { 182 | return false; 183 | } 184 | return true; 185 | } 186 | }) 187 | .reduce((res, current) => { 188 | res[current] = _params[current]; 189 | return res; 190 | }, {}); 191 | } catch (e) { 192 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 193 | console.log('error') 194 | } 195 | } 196 | // 对比新老值 197 | compare(newValue: any, oldValue: any) { 198 | try { 199 | return JSON.stringify(newValue) === JSON.stringify(oldValue); 200 | } catch (e) { 201 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 202 | return false; 203 | } 204 | 205 | } 206 | clone(value: any) { 207 | try { 208 | return JSON.parse(JSON.stringify(value)); 209 | } catch (e) { 210 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 211 | return value; 212 | } 213 | } 214 | unReady() { 215 | console.warn('sdk is not ready, please use this api after start') 216 | return; 217 | } 218 | } -------------------------------------------------------------------------------- /src/plugin/route/route.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | import { DebuggerMesssge } from '../../collect/hooktype' 3 | 4 | class RuotePage { 5 | storage: any 6 | lastLocation: string 7 | autotrack: boolean = false 8 | spa: boolean = false 9 | fncArray: any 10 | collect: any 11 | config: any 12 | cache_key: string 13 | cache: any = {} 14 | appid: number 15 | allowHash: boolean = false 16 | apply(collect: any, config: any) { 17 | if (!config.spa && !config.autotrack) return; 18 | const { Types } = collect 19 | this.collect = collect 20 | this.config = config 21 | this.appid = config.app_id 22 | this.allowHash = config.allow_hash 23 | this.fncArray = new Map() 24 | this.setKey() 25 | this.setLocation() 26 | this.hack() 27 | this.init() 28 | this.listener() 29 | collect.emit(Types.RouteReady) 30 | } 31 | setKey() { 32 | const { storage } = this.collect.adapters; 33 | this.storage = new storage(false) 34 | this.cache_key = `__tea_cache_refer_${this.appid}` 35 | this.cache = { 36 | refer_key: '', 37 | refer_title: document.title || location.pathname, 38 | refer_manual_key: '', 39 | routeChange: false 40 | } 41 | if (this.config.autotrack && typeof this.config.autotrack === 'object' && this.config.autotrack.page_manual_key) { 42 | this.cache.refer_manual_key = this.config.autotrack.page_manual_key 43 | } 44 | this.storage.setItem(this.cache_key, this.cache) 45 | } 46 | hack() { 47 | const oldPushState = window.history.pushState; 48 | history.pushState = (state, ...args) => { 49 | if (typeof history['onpushstate'] === 'function') { 50 | history['onpushstate']({ state }) 51 | } 52 | 53 | const ret = oldPushState.call(history, state, ...args) 54 | if (this.lastLocation === location.href) return; 55 | const config = this.getPopStateChangeEventData() 56 | this.setReferCache(this.lastLocation) 57 | this.lastLocation = location.href; 58 | this.sendPv(config, 'pushState') 59 | return ret 60 | } 61 | const oldReplaceState = history.replaceState 62 | history.replaceState = (state, ...args) => { 63 | if (typeof history['onreplacestate'] === 'function') { 64 | history['onreplacestate']({ state }) 65 | } 66 | 67 | const ret = oldReplaceState.call(history, state, ...args) 68 | if (this.lastLocation === location.href) return; 69 | const config = this.getPopStateChangeEventData() 70 | this.setReferCache(this.lastLocation) 71 | this.lastLocation = location.href; 72 | this.sendPv(config) 73 | return ret 74 | } 75 | } 76 | setLocation() { 77 | if (typeof window !== 'undefined') { 78 | this.lastLocation = window.location.href 79 | } 80 | } 81 | getLocation() { 82 | return this.lastLocation 83 | } 84 | init() { 85 | const config = this.getPopStateChangeEventData() 86 | this.collect.emit('route-change', { config, init: true }) 87 | } 88 | listener() { 89 | let timeoutId = null 90 | const time = 10 91 | window.addEventListener('hashchange', (e) => { 92 | if (this.lastLocation === window.location.href) return 93 | clearTimeout(timeoutId) 94 | if (this.allowHash) { // 如果允许hashTag,就执行回调函数 95 | this.setReferCache(this.lastLocation) 96 | this.lastLocation = window.location.href 97 | const config = this.getPopStateChangeEventData(); 98 | this.sendPv(config); 99 | } 100 | }); 101 | window.addEventListener('popstate', (e) => { 102 | if (this.lastLocation === window.location.href) { 103 | return 104 | } 105 | timeoutId = setTimeout(() => { 106 | this.setReferCache(this.lastLocation) 107 | this.lastLocation = window.location.href 108 | const config = this.getPopStateChangeEventData() 109 | this.sendPv(config) 110 | }, time) 111 | }) 112 | } 113 | getPopStateChangeEventData() { 114 | const config = this.pageConfig() 115 | config['is_back'] = 0 116 | return config 117 | } 118 | pageConfig() { 119 | try { 120 | const cache_local = this.storage.getItem(this.cache_key) || {} 121 | let is_first_time = false 122 | const firstStatus = this.storage.getItem(`__tea_cache_first_${this.appid}`) 123 | if (firstStatus && firstStatus == 1) { 124 | is_first_time = false 125 | } else { 126 | is_first_time = true 127 | } 128 | return { 129 | is_html: 1, 130 | url: location.href, 131 | referrer: this.handleRefer(), 132 | page_key: location.href, 133 | refer_page_key: this.handleRefer(), 134 | page_title: document.title || location.pathname, 135 | page_manual_key: this.config.autotrack && this.config.autotrack.page_manual_key || '', 136 | refer_page_manual_key: cache_local && cache_local.refer_manual_key || '', 137 | refer_page_title: cache_local && cache_local.refer_title || '', 138 | page_path: location.pathname, 139 | page_host: location.host, 140 | is_first_time: `${is_first_time}` 141 | } 142 | } catch (e) { 143 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: '发生了异常', level: 'error', time: Date.now(), data: e.message }); 144 | return {} 145 | } 146 | } 147 | sendPv(config: any, name?: string) { 148 | this.collect.emit('route-change', { config, init: false }) 149 | } 150 | handleRefer() { 151 | let refer = '' 152 | try { 153 | const cache_local = this.storage.getItem() || {} 154 | if (cache_local.routeChange) { 155 | // 已经发生路由变化 156 | refer = cache_local.refer_key; 157 | } else { 158 | // 首页,用浏览器的refer 159 | refer = this.collect.configManager.get('referrer'); 160 | } 161 | } catch (e) { 162 | refer = document.referrer; 163 | } 164 | 165 | return refer 166 | } 167 | setReferCache(url: string) { 168 | const cache_local = this.storage.getItem(this.cache_key) || {} 169 | cache_local.refer_key = url 170 | // 不再是第一次进入页面 171 | cache_local.routeChange = true 172 | this.storage.setItem(this.cache_key, cache_local) 173 | } 174 | } 175 | export default RuotePage 176 | -------------------------------------------------------------------------------- /src/plugin/stay/alive.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { isSupVisChange, isObject } from '../../util/tool' 4 | interface Option { 5 | aliveName?: string, 6 | params?: Record | Function 7 | } 8 | export default class Alive { 9 | collect: any 10 | config: any 11 | pageStartTime: number 12 | maxDuration: number = 12 * 60 * 60 * 1000 13 | sessionStartTime: number 14 | timerHandler: any 15 | url_path: string 16 | url: string 17 | title: string 18 | set_path: string 19 | set_url: string 20 | set_title: string 21 | aliveDTime: number = 60 * 1000 22 | aliveName: string 23 | disableCallback: any 24 | customParmas: Record 25 | options: Option = { aliveName: 'predefine_page_alive', params: {} } 26 | constructor(collect: any, config: any) { 27 | this.collect = collect 28 | this.config = config 29 | this.pageStartTime = Date.now() 30 | this.sessionStartTime = this.pageStartTime 31 | this.timerHandler = null 32 | if (isObject(config.enable_stay_duration)) { 33 | this.options = Object.assign(this.options, config.enable_stay_duration) 34 | } 35 | } 36 | setParams(url_path: string, title: string, url: string) { 37 | this.set_path = url_path 38 | this.set_url = url 39 | this.set_title = title 40 | } 41 | resetParams(url_path: string, title: string, url: string) { 42 | this.url_path = url_path 43 | this.url = url 44 | this.title = title 45 | } 46 | enable(url_path: string, title: string, url: string) { 47 | this.url_path = url_path || this.url_path 48 | this.url = url || this.url 49 | this.title = title || this.title 50 | this.disableCallback = this.enablePageAlive() 51 | if (this.options.params instanceof Function) { 52 | this.customParmas = this.options.params() 53 | } else { 54 | this.customParmas = this.options.params; 55 | } 56 | } 57 | 58 | disable() { 59 | this.disableCallback() 60 | this.pageStartTime = Date.now() 61 | } 62 | 63 | sendEvent(leave: boolean, limited = false) { 64 | const duration = limited ? this.aliveDTime : Date.now() - this.sessionStartTime 65 | if (duration < 0 || duration > this.aliveDTime || (Date.now() - this.pageStartTime > this.maxDuration)) { 66 | return 67 | } 68 | 69 | this.collect.beconEvent(this.options.aliveName, { 70 | url_path: this.getParams('url_path'), 71 | title: this.getParams('title'), 72 | url: this.getParams('url'), 73 | duration: duration, 74 | is_support_visibility_change: isSupVisChange(), 75 | startTime: this.sessionStartTime, 76 | hidden: document.visibilityState, 77 | leave, 78 | ...this.customParmas 79 | }) 80 | this.sessionStartTime = Date.now() 81 | this.resetParams(location.pathname, document.title, location.href) 82 | } 83 | 84 | getParams(type: string) { 85 | switch (type) { 86 | case 'url_path': 87 | return this.set_path || this.url_path || location.pathname 88 | case 'title': 89 | return this.set_title || this.title || document.title || location.pathname 90 | case 'url': 91 | return this.set_url || this.url || location.href 92 | } 93 | } 94 | setUpTimer() { 95 | if (this.timerHandler) clearInterval(this.timerHandler) 96 | return setInterval(() => { 97 | if (Date.now() - this.sessionStartTime > this.aliveDTime) { 98 | this.sendEvent(false, true) 99 | } 100 | }, 1000) 101 | } 102 | 103 | visibilitychange() { 104 | if (document.visibilityState === 'hidden') { 105 | if (this.timerHandler) { 106 | clearInterval(this.timerHandler) 107 | this.sendEvent(false) 108 | } 109 | } else if (document.visibilityState === 'visible') { 110 | // 重置时间 111 | this.sessionStartTime = Date.now() 112 | this.timerHandler = this.setUpTimer() 113 | } 114 | } 115 | 116 | beforeunload() { 117 | if (!document.hidden) { 118 | this.sendEvent(true) 119 | } 120 | } 121 | enablePageAlive() { 122 | this.timerHandler = this.setUpTimer() 123 | const change = this.visibilitychange.bind(this) 124 | const before = this.beforeunload.bind(this) 125 | document.addEventListener('visibilitychange', change) 126 | window.addEventListener('pagehide', before) 127 | return () => { 128 | this.beforeunload() 129 | document.removeEventListener('visibilitychange', change) 130 | window.removeEventListener('beforeunload', before) 131 | window.removeEventListener('pagehide', before) 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/plugin/stay/close.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { isSupVisChange, isObject } from '../../util/tool' 4 | 5 | interface Option { 6 | closeName?: string, 7 | params?: Record | Function 8 | } 9 | export default class Close { 10 | collect: any 11 | config: any 12 | pageStartTime: number 13 | maxDuration: number = 12 * 60 * 60 * 1000 14 | sessionStartTime: number 15 | timerHandler: any 16 | url_path: string 17 | url: string 18 | title: string 19 | set_path: string 20 | set_url: string 21 | set_title: string 22 | aliveDTime: number = 60 * 1000 23 | aliveName: string 24 | options: Option = { closeName: 'predefine_page_close', params: {} } 25 | activeStartTime: number 26 | activeEndTime: any 27 | activeTimes: number 28 | totalTime: number 29 | disableCallback: any 30 | customParmas: Record 31 | constructor(collect: any, config: any) { 32 | this.collect = collect 33 | this.config = config 34 | this.maxDuration = config.maxDuration || 24 * 60 * 60 * 1000 35 | this.pageStartTime = Date.now() 36 | if (isObject(config.enable_stay_duration)) { 37 | this.options = Object.assign(this.options, config.enable_stay_duration) 38 | } 39 | this.resetData() 40 | } 41 | 42 | setParams(url_path: string, title: string, url: string) { 43 | this.set_path = url_path 44 | this.set_url = url 45 | this.set_title = title 46 | } 47 | resetParams(url_path: string, title: string, url: string) { 48 | this.url_path = url_path 49 | this.url = url 50 | this.title = title 51 | } 52 | enable(url_path: string, title: string, url: string) { 53 | this.url_path = url_path || this.url_path 54 | this.url = url || this.url 55 | this.title = title || this.title 56 | this.disableCallback = this.enablePageClose() 57 | } 58 | 59 | disable() { 60 | this.disableCallback() 61 | } 62 | 63 | resetData() { 64 | this.activeStartTime = this.activeStartTime === undefined ? this.pageStartTime : Date.now() 65 | this.activeEndTime = undefined 66 | this.activeTimes = 1 67 | this.totalTime = 0 68 | if (this.options.params instanceof Function) { 69 | this.customParmas = this.options.params() 70 | } else { 71 | this.customParmas = this.options.params; 72 | } 73 | this.resetParams(location.pathname, document.title, location.href) 74 | } 75 | 76 | sendEventPageClose() { 77 | const total_duration = Date.now() - this.pageStartTime 78 | if (this.totalTime < 0 || total_duration < 0) return 79 | // 超过24小时的时长没有统计意义 80 | if (this.totalTime >= this.maxDuration) return 81 | this.collect.beconEvent(this.options.closeName, { 82 | url_path: this.getParams('url_path'), 83 | title: this.getParams('title'), 84 | url: this.getParams('url'), 85 | active_times: this.activeTimes, 86 | duration: this.totalTime, 87 | total_duration: total_duration, 88 | is_support_visibility_change: isSupVisChange(), 89 | ...this.customParmas 90 | }) 91 | this.pageStartTime = Date.now() 92 | 93 | this.resetData() 94 | } 95 | 96 | getParams(type: string) { 97 | switch (type) { 98 | case 'url_path': 99 | return this.set_path || this.url_path || location.pathname 100 | case 'title': 101 | return this.set_title || this.title || document.title || location.pathname 102 | case 'url': 103 | return this.set_url || this.url || location.href 104 | } 105 | } 106 | 107 | visibilitychange = () => { 108 | if (document.visibilityState === 'hidden') { 109 | this.activeEndTime = Date.now() 110 | 111 | } else if (document.visibilityState === 'visible') { 112 | if (this.activeEndTime) { 113 | this.totalTime += (this.activeEndTime - this.activeStartTime) 114 | this.activeTimes += 1 115 | } 116 | this.activeEndTime = undefined 117 | this.activeStartTime = Date.now() 118 | } 119 | }; 120 | 121 | beforeunload = () => { 122 | this.totalTime += ((this.activeEndTime || Date.now()) - this.activeStartTime) 123 | if (this.config.autotrack) { 124 | const durationKey = '_tea_cache_duration' 125 | try { 126 | var session = window.sessionStorage 127 | session.setItem(durationKey, JSON.stringify({ 128 | duration: this.totalTime, 129 | page_title: document.title || location.pathname 130 | })) 131 | } catch (e) { } 132 | } 133 | this.sendEventPageClose() 134 | }; 135 | 136 | enablePageClose() { 137 | const change = this.visibilitychange.bind(this) 138 | const before = this.beforeunload.bind(this) 139 | document.addEventListener('visibilitychange', change) 140 | window.addEventListener('pagehide', before) 141 | return () => { 142 | this.beforeunload() 143 | document.removeEventListener('visibilitychange', change) 144 | window.removeEventListener('beforeunload', before) 145 | window.removeEventListener('pagehide', before) 146 | }; 147 | } 148 | } -------------------------------------------------------------------------------- /src/plugin/stay/stay.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Alive from './alive' 4 | import Close from './close' 5 | import { DebuggerMesssge } from '../../collect/hooktype' 6 | 7 | export default class Stay { 8 | collect: any 9 | config: any 10 | title: string 11 | url: string 12 | url_path: string 13 | pageAlive: any 14 | pageClose: any 15 | apply(collect: any, config: any) { 16 | this.collect = collect 17 | this.config = config 18 | if (!this.config.enable_stay_duration) return 19 | this.title = document.title || location.pathname 20 | this.url = location.href 21 | this.url_path = location.pathname 22 | this.pageAlive = new Alive(collect, config) 23 | this.pageClose = new Close(collect, config) 24 | const { Types } = this.collect 25 | this.collect.on(Types.ResetStay, ({ url_path, title, url }) => { 26 | this.resetStayDuration(url_path, title, url) 27 | }) 28 | this.collect.on(Types.RouteChange, (info) => { 29 | if (info.init) return; 30 | if (config.disable_route_report) return; 31 | this.resetStayDuration() 32 | }) 33 | this.collect.on(Types.SetStay, ({ url_path, title, url }) => { 34 | this.setStayParmas(url_path, title, url) 35 | }) 36 | this.enable(this.url_path, this.title, this.url) 37 | this.ready(Types.Stay) 38 | this.collect.emit(Types.StayReady) 39 | } 40 | ready(name: string) { 41 | this.collect.set(name) 42 | if (this.collect.hook._hooksCache.hasOwnProperty(name)) { 43 | const emits = this.collect.hook._hooksCache[name] 44 | if (!Object.keys(emits).length) return 45 | for (let key in emits) { 46 | if (emits[key].length) { 47 | emits[key].forEach(item => { 48 | this.collect.hook.emit(key, item); 49 | }) 50 | } 51 | } 52 | } 53 | } 54 | enable(url_path: string, title: string, url: string) { 55 | this.pageAlive.enable(url_path, title, url) 56 | this.pageClose.enable(url_path, title, url) 57 | } 58 | disable() { 59 | this.pageAlive.disable() 60 | this.pageClose.disable() 61 | } 62 | setStayParmas(url_path: string = '', title: string = '', url: string = '') { 63 | // 专门用来设置stay的参数 64 | this.pageAlive.setParams(url_path, title, url) 65 | this.pageClose.setParams(url_path, title, url) 66 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: 'SDK 执行 resetStayParams', level: 'info', time: Date.now(), data: { url_path, title, url } }) 67 | } 68 | reset(url_path: string, title: string, url: string) { 69 | this.disable() 70 | this.enable(url_path, title, url) 71 | } 72 | resetStayDuration(url_path?: string, title?: string, url?: string) { 73 | this.reset(url_path, title, url) 74 | this.collect.emit(DebuggerMesssge.DEBUGGER_MESSAGE, { type: DebuggerMesssge.DEBUGGER_MESSAGE_SDK, info: 'SDK 执行 resetStayDuration', level: 'info', time: Date.now(), data: { url_path, title, url } }) 75 | } 76 | } -------------------------------------------------------------------------------- /src/plugin/store/store.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | class Store { 4 | collect: any 5 | config: any 6 | storage: any 7 | eventKey: string 8 | storageNum: number 9 | fetch: any 10 | eventUrl: string 11 | retryNum: number 12 | retryInterval: number 13 | retryWaitTime: number = 3000 // 3秒后重试 14 | retryStatus: boolean = false 15 | retryCacheStatus: boolean = false 16 | errorCache: [any] 17 | apply(collect: any, config: any) { 18 | if (!config.enable_storage || config.disable_storage) return; 19 | this.collect = collect; 20 | this.config = config; 21 | if (this.collect.destroyInstance) return; 22 | const { Types } = collect; 23 | const { storage, fetch } = collect.adapters; 24 | this.storage = new storage(false); 25 | this.fetch = fetch; 26 | this.eventUrl = this.collect.configManager.getUrl('event'); 27 | this.eventKey = `__tea_cache_events_${config.app_id}`; 28 | this.storageNum = config.storage_num || 50; // 默认最大存储50条数据 29 | this.retryNum = config.retry_num || 3; // 默认最多重试3次 30 | this.retryInterval = 1000; // 默认每隔1000ms重试一次 31 | collect.on(Types.SubmitError, (errorInfo) => { 32 | if (errorInfo.type !== 'f_data') return; 33 | // this.retryRightNow(errorInfo); 34 | this.storeData(errorInfo); 35 | }) 36 | collect.on(Types.Ready, () => { 37 | this.checkStorage(); 38 | }) 39 | } 40 | retryRightNow(errorInfo: any) { 41 | if (this.retryStatus) { 42 | // 再重试过程中又有数据异常,则先暂存 43 | this.errorCache.push(errorInfo); 44 | return; 45 | } 46 | let currentNum = 0; 47 | this.retryStatus = true; 48 | const currentInterval = setInterval(() => { 49 | if (currentNum === 3) { 50 | // 达到重试次数后不再重试,存储起来 51 | this.storeData(this.errorCache); 52 | this.retryStatus = false; 53 | clearInterval(currentInterval); 54 | return; 55 | } 56 | const { eventData } = errorInfo; 57 | this.fetchData(eventData, () => { 58 | this.retryStatus = false; 59 | clearInterval(currentInterval); 60 | if (this.retryCacheStatus) { 61 | this.errorCache.splice(0, 1); 62 | } 63 | if (this.errorCache.length) { 64 | this.retryCacheStatus = true; 65 | this.retryRightNow(this.errorCache[0]); 66 | } 67 | }, () => { 68 | currentNum++; 69 | }) 70 | }, this.retryInterval); 71 | } 72 | storeData(errorInfo: any) { 73 | let data = this.storage.getItem(this.eventKey); 74 | const { eventData } = errorInfo; 75 | // 数据错误不进行存储 76 | if (Object.keys(data).length === this.storageNum) return; 77 | // 数据满了,就不再添加数据 78 | data[Date.now()] = eventData; 79 | this.storage.setItem(this.eventKey, data); 80 | } 81 | checkStorage() { 82 | try { 83 | if (!window.navigator.onLine) return; // 设备未联网 84 | let data = this.storage.getItem(this.eventKey); 85 | if (!data || !Object.keys(data).length) return; 86 | const loadData = { 87 | events: [{ 88 | event: 'ontest', 89 | params: { 90 | app_id: this.config.app_id 91 | }, 92 | local_time_ms: Date.now(), 93 | }], 94 | user: { 95 | user_unique_id: this.collect.configManager.get('web_id') 96 | }, 97 | header: {}, 98 | } 99 | const success = () => { 100 | const copyData = JSON.parse(JSON.stringify(data)); 101 | for (let key in data) { 102 | this.fetchData(data[key], () => { 103 | delete (copyData[key]); 104 | this.storage.setItem(this.eventKey, copyData); 105 | }, () => { }, false) 106 | } 107 | } 108 | this.fetchData([loadData], success, () => { }, true); 109 | } catch (e) { 110 | console.warn('error check storage'); 111 | } 112 | } 113 | fetchData(data: any, success?: any, fail?: any, test?: boolean) { 114 | this.fetch(this.eventUrl, data, 30000, false, () => { 115 | success && success(); 116 | }, () => { 117 | fail && fail(); 118 | console.log('network error,compensate report failk'); 119 | }, test && !this.config.channel_domain ? '566f58151b0ed37e' : '') 120 | } 121 | } 122 | 123 | export default Store 124 | -------------------------------------------------------------------------------- /src/plugin/track/config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { EventConfig, ScoutConfig } from './type' 4 | 5 | 6 | 7 | export type _Config = { 8 | eventConfig: EventConfig, 9 | scoutConfig: ScoutConfig 10 | } 11 | 12 | export const defaultConfig = { 13 | eventConfig: { 14 | mode: "proxy-capturing", 15 | submit: false, 16 | click: true, 17 | change: false, 18 | pv: true, 19 | beat: true, 20 | hashTag: false, 21 | impr: false, 22 | }, 23 | scoutConfig: { 24 | mode: "xpath" 25 | }, 26 | } 27 | 28 | export default class Config { 29 | config: _Config 30 | 31 | constructor(config, options) { 32 | this.config = config 33 | this.config.eventConfig = Object.assign(this.config.eventConfig, options) 34 | } 35 | getConfig() { 36 | return this.config 37 | } 38 | setConfig(config: _Config) { 39 | return this.config = config 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/plugin/track/dom.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { isArray } from '../../util/tool' 4 | export function isNeedElement(element: HTMLElement | null, type: string = 'list'): boolean { 5 | if (!element) return false 6 | if (type && type === 'list') { 7 | if (['LI', 'TR', 'DL'].includes(element.nodeName)) return true 8 | if (element.dataset && element.dataset.hasOwnProperty('teaIdx')) return true 9 | if (element.hasAttribute && element.hasAttribute('data-tea-idx')) return true 10 | } else { 11 | if (['A', 'BUTTON'].includes(element.nodeName)) return true 12 | if (element.dataset && element.dataset.hasOwnProperty('teaContainer')) return true 13 | if (element.hasAttribute && element.hasAttribute('data-tea-container')) return true 14 | if (element.hasAttribute && hasAttributes(element, 'ss')) return true 15 | } 16 | return false 17 | } 18 | export const isAttrFilter = (element: HTMLElement, attrs: any) => { 19 | if (hasAttributes(element, attrs)) return true 20 | return false 21 | } 22 | export function getContainer(element: HTMLElement): HTMLElement { 23 | let current = element 24 | while (current && !isNeedElement(current, 'container')) { 25 | if (current.nodeName === 'HTML' || current.nodeName === 'BODY') { 26 | return element 27 | } 28 | current = current.parentElement 29 | } 30 | return current || element 31 | } 32 | 33 | export function getNodeText(node: Node): string { 34 | let text = '' 35 | if (node.nodeType === 3) { 36 | text = node.textContent.trim() 37 | } else if (node['dataset'] && node['dataset'].hasOwnProperty('teaTitle')) { 38 | text = node['getAttribute']('data-tea-title') 39 | } else if (node['hasAttribute']('ata-tea-title')) { 40 | text = node['getAttribute']('data-tea-title') 41 | } else if (node['hasAttribute']('title')) { 42 | text = node['getAttribute']('title') 43 | } else if (node.nodeName === 'INPUT' && ['button', 'submit'].includes(node['getAttribute']('type'))) { 44 | text = node['getAttribute']('value') 45 | } else if (node.nodeName === 'IMG' && node['getAttribute']('alt')) { 46 | text = node['getAttribute']('alt') 47 | } 48 | return text.slice(0, 200) 49 | } 50 | 51 | export function getText(element: HTMLElement): string[] { 52 | const ele = getContainer(element) 53 | const textArr = []; 54 | (function _get(node: Node) { 55 | const text = getNodeText(node) 56 | if (text && textArr.indexOf(text) === -1) { 57 | textArr.push(text) 58 | } 59 | if (node.childNodes.length > 0) { 60 | const { childNodes } = node 61 | for (let i = 0; i < childNodes.length; i++) { 62 | if (childNodes[i].nodeType !== 8) { 63 | _get(childNodes[i]) 64 | } 65 | } 66 | } 67 | })(ele) 68 | return textArr 69 | } 70 | 71 | export function getTextSingle(element: HTMLElement): string { 72 | const ele = getContainer(element) 73 | let text = ''; 74 | (function _get(node: Node) { 75 | const _text = getNodeText(node) 76 | if (_text) { 77 | text = text + _text 78 | } 79 | if (node.childNodes.length > 0) { 80 | const { childNodes } = node 81 | for (let i = 0; i < childNodes.length; i++) { 82 | if (childNodes[i].nodeType === 3) { 83 | _get(childNodes[i]) 84 | } 85 | } 86 | } 87 | })(ele) 88 | return text 89 | } 90 | 91 | export function ignore(element: any): boolean { 92 | let _element = element 93 | while (_element && _element.parentNode) { 94 | if (_element.hasAttribute('data-tea-ignore')) return true 95 | if (_element.nodeName === 'HTML' || _element.nodeName === 'body') return false 96 | _element = _element.parentNode 97 | } 98 | return false 99 | } 100 | 101 | export const hasAttribute = (ele: HTMLElement, attr: string) => { 102 | if (ele.hasAttribute) { 103 | return ele.hasAttribute(attr); 104 | } else if (ele.attributes) { 105 | return !!(ele.attributes[attr] && ele.attributes[attr].specified); 106 | } 107 | } 108 | 109 | export const hasAttributes = (ele: HTMLElement, attrs: any) => { 110 | if (typeof attrs === 'string') { 111 | return hasAttribute(ele, attrs); 112 | } else if (isArray(attrs)) { 113 | let result = false; 114 | for (let i = 0; i < attrs.length; i++) { 115 | let testResult = hasAttribute(ele, attrs[i]); 116 | if (testResult) { 117 | result = true; 118 | break; 119 | } 120 | } 121 | return result; 122 | } 123 | } 124 | 125 | export const getAttributes = (ele: HTMLElement, attrs: any) => { 126 | const result = {} 127 | if (typeof attrs === 'string') { 128 | if (hasAttribute(ele, attrs)) { 129 | result['attrs'] = ele.getAttribute(attrs) 130 | } 131 | } else { 132 | if (isArray(attrs)) { 133 | for (let i = 0; i < attrs.length; i++) { 134 | let testResult = hasAttribute(ele, attrs[i]); 135 | if (testResult) { 136 | result[attrs[i]] = ele.getAttribute(attrs[i]) 137 | } 138 | } 139 | } 140 | } 141 | return result 142 | } 143 | 144 | -------------------------------------------------------------------------------- /src/plugin/track/element.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { getText, getContainer, getTextSingle, isAttrFilter, getAttributes } from './dom' 4 | import { getXpath, getPositionData, getEventData } from './path' 5 | import { IGNORE } from './event' 6 | interface ElementData { 7 | element_path: string 8 | positions: Array 9 | texts: Array 10 | element_width?: number 11 | element_height?: number 12 | touch_x?: number 13 | touch_y?: number 14 | href?: string 15 | src?: string 16 | page_manual_key?: string 17 | elememt_manual_key?: string 18 | since_page_start_ms?: number 19 | page_start_ms?: number 20 | element_title?: string 21 | element_id?: string 22 | element_class_name?: string 23 | element_type?: number 24 | element_target_page?: string 25 | page_path?: string 26 | page_host?: string 27 | } 28 | 29 | 30 | export default function getElementData(event: any, element: HTMLElement, options: any, ignore?: IGNORE) { 31 | const elementData: any = {} 32 | 33 | const positionData = getPositionData(element) 34 | const eventData = getEventData(event, positionData) 35 | const { element_width, element_height } = positionData 36 | const { touch_x, touch_y } = eventData 37 | const { element_path, positions } = getXpath(element) 38 | const texts = getText(element) 39 | const page_start_ms = window.performance.timing.navigationStart 40 | const since_page_start_ms = Date.now() - page_start_ms 41 | const _position = positions.map(item => `${item}`) 42 | let elementObj = null 43 | if (window.TEAVisualEditor.getOriginXpath) { 44 | elementObj = window.TEAVisualEditor.getOriginXpath({ 45 | xpath: element_path, 46 | positions: _position 47 | }) 48 | } 49 | elementData.element_path = elementObj && elementObj.xpath || element_path 50 | elementData.positions = elementObj && elementObj.positions || _position 51 | if (ignore && !ignore.text) { 52 | elementData.texts = texts 53 | elementData.element_title = getTextSingle(element) 54 | } 55 | elementData.element_id = element.getAttribute('id') || '' 56 | elementData.element_class_name = element.getAttribute('class') || '' 57 | elementData.element_type = element.nodeType 58 | elementData.element_width = Math.floor(element_width) 59 | elementData.element_height = Math.floor(element_height) 60 | elementData.touch_x = touch_x 61 | elementData.touch_y = touch_y 62 | elementData.page_manual_key = '' 63 | elementData.elememt_manual_key = '' 64 | elementData.since_page_start_ms = since_page_start_ms 65 | elementData.page_start_ms = page_start_ms 66 | elementData.page_path = location.pathname 67 | elementData.page_host = location.host 68 | 69 | if (options.track_attr) { 70 | if (isAttrFilter(element, options.track_attr)) { 71 | const attrData = getAttributes(element, options.track_attr) 72 | for (let attr in attrData) { 73 | elementData[attr] = attrData[attr] 74 | } 75 | } 76 | } 77 | const containerNoode = getContainer(element) 78 | if (containerNoode.tagName === 'A') { 79 | elementData.href = containerNoode.getAttribute('href') 80 | } 81 | 82 | if (element.tagName === 'IMG') { 83 | elementData.src = element.getAttribute('src') 84 | } 85 | 86 | return elementData 87 | } -------------------------------------------------------------------------------- /src/plugin/track/event.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import getElementData from './element' 4 | import { ignore } from './dom' 5 | import { ParamsStruct } from './type' 6 | 7 | 8 | export type EventType = 'bav2b_click' | 'bav2b_change' | 'bav2b_submit' | 'bav2b_exposure' 9 | export interface IGNORE { 10 | text?: boolean 11 | } 12 | 13 | const getEventData = (event, e, element: HTMLElement, options: any, ignore?: IGNORE): ParamsStruct => { 14 | return { 15 | event, 16 | ...getElementData(e, element, options, ignore), 17 | is_html: 1, 18 | page_key: window.location.href, 19 | page_title: document.title 20 | } 21 | } 22 | 23 | 24 | const getExtraEventData = (event, element: HTMLInputElement): Object => { 25 | try { 26 | if (event === 'bav2b_change') { 27 | if (element.hasAttribute('data-tea-track')) { 28 | return { value: element.value } 29 | } 30 | return {} 31 | } 32 | } catch (err) { 33 | return {} 34 | } 35 | } 36 | 37 | export default class EventHandle { 38 | eventName: any 39 | ignore: IGNORE = { text: false } 40 | initConfig: any 41 | options: any 42 | constructor(initConfig: any, options: any) { 43 | this.initConfig = initConfig 44 | this.options = options 45 | this.eventName = options && options.custom === 'tea' ? { 46 | click: '__bav_click', 47 | page: '__bav_page', 48 | beat: '__bav_beat', 49 | static: '__bav_page_statistics', 50 | exposure: '__bav_page_exposure' 51 | } : { 52 | click: 'bav2b_click', 53 | page: 'bav2b_page', 54 | beat: 'bav2b_beat', 55 | static: 'bav2b_page_statistics', 56 | exposure: 'bav2b_exposure' 57 | } 58 | if (options && options.text === false) { 59 | this.ignore.text = true 60 | } 61 | if (options && options.exposure && options.exposure.eventName) { 62 | this.eventName['exposure'] = options.exposure.eventName 63 | } 64 | } 65 | handleEvent(e, eventType): ParamsStruct { 66 | try { 67 | if (ignore(e.target)) { 68 | return null 69 | } 70 | let event = 'bav2b_click' 71 | switch (eventType) { 72 | case 'click': 73 | event = this.eventName['click'] 74 | return getEventData(event, e, e.target, this.options, this.ignore) 75 | case 'exposure': 76 | event = this.eventName['exposure'] 77 | return getEventData(event, e, e.target, this.options, this.ignore) 78 | case 'change': 79 | event = 'bav2b_change' 80 | return { ...getEventData(event, e, e.target, this.options), ...getExtraEventData(event, e.target) } 81 | case 'submit': 82 | event = 'bav2b_submit' 83 | return getEventData(event, e, e.target, this.options) 84 | 85 | } 86 | } catch (err) { 87 | console.error(err) 88 | return null 89 | } 90 | } 91 | 92 | handleViewEvent(data: any) { 93 | data.event = this.eventName['page'] 94 | data.page_title = document.title 95 | data.page_total_width = window.innerWidth 96 | data.page_total_height = window.innerHeight 97 | try { 98 | const cache = window.sessionStorage.getItem('_tea_cache_duration') 99 | if (cache) { 100 | const duration = JSON.parse(cache) 101 | data.refer_page_duration_ms = duration ? duration.duration : '' 102 | } 103 | data.scroll_width = document.documentElement.scrollLeft ? document.documentElement.scrollLeft + window.innerWidth : window.innerWidth 104 | data.scroll_height = document.documentElement.scrollTop ? document.documentElement.scrollTop + window.innerHeight : window.innerHeight 105 | data.page_start_ms = window.performance.timing.navigationStart 106 | } catch (e) { 107 | console.log(`page event error ${JSON.stringify(e)}`) 108 | } 109 | return data 110 | } 111 | handleStatisticsEvent(data: any) { 112 | let _data = {} 113 | _data['event'] = this.eventName['static'] 114 | _data['is_html'] = 1 115 | _data['page_key'] = location.href 116 | _data['refer_page_key'] = document.referrer || '' 117 | _data['page_title'] = document.title 118 | _data['page_manual_key'] = this.initConfig.autotrack.page_manual_key || '' 119 | _data['refer_page_manual_key'] = '' 120 | try { 121 | const { lcp } = data 122 | const timing = window.performance.timing 123 | let init_cos = timing.loadEventEnd - timing.navigationStart 124 | _data['page_init_cost_ms'] = parseInt(lcp || (init_cos > 0 ? init_cos : 0)) 125 | _data['page_start_ms'] = timing.navigationStart 126 | } catch (e) { 127 | console.log(`page_statistics event error ${JSON.stringify(e)}`) 128 | } 129 | return _data 130 | } 131 | handleBeadtEvent(data: any) { 132 | data.event = this.eventName['beat'] 133 | data.page_key = window.location.href 134 | data.is_html = 1 135 | data.page_title = document.title 136 | data.page_manual_key = this.initConfig.autotrack.page_manual_key || '' 137 | try { 138 | data.page_viewport_width = window.innerWidth 139 | data.page_viewport_height = window.innerHeight 140 | data.page_total_width = document.documentElement.scrollWidth 141 | data.page_total_height = document.documentElement.scrollHeight 142 | data.scroll_width = document.documentElement.scrollLeft + window.innerWidth 143 | data.scroll_height = document.documentElement.scrollTop + window.innerHeight 144 | data.since_page_start_ms = Date.now() - window.performance.timing.navigationStart 145 | data.page_start_ms = window.performance.timing.navigationStart 146 | } catch (e) { 147 | console.log(`beat event error ${JSON.stringify(e)}`) 148 | } 149 | return data 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/plugin/track/exposure/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Observer from "./observer"; 4 | import Intersection from "./intersection"; 5 | 6 | class Exposure { 7 | _observer: any 8 | _intersection: any 9 | constructor(config: any, eventHandle: any) { 10 | if (!config.autotrack || !config.autotrack.exposure) return; 11 | this._intersection = new Intersection(config, eventHandle); 12 | this._observer = new Observer(this._intersection); 13 | if (this._intersection && this._observer) { 14 | this.initObserver() 15 | } else { 16 | console.log('your browser version cannot support exposure, please update~') 17 | } 18 | } 19 | initObserver() { 20 | const self = this; 21 | Array.prototype.forEach.call(document.querySelectorAll('[data-exposure]'), (dom) => { 22 | self._intersection.exposureAdd(dom, 'intersect'); 23 | }); 24 | } 25 | } 26 | export default Exposure 27 | -------------------------------------------------------------------------------- /src/plugin/track/exposure/intersection.ts: -------------------------------------------------------------------------------- 1 | 2 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 3 | 4 | // 曝光监听 5 | export default class Intersection { 6 | static _observer_instance = null; 7 | static _observer_map = new Map(); 8 | count: number = 1; 9 | instance: any 10 | observeMap: any 11 | Ratio: number 12 | EventHandle: any 13 | constructor(config: any, eventHandle: any) { 14 | this.instance = this.buildObserver(); 15 | this.observeMap = Intersection._observer_map; 16 | if (config.autotrack.exposure.ratio) { 17 | this.Ratio = config.autotrack.exposure.ratio 18 | } else if (config.autotrack.exposure.ratio === 0) { 19 | this.Ratio = 0 20 | } else { 21 | this.Ratio = 0.5 22 | } 23 | this.EventHandle = eventHandle 24 | } 25 | 26 | // dom元素出现在视窗,出现回调; 27 | buildObserver() { 28 | if (!Intersection._observer_instance) { 29 | if (IntersectionObserver) { 30 | Intersection._observer_instance = new IntersectionObserver(entries => { 31 | entries.forEach(entry => { 32 | const exposureDom = this.observeMap.get(entry.target['_observeId']); 33 | if (exposureDom) { 34 | this.exposureEvent(entry); 35 | } 36 | }); 37 | }, { 38 | threshold: [0.01, 0.25, 0.5, 0.75, 1], 39 | }); 40 | } 41 | return Intersection._observer_instance; 42 | } else { 43 | console.log('your browser cannot support IntersectionObserver'); 44 | return null 45 | } 46 | } 47 | 48 | // 添加进入曝光队列 49 | exposureAdd(dom: any, type: string) { 50 | let _dom = dom; 51 | if (type === 'mutation') { 52 | _dom = dom.target; 53 | } 54 | const count = _dom['_observeId']; 55 | if (!count && !this.observeMap.has(count)) { 56 | _dom['_observeId'] = this.count; 57 | _dom['visible'] = false; 58 | this.observeMap.set(this.count, _dom); 59 | this.observe(_dom); 60 | this.count++; 61 | } else { 62 | if (_dom['visible'] === false) { 63 | const { top, left, right, bottom } = _dom.getBoundingClientRect(); 64 | if (top >= 0 && bottom <= window.innerHeight && left >= 0 && right <= window.innerWidth) { 65 | _dom['visible'] = true; 66 | this.EventHandle({ eventType: 'dom', eventName: 'exposure' }, dom) 67 | } 68 | } 69 | } 70 | } 71 | 72 | // 从曝光队列中移除 73 | exposureRemove(dom: Element) { 74 | if (this.observeMap.has(dom['_observeId'])) { 75 | this.observeMap.delete(dom['_observeId']) 76 | this.unobserve(dom) 77 | } 78 | } 79 | exposureEvent(entry) { 80 | if (entry.intersectionRatio >= this.Ratio && entry.isIntersecting) { 81 | if (entry.target.style.opacity === '0' || entry.target.style.visibility === 'hidden') return; 82 | if (entry.target.visible === true) return; 83 | entry.target.visible = true; 84 | this.EventHandle({ eventType: 'dom', eventName: 'exposure' }, entry) 85 | } else { 86 | entry.target.visible = false; 87 | } 88 | } 89 | observe(dom: Element) { 90 | this.instance && this.instance.observe(dom); 91 | } 92 | 93 | unobserve(dom: Element) { 94 | this.instance && this.instance.unobserve(dom); 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /src/plugin/track/exposure/observer.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | // 监视曝光的上层类 处理动态dom 4 | export default class Observer { 5 | 6 | static _exposure_observer = null; 7 | _instance: any 8 | _intersection: any 9 | constructor(intersection: any) { 10 | this._instance = null; 11 | this._intersection = intersection; 12 | if (!this._intersection) return; 13 | this.init(); 14 | } 15 | 16 | // 初始化mutation对象观察动态dom 17 | init() { 18 | if (MutationObserver) { 19 | this._instance = new MutationObserver((mutations) => { 20 | mutations.forEach((mutation) => { 21 | // 更新dom节点可能是update,只是属性改变,需要监听 22 | if (mutation.type === 'attributes') { 23 | this.attributeChangeObserve(mutation); 24 | } 25 | // dom节点变化 26 | if (mutation.type === 'childList') { 27 | this.modifyNodeObserve(mutation); 28 | } 29 | }); 30 | }); 31 | 32 | this._instance.observe(document.body, { 33 | childList: true, attributes: true, subtree: true, attributeOldValue: false, 34 | }); 35 | } else { 36 | console.log('your browser cannot support MutationObserver') 37 | } 38 | } 39 | 40 | 41 | // 监听dom属性变化添加或删除节点曝光监听 42 | attributeChangeObserve(mutation) { 43 | const dom = mutation.target; 44 | if (dom.hasAttribute('data-exposure')) { 45 | this.exposureAdd(mutation, 'mutation'); 46 | } else { 47 | this.exposureRemove(mutation); 48 | } 49 | } 50 | 51 | // 监听dom变化添加或删除节点曝光监听 52 | modifyNodeObserve(mutation) { 53 | // 不能为文本节点&&要有auto-exp属性 54 | Array.prototype.forEach.call(mutation.addedNodes, (node) => { 55 | if (node.nodeType === 1 && node.hasAttribute('data-exposure')) { 56 | this.exposureAdd(node, 'intersect'); 57 | } 58 | 59 | this.mapChild(node, this.exposureAdd); 60 | }); 61 | 62 | // 遍历子节点的删除 63 | Array.prototype.forEach.call(mutation.removedNodes, (node) => { 64 | if (node.nodeType === 1 && node.hasAttribute('data-exposure')) { 65 | this.exposureRemove(node); 66 | } 67 | this.mapChild(node, this.exposureRemove); 68 | }) 69 | } 70 | 71 | // 递归后代节点 72 | mapChild(node, action) { 73 | if (node.nodeType !== 1) { 74 | return; 75 | } 76 | if (!node.children.length) { 77 | return; 78 | } 79 | 80 | Array.prototype.forEach.call(node.children, (item) => { 81 | if (item.nodeType === 1 && item.hasAttribute('data-exposure')) { 82 | action(item); 83 | } 84 | this.mapChild(item, action); 85 | }); 86 | } 87 | 88 | // 添加进入曝光队列 89 | exposureAdd(dom: Element, type: string) { 90 | try { 91 | this._intersection && this._intersection.exposureAdd(dom, type); 92 | } catch (e) { 93 | console.log('intersection error', JSON.stringify(e.message)) 94 | } 95 | 96 | } 97 | 98 | // 从曝光队列中移除 99 | exposureRemove(dom: Element) { 100 | try { 101 | this._intersection && this._intersection.exposureRemove(dom); 102 | } catch (e) { 103 | console.log('intersection error', JSON.stringify(e.message)) 104 | } 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /src/plugin/track/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Listener from './listener' 4 | import Config, { defaultConfig } from './config' 5 | import EventHandle from './event' 6 | import Request from './request' 7 | import { OptionsType, EventInfo } from './type' 8 | import readyToLoadEditor from './load' 9 | import Exposure from './exposure' 10 | 11 | const defaultOpt = { 12 | hashTag: false, 13 | impr: false, 14 | } 15 | export default class AutoTrack { 16 | engine: any 17 | Listener: Listener 18 | EventHandle: EventHandle 19 | Request: Request 20 | Exposure: Exposure 21 | Config: Config 22 | options: OptionsType 23 | destroyed: boolean 24 | autoTrackStart: boolean 25 | collect: any 26 | config: any 27 | apply(collect: any, config: any) { 28 | this.autoTrackStart = false 29 | this.collect = collect 30 | this.config = config 31 | if (!config.autotrack) return 32 | const { Types } = collect 33 | if (config.autotrack && config.autotrack.collect_url) { 34 | if (!config.autotrack.collect_url()) return; 35 | } 36 | this.ready(Types.Autotrack) 37 | this.collect.emit(Types.AutotrackReady) 38 | } 39 | ready(name: string) { 40 | this.collect.set(name) 41 | let options = this.config.autotrack 42 | options = typeof options === 'object' ? options : {} 43 | options = Object.assign(defaultOpt, options) 44 | this.destroyed = false 45 | this.options = options 46 | this.Config = new Config(defaultConfig, this.options) 47 | this.Exposure = new Exposure(this.config, this.handle.bind(this)) 48 | this.Listener = new Listener(options, this.collect, this.Config) 49 | this.EventHandle = new EventHandle(this.config, options) 50 | this.Request = new Request(this.collect) 51 | this.autoTrackStart = true 52 | this.init() 53 | readyToLoadEditor(this, this.config) 54 | } 55 | init() { 56 | this.Listener.init(this.handle.bind(this)) 57 | } 58 | handle(_eventInfo: EventInfo, _data?: any) { 59 | const { eventType } = _eventInfo 60 | if (eventType === 'dom') { 61 | this.handleDom(_eventInfo, _data) 62 | } 63 | } 64 | handleDom(eventInfo: EventInfo, data: any) { 65 | try { 66 | const { eventName } = eventInfo 67 | if (eventName === 'click' || eventName === 'exposure' || eventName === 'change' || eventName === 'submit') { 68 | const handleResult = this.EventHandle.handleEvent(data, eventName) 69 | handleResult !== null && this.Request.send({ eventType: 'custom', eventName: `report_${eventName}_event`, extra: { methods: 'GET' } }, handleResult) 70 | } else if (eventName === 'page_view' || eventName === 'page_statistics') { 71 | let pageData 72 | if (eventName === 'page_view') { 73 | pageData = this.EventHandle.handleViewEvent(data) 74 | } else { 75 | pageData = this.EventHandle.handleStatisticsEvent(data) 76 | } 77 | this.Request.send({ eventType: 'custom', eventName: 'report_${eventName}_event', extra: { methods: 'GET' } }, pageData) 78 | } else if (eventName === 'beat') { 79 | const beatData = this.EventHandle.handleBeadtEvent(data) 80 | const { eventSend } = eventInfo 81 | this.Request.send({ eventType: 'custom', eventName: 'report_${eventName}_event', extra: { methods: 'GET' }, eventSend }, beatData) 82 | } 83 | } catch (e) { 84 | console.log(`handel dom event error ${JSON.stringify(e)}`) 85 | } 86 | 87 | } 88 | destroy() { 89 | if (!this.autoTrackStart) { 90 | return console.warn('engine is undefined, make sure you have called autoTrack.start()') 91 | } 92 | this.autoTrackStart = false 93 | this.Listener.removeListener() 94 | } 95 | } 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/plugin/track/listener.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { isTrack } from './node' 4 | import { OptionsType, EventConfig } from './type' 5 | import { beforePageUnload } from '../../util/tool' 6 | 7 | export default class Listener { 8 | config: EventConfig 9 | options: OptionsType 10 | beatTime: number 11 | statistics: boolean 12 | eventHandel: Function 13 | collect: any 14 | constructor(options: OptionsType, collect: any, Config: any) { 15 | this.config = Config.getConfig().eventConfig 16 | this.collect = collect 17 | this.options = options 18 | this.beatTime = options.beat 19 | this.statistics = false 20 | } 21 | init(eventHandel: Function) { 22 | this.eventHandel = eventHandel 23 | const mode = this.config.mode 24 | this.addListener(mode) 25 | } 26 | 27 | addListener(mode: 'proxy-capturing') { 28 | // 注册事件捕获 29 | if (mode === 'proxy-capturing') { 30 | if (this.config.click) { 31 | window.document.addEventListener('click', this.clickEvent, true) 32 | } 33 | if (this.config.change) { 34 | window.document.addEventListener('change', this.changeEvent, true) 35 | } 36 | if (this.config.submit) { 37 | window.document.addEventListener('submit', this.submitEvent, true) 38 | } 39 | if (this.config.pv) { 40 | this.collect.on('route-change', (info) => { 41 | const { config, name } = info 42 | this.getPageViewEvent(config, name) 43 | }) 44 | } 45 | if (this.config.beat) { 46 | try { 47 | if (document.readyState === 'complete') { 48 | this.beatEvent(this.beatTime) 49 | } else { 50 | window.addEventListener('load', () => { 51 | this.beatEvent(this.beatTime) 52 | }) 53 | } 54 | let t1 = 0; 55 | let t2 = 0; 56 | let timer = null; // 定时器 57 | window.addEventListener('scroll', () => { 58 | clearTimeout(timer); 59 | timer = setTimeout(isScrollEnd, 500); 60 | t1 = document.documentElement.scrollTop || document.body.scrollTop; 61 | }) 62 | const isScrollEnd = () => { 63 | t2 = document.documentElement.scrollTop || document.body.scrollTop; 64 | if (t2 == t1) { 65 | this.eventHandel({ eventType: 'dom', eventName: 'beat' }, { 66 | beat_type: 1 67 | }) 68 | } 69 | } 70 | } catch (e) { } 71 | try { 72 | var entryList = window.performance && window.performance.getEntriesByType('paint') 73 | if (entryList && entryList.length) { 74 | var observer = new PerformanceObserver((entryList) => { 75 | var entries = entryList.getEntries(); 76 | var lastEntry = entries[entries.length - 1]; 77 | var lcp = lastEntry['renderTime'] || lastEntry['loadTime']; 78 | if (!this.statistics) { 79 | this.getPageLoadEvent(lcp) 80 | this.statistics = true 81 | } 82 | }); 83 | observer.observe({ 84 | entryTypes: ['largest-contentful-paint'] 85 | }); 86 | // 没触发2S后强制触发 87 | setTimeout(() => { 88 | if (this.statistics) return 89 | this.getPageLoadEvent(entryList[0].startTime || 0) 90 | this.statistics = true 91 | }, 2000); 92 | } else { 93 | this.getPageLoadEvent(0) 94 | } 95 | } catch (e) { 96 | this.getPageLoadEvent(0) 97 | } 98 | } 99 | } 100 | } 101 | removeListener() { 102 | window.document.removeEventListener('click', this.clickEvent, true) 103 | window.document.removeEventListener('change', this.changeEvent, true) 104 | window.document.removeEventListener('submit', this.submitEvent, true) 105 | } 106 | clickEvent = (e: Object) => { 107 | if (isTrack(e['target'], this.options)) { 108 | this.eventHandel({ eventType: 'dom', eventName: 'click' }, e) 109 | } 110 | } 111 | changeEvent = (e: Object) => { 112 | this.eventHandel({ eventType: 'dom', eventName: 'change' }, e) 113 | } 114 | submitEvent = (e: Object) => { 115 | this.eventHandel({ eventType: 'dom', eventName: 'submit' }, e) 116 | } 117 | beatEvent(beatTime: number) { 118 | try { 119 | this.eventHandel({ eventType: 'dom', eventName: 'beat' }, { 120 | beat_type: 3 121 | }) 122 | let beaInterval 123 | if (this.beatTime) { 124 | beaInterval = setInterval(() => { 125 | this.eventHandel({ eventType: 'dom', eventName: 'beat' }, { 126 | beat_type: 2 127 | }) 128 | }, beatTime) 129 | } 130 | beforePageUnload(() => { 131 | this.eventHandel({ eventType: 'dom', eventName: 'beat', eventSend: 'becon' }, { 132 | beat_type: 0 133 | }) 134 | if (this.beatTime) { 135 | clearInterval(beaInterval) 136 | } 137 | }) 138 | } catch (e) { } 139 | } 140 | getPageViewEvent = (eventData: Object, name?: string) => { 141 | if (name && name === 'pushState') { 142 | this.eventHandel({ eventType: 'dom', eventName: 'beat' }, { 143 | beat_type: 0, 144 | ...eventData 145 | }) 146 | } 147 | this.eventHandel({ eventType: 'dom', eventName: 'page_view' }, eventData) 148 | } 149 | getPageLoadEvent = (lcp: any) => { 150 | this.eventHandel({ eventType: 'dom', eventName: 'page_statistics' }, { lcp: lcp }) 151 | } 152 | } -------------------------------------------------------------------------------- /src/plugin/track/load.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { init, addAllowdOrigin, dispatchMsg, receiveMsg, IDataReceive } from '../../util/postMessage' 4 | import { checkSession, checkSessionHost, setSession, checkEditUrl } from './session' 5 | import { VISUAL_EDITOR_RANGERS, HOT_PIC_URL, SDK_VERSION } from '../../collect/constant' 6 | import { loadScript } from '../../util/tool' 7 | 8 | interface IAppData { 9 | aid: number; 10 | tid: number; 11 | } 12 | interface IXPath { 13 | xpath: string; 14 | positions: string[]; 15 | } 16 | // eslint-disable-next-line 17 | declare global { 18 | interface Window { 19 | TEAVisualEditor: { 20 | __editor_url?: string; 21 | __editor_ajax_domain?: string; 22 | appId?: number | string; 23 | appData?: IAppData; 24 | lang?: string; 25 | __editor_verison?: string; 26 | __ab_domin?: string; 27 | __ab_config?: any 28 | __ab_appId?: number | string; 29 | getOriginXpath?: (IXPath) => IXPath; 30 | openAutotrackEditor?: () => void; 31 | }; 32 | } 33 | } 34 | 35 | // window.TEAVisualEditor = window.TEAVisualEditor || {} 36 | 37 | let isLoaded = false; 38 | 39 | function loadEditorScript({ event, editorUrl, autoTrackInstance }) { 40 | if (isLoaded) { 41 | return 42 | } 43 | isLoaded = true 44 | loadScript(editorUrl, () => { 45 | dispatchMsg(event, 'editorScriptloadSuccess') 46 | autoTrackInstance.destroy() 47 | 48 | }, 49 | () => { 50 | if (event) dispatchMsg(event, 'editorScriptloadError') 51 | isLoaded = false; 52 | }); 53 | } 54 | 55 | export default function readyToLoadEditor(autoTrackInstance, options) { 56 | window.TEAVisualEditor = window.TEAVisualEditor || {} 57 | let EDITOR_URL = '' 58 | const EDITOR_URL_NEW = `${VISUAL_EDITOR_RANGERS}?query=${Date.now()}` 59 | window.TEAVisualEditor.appId = options.app_id 60 | var isPrivate = options.channel_domain 61 | var _editorUrl = '' 62 | addAllowdOrigin(['*']) 63 | if (isPrivate) { 64 | // 添加域名白名单 65 | var domain 66 | var scriptSrc = '' 67 | try { 68 | var resourceList = window.performance.getEntriesByType('resource') 69 | if (resourceList && resourceList.length) { 70 | resourceList.forEach(item => { 71 | if (item['initiatorType'] === 'script') { 72 | if (item.name && item.name.indexOf('collect') !== -1) { 73 | scriptSrc = item.name 74 | } 75 | } 76 | }) 77 | if (!scriptSrc) { 78 | if (document.currentScript) { 79 | scriptSrc = document.currentScript['src'] 80 | } 81 | } 82 | if (scriptSrc) { 83 | domain = scriptSrc.split('/') 84 | if (domain && domain.length) { 85 | _editorUrl = `https:/` 86 | for (let i = 2; i < domain.length; i++) { 87 | if (i === domain.length - 1) break; 88 | _editorUrl = _editorUrl + `/${domain[i]}` 89 | } 90 | if (_editorUrl && _editorUrl.indexOf('/5.0')) { 91 | const editorAry = _editorUrl.split('/5.0') 92 | _editorUrl = editorAry[0] || _editorUrl 93 | } 94 | } 95 | } 96 | } 97 | } catch (e) { } 98 | } 99 | init(options, SDK_VERSION) 100 | if (checkSession()) { 101 | const API_HOST = checkSessionHost() 102 | let cacheUrl = '' 103 | if (API_HOST) { 104 | window.TEAVisualEditor.__editor_ajax_domain = API_HOST 105 | cacheUrl = checkEditUrl() 106 | } 107 | loadEditorScript({ event: null, editorUrl: cacheUrl || EDITOR_URL_NEW, autoTrackInstance }) 108 | setSession() 109 | } else { 110 | try { 111 | receiveMsg('tea:openVisualEditor', (event) => { 112 | let rawData: IDataReceive = event.data 113 | if (typeof event.data === 'string') { 114 | try { 115 | rawData = JSON.parse(event.data); 116 | } catch (e) { 117 | rawData = undefined; 118 | } 119 | } 120 | if (!rawData) return 121 | const { referrer, lang } = rawData 122 | if (referrer) { 123 | window.TEAVisualEditor.__editor_ajax_domain = referrer 124 | } 125 | EDITOR_URL = EDITOR_URL_NEW 126 | if (isPrivate) { 127 | const { version } = rawData 128 | const _version = version ? `/visual-editor-rangers-v${version}` : '/visual-editor-rangers-v1.0.0' 129 | if (_editorUrl) { 130 | EDITOR_URL = `${_editorUrl}${_version}.js` 131 | } else { 132 | EDITOR_URL = EDITOR_URL_NEW 133 | } 134 | window.TEAVisualEditor.__editor_verison = version 135 | } 136 | window.TEAVisualEditor.__editor_url = EDITOR_URL 137 | window.TEAVisualEditor.lang = lang 138 | loadEditorScript({ event, editorUrl: EDITOR_URL, autoTrackInstance }) 139 | setSession() 140 | }) 141 | window.TEAVisualEditor.openAutotrackEditor = () => { 142 | loadEditorScript({ event: null, editorUrl: window.TEAVisualEditor.__editor_url, autoTrackInstance }) 143 | } 144 | } catch (e) { 145 | console.log('receive message error') 146 | } 147 | } 148 | try { 149 | receiveMsg('tea:openHeatMapCore', (event) => { 150 | let hotUrl = HOT_PIC_URL 151 | loadEditorScript({ event, editorUrl: `${hotUrl}.js?query=${Date.now()}`, autoTrackInstance }) 152 | }) 153 | } catch (e) { 154 | console.log('openHeatMapCore error') 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/plugin/track/node.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { isNeedElement, isAttrFilter } from './dom' 4 | 5 | 6 | const elementLevel = (element) => { 7 | if (element.children.length) { 8 | const childElement = element.children 9 | if ([].slice.call(childElement).some(element => element.children.length > 0)) { 10 | return false 11 | } 12 | return true 13 | } 14 | return true 15 | }; 16 | 17 | const isSVG = (element) => { 18 | if (element.tagName.toLowerCase() === 'svg') { 19 | return true 20 | } 21 | let parent = element.parentElement 22 | let flag = false 23 | while (parent) { 24 | if (parent.tagName.toLowerCase() === 'svg') { 25 | parent = null 26 | flag = true 27 | } else { 28 | parent = parent.parentElement 29 | } 30 | } 31 | return flag 32 | } 33 | 34 | export function isTrack(node: Element, options: any): boolean { 35 | 36 | if (node.nodeType !== 1) { 37 | return false 38 | } 39 | if (!options.svg && isSVG(node)) { 40 | return false 41 | } 42 | if (['HTML', 'BODY'].includes(node.tagName.toUpperCase())) { 43 | return false 44 | } 45 | 46 | const element = node as HTMLElement 47 | if (element.style.display === 'none') { 48 | return false 49 | } 50 | if (isNeedElement(element, 'container')) { 51 | return true 52 | } 53 | if (options.track_attr) { 54 | if (isAttrFilter(element, options.track_attr)) { 55 | return true 56 | } 57 | } 58 | 59 | if (!elementLevel(element)) { 60 | return false 61 | } 62 | 63 | return true 64 | } 65 | -------------------------------------------------------------------------------- /src/plugin/track/path.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import { isNeedElement } from './dom' 4 | 5 | export function getPositionData(target: HTMLElement) { 6 | if (!target) { 7 | return 8 | } 9 | const rect = target.getBoundingClientRect() 10 | const { width, height, left, top } = rect 11 | return { 12 | left, 13 | top, 14 | element_width: width, 15 | element_height: height, 16 | } 17 | } 18 | 19 | export function getEventData(event: any = {}, extData: any = {}) { 20 | const { clientX, clientY } = event 21 | const { left, top } = extData 22 | const touchX = clientX - left >= 0 ? clientX - left : 0 23 | const touchY = clientY - top >= 0 ? clientY - top : 0 24 | return { 25 | touch_x: Math.floor(touchX), 26 | touch_y: Math.floor(touchY), 27 | } 28 | } 29 | 30 | export function getXpath(target: HTMLElement): { element_path: string, positions: Array } { 31 | const targetList = [] 32 | while (target.parentElement !== null) { 33 | targetList.push(target) 34 | target = target.parentElement 35 | } 36 | 37 | let xpathArr: Array = [] 38 | const positions: Array = [] 39 | targetList.forEach(cur => { 40 | const { str, index } = getXpathIndex(cur) 41 | xpathArr.unshift(str) 42 | positions.unshift(index) 43 | }) 44 | return { element_path: `/${xpathArr.join('/')}`, positions } 45 | } 46 | 47 | function getXpathIndex(dom: HTMLElement): { str: string, index: number } { 48 | if (dom === null) { 49 | return { str: '', index: 0 } 50 | } 51 | let index = 0 52 | const parent = dom.parentElement 53 | if (parent) { 54 | const childrens = parent.children 55 | for (let i = 0; i < childrens.length; i++) { 56 | if (childrens[i] === dom) break 57 | if (childrens[i].nodeName === dom.nodeName) { 58 | index++ 59 | } 60 | } 61 | } 62 | const tag = [ 63 | dom.nodeName.toLowerCase(), 64 | (isNeedElement(dom, 'list') ? '[]' : '') 65 | ].join('') 66 | return { str: tag, index: index } 67 | } 68 | -------------------------------------------------------------------------------- /src/plugin/track/request.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 4 | 5 | import { EventInfo } from './type' 6 | 7 | export type RequestOptions = { 8 | headers?: RequestHeaders, 9 | body?: string, 10 | cache?: string, 11 | credentials?: string, 12 | method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH', 13 | mode?: string, 14 | redirect?: string, 15 | referrer?: string 16 | } 17 | 18 | export type RequestHeaders = { 19 | 'content-type'?: string, 20 | 'user-agent'?: string, 21 | } 22 | 23 | 24 | 25 | class Request { 26 | logFunc: any 27 | logFuncBecon: any 28 | eventNameList: string[] 29 | collect: any 30 | constructor(collect: any) { 31 | this.collect = collect 32 | this.eventNameList = [ 33 | 'report_click_event', 34 | 'report_change_event', 35 | 'report_submit_event', 36 | 'report_exposure_event', 37 | 'report_page_view_event', 38 | 'report_page_statistics_event', 39 | 'report_beat_event' 40 | ] 41 | } 42 | send(_eventInfo: EventInfo, _data: any) { 43 | const { eventSend } = _eventInfo 44 | const event = _data['event'] 45 | delete _data['event'] 46 | if (eventSend && eventSend === 'becon') { 47 | this.collect.beconEvent(event, _data) 48 | } else { 49 | this.collect.event(event, _data) 50 | } 51 | } 52 | 53 | get(url: string, options: RequestOptions) { 54 | const reqOptions: RequestOptions = { 55 | headers: { 'content-type': 'application/json' }, 56 | method: 'GET' 57 | } 58 | const myOptions: Object = Object.assign(reqOptions, options) 59 | fetch(url, myOptions) 60 | } 61 | 62 | post(url: string, options: RequestOptions) { 63 | const reqOptions: RequestOptions = { 64 | headers: { 'content-type': 'application/json' }, 65 | method: 'POST' 66 | } 67 | const myOptions: Object = Object.assign(reqOptions, options) 68 | fetch(url, myOptions) 69 | } 70 | } 71 | 72 | export default Request 73 | -------------------------------------------------------------------------------- /src/plugin/track/session.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Cookies from 'js-cookie' 4 | 5 | export const COOKIE_KEY = '_TEA_VE_OPEN'; 6 | export const COOKIE_KEY_HOST = '_TEA_VE_APIHOST'; 7 | export const COOKIE_LANG = 'lang' 8 | export const COOKIE_EDIT_VERISON = '_VISUAL_EDITOR_V' 9 | export const COOKIE_EDIT_URL = '_VISUAL_EDITOR_U' 10 | 11 | 12 | export function checkSession(): boolean { 13 | return Cookies.get(COOKIE_KEY) === '1'; 14 | } 15 | 16 | export function checkSessionHost(): string { 17 | let HOST = Cookies.get(COOKIE_KEY_HOST) 18 | try { 19 | HOST = JSON.parse(HOST) 20 | } catch (e) { } 21 | return HOST 22 | } 23 | export function checkEditUrl(): string { 24 | let url = Cookies.get(COOKIE_EDIT_URL) 25 | return url 26 | } 27 | 28 | export function setSession() { 29 | try { 30 | const lang = (window.TEAVisualEditor.lang = window.TEAVisualEditor.lang || Cookies.get(COOKIE_LANG)) 31 | const apiHost = (window.TEAVisualEditor.__editor_ajax_domain = window.TEAVisualEditor.__editor_ajax_domain || Cookies.get(COOKIE_KEY_HOST)) 32 | const verison = (window.TEAVisualEditor.__editor_verison = window.TEAVisualEditor.__editor_verison || Cookies.get(COOKIE_EDIT_VERISON)) 33 | const editUrl = (window.TEAVisualEditor.__editor_url = window.TEAVisualEditor.__editor_url || Cookies.get(COOKIE_EDIT_URL)) 34 | const timestamp = +new Date(); 35 | const furureTimestamp = timestamp + 30 * 60 * 1000; // 30min 36 | const expires = new Date(furureTimestamp) 37 | Cookies.set(COOKIE_KEY, '1', { 38 | expires: expires, 39 | }); 40 | Cookies.set(COOKIE_KEY_HOST, apiHost, { 41 | expires: expires, 42 | }); 43 | Cookies.set(COOKIE_EDIT_URL, editUrl, { 44 | expires: expires, 45 | }); 46 | Cookies.set(COOKIE_LANG, lang, { 47 | expires: expires, 48 | }); 49 | Cookies.set(COOKIE_EDIT_VERISON, verison || '', { 50 | expires: expires, 51 | }); 52 | } catch (e) { 53 | console.log('set cookie err') 54 | } 55 | } 56 | 57 | export function removeSession() { 58 | Cookies.remove(COOKIE_KEY); 59 | } 60 | -------------------------------------------------------------------------------- /src/plugin/track/type.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | export type OptionsType = { 4 | hashTag?: boolean, 5 | impr?: boolean, 6 | custom?: string, 7 | beat?: number 8 | } 9 | export type EventInfo = { 10 | eventType: 'custom' | 'dom', 11 | eventName: string, 12 | extra?: Object, 13 | eventSend?: string 14 | } 15 | 16 | export type EventConfig = { 17 | mode: 'proxy-capturing', 18 | submit: boolean, 19 | click: boolean, 20 | change: boolean, 21 | pv: boolean, 22 | beat: boolean, 23 | hashTag: boolean, 24 | impr: boolean 25 | } 26 | 27 | export type ScoutConfig = { 28 | mode: 'xpath' | 'cssSelector' | 'xpathAndCssSelector', 29 | } 30 | 31 | export type ParamsStruct = { 32 | event: string 33 | is_html: 1 34 | page_key: string 35 | element_path: string 36 | positions: Array 37 | texts: Array 38 | element_width?: number 39 | element_height?: number 40 | touch_x?: number 41 | touch_y?: number 42 | href?: string 43 | src?: string 44 | page_title?: string 45 | } -------------------------------------------------------------------------------- /src/plugin/verify/verify_h5.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | // H5埋点实时验证 4 | import { isArray, parseUrlQuery, beforePageUnload } from '../../util/tool' 5 | import fetch from '../../util/fetch' 6 | export default class VerifyH { 7 | collector: any 8 | config: any 9 | eventStorage: any[] 10 | domain: string 11 | verifyReady: boolean = false 12 | cleanStatus: boolean = false 13 | key: string 14 | heart: any 15 | apply(collector: any, config: any) { 16 | this.collector = collector; 17 | this.config = config; 18 | this.eventStorage = []; 19 | this.collector.on('submit-verify-h5', (events: any) => { 20 | if (events && events.length) { 21 | this.eventStore(events[0]); 22 | } 23 | }); 24 | this.checkUrl(); 25 | this.heartbeat(); 26 | } 27 | checkUrl() { 28 | const page = window.location.href; 29 | const query = parseUrlQuery(page); 30 | if (query['_r_d_'] && query['_r_c_k_']) { 31 | this.verifyReady = true; 32 | this.domain = query['_r_d_']; 33 | this.key = query['_r_c_k_'] 34 | this.checkCache(); 35 | } else { 36 | this.collector.off('submit-verify-h5') 37 | } 38 | } 39 | checkCache() { 40 | if (!this.eventStorage.length) return; 41 | this.postVerify(this.eventStorage); 42 | } 43 | heartbeat() { 44 | this.heart = setInterval(() => { 45 | const event = { 46 | event: 'simulator_test__', 47 | local_time_ms: Date.now(), 48 | }; 49 | let { header, user } = this.collector.configManager.get(); 50 | const Data = { 51 | events: [event], 52 | user, 53 | header, 54 | }; 55 | this.eventStore(Data); 56 | }, 1000 * 60) 57 | } 58 | eventStore(events: any) { 59 | if (this.cleanStatus) return; 60 | if (this.verifyReady) { 61 | this.postVerify(events); 62 | } else { 63 | this.eventStorage.push(events); 64 | } 65 | } 66 | cleanVerify() { 67 | this.cleanStatus = true; 68 | this.eventStorage = []; 69 | clearInterval(this.heart); 70 | } 71 | postVerify(events: any) { 72 | try { 73 | const _events = JSON.parse(JSON.stringify(events)); 74 | if (isArray(events)) { 75 | _events.forEach(item => { 76 | this.fetchLog(item); 77 | }) 78 | } else { 79 | this.fetchLog(_events); 80 | } 81 | } catch (e) { 82 | console.log('web verify post error ~'); 83 | } 84 | } 85 | fetchLog(data: any) { 86 | fetch(`${this.domain}/simulator/h5/log?connection_key=${this.key}`, data, 20000, false); 87 | } 88 | leave() { 89 | document.addEventListener('visibilitychange', () => { 90 | if (document.visibilityState === 'hidden') { 91 | this.cleanVerify(); 92 | } 93 | }) 94 | beforePageUnload(() => { 95 | this.cleanVerify(); 96 | }) 97 | } 98 | } -------------------------------------------------------------------------------- /src/util/client.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import UTM from './utm' 4 | import { parseURL, parseUrlQuery } from './tool' 5 | class Client { 6 | appid: number 7 | domain: string 8 | userAgent: string 9 | appVersion: string 10 | utm: any 11 | cookie_expire: number 12 | constructor(app_id: number, domain: string, cookie_expire: number) { 13 | this.appid = app_id 14 | this.domain = domain 15 | this.userAgent = window.navigator.userAgent 16 | this.appVersion = window.navigator.appVersion 17 | this.cookie_expire = cookie_expire 18 | } 19 | init() { 20 | const userAgent = window.navigator.userAgent 21 | const language = window.navigator.language 22 | const referrer = document.referrer 23 | const referrer_host = referrer ? parseURL(referrer).hostname : '' 24 | const urlQueryObj = parseUrlQuery(window.location.href) 25 | const platform = /Mobile|htc|mini|Android|iP(ad|od|hone)/.test(this.appVersion) ? 'wap' : 'web' 26 | 27 | this.utm = UTM(this.appid, urlQueryObj, this.domain, this.cookie_expire) 28 | const _browser = this.browser() 29 | const _os = this.os() 30 | return { 31 | browser: _browser.browser, 32 | browser_version: _browser.browser_version, 33 | platform, // 平台类型 34 | os_name: _os.os_name, // 软件操作系统名称 35 | os_version: _os.os_version, 36 | userAgent, 37 | screen_width: window.screen && window.screen.width, 38 | screen_height: window.screen && window.screen.height, 39 | device_model: this.getDeviceModel(_os.os_name), // 硬件设备型号 40 | language, 41 | referrer, 42 | referrer_host, 43 | utm: this.utm, 44 | latest_data: this.last(referrer, referrer_host) 45 | } 46 | } 47 | last(referrer: string, referrer_host: string) { 48 | let $latest_referrer = '' 49 | let $latest_referrer_host = '' 50 | let $latest_search_keyword = '' 51 | const hostname = location.hostname 52 | let isLast = false 53 | if (referrer && referrer_host && hostname !== referrer_host) { 54 | $latest_referrer = referrer 55 | $latest_referrer_host = referrer_host 56 | isLast = true 57 | const referQuery = parseUrlQuery(referrer) 58 | if (referQuery['keyword']) { 59 | $latest_search_keyword = referQuery['keyword'] 60 | } 61 | } 62 | return { $latest_referrer, $latest_referrer_host, $latest_search_keyword, isLast } 63 | } 64 | browser() { 65 | let browser = '' 66 | let browser_version = `${parseFloat(this.appVersion)}` 67 | let versionOffset 68 | let semiIndex 69 | const userAgent = this.userAgent 70 | if (userAgent.indexOf('Edge') !== -1 || userAgent.indexOf('Edg') !== -1) { 71 | browser = 'Microsoft Edge' 72 | if (userAgent.indexOf('Edge') !== -1) { 73 | versionOffset = userAgent.indexOf('Edge') 74 | browser_version = userAgent.substring(versionOffset + 5) 75 | } else { 76 | versionOffset = userAgent.indexOf('Edg') 77 | browser_version = userAgent.substring(versionOffset + 4) 78 | } 79 | } else if ((versionOffset = userAgent.indexOf('MSIE')) !== -1) { 80 | browser = 'Microsoft Internet Explorer' 81 | browser_version = userAgent.substring(versionOffset + 5) 82 | } else if ((versionOffset = userAgent.indexOf('Lark')) !== -1) { 83 | browser = 'Lark' 84 | browser_version = userAgent.substring(versionOffset + 5, versionOffset + 11) 85 | } else if ((versionOffset = userAgent.indexOf('MetaSr')) !== -1) { 86 | browser = 'sougoubrowser' 87 | browser_version = userAgent.substring(versionOffset + 7, versionOffset + 10) 88 | } else if (userAgent.indexOf('MQQBrowser') !== -1 || userAgent.indexOf('QQBrowser') !== -1) { 89 | browser = 'qqbrowser' 90 | if (userAgent.indexOf('MQQBrowser') !== -1) { 91 | versionOffset = userAgent.indexOf('MQQBrowser') 92 | browser_version = userAgent.substring(versionOffset + 11, versionOffset + 15) 93 | } else if (userAgent.indexOf('QQBrowser') !== -1) { 94 | versionOffset = userAgent.indexOf('QQBrowser') 95 | browser_version = userAgent.substring(versionOffset + 10, versionOffset + 17) 96 | } 97 | } else if (userAgent.indexOf('Chrome') !== -1) { 98 | if ((versionOffset = userAgent.indexOf('MicroMessenger')) !== -1) { 99 | browser = 'weixin' 100 | browser_version = userAgent.substring(versionOffset + 15, versionOffset + 20) 101 | } else if ((versionOffset = userAgent.indexOf('360')) !== -1) { 102 | browser = '360browser' 103 | browser_version = userAgent.substring(userAgent.indexOf('Chrome') + 7); 104 | } else if (userAgent.indexOf('baidubrowser') !== -1 || userAgent.indexOf('BIDUBrowser') !== -1) { 105 | if (userAgent.indexOf('baidubrowser') !== -1) { 106 | versionOffset = userAgent.indexOf('baidubrowser') 107 | browser_version = userAgent.substring(versionOffset + 13, versionOffset + 16) 108 | } else if (userAgent.indexOf('BIDUBrowser') !== -1) { 109 | versionOffset = userAgent.indexOf('BIDUBrowser') 110 | browser_version = userAgent.substring(versionOffset + 12, versionOffset + 15) 111 | } 112 | browser = 'baidubrowser' 113 | } else if ((versionOffset = userAgent.indexOf('xiaomi')) !== -1) { 114 | if (userAgent.indexOf('openlanguagexiaomi') !== -1) { 115 | browser = 'openlanguage xiaomi' 116 | browser_version = userAgent.substring(versionOffset + 7, versionOffset + 13) 117 | } else { 118 | browser = 'xiaomi' 119 | browser_version = userAgent.substring(versionOffset - 7, versionOffset - 1) 120 | } 121 | } else if ((versionOffset = userAgent.indexOf('TTWebView')) !== -1) { 122 | browser = 'TTWebView' 123 | browser_version = userAgent.substring(versionOffset + 10, versionOffset + 23) 124 | } else if ((versionOffset = userAgent.indexOf('Chrome')) !== -1) { 125 | browser = 'Chrome' 126 | browser_version = userAgent.substring(versionOffset + 7) 127 | } else if ((versionOffset = userAgent.indexOf('Chrome')) !== -1) { 128 | browser = 'Chrome' 129 | browser_version = userAgent.substring(versionOffset + 7) 130 | } 131 | } else if (userAgent.indexOf('Safari') !== -1) { 132 | if ((versionOffset = userAgent.indexOf('QQ')) !== -1) { 133 | browser = 'qqbrowser' 134 | browser_version = userAgent.substring(versionOffset + 10, versionOffset + 16) 135 | } else if ((versionOffset = userAgent.indexOf('Safari')) !== -1) { 136 | browser = 'Safari' 137 | browser_version = userAgent.substring(versionOffset + 7) 138 | if ((versionOffset = userAgent.indexOf('Version')) !== -1) { 139 | browser_version = userAgent.substring(versionOffset + 8) 140 | } 141 | } 142 | } else if ((versionOffset = userAgent.indexOf('Firefox')) !== -1) { 143 | browser = 'Firefox' 144 | browser_version = userAgent.substring(versionOffset + 8) 145 | } else if ((versionOffset = userAgent.indexOf('MicroMessenger')) !== -1) { 146 | browser = 'weixin' 147 | browser_version = userAgent.substring(versionOffset + 15, versionOffset + 20) 148 | } else if ((versionOffset = userAgent.indexOf('QQ')) !== -1) { 149 | browser = 'qqbrowser' 150 | browser_version = userAgent.substring(versionOffset + 3, versionOffset + 8) 151 | } else if ((versionOffset = userAgent.indexOf('Opera')) !== -1) { 152 | browser = 'Opera' 153 | browser_version = userAgent.substring(versionOffset + 6) 154 | if ((versionOffset = userAgent.indexOf('Version')) !== -1) { 155 | browser_version = userAgent.substring(versionOffset + 8) 156 | } 157 | } 158 | if ((semiIndex = browser_version.indexOf(';')) !== -1) { 159 | browser_version = browser_version.substring(0, semiIndex) 160 | } 161 | if ((semiIndex = browser_version.indexOf(' ')) !== -1) { 162 | browser_version = browser_version.substring(0, semiIndex) 163 | } 164 | if ((semiIndex = browser_version.indexOf(')')) !== -1) { 165 | browser_version = browser_version.substring(0, semiIndex) 166 | } 167 | return { browser, browser_version } 168 | } 169 | os() { 170 | let os_name = '' 171 | let os_version: any = '' 172 | const clientOpts = [ 173 | { 174 | s: 'Windows 10', 175 | r: /(Windows 10.0|Windows NT 10.0|Windows NT 10.1)/, 176 | }, 177 | { 178 | s: 'Windows 8.1', 179 | r: /(Windows 8.1|Windows NT 6.3)/, 180 | }, 181 | { 182 | s: 'Windows 8', 183 | r: /(Windows 8|Windows NT 6.2)/, 184 | }, 185 | { 186 | s: 'Windows 7', 187 | r: /(Windows 7|Windows NT 6.1)/, 188 | }, 189 | { 190 | s: 'Android', 191 | r: /Android/, 192 | }, 193 | { 194 | s: 'Sun OS', 195 | r: /SunOS/, 196 | }, 197 | { 198 | s: 'Linux', 199 | r: /(Linux|X11)/, 200 | }, 201 | { 202 | s: 'iOS', 203 | r: /(iPhone|iPad|iPod)/, 204 | }, 205 | { 206 | s: 'Mac OS X', 207 | r: /Mac OS X/, 208 | }, 209 | { 210 | s: 'Mac OS', 211 | r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/, 212 | }, 213 | ]; 214 | for (let i = 0; i < clientOpts.length; i++) { 215 | const cs = clientOpts[i] 216 | if (cs.r.test(this.userAgent)) { 217 | os_name = cs.s 218 | if (os_name === 'Mac OS X' && this.isNewIpad()) { 219 | os_name = 'iOS' 220 | } 221 | break 222 | } 223 | } 224 | const getVersion = (reg, ua) => { 225 | const result = reg.exec(ua) 226 | if (result && result[1]) { 227 | return result[1] 228 | } 229 | return '' 230 | } 231 | const getMacVersion = (rawRegex, ua) => { 232 | const regexInstance = RegExp(`(?:^|[^A-Z0-9-_]|[^A-Z0-9-]_|sprd-)(?:${rawRegex})`, "i"); 233 | const match = regexInstance.exec(ua) 234 | if (match) { 235 | const result = match.slice(1) 236 | return result[0] 237 | } 238 | return '' 239 | } 240 | if (/Windows/.test(os_name)) { 241 | os_version = getVersion(/Windows (.*)/, os_name) 242 | os_name = 'windows' 243 | } 244 | 245 | const getAndroidVersion = (ua) => { 246 | let version = getVersion(/Android ([\.\_\d]+)/, ua) 247 | if (!version) { 248 | version = getVersion(/Android\/([\.\_\d]+)/, ua) 249 | } 250 | return version 251 | } 252 | switch (os_name) { 253 | case 'Mac OS X': 254 | os_version = getMacVersion('Mac[ +]OS[ +]X(?:[ /](?:Version )?(\\d+(?:[_\\.]\\d+)+))?', this.userAgent) 255 | os_name = 'mac' 256 | break 257 | case 'Android': 258 | os_version = getAndroidVersion(this.userAgent) 259 | os_name = 'android' 260 | break; 261 | case 'iOS': 262 | if (this.isNewIpad()) { 263 | os_version = getMacVersion('Mac[ +]OS[ +]X(?:[ /](?:Version )?(\\d+(?:[_\\.]\\d+)+))?', this.userAgent) 264 | } else { 265 | os_version = /OS (\d+)_(\d+)_?(\d+)?/.exec(this.appVersion) 266 | if (!os_version) { 267 | os_version = '' 268 | } else { 269 | os_version = `${os_version[1]}.${os_version[2]}.${os_version[3] | 0}` 270 | } 271 | } 272 | os_name = 'ios' 273 | break 274 | } 275 | return { os_name, os_version } 276 | } 277 | getDeviceModel(osName: string) { 278 | let model = '' 279 | try { 280 | if (osName === 'android') { 281 | const tempArray = navigator.userAgent.split(';') 282 | tempArray.forEach(function (item) { 283 | if (item.indexOf('Build/') > -1) { 284 | model = item.slice(0, item.indexOf('Build/')) 285 | } 286 | }) 287 | } else if (osName === 'ios' || osName === 'mac' || osName === 'windows') { 288 | if (this.isNewIpad()) { 289 | model = 'iPad' 290 | } else { 291 | const temp = navigator.userAgent.replace('Mozilla/5.0 (', '') 292 | const firstSeperatorIndex = temp.indexOf(';') 293 | model = temp.slice(0, firstSeperatorIndex) 294 | } 295 | } 296 | } catch (e) { 297 | return model.trim() 298 | } 299 | return model.trim() 300 | } 301 | isNewIpad() { 302 | return this.userAgent !== undefined && navigator.platform === 'MacIntel' 303 | && typeof navigator.maxTouchPoints === 'number' && navigator.maxTouchPoints > 1 304 | } 305 | } 306 | export default Client -------------------------------------------------------------------------------- /src/util/fetch.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | const ERROR = { 4 | NO_URL: 4001, 5 | IMG_ON: 4000, 6 | IMG_CATCH: 4002, 7 | BEACON_FALSE: 4003, 8 | XHR_ON: 500, 9 | RESPONSE: 5001, 10 | TIMEOUT: 5005, 11 | } 12 | export default function fetch(url: string, data: any, timeout?: number, withCredentials?: boolean, success?: any, fail?: any, app_key?: string, method?: string, encryption?: boolean, encryption_header?: string): void { 13 | try { 14 | var xhr = new XMLHttpRequest() 15 | var _method = method || 'POST' 16 | xhr.open(_method, `${url}`, true) 17 | if (encryption && encryption_header) { 18 | xhr.setRequestHeader('Content-Type', `application/octet-stream;tt-data=${encryption_header}`) 19 | } else { 20 | xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8') 21 | } 22 | if (app_key) { 23 | xhr.setRequestHeader('X-MCS-AppKey', `${app_key}`) 24 | } 25 | if (withCredentials) { 26 | xhr.withCredentials = true 27 | } 28 | if (timeout) { 29 | xhr.timeout = timeout 30 | xhr.ontimeout = () => { 31 | fail && fail(data, ERROR.TIMEOUT) 32 | } 33 | } 34 | xhr.onload = () => { 35 | if (success) { 36 | var res = null 37 | if (xhr.responseText) { 38 | try { 39 | res = JSON.parse(xhr.responseText) 40 | } catch (e) { 41 | res = {} 42 | } 43 | success(res, data) 44 | } 45 | } 46 | } 47 | xhr.onerror = () => { 48 | xhr.abort() 49 | fail && fail(data, ERROR.XHR_ON) 50 | } 51 | if (encryption) { 52 | xhr.send(data) 53 | } else { 54 | xhr.send(JSON.stringify(data)) 55 | } 56 | } catch (e) { } 57 | } 58 | -------------------------------------------------------------------------------- /src/util/hook.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | export type THookInfo = any; 4 | export type THook = (hookInfo?: THookInfo) => void; 5 | export interface IHooks { 6 | [key: string]: THook[]; 7 | } 8 | 9 | class Hook { 10 | _hooks: IHooks; 11 | _cache: any 12 | _hooksCache: any 13 | constructor() { 14 | this._hooks = {}; 15 | this._cache = []; 16 | this._hooksCache = {}; 17 | } 18 | 19 | on(type: string, hook: THook) { 20 | if (!type || !hook || typeof hook !== 'function') { 21 | return; 22 | } 23 | if (!this._hooks[type]) { 24 | this._hooks[type] = []; 25 | } 26 | this._hooks[type].push(hook); 27 | } 28 | 29 | once(type: string, hook: THook) { 30 | if (!type || !hook || typeof hook !== 'function') { 31 | return; 32 | } 33 | const proxyHook: THook = (hookInfo: THookInfo) => { 34 | hook(hookInfo); 35 | this.off(type, proxyHook); 36 | }; 37 | this.on(type, proxyHook); 38 | } 39 | 40 | off(type: string, hook?: THook) { 41 | if (!type || !this._hooks[type] || !this._hooks[type].length) { 42 | return; 43 | } 44 | if (hook) { 45 | const index = this._hooks[type].indexOf(hook); 46 | if (index !== -1) { 47 | this._hooks[type].splice(index, 1); 48 | } 49 | } else { 50 | this._hooks[type] = []; 51 | } 52 | } 53 | 54 | emit(type: string, info?: THookInfo, wait?: string) { 55 | if (!wait) { 56 | this._emit(type, info) 57 | } else { 58 | if (!type) { 59 | return 60 | } 61 | if (this._cache.indexOf(wait) !== -1) { 62 | this._emit(type, info) 63 | } else { 64 | if (!this._hooksCache.hasOwnProperty(wait)) { 65 | this._hooksCache[wait] = {} 66 | } 67 | if (!this._hooksCache[wait].hasOwnProperty(type)) { 68 | this._hooksCache[wait][type] = [] 69 | } 70 | this._hooksCache[wait][type].push(info) 71 | } 72 | } 73 | 74 | } 75 | _emit(type: string, info?: THookInfo) { 76 | if (!type || !this._hooks[type] || !this._hooks[type].length) { 77 | return; 78 | } 79 | [...this._hooks[type]].forEach((hook) => { 80 | try { 81 | hook(info); 82 | } catch (e) { 83 | //TODO 84 | } 85 | }); 86 | } 87 | set(type: string) { 88 | if (!type || this._cache.indexOf(type) !== -1) { 89 | return 90 | } 91 | this._cache.push(type) 92 | } 93 | } 94 | 95 | export default Hook; -------------------------------------------------------------------------------- /src/util/jsbridge.js: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | // @ts-nocheck 4 | class AppBridge { 5 | constructor(config, cfg) { 6 | this.native = config.enable_native || config['evitaN'.split('').reverse().join('')] 7 | this.os = cfg.get('os_name') 8 | } 9 | bridgeInject(){ 10 | try { 11 | if (!this.native) return false 12 | if (AppLogBridge) { 13 | console.log(`AppLogBridge is injected`) 14 | return true 15 | } else { 16 | console.log(`AppLogBridge is not inject`) 17 | return false 18 | } 19 | } catch(e) { 20 | console.log(`AppLogBridge is not inject`) 21 | return false 22 | } 23 | } 24 | bridgeReady(){ 25 | return new Promise((resolve, reject) => { 26 | try { 27 | if (this.bridgeInject()) { 28 | AppLogBridge.hasStarted(start => { 29 | console.log(`AppLogBridge is started? : ${start}`) 30 | if (start) { 31 | resolve(true) 32 | } else { 33 | reject(false) 34 | } 35 | }) 36 | } else { 37 | reject(false) 38 | } 39 | } catch (e) { 40 | console.log(`AppLogBridge, error:${JSON.stringify(e.stack)}`) 41 | reject(false) 42 | } 43 | }) 44 | } 45 | setNativeAppId(appId) { 46 | try { 47 | AppLogBridge.setNativeAppId(JSON.stringify(appId)) 48 | console.log(`change bridge appid, event report with appid: ${appId}`) 49 | } catch (e) { 50 | console.error(`setNativeAppId error`) 51 | } 52 | } 53 | setConfig(info) { 54 | try { 55 | Object.keys(info).forEach((key) => { 56 | if (key === 'user_unique_id'){ 57 | this.setUserUniqueId(info[key]) 58 | } else { 59 | if (info[key]) { 60 | this.addHeaderInfo(key, info[key]) 61 | } else { 62 | this.removeHeaderInfo(key) 63 | } 64 | } 65 | }) 66 | } catch (e) { 67 | console.error(`setConfig error`) 68 | } 69 | } 70 | setUserUniqueId(uuid){ 71 | try { 72 | AppLogBridge.setUserUniqueId(uuid) 73 | } catch(e) { 74 | console.error(`setUserUniqueId error`) 75 | } 76 | } 77 | addHeaderInfo(key, value){ 78 | try { 79 | AppLogBridge.addHeaderInfo(key, value) 80 | } catch (e){ 81 | console.error(`addHeaderInfo error`) 82 | } 83 | } 84 | setHeaderInfo(map){ 85 | try { 86 | AppLogBridge.setHeaderInfo(JSON.stringify(map)) 87 | } catch (e) { 88 | console.error(`setHeaderInfo error`) 89 | } 90 | } 91 | removeHeaderInfo(key){ 92 | try { 93 | AppLogBridge.removeHeaderInfo(key) 94 | } catch (e) { 95 | console.error(`removeHeaderInfo error`) 96 | } 97 | } 98 | reportPv(params) { 99 | this.onEventV3('predefine_pageview', params) 100 | } 101 | onEventV3(event, params) { 102 | try { 103 | AppLogBridge.onEventV3(event, params) 104 | } catch(e) { 105 | console.error(`onEventV3 error`) 106 | } 107 | } 108 | profileSet(profile){ 109 | try { 110 | AppLogBridge.profileSet(profile) 111 | } catch (error) { 112 | console.error(`profileSet error`) 113 | } 114 | } 115 | profileSetOnce(profile){ 116 | try { 117 | AppLogBridge.profileSetOnce(profile) 118 | } catch (error) { 119 | console.error(`profileSetOnce error`) 120 | } 121 | } 122 | profileIncrement(profile){ 123 | try { 124 | AppLogBridge.profileIncrement(profile) 125 | } catch (error) { 126 | console.error(`profileIncrement error`) 127 | } 128 | } 129 | profileUnset(key){ 130 | try { 131 | AppLogBridge.profileUnset(key) 132 | } catch (error) { 133 | console.error(`profileUnset error`) 134 | } 135 | } 136 | profileAppend(profile){ 137 | try { 138 | AppLogBridge.profileAppend(profile) 139 | } catch (error) { 140 | console.error(`profileAppend error`) 141 | } 142 | } 143 | // AB 144 | setExternalAbVersion(vid) { 145 | try { 146 | AppLogBridge.setExternalAbVersion(vid) 147 | } catch (error) { 148 | console.error(`setExternalAbVersion error`) 149 | } 150 | } 151 | getVar(key, defaultValue, callback) { 152 | try { 153 | if (this.os === 'android') { 154 | callback(AppLogBridge.getABTestConfigValueForKey(key, defaultValue)) 155 | alert(`getVar: ${AppLogBridge.getABTestConfigValueForKey(key, defaultValue)}`) 156 | } else { 157 | AppLogBridge.getABTestConfigValueForKey(key, defaultValue, (res) => { 158 | alert(`getVar: ${JSON.stringify(res)}`) 159 | callback(res) 160 | }) 161 | } 162 | } catch (error) { 163 | console.error(`getVar error`) 164 | callback(defaultValue) 165 | } 166 | } 167 | getAllVars(callback) { 168 | try { 169 | if (this.os === 'android') { 170 | callback(AppLogBridge.getAllAbTestConfigs()) 171 | alert(`getAll: ${AppLogBridge.getAllAbTestConfigs()}`) 172 | } else { 173 | AppLogBridge.getAllAbTestConfigs((res) => { 174 | callback(res) 175 | alert(`getAll: ${res}`) 176 | }) 177 | } 178 | } catch (error) { 179 | console.error(`getAllVars error`) 180 | callback(null) 181 | } 182 | } 183 | getAbSdkVersion(callback) { 184 | try { 185 | if (this.os === 'android') { 186 | callback(AppLogBridge.getAbSdkVersion()) 187 | alert(AppLogBridge.getAbSdkVersion()) 188 | } else { 189 | AppLogBridge.getAbSdkVersion(res => { 190 | callback(res) 191 | alert(res) 192 | }) 193 | } 194 | } catch (error) { 195 | console.error(`getAbSdkVersion error`) 196 | callback('') 197 | } 198 | } 199 | } 200 | 201 | export default AppBridge 202 | -------------------------------------------------------------------------------- /src/util/local.js: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | const b = (a) => { 4 | return a ? ( a ^ Math.random() * 16 >> a/4 ).toString(10) : ([1e7] + (-1e3) + (-4e3) + (-8e3) + (-1e11)).replace(/[018]/g,b) 5 | } 6 | const localWebId = () => { 7 | return b().replace(/-/g,'').slice(0,19) 8 | } 9 | export default localWebId -------------------------------------------------------------------------------- /src/util/log.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | export default class Logger { 4 | isLog: boolean 5 | name: string 6 | constructor(name: string, isLog?: boolean) { 7 | this.isLog = isLog || false 8 | this.name = name || '' 9 | } 10 | info(message: string) { 11 | if (this.isLog) { 12 | console.log('%c %s', 'color: yellow; background-color: black;', `[instance: ${this.name}]` + ' ' + message) 13 | } 14 | } 15 | warn(message: string) { 16 | if (this.isLog) { 17 | console.warn('%c %s', 'color: yellow; background-color: black;', `[instance: ${this.name}]` + ' ' + message) 18 | } 19 | } 20 | error(message: string) { 21 | if (this.isLog) { 22 | console.error(`[instance: ${this.name}]` + ' ' + message) 23 | } 24 | } 25 | throw(msg: string) { 26 | this.error(this.name) 27 | throw new Error(msg) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/util/postMessage.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | export type TReceiveMsgCallback = (event: MessageEvent, payload: any) => any; 4 | export interface IDataObj { 5 | type: string; 6 | payload: object | [] | string; 7 | } 8 | export interface IDataReceive { 9 | referrer: string; 10 | type: string; 11 | payload: object | [] | string; 12 | lang?: string; 13 | appId?: number; 14 | version?: string; 15 | } 16 | const msgQueueMap: { [msgType: string]: any[] } = {}; 17 | const allowdOrigins: string[] = []; 18 | 19 | export const addAllowdOrigin = (origin: string[]) => { 20 | if (origin.length) { 21 | origin.forEach(originItem => { 22 | allowdOrigins.push(originItem) 23 | }) 24 | } 25 | } 26 | 27 | export function dispatchMsg( 28 | event: any, 29 | type: string, 30 | payload?: any, 31 | targetOrigin?: string, 32 | ) { 33 | const win: Window = (event && event.source) || window.opener || window.parent; 34 | const origin: string = (event && event.origin) || targetOrigin || '*'; 35 | const msg: IDataObj = { 36 | type, 37 | payload, 38 | }; 39 | win.postMessage(JSON.stringify(msg), origin); 40 | } 41 | 42 | export function receiveMsg(msgType: string, fn: TReceiveMsgCallback) { 43 | msgQueueMap[msgType] = msgQueueMap[msgType] || []; 44 | msgQueueMap[msgType].push(fn); 45 | } 46 | 47 | function processMsg(event: MessageEvent) { 48 | 49 | if (allowdOrigins.some(domain => domain === '*') || 50 | allowdOrigins.some(domain => event.origin === domain) 51 | ) { 52 | 53 | let rawData: IDataReceive = event.data; 54 | if (typeof event.data === 'string') { 55 | try { 56 | rawData = JSON.parse(event.data); 57 | } catch (e) { 58 | rawData = undefined; 59 | } 60 | } 61 | if (!rawData) { 62 | return; 63 | } 64 | 65 | 66 | const { type, payload } = rawData; 67 | if (msgQueueMap[type]) { 68 | msgQueueMap[type].forEach((fn) => { 69 | if (typeof fn === 'function') { 70 | fn(event, payload); 71 | } 72 | }); 73 | } 74 | 75 | } 76 | } 77 | 78 | export function init(config: any, version: string) { 79 | const copyConfig = { ...config } 80 | if (copyConfig['filter']) { 81 | delete copyConfig['filter']; 82 | } 83 | if (typeof copyConfig['autotrack'] === 'object' && copyConfig['autotrack']['collect_url']) { 84 | delete copyConfig['autotrack']['collect_url'] 85 | } 86 | (window.opener || window.parent).postMessage({ 87 | type: 'tea:sdk:info', 88 | config: copyConfig, 89 | version 90 | }, '*'); 91 | window.addEventListener('message', processMsg, false); 92 | } 93 | -------------------------------------------------------------------------------- /src/util/request.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import fetch from './fetch' 4 | const GIF_URL = '/gif' 5 | const ERROR = { 6 | NO_URL: 4001, 7 | IMG_ON: 4000, 8 | IMG_CATCH: 4002, 9 | BEACON_FALSE: 4003, 10 | XHR_ON: 500, 11 | RESPONSE: 5001, 12 | TIMEOUT: 5005, 13 | } 14 | const isSupportBeacon = function () { 15 | if (window.navigator && window.navigator.sendBeacon) { 16 | return true 17 | } else { 18 | return false 19 | } 20 | } 21 | const NOOP = () => { } 22 | const encodePayload = (obj: any) => { 23 | let string = '' 24 | for (const key in obj) { 25 | if (obj.hasOwnProperty(key) && (typeof obj[key] !== 'undefined')) { 26 | string += `&${key}=${encodeURIComponent(JSON.stringify(obj[key]))}` 27 | } 28 | } 29 | string = string[0] === '&' ? string.slice(1) : string 30 | return string 31 | }; 32 | 33 | const sendByImg = (url: string, data: any, success?: any, fail?: any) => { 34 | try { 35 | const splitStringMatch = url.match(/\/v\d\//) 36 | let splitString = '' 37 | if (splitStringMatch) { 38 | splitString = splitStringMatch[0] 39 | } else { 40 | splitString = url.indexOf('/v1/') !== -1 ? '/v1/' : '/v2/' 41 | } 42 | const urlPrefix = url.split(splitString)[0] 43 | if (!urlPrefix) { 44 | fail(url, data, ERROR.NO_URL) 45 | return 46 | } 47 | data.forEach((item) => { 48 | const str = encodePayload(item); 49 | let img = new Image(1, 1) 50 | img.onload = () => { 51 | img = null 52 | success && success() 53 | }; 54 | img.onerror = () => { 55 | img = null 56 | fail && fail(url, data, ERROR.IMG_ON) 57 | } 58 | img.src = `${urlPrefix}${GIF_URL}?${str}` 59 | }) 60 | } catch (e) { 61 | fail && fail(url, data, ERROR.IMG_CATCH, e.message) 62 | } 63 | } 64 | const request = (url: string, data: any, timeout?: number, withCredentials?: boolean, success?: any, fail?: any, sendBecon?: boolean, encryption?: boolean, encryption_header?: string) => { 65 | const UA = window.navigator.userAgent 66 | const browserName = window.navigator.appName 67 | const isIE89 = browserName.indexOf('Microsoft Internet Explorer') !== -1 && 68 | (UA.indexOf('MSIE 8.0') !== -1 || UA.indexOf('MSIE 9.0') !== -1) 69 | if (isIE89) { 70 | sendByImg(url, data, success, fail) 71 | } else { 72 | if (sendBecon) { 73 | if (isSupportBeacon()) { 74 | NOOP() 75 | const status = window.navigator.sendBeacon(url, JSON.stringify(data)) 76 | if (status) { 77 | success() 78 | } else { 79 | fail(url, data, ERROR.BEACON_FALSE) 80 | } 81 | return 82 | } 83 | sendByImg(url, data, success, fail) 84 | return 85 | } 86 | } 87 | fetch(url, data, timeout, withCredentials, success, fail, '', '', encryption, encryption_header) 88 | } 89 | 90 | export default request -------------------------------------------------------------------------------- /src/util/sm2crypto.ts: -------------------------------------------------------------------------------- 1 | import { sm2 } from "sm-crypto"; 2 | /* 3 | sm2加解密 4 | 分为04 非04开头 5 | */ 6 | /** 7 | *生成sm2公钥密钥 04 8 | * 9 | * @export 10 | * @return {publicKey:公钥 privateKey密钥} 11 | */ 12 | export function generateKey04() { 13 | let { publicKey, privateKey } = sm2.generateKeyPairHex(); 14 | return { publicKey, privateKey }; 15 | } 16 | /** 17 | *生成sm2公钥密钥 04 18 | * 19 | * @export 20 | * @return {publicKey:公钥 privateKey密钥} 21 | */ 22 | export function generateKey() { 23 | let { publicKey, privateKey } = sm2.generateKeyPairHex(); 24 | return { 25 | publicKey: publicKey.substring(2), 26 | privateKey: privateKey, 27 | }; 28 | } 29 | /** 30 | *加密 31 | * 32 | * @export 33 | * @param {*} msgString 加密数据 非04开头 34 | * @param {*} publicKey 公钥 35 | * @param {number} [cipherMode=1] 1 - C1C3C2,0 - C1C2C3,默认为1 36 | * @return {*} 加密结果 37 | */ 38 | export function doEncrypt(msgString, publicKey, cipherMode = 1) { 39 | return sm2.doEncrypt(msgString, publicKey, cipherMode); // 加密结果 40 | } 41 | 42 | /** 43 | *解密 44 | * 45 | * @export 46 | * @param {*} msgString 解密数据 非04开头 47 | * @param {*} privateKey 密钥 48 | * @param {*} cipherMode [cipherMode=1] // 1 - C1C3C2,0 - C1C2C3,默认为1 49 | * @return {*} 解密结果 50 | */ 51 | export function doDecrypt(msgString, privateKey, cipherMode = 1) { 52 | return sm2.doDecrypt(msgString.substring(2), privateKey, cipherMode); 53 | } 54 | /** 55 | *加密 56 | * 57 | * @export 58 | * @param {*} msgString 加密数据 04开头 59 | * @param {*} publicKey 公钥 60 | * @param {number} [cipherMode=1] 1 - C1C3C2,0 - C1C2C3,默认为1 61 | * @return {*} 加密结果 62 | */ 63 | export function Encrypt(msgString, publicKey, cipherMode = 1) { 64 | return "04" + sm2.doEncrypt(msgString, publicKey, cipherMode); // 加密结果 65 | } 66 | 67 | /** 68 | *解密 69 | * 70 | * @export 71 | * @param {*} msgString 解密数据 04开头 72 | * @param {*} privateKey 密钥 73 | * @param {*} cipherMode [cipherMode=1] // 1 - C1C3C2,0 - C1C2C3,默认为1 74 | * @return {*} 解密结果 75 | */ 76 | export function Decrypt(msgString, privateKey, cipherMode = 1) { 77 | return sm2.doDecrypt(msgString.substring(2), privateKey, cipherMode); 78 | } 79 | -------------------------------------------------------------------------------- /src/util/storage.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Cookies from 'js-cookie'; 4 | class Memory { 5 | cache: any 6 | constructor() { 7 | this.cache = {} 8 | } 9 | setItem(cacheKey, data) { 10 | this.cache[cacheKey] = data 11 | } 12 | getItem(cacheKey) { 13 | return this.cache[cacheKey] 14 | } 15 | removeItem(cacheKey) { 16 | this.cache[cacheKey] = undefined 17 | } 18 | getCookie(name) { 19 | this.getItem(name) 20 | } 21 | setCookie(key, value) { 22 | this.setItem(key, value) 23 | } 24 | } 25 | 26 | function isSupportLS() { 27 | try { 28 | localStorage.setItem('_ranger-test-key', 'hi') 29 | localStorage.getItem('_ranger-test-key') 30 | localStorage.removeItem('_ranger-test-key') 31 | return true 32 | } catch (e) { 33 | return false 34 | } 35 | } 36 | function isSupportSession() { 37 | try { 38 | sessionStorage.setItem('_ranger-test-key', 'hi') 39 | sessionStorage.getItem('_ranger-test-key') 40 | sessionStorage.removeItem('_ranger-test-key') 41 | return true 42 | } catch (e) { 43 | return false 44 | } 45 | } 46 | 47 | const local = { 48 | getItem(key) { 49 | try { 50 | var value = localStorage.getItem(key) 51 | let ret = value 52 | try { 53 | if (value && typeof value === 'string') { 54 | ret = JSON.parse(value) 55 | } 56 | } catch (e) { } 57 | 58 | return ret || {} 59 | } catch (e) { } 60 | return {} 61 | }, 62 | setItem(key, value) { 63 | try { 64 | var stringValue = typeof value === 'string' ? value : JSON.stringify(value) 65 | localStorage.setItem(key, stringValue) 66 | } catch (e) { } 67 | }, 68 | removeItem(key) { 69 | try { 70 | localStorage.removeItem(key) 71 | } catch (e) { } 72 | }, 73 | getCookie(name: string, domain?: string) { 74 | try { 75 | var _matches = Cookies.get(name, { domain: domain || document.domain }) 76 | return _matches 77 | } catch (e) { 78 | return '' 79 | } 80 | }, 81 | setCookie(name, value, expiresTime, domain) { 82 | try { 83 | const _domain = domain || document.domain 84 | const timestamp = +new Date(); 85 | const furureTimestamp = timestamp + expiresTime; // 3年 86 | Cookies.set(name, value, { 87 | expires: new Date(furureTimestamp), 88 | path: '/', 89 | domain: _domain 90 | }) 91 | } catch (e) { 92 | } 93 | }, 94 | isSupportLS: isSupportLS(), 95 | } 96 | const session = { 97 | getItem(key) { 98 | try { 99 | var value = sessionStorage.getItem(key) 100 | let ret = value 101 | try { 102 | if (value && typeof value === 'string') { 103 | ret = JSON.parse(value) 104 | } 105 | } catch (e) { } 106 | 107 | return ret || {} 108 | } catch (e) { } 109 | return {} 110 | }, 111 | setItem(key, value) { 112 | try { 113 | var stringValue = typeof value === 'string' ? value : JSON.stringify(value) 114 | sessionStorage.setItem(key, stringValue) 115 | } catch (e) { } 116 | }, 117 | removeItem(key) { 118 | try { 119 | sessionStorage.removeItem(key) 120 | } catch (e) { } 121 | }, 122 | getCookie(name) { 123 | this.getItem(name) 124 | }, 125 | setCookie(key, value) { 126 | this.setItem(key, value) 127 | }, 128 | isSupportSession: isSupportSession(), 129 | } 130 | 131 | export default class Storage { 132 | _storage: any 133 | constructor(isCache, type?: string) { 134 | if (type && type === 'session') { 135 | this._storage = session 136 | } else { 137 | this._storage = !isCache && local.isSupportLS ? local : new Memory() 138 | } 139 | } 140 | getItem(key) { 141 | return this._storage.getItem(key) 142 | } 143 | setItem(key, value) { 144 | this._storage.setItem(key, value) 145 | } 146 | getCookie(name: string, domain?: string) { 147 | return this._storage.getCookie(name, domain) 148 | } 149 | setCookie(key, value, expiresTime, domain) { 150 | this._storage.setCookie(key, value, expiresTime, domain) 151 | } 152 | removeItem(key) { 153 | this._storage.removeItem(key) 154 | } 155 | } -------------------------------------------------------------------------------- /src/util/tool.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | // @ts-nocheck 4 | // @ts-nocheck 5 | export const isObject = (obj: any) => 6 | obj != null && Object.prototype.toString.call(obj) == '[object Object]'; 7 | 8 | export const isFunction = (obj: any) => typeof obj === 'function'; 9 | 10 | export const isNumber = (obj: any) => typeof obj == 'number' && !isNaN(obj); 11 | 12 | export const isString = (obj: any) => typeof obj == 'string'; 13 | 14 | export const isArray = (obj: any) => Array.isArray(obj); 15 | 16 | export const getIndex = (() => { 17 | var lastEventId = +Date.now() + Number(`${Math.random()}`.slice(2, 8)) 18 | return () => { 19 | lastEventId += 1 20 | return lastEventId 21 | } 22 | })() 23 | 24 | /* eslint-disable no-param-reassign */ 25 | const decrypto = (str, xor, hex) => { 26 | if (typeof str !== 'string' || typeof xor !== 'number' || typeof hex !== 'number') { 27 | return; 28 | } 29 | let strCharList = []; 30 | const resultList = []; 31 | hex = hex <= 25 ? hex : hex % 25; 32 | const splitStr = String.fromCharCode(hex + 97); 33 | strCharList = str.split(splitStr); 34 | 35 | for (let i = 0; i < strCharList.length; i++) { 36 | let charCode = parseInt(strCharList[i], hex); 37 | charCode = (charCode * 1) ^ xor; 38 | const strChar = String.fromCharCode(charCode); 39 | resultList.push(strChar); 40 | } 41 | const resultStr = resultList.join(''); 42 | return resultStr; 43 | } 44 | 45 | export const encodeBase64 = (string) => { 46 | if (window.btoa) { 47 | return window.btoa(encodeURIComponent(string)); 48 | } 49 | return encodeURIComponent(string); 50 | } 51 | 52 | export const decodeBase64 = (string) => { 53 | if (window.atob) { 54 | return decodeURIComponent(window.atob(string)); 55 | } 56 | return decodeURIComponent(string); 57 | } 58 | 59 | export const decodeUrl = string => decrypto(string, 64, 25); 60 | 61 | export const beforePageUnload = (fn) => { 62 | var isiOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) 63 | if (isiOS) { 64 | window.addEventListener("pagehide", fn, false) 65 | } else { 66 | window.addEventListener("beforeunload", fn, false) 67 | } 68 | } 69 | 70 | export const getIframeUrl = function () { 71 | try { 72 | var name = JSON.parse(atob(window.name)) 73 | if (name) { 74 | return name 75 | } else { 76 | return undefined 77 | } 78 | } catch (e) { 79 | return undefined 80 | } 81 | } 82 | 83 | export const splitArrayByFilter = (list = [], valueFn = item => item, threshold = 20) => { 84 | const result = []; 85 | let index = 0; 86 | let prev; 87 | list.forEach((item) => { 88 | const cur = valueFn(item); 89 | if (typeof prev === 'undefined') { 90 | prev = cur; 91 | } else if (cur !== prev || result[index].length >= threshold) { 92 | index += 1; 93 | prev = cur; 94 | } 95 | result[index] = result[index] || []; 96 | result[index].push(item); 97 | }); 98 | 99 | return result; 100 | } 101 | 102 | export const loadScript = (src, success, error) => { 103 | const script = document.createElement('script'); 104 | script.src = src; 105 | 106 | script.onerror = function () { 107 | error(src); 108 | }; 109 | 110 | script.onload = function () { 111 | success(); 112 | }; 113 | 114 | document.getElementsByTagName('head')[0].appendChild(script); 115 | } 116 | 117 | export const isSupVisChange = () => { 118 | let flag = 0; 119 | ['hidden', 'msHidden', 'webkitHidden'].forEach(hidden => { 120 | if (document[hidden] !== undefined) { 121 | flag = 1 122 | } 123 | }) 124 | return flag 125 | } 126 | 127 | export const selfAdjust = (cb = () => undefined, interval = 1000) => { 128 | let expected = Date.now() + interval 129 | let timerHander: number 130 | function step() { 131 | const dt = Date.now() - expected 132 | cb() 133 | expected += interval 134 | timerHander = window.setTimeout(step, Math.max(0, interval - dt)) 135 | } 136 | timerHander = window.setTimeout(step, interval) 137 | return () => { 138 | window.clearTimeout(timerHander) 139 | } 140 | } 141 | export const stringify = (origin, path: any = '', query = {}) => { 142 | let str = origin 143 | str = str.split('#')[0].split('?')[0] 144 | if (str[origin.length - 1] === '/') { 145 | str = str.substr(0, origin.length - 1) 146 | } 147 | if (path[0] === '/') { 148 | // 绝对路径 149 | str = str.replace(/(https?:\/\/[\w-]+(\.[\w-]+){1,}(:[0-9]{1,5})?)(\/[.\w-]+)*\/?$/, `$1${path}`) 150 | } else { 151 | // 相对路径 152 | str = str.replace(/(https?:\/\/[\w-]+(\.[\w-]+){1,}(:[0-9]{1,5})?(\/[.\w-]+)*?)(\/[.\w-]+)?\/?$/, `$1/${path}`) 153 | } 154 | const keys = Object.keys(query) 155 | const querystr = keys.map(key => `${key}=${query[key]}`).join('&') 156 | return querystr.length > 0 ? `${str}?${querystr}` : str 157 | } 158 | 159 | export const parseURL = (url: string) => { 160 | const a = document.createElement('a') 161 | a.href = url 162 | return a 163 | } 164 | export const parseUrlQuery = (url: string) => { 165 | const queryObj = {} 166 | try { 167 | let queryString = parseURL(url).search 168 | queryString = queryString.slice(1) 169 | queryString.split('&').forEach(function (keyValue) { 170 | let _keyValue = keyValue.split('=') 171 | let key 172 | let value 173 | if (_keyValue.length) { 174 | key = _keyValue[0] 175 | value = _keyValue[1] 176 | } 177 | try { 178 | queryObj[key] = decodeURIComponent(typeof value === 'undefined' ? '' : value) 179 | } catch (e) { 180 | queryObj[key] = value 181 | } 182 | }) 183 | } catch (e) { } 184 | return queryObj 185 | } 186 | 187 | export const hashCode = (str: string): number => { 188 | str += ''; 189 | let h = 0; 190 | let off = 0; 191 | const len = str.length; 192 | 193 | for (let i = 0; i < len; i++) { 194 | h = 31 * h + str.charCodeAt(off++); 195 | if (h > 0x7fffffffffff || h < -0x800000000000) { 196 | h &= 0xffffffffffff; 197 | } 198 | } 199 | if (h < 0) { 200 | h += 0x7ffffffffffff; 201 | } 202 | return h; 203 | } 204 | 205 | 206 | function leftPad(input, num) { 207 | if (input.length >= num) return input 208 | 209 | return (new Array(num - input.length + 1)).join('0') + input 210 | } 211 | 212 | export const hexToArray = (hexStr) => { 213 | const words = [] 214 | let hexStrLength = hexStr.length 215 | 216 | if (hexStrLength % 2 !== 0) { 217 | hexStr = leftPad(hexStr, hexStrLength + 1) 218 | } 219 | 220 | hexStrLength = hexStr.length 221 | 222 | for (let i = 0; i < hexStrLength; i += 2) { 223 | words.push(parseInt(hexStr.substr(i, 2), 16)) 224 | } 225 | return words 226 | } 227 | 228 | export const hexToBtyes = (hexStr) => { 229 | for (var bytes = [], c = 0; c < hexStr.length; c += 2) 230 | bytes.push(parseInt(hexStr.substr(c, 2), 16)); 231 | return bytes; 232 | } 233 | var base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 234 | var base64encode = function (e) { 235 | var r, a, c, h, o, t; 236 | for (c = e.length, a = 0, r = ''; a < c;) { 237 | if (h = 255 & e.charCodeAt(a++), a == c) { 238 | r += base64EncodeChars.charAt(h >> 2), 239 | r += base64EncodeChars.charAt((3 & h) << 4), 240 | r += '=='; 241 | break 242 | } 243 | if (o = e.charCodeAt(a++), a == c) { 244 | r += base64EncodeChars.charAt(h >> 2), 245 | r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4), 246 | r += base64EncodeChars.charAt((15 & o) << 2), 247 | r += '='; 248 | break 249 | } 250 | t = e.charCodeAt(a++), 251 | r += base64EncodeChars.charAt(h >> 2), 252 | r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4), 253 | r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6), 254 | r += base64EncodeChars.charAt(63 & t) 255 | } 256 | return r 257 | } 258 | export const hextobase = (str) => { 259 | return base64encode(String.fromCharCode.apply(null, str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))); 260 | 261 | } 262 | -------------------------------------------------------------------------------- /src/util/url-polyfill.js: -------------------------------------------------------------------------------- 1 | (function(t){var e=function(){try{return!!Symbol.iterator}catch(e){return false}};var r=e();var n=function(t){var e={next:function(){var e=t.shift();return{done:e===void 0,value:e}}};if(r){e[Symbol.iterator]=function(){return e}}return e};var i=function(e){return encodeURIComponent(e).replace(/%20/g,"+")};var o=function(e){return decodeURIComponent(String(e).replace(/\+/g," "))};var a=function(){var a=function(e){Object.defineProperty(this,"_entries",{writable:true,value:{}});var t=typeof e;if(t==="undefined"){}else if(t==="string"){if(e!==""){this._fromString(e)}}else if(e instanceof a){var r=this;e.forEach(function(e,t){r.append(t,e)})}else if(e!==null&&t==="object"){if(Object.prototype.toString.call(e)==="[object Array]"){for(var n=0;nt[0]){return+1}else{return 0}});if(r._entries){r._entries={}}for(var e=0;e1?o(i[1]):"")}}})}})(typeof global!=="undefined"?global:typeof window!=="undefined"?window:typeof self!=="undefined"?self:this);(function(u){var e=function(){try{var e=new u.URL("b","http://a");e.pathname="c d";return e.href==="http://a/c%20d"&&e.searchParams}catch(e){return false}};var t=function(){var t=u.URL;var e=function(e,t){if(typeof e!=="string")e=String(e);if(t&&typeof t!=="string")t=String(t);var r=document,n;if(t&&(u.location===void 0||t!==u.location.href)){t=t.toLowerCase();r=document.implementation.createHTMLDocument("");n=r.createElement("base");n.href=t;r.head.appendChild(n);try{if(n.href.indexOf(t)!==0)throw new Error(n.href)}catch(e){throw new Error("URL unable to set base "+t+" due to "+e)}}var i=r.createElement("a");i.href=e;if(n){r.body.appendChild(i);i.href=i.href}var o=r.createElement("input");o.type="url";o.value=e;if(i.protocol===":"||!/:/.test(i.href)||!o.checkValidity()&&!t){throw new TypeError("Invalid URL")}Object.defineProperty(this,"_anchorElement",{value:i});var a=new u.URLSearchParams(this.search);var s=true;var f=true;var c=this;["append","delete","set"].forEach(function(e){var t=a[e];a[e]=function(){t.apply(a,arguments);if(s){f=false;c.search=a.toString();f=true}}});Object.defineProperty(this,"searchParams",{value:a,enumerable:true});var h=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:false,configurable:false,writable:false,value:function(){if(this.search!==h){h=this.search;if(f){s=false;this.searchParams._fromString(this.search);s=true}}}})};var r=e.prototype;var n=function(t){Object.defineProperty(r,t,{get:function(){return this._anchorElement[t]},set:function(e){this._anchorElement[t]=e},enumerable:true})};["hash","host","hostname","port","protocol"].forEach(function(e){n(e)});Object.defineProperty(r,"search",{get:function(){return this._anchorElement["search"]},set:function(e){this._anchorElement["search"]=e;this._updateSearchParams()},enumerable:true});Object.defineProperties(r,{toString:{get:function(){var e=this;return function(){return e.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(e){this._anchorElement.href=e;this._updateSearchParams()},enumerable:true},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(e){this._anchorElement.pathname=e},enumerable:true},origin:{get:function(){var e={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol];var t=this._anchorElement.port!=e&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(t?":"+this._anchorElement.port:"")},enumerable:true},password:{get:function(){return""},set:function(e){},enumerable:true},username:{get:function(){return""},set:function(e){},enumerable:true}});e.createObjectURL=function(e){return t.createObjectURL.apply(t,arguments)};e.revokeObjectURL=function(e){return t.revokeObjectURL.apply(t,arguments)};u.URL=e};if(!e()){t()}if(u.location!==void 0&&!("origin"in u.location)){var r=function(){return u.location.protocol+"//"+u.location.hostname+(u.location.port?":"+u.location.port:"")};try{Object.defineProperty(u.location,"origin",{get:r,enumerable:true})}catch(e){setInterval(function(){u.location.origin=r()},100)}}})(typeof global!=="undefined"?global:typeof window!=="undefined"?window:typeof self!=="undefined"?self:this); -------------------------------------------------------------------------------- /src/util/utm.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Beijing Volcanoengine Technology Ltd. All Rights Reserved. 2 | 3 | import Storage from './storage' 4 | const UTM = (app_id: number, urlQueryObj: any, domain: string, cookie_expire: number) => { 5 | const storage = new Storage(false) 6 | const session = new Storage(false, 'session') 7 | const cacheKey = app_id ? `_tea_utm_cache_${app_id}` : '_tea_utm_cache' 8 | const sourceKey = app_id ? `_$utm_from_url_${app_id}` : '_$utm_from_url' 9 | let utmObj = {} 10 | const tracer_data = ['tr_shareuser', 'tr_admaster', 'tr_param1', 'tr_param2', 'tr_param3', 'tr_param4', '$utm_from_url'] 11 | let _utmObj = { 12 | ad_id: Number(urlQueryObj.ad_id) || undefined, 13 | campaign_id: Number(urlQueryObj.campaign_id) || undefined, 14 | creative_id: Number(urlQueryObj.creative_id) || undefined, 15 | utm_source: urlQueryObj.utm_source, 16 | utm_medium: urlQueryObj.utm_medium, 17 | utm_campaign: urlQueryObj.utm_campaign, 18 | utm_term: urlQueryObj.utm_term, 19 | utm_content: urlQueryObj.utm_content, 20 | tr_shareuser: urlQueryObj.tr_shareuser, 21 | tr_admaster: urlQueryObj.tr_admaster, 22 | tr_param1: urlQueryObj.tr_param1, 23 | tr_param2: urlQueryObj.tr_param2, 24 | tr_param3: urlQueryObj.tr_param3, 25 | tr_param4: urlQueryObj.tr_param4, 26 | } 27 | try { 28 | let utmFromUrl = false 29 | for (let key in _utmObj) { 30 | if (_utmObj[key]) { 31 | if (tracer_data.indexOf(key) !== -1) { 32 | if (!utmObj.hasOwnProperty('tracer_data')) { 33 | utmObj['tracer_data'] = {} 34 | } 35 | utmObj['tracer_data'][key] = _utmObj[key] 36 | } else { 37 | utmObj[key] = _utmObj[key] 38 | } 39 | utmFromUrl = true 40 | } 41 | } 42 | if (utmFromUrl) { 43 | // 发现url上有则更新缓存,并上报 44 | session.setItem(sourceKey, '1') 45 | storage.setCookie(cacheKey, JSON.stringify(utmObj), cookie_expire, domain) 46 | } else { 47 | // url没有则取缓存 48 | let cache = storage.getCookie(cacheKey, domain) 49 | if (cache) { 50 | utmObj = JSON.parse(cache) 51 | } 52 | } 53 | if (session.getItem(sourceKey)) { 54 | if (!utmObj.hasOwnProperty('tracer_data')) { 55 | utmObj['tracer_data'] = {} 56 | } 57 | utmObj['tracer_data']['$utm_from_url'] = 1 58 | } 59 | } catch (e) { 60 | return _utmObj 61 | } 62 | return utmObj 63 | } 64 | export default UTM -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "lib": ["esnext", "dom"], 5 | "allowJs": true, 6 | "target": "es5", 7 | "downlevelIteration": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /types/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface IInitParam { 2 | app_id: number; 3 | channel?: 'cn' | 'va' | 'sg'; 4 | channel_domain?: string; 5 | app_key?: string; 6 | caller?: string; 7 | log?: boolean; 8 | disable_webid?: boolean; 9 | disable_sdk_monitor?: boolean; 10 | disable_storage?: boolean; 11 | autotrack?: any; 12 | enable_stay_duration?: any; 13 | disable_route_report?: boolean; 14 | disable_session?: boolean; 15 | disable_heartbeat?: boolean; 16 | disable_auto_pv?: boolean; 17 | enable_tracer?: boolean; 18 | enable_spa?: boolean; 19 | event_verify_url?: string; 20 | enable_ttwebid?: boolean; 21 | user_unique_type?: string; 22 | enable_ab_test?: boolean; 23 | max_storage_num?: number; 24 | enable_storage?: boolean; 25 | enable_cookie?: boolean; 26 | enable_ab_visual?: boolean; 27 | cross_subdomain?: boolean; 28 | cookie_domain?: string; 29 | enable_multilink?: boolean; 30 | multilink_timeout_ms?: number; 31 | reportTime?: number; 32 | timeout?: number; 33 | max_report?: number; 34 | report_url?: string; 35 | maxDuration?: number; 36 | ab_channel_domain?: string; 37 | configPersist?: number; 38 | extend?: any; 39 | ab_timeout?: number; 40 | disable_tracer?: boolean; 41 | extendConfig?: any; 42 | filter?: any; 43 | cep?: boolean; 44 | cep_url?: string; 45 | spa?: boolean; 46 | cookie_expire?: number; 47 | enable_custom_webid?: boolean; 48 | disable_track_event?: boolean; 49 | allow_hash?: boolean; 50 | enable_native?: boolean; 51 | ab_cross?: boolean; 52 | ab_cookie_domain?: string; 53 | disable_ab_reset?: boolean; 54 | enable_encryption?: boolean; 55 | enable_anonymousid?: boolean 56 | enable_debug?: boolean 57 | crypto_publicKey?: string 58 | encryption_type?: string 59 | encryption_header?: string 60 | } 61 | 62 | export interface IConfigParam { 63 | _staging_flag?: 0 | 1; 64 | user_unique_id?: string; 65 | disable_auto_pv?: boolean; 66 | web_id?: string; 67 | user_type?: number; 68 | os_name?: string; 69 | os_version?: string; 70 | device_model?: string; 71 | ab_client?: string; 72 | ab_version?: string; 73 | ab_sdk_version?: string; 74 | traffic_type?: string; 75 | utm_source?: string; 76 | utm_medium?: string; 77 | utm_campaign?: string; 78 | utm_term?: string; 79 | utm_content?: string; 80 | platform?: string; 81 | browser?: string; 82 | browser_version?: string; 83 | region?: string; 84 | province?: string; 85 | city?: string; 86 | language?: string; 87 | timezone?: number; 88 | tz_offset?: number; 89 | screen_height?: number; 90 | screen_width?: number; 91 | referrer?: string; 92 | referrer_host?: string; 93 | os_api?: number; 94 | creative_id?: number; 95 | ad_id?: number; 96 | campaign_id?: number; 97 | ip_addr_id?: number; 98 | user_agent?: string; 99 | verify_type?: string; 100 | sdk_version?: string; 101 | channel?: string; 102 | app_id?: number; 103 | app_name?: string; 104 | app_version?: string; 105 | app_install_id?: number; 106 | user_id?: any; 107 | device_id?: any; 108 | wechat_openid?: string, 109 | wechat_unionid?: string, 110 | evtParams?: EventParams, 111 | reportErrorCallback?(eventData: any, errorCode: any): void; 112 | [key: string]: any; 113 | } 114 | 115 | type EventParams = Record; 116 | 117 | export type SdkOption = Omit; 118 | 119 | export type SdkHookListener = (hookInfo?: any) => void; 120 | 121 | export interface Plugin { 122 | apply(sdk: Sdk, options: SdkOption): void; 123 | } 124 | export interface PluginConstructor { 125 | new(): Plugin; 126 | pluginName?: string; 127 | init?(Sdk: SdkConstructor): void; 128 | } 129 | 130 | export declare enum SdkHook { 131 | Init = 'init', 132 | Config = 'config', 133 | Start = 'start', 134 | Ready = 'ready', 135 | TokenComplete = 'token-complete', 136 | TokenStorage = 'token-storage', 137 | TokenFetch = 'token-fetch', 138 | TokenError = 'token-error', 139 | ConfigUuid = 'config-uuid', 140 | ConfigWebId = 'config-webid', 141 | ConfigDomain = 'config-domain', 142 | CustomWebId = 'custom-webid', 143 | TokenChange = 'token-change', 144 | TokenReset = 'token-reset', 145 | ConfigTransform = 'config-transform', 146 | EnvTransform = 'env-transform', 147 | SessionReset = 'session-reset', 148 | SessionResetTime = 'session-reset-time', 149 | Event = 'event', 150 | Events = 'events', 151 | EventNow = 'event-now', 152 | CleanEvents = 'clean-events', 153 | BeconEvent = 'becon-event', 154 | SubmitBefore = 'submit-before', 155 | SubmitScuess = 'submit-scuess', 156 | SubmitAfter = 'submit-after', 157 | SubmitError = 'submit-error', 158 | SubmitVerify = 'submit-verify', 159 | SubmitVerifyH = 'submit-verify-h5', 160 | 161 | Stay = 'stay', 162 | ResetStay = 'reset-stay', 163 | StayReady = 'stay-ready', 164 | SetStay = 'set-stay', 165 | 166 | RouteChange = 'route-change', 167 | RouteReady = 'route-ready', 168 | 169 | Ab = 'ab', 170 | AbVar = 'ab-var', 171 | AbAllVars = 'ab-all-vars', 172 | AbConfig = 'ab-config', 173 | AbExternalVersion = 'ab-external-version', 174 | AbVersionChangeOn = 'ab-version-change-on', 175 | AbVersionChangeOff = 'ab-version-change-off', 176 | AbOpenLayer = 'ab-open-layer', 177 | AbCloseLayer = 'ab-close-layer', 178 | AbReady = 'ab-ready', 179 | AbComplete = 'ab-complete', 180 | 181 | Profile = 'profile', 182 | ProfileSet = 'profile-set', 183 | ProfileSetOnce = 'profile-set-once', 184 | ProfileUnset = 'profile-unset', 185 | ProfileIncrement = 'profile-increment', 186 | ProfileAppend = 'profile-append', 187 | ProfileClear = 'profile-clear', 188 | 189 | TrackDuration = 'track-duration', 190 | TrackDurationStart = 'track-duration-start', 191 | TrackDurationEnd = 'track-duration-end', 192 | TrackDurationPause = 'track-duration-pause', 193 | TrackDurationResume = 'tracl-duration-resume', 194 | 195 | Autotrack = 'autotrack', 196 | AutotrackReady = 'autotrack-ready', 197 | 198 | CepReady = 'cep-ready', 199 | 200 | TracerReady = 'tracer-ready' 201 | } 202 | interface SdkConstructor { 203 | new(name: string): Sdk; 204 | instances: Array; 205 | usePlugin: (plugin: PluginConstructor, pluginName?: string) => void; 206 | } 207 | interface Sdk { 208 | Types: typeof SdkHook; 209 | 210 | on(type: string, hook: SdkHookListener): void; 211 | once(type: string, hook: SdkHookListener): void; 212 | off(type: string, hook?: SdkHookListener): void; 213 | emit(type: string, info?: any, wait?: string): void; 214 | 215 | init(options: IInitParam): void; 216 | config(configs?: IConfigParam): void; 217 | getConfig(key?: string): Record; 218 | setDomain(domain: string): void; 219 | start(): void; 220 | send(): void; 221 | set(type: string): void; 222 | event(event: string, params?: EventParams): void; 223 | beconEvent(event: string, params?: EventParams): void; 224 | event( 225 | events: 226 | | Array<[string, EventParams] | [string, EventParams, number]> 227 | ): void; 228 | 229 | predefinePageView(params: any): void; 230 | clearEventCache(): void; 231 | setWebIDviaUnionID(unionId: string): void; 232 | setWebIDviaOpenID(openId): void; 233 | getToken(callback: (info: Record) => void, timeout?: number): void; 234 | 235 | resetStayDuration(url_path?: string, title?: string, url?: string): void; 236 | resetStayParams(url_path?: string, title?: string, url?: string): void; 237 | 238 | profileSet(profile: any): void; 239 | profileSetOnce(profile: any): void; 240 | profileIncrement(profile: any): void; 241 | profileUnset(key: string): void; 242 | profileAppend(profile: any): void; 243 | 244 | startTrackEvent(eventName: string): void; 245 | endTrackEvent(eventName: string, params: any): void; 246 | pauseTrackEvent(eventName: string): void; 247 | resumeTrackEvent(eventName: string): void; 248 | 249 | setExternalAbVersion(vids: string): void; 250 | getVar(name: string, defaultValue: any, callback: (value: any) => void): void; 251 | getAllVars(callback: (value: any) => void): void; 252 | getAbSdkVersion(): string; 253 | onAbSdkVersionChange(callback: (vids: string) => void): () => void; 254 | offAbSdkVersionChange(callback: (vids: string) => void): void; 255 | setExternalAbVersion(vids: string | null): void; 256 | getABconfig(params: Record, callback: (value: any) => void): void; 257 | openOverlayer(): void; 258 | closeOverlayer(): void; 259 | autoInitializationRangers( 260 | config: IInitParam & { onTokenReady: (webId: string) => void }, 261 | ): void; 262 | } 263 | declare const Sdk: Sdk; 264 | export const Collector: SdkConstructor; 265 | export default Sdk; --------------------------------------------------------------------------------