├── .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;
--------------------------------------------------------------------------------