├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── babel.cjs.json
├── babel.esm.json
├── demo
├── node_modules
│ └── .package-lock.json
├── package-lock.json
└── package.json
├── package-lock.json
├── package.json
├── scripts
├── build-env.js
└── replace-build-env.js
├── src
├── boot
│ ├── buildEnv.js
│ ├── rum.entry.js
│ └── rum.js
├── core
│ ├── baseInfo.js
│ ├── boundedBuffer.js
│ ├── configuration.js
│ ├── dataMap.js
│ ├── downloadProxy.js
│ ├── errorCollection.js
│ ├── errorTools.js
│ ├── lifeCycle.js
│ ├── observable.js
│ ├── sdk.js
│ ├── transport.js
│ └── xhrProxy.js
├── helper
│ ├── enums.js
│ ├── tracekit.js
│ └── utils.js
├── index.js
└── rumEventsCollection
│ ├── action
│ ├── actionCollection.js
│ └── trackActions.js
│ ├── app
│ ├── appCollection.js
│ └── index.js
│ ├── assembly.js
│ ├── error
│ └── errorCollection.js
│ ├── page
│ ├── index.js
│ └── viewCollection.js
│ ├── parentContexts.js
│ ├── performanceCollection.js
│ ├── requestCollection.js
│ ├── resource
│ ├── resourceCollection.js
│ └── resourceUtils.js
│ ├── setDataCollection.js
│ ├── tracing
│ ├── ddtraceTracer.js
│ ├── jaegerTracer.js
│ ├── skywalkingTracer.js
│ ├── tracer.js
│ ├── w3cTraceParentTracer.js
│ ├── zipkinMultiTracer.js
│ └── zipkinSingleTracer.js
│ ├── trackEventCounts.js
│ ├── trackPageActiveites.js
│ └── transport
│ └── batch.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | cjs
2 | esm
3 | bundle
4 |
5 | demo
6 | demo2
7 | node_modules
8 | .vscode
9 | *.pyc
10 | *.tsbuildinfo
11 | scripts/publish-oss.js
12 | # logs
13 | yarn-error.log
14 | npm-debug.log
15 | lerna-debug.log
16 | local.log
17 |
18 | # ide
19 | .idea
20 | *.sublime-*
21 | # misc
22 | .DS_Store
23 | ._*
24 | .Spotlight-V100
25 | .Trashes
26 |
27 | package-lock.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !/cjs/**/*
3 | !/esm/**/*
4 | *.tsbuildinfo
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 CloudCare
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 基于uniapp开发框架 兼容各平台小程序 DataFlux RUM 数据采集SDK
2 | 通过引入sdk文件,监控小程序性能指标,错误log,以及资源请求情况数据,上报到DataFlux 平台datakit
3 |
4 | ## 使用方法
5 | ### 在uniapp项目入口文件`main.js`文件头部位置以如下方式引入代码
6 | ### npm 引入(可参考uniapp官方[npm引入方式](https://uniapp.dcloud.net.cn/frame?id=npm%e6%94%af%e6%8c%81))
7 | ```javascript
8 | //#ifndef H5 || APP-PLUS || APP-NVUE || APP-PLUS-NVUE
9 | const { datafluxRum } = require('@cloudcare/rum-uniapp')
10 | // 初始化 Rum
11 | datafluxRum.init({
12 | datakitOrigin: 'https://datakit.xxx.com/',// 必填,Datakit域名地址 需要在微信小程序管理后台加上域名白名单
13 | applicationId: 'appid_xxxxxxx', // 必填,dataflux 平台生成的应用ID
14 | env: 'testing', // 选填,小程序的环境
15 | version: '1.0.0', // 选填,小程序版本
16 | trackInteractions: true, // 用户行为数据
17 | })
18 | //#endif
19 | ```
20 | ### CDN 下载文件本地方式引入([下载地址](https://static.dataflux.cn/miniapp-sdk/v1/dataflux-rum-uniapp.js))
21 |
22 | ```javascript
23 | //#ifndef H5 || APP-PLUS || APP-NVUE || APP-PLUS-NVUE
24 | const { datafluxRum } = require('@cloudcare/rum-uniapp')
25 | // 初始化 Rum
26 | datafluxRum.init({
27 | datakitOrigin: 'https://datakit.xxx.com/',// 必填,Datakit域名地址 需要在微信小程序管理后台加上域名白名单
28 | applicationId: 'appid_xxxxxxx', // 必填,dataflux 平台生成的应用ID
29 | env: 'testing', // 选填,小程序的环境
30 | version: '1.0.0', // 选填,小程序版本
31 | trackInteractions: true, // 用户行为数据
32 | })
33 | //#endif
34 | ```
35 |
36 | ## 配置
37 |
38 | ### 初始化参数
39 |
40 | | 参数 | 类型 | 是否必须 | 默认值 | 描述 |
41 | | ----------------------------------------------- | ------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42 | | `applicationId` | String | 是 | | 从 dataflux 创建的应用 ID |
43 | | `datakitOrigin` | String | 是 | | datakit 数据上报 Origin;`注意:需要在小程序管理后台加上request白名单` |
44 | | `env` | String | 否 | | 小程序 应用当前环境, 如 prod:线上环境;gray:灰度环境;pre:预发布环境 common:日常环境;local:本地环境; |
45 | | `version` | String | 否 | | 小程序 应用的版本号 |
46 | | `sampleRate` | Number | 否 | `100` | 指标数据收集百分比: `100`表示全收集,`0`表示不收集 |
47 | | `traceType` $\color{#FF0000}{新增}$ | Enum | 否 | `ddtrace` | 与 APM 采集工具连接的请求header类型,目前兼容的类型包括:`ddtrace`、`zipkin`、`skywalking_v3`、`jaeger`、`zipkin_single_header`、`w3c_traceparent`。*注: opentelemetry 支持 `zipkin_single_header`,`w3c_traceparent`,`zipkin`三种类型* |
48 | | `traceId128Bit` $\color{#FF0000}{新增}$ | Boolean | 否 | `false` | 是否以128位的方式生成 `traceID`,与`traceType` 对应,目前支持类型 `zipkin`、`jaeger` |
49 | | `allowedTracingOrigins` $\color{#FF0000}{新增}$ | Array | 否 | `[]` | 允许注入 `trace` 采集器所需header头部的所有请求列表。可以是请求的origin,也可以是是正则,origin: `协议(包括://),域名(或IP地址)[和端口号]` 例如:`["https://api.example.com", /https:\/\/.*\.my-api-domain\.com/]` |
50 | | `trackInteractions` | Boolean | 否 | `false` | 是否开启用户行为采集 |
51 |
52 | ## 注意事项
53 |
54 | 1. `datakitOrigin` 所对应的datakit域名必须在小程序管理后台加上request白名单
55 | 2. 目前各平台小程序在性能数据api暴露这块,并没有完善统一,所以导致一些性能数据并不能完善收集,比如`小程序启动`、`小程序包下载`、`脚本注入` 等一些数据除微信平台外,都有可能会存在缺失的情况。
56 | 3. 目前各平台小程序请求资源API`uni.request`、`uni.downloadFile`返回数据中`profile`字段目前只有微信小程序ios系统不支持返回,所以会导致收集的资源信息中和timing相关的数据收集不全。目前暂无解决方案,[request](https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html), [downloadFile](https://developers.weixin.qq.com/miniprogram/dev/api/network/download/wx.downloadFile.html) ;[API支持情况](https://developers.weixin.qq.com/community/develop/doc/000ecaa8b580c80601cac8e6f56000?highLine=%2520request%2520profile)
57 | 3. `trackInteractions` 用户行为采集开启后,因为微信小程序的限制,无法采集到控件的内容和结构数据,所以在小程序 SDK 里面我们采取的是声明式编程,通过在 模版 里面设置 data-name 属性,可以给 交互元素 添加名称,方便后续统计是定位操作记录, 例如:
58 | ```js
59 |
60 | ```
61 |
62 |
--------------------------------------------------------------------------------
/babel.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/env",
5 | {
6 | "targets": {
7 | "esmodules": false
8 | },
9 | "modules": "cjs"
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/babel.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/env",
5 | {
6 | "targets": {
7 | "esmodules": true
8 | },
9 | "modules": false
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/demo/node_modules/.package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "node_modules/@cloudcare/rum-uniapp": {
8 | "version": "1.0.1",
9 | "resolved": "https://registry.npmjs.org/@cloudcare/rum-uniapp/-/rum-uniapp-1.0.1.tgz",
10 | "integrity": "sha512-wYOXSQpe5cdocHmSSarSU3JzrUhZpTJfZlQWID0jJTGcNfEMqMlVI2ifAEJp1SqAw5Q+f8UR8nrgFLbQ3PC1jQ=="
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/demo/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "version": "1.0.0",
9 | "license": "ISC",
10 | "dependencies": {
11 | "@cloudcare/rum-uniapp": "^1.0.1"
12 | }
13 | },
14 | "node_modules/@cloudcare/rum-uniapp": {
15 | "version": "1.0.1",
16 | "resolved": "https://registry.npmjs.org/@cloudcare/rum-uniapp/-/rum-uniapp-1.0.1.tgz",
17 | "integrity": "sha512-wYOXSQpe5cdocHmSSarSU3JzrUhZpTJfZlQWID0jJTGcNfEMqMlVI2ifAEJp1SqAw5Q+f8UR8nrgFLbQ3PC1jQ=="
18 | }
19 | },
20 | "dependencies": {
21 | "@cloudcare/rum-uniapp": {
22 | "version": "1.0.1",
23 | "resolved": "https://registry.npmjs.org/@cloudcare/rum-uniapp/-/rum-uniapp-1.0.1.tgz",
24 | "integrity": "sha512-wYOXSQpe5cdocHmSSarSU3JzrUhZpTJfZlQWID0jJTGcNfEMqMlVI2ifAEJp1SqAw5Q+f8UR8nrgFLbQ3PC1jQ=="
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "main.js",
6 | "dependencies": {
7 | "@cloudcare/rum-uniapp": "^1.0.1"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC"
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cloudcare/rum-uniapp",
3 | "version": "2.0.1",
4 | "main": "cjs/index.js",
5 | "module": "esm/index.js",
6 | "miniprogram": "cjs",
7 | "types": "cjs/index.d.ts",
8 | "devDependencies": {
9 | "@babel/core": "^7.12.10",
10 | "@babel/preset-env": "^7.12.10",
11 | "ali-oss": "^6.12.0",
12 | "npm-run-all": "^4.1.5",
13 | "replace-in-file": "^6.1.0",
14 | "webpack": "^5.37.0",
15 | "webpack-cli": "^4.7.0",
16 | "webpack-dev-server": "^3.11.2"
17 | },
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1",
20 | "build": "run-p build:cjs build:esm build:wx",
21 | "build:watch": "webpack --watch --config ./webpack.config.js --mode=development",
22 | "build:wx": "rm -rf demo/miniprogram && webpack --config webpack.config.js --mode=production && npm run replace-build-env demo/miniprogram",
23 | "build:cjs": "rm -rf cjs && babel --config-file ./babel.cjs.json --out-dir cjs ./src && npm run replace-build-env cjs",
24 | "build:esm": "rm -rf esm && babel --config-file ./babel.esm.json --out-dir esm ./src && npm run replace-build-env esm",
25 | "publish:npm": "npm run build && node ./scripts/publish-oss.js && npm publish --access=public",
26 | "publish:oss:test": "npm run build && node ./scripts/publish-oss.js test",
27 | "replace-build-env": "node scripts/replace-build-env.js"
28 | },
29 | "keywords": [
30 | "dataflux",
31 | "rum",
32 | "sdk",
33 | "小程序",
34 | "miniapp"
35 | ],
36 | "author": "dataflux",
37 | "license": "MIT",
38 | "repository": {
39 | "url": "https://github.com/DataFlux-cn/datakit-miniprogram-uniapp",
40 | "type": "git"
41 | },
42 | "description": "DataFlux RUM 小程序 端数据指标监控",
43 | "dependencies": {}
44 | }
45 |
--------------------------------------------------------------------------------
/scripts/build-env.js:
--------------------------------------------------------------------------------
1 | const execSync = require('child_process').execSync
2 | const packageJSON = require('../package.json')
3 |
4 | let sdkVersion = packageJSON.version
5 |
6 | module.exports = {
7 | SDK_VERSION: sdkVersion,
8 | }
9 |
--------------------------------------------------------------------------------
/scripts/replace-build-env.js:
--------------------------------------------------------------------------------
1 | const replace = require('replace-in-file')
2 | const buildEnv = require('./build-env')
3 |
4 | /**
5 | * Replace BuildEnv in build files
6 | * Usage:
7 | * TARGET_DATACENTER=xxx BUILD_MODE=zzz node replace-build-env.js /path/to/build/directory
8 | */
9 |
10 | const buildDirectory = process.argv[2]
11 | console.log(process.argv, '=====')
12 | console.log(`Replace BuildEnv in '${buildDirectory}' with:`)
13 | console.log(JSON.stringify(buildEnv, null, 2))
14 |
15 | try {
16 | const results = replace.sync({
17 | files: `${buildDirectory}/**/*.js`,
18 | from: Object.keys(buildEnv).map((entry) => `<<< ${entry} >>>`),
19 | to: Object.values(buildEnv),
20 | })
21 |
22 | console.log(
23 | 'Changed files:',
24 | results.filter((entry) => entry.hasChanged).map((entry) => entry.file),
25 | )
26 | process.exit(0)
27 | } catch (error) {
28 | console.error('Error occurred:', error)
29 | process.exit(1)
30 | }
31 |
--------------------------------------------------------------------------------
/src/boot/buildEnv.js:
--------------------------------------------------------------------------------
1 | export const buildEnv = {
2 | sdkVersion: '<<< SDK_VERSION >>>',
3 | sdkName: 'df_uniapp_rum_sdk',
4 | }
5 |
--------------------------------------------------------------------------------
/src/boot/rum.entry.js:
--------------------------------------------------------------------------------
1 | import { isPercentage, extend2Lev, createContextManager } from '../helper/utils'
2 | import { startRum } from './rum'
3 |
4 | export const makeRum = function (startRumImpl) {
5 | var isAlreadyInitialized = false
6 | var globalContextManager = createContextManager()
7 | var user = {}
8 | function clonedCommonContext() {
9 | return extend2Lev(
10 | {},
11 | {
12 | context: globalContextManager.get(),
13 | user: user
14 | }
15 | )
16 | }
17 | var rumGlobal = {
18 | init: function (Vue, userConfiguration) {
19 | if (typeof userConfiguration === 'undefined') {
20 | userConfiguration = {}
21 | }
22 | if (!Vue) return
23 | if (!canInitRum(userConfiguration)) {
24 | return
25 | }
26 | startRumImpl(Vue, userConfiguration, function () {
27 | return {
28 | user: user,
29 | context: globalContextManager.get()
30 | }
31 | })
32 | isAlreadyInitialized = true
33 | },
34 | addRumGlobalContext: globalContextManager.add,
35 | removeRumGlobalContext: globalContextManager.remove,
36 | getRumGlobalContext: globalContextManager.get,
37 | setRumGlobalContext: globalContextManager.set,
38 | setUser: function (newUser) {
39 | var sanitizedUser = sanitizeUser(newUser)
40 | if (sanitizedUser) {
41 | user = sanitizedUser
42 | } else {
43 | console.error('Unsupported user:', newUser)
44 | }
45 | },
46 | removeUser: function () {
47 | user = {}
48 | }
49 | }
50 | return rumGlobal
51 |
52 | function canInitRum(userConfiguration) {
53 | if (isAlreadyInitialized) {
54 | console.error('DATAFLUX_RUM is already initialized.')
55 | return false
56 | }
57 |
58 | if (!userConfiguration.applicationId) {
59 | console.error(
60 | 'Application ID is not configured, no RUM data will be collected.',
61 | )
62 | return false
63 | }
64 | if (!userConfiguration.datakitOrigin) {
65 | console.error(
66 | 'datakitOrigin is not configured, no RUM data will be collected.',
67 | )
68 | return false
69 | }
70 | if (
71 | userConfiguration.sampleRate !== undefined &&
72 | !isPercentage(userConfiguration.sampleRate)
73 | ) {
74 | console.error('Sample Rate should be a number between 0 and 100')
75 | return false
76 | }
77 | return true
78 | }
79 | function sanitizeUser(newUser) {
80 | if (typeof newUser !== 'object' || !newUser) {
81 | return
82 | }
83 | var result = extend2Lev({}, newUser)
84 | if ('id' in result) {
85 | result.id = String(result.id)
86 | }
87 | if ('name' in result) {
88 | result.name = String(result.name)
89 | }
90 | if ('email' in result) {
91 | result.email = String(result.email)
92 | }
93 | return result
94 | }
95 | }
96 | export const datafluxRum = makeRum(startRum)
97 |
--------------------------------------------------------------------------------
/src/boot/rum.js:
--------------------------------------------------------------------------------
1 | import { buildEnv } from './buildEnv'
2 | import { LifeCycle } from '../core/lifeCycle'
3 | import { commonInit } from '../core/configuration'
4 | import { startErrorCollection } from '../rumEventsCollection/error/errorCollection'
5 | import { startRumAssembly } from '../rumEventsCollection/assembly'
6 | import { startParentContexts } from '../rumEventsCollection/parentContexts'
7 | import { startRumBatch } from '../rumEventsCollection/transport/batch'
8 | import { startViewCollection } from '../rumEventsCollection/page/viewCollection'
9 | import { startRequestCollection } from '../rumEventsCollection/requestCollection'
10 | import { startResourceCollection } from '../rumEventsCollection/resource/resourceCollection'
11 | import { startAppCollection } from '../rumEventsCollection/app/appCollection'
12 | import { startPagePerformanceObservable } from '../rumEventsCollection/performanceCollection'
13 | import { startSetDataColloction } from '../rumEventsCollection/setDataCollection'
14 | import { startActionCollection } from '../rumEventsCollection/action/actionCollection'
15 |
16 | import { sdk } from '../core/sdk'
17 | export const startRum = function (Vue, userConfiguration, getCommonContext) {
18 | const configuration = commonInit(userConfiguration, buildEnv)
19 | const lifeCycle = new LifeCycle()
20 | var parentContexts = startParentContexts(lifeCycle)
21 | var batch = startRumBatch(configuration, lifeCycle)
22 | startRumAssembly(
23 | userConfiguration.applicationId,
24 | configuration,
25 | lifeCycle,
26 | parentContexts,
27 | getCommonContext
28 | )
29 | startAppCollection(lifeCycle, configuration)
30 | startResourceCollection(lifeCycle, configuration)
31 | startViewCollection(lifeCycle, configuration, Vue)
32 | startErrorCollection(lifeCycle, configuration)
33 | startRequestCollection(lifeCycle, configuration)
34 | startPagePerformanceObservable(lifeCycle, configuration)
35 | startSetDataColloction(lifeCycle, Vue)
36 | startActionCollection(lifeCycle, configuration, Vue)
37 | }
38 |
--------------------------------------------------------------------------------
/src/core/baseInfo.js:
--------------------------------------------------------------------------------
1 | import { sdk } from '../core/sdk'
2 | import { UUID } from '../helper/utils'
3 | import { CLIENT_ID_TOKEN } from '../helper/enums'
4 | class BaseInfo {
5 | constructor() {
6 | this.sessionId = UUID()
7 | this.getDeviceInfo()
8 | this.getNetWork()
9 | }
10 | getDeviceInfo() {
11 | try {
12 | const deviceInfo = sdk.getSystemInfoSync()
13 | var osInfo = deviceInfo.system.split(' ')
14 | var osVersion = ''
15 | if (osInfo.length > 1) {
16 | osVersion = osInfo[1]
17 | } else {
18 | osVersion = osInfo[0] || ''
19 | }
20 | var osVersionMajor =
21 | osVersion.split('.').length && osVersion.split('.')[0]
22 |
23 | this.deviceInfo = {
24 | screenSize: `${deviceInfo.screenWidth}*${deviceInfo.screenHeight} `,
25 | platform: deviceInfo.platform,
26 | platformVersion: deviceInfo.version,
27 | osVersion: osVersion,
28 | osVersionMajor: osVersionMajor,
29 | os: osInfo.length > 1 && osInfo[0],
30 | app: deviceInfo.app,
31 | brand: deviceInfo.brand,
32 | model: deviceInfo.model,
33 | frameworkVersion: deviceInfo.SDKVersion,
34 | pixelRatio: deviceInfo.pixelRatio,
35 | deviceUuid: deviceInfo.deviceId,
36 | }
37 | } catch (e) {
38 | this.deviceInfo = {}
39 | }
40 | }
41 | getClientID() {
42 | var clienetId = sdk.getStorageSync(CLIENT_ID_TOKEN)
43 | if (!clienetId) {
44 | clienetId = UUID()
45 | sdk.setStorageSync(CLIENT_ID_TOKEN, clienetId)
46 | }
47 | return clienetId
48 | }
49 | getNetWork() {
50 | sdk.getNetworkType({
51 | success: (e) => {
52 | this.deviceInfo.network = e.networkType ? e.networkType : 'unknown'
53 | },
54 | })
55 | sdk.onNetworkStatusChange((e) => {
56 | this.deviceInfo.network = e.networkType ? e.networkType : 'unknown'
57 | })
58 | }
59 | getSessionId() {
60 | return this.sessionId
61 | }
62 | }
63 |
64 | export default new BaseInfo()
65 |
--------------------------------------------------------------------------------
/src/core/boundedBuffer.js:
--------------------------------------------------------------------------------
1 | var _BoundedBuffer = function () {
2 | this.buffer = []
3 | }
4 | _BoundedBuffer.prototype = {
5 | add: function (item) {
6 | var length = this.buffer.push(item)
7 | if (length > this.limit) {
8 | this.buffer.splice(0, 1)
9 | }
10 | },
11 |
12 | drain: function (fn) {
13 | this.buffer.forEach(function(item) {
14 | fn(item)
15 | })
16 |
17 | this.buffer.length = 0
18 | }
19 | }
20 | export var BoundedBuffer = _BoundedBuffer
21 |
--------------------------------------------------------------------------------
/src/core/configuration.js:
--------------------------------------------------------------------------------
1 | import { extend2Lev, urlParse, values } from '../helper/utils'
2 | import { ONE_KILO_BYTE, ONE_SECOND, TraceType } from '../helper/enums'
3 | var TRIM_REGIX = /^\s+|\s+$/g
4 | export var DEFAULT_CONFIGURATION = {
5 | sampleRate: 100,
6 | flushTimeout: 30 * ONE_SECOND,
7 | maxErrorsByMinute: 3000,
8 | /**
9 | * Logs intake limit
10 | */
11 | maxBatchSize: 50,
12 | maxMessageSize: 256 * ONE_KILO_BYTE,
13 |
14 | /**
15 | * beacon payload max queue size implementation is 64kb
16 | * ensure that we leave room for logs, rum and potential other users
17 | */
18 | batchBytesLimit: 16 * ONE_KILO_BYTE,
19 | datakitUrl: '',
20 | /**
21 | * arbitrary value, byte precision not needed
22 | */
23 | requestErrorResponseLengthLimit: 32 * ONE_KILO_BYTE,
24 | trackInteractions: false,
25 | traceType: TraceType.DDTRACE,
26 | traceId128Bit: false,
27 | allowedTracingOrigins:[], // 新增
28 | }
29 | function trim(str) {
30 | return str.replace(TRIM_REGIX, '')
31 | }
32 | function getDatakitUrlUrl(url) {
33 | if (url && url.lastIndexOf('/') === url.length - 1)
34 | return trim(url) + 'v1/write/rum'
35 | return trim(url) + '/v1/write/rum'
36 | }
37 | export function commonInit(userConfiguration, buildEnv) {
38 | var transportConfiguration = {
39 | applicationId: userConfiguration.applicationId,
40 | env: userConfiguration.env || '',
41 | version: userConfiguration.version || '',
42 | sdkVersion: buildEnv.sdkVersion,
43 | sdkName: buildEnv.sdkName,
44 | datakitUrl: getDatakitUrlUrl(
45 | userConfiguration.datakitUrl || userConfiguration.datakitOrigin,
46 | ),
47 | tags: userConfiguration.tags || [],
48 | }
49 | if ('trackInteractions' in userConfiguration) {
50 | transportConfiguration.trackInteractions = !!userConfiguration.trackInteractions
51 | }
52 | if ('allowedTracingOrigins' in userConfiguration) {
53 | transportConfiguration.allowedTracingOrigins = userConfiguration.allowedTracingOrigins
54 | }
55 | if ('traceId128Bit' in userConfiguration) {
56 | transportConfiguration.traceId128Bit = !!userConfiguration.traceId128Bit
57 | }
58 | if ('traceType' in userConfiguration && hasTraceType(userConfiguration.traceType)) {
59 | transportConfiguration.traceType = userConfiguration.traceType
60 | }
61 | return extend2Lev(DEFAULT_CONFIGURATION, transportConfiguration)
62 | }
63 | function hasTraceType(traceType) {
64 | if (traceType && values(TraceType).indexOf(traceType) > -1) return true
65 | return false
66 | }
67 | const haveSameOrigin = function (url1, url2) {
68 | const parseUrl1 = urlParse(url1).getParse()
69 | const parseUrl2 = urlParse(url2).getParse()
70 | return parseUrl1.Origin === parseUrl2.Origin
71 | }
72 | export function isIntakeRequest(url, configuration) {
73 | return haveSameOrigin(url, configuration.datakitUrl)
74 | }
75 |
--------------------------------------------------------------------------------
/src/core/dataMap.js:
--------------------------------------------------------------------------------
1 | import { RumEventType } from '../helper/enums'
2 | // 需要用双引号将字符串类型的field value括起来, 这里有数组标示[string, path]
3 | export var commonTags = {
4 | sdk_name: '_dd.sdk_name',
5 | sdk_version: '_dd.sdk_version',
6 | app_id: 'application.id',
7 | env: '_dd.env',
8 | version: '_dd.version',
9 | userid: 'user.id',
10 | user_email: 'user.email',
11 | user_name: 'user.name',
12 | session_id: 'session.id',
13 | session_type: 'session.type',
14 | is_signin: 'user.is_signin',
15 | device: 'device.brand',
16 | model: 'device.model',
17 | device_uuid: 'device.device_uuid',
18 | os: 'device.os',
19 | app: 'device.app',
20 | os_version: 'device.os_version',
21 | os_version_major: 'device.os_version_major',
22 | screen_size: 'device.screen_size',
23 | network_type: 'device.network_type',
24 | platform: 'device.platform',
25 | platform_version: 'device.platform_version',
26 | app_framework_version: 'device.framework_version',
27 | view_id: 'page.id',
28 | view_name: 'page.route',
29 | view_referer: 'page.referer',
30 | }
31 | export var dataMap = {
32 | view: {
33 | type: RumEventType.VIEW,
34 | tags: {
35 | view_apdex_level: 'page.apdex_level',
36 | is_active: 'page.is_active',
37 | },
38 | fields: {
39 | page_fmp: 'page.fmp',
40 | first_paint_time: 'page.fpt',
41 | loading_time: 'page.loading_time',
42 | onload_to_onshow: 'page.onload2onshow',
43 | onshow_to_onready: 'page.onshow2onready',
44 | time_spent: 'page.time_spent',
45 | view_error_count: 'page.error.count',
46 | view_resource_count: 'page.resource.count',
47 | view_long_task_count: 'page.long_task.count',
48 | view_action_count: 'page.action.count',
49 | view_setdata_count: 'page.setdata.count',
50 | },
51 | },
52 | resource: {
53 | type: RumEventType.RESOURCE,
54 | tags: {
55 | trace_id: '_dd.trace_id',
56 | span_id: '_dd.span_id',
57 | resource_type: 'resource.type',
58 | resource_status: 'resource.status',
59 | resource_status_group: 'resource.status_group',
60 | resource_method: 'resource.method',
61 | resource_url: 'resource.url',
62 | resource_url_host: 'resource.url_host',
63 | resource_url_path: 'resource.url_path',
64 | resource_url_path_group: 'resource.url_path_group',
65 | resource_url_query: 'resource.url_query',
66 | },
67 | fields: {
68 | resource_size: 'resource.size',
69 | resource_load: 'resource.load',
70 | resource_dns: 'resource.dns',
71 | resource_tcp: 'resource.tcp',
72 | resource_ssl: 'resource.ssl',
73 | resource_ttfb: 'resource.ttfb',
74 | resource_trans: 'resource.trans',
75 | resource_first_byte: 'resource.firstbyte',
76 | duration: 'resource.duration',
77 | },
78 | },
79 | error: {
80 | type: RumEventType.ERROR,
81 | tags: {
82 | error_source: 'error.source',
83 | error_type: 'error.type',
84 | resource_url: 'error.resource.url',
85 | resource_url_host: 'error.resource.url_host',
86 | resource_url_path: 'error.resource.url_path',
87 | resource_url_path_group: 'error.resource.url_path_group',
88 | resource_status: 'error.resource.status',
89 | resource_status_group: 'error.resource.status_group',
90 | resource_method: 'error.resource.method',
91 | },
92 | fields: {
93 | error_message: ['string', 'error.message'],
94 | error_stack: ['string', 'error.stack'],
95 | },
96 | },
97 | long_task: {
98 | type: RumEventType.LONG_TASK,
99 | tags: {},
100 | fields: {
101 | duration: 'long_task.duration',
102 | },
103 | },
104 | action: {
105 | type: RumEventType.ACTION,
106 | tags: {
107 | action_id: 'action.id',
108 | action_name: 'action.target.name',
109 | action_type: 'action.type',
110 | },
111 | fields: {
112 | duration: 'action.loading_time',
113 | action_error_count: 'action.error.count',
114 | action_resource_count: 'action.resource.count',
115 | action_long_task_count: 'action.long_task.count',
116 | },
117 | },
118 | app: {
119 | alias_key: 'action', // metrc 别名,
120 | type: RumEventType.APP,
121 | tags: {
122 | action_id: 'app.id',
123 | action_name: 'app.name',
124 | action_type: 'app.type',
125 | },
126 | fields: {
127 | duration: 'app.duration',
128 | },
129 | },
130 | }
131 |
--------------------------------------------------------------------------------
/src/core/downloadProxy.js:
--------------------------------------------------------------------------------
1 | import { sdk } from './sdk'
2 | import { now } from '../helper/utils'
3 | import { RequestType } from '../helper/enums'
4 | var downloadProxySingleton
5 | var beforeSendCallbacks = []
6 | var onRequestCompleteCallbacks = []
7 | var originalDownloadRequest
8 | export function startDownloadProxy() {
9 | if (!downloadProxySingleton) {
10 | proxyDownload()
11 | downloadProxySingleton = {
12 | beforeSend: function (callback) {
13 | beforeSendCallbacks.push(callback)
14 | },
15 | onRequestComplete: function (callback) {
16 | onRequestCompleteCallbacks.push(callback)
17 | },
18 | }
19 | }
20 | return downloadProxySingleton
21 | }
22 |
23 | export function resetDownloadProxy() {
24 | if (downloadProxySingleton) {
25 | downloadProxySingleton = undefined
26 | beforeSendCallbacks.splice(0, beforeSendCallbacks.length)
27 | onRequestCompleteCallbacks.splice(0, onRequestCompleteCallbacks.length)
28 | sdk.downloadFile = originalDownloadRequest
29 | }
30 | }
31 |
32 | function proxyDownload() {
33 | originalDownloadRequest = sdk.downloadFile
34 | sdk.downloadFile = function () {
35 | var _this = this
36 | var dataflux_xhr = {
37 | method: 'GET',
38 | startTime: 0,
39 | url: arguments[0].url,
40 | type: RequestType.DOWNLOAD,
41 | responseType: 'file',
42 | }
43 | dataflux_xhr.startTime = now()
44 |
45 | var originalSuccess = arguments[0].success
46 |
47 | arguments[0].success = function () {
48 | reportXhr(arguments[0])
49 |
50 | if (originalSuccess) {
51 | originalSuccess.apply(_this, arguments)
52 | }
53 | }
54 | var originalFail = arguments[0].fail
55 | arguments[0].fail = function () {
56 | reportXhr(arguments[0])
57 | if (originalFail) {
58 | originalFail.apply(_this, arguments)
59 | }
60 | }
61 | var hasBeenReported = false
62 | var reportXhr = function (res) {
63 | if (hasBeenReported) {
64 | return
65 | }
66 | hasBeenReported = true
67 | dataflux_xhr.duration = now() - dataflux_xhr.startTime
68 | dataflux_xhr.response = JSON.stringify({
69 | filePath: res.filePath,
70 | tempFilePath: res.tempFilePath,
71 | })
72 | dataflux_xhr.header = res.header || {}
73 | dataflux_xhr.profile = res.profile
74 | dataflux_xhr.status = res.statusCode || res.status || 0
75 | onRequestCompleteCallbacks.forEach(function (callback) {
76 | callback(dataflux_xhr)
77 | })
78 | }
79 | beforeSendCallbacks.forEach(function (callback) {
80 | callback(dataflux_xhr)
81 | })
82 | return originalDownloadRequest.apply(this, arguments)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/core/errorCollection.js:
--------------------------------------------------------------------------------
1 | import { toArray, now } from '../helper/utils'
2 | import { ONE_MINUTE, RequestType } from '../helper/enums'
3 | import {
4 | ErrorSource,
5 | formatUnknownError,
6 | toStackTraceString,
7 | } from './errorTools'
8 | import { computeStackTrace, report } from '../helper/tracekit'
9 | import { Observable } from './observable'
10 | import { isIntakeRequest } from './configuration'
11 | import { resetXhrProxy, startXhrProxy } from './xhrProxy'
12 | import { resetDownloadProxy, startDownloadProxy } from './downloadProxy'
13 | var originalConsoleError
14 |
15 | export function startConsoleTracking(errorObservable) {
16 | originalConsoleError = console.error
17 | console.error = function () {
18 | originalConsoleError.apply(console, arguments)
19 | var args = toArray(arguments)
20 | var message = []
21 | args.concat(['console error:']).forEach(function (para) {
22 | message.push(formatConsoleParameters(para))
23 | })
24 |
25 | errorObservable.notify({
26 | message: message.join(' '),
27 | source: ErrorSource.CONSOLE,
28 | startTime: now(),
29 | })
30 | }
31 | }
32 |
33 | export function stopConsoleTracking() {
34 | console.error = originalConsoleError
35 | }
36 |
37 | function formatConsoleParameters(param) {
38 | if (typeof param === 'string') {
39 | return param
40 | }
41 | if (param instanceof Error) {
42 | return toStackTraceString(computeStackTrace(param))
43 | }
44 | return JSON.stringify(param, undefined, 2)
45 | }
46 | export function filterErrors(configuration, errorObservable) {
47 | var errorCount = 0
48 | var filteredErrorObservable = new Observable()
49 | errorObservable.subscribe(function (error) {
50 | if (errorCount < configuration.maxErrorsByMinute) {
51 | errorCount += 1
52 | filteredErrorObservable.notify(error)
53 | } else if (errorCount === configuration.maxErrorsByMinute) {
54 | errorCount += 1
55 | filteredErrorObservable.notify({
56 | message:
57 | 'Reached max number of errors by minute: ' +
58 | configuration.maxErrorsByMinute,
59 | source: ErrorSource.AGENT,
60 | startTime: now(),
61 | })
62 | }
63 | })
64 | setInterval(function () {
65 | errorCount = 0
66 | }, ONE_MINUTE)
67 | return filteredErrorObservable
68 | }
69 | var traceKitReportHandler
70 |
71 | export function startRuntimeErrorTracking(errorObservable) {
72 | traceKitReportHandler = function (stackTrace, _, errorObject) {
73 | var error = formatUnknownError(stackTrace, errorObject, 'Uncaught')
74 | errorObservable.notify({
75 | message: error.message,
76 | stack: error.stack,
77 | type: error.type,
78 | source: ErrorSource.SOURCE,
79 | startTime: now(),
80 | })
81 | }
82 | report.subscribe(traceKitReportHandler)
83 | }
84 |
85 | export function stopRuntimeErrorTracking() {
86 | report.unsubscribe(traceKitReportHandler)
87 | }
88 | var filteredErrorsObservable
89 |
90 | export function startAutomaticErrorCollection(configuration) {
91 | if (!filteredErrorsObservable) {
92 | var errorObservable = new Observable()
93 | trackNetworkError(configuration, errorObservable)
94 | startConsoleTracking(errorObservable)
95 | startRuntimeErrorTracking(errorObservable)
96 | filteredErrorsObservable = filterErrors(configuration, errorObservable)
97 | }
98 | return filteredErrorsObservable
99 | }
100 |
101 | export function trackNetworkError(configuration, errorObservable) {
102 | startXhrProxy().onRequestComplete(function (context) {
103 | return handleCompleteRequest(context.type, context)
104 | })
105 | startDownloadProxy().onRequestComplete(function (context) {
106 | return handleCompleteRequest(context.type, context)
107 | })
108 |
109 | function handleCompleteRequest(type, request) {
110 | if (
111 | !isIntakeRequest(request.url, configuration) &&
112 | (isRejected(request) || isServerError(request))
113 | ) {
114 | errorObservable.notify({
115 | message: format(type) + 'error' + request.method + ' ' + request.url,
116 | resource: {
117 | method: request.method,
118 | statusCode: request.status,
119 | url: request.url,
120 | },
121 | type: ErrorSource.NETWORK,
122 | source: ErrorSource.NETWORK,
123 | stack:
124 | truncateResponse(request.response, configuration) || 'Failed to load',
125 | startTime: request.startTime,
126 | })
127 | }
128 | }
129 |
130 | return {
131 | stop: function () {
132 | resetXhrProxy()
133 | resetDownloadProxy()
134 | },
135 | }
136 | }
137 | function isRejected(request) {
138 | return request.status === 0 && request.responseType !== 'opaque'
139 | }
140 |
141 | function isServerError(request) {
142 | return request.status >= 500
143 | }
144 |
145 | function truncateResponse(response, configuration) {
146 | if (
147 | response &&
148 | response.length > configuration.requestErrorResponseLengthLimit
149 | ) {
150 | return (
151 | response.substring(0, configuration.requestErrorResponseLengthLimit) +
152 | '...'
153 | )
154 | }
155 | return response
156 | }
157 |
158 | function format(type) {
159 | if (RequestType.XHR === type) {
160 | return 'XHR'
161 | }
162 | return RequestType.DOWNLOAD
163 | }
164 |
--------------------------------------------------------------------------------
/src/core/errorTools.js:
--------------------------------------------------------------------------------
1 | export var ErrorSource = {
2 | AGENT: 'agent',
3 | CONSOLE: 'console',
4 | NETWORK: 'network',
5 | SOURCE: 'source',
6 | LOGGER: 'logger',
7 | }
8 | export function formatUnknownError(stackTrace, errorObject, nonErrorPrefix) {
9 | if (
10 | !stackTrace ||
11 | (stackTrace.message === undefined && !(errorObject instanceof Error))
12 | ) {
13 | return {
14 | message: nonErrorPrefix + '' + JSON.stringify(errorObject),
15 | stack: 'No stack, consider using an instance of Error',
16 | type: stackTrace && stackTrace.name,
17 | }
18 | }
19 | return {
20 | message: stackTrace.message || 'Empty message',
21 | stack: toStackTraceString(stackTrace),
22 | type: stackTrace.name,
23 | }
24 | }
25 |
26 | export function toStackTraceString(stack) {
27 | var result = stack.name || 'Error' + ': ' + stack.message
28 | stack.stack.forEach(function (frame) {
29 | var func = frame.func === '?' ? '' : frame.func
30 | var args =
31 | frame.args && frame.args.length > 0
32 | ? '(' + frame.args.join(', ') + ')'
33 | : ''
34 | var line = frame.line ? ':' + frame.line : ''
35 | var column = frame.line && frame.column ? ':' + frame.column : ''
36 | result += '\n at ' + func + args + ' @ ' + frame.url + line + column
37 | })
38 | return result
39 | }
40 |
--------------------------------------------------------------------------------
/src/core/lifeCycle.js:
--------------------------------------------------------------------------------
1 | export class LifeCycle {
2 | constructor() {
3 | this.callbacks = {}
4 | }
5 | notify(eventType, data) {
6 | const eventCallbacks = this.callbacks[eventType]
7 | if (eventCallbacks) {
8 | eventCallbacks.forEach((callback) => callback(data))
9 | }
10 | }
11 | subscribe(eventType, callback) {
12 | if (!this.callbacks[eventType]) {
13 | this.callbacks[eventType] = []
14 | }
15 | this.callbacks[eventType].push(callback)
16 | return {
17 | unsubscribe: () => {
18 | this.callbacks[eventType] = this.callbacks[eventType].filter(
19 | (other) => callback !== other,
20 | )
21 | },
22 | }
23 | }
24 | }
25 |
26 | export var LifeCycleEventType = {
27 | PERFORMANCE_ENTRY_COLLECTED: 'PERFORMANCE_ENTRY_COLLECTED',
28 | AUTO_ACTION_CREATED: 'AUTO_ACTION_CREATED',
29 | AUTO_ACTION_COMPLETED: 'AUTO_ACTION_COMPLETED',
30 | AUTO_ACTION_DISCARDED: 'AUTO_ACTION_DISCARDED',
31 | APP_HIDE: 'APP_HIDE',
32 | APP_UPDATE: 'APP_UPDATE',
33 | PAGE_SET_DATA_UPDATE: 'PAGE_SET_DATA_UPDATE',
34 | PAGE_ALIAS_ACTION: 'PAGE_ALIAS_ACTION',
35 | VIEW_CREATED: 'VIEW_CREATED',
36 | VIEW_UPDATED: 'VIEW_UPDATED',
37 | VIEW_ENDED: 'VIEW_ENDED',
38 | REQUEST_STARTED: 'REQUEST_STARTED',
39 | REQUEST_COMPLETED: 'REQUEST_COMPLETED',
40 | RAW_RUM_EVENT_COLLECTED: 'RAW_RUM_EVENT_COLLECTED',
41 | RUM_EVENT_COLLECTED: 'RUM_EVENT_COLLECTED',
42 | }
43 |
--------------------------------------------------------------------------------
/src/core/observable.js:
--------------------------------------------------------------------------------
1 | export class Observable {
2 | constructor() {
3 | this.observers = []
4 | }
5 | subscribe(f) {
6 | this.observers.push(f)
7 | }
8 | notify(data) {
9 | this.observers.forEach(function (observer) {
10 | observer(data)
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/core/sdk.js:
--------------------------------------------------------------------------------
1 | import { deepMixObject } from '../helper/utils'
2 |
3 | function getSDK() {
4 | var sdk = null,
5 | tracker = ''
6 | try {
7 | if (uni && typeof uni === 'object' && typeof uni.request === 'function') {
8 | sdk = uni
9 | }
10 |
11 | if (wx && typeof wx === 'object' && typeof wx.request === 'function') {
12 | // 微信
13 | tracker = wx
14 | } else if (
15 | my &&
16 | typeof my === 'object' &&
17 | typeof my.request === 'function'
18 | ) {
19 | // 支付宝
20 | tracker = my
21 | } else if (
22 | tt &&
23 | typeof tt === 'object' &&
24 | typeof tt.request === 'function'
25 | ) {
26 | // 头条
27 | tracker = tt
28 | } else if (
29 | dd &&
30 | typeof dd === 'object' &&
31 | typeof dd.request === 'function'
32 | ) {
33 | // dingding
34 | tracker = dd
35 | } else if (
36 | qq &&
37 | typeof qq === 'object' &&
38 | typeof qq.request === 'function'
39 | ) {
40 | // QQ 小程序、QQ 小游戏
41 | tracker = qq
42 | } else if (
43 | swan &&
44 | typeof swan === 'object' &&
45 | typeof swan.request === 'function'
46 | ) {
47 | // 百度小程序
48 | tracker = swan
49 | } else {
50 | tracker = uni
51 | }
52 | } catch (err) {
53 | console.warn('unsupport platform, Fail to start')
54 | }
55 | console.log('------get SDK-------')
56 | return { sdk, tracker }
57 | }
58 | const instance = getSDK()
59 |
60 | export const sdk = instance.sdk
61 | export const tracker = instance.tracker
62 |
--------------------------------------------------------------------------------
/src/core/transport.js:
--------------------------------------------------------------------------------
1 | import {
2 | findByPath,
3 | escapeRowData,
4 | isNumber,
5 | each,
6 | isString,
7 | values,
8 | extend,
9 | isObject,
10 | isEmptyObject,
11 | isArray,
12 | isBoolean,
13 | toServerDuration
14 | } from '../helper/utils'
15 | import { sdk } from '../core/sdk'
16 | import { LifeCycleEventType } from '../core/lifeCycle'
17 | import { commonTags, dataMap } from './dataMap'
18 | import { RumEventType } from '../helper/enums'
19 |
20 | // https://en.wikipedia.org/wiki/UTF-8
21 | var HAS_MULTI_BYTES_CHARACTERS = /[^\u0000-\u007F]/
22 | var CUSTOM_KEYS = 'custom_keys'
23 |
24 | function addBatchPrecision(url) {
25 | if (!url) return url
26 | return url + (url.indexOf('?') === -1 ? '?' : '&') + 'precision=ms'
27 | }
28 | var httpRequest = function (endpointUrl, bytesLimit) {
29 | this.endpointUrl = endpointUrl
30 | this.bytesLimit = bytesLimit
31 | }
32 | httpRequest.prototype = {
33 | send: function (data) {
34 | var url = addBatchPrecision(this.endpointUrl)
35 | sdk.request({
36 | method: 'POST',
37 | header: {
38 | 'content-type': 'text/plain;charset=UTF-8',
39 | },
40 | url,
41 | data,
42 | })
43 | },
44 | }
45 |
46 | export var HttpRequest = httpRequest
47 | export var processedMessageByDataMap = function (message) {
48 | if (!message || !message.type) return {
49 | rowStr: '',
50 | rowData: undefined
51 | }
52 | var rowData = { tags: {}, fields: {} }
53 | var hasFileds = false
54 | var rowStr = ''
55 | each(dataMap, function (value, key) {
56 | if (value.type === message.type) {
57 | if (value.alias_key) {
58 | rowStr += value.alias_key + ','
59 | } else {
60 | rowStr += key + ','
61 | }
62 | rowData.measurement = key
63 | var tagsStr = []
64 | var tags = extend({}, commonTags, value.tags)
65 | var filterFileds = ['date', 'type'] // 已经在datamap中定义过的fields和tags
66 | each(tags, function (value_path, _key) {
67 | var _value = findByPath(message, value_path)
68 | filterFileds.push(_key)
69 | if (_value || isNumber(_value)) {
70 | rowData.tags[_key] = _value
71 | tagsStr.push(escapeRowData(_key) + '=' + escapeRowData(_value))
72 | }
73 | })
74 | if (message.tags && isObject(message.tags) && !isEmptyObject(message.tags)) {
75 | // 自定义tag
76 | const _tagKeys = []
77 | each(message.tags, function (_value, _key) {
78 | // 如果和之前tag重名,则舍弃
79 | if (filterFileds.indexOf(_key) > -1) return
80 | filterFileds.push(_key)
81 | if (_value || isNumber(_value)) {
82 | _tagKeys.push(_key)
83 | rowData.tags[_key] = _value
84 | tagsStr.push(escapeRowData(_key) + '=' + escapeRowData(_value))
85 | }
86 | })
87 | if (_tagKeys.length) {
88 | rowData.tags[CUSTOM_KEYS] = _tagKeys
89 | tagsStr.push(escapeRowData(CUSTOM_KEYS) + '=' + escapeRowData(_tagKeys))
90 | }
91 | }
92 | var fieldsStr = []
93 | each(value.fields, function (_value, _key) {
94 | if (isArray(_value) && _value.length === 2) {
95 | var type = _value[0],
96 | value_path = _value[1]
97 | var _valueData = findByPath(message, value_path)
98 | filterFileds.push(_key)
99 | if (_valueData || isNumber(_valueData)) {
100 | rowData.fields[_key] = _valueData // 这里不需要转译
101 | _valueData =
102 | type === 'string'
103 | ? '"' +
104 | _valueData.replace(/[\\]*"/g, '"').replace(/"/g, '\\"') +
105 | '"'
106 | : escapeRowData(_valueData)
107 | fieldsStr.push(escapeRowData(_key) + '=' + _valueData)
108 | }
109 | } else if (isString(_value)) {
110 | var _valueData = findByPath(message, _value)
111 | filterFileds.push(_key)
112 | if (_valueData || isNumber(_valueData)) {
113 | rowData.fields[_key] = _valueData // 这里不需要转译
114 | _valueData = escapeRowData(_valueData)
115 | fieldsStr.push(escapeRowData(_key) + '=' + _valueData)
116 | }
117 | }
118 | })
119 | if (message.type === RumEventType.LOGGER) {
120 | // 这里处理日志类型数据自定义字段
121 |
122 | each(message, function (value, key) {
123 | if (
124 | filterFileds.indexOf(key) === -1 &&
125 | (isNumber(value) || isString(value) || isBoolean(value))
126 | ) {
127 | tagsStr.push(escapeRowData(key) + '=' + escapeRowData(value))
128 | }
129 | })
130 | }
131 | if (tagsStr.length) {
132 | rowStr += tagsStr.join(',')
133 | }
134 | if (fieldsStr.length) {
135 | rowStr += ' '
136 | rowStr += fieldsStr.join(',')
137 | hasFileds = true
138 | }
139 | rowStr = rowStr + ' ' + message.date
140 | rowData.time = toServerDuration(message.date) // 这里不需要转译
141 | }
142 | })
143 | return {
144 | rowStr: hasFileds ? rowStr : '',
145 | rowData: hasFileds ? rowData : undefined
146 | }
147 | }
148 | function batch(
149 | request,
150 | maxSize,
151 | bytesLimit,
152 | maxMessageSize,
153 | flushTimeout,
154 | lifeCycle,
155 | ) {
156 | this.request = request
157 | this.maxSize = maxSize
158 | this.bytesLimit = bytesLimit
159 | this.maxMessageSize = maxMessageSize
160 | this.flushTimeout = flushTimeout
161 | this.lifeCycle = lifeCycle
162 | this.pushOnlyBuffer = []
163 | this.upsertBuffer = {}
164 | this.bufferBytesSize = 0
165 | this.bufferMessageCount = 0
166 | this.flushOnVisibilityHidden()
167 | this.flushPeriodically()
168 | }
169 | batch.prototype = {
170 | add: function (message) {
171 | this.addOrUpdate(message)
172 | },
173 |
174 | upsert: function (message, key) {
175 | this.addOrUpdate(message, key)
176 | },
177 |
178 | flush: function () {
179 | if (this.bufferMessageCount !== 0) {
180 | var messages = this.pushOnlyBuffer.concat(values(this.upsertBuffer))
181 | this.request.send(messages.join('\n'), this.bufferBytesSize)
182 | this.pushOnlyBuffer = []
183 | this.upsertBuffer = {}
184 | this.bufferBytesSize = 0
185 | this.bufferMessageCount = 0
186 | }
187 | },
188 |
189 | processSendData: function (message) {
190 | return processedMessageByDataMap(message).rowStr
191 | },
192 | sizeInBytes: function (candidate) {
193 | // Accurate byte size computations can degrade performances when there is a lot of events to process
194 | if (!HAS_MULTI_BYTES_CHARACTERS.test(candidate)) {
195 | return candidate.length
196 | }
197 | var total = 0,
198 | charCode
199 | // utf-8编码
200 | for (var i = 0, len = candidate.length; i < len; i++) {
201 | charCode = candidate.charCodeAt(i)
202 | if (charCode <= 0x007f) {
203 | total += 1
204 | } else if (charCode <= 0x07ff) {
205 | total += 2
206 | } else if (charCode <= 0xffff) {
207 | total += 3
208 | } else {
209 | total += 4
210 | }
211 | }
212 | return total
213 | },
214 |
215 | addOrUpdate: function (message, key) {
216 | var process = this.process(message)
217 | if (!process.processedMessage || process.processedMessage === '') return
218 | if (process.messageBytesSize >= this.maxMessageSize) {
219 | console.warn(
220 | 'Discarded a message whose size was bigger than the maximum allowed size' +
221 | this.maxMessageSize +
222 | 'KB.',
223 | )
224 | return
225 | }
226 | if (this.hasMessageFor(key)) {
227 | this.remove(key)
228 | }
229 | if (this.willReachedBytesLimitWith(process.messageBytesSize)) {
230 | this.flush()
231 | }
232 | this.push(process.processedMessage, process.messageBytesSize, key)
233 | if (this.isFull()) {
234 | this.flush()
235 | }
236 | },
237 | process: function (message) {
238 | var processedMessage = this.processSendData(message)
239 | var messageBytesSize = this.sizeInBytes(processedMessage)
240 | return {
241 | processedMessage: processedMessage,
242 | messageBytesSize: messageBytesSize,
243 | }
244 | },
245 |
246 | push: function (processedMessage, messageBytesSize, key) {
247 | if (this.bufferMessageCount > 0) {
248 | // \n separator at serialization
249 | this.bufferBytesSize += 1
250 | }
251 | if (key !== undefined) {
252 | this.upsertBuffer[key] = processedMessage
253 | } else {
254 | this.pushOnlyBuffer.push(processedMessage)
255 | }
256 | this.bufferBytesSize += messageBytesSize
257 | this.bufferMessageCount += 1
258 | },
259 |
260 | remove: function (key) {
261 | var removedMessage = this.upsertBuffer[key]
262 | delete this.upsertBuffer[key]
263 | var messageBytesSize = this.sizeInBytes(removedMessage)
264 | this.bufferBytesSize -= messageBytesSize
265 | this.bufferMessageCount -= 1
266 | if (this.bufferMessageCount > 0) {
267 | this.bufferBytesSize -= 1
268 | }
269 | },
270 |
271 | hasMessageFor: function (key) {
272 | return key !== undefined && this.upsertBuffer[key] !== undefined
273 | },
274 |
275 | willReachedBytesLimitWith: function (messageBytesSize) {
276 | // byte of the separator at the end of the message
277 | return this.bufferBytesSize + messageBytesSize + 1 >= this.bytesLimit
278 | },
279 |
280 | isFull: function () {
281 | return (
282 | this.bufferMessageCount === this.maxSize ||
283 | this.bufferBytesSize >= this.bytesLimit
284 | )
285 | },
286 |
287 | flushPeriodically: function () {
288 | var _this = this
289 | setTimeout(function () {
290 | _this.flush()
291 | _this.flushPeriodically()
292 | }, _this.flushTimeout)
293 | },
294 |
295 | flushOnVisibilityHidden: function () {
296 | var _this = this
297 | /**
298 | * With sendBeacon, requests are guaranteed to be successfully sent during document unload
299 | */
300 | // @ts-ignore this function is not always defined
301 | this.lifeCycle.subscribe(LifeCycleEventType.APP_HIDE, function () {
302 | _this.flush()
303 | })
304 | },
305 | }
306 |
307 | export var Batch = batch
308 |
--------------------------------------------------------------------------------
/src/core/xhrProxy.js:
--------------------------------------------------------------------------------
1 | import { sdk } from './sdk'
2 | import { now } from '../helper/utils'
3 | import { RequestType } from '../helper/enums'
4 | var xhrProxySingleton
5 | var beforeSendCallbacks = []
6 | var onRequestCompleteCallbacks = []
7 | var originalXhrRequest
8 | export function startXhrProxy() {
9 | if (!xhrProxySingleton) {
10 | proxyXhr()
11 | xhrProxySingleton = {
12 | beforeSend: function (callback) {
13 | beforeSendCallbacks.push(callback)
14 | },
15 | onRequestComplete: function (callback) {
16 | onRequestCompleteCallbacks.push(callback)
17 | },
18 | }
19 | }
20 | return xhrProxySingleton
21 | }
22 |
23 | export function resetXhrProxy() {
24 | if (xhrProxySingleton) {
25 | xhrProxySingleton = undefined
26 | beforeSendCallbacks.splice(0, beforeSendCallbacks.length)
27 | onRequestCompleteCallbacks.splice(0, onRequestCompleteCallbacks.length)
28 | sdk.request = originalXhrRequest
29 | }
30 | }
31 |
32 | function proxyXhr() {
33 | originalXhrRequest = sdk.request
34 | sdk.request = function () {
35 | var _this = this
36 | var dataflux_xhr = {
37 | method: arguments[0].method || 'GET',
38 | startTime: 0,
39 | url: arguments[0].url,
40 | type: RequestType.XHR,
41 | responseType: arguments[0].responseType || 'text',
42 | option: arguments[0]
43 | }
44 | dataflux_xhr.startTime = now()
45 |
46 | var originalSuccess = arguments[0].success
47 |
48 | arguments[0].success = function () {
49 | reportXhr(arguments[0])
50 | if (originalSuccess) {
51 | originalSuccess.apply(_this, arguments)
52 | }
53 | }
54 | var originalFail = arguments[0].fail
55 | arguments[0].fail = function () {
56 | reportXhr(arguments[0])
57 | if (originalFail) {
58 | originalFail.apply(_this, arguments)
59 | }
60 | }
61 | var hasBeenReported = false
62 | var reportXhr = function (res) {
63 | if (hasBeenReported) {
64 | return
65 | }
66 | hasBeenReported = true
67 | dataflux_xhr.duration = now() - dataflux_xhr.startTime
68 | dataflux_xhr.response = JSON.stringify(res.data)
69 | dataflux_xhr.header = res.header || {}
70 | dataflux_xhr.profile = res.profile
71 | dataflux_xhr.status = res.statusCode || res.status || 0
72 | onRequestCompleteCallbacks.forEach(function (callback) {
73 | callback(dataflux_xhr)
74 | })
75 | }
76 | beforeSendCallbacks.forEach(function (callback) {
77 | callback(dataflux_xhr)
78 | })
79 | return originalXhrRequest.call(this, dataflux_xhr.option)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/helper/enums.js:
--------------------------------------------------------------------------------
1 | export const ONE_SECOND = 1000
2 | export const ONE_MINUTE = 60 * ONE_SECOND
3 | export const ONE_HOUR = 60 * ONE_MINUTE
4 | export const ONE_KILO_BYTE = 1024
5 | export const CLIENT_ID_TOKEN = 'datafluxRum:client:id'
6 | export const RumEventType = {
7 | ACTION: 'action',
8 | ERROR: 'error',
9 | LONG_TASK: 'long_task',
10 | VIEW: 'view',
11 | RESOURCE: 'resource',
12 | APP: 'app',
13 | ACTION: 'action',
14 | }
15 |
16 | export var RequestType = {
17 | XHR: 'network',
18 | DOWNLOAD: 'resource',
19 | }
20 |
21 | export var ActionType = {
22 | tap: 'tap',
23 | longpress: 'longpress',
24 | longtap: 'longtap',
25 | }
26 | export var MpHook = {
27 | data: 1,
28 | onLoad: 1,
29 | onShow: 1,
30 | onReady: 1,
31 | render: 1,
32 | onPullDownRefresh: 1,
33 | onReachBottom: 1,
34 | onPageScroll: 1,
35 | onResize: 1,
36 | onHide: 1,
37 | onUnload: 1,
38 | }
39 | export var TraceType = {
40 | DDTRACE: 'ddtrace',
41 | ZIPKIN_MULTI_HEADER: 'zipkin',
42 | ZIPKIN_SINGLE_HEADER: 'zipkin_single_header',
43 | W3C_TRACEPARENT: 'w3c_traceparent',
44 | SKYWALKING_V3: 'skywalking_v3',
45 | JAEGER: 'jaeger',
46 | }
--------------------------------------------------------------------------------
/src/helper/tracekit.js:
--------------------------------------------------------------------------------
1 | import { sdk } from '../core/sdk'
2 |
3 | const UNKNOWN_FUNCTION = '?'
4 | function has(object, key) {
5 | return Object.prototype.hasOwnProperty.call(object, key)
6 | }
7 | function isUndefined(what) {
8 | return typeof what === 'undefined'
9 | }
10 | export function wrap(func) {
11 | var _this = this
12 | function wrapped() {
13 | try {
14 | return func.apply(_this, arguments)
15 | } catch (e) {
16 | report(e)
17 | throw e
18 | }
19 | }
20 | return wrapped
21 | }
22 | /**
23 | * Cross-browser processing of unhandled exceptions
24 | *
25 | * Syntax:
26 | * ```js
27 | * report.subscribe(function(stackInfo) { ... })
28 | * report.unsubscribe(function(stackInfo) { ... })
29 | * report(exception)
30 | * try { ...code... } catch(ex) { report(ex); }
31 | * ```
32 | *
33 | * Supports:
34 | * - Firefox: full stack trace with line numbers, plus column number
35 | * on top frame; column number is not guaranteed
36 | * - Opera: full stack trace with line and column numbers
37 | * - Chrome: full stack trace with line and column numbers
38 | * - Safari: line and column number for the top frame only; some frames
39 | * may be missing, and column number is not guaranteed
40 | * - IE: line and column number for the top frame only; some frames
41 | * may be missing, and column number is not guaranteed
42 | *
43 | * In theory, TraceKit should work on all of the following versions:
44 | * - IE5.5+ (only 8.0 tested)
45 | * - Firefox 0.9+ (only 3.5+ tested)
46 | * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require
47 | * Exceptions Have Stacktrace to be enabled in opera:config)
48 | * - Safari 3+ (only 4+ tested)
49 | * - Chrome 1+ (only 5+ tested)
50 | * - Konqueror 3.5+ (untested)
51 | *
52 | * Requires computeStackTrace.
53 | *
54 | * Tries to catch all unhandled exceptions and report them to the
55 | * subscribed handlers. Please note that report will rethrow the
56 | * exception. This is REQUIRED in order to get a useful stack trace in IE.
57 | * If the exception does not reach the top of the browser, you will only
58 | * get a stack trace from the point where report was called.
59 | *
60 | * Handlers receive a StackTrace object as described in the
61 | * computeStackTrace docs.
62 | *
63 | * @memberof TraceKit
64 | * @namespace
65 | */
66 | export var report = (function reportModuleWrapper() {
67 | var handlers = []
68 |
69 | /**
70 | * Add a crash handler.
71 | * @param {Function} handler
72 | * @memberof report
73 | */
74 | function subscribe(handler) {
75 | installGlobalHandler()
76 | installGlobalUnhandledRejectionHandler()
77 | installGlobalOnPageNotFoundHandler()
78 | installGlobalOnMemoryWarningHandler()
79 | handlers.push(handler)
80 | }
81 |
82 | /**
83 | * Remove a crash handler.
84 | * @param {Function} handler
85 | * @memberof report
86 | */
87 | function unsubscribe(handler) {
88 | for (var i = handlers.length - 1; i >= 0; i -= 1) {
89 | if (handlers[i] === handler) {
90 | handlers.splice(i, 1)
91 | }
92 | }
93 | }
94 |
95 | /**
96 | * Dispatch stack information to all handlers.
97 | * @param {StackTrace} stack
98 | * @param {boolean} isWindowError Is this a top-level window error?
99 | * @param {Error=} error The error that's being handled (if available, null otherwise)
100 | * @memberof report
101 | * @throws An exception if an error occurs while calling an handler.
102 | */
103 | function notifyHandlers(stack, isWindowError, error) {
104 | var exception
105 | for (var i in handlers) {
106 | if (has(handlers, i)) {
107 | try {
108 | handlers[i](stack, isWindowError, error)
109 | } catch (inner) {
110 | exception = inner
111 | }
112 | }
113 | }
114 |
115 | if (exception) {
116 | throw exception
117 | }
118 | }
119 |
120 | var onErrorHandlerInstalled
121 | var onUnhandledRejectionHandlerInstalled
122 | var onPageNotFoundHandlerInstalled
123 | var onOnMemoryWarningHandlerInstalled
124 | /**
125 | * Ensures all global unhandled exceptions are recorded.
126 | * Supported by Gecko and IE.
127 | * @param {Event|string} message Error message.
128 | * @param {string=} url URL of script that generated the exception.
129 | * @param {(number|string)=} lineNo The line number at which the error occurred.
130 | * @param {(number|string)=} columnNo The column number at which the error occurred.
131 | * @param {Error=} errorObj The actual Error object.
132 | * @memberof report
133 | */
134 | function traceKitWindowOnError(err) {
135 | const error = typeof err === 'string' ? new Error(err) : err
136 | var stack
137 | var name = ''
138 | var msg = ''
139 | stack = computeStackTrace(error)
140 | if (
141 | error &&
142 | error.message &&
143 | {}.toString.call(error.message) === '[object String]'
144 | ) {
145 | const messages = error.message.split('\n')
146 | if (messages.length >= 3) {
147 | msg = messages[2]
148 | const groups = msg.match(ERROR_TYPES_RE)
149 | if (groups) {
150 | name = groups[1]
151 | msg = groups[2]
152 | }
153 | }
154 | }
155 | if (msg) {
156 | stack.message = msg
157 | }
158 | if (name) {
159 | stack.name = name
160 | }
161 | notifyHandlers(stack, true, error)
162 | }
163 |
164 | /**
165 | * Ensures all unhandled rejections are recorded.
166 | * @param {PromiseRejectionEvent} e event.
167 | * @memberof report
168 | * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunhandledrejection
169 | * @see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
170 | */
171 | function traceKitWindowOnUnhandledRejection({ reason, promise }) {
172 | const error = typeof reason === 'string' ? new Error(reason) : reason
173 | var stack
174 | var name = ''
175 | var msg = ''
176 | stack = computeStackTrace(error)
177 | if (
178 | error &&
179 | error.message &&
180 | {}.toString.call(error.message) === '[object String]'
181 | ) {
182 | const messages = error.message.split('\n')
183 | if (messages.length >= 3) {
184 | msg = messages[2]
185 | const groups = msg.match(ERROR_TYPES_RE)
186 | if (groups) {
187 | name = groups[1]
188 | msg = groups[2]
189 | }
190 | }
191 | }
192 | if (msg) {
193 | stack.message = msg
194 | }
195 | if (name) {
196 | stack.name = name
197 | }
198 | notifyHandlers(stack, true, error)
199 | }
200 |
201 | /**
202 | * Install a global onerror handler
203 | * @memberof report
204 | */
205 | function installGlobalHandler() {
206 | if (onErrorHandlerInstalled || !sdk.onError) {
207 | return
208 | }
209 | sdk.onError(traceKitWindowOnError)
210 | onErrorHandlerInstalled = true
211 | }
212 |
213 | /**
214 | * Install a global onunhandledrejection handler
215 | * @memberof report
216 | */
217 | function installGlobalUnhandledRejectionHandler() {
218 | if (onUnhandledRejectionHandlerInstalled || !sdk.onUnhandledRejection) {
219 | return
220 | }
221 |
222 | sdk.onUnhandledRejection &&
223 | sdk.onUnhandledRejection(traceKitWindowOnUnhandledRejection)
224 | onUnhandledRejectionHandlerInstalled = true
225 | }
226 | function installGlobalOnPageNotFoundHandler() {
227 | if (onPageNotFoundHandlerInstalled || !sdk.onPageNotFound) {
228 | return
229 | }
230 | sdk.onPageNotFound((res) => {
231 | const url = res.path.split('?')[0]
232 | notifyHandlers(
233 | {
234 | message: JSON.stringify(res),
235 | type: 'pagenotfound',
236 | name: url + '页面无法找到',
237 | },
238 | true,
239 | {},
240 | )
241 | })
242 | onPageNotFoundHandlerInstalled = true
243 | }
244 | function installGlobalOnMemoryWarningHandler() {
245 | if (onOnMemoryWarningHandlerInstalled || !sdk.onMemoryWarning) {
246 | return
247 | }
248 | sdk.onMemoryWarning(({ level = -1 }) => {
249 | let levelMessage = '没有获取到告警级别信息'
250 |
251 | switch (level) {
252 | case 5:
253 | levelMessage = 'TRIM_MEMORY_RUNNING_MODERATE'
254 | break
255 | case 10:
256 | levelMessage = 'TRIM_MEMORY_RUNNING_LOW'
257 | break
258 | case 15:
259 | levelMessage = 'TRIM_MEMORY_RUNNING_CRITICAL'
260 | break
261 | default:
262 | return
263 | }
264 | notifyHandlers(
265 | {
266 | message: levelMessage,
267 | type: 'memorywarning',
268 | name: '内存不足告警',
269 | },
270 | true,
271 | {},
272 | )
273 | })
274 | onOnMemoryWarningHandlerInstalled = true
275 | }
276 | /**
277 | * Reports an unhandled Error.
278 | * @param {Error} ex
279 | * @memberof report
280 | * @throws An exception if an incompvare stack trace is detected (old IE browsers).
281 | */
282 | function doReport(ex) {}
283 |
284 | doReport.subscribe = subscribe
285 | doReport.unsubscribe = unsubscribe
286 | doReport.traceKitWindowOnError = traceKitWindowOnError
287 |
288 | return doReport
289 | })()
290 |
291 | /**
292 | * computeStackTrace: cross-browser stack traces in JavaScript
293 | *
294 | * Syntax:
295 | * ```js
296 | * s = computeStackTrace.ofCaller([depth])
297 | * s = computeStackTrace(exception) // consider using report instead (see below)
298 | * ```
299 | *
300 | * Supports:
301 | * - Firefox: full stack trace with line numbers and unreliable column
302 | * number on top frame
303 | * - Opera 10: full stack trace with line and column numbers
304 | * - Opera 9-: full stack trace with line numbers
305 | * - Chrome: full stack trace with line and column numbers
306 | * - Safari: line and column number for the topmost stacktrace element
307 | * only
308 | * - IE: no line numbers whatsoever
309 | *
310 | * Tries to guess names of anonymous functions by looking for assignments
311 | * in the source code. In IE and Safari, we have to guess source file names
312 | * by searching for function bodies inside all page scripts. This will not
313 | * work for scripts that are loaded cross-domain.
314 | * Here be dragons: some function names may be guessed incorrectly, and
315 | * duplicate functions may be mismatched.
316 | *
317 | * computeStackTrace should only be used for tracing purposes.
318 | * Logging of unhandled exceptions should be done with report,
319 | * which builds on top of computeStackTrace and provides better
320 | * IE support by utilizing the sdk.onError event to retrieve information
321 | * about the top of the stack.
322 | *
323 | * Note: In IE and Safari, no stack trace is recorded on the Error object,
324 | * so computeStackTrace instead walks its *own* chain of callers.
325 | * This means that:
326 | * * in Safari, some methods may be missing from the stack trace;
327 | * * in IE, the topmost function in the stack trace will always be the
328 | * caller of computeStackTrace.
329 | *
330 | * This is okay for tracing (because you are likely to be calling
331 | * computeStackTrace from the function you want to be the topmost element
332 | * of the stack trace anyway), but not okay for logging unhandled
333 | * exceptions (because your catch block will likely be far away from the
334 | * inner function that actually caused the exception).
335 | *
336 | * Tracing example:
337 | * ```js
338 | * function trace(message) {
339 | * var stackInfo = computeStackTrace.ofCaller();
340 | * var data = message + "\n";
341 | * for(var i in stackInfo.stack) {
342 | * var item = stackInfo.stack[i];
343 | * data += (item.func || '[anonymous]') + "() in " + item.url + ":" + (item.line || '0') + "\n";
344 | * }
345 | * if (window.console)
346 | * console.info(data);
347 | * else
348 | * alert(data);
349 | * }
350 | * ```
351 | * @memberof TraceKit
352 | * @namespace
353 | */
354 | export var computeStackTrace = (function computeStackTraceWrapper() {
355 | var debug = false
356 |
357 | // Contents of Exception in various browsers.
358 | //
359 | // SAFARI:
360 | // ex.message = Can't find variable: qq
361 | // ex.line = 59
362 | // ex.sourceId = 580238192
363 | // ex.sourceURL = http://...
364 | // ex.expressionBeginOffset = 96
365 | // ex.expressionCaretOffset = 98
366 | // ex.expressionEndOffset = 98
367 | // ex.name = ReferenceError
368 | //
369 | // FIREFOX:
370 | // ex.message = qq is not defined
371 | // ex.fileName = http://...
372 | // ex.lineNumber = 59
373 | // ex.columnNumber = 69
374 | // ex.stack = ...stack trace... (see the example below)
375 | // ex.name = ReferenceError
376 | //
377 | // CHROME:
378 | // ex.message = qq is not defined
379 | // ex.name = ReferenceError
380 | // ex.type = not_defined
381 | // ex.arguments = ['aa']
382 | // ex.stack = ...stack trace...
383 | //
384 | // INTERNET EXPLORER:
385 | // ex.message = ...
386 | // ex.name = ReferenceError
387 | //
388 | // OPERA:
389 | // ex.message = ...message... (see the example below)
390 | // ex.name = ReferenceError
391 | // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message)
392 | // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace'
393 |
394 | /**
395 | * Computes stack trace information from the stack property.
396 | * Chrome and Gecko use this property.
397 | * @param {Error} ex
398 | * @return {?StackTrace} Stack trace information.
399 | * @memberof computeStackTrace
400 | */
401 | function computeStackTraceFromStackProp(ex) {
402 | if (!ex.stack) {
403 | return
404 | }
405 |
406 | // tslint:disable-next-line max-line-length
407 | var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i
408 | // tslint:disable-next-line max-line-length
409 | var gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i
410 | // tslint:disable-next-line max-line-length
411 | var winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i
412 |
413 | // Used to additionally parse URL/line/column from eval frames
414 | var isEval
415 | var geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i
416 | var chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/
417 | var lines = ex.stack.split('\n')
418 | var stack = []
419 | var submatch
420 | var parts
421 | var element
422 |
423 | for (var i = 0, j = lines.length; i < j; i += 1) {
424 | if (chrome.exec(lines[i])) {
425 | parts = chrome.exec(lines[i])
426 | var isNative = parts[2] && parts[2].indexOf('native') === 0 // start of line
427 | isEval = parts[2] && parts[2].indexOf('eval') === 0 // start of line
428 | submatch = chromeEval.exec(parts[2])
429 | if (isEval && submatch) {
430 | // throw out eval line/column and use top-most line/column number
431 | parts[2] = submatch[1] // url
432 | parts[3] = submatch[2] // line
433 | parts[4] = submatch[3] // column
434 | }
435 | element = {
436 | args: isNative ? [parts[2]] : [],
437 | column: parts[4] ? +parts[4] : undefined,
438 | func: parts[1] || UNKNOWN_FUNCTION,
439 | line: parts[3] ? +parts[3] : undefined,
440 | url: !isNative ? parts[2] : undefined,
441 | }
442 | } else if (winjs.exec(lines[i])) {
443 | parts = winjs.exec(lines[i])
444 | element = {
445 | args: [],
446 | column: parts[4] ? +parts[4] : undefined,
447 | func: parts[1] || UNKNOWN_FUNCTION,
448 | line: +parts[3],
449 | url: parts[2],
450 | }
451 | } else if (gecko.exec(lines[i])) {
452 | parts = gecko.exec(lines[i])
453 | isEval = parts[3] && parts[3].indexOf(' > eval') > -1
454 | submatch = geckoEval.exec(parts[3])
455 | if (isEval && submatch) {
456 | // throw out eval line/column and use top-most line number
457 | parts[3] = submatch[1]
458 | parts[4] = submatch[2]
459 | parts[5] = undefined // no column when eval
460 | } else if (i === 0 && !parts[5] && !isUndefined(ex.columnNumber)) {
461 | // FireFox uses this awesome columnNumber property for its top frame
462 | // Also note, Firefox's column number is 0-based and everything else expects 1-based,
463 | // so adding 1
464 | // NOTE: this hack doesn't work if top-most frame is eval
465 | stack[0].column = ex.columnNumber + 1
466 | }
467 | element = {
468 | args: parts[2] ? parts[2].split(',') : [],
469 | column: parts[5] ? +parts[5] : undefined,
470 | func: parts[1] || UNKNOWN_FUNCTION,
471 | line: parts[4] ? +parts[4] : undefined,
472 | url: parts[3],
473 | }
474 | } else {
475 | continue
476 | }
477 |
478 | if (!element.func && element.line) {
479 | element.func = UNKNOWN_FUNCTION
480 | }
481 | stack.push(element)
482 | }
483 |
484 | if (!stack.length) {
485 | return
486 | }
487 |
488 | return {
489 | stack,
490 | message: extractMessage(ex),
491 | name: ex.name,
492 | }
493 | }
494 |
495 | /**
496 | * Computes stack trace information from the stacktrace property.
497 | * Opera 10+ uses this property.
498 | * @param {Error} ex
499 | * @return {?StackTrace} Stack trace information.
500 | * @memberof computeStackTrace
501 | */
502 | function computeStackTraceFromStacktraceProp(ex) {
503 | // Access and store the stacktrace property before doing ANYTHING
504 | // else to it because Opera is not very good at providing it
505 | // reliably in other circumstances.
506 | var stacktrace = ex.stacktrace
507 | if (!stacktrace) {
508 | return
509 | }
510 |
511 | var opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i
512 | // tslint:disable-next-line max-line-length
513 | var opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i
514 | var lines = stacktrace.split('\n')
515 | var stack = []
516 | var parts
517 |
518 | for (var line = 0; line < lines.length; line += 2) {
519 | var element
520 | if (opera10Regex.exec(lines[line])) {
521 | parts = opera10Regex.exec(lines[line])
522 | element = {
523 | args: [],
524 | column: undefined,
525 | func: parts[3],
526 | line: +parts[1],
527 | url: parts[2],
528 | }
529 | } else if (opera11Regex.exec(lines[line])) {
530 | parts = opera11Regex.exec(lines[line])
531 | element = {
532 | args: parts[5] ? parts[5].split(',') : [],
533 | column: +parts[2],
534 | func: parts[3] || parts[4],
535 | line: +parts[1],
536 | url: parts[6],
537 | }
538 | }
539 |
540 | if (element) {
541 | if (!element.func && element.line) {
542 | element.func = UNKNOWN_FUNCTION
543 | }
544 | element.context = [lines[line + 1]]
545 |
546 | stack.push(element)
547 | }
548 | }
549 |
550 | if (!stack.length) {
551 | return
552 | }
553 |
554 | return {
555 | stack,
556 | message: extractMessage(ex),
557 | name: ex.name,
558 | }
559 | }
560 |
561 | /**
562 | * NOT TESTED.
563 | * Computes stack trace information from an error message that includes
564 | * the stack trace.
565 | * Opera 9 and earlier use this method if the option to show stack
566 | * traces is turned on in opera:config.
567 | * @param {Error} ex
568 | * @return {?StackTrace} Stack information.
569 | * @memberof computeStackTrace
570 | */
571 | function computeStackTraceFromOperaMultiLineMessage(ex) {
572 | // TODO: Clean this function up
573 | // Opera includes a stack trace into the exception message. An example is:
574 | //
575 | // Statement on line 3: Undefined variable: undefinedFunc
576 | // Backtrace:
577 | // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js:
578 | // In function zzz
579 | // undefinedFunc(a);
580 | // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html:
581 | // In function yyy
582 | // zzz(x, y, z);
583 | // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html:
584 | // In function xxx
585 | // yyy(a, a, a);
586 | // Line 1 of function script
587 | // try { xxx('hi'); return false; } catch(ex) { report(ex); }
588 | // ...
589 |
590 | var lines = ex.message.split('\n')
591 | if (lines.length < 4) {
592 | return
593 | }
594 |
595 | var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i
596 | var lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i
597 | var lineRE3 = /^\s*Line (\d+) of function script\s*$/i
598 | var stack = []
599 | var scripts =
600 | window &&
601 | window.document &&
602 | window.document.getElementsByTagName('script')
603 | var inlineScriptBlocks = []
604 | var parts
605 |
606 | for (var s in scripts) {
607 | if (has(scripts, s) && !scripts[s].src) {
608 | inlineScriptBlocks.push(scripts[s])
609 | }
610 | }
611 |
612 | for (var line = 2; line < lines.length; line += 2) {
613 | var item
614 | if (lineRE1.exec(lines[line])) {
615 | parts = lineRE1.exec(lines[line])
616 | item = {
617 | args: [],
618 | column: undefined,
619 | func: parts[3],
620 | line: +parts[1],
621 | url: parts[2],
622 | }
623 | } else if (lineRE2.exec(lines[line])) {
624 | parts = lineRE2.exec(lines[line])
625 | item = {
626 | args: [],
627 | column: undefined, // TODO: Check to see if inline#1 (+parts[2]) points to the script number or column number.
628 | func: parts[4],
629 | line: +parts[1],
630 | url: parts[3],
631 | }
632 | } else if (lineRE3.exec(lines[line])) {
633 | parts = lineRE3.exec(lines[line])
634 | var url = window.location.href.replace(/#.*$/, '')
635 | item = {
636 | url,
637 | args: [],
638 | column: undefined,
639 | func: '',
640 | line: +parts[1],
641 | }
642 | }
643 |
644 | if (item) {
645 | if (!item.func) {
646 | item.func = UNKNOWN_FUNCTION
647 | }
648 | item.context = [lines[line + 1]]
649 | stack.push(item)
650 | }
651 | }
652 | if (!stack.length) {
653 | return // could not parse multiline exception message as Opera stack trace
654 | }
655 |
656 | return {
657 | stack,
658 | message: lines[0],
659 | name: ex.name,
660 | }
661 | }
662 |
663 | /**
664 | * Adds information about the first frame to incompvare stack traces.
665 | * Safari and IE require this to get compvare data on the first frame.
666 | * @param {StackTrace} stackInfo Stack trace information from
667 | * one of the compute* methods.
668 | * @param {string=} url The URL of the script that caused an error.
669 | * @param {(number|string)=} lineNo The line number of the script that
670 | * caused an error.
671 | * @param {string=} message The error generated by the browser, which
672 | * hopefully contains the name of the object that caused the error.
673 | * @return {boolean} Whether or not the stack information was
674 | * augmented.
675 | * @memberof computeStackTrace
676 | */
677 | function augmentStackTraceWithInitialElement(
678 | stackInfo,
679 | url,
680 | lineNo,
681 | message,
682 | ) {
683 | var initial = {
684 | url,
685 | line: lineNo ? +lineNo : undefined,
686 | }
687 |
688 | if (initial.url && initial.line) {
689 | stackInfo.incompvare = false
690 |
691 | var stack = stackInfo.stack
692 | if (stack.length > 0) {
693 | if (stack[0].url === initial.url) {
694 | if (stack[0].line === initial.line) {
695 | return false // already in stack trace
696 | }
697 | if (!stack[0].line && stack[0].func === initial.func) {
698 | stack[0].line = initial.line
699 | stack[0].context = initial.context
700 | return false
701 | }
702 | }
703 | }
704 |
705 | stack.unshift(initial)
706 | stackInfo.partial = true
707 | return true
708 | }
709 | stackInfo.incompvare = true
710 |
711 | return false
712 | }
713 |
714 | /**
715 | * Computes stack trace information by walking the arguments.caller
716 | * chain at the time the exception occurred. This will cause earlier
717 | * frames to be missed but is the only way to get any stack trace in
718 | * Safari and IE. The top frame is restored by
719 | * {@link augmentStackTraceWithInitialElement}.
720 | * @param {Error} ex
721 | * @param {number} depth
722 | * @return {StackTrace} Stack trace information.
723 | * @memberof computeStackTrace
724 | */
725 | function computeStackTraceByWalkingCallerChain(ex, depth) {
726 | var functionName = /function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i
727 | var stack = []
728 | var funcs = {}
729 | var recursion = false
730 | var parts
731 | var item
732 |
733 | for (
734 | var curr = computeStackTraceByWalkingCallerChain.caller;
735 | curr && !recursion;
736 | curr = curr.caller
737 | ) {
738 | if (curr === computeStackTrace || curr === report) {
739 | continue
740 | }
741 |
742 | item = {
743 | args: [],
744 | column: undefined,
745 | func: UNKNOWN_FUNCTION,
746 | line: undefined,
747 | url: undefined,
748 | }
749 |
750 | parts = functionName.exec(curr.toString())
751 | if (curr.name) {
752 | item.func = curr.name
753 | } else if (parts) {
754 | item.func = parts[1]
755 | }
756 |
757 | if (typeof item.func === 'undefined') {
758 | item.func = parts
759 | ? parts.input.substring(0, parts.input.indexOf('{'))
760 | : undefined
761 | }
762 |
763 | if (funcs[curr + '']) {
764 | recursion = true
765 | } else {
766 | funcs[curr + ''] = true
767 | }
768 |
769 | stack.push(item)
770 | }
771 |
772 | if (depth) {
773 | stack.splice(0, depth)
774 | }
775 |
776 | var result = {
777 | stack,
778 | message: ex.message,
779 | name: ex.name,
780 | }
781 | augmentStackTraceWithInitialElement(
782 | result,
783 | ex.sourceURL || ex.fileName,
784 | ex.line || ex.lineNumber,
785 | ex.message || ex.description,
786 | )
787 | return result
788 | }
789 |
790 | /**
791 | * Computes a stack trace for an exception.
792 | * @param {Error} ex
793 | * @param {(string|number)=} depth
794 | * @memberof computeStackTrace
795 | */
796 | function doComputeStackTrace(ex, depth) {
797 | var stack
798 | var normalizedDepth = depth === undefined ? 0 : +depth
799 |
800 | try {
801 | // This must be tried first because Opera 10 *destroys*
802 | // its stacktrace property if you try to access the stack
803 | // property first!!
804 | stack = computeStackTraceFromStacktraceProp(ex)
805 | if (stack) {
806 | return stack
807 | }
808 | } catch (e) {
809 | if (debug) {
810 | throw e
811 | }
812 | }
813 |
814 | try {
815 | stack = computeStackTraceFromStackProp(ex)
816 | if (stack) {
817 | return stack
818 | }
819 | } catch (e) {
820 | if (debug) {
821 | throw e
822 | }
823 | }
824 |
825 | try {
826 | stack = computeStackTraceFromOperaMultiLineMessage(ex)
827 | if (stack) {
828 | return stack
829 | }
830 | } catch (e) {
831 | if (debug) {
832 | throw e
833 | }
834 | }
835 |
836 | try {
837 | stack = computeStackTraceByWalkingCallerChain(ex, normalizedDepth + 1)
838 | if (stack) {
839 | return stack
840 | }
841 | } catch (e) {
842 | if (debug) {
843 | throw e
844 | }
845 | }
846 |
847 | return {
848 | message: extractMessage(ex),
849 | name: ex.name,
850 | stack: [],
851 | }
852 | }
853 |
854 | /**
855 | * Logs a stacktrace starting from the previous call and working down.
856 | * @param {(number|string)=} depth How many frames deep to trace.
857 | * @return {StackTrace} Stack trace information.
858 | * @memberof computeStackTrace
859 | */
860 | function computeStackTraceOfCaller(depth) {
861 | var currentDepth = (depth === undefined ? 0 : +depth) + 1 // "+ 1" because "ofCaller" should drop one frame
862 | try {
863 | throw new Error()
864 | } catch (ex) {
865 | return computeStackTrace(ex, currentDepth + 1)
866 | }
867 | }
868 |
869 | doComputeStackTrace.augmentStackTraceWithInitialElement = augmentStackTraceWithInitialElement
870 | doComputeStackTrace.computeStackTraceFromStackProp = computeStackTraceFromStackProp
871 | doComputeStackTrace.ofCaller = computeStackTraceOfCaller
872 |
873 | return doComputeStackTrace
874 | })()
875 | var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/
876 | function extractMessage(ex) {
877 | const message = ex && ex.message
878 | // console.log('message',message)
879 | if (!message) {
880 | return 'No error message'
881 | }
882 | if (message.error && typeof message.error.message === 'string') {
883 | return message.error.message
884 | }
885 |
886 | return message
887 | }
888 |
--------------------------------------------------------------------------------
/src/helper/utils.js:
--------------------------------------------------------------------------------
1 | import { MpHook } from './enums'
2 | var ArrayProto = Array.prototype
3 | var ObjProto = Object.prototype
4 | var ObjProto = Object.prototype
5 | var hasOwnProperty = ObjProto.hasOwnProperty
6 | var slice = ArrayProto.slice
7 | var toString = ObjProto.toString
8 | var nativeForEach = ArrayProto.forEach
9 | var nativeIsArray = Array.isArray
10 | var breaker = false
11 | export var isArguments = function (obj) {
12 | return !!(obj && hasOwnProperty.call(obj, 'callee'))
13 | }
14 | export var each = function (obj, iterator, context) {
15 | if (obj === null) return false
16 | if (nativeForEach && obj.forEach === nativeForEach) {
17 | obj.forEach(iterator, context)
18 | } else if (obj.length === +obj.length) {
19 | for (var i = 0, l = obj.length; i < l; i++) {
20 | if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) {
21 | return false
22 | }
23 | }
24 | } else {
25 | for (var key in obj) {
26 | if (hasOwnProperty.call(obj, key)) {
27 | if (iterator.call(context, obj[key], key, obj) === breaker) {
28 | return false
29 | }
30 | }
31 | }
32 | }
33 | }
34 | export var values = function (obj) {
35 | var results = []
36 | if (obj === null) {
37 | return results
38 | }
39 | each(obj, function (value) {
40 | results[results.length] = value
41 | })
42 | return results
43 | }
44 | export function round(num, decimals) {
45 | return +num.toFixed(decimals)
46 | }
47 | export function toServerDuration(duration) {
48 | if (!isNumber(duration)) {
49 | return duration
50 | }
51 | return round(duration * 1e6, 0)
52 | }
53 | export function msToNs(duration) {
54 | if (typeof duration !== 'number') {
55 | return duration
56 | }
57 | return round(duration * 1e6, 0)
58 | }
59 | export var isUndefined = function (obj) {
60 | return obj === void 0
61 | }
62 | export var isString = function (obj) {
63 | return toString.call(obj) === '[object String]'
64 | }
65 | export var isDate = function (obj) {
66 | return toString.call(obj) === '[object Date]'
67 | }
68 | export var isBoolean = function (obj) {
69 | return toString.call(obj) === '[object Boolean]'
70 | }
71 | export var isNumber = function (obj) {
72 | return toString.call(obj) === '[object Number]' && /[\d\.]+/.test(String(obj))
73 | }
74 | export var isArray =
75 | nativeIsArray ||
76 | function (obj) {
77 | return toString.call(obj) === '[object Array]'
78 | }
79 | export var toArray = function (iterable) {
80 | if (!iterable) return []
81 | if (iterable.toArray) {
82 | return iterable.toArray()
83 | }
84 | if (Array.isArray(iterable)) {
85 | return slice.call(iterable)
86 | }
87 | if (isArguments(iterable)) {
88 | return slice.call(iterable)
89 | }
90 | return values(iterable)
91 | }
92 | export var areInOrder = function () {
93 | var numbers = toArray(arguments)
94 | for (var i = 1; i < numbers.length; i += 1) {
95 | if (numbers[i - 1] > numbers[i]) {
96 | return false
97 | }
98 | }
99 | return true
100 | }
101 | /**
102 | * UUID v4
103 | * from https://gist.github.com/jed/982883
104 | */
105 | export function UUID(placeholder) {
106 | return placeholder
107 | ? // tslint:disable-next-line no-bitwise
108 | (
109 | parseInt(placeholder, 10) ^
110 | ((Math.random() * 16) >> (parseInt(placeholder, 10) / 4))
111 | ).toString(16)
112 | : `${1e7}-${1e3}-${4e3}-${8e3}-${1e11}`.replace(/[018]/g, UUID)
113 | }
114 | export function jsonStringify(value, replacer, space) {
115 | if (value === null || value === undefined) {
116 | return JSON.stringify(value)
117 | }
118 | var originalToJSON = [false, undefined]
119 | if (hasToJSON(value)) {
120 | // We need to add a flag and not rely on the truthiness of value.toJSON
121 | // because it can be set but undefined and that's actually significant.
122 | originalToJSON = [true, value.toJSON]
123 | delete value.toJSON
124 | }
125 |
126 | var originalProtoToJSON = [false, undefined]
127 | var prototype
128 | if (typeof value === 'object') {
129 | prototype = Object.getPrototypeOf(value)
130 | if (hasToJSON(prototype)) {
131 | originalProtoToJSON = [true, prototype.toJSON]
132 | delete prototype.toJSON
133 | }
134 | }
135 |
136 | var result
137 | try {
138 | result = JSON.stringify(value, undefined, space)
139 | } catch (e) {
140 | result = ''
141 | } finally {
142 | if (originalToJSON[0]) {
143 | value.toJSON = originalToJSON[1]
144 | }
145 | if (originalProtoToJSON[0]) {
146 | prototype.toJSON = originalProtoToJSON[1]
147 | }
148 | }
149 | return result
150 | }
151 | export var utf8Encode = function (string) {
152 | string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
153 |
154 | var utftext = '',
155 | start,
156 | end
157 | var stringl = 0,
158 | n
159 |
160 | start = end = 0
161 | stringl = string.length
162 |
163 | for (n = 0; n < stringl; n++) {
164 | var c1 = string.charCodeAt(n)
165 | var enc = null
166 |
167 | if (c1 < 128) {
168 | end++
169 | } else if (c1 > 127 && c1 < 2048) {
170 | enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128)
171 | } else {
172 | enc = String.fromCharCode(
173 | (c1 >> 12) | 224,
174 | ((c1 >> 6) & 63) | 128,
175 | (c1 & 63) | 128
176 | )
177 | }
178 | if (enc !== null) {
179 | if (end > start) {
180 | utftext += string.substring(start, end)
181 | }
182 | utftext += enc
183 | start = end = n + 1
184 | }
185 | }
186 |
187 | if (end > start) {
188 | utftext += string.substring(start, string.length)
189 | }
190 |
191 | return utftext
192 | }
193 | export var base64Encode = function (data) {
194 | data = String(data)
195 | var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
196 | var o1,
197 | o2,
198 | o3,
199 | h1,
200 | h2,
201 | h3,
202 | h4,
203 | bits,
204 | i = 0,
205 | ac = 0,
206 | enc = '',
207 | tmp_arr = []
208 | if (!data) {
209 | return data
210 | }
211 | data = utf8Encode(data)
212 | do {
213 | o1 = data.charCodeAt(i++)
214 | o2 = data.charCodeAt(i++)
215 | o3 = data.charCodeAt(i++)
216 |
217 | bits = (o1 << 16) | (o2 << 8) | o3
218 |
219 | h1 = (bits >> 18) & 0x3f
220 | h2 = (bits >> 12) & 0x3f
221 | h3 = (bits >> 6) & 0x3f
222 | h4 = bits & 0x3f
223 | tmp_arr[ac++] =
224 | b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4)
225 | } while (i < data.length)
226 |
227 | enc = tmp_arr.join('')
228 |
229 | switch (data.length % 3) {
230 | case 1:
231 | enc = enc.slice(0, -2) + '=='
232 | break
233 | case 2:
234 | enc = enc.slice(0, -1) + '='
235 | break
236 | }
237 |
238 | return enc
239 | }
240 | function hasToJSON(value) {
241 | return (
242 | typeof value === 'object' &&
243 | value !== null &&
244 | value.hasOwnProperty('toJSON')
245 | )
246 | }
247 | export function elapsed(start, end) {
248 | return end - start
249 | }
250 | export function getMethods(obj) {
251 | var funcs = []
252 | for (var key in obj) {
253 | if (typeof obj[key] === 'function' && !MpHook[key]) {
254 | funcs.push(key)
255 | }
256 | }
257 | return funcs
258 | }
259 | // 替换url包含数字的路由
260 | export function replaceNumberCharByPath(path) {
261 | if (path) {
262 | return path.replace(/\/([^\/]*)\d([^\/]*)/g, '/?')
263 | } else {
264 | return ''
265 | }
266 | }
267 | export function getStatusGroup(status) {
268 | if (!status) return status
269 | return (
270 | String(status).substr(0, 1) + String(status).substr(1).replace(/\d*/g, 'x')
271 | )
272 | }
273 | export var getQueryParamsFromUrl = function (url) {
274 | var result = {}
275 | var arr = url.split('?')
276 | var queryString = arr[1] || ''
277 | if (queryString) {
278 | result = getURLSearchParams('?' + queryString)
279 | }
280 | return result
281 | }
282 | export var getURLSearchParams = function (queryString) {
283 | queryString = queryString || ''
284 | var decodeParam = function (str) {
285 | return decodeURIComponent(str)
286 | }
287 | var args = {}
288 | var query = queryString.substring(1)
289 | var pairs = query.split('&')
290 | for (var i = 0; i < pairs.length; i++) {
291 | var pos = pairs[i].indexOf('=')
292 | if (pos === -1) continue
293 | var name = pairs[i].substring(0, pos)
294 | var value = pairs[i].substring(pos + 1)
295 | name = decodeParam(name)
296 | value = decodeParam(value)
297 | args[name] = value
298 | }
299 | return args
300 | }
301 | export function isPercentage(value) {
302 | return isNumber(value) && value >= 0 && value <= 100
303 | }
304 |
305 | export var extend = function (obj) {
306 | slice.call(arguments, 1).forEach(function (source) {
307 | for (var prop in source) {
308 | if (source[prop] !== void 0) {
309 | obj[prop] = source[prop]
310 | }
311 | }
312 | })
313 | return obj
314 | }
315 | export var extend2Lev = function (obj) {
316 | slice.call(arguments, 1).forEach(function (source) {
317 | for (var prop in source) {
318 | if (source[prop] !== void 0) {
319 | if (isObject(source[prop]) && isObject(obj[prop])) {
320 | extend(obj[prop], source[prop])
321 | } else {
322 | obj[prop] = source[prop]
323 | }
324 | }
325 | }
326 | })
327 | return obj
328 | }
329 |
330 | export var trim = function (str) {
331 | return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')
332 | }
333 | export var isObject = function (obj) {
334 | if (obj === null) return false
335 | return toString.call(obj) === '[object Object]'
336 | }
337 | export var isEmptyObject = function (obj) {
338 | if (isObject(obj)) {
339 | for (var key in obj) {
340 | if (hasOwnProperty.call(obj, key)) {
341 | return false
342 | }
343 | }
344 | return true
345 | } else {
346 | return false
347 | }
348 | }
349 |
350 | export var isJSONString = function (str) {
351 | try {
352 | JSON.parse(str)
353 | } catch (e) {
354 | return false
355 | }
356 | return true
357 | }
358 | export var safeJSONParse = function (str) {
359 | var val = null
360 | try {
361 | val = JSON.parse(str)
362 | } catch (e) {
363 | return false
364 | }
365 | return val
366 | }
367 | export var now =
368 | Date.now ||
369 | function () {
370 | return new Date().getTime()
371 | }
372 | export var throttle = function (func, wait, options) {
373 | var timeout, context, args, result
374 | var previous = 0
375 | if (!options) options = {}
376 |
377 | var later = function () {
378 | previous = options.leading === false ? 0 : new Date().getTime()
379 | timeout = null
380 | result = func.apply(context, args)
381 | if (!timeout) context = args = null
382 | }
383 |
384 | var throttled = function () {
385 | args = arguments
386 | var now = new Date().getTime()
387 | if (!previous && options.leading === false) previous = now
388 | //下次触发 func 剩余的时间
389 | var remaining = wait - (now - previous)
390 | context = this
391 | // 如果没有剩余的时间了或者你改了系统时间
392 | if (remaining <= 0 || remaining > wait) {
393 | if (timeout) {
394 | clearTimeout(timeout)
395 | timeout = null
396 | }
397 | previous = now
398 | result = func.apply(context, args)
399 | if (!timeout) context = args = null
400 | } else if (!timeout && options.trailing !== false) {
401 | timeout = setTimeout(later, remaining)
402 | }
403 | return result
404 | }
405 | throttled.cancel = function () {
406 | clearTimeout(timeout)
407 | previous = 0
408 | timeout = null
409 | }
410 | return throttled
411 | }
412 | export function noop() {}
413 | /**
414 | * Return true if the draw is successful
415 | * @param threshold between 0 and 100
416 | */
417 | export function performDraw(threshold) {
418 | return threshold !== 0 && Math.random() * 100 <= threshold
419 | }
420 | export function findByPath(source, path) {
421 | var pathArr = path.split('.')
422 | while (pathArr.length) {
423 | var key = pathArr.shift()
424 | if (source && key in source && hasOwnProperty.call(source, key)) {
425 | source = source[key]
426 | } else {
427 | return undefined
428 | }
429 | }
430 | return source
431 | }
432 | export function withSnakeCaseKeys(candidate) {
433 | const result = {}
434 | Object.keys(candidate).forEach((key) => {
435 | result[toSnakeCase(key)] = deepSnakeCase(candidate[key])
436 | })
437 | return result
438 | }
439 |
440 | export function deepSnakeCase(candidate) {
441 | if (Array.isArray(candidate)) {
442 | return candidate.map((value) => deepSnakeCase(value))
443 | }
444 | if (typeof candidate === 'object' && candidate !== null) {
445 | return withSnakeCaseKeys(candidate)
446 | }
447 | return candidate
448 | }
449 |
450 | export function toSnakeCase(word) {
451 | return word
452 | .replace(/[A-Z]/g, function (uppercaseLetter, index) {
453 | return (index !== 0 ? '_' : '') + uppercaseLetter.toLowerCase()
454 | })
455 | .replace(/-/g, '_')
456 | }
457 |
458 | export function escapeRowData(str) {
459 | if (typeof str === 'object' && str) {
460 | str = jsonStringify(str)
461 | } else if (!isString(str)) {
462 | return str
463 | }
464 | var reg = /[\s=,"]/g
465 | return String(str).replace(reg, function (word) {
466 | return '\\' + word
467 | })
468 | }
469 | export var urlParse = function (para) {
470 | var URLParser = function (a) {
471 | this._fields = {
472 | Username: 4,
473 | Password: 5,
474 | Port: 7,
475 | Protocol: 2,
476 | Host: 6,
477 | Path: 8,
478 | URL: 0,
479 | QueryString: 9,
480 | Fragment: 10,
481 | }
482 | this._values = {}
483 | this._regex = null
484 | this._regex = /^((\w+):\/\/)?((\w+):?(\w+)?@)?([^\/\?:]+):?(\d+)?(\/?[^\?#]+)?\??([^#]+)?#?(\w*)/
485 |
486 | if (typeof a != 'undefined') {
487 | this._parse(a)
488 | }
489 | }
490 | URLParser.prototype.setUrl = function (a) {
491 | this._parse(a)
492 | }
493 | URLParser.prototype._initValues = function () {
494 | for (var a in this._fields) {
495 | this._values[a] = ''
496 | }
497 | }
498 | URLParser.prototype.addQueryString = function (queryObj) {
499 | if (typeof queryObj !== 'object') {
500 | return false
501 | }
502 | var query = this._values.QueryString || ''
503 | for (var i in queryObj) {
504 | if (new RegExp(i + '[^&]+').test(query)) {
505 | query = query.replace(new RegExp(i + '[^&]+'), i + '=' + queryObj[i])
506 | } else {
507 | if (query.slice(-1) === '&') {
508 | query = query + i + '=' + queryObj[i]
509 | } else {
510 | if (query === '') {
511 | query = i + '=' + queryObj[i]
512 | } else {
513 | query = query + '&' + i + '=' + queryObj[i]
514 | }
515 | }
516 | }
517 | }
518 | this._values.QueryString = query
519 | }
520 | URLParser.prototype.getParse = function () {
521 | return this._values
522 | }
523 | URLParser.prototype.getUrl = function () {
524 | var url = ''
525 | url += this._values.Origin
526 | // url += this._values.Port ? ':' + this._values.Port : ''
527 | url += this._values.Path
528 | url += this._values.QueryString ? '?' + this._values.QueryString : ''
529 | return url
530 | }
531 | URLParser.prototype._parse = function (a) {
532 | this._initValues()
533 | var b = this._regex.exec(a)
534 | if (!b) {
535 | throw 'DPURLParser::_parse -> Invalid URL'
536 | }
537 | for (var c in this._fields) {
538 | if (typeof b[this._fields[c]] != 'undefined') {
539 | this._values[c] = b[this._fields[c]]
540 | }
541 | }
542 | this._values['Path'] = this._values['Path'] || '/'
543 | this._values['Hostname'] = this._values['Host'].replace(/:\d+$/, '')
544 | this._values['Origin'] =
545 | this._values['Protocol'] + '://' + this._values['Hostname'] + (this._values.Port ? ':' + this._values.Port : '')
546 | }
547 | return new URLParser(para)
548 | }
549 | export const getOwnObjectKeys = function (obj, isEnumerable) {
550 | var keys = Object.keys(obj)
551 | if (Object.getOwnPropertySymbols) {
552 | var symbols = Object.getOwnPropertySymbols(obj)
553 | if (isEnumerable) {
554 | symbols = symbols.filter(function (t) {
555 | return Object.getOwnPropertyDescriptor(obj, t).enumerable
556 | })
557 | }
558 | keys.push.apply(keys, symbols)
559 | }
560 | return keys
561 | }
562 | export const defineObject = function (obj, key, value) {
563 | if (key in obj) {
564 | Object.defineProperty(obj, key, {
565 | value,
566 | enumerable: true,
567 | configurable: true,
568 | writable: true,
569 | })
570 | } else {
571 | obj[key] = value
572 | }
573 | return obj
574 | }
575 | export const deepMixObject = function (targetObj) {
576 | for (var t = 1; t < arguments.length; t++) {
577 | var target = arguments[t] != null ? arguments[t] : {}
578 | if (t % 2) {
579 | getOwnObjectKeys(Object(target), true).forEach(function (t) {
580 | defineObject(targetObj, t, target[t])
581 | })
582 | } else {
583 | if (Object.getOwnPropertyDescriptors) {
584 | Object.defineProperties(
585 | targetObj,
586 | Object.getOwnPropertyDescriptors(target),
587 | )
588 | } else {
589 | getOwnObjectKeys(Object(target)).forEach(function (t) {
590 | Object.defineProperty(
591 | targetObj,
592 | t,
593 | Object.getOwnPropertyDescriptor(target, t),
594 | )
595 | })
596 | }
597 | }
598 | }
599 | return targetObj
600 | }
601 | export function getOrigin(url) {
602 | return urlParse(url).getParse().Origin
603 | }
604 | export function createContextManager() {
605 | var context = {}
606 |
607 | return {
608 | get: function () {
609 | return context
610 | },
611 |
612 | add: function (key, value) {
613 | if (isString(key)) {
614 | context[key] = value
615 | } else {
616 | console.error('key 需要传递字符串类型')
617 | }
618 | },
619 |
620 | remove: function (key) {
621 | delete context[key]
622 | },
623 |
624 | set: function (newContext) {
625 | if (isObject(newContext)) {
626 | context = newContext
627 | } else {
628 | console.error('content 需要传递对象类型数据')
629 | }
630 |
631 | }
632 | }
633 | }
634 | export function getActivePage() {
635 | const curPages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
636 | if (curPages.length) {
637 | return curPages[curPages.length - 1]
638 | }
639 | return {}
640 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { datafluxRum } from './boot/rum.entry'
2 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/action/actionCollection.js:
--------------------------------------------------------------------------------
1 | import { msToNs, extend2Lev } from '../../helper/utils'
2 | import { LifeCycleEventType } from '../../core/lifeCycle'
3 | import { RumEventType } from '../../helper/enums'
4 | import { trackActions } from './trackActions'
5 |
6 | export function startActionCollection(lifeCycle, configuration, Vue) {
7 | lifeCycle.subscribe(
8 | LifeCycleEventType.AUTO_ACTION_COMPLETED,
9 | function (action) {
10 | lifeCycle.notify(
11 | LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
12 | processAction(action),
13 | )
14 | },
15 | )
16 | if (configuration.trackInteractions) {
17 | trackActions(lifeCycle, Vue)
18 | }
19 | }
20 |
21 | function processAction(action) {
22 | var autoActionProperties = {
23 | action: {
24 | error: {
25 | count: action.counts.errorCount,
26 | },
27 | id: action.id,
28 | loadingTime: msToNs(action.duration),
29 | long_task: {
30 | count: action.counts.longTaskCount,
31 | },
32 | resource: {
33 | count: action.counts.resourceCount,
34 | },
35 | },
36 | }
37 | var actionEvent = extend2Lev(
38 | {
39 | action: {
40 | target: {
41 | name: action.name,
42 | },
43 | type: action.type,
44 | },
45 | date: action.startClocks,
46 | type: RumEventType.ACTION,
47 | },
48 | autoActionProperties,
49 | )
50 | return {
51 | rawRumEvent: actionEvent,
52 | startTime: action.startClocks,
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/action/trackActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | elapsed,
3 | now,
4 | UUID,
5 | getMethods,
6 | isObject,
7 | keys,
8 | } from '../../helper/utils'
9 | import { LifeCycleEventType } from '../../core/lifeCycle'
10 | import { trackEventCounts } from '../trackEventCounts'
11 | import { waitIdlePageActivity } from '../trackPageActiveites'
12 | import { ActionType } from '../../helper/enums'
13 | export function trackActions(lifeCycle, Vue) {
14 | var action = startActionManagement(lifeCycle)
15 |
16 | // New views trigger the discard of the current pending Action
17 | lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, function () {
18 | action.discardCurrent()
19 | })
20 | var originVueExtend = Vue.extend
21 |
22 | Vue.extend = function (vueOptions) {
23 | // methods 方法
24 | if (vueOptions.methods) {
25 | const vueMethods = Object.keys(vueOptions.methods)
26 | vueMethods.forEach((methodName) => {
27 | clickProxy(
28 | vueOptions.methods,
29 | methodName,
30 | function (_action) {
31 | action.create(_action.type, _action.name)
32 | },
33 | lifeCycle,
34 | )
35 | })
36 | }
37 |
38 | const originMethods = getMethods(vueOptions)
39 | originMethods.forEach((methodName) => {
40 | clickProxy(
41 | vueOptions,
42 | methodName,
43 | function (_action) {
44 | action.create(_action.type, _action.name)
45 | },
46 | lifeCycle,
47 | )
48 | })
49 | return originVueExtend.call(this, vueOptions)
50 | }
51 | // var originPage = Page
52 | // Page = function (page) {
53 | // const methods = getMethods(page)
54 | // methods.forEach((methodName) => {
55 | // clickProxy(
56 | // page,
57 | // methodName,
58 | // function (_action) {
59 | // action.create(_action.type, _action.name)
60 | // },
61 | // lifeCycle,
62 | // )
63 | // })
64 | // return originPage(page)
65 | // }
66 | // var originComponent = Component
67 | // Component = function (component) {
68 | // const methods = getMethods(component)
69 | // methods.forEach((methodName) => {
70 | // clickProxy(component, methodName, function (_action) {
71 | // action.create(_action.type, _action.name)
72 | // })
73 | // })
74 | // return originComponent(component)
75 | // }
76 | return {
77 | stop: function () {
78 | action.discardCurrent()
79 | // stopListener()
80 | },
81 | }
82 | }
83 | function clickProxy(page, methodName, callback, lifeCycle) {
84 | var oirginMethod = page[methodName]
85 |
86 | page[methodName] = function () {
87 | const result = oirginMethod.apply(this, arguments)
88 | var action = {}
89 | if (isObject(arguments[0])) {
90 | var currentTarget = arguments[0].currentTarget || {}
91 | var dataset = currentTarget.dataset || {}
92 | var actionType = arguments[0].type
93 | if (actionType && ActionType[actionType]) {
94 | action.type = actionType
95 | action.name = dataset.name || dataset.content || dataset.type
96 | callback(action)
97 | lifeCycle.notify(LifeCycleEventType.PAGE_ALIAS_ACTION, true)
98 | } else if (methodName === 'onAddToFavorites') {
99 | action.type = 'click'
100 | action.name =
101 | '收藏 ' +
102 | '标题: ' +
103 | result.title +
104 | (result.query ? ' query: ' + result.query : '')
105 | callback(action)
106 | lifeCycle.notify(LifeCycleEventType.PAGE_ALIAS_ACTION, true)
107 | } else if (methodName === 'onShareAppMessage') {
108 | action.type = 'click'
109 | action.name =
110 | '转发 ' +
111 | '标题: ' +
112 | result.title +
113 | (result.path ? ' path: ' + result.path : '')
114 | callback(action)
115 | lifeCycle.notify(LifeCycleEventType.PAGE_ALIAS_ACTION, true)
116 | } else if (methodName === 'onShareTimeline') {
117 | action.type = 'click'
118 | action.name =
119 | '分享到朋友圈 ' +
120 | '标题: ' +
121 | result.title +
122 | (result.query ? ' query: ' + result.query : '')
123 | callback(action)
124 | lifeCycle.notify(LifeCycleEventType.PAGE_ALIAS_ACTION, true)
125 | } else if (methodName === 'onTabItemTap') {
126 | var item = arguments.length && arguments[0]
127 | action.type = 'click'
128 | action.name =
129 | 'tab ' +
130 | '名称: ' +
131 | item.text +
132 | (item.pagePath ? ' 跳转到: ' + item.pagePath : '')
133 | callback(action)
134 | lifeCycle.notify(LifeCycleEventType.PAGE_ALIAS_ACTION, true)
135 | }
136 | }
137 | return result
138 | }
139 | }
140 | function startActionManagement(lifeCycle) {
141 | var currentAction
142 | var currentIdlePageActivitySubscription
143 |
144 | return {
145 | create: function (type, name) {
146 | if (currentAction) {
147 | // Ignore any new action if another one is already occurring.
148 | return
149 | }
150 | var pendingAutoAction = new PendingAutoAction(lifeCycle, type, name)
151 |
152 | currentAction = pendingAutoAction
153 | currentIdlePageActivitySubscription = waitIdlePageActivity(
154 | lifeCycle,
155 | function (params) {
156 | if (params.hadActivity) {
157 | pendingAutoAction.complete(params.endTime)
158 | } else {
159 | pendingAutoAction.discard()
160 | }
161 | currentAction = undefined
162 | },
163 | )
164 | },
165 | discardCurrent: function () {
166 | if (currentAction) {
167 | currentIdlePageActivitySubscription.stop()
168 | currentAction.discard()
169 | currentAction = undefined
170 | }
171 | },
172 | }
173 | }
174 | var PendingAutoAction = function (lifeCycle, type, name) {
175 | this.id = UUID()
176 | this.startClocks = now()
177 | this.name = name
178 | this.type = type
179 | this.lifeCycle = lifeCycle
180 | this.eventCountsSubscription = trackEventCounts(lifeCycle)
181 | this.lifeCycle.notify(LifeCycleEventType.AUTO_ACTION_CREATED, {
182 | id: this.id,
183 | startClocks: this.startClocks,
184 | })
185 | }
186 | PendingAutoAction.prototype = {
187 | complete: function (endTime) {
188 | var eventCounts = this.eventCountsSubscription.eventCounts
189 | this.lifeCycle.notify(LifeCycleEventType.AUTO_ACTION_COMPLETED, {
190 | counts: {
191 | errorCount: eventCounts.errorCount,
192 | longTaskCount: eventCounts.longTaskCount,
193 | resourceCount: eventCounts.resourceCount,
194 | },
195 | duration: elapsed(this.startClocks, endTime),
196 | id: this.id,
197 | name: this.name,
198 | startClocks: this.startClocks,
199 | type: this.type,
200 | })
201 | this.eventCountsSubscription.stop()
202 | },
203 | discard: function () {
204 | this.lifeCycle.notify(LifeCycleEventType.AUTO_ACTION_DISCARDED)
205 | this.eventCountsSubscription.stop()
206 | },
207 | }
208 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/app/appCollection.js:
--------------------------------------------------------------------------------
1 | import { rewriteApp } from './index'
2 | import { LifeCycleEventType } from '../../core/lifeCycle'
3 | import { RumEventType } from '../../helper/enums'
4 | import { msToNs } from '../../helper/utils'
5 | export function startAppCollection(lifeCycle, configuration) {
6 | lifeCycle.subscribe(LifeCycleEventType.APP_UPDATE, function (appinfo) {
7 | lifeCycle.notify(
8 | LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
9 | processAppUpdate(appinfo),
10 | )
11 | })
12 |
13 | return rewriteApp(configuration, lifeCycle)
14 | }
15 |
16 | function processAppUpdate(appinfo) {
17 | var appEvent = {
18 | date: appinfo.startTime,
19 | type: RumEventType.APP,
20 | app: {
21 | type: appinfo.type,
22 | name: appinfo.name,
23 | id: appinfo.id,
24 | duration: msToNs(appinfo.duration),
25 | },
26 | }
27 | console.log(appEvent, 'appEvent====')
28 | return {
29 | rawRumEvent: appEvent,
30 | startTime: appinfo.startTime,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/app/index.js:
--------------------------------------------------------------------------------
1 | import { now, areInOrder, UUID } from '../../helper/utils'
2 | import { LifeCycleEventType } from '../../core/lifeCycle'
3 |
4 | // 劫持原小程序App方法
5 | export var THROTTLE_VIEW_UPDATE_PERIOD = 3000
6 | export const startupTypes = {
7 | COLD: 'cold',
8 | HOT: 'hot',
9 | }
10 | export function rewriteApp(configuration, lifeCycle, Vue) {
11 | const originApp = App
12 | var appInfo = {
13 | isStartUp: false, // 是否启动
14 | }
15 | var startTime
16 | App = function (app) {
17 | startTime = now()
18 | // 合并方法,插入记录脚本
19 | ;['onLaunch', 'onShow', 'onHide'].forEach((methodName) => {
20 | const userDefinedMethod = app[methodName] // 暂存用户定义的方法
21 | app[methodName] = function (options) {
22 | if (methodName === 'onLaunch') {
23 | appInfo.isStartUp = true
24 | appInfo.isHide = false
25 | appInfo.startupType = startupTypes.COLD
26 | } else if (methodName === 'onShow') {
27 | if (appInfo.isStartUp && appInfo.isHide) {
28 | // 判断是热启动
29 | appInfo.startupType = startupTypes.HOT
30 | // appUpdate()
31 | }
32 | } else if (methodName === 'onHide') {
33 | lifeCycle.notify(LifeCycleEventType.APP_HIDE)
34 | appInfo.isHide = true
35 | }
36 | return userDefinedMethod && userDefinedMethod.call(this, options)
37 | }
38 | })
39 | return originApp(app)
40 | }
41 |
42 | startPerformanceObservable(lifeCycle)
43 | }
44 |
45 | function startPerformanceObservable(lifeCycle) {
46 | var subscribe = lifeCycle.subscribe(
47 | LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
48 | function (entitys) {
49 | // 过滤掉其他页面监听,只保留首次启动
50 | var codeDownloadDuration
51 | const launchEntity = entitys.find(
52 | (entity) =>
53 | entity.entryType === 'navigation' &&
54 | entity.navigationType === 'appLaunch',
55 | )
56 | if (typeof launchEntity !== 'undefined') {
57 | lifeCycle.notify(LifeCycleEventType.APP_UPDATE, {
58 | startTime: launchEntity.startTime,
59 | name: '启动',
60 | type: 'launch',
61 | id: UUID(),
62 | duration: launchEntity.duration,
63 | })
64 | }
65 | const scriptentity = entitys.find(
66 | (entity) =>
67 | entity.entryType === 'script' && entity.name === 'evaluateScript',
68 | )
69 | if (typeof scriptentity !== 'undefined') {
70 | lifeCycle.notify(LifeCycleEventType.APP_UPDATE, {
71 | startTime: scriptentity.startTime,
72 | name: '脚本注入',
73 | type: 'script_insert',
74 | id: UUID(),
75 | duration: scriptentity.duration,
76 | })
77 | }
78 | const firstEntity = entitys.find(
79 | (entity) =>
80 | entity.entryType === 'render' && entity.name === 'firstRender',
81 | )
82 | if (firstEntity && scriptentity && launchEntity) {
83 | if (
84 | !areInOrder(firstEntity.duration, launchEntity.duration) ||
85 | !areInOrder(scriptentity.duration, launchEntity.duration)
86 | ) {
87 | return
88 | }
89 | codeDownloadDuration =
90 | launchEntity.duration - firstEntity.duration - scriptentity.duration
91 | // 资源下载耗时
92 | lifeCycle.notify(LifeCycleEventType.APP_UPDATE, {
93 | startTime: launchEntity.startTime,
94 | name: '小程序包下载',
95 | type: 'package_download',
96 | id: UUID(),
97 | duration: codeDownloadDuration,
98 | })
99 | // 资源下载时间暂时定为:首次启动时间-脚本加载时间-初次渲染时间
100 | }
101 | },
102 | )
103 | return {
104 | stop: subscribe.unsubscribe,
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/assembly.js:
--------------------------------------------------------------------------------
1 | import { extend2Lev, withSnakeCaseKeys, performDraw, isEmptyObject } from '../helper/utils'
2 | import { LifeCycleEventType } from '../core/lifeCycle'
3 | import { RumEventType } from '../helper/enums'
4 | import baseInfo from '../core/baseInfo'
5 | function isTracked(configuration) {
6 | return performDraw(configuration.sampleRate)
7 | }
8 | var SessionType = {
9 | SYNTHETICS: 'synthetics',
10 | USER: 'user',
11 | }
12 | export function startRumAssembly(
13 | applicationId,
14 | configuration,
15 | lifeCycle,
16 | parentContexts,
17 | getCommonContext
18 | ) {
19 | lifeCycle.subscribe(
20 | LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
21 | function (data) {
22 | var startTime = data.startTime
23 | var rawRumEvent = data.rawRumEvent
24 | var viewContext = parentContexts.findView(startTime)
25 | var savedCommonContext = data.savedGlobalContext
26 | var customerContext = data.customerContext
27 | var deviceContext = {
28 | device: baseInfo.deviceInfo,
29 | }
30 | if (
31 | isTracked(configuration) &&
32 | (viewContext || rawRumEvent.type === RumEventType.APP)
33 | ) {
34 | var actionContext = parentContexts.findAction(startTime)
35 | var commonContext = savedCommonContext || getCommonContext()
36 | var rumContext = {
37 | _dd: {
38 | sdkName: configuration.sdkName,
39 | sdkVersion: configuration.sdkVersion,
40 | env: configuration.env,
41 | version: configuration.version,
42 | },
43 | tags: configuration.tags,
44 | application: {
45 | id: applicationId,
46 | },
47 | device: {},
48 | date: new Date().getTime(),
49 | session: {
50 | id: baseInfo.getSessionId(),
51 | type: SessionType.USER,
52 | },
53 | user: {
54 | id: configuration.user_id || baseInfo.getClientID(),
55 | is_signin: configuration.user_id ? 'T' : 'F',
56 | },
57 | }
58 |
59 | var rumEvent = extend2Lev(
60 | rumContext,
61 | deviceContext,
62 | viewContext,
63 | actionContext,
64 | rawRumEvent,
65 | )
66 |
67 | var serverRumEvent = withSnakeCaseKeys(rumEvent)
68 | var context = extend2Lev(commonContext.context, customerContext)
69 | if (!isEmptyObject(context)) {
70 | serverRumEvent.tags = context
71 | }
72 | if (!isEmptyObject(commonContext.user)) {
73 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
74 | serverRumEvent.user = extend2Lev(
75 | {
76 | id: baseInfo.getClientID(),
77 | is_signin: 'T'
78 | },
79 | commonContext.user
80 | )
81 | }
82 | lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, serverRumEvent)
83 | }
84 | },
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/error/errorCollection.js:
--------------------------------------------------------------------------------
1 | import { startAutomaticErrorCollection } from '../../core/errorCollection'
2 | import { RumEventType } from '../../helper/enums'
3 | import { LifeCycleEventType } from '../../core/lifeCycle'
4 | import {
5 | urlParse,
6 | replaceNumberCharByPath,
7 | getStatusGroup,
8 | } from '../../helper/utils'
9 | export function startErrorCollection(lifeCycle, configuration) {
10 | return doStartErrorCollection(
11 | lifeCycle,
12 | configuration,
13 | startAutomaticErrorCollection(configuration),
14 | )
15 | }
16 |
17 | export function doStartErrorCollection(lifeCycle, configuration, observable) {
18 | observable.subscribe(function (error) {
19 | lifeCycle.notify(
20 | LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
21 | processError(error),
22 | )
23 | })
24 | }
25 |
26 | function processError(error) {
27 | var resource = error.resource
28 | if (resource) {
29 | var urlObj = urlParse(error.resource.url).getParse()
30 | resource = {
31 | method: error.resource.method,
32 | status: error.resource.statusCode,
33 | statusGroup: getStatusGroup(error.resource.statusCode),
34 | url: error.resource.url,
35 | urlHost: urlObj.Host,
36 | urlPath: urlObj.Path,
37 | urlPathGroup: replaceNumberCharByPath(urlObj.Path),
38 | }
39 | }
40 | var rawRumEvent = {
41 | date: error.startTime,
42 | error: {
43 | message: error.message,
44 | resource: resource,
45 | source: error.source,
46 | stack: error.stack,
47 | type: error.type,
48 | starttime: error.startTime,
49 | },
50 | type: RumEventType.ERROR,
51 | }
52 | return {
53 | rawRumEvent: rawRumEvent,
54 | startTime: error.startTime,
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/page/index.js:
--------------------------------------------------------------------------------
1 | import { extend, now, throttle, UUID, isNumber, getActivePage } from '../../helper/utils'
2 | import { trackEventCounts } from '../trackEventCounts'
3 | import { LifeCycleEventType } from '../../core/lifeCycle'
4 | import { sdk } from '../../core/sdk'
5 |
6 | // 劫持原小程序App方法
7 | export var THROTTLE_VIEW_UPDATE_PERIOD = 3000
8 |
9 | export function rewritePage(configuration, lifeCycle, Vue) {
10 | var originVueExtend = Vue.extend
11 |
12 | Vue.extend = function (vueOptions) {
13 | // 合并方法,插入记录脚本
14 | var currentView,
15 | startTime = now()
16 | ;['onReady', 'onShow', 'onLoad', 'onUnload', 'onHide'].forEach(
17 | (methodName) => {
18 | const userDefinedMethod = vueOptions[methodName]
19 | vueOptions[methodName] = function () {
20 | if (this.mpType !== 'page') {
21 | return userDefinedMethod && userDefinedMethod.apply(this, arguments)
22 | }
23 | // 只处理page类型
24 | if (methodName === 'onShow' || methodName === 'onLoad') {
25 | if (typeof currentView === 'undefined') {
26 | const activePage = getActivePage()
27 | currentView = newView(
28 | lifeCycle,
29 | activePage && activePage.route,
30 | startTime,
31 | )
32 | }
33 | }
34 |
35 | currentView && currentView.setLoadEventEnd(methodName)
36 |
37 | if (
38 | (methodName === 'onUnload' ||
39 | methodName === 'onHide' ||
40 | methodName === 'onShow') &&
41 | currentView
42 | ) {
43 | currentView.triggerUpdate()
44 | if (methodName === 'onUnload' || methodName === 'onHide') {
45 | currentView.end()
46 | }
47 | }
48 | return userDefinedMethod && userDefinedMethod.apply(this, arguments)
49 | }
50 | },
51 | )
52 | return originVueExtend.call(this, vueOptions)
53 | }
54 | }
55 | function newView(lifeCycle, route, startTime) {
56 | if (typeof startTime === 'undefined') {
57 | startTime = now()
58 | }
59 | var id = UUID()
60 | var isActive = true
61 | var eventCounts = {
62 | errorCount: 0,
63 | resourceCount: 0,
64 | userActionCount: 0,
65 | }
66 | var setdataCount = 0
67 |
68 | var documentVersion = 0
69 | var setdataDuration = 0
70 | var loadingDuration = 0
71 | var loadingTime
72 | var showTime
73 | var onload2onshowTime
74 | var onshow2onready
75 | var stayTime
76 | var fpt, fmp
77 | lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
78 | id,
79 | startTime,
80 | route,
81 | })
82 | var scheduleViewUpdate = throttle(
83 | triggerViewUpdate,
84 | THROTTLE_VIEW_UPDATE_PERIOD,
85 | {
86 | leading: false,
87 | },
88 | )
89 | var cancelScheduleViewUpdate = scheduleViewUpdate.cancel
90 | var _trackEventCounts = trackEventCounts(
91 | lifeCycle,
92 | function (newEventCounts) {
93 | eventCounts = newEventCounts
94 | scheduleViewUpdate()
95 | },
96 | )
97 | var stopEventCountsTracking = _trackEventCounts.stop
98 | var _trackFptTime = trackFptTime(lifeCycle, function (duration) {
99 | fpt = duration
100 | scheduleViewUpdate()
101 | })
102 | var stopFptTracking = _trackFptTime.stop
103 | var _trackSetDataTime = trackSetDataTime(lifeCycle, function (duration) {
104 | if (isNumber(duration)) {
105 | setdataDuration += duration
106 | setdataCount++
107 | scheduleViewUpdate()
108 | }
109 | })
110 | var stopSetDataTracking = _trackSetDataTime.stop
111 | var _trackLoadingTime = trackLoadingTime(lifeCycle, function (duration) {
112 | if (isNumber(duration)) {
113 | loadingDuration = duration
114 | scheduleViewUpdate()
115 | }
116 | })
117 | var stopLoadingTimeTracking = _trackLoadingTime.stop
118 |
119 | var setLoadEventEnd = function (type) {
120 | if (type === 'onLoad') {
121 | loadingTime = now()
122 | loadingDuration = loadingTime - startTime
123 | } else if (type === 'onShow') {
124 | showTime = now()
125 | if (
126 | typeof onload2onshowTime === 'undefined' &&
127 | typeof loadingTime !== 'undefined'
128 | ) {
129 | onload2onshowTime = showTime - loadingTime
130 | }
131 | } else if (type === 'onReady') {
132 | if (
133 | typeof onshow2onready === 'undefined' &&
134 | typeof showTime !== 'undefined'
135 | ) {
136 | onshow2onready = now() - showTime
137 | }
138 | if (typeof fmp === 'undefined') {
139 | fmp = now() - startTime // 从开发者角度看,小程序首屏渲染完成的标志是首页 Page.onReady 事件触发。
140 | }
141 | } else if (type === 'onHide' || type === 'onUnload') {
142 | if (typeof showTime !== 'undefined') {
143 | stayTime = now() - showTime
144 | }
145 | isActive = false
146 | }
147 | triggerViewUpdate()
148 | }
149 | function triggerViewUpdate() {
150 | documentVersion += 1
151 | lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, {
152 | documentVersion: documentVersion,
153 | eventCounts: eventCounts,
154 | id: id,
155 | loadingTime: loadingDuration,
156 | stayTime,
157 | onload2onshowTime,
158 | onshow2onready,
159 | setdataDuration,
160 | setdataCount,
161 | fmp,
162 | fpt,
163 | startTime: startTime,
164 | route: route,
165 | duration: now() - startTime,
166 | isActive: isActive,
167 | })
168 | }
169 | return {
170 | scheduleUpdate: scheduleViewUpdate,
171 | setLoadEventEnd,
172 | triggerUpdate: function () {
173 | cancelScheduleViewUpdate()
174 | triggerViewUpdate()
175 | },
176 | end: function () {
177 | stopEventCountsTracking()
178 | stopFptTracking()
179 | cancelScheduleViewUpdate()
180 | stopSetDataTracking()
181 | stopLoadingTimeTracking()
182 | lifeCycle.notify(LifeCycleEventType.VIEW_ENDED, { endClocks: now() })
183 | },
184 | }
185 | }
186 | function trackFptTime(lifeCycle, callback) {
187 | var subscribe = lifeCycle.subscribe(
188 | LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
189 | function (entitys) {
190 | const firstRenderEntity = entitys.find(
191 | (entity) =>
192 | entity.entryType === 'render' && entity.name === 'firstRender',
193 | )
194 |
195 | if (typeof firstRenderEntity !== 'undefined') {
196 | callback(firstRenderEntity.duration)
197 | }
198 | },
199 | )
200 | return {
201 | stop: subscribe.unsubscribe,
202 | }
203 | }
204 | function trackLoadingTime(lifeCycle, callback) {
205 | var subscribe = lifeCycle.subscribe(
206 | LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
207 | function (entitys) {
208 | const navigationEnity = entitys.find(
209 | (entity) => entity.entryType === 'navigation',
210 | )
211 | if (typeof navigationEnity !== 'undefined') {
212 | callback(navigationEnity.duration)
213 | }
214 | },
215 | )
216 | return {
217 | stop: subscribe.unsubscribe,
218 | }
219 | }
220 | function trackSetDataTime(lifeCycle, callback) {
221 | var subscribe = lifeCycle.subscribe(
222 | LifeCycleEventType.PAGE_SET_DATA_UPDATE,
223 | function (data) {
224 | if (!data) return
225 | callback(data.updateEndTimestamp - data.pendingStartTimestamp)
226 | },
227 | )
228 | return {
229 | stop: subscribe.unsubscribe,
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/page/viewCollection.js:
--------------------------------------------------------------------------------
1 | import { rewritePage } from './index'
2 | import { RumEventType } from '../../helper/enums'
3 | import { msToNs } from '../../helper/utils'
4 | import { LifeCycleEventType } from '../../core/lifeCycle'
5 | export function startViewCollection(lifeCycle, configuration, Vue) {
6 | lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, function (view) {
7 | lifeCycle.notify(
8 | LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
9 | processViewUpdate(view),
10 | )
11 | })
12 |
13 | return rewritePage(configuration, lifeCycle, Vue)
14 | }
15 | function processViewUpdate(view) {
16 | var apdexLevel
17 | if (view.fmp) {
18 | apdexLevel = parseInt(Number(view.fmp) / 1000)
19 | apdexLevel = apdexLevel > 9 ? 9 : apdexLevel
20 | }
21 | var viewEvent = {
22 | _dd: {
23 | documentVersion: view.documentVersion,
24 | },
25 | date: view.startTime,
26 | type: RumEventType.VIEW,
27 | page: {
28 | action: {
29 | count: view.eventCounts.userActionCount,
30 | },
31 | error: {
32 | count: view.eventCounts.errorCount,
33 | },
34 | setdata: {
35 | count: view.setdataCount,
36 | },
37 | setdata_duration: msToNs(view.setdataDuration),
38 | loadingTime: msToNs(view.loadingTime),
39 | stayTime: msToNs(view.stayTime),
40 | onload2onshow: msToNs(view.onload2onshowTime),
41 | onshow2onready: msToNs(view.onshow2onready),
42 | fpt: msToNs(view.fpt),
43 | fmp: msToNs(view.fmp),
44 | isActive: view.isActive,
45 | apdexLevel,
46 | // longTask: {
47 | // count: view.eventCounts.longTaskCount
48 | // },
49 | resource: {
50 | count: view.eventCounts.resourceCount,
51 | },
52 | timeSpent: msToNs(view.duration),
53 | },
54 | }
55 | return {
56 | rawRumEvent: viewEvent,
57 | startTime: view.startTime,
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/parentContexts.js:
--------------------------------------------------------------------------------
1 | import { ONE_MINUTE, ONE_HOUR } from '../helper/enums'
2 | import { each, now } from '../helper/utils'
3 | import { LifeCycleEventType } from '../core/lifeCycle'
4 | export var VIEW_CONTEXT_TIME_OUT_DELAY = 4 * ONE_HOUR
5 | export var CLEAR_OLD_CONTEXTS_INTERVAL = ONE_MINUTE
6 |
7 | export function startParentContexts(lifeCycle) {
8 | var currentView
9 | var currentAction
10 | var previousViews = []
11 | var previousActions = []
12 | lifeCycle.subscribe(
13 | LifeCycleEventType.VIEW_CREATED,
14 | function (currentContext) {
15 | currentView = currentContext
16 | },
17 | )
18 |
19 | lifeCycle.subscribe(
20 | LifeCycleEventType.VIEW_UPDATED,
21 | function (currentContext) {
22 | // A view can be updated after its end. We have to ensure that the view being updated is the
23 | // most recently created.
24 | if (currentView && currentView.id === currentContext.id) {
25 | currentView = currentContext
26 | }
27 | },
28 | )
29 | lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, function (data) {
30 | if (currentView) {
31 | previousViews.unshift({
32 | endTime: data.endClocks,
33 | context: buildCurrentViewContext(),
34 | startTime: currentView.startTime,
35 | })
36 | currentView = undefined
37 | }
38 | })
39 | lifeCycle.subscribe(
40 | LifeCycleEventType.AUTO_ACTION_CREATED,
41 | function (currentContext) {
42 | currentAction = currentContext
43 | },
44 | )
45 |
46 | lifeCycle.subscribe(
47 | LifeCycleEventType.AUTO_ACTION_COMPLETED,
48 | function (action) {
49 | if (currentAction) {
50 | previousActions.unshift({
51 | context: buildCurrentActionContext(),
52 | endTime: currentAction.startClocks + action.duration,
53 | startTime: currentAction.startClocks,
54 | })
55 | }
56 | currentAction = undefined
57 | },
58 | )
59 |
60 | lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_DISCARDED, function () {
61 | currentAction = undefined
62 | })
63 | lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, function () {
64 | previousViews = []
65 | previousActions = []
66 | currentView = undefined
67 | currentAction = undefined
68 | })
69 | var clearOldContextsInterval = setInterval(function () {
70 | clearOldContexts(previousViews, VIEW_CONTEXT_TIME_OUT_DELAY)
71 | }, CLEAR_OLD_CONTEXTS_INTERVAL)
72 |
73 | function clearOldContexts(previousContexts, timeOutDelay) {
74 | var oldTimeThreshold = now() - timeOutDelay
75 | while (
76 | previousContexts.length > 0 &&
77 | previousContexts[previousContexts.length - 1].startTime < oldTimeThreshold
78 | ) {
79 | previousContexts.pop()
80 | }
81 | }
82 | function buildCurrentActionContext() {
83 | return { userAction: { id: currentAction.id } }
84 | }
85 | function buildCurrentViewContext() {
86 | return {
87 | page: {
88 | id: currentView.id,
89 | referer:
90 | (previousViews.length &&
91 | previousViews[previousViews.length - 1].context.page.route) ||
92 | undefined,
93 | route: currentView.route,
94 | },
95 | }
96 | }
97 |
98 | function findContext(
99 | buildContext,
100 | previousContexts,
101 | currentContext,
102 | startTime,
103 | ) {
104 | if (startTime === undefined) {
105 | return currentContext ? buildContext() : undefined
106 | }
107 | if (currentContext && startTime >= currentContext.startTime) {
108 | return buildContext()
109 | }
110 | var flag = undefined
111 | each(previousContexts, function (previousContext) {
112 | if (startTime > previousContext.endTime) {
113 | return false
114 | }
115 | if (startTime >= previousContext.startTime) {
116 | flag = previousContext.context
117 | return false
118 | }
119 | })
120 |
121 | return flag
122 | }
123 |
124 | var parentContexts = {
125 | findView: function (startTime) {
126 | return findContext(
127 | buildCurrentViewContext,
128 | previousViews,
129 | currentView,
130 | startTime,
131 | )
132 | },
133 | findAction: function (startTime) {
134 | return findContext(
135 | buildCurrentActionContext,
136 | previousActions,
137 | currentAction,
138 | startTime,
139 | )
140 | },
141 |
142 | stop: function () {
143 | clearInterval(clearOldContextsInterval)
144 | },
145 | }
146 | return parentContexts
147 | }
148 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/performanceCollection.js:
--------------------------------------------------------------------------------
1 | import { LifeCycleEventType } from '../core/lifeCycle'
2 | import { tracker } from '../core/sdk'
3 | export function startPagePerformanceObservable(lifeCycle, configuration) {
4 | if (!!tracker.getPerformance) {
5 | const performance = tracker.getPerformance()
6 | if (!performance || typeof performance.createObserver !== 'function') return
7 | const observer = performance.createObserver((entryList) => {
8 | lifeCycle.notify(
9 | LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
10 | entryList.getEntries(),
11 | )
12 | })
13 | observer.observe({ entryTypes: ['render', 'script', 'navigation'] })
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/requestCollection.js:
--------------------------------------------------------------------------------
1 | import { startXhrProxy } from '../core/xhrProxy'
2 | import { startDownloadProxy } from '../core/downloadProxy'
3 | import { LifeCycleEventType } from '../core/lifeCycle'
4 | import { isObject } from '../helper/utils'
5 | import { isAllowedRequestUrl } from '../rumEventsCollection/resource/resourceUtils'
6 | import {startTracer} from '../rumEventsCollection/tracing/tracer'
7 | var nextRequestIndex = 1
8 |
9 | export function startRequestCollection(lifeCycle, configuration) {
10 | var tracer = startTracer(configuration)
11 | trackXhr(lifeCycle, configuration,tracer)
12 | trackDownload(lifeCycle, configuration)
13 | }
14 | function parseHeader(header) {
15 | // 大小写兼容
16 | if (!isObject(header)) return header
17 | var res = {}
18 | Object.keys(header).forEach(function (key) {
19 | res[key.toLowerCase()] = header[key]
20 | })
21 | return res
22 | }
23 | function getHeaderString(header) {
24 | if (!isObject(header)) return header
25 | var headerStr = ''
26 | Object.keys(header).forEach(function (key) {
27 | headerStr += key + ':' + header[key] + ';'
28 | })
29 | return headerStr
30 | }
31 | export function trackXhr(lifeCycle, configuration,tracer) {
32 | var xhrProxy = startXhrProxy()
33 | xhrProxy.beforeSend(function (context) {
34 | if (isAllowedRequestUrl(configuration, context.url)) {
35 | tracer.traceXhr(context)
36 | context.requestIndex = getNextRequestIndex()
37 | lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, {
38 | requestIndex: context.requestIndex,
39 | })
40 | }
41 | })
42 | xhrProxy.onRequestComplete(function (context) {
43 | if (isAllowedRequestUrl(configuration, context.url)) {
44 | tracer.clearTracingIfCancelled(context)
45 | lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, {
46 | duration: context.duration,
47 | method: context.method,
48 | requestIndex: context.requestIndex,
49 | performance: context.profile,
50 | response: context.response,
51 | startTime: context.startTime,
52 | status: context.status,
53 | traceId: context.traceId,
54 | spanId: context.spanId,
55 | type: context.type,
56 | url: context.url,
57 | })
58 | }
59 | })
60 | return xhrProxy
61 | }
62 | export function trackDownload(lifeCycle, configuration) {
63 | var dwonloadProxy = startDownloadProxy()
64 | dwonloadProxy.beforeSend(function (context) {
65 | if (isAllowedRequestUrl(configuration, context.url)) {
66 | context.requestIndex = getNextRequestIndex()
67 | lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, {
68 | requestIndex: context.requestIndex,
69 | })
70 | }
71 | })
72 | dwonloadProxy.onRequestComplete(function (context) {
73 | if (isAllowedRequestUrl(configuration, context.url)) {
74 | lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, {
75 | duration: context.duration,
76 | method: context.method,
77 | requestIndex: context.requestIndex,
78 | performance: context.profile,
79 | response: context.response,
80 | startTime: context.startTime,
81 | status: context.status,
82 | type: context.type,
83 | url: context.url,
84 | })
85 | }
86 | })
87 | return dwonloadProxy
88 | }
89 | function getNextRequestIndex() {
90 | var result = nextRequestIndex
91 | nextRequestIndex += 1
92 | return result
93 | }
94 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/resource/resourceCollection.js:
--------------------------------------------------------------------------------
1 | import {
2 | computePerformanceResourceDuration,
3 | computePerformanceResourceDetails,
4 | computeSize,
5 | } from './resourceUtils'
6 | import { LifeCycleEventType } from '../../core/lifeCycle'
7 | import {
8 | msToNs,
9 | extend2Lev,
10 | urlParse,
11 | getQueryParamsFromUrl,
12 | replaceNumberCharByPath,
13 | jsonStringify,
14 | UUID,
15 | getStatusGroup,
16 | } from '../../helper/utils'
17 | import { RumEventType } from '../../helper/enums'
18 | export function startResourceCollection(lifeCycle, configuration) {
19 | lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, function (request) {
20 | lifeCycle.notify(
21 | LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
22 | processRequest(request),
23 | )
24 | })
25 | }
26 |
27 | function processRequest(request) {
28 | var type = request.type
29 | var timing = request.performance
30 | var correspondingTimingOverrides = timing
31 | ? computePerformanceEntryMetrics(timing)
32 | : undefined
33 | var tracingInfo = computeRequestTracingInfo(request)
34 | var urlObj = urlParse(request.url).getParse()
35 | var startTime = request.startTime
36 | console.log(request, 'request=========')
37 | var resourceEvent = extend2Lev(
38 | {
39 | date: startTime,
40 | resource: {
41 | type: type,
42 | duration: msToNs(request.duration),
43 | method: request.method,
44 | status: request.status,
45 | statusGroup: getStatusGroup(request.status),
46 | url: request.url,
47 | urlHost: urlObj.Host,
48 | urlPath: urlObj.Path,
49 | urlPathGroup: replaceNumberCharByPath(urlObj.Path),
50 | urlQuery: jsonStringify(getQueryParamsFromUrl(request.url)),
51 | },
52 | type: RumEventType.RESOURCE,
53 | },
54 | tracingInfo,
55 | correspondingTimingOverrides,
56 | )
57 | return { startTime: startTime, rawRumEvent: resourceEvent }
58 | }
59 | function computeRequestTracingInfo(request) {
60 | var hasBeenTraced = request.traceId && request.spanId
61 | if (!hasBeenTraced) {
62 | return undefined
63 | }
64 | return {
65 | _dd: {
66 | spanId: request.spanId,
67 | traceId: request.traceId
68 | },
69 | resource: { id: UUID() }
70 | }
71 | }
72 | function computePerformanceEntryMetrics(timing) {
73 | return {
74 | resource: extend2Lev(
75 | {},
76 | {
77 | load: computePerformanceResourceDuration(timing),
78 | size: computeSize(timing),
79 | },
80 | computePerformanceResourceDetails(timing),
81 | ),
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/resource/resourceUtils.js:
--------------------------------------------------------------------------------
1 | import { msToNs, toArray, extend } from '../../helper/utils'
2 | import { isIntakeRequest } from '../../core/configuration'
3 |
4 | function areInOrder() {
5 | var numbers = toArray(arguments)
6 | for (var i = 1; i < numbers.length; i += 1) {
7 | if (numbers[i - 1] > numbers[i]) {
8 | return false
9 | }
10 | }
11 | return true
12 | }
13 |
14 | export function computePerformanceResourceDuration(entry) {
15 | // Safari duration is always 0 on timings blocked by cross origin policies.
16 | if (entry.startTime < entry.responseEnd) {
17 | return msToNs(entry.responseEnd - entry.startTime)
18 | }
19 | }
20 |
21 | // interface PerformanceResourceDetails {
22 | // redirect?: PerformanceResourceDetailsElement
23 | // dns?: PerformanceResourceDetailsElement
24 | // connect?: PerformanceResourceDetailsElement
25 | // ssl?: PerformanceResourceDetailsElement
26 | // firstByte: PerformanceResourceDetailsElement
27 | // download: PerformanceResourceDetailsElement
28 | // fmp:
29 | // }
30 | // page_fmp float 首屏时间(用于衡量用户什么时候看到页面的主要内容),跟FCP的时长非常接近,这里我们就用FCP的时间作为首屏时间 firstPaintContentEnd - firstPaintContentStart
31 | // page_fpt float 首次渲染时间,即白屏时间(从请求开始到浏览器开始解析第一批HTML文档字节的时间差。) responseEnd - fetchStart
32 | // page_tti float 首次可交互时间(浏览器完成所有HTML解析并且完成DOM构建,此时浏览器开始加载资源。) domInteractive - fetchStart
33 | // page_firstbyte float 首包时间 responseStart - domainLookupStart
34 | // page_dom_ready float DOM Ready时间(如果页面有同步执行的JS,则同步JS执行时间=ready-tti。) domContentLoadEventEnd - fetchStart
35 | // page_load float 页面完全加载时间(load=首次渲染时间+DOM解析耗时+同步JS执行+资源加载耗时。) loadEventStart - fetchStart
36 | // page_dns float dns解析时间 domainLookupEnd - domainLookupStart
37 | // page_tcp float tcp连接时间 connectEnd - connectStart
38 | // page_ssl float ssl安全连接时间(仅适用于https) connectEnd - secureConnectionStart
39 | // page_ttfb float 请求响应耗时 responseStart - requestStart
40 | // page_trans float 内容传输时间 responseEnd - responseStart
41 | // page_dom float DOM解析耗时 domInteractive - responseEnd
42 | // page_resource_load_time float 资源加载时间 loadEventStart - domContentLoadedEventEnd
43 |
44 | // navigationStart:当前浏览器窗口的前一个网页关闭,发生unload事件时的Unix毫秒时间戳。如果没有前一个网页,则等于fetchStart属性。
45 |
46 | // · unloadEventStart:如果前一个网页与当前网页属于同一个域名,则返回前一个网页的unload事件发生时的Unix毫秒时间戳。如果没有前一个网页,或者之前的网页跳转不是在同一个域名内,则返回值为0。
47 |
48 | // · unloadEventEnd:如果前一个网页与当前网页属于同一个域名,则返回前一个网页unload事件的回调函数结束时的Unix毫秒时间戳。如果没有前一个网页,或者之前的网页跳转不是在同一个域名内,则返回值为0。
49 |
50 | // · redirectStart:返回第一个HTTP跳转开始时的Unix毫秒时间戳。如果没有跳转,或者不是同一个域名内部的跳转,则返回值为0。
51 |
52 | // · redirectEnd:返回最后一个HTTP跳转结束时(即跳转回应的最后一个字节接受完成时)的Unix毫秒时间戳。如果没有跳转,或者不是同一个域名内部的跳转,则返回值为0。
53 |
54 | // · fetchStart:返回浏览器准备使用HTTP请求读取文档时的Unix毫秒时间戳。该事件在网页查询本地缓存之前发生。
55 |
56 | // · domainLookupStart:返回域名查询开始时的Unix毫秒时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值。
57 |
58 | // · domainLookupEnd:返回域名查询结束时的Unix毫秒时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值。
59 |
60 | // · connectStart:返回HTTP请求开始向服务器发送时的Unix毫秒时间戳。如果使用持久连接(persistent connection),则返回值等同于fetchStart属性的值。
61 |
62 | // · connectEnd:返回浏览器与服务器之间的连接建立时的Unix毫秒时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。
63 |
64 | // · secureConnectionStart:返回浏览器与服务器开始安全链接的握手时的Unix毫秒时间戳。如果当前网页不要求安全连接,则返回0。
65 |
66 | // · requestStart:返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的Unix毫秒时间戳。
67 |
68 | // · responseStart:返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳。
69 |
70 | // · responseEnd:返回浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的Unix毫秒时间戳。
71 |
72 | // · domLoading:返回当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的readystatechange事件触发时)的Unix毫秒时间戳。
73 |
74 | // · domInteractive:返回当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的Unix毫秒时间戳。
75 |
76 | // · domContentLoadedEventStart:返回当前网页DOMContentLoaded事件发生时(即DOM结构解析完毕、所有脚本开始运行时)的Unix毫秒时间戳。
77 |
78 | // · domContentLoadedEventEnd:返回当前网页所有需要执行的脚本执行完成时的Unix毫秒时间戳。
79 |
80 | // · domComplete:返回当前网页DOM结构生成时(即Document.readyState属性变为“complete”,以及相应的readystatechange事件发生时)的Unix毫秒时间戳。
81 |
82 | // · loadEventStart:返回当前网页load事件的回调函数开始时的Unix毫秒时间戳。如果该事件还没有发生,返回0。
83 |
84 | // · loadEventEnd:返回当前网页load事件的回调函数运行结束时的Unix毫秒时间戳。如果该事件还没有发生,返回0
85 | export function computePerformanceResourceDetails(entry) {
86 | var validEntry = toValidEntry(entry)
87 |
88 | if (!validEntry) {
89 | return undefined
90 | }
91 |
92 | var startTime = validEntry.startTime,
93 | fetchStart = validEntry.fetchStart,
94 | redirectStart = validEntry.redirectStart,
95 | redirectEnd = validEntry.redirectEnd,
96 | domainLookupStart =
97 | validEntry.domainLookupStart || validEntry.domainLookUpStart,
98 | domainLookupEnd = validEntry.domainLookupEnd || validEntry.domainLookUpEnd,
99 | connectStart = validEntry.connectStart,
100 | SSLconnectionStart = validEntry.SSLconnectionStart,
101 | SSLconnectionEnd = validEntry.SSLconnectionEnd,
102 | connectEnd = validEntry.connectEnd,
103 | requestStart = validEntry.requestStart,
104 | responseStart = validEntry.responseStart,
105 | responseEnd = validEntry.responseEnd
106 | var details = {
107 | firstbyte: formatTiming(startTime, domainLookupStart, responseStart),
108 | trans: formatTiming(startTime, responseStart, responseEnd),
109 | ttfb: formatTiming(startTime, requestStart, responseStart),
110 | }
111 | // Make sure a connection occurred
112 | if (connectEnd !== fetchStart) {
113 | details.tcp = formatTiming(startTime, connectStart, connectEnd)
114 |
115 | // Make sure a secure connection occurred
116 | if (areInOrder(connectStart, SSLconnectionStart, SSLconnectionEnd)) {
117 | details.ssl = formatTiming(
118 | startTime,
119 | SSLconnectionStart,
120 | SSLconnectionEnd,
121 | )
122 | }
123 | }
124 |
125 | // Make sure a domain lookup occurred
126 | if (domainLookupEnd !== fetchStart) {
127 | details.dns = formatTiming(startTime, domainLookupStart, domainLookupEnd)
128 | }
129 |
130 | if (hasRedirection(entry)) {
131 | details.redirect = formatTiming(startTime, redirectStart, redirectEnd)
132 | }
133 |
134 | return details
135 | }
136 |
137 | export function toValidEntry(entry) {
138 | // Ensure timings are in the right order. On top of filtering out potential invalid
139 | // RumPerformanceResourceTiming, it will ignore entries from requests where timings cannot be
140 | // collected, for example cross origin requests without a "Timing-Allow-Origin" header allowing
141 | // it.
142 | // page_fmp float 首屏时间(用于衡量用户什么时候看到页面的主要内容),跟FCP的时长非常接近,这里我们就用FCP的时间作为首屏时间 firstPaintContentEnd - firstPaintContentStart
143 | // page_fpt float 首次渲染时间,即白屏时间(从请求开始到浏览器开始解析第一批HTML文档字节的时间差。) responseEnd - fetchStart
144 | // page_tti float 首次可交互时间(浏览器完成所有HTML解析并且完成DOM构建,此时浏览器开始加载资源。) domInteractive - fetchStart
145 | // page_firstbyte float 首包时间 responseStart - domainLookupStart
146 | // page_dom_ready float DOM Ready时间(如果页面有同步执行的JS,则同步JS执行时间=ready-tti。) domContentLoadEventEnd - fetchStart
147 | // page_load float 页面完全加载时间(load=首次渲染时间+DOM解析耗时+同步JS执行+资源加载耗时。) loadEventStart - fetchStart
148 | // page_dns float dns解析时间 domainLookupEnd - domainLookupStart
149 | // page_tcp float tcp连接时间 connectEnd - connectStart
150 | // page_ssl float ssl安全连接时间(仅适用于https) connectEnd - secureConnectionStart
151 | // page_ttfb float 请求响应耗时 responseStart - requestStart
152 | // page_trans float 内容传输时间 responseEnd - responseStart
153 | // page_dom float DOM解析耗时 domInteractive - responseEnd
154 | // page_resource_load_time float 资源加载时间 loadEventStart - domContentLoadedEventEnd
155 | if (
156 | !areInOrder(
157 | entry.startTime,
158 | entry.fetchStart,
159 | entry.domainLookupStart,
160 | entry.domainLookupEnd,
161 | entry.connectStart,
162 | entry.connectEnd,
163 | entry.requestStart,
164 | entry.responseStart,
165 | entry.responseEnd,
166 | )
167 | ) {
168 | return undefined
169 | }
170 |
171 | if (!hasRedirection(entry)) {
172 | return entry
173 | }
174 |
175 | var redirectStart = entry.redirectStart
176 | var redirectEnd = entry.redirectEnd
177 | // Firefox doesn't provide redirect timings on cross origin requests.
178 | // Provide a default for those.
179 | if (redirectStart < entry.startTime) {
180 | redirectStart = entry.startTime
181 | }
182 | if (redirectEnd < entry.startTime) {
183 | redirectEnd = entry.fetchStart
184 | }
185 |
186 | // Make sure redirect timings are in order
187 | if (
188 | !areInOrder(entry.startTime, redirectStart, redirectEnd, entry.fetchStart)
189 | ) {
190 | return undefined
191 | }
192 | return extend({}, entry, {
193 | redirectEnd: redirectEnd,
194 | redirectStart: redirectStart,
195 | })
196 | // return {
197 | // ...entry,
198 | // redirectEnd,
199 | // redirectStart
200 | // }
201 | }
202 |
203 | function hasRedirection(entry) {
204 | // The only time fetchStart is different than startTime is if a redirection occurred.
205 | return entry.fetchStart !== entry.startTime
206 | }
207 |
208 | function formatTiming(origin, start, end) {
209 | return msToNs(end - start)
210 | }
211 |
212 | export function computeSize(entry) {
213 | // Make sure a request actually occurred
214 | if (entry.startTime < entry.responseStart) {
215 | return entry.receivedBytedCount
216 | }
217 | return undefined
218 | }
219 |
220 | export function isAllowedRequestUrl(configuration, url) {
221 | return url && !isIntakeRequest(url, configuration)
222 | }
223 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/setDataCollection.js:
--------------------------------------------------------------------------------
1 | import { LifeCycleEventType } from '../core/lifeCycle'
2 | import { now } from '../helper/utils'
3 | function resetSetData(data, callback, lifeCycle, mpInstance) {
4 | var pendingStartTimestamp = now()
5 | var _callback = function () {
6 | lifeCycle.notify(LifeCycleEventType.PAGE_SET_DATA_UPDATE, {
7 | pendingStartTimestamp: pendingStartTimestamp,
8 | updateEndTimestamp: now(),
9 | })
10 | if (typeof callback === 'function') {
11 | callback.call(mpInstance)
12 | }
13 | }
14 | return _callback
15 | }
16 | export function startSetDataColloction(lifeCycle, Vue) {
17 | var originVueExtend = Vue.extend
18 |
19 | Vue.extend = function (vueOptions) {
20 | const userDefinedMethod = vueOptions['onLoad']
21 | vueOptions['onLoad'] = function () {
22 | var mpInstance = this.$scope
23 | var setData = mpInstance.setData
24 |
25 | // 重写setData
26 | if (typeof setData === 'function') {
27 | try {
28 | // 这里暂时这么处理 只读属性 会抛出错误
29 | mpInstance.setData = function (data, callback) {
30 | return setData.call(
31 | mpInstance,
32 | data,
33 | resetSetData(data, callback, lifeCycle, mpInstance),
34 | )
35 | }
36 | } catch (err) {
37 | Object.defineProperty(mpInstance.__proto__, 'setData', {
38 | configurable: false,
39 | enumerable: false,
40 | value: function (data, callback) {
41 | return setData.call(
42 | mpInstance,
43 | data,
44 | resetSetData(data, callback, lifeCycle, mpInstance),
45 | )
46 | },
47 | })
48 | }
49 | }
50 |
51 | return userDefinedMethod && userDefinedMethod.apply(this, arguments)
52 | }
53 | return originVueExtend.call(this, vueOptions)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/tracing/ddtraceTracer.js:
--------------------------------------------------------------------------------
1 | // === Generate a random 64-bit number in fixed-length hex format
2 | function randomTraceId() {
3 | const digits = '0123456789abcdef';
4 | let n = '';
5 | for (let i = 0; i < 19; i += 1) {
6 | const rand = Math.floor(Math.random() * 10);
7 | n += digits[rand];
8 | }
9 | return n;
10 | }
11 | /**
12 | *
13 | * @param {*} configuration 配置信息
14 | */
15 | export function DDtraceTracer(configuration) {
16 | this._spanId = randomTraceId()
17 | this._traceId = randomTraceId()
18 | }
19 | DDtraceTracer.prototype = {
20 | isTracingSupported: function() {
21 | return true
22 | },
23 | getSpanId:function() {
24 | return this._spanId
25 | },
26 | getTraceId: function() {
27 | return this._traceId
28 | },
29 | makeTracingHeaders: function() {
30 | return {
31 | 'x-datadog-origin': 'rum',
32 | // 'x-datadog-parent-id': spanId.toDecimalString(),
33 | 'x-datadog-sampled': '1',
34 | 'x-datadog-sampling-priority': '1',
35 | 'x-datadog-trace-id': this.getTraceId()
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/rumEventsCollection/tracing/jaegerTracer.js:
--------------------------------------------------------------------------------
1 | // === Generate a random 64-bit number in fixed-length hex format
2 | function randomTraceId() {
3 | const digits = '0123456789abcdef';
4 | let n = '';
5 | for (let i = 0; i < 16; i += 1) {
6 | const rand = Math.floor(Math.random() * 16);
7 | n += digits[rand];
8 | }
9 | return n;
10 | }
11 |
12 | /**
13 | *
14 | * @param {*} configuration 配置信息
15 | */
16 | export function JaegerTracer(configuration) {
17 | const rootSpanId = randomTraceId();
18 | // this._traceId = randomTraceId() + rootSpanId // 默认用128bit,兼容其他配置
19 | if (configuration.traceId128Bit) {
20 | // 128bit生成traceid
21 | this._traceId = randomTraceId() + rootSpanId
22 | } else {
23 | this._traceId = rootSpanId
24 | }
25 | this._spanId = rootSpanId
26 | }
27 | JaegerTracer.prototype = {
28 | isTracingSupported: function() {
29 | return true
30 | },
31 | getSpanId:function() {
32 | return this._spanId
33 | },
34 | getTraceId: function() {
35 | return this._traceId
36 | },
37 | getUberTraceId: function() {
38 | //{trace-id}:{span-id}:{parent-span-id}:{flags}
39 | return this._traceId + ':' + this._spanId + ':' + '0' + ':' + '1'
40 | },
41 | makeTracingHeaders: function() {
42 | return {
43 | 'uber-trace-id': this.getUberTraceId(),
44 | }
45 |
46 | }
47 | }
--------------------------------------------------------------------------------
/src/rumEventsCollection/tracing/skywalkingTracer.js:
--------------------------------------------------------------------------------
1 | import {base64Encode, urlParse , getActivePage} from '../../helper/utils'
2 | // start SkyWalking
3 | function uuid() {
4 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
5 | /* tslint:disable */
6 | const r = (Math.random() * 16) | 0;
7 | /* tslint:disable */
8 | const v = c === 'x' ? r : (r & 0x3) | 0x8;
9 |
10 | return v.toString(16);
11 | });
12 | }
13 |
14 | /**
15 | *
16 | * @param {*} configuration 配置信息
17 | * @param {*} requestUrl 请求的url
18 | */
19 | export function SkyWalkingTracer(configuration, requestUrl) {
20 | this._spanId = uuid()
21 | this._traceId = uuid()
22 | this._applicationId = configuration.applicationId
23 | this._env = configuration.env
24 | this._version = configuration.version
25 | this._urlParse = urlParse(requestUrl).getParse()
26 | }
27 | SkyWalkingTracer.prototype = {
28 | isTracingSupported: function() {
29 | if (this._env && this._version && this._urlParse) return true
30 | return false
31 | },
32 | getSpanId:function() {
33 | return this._spanId
34 | },
35 | getTraceId: function() {
36 | return this._traceId
37 | },
38 | getSkyWalkingSw8:function() {
39 | try {
40 | var traceIdStr = String(base64Encode(this._traceId));
41 | var segmentId = String(base64Encode(this._spanId));
42 | var service = String(base64Encode(this._applicationId + '_rum_' + this.env));
43 | var instance = String(base64Encode(this._version));
44 | var activePage = getActivePage()
45 | var endpointPage = ''
46 | if (activePage && activePage.route) {
47 | endpointPage = activePage.route
48 | }
49 | var endpoint = String(base64Encode(endpointPage));
50 | var peer = String(base64Encode(this._urlParse.Host));
51 | var index = '0'
52 | // var values = `${1}-${traceIdStr}-${segmentId}-${index}-${service}-${instance}-${endpoint}-${peer}`;
53 | return '1-' + traceIdStr + '-'+ segmentId + '-' +index + '-'+ service + '-'+ instance + '-'+ endpoint + '-'+ peer
54 | } catch(err) {
55 | return ''
56 | }
57 | },
58 | makeTracingHeaders: function() {
59 | return {
60 | 'sw8': this.getSkyWalkingSw8(),
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/rumEventsCollection/tracing/tracer.js:
--------------------------------------------------------------------------------
1 |
2 | import {each, extend, getOrigin} from '../../helper/utils'
3 | import {TraceType} from '../../helper/enums'
4 | import { DDtraceTracer} from './ddtraceTracer'
5 | import { SkyWalkingTracer} from './skywalkingTracer'
6 | import { JaegerTracer} from './jaegerTracer'
7 | import { ZipkinSingleTracer} from './zipkinSingleTracer'
8 | import { ZipkinMultiTracer} from './zipkinMultiTracer'
9 | import { W3cTraceParentTracer} from './w3cTraceParentTracer'
10 |
11 | export function clearTracingIfCancelled(context) {
12 | if (context.status === 0) {
13 | context.traceId = undefined
14 | context.spanId = undefined
15 | }
16 | }
17 |
18 | export function startTracer(configuration) {
19 | return {
20 | clearTracingIfCancelled: clearTracingIfCancelled,
21 | traceXhr: function (context) {
22 | return injectHeadersIfTracingAllowed(
23 | configuration,
24 | context,
25 | function (tracingHeaders) {
26 | context.option = extend({}, context.option)
27 | var header = {}
28 | if (context.option.header) {
29 | each(context.option.header, function (value, key) {
30 | header[key] = value
31 | })
32 | }
33 | context.option.header = extend(header, tracingHeaders)
34 | }
35 | )
36 | }
37 | }
38 | }
39 | function isAllowedUrl(configuration, requestUrl) {
40 | var requestOrigin = getOrigin(requestUrl)
41 | var flag = false
42 | each(configuration.allowedTracingOrigins, function (allowedOrigin) {
43 | if (
44 | requestOrigin === allowedOrigin ||
45 | (allowedOrigin instanceof RegExp && allowedOrigin.test(requestOrigin))
46 | ) {
47 | flag = true
48 | return false
49 | }
50 | })
51 | return flag
52 | }
53 |
54 | export function injectHeadersIfTracingAllowed(configuration, context, inject) {
55 | if (!isAllowedUrl(configuration, context.url) || !configuration.traceType) {
56 | return
57 | }
58 | var tracer;
59 | switch(configuration.traceType) {
60 | case TraceType.DDTRACE:
61 | tracer = new DDtraceTracer();
62 | break;
63 | case TraceType.SKYWALKING_V3:
64 | tracer = new SkyWalkingTracer(configuration, context.url);
65 | break;
66 | case TraceType.ZIPKIN_MULTI_HEADER:
67 | tracer = new ZipkinMultiTracer(configuration);
68 | break;
69 | case TraceType.JAEGER:
70 | tracer = new JaegerTracer(configuration);
71 | break;
72 | case TraceType.W3C_TRACEPARENT:
73 | tracer = new W3cTraceParentTracer(configuration);
74 | break;
75 | case TraceType.ZIPKIN_SINGLE_HEADER:
76 | tracer = new ZipkinSingleTracer(configuration);
77 | break;
78 | default:
79 | break;
80 | }
81 | if (!tracer || !tracer.isTracingSupported()) {
82 | return
83 | }
84 |
85 | context.traceId = tracer.getTraceId()
86 | context.spanId = tracer.getSpanId()
87 | inject(tracer.makeTracingHeaders())
88 | }
89 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/tracing/w3cTraceParentTracer.js:
--------------------------------------------------------------------------------
1 | // === Generate a random 64-bit number in fixed-length hex format
2 | function randomTraceId() {
3 | const digits = '0123456789abcdef';
4 | let n = '';
5 | for (let i = 0; i < 16; i += 1) {
6 | const rand = Math.floor(Math.random() * 16);
7 | n += digits[rand];
8 | }
9 | return n;
10 | }
11 |
12 | /**
13 | *
14 | * @param {*} configuration 配置信息
15 | */
16 | export function W3cTraceParentTracer(configuration) {
17 | const rootSpanId = randomTraceId();
18 | this._traceId = randomTraceId() + rootSpanId
19 | this._spanId = rootSpanId
20 | }
21 | W3cTraceParentTracer.prototype = {
22 | isTracingSupported: function() {
23 | return true
24 | },
25 | getSpanId:function() {
26 | return this._spanId
27 | },
28 | getTraceId: function() {
29 | return this._traceId
30 | },
31 | getTraceParent: function() {
32 | // '{version}-{traceId}-{spanId}-{sampleDecision}'
33 | return '00-' + this._traceId + '-' + this._spanId + '-01'
34 | },
35 | makeTracingHeaders: function() {
36 | return {
37 | 'traceparent': this.getTraceParent()
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/rumEventsCollection/tracing/zipkinMultiTracer.js:
--------------------------------------------------------------------------------
1 | // === Generate a random 64-bit number in fixed-length hex format
2 | function randomTraceId() {
3 | const digits = '0123456789abcdef';
4 | let n = '';
5 | for (let i = 0; i < 16; i += 1) {
6 | const rand = Math.floor(Math.random() * 16);
7 | n += digits[rand];
8 | }
9 | return n;
10 | }
11 |
12 | /**
13 | *
14 | * @param {*} configuration 配置信息
15 | */
16 | export function ZipkinMultiTracer(configuration) {
17 | const rootSpanId = randomTraceId();
18 | if (configuration.traceId128Bit) {
19 | // 128bit生成traceid
20 | this._traceId = randomTraceId() + rootSpanId
21 | } else {
22 | this._traceId = rootSpanId
23 | }
24 | this._spanId = rootSpanId
25 | }
26 | ZipkinMultiTracer.prototype = {
27 | isTracingSupported: function() {
28 | return true
29 | },
30 | getSpanId:function() {
31 | return this._spanId
32 | },
33 | getTraceId: function() {
34 | return this._traceId
35 | },
36 |
37 | makeTracingHeaders: function() {
38 | return {
39 | 'X-B3-TraceId': this.getTraceId(),
40 | 'X-B3-SpanId': this.getSpanId(),
41 | // 'X-B3-ParentSpanId': '',
42 | 'X-B3-Sampled': '1',
43 | // 'X-B3-Flags': '0'
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/rumEventsCollection/tracing/zipkinSingleTracer.js:
--------------------------------------------------------------------------------
1 | // === Generate a random 64-bit number in fixed-length hex format
2 | function randomTraceId() {
3 | const digits = '0123456789abcdef';
4 | let n = '';
5 | for (let i = 0; i < 16; i += 1) {
6 | const rand = Math.floor(Math.random() * 16);
7 | n += digits[rand];
8 | }
9 | return n;
10 | }
11 |
12 | /**
13 | *
14 | * @param {*} configuration 配置信息
15 | */
16 | export function ZipkinSingleTracer(configuration) {
17 | const rootSpanId = randomTraceId();
18 | this._traceId = randomTraceId() + rootSpanId
19 | this._spanId = rootSpanId
20 | }
21 | ZipkinSingleTracer.prototype = {
22 | isTracingSupported: function() {
23 | return true
24 | },
25 | getSpanId:function() {
26 | return this._spanId
27 | },
28 | getTraceId: function() {
29 | return this._traceId
30 | },
31 | getB3Str: function() {
32 | //{TraceId}-{SpanId}-{SamplingState}-{ParentSpanId}
33 | return this._traceId + '-' + this._spanId + '-1'
34 | },
35 | makeTracingHeaders: function() {
36 | return {
37 | 'b3': this.getB3Str()
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/rumEventsCollection/trackEventCounts.js:
--------------------------------------------------------------------------------
1 | import { noop } from '../helper/utils'
2 | import { RumEventType } from '../helper/enums'
3 | import { LifeCycleEventType } from '../core/lifeCycle'
4 |
5 | export function trackEventCounts(lifeCycle, callback) {
6 | if (typeof callback === 'undefined') {
7 | callback = noop
8 | }
9 | var eventCounts = {
10 | errorCount: 0,
11 | resourceCount: 0,
12 | longTaskCount: 0,
13 | userActionCount: 0,
14 | }
15 |
16 | var subscription = lifeCycle.subscribe(
17 | LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
18 | function (data) {
19 | var rawRumEvent = data.rawRumEvent
20 | switch (rawRumEvent.type) {
21 | case RumEventType.ERROR:
22 | eventCounts.errorCount += 1
23 | callback(eventCounts)
24 | break
25 | case RumEventType.RESOURCE:
26 | eventCounts.resourceCount += 1
27 | callback(eventCounts)
28 | break
29 | case RumEventType.ACTION:
30 | eventCounts.userActionCount += 1
31 | callback(eventCounts)
32 | break
33 | }
34 | },
35 | )
36 |
37 | return {
38 | stop: function () {
39 | subscription.unsubscribe()
40 | },
41 | eventCounts: eventCounts,
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/trackPageActiveites.js:
--------------------------------------------------------------------------------
1 | import { each, now } from '../helper/utils'
2 | import { LifeCycleEventType } from '../core/lifeCycle'
3 | import { Observable } from '../core/observable'
4 | // Delay to wait for a page activity to validate the tracking process
5 | export var PAGE_ACTIVITY_VALIDATION_DELAY = 100
6 | // Delay to wait after a page activity to end the tracking process
7 | export var PAGE_ACTIVITY_END_DELAY = 100
8 | // Maximum duration of the tracking process
9 | export var PAGE_ACTIVITY_MAX_DURATION = 10000
10 |
11 | export function waitIdlePageActivity(lifeCycle, completionCallback) {
12 | var _trackPageActivities = trackPageActivities(lifeCycle)
13 | var pageActivitiesObservable = _trackPageActivities.observable
14 | var stopPageActivitiesTracking = _trackPageActivities.stop
15 | var _waitPageActivitiesCompletion = waitPageActivitiesCompletion(
16 | pageActivitiesObservable,
17 | stopPageActivitiesTracking,
18 | completionCallback,
19 | )
20 |
21 | var stopWaitPageActivitiesCompletion = _waitPageActivitiesCompletion.stop
22 | function stop() {
23 | stopWaitPageActivitiesCompletion()
24 | stopPageActivitiesTracking()
25 | }
26 |
27 | return { stop: stop }
28 | }
29 |
30 | // Automatic action collection lifecycle overview:
31 | // (Start new trackPageActivities)
32 | // .-------------------'--------------------.
33 | // v v
34 | // [Wait for a page activity ] [Wait for a maximum duration]
35 | // [timeout: VALIDATION_DELAY] [ timeout: MAX_DURATION ]
36 | // / \ |
37 | // v v |
38 | // [No page activity] [Page activity] |
39 | // | |,----------------------. |
40 | // v v | |
41 | // (Discard) [Wait for a page activity] | |
42 | // [ timeout: END_DELAY ] | |
43 | // / \ | |
44 | // v v | |
45 | // [No page activity] [Page activity] | |
46 | // | | | |
47 | // | '------------' |
48 | // '-----------. ,--------------------'
49 | // v
50 | // (End)
51 | //
52 | // Note: because MAX_DURATION > VALIDATION_DELAY, we are sure that if the process is still alive
53 | // after MAX_DURATION, it has been validated.
54 | export function trackPageActivities(lifeCycle) {
55 | var observable = new Observable()
56 | var subscriptions = []
57 | var firstRequestIndex
58 | var pendingRequestsCount = 0
59 |
60 | subscriptions.push(
61 | lifeCycle.subscribe(LifeCycleEventType.PAGE_SET_DATA_UPDATE, function () {
62 | notifyPageActivity()
63 | }),
64 | lifeCycle.subscribe(LifeCycleEventType.PAGE_ALIAS_ACTION, function () {
65 | notifyPageActivity()
66 | }),
67 | )
68 |
69 | subscriptions.push(
70 | lifeCycle.subscribe(
71 | LifeCycleEventType.REQUEST_STARTED,
72 | function (startEvent) {
73 | if (firstRequestIndex === undefined) {
74 | firstRequestIndex = startEvent.requestIndex
75 | }
76 |
77 | pendingRequestsCount += 1
78 | notifyPageActivity()
79 | },
80 | ),
81 | )
82 |
83 | subscriptions.push(
84 | lifeCycle.subscribe(
85 | LifeCycleEventType.REQUEST_COMPLETED,
86 | function (request) {
87 | // If the request started before the tracking start, ignore it
88 | if (
89 | firstRequestIndex === undefined ||
90 | request.requestIndex < firstRequestIndex
91 | ) {
92 | return
93 | }
94 | pendingRequestsCount -= 1
95 | notifyPageActivity()
96 | },
97 | ),
98 | )
99 |
100 | function notifyPageActivity() {
101 | observable.notify({ isBusy: pendingRequestsCount > 0 })
102 | }
103 |
104 | return {
105 | observable: observable,
106 | stop: function () {
107 | each(subscriptions, function (sub) {
108 | sub.unsubscribe()
109 | })
110 | },
111 | }
112 | }
113 |
114 | export function waitPageActivitiesCompletion(
115 | pageActivitiesObservable,
116 | stopPageActivitiesTracking,
117 | completionCallback,
118 | ) {
119 | var idleTimeoutId
120 | var hasCompleted = false
121 |
122 | var validationTimeoutId = setTimeout(function () {
123 | complete({ hadActivity: false })
124 | }, PAGE_ACTIVITY_VALIDATION_DELAY)
125 | var maxDurationTimeoutId = setTimeout(function () {
126 | complete({ hadActivity: true, endTime: now() })
127 | }, PAGE_ACTIVITY_MAX_DURATION)
128 | pageActivitiesObservable.subscribe(function (data) {
129 | var isBusy = data.isBusy
130 | clearTimeout(validationTimeoutId)
131 | clearTimeout(idleTimeoutId)
132 | var lastChangeTime = now()
133 | if (!isBusy) {
134 | idleTimeoutId = setTimeout(function () {
135 | complete({ hadActivity: true, endTime: lastChangeTime })
136 | }, PAGE_ACTIVITY_END_DELAY)
137 | }
138 | })
139 |
140 | function stop() {
141 | hasCompleted = true
142 | clearTimeout(validationTimeoutId)
143 | clearTimeout(idleTimeoutId)
144 | clearTimeout(maxDurationTimeoutId)
145 | stopPageActivitiesTracking()
146 | }
147 |
148 | function complete(params) {
149 | if (hasCompleted) {
150 | return
151 | }
152 | stop()
153 | completionCallback(params)
154 | }
155 |
156 | return { stop: stop }
157 | }
158 |
--------------------------------------------------------------------------------
/src/rumEventsCollection/transport/batch.js:
--------------------------------------------------------------------------------
1 | import { LifeCycleEventType } from '../../core/lifeCycle'
2 | import { Batch, HttpRequest } from '../../core/transport'
3 | import { RumEventType } from '../../helper/enums'
4 | export function startRumBatch(configuration, lifeCycle) {
5 | var batch = makeRumBatch(configuration, lifeCycle)
6 | lifeCycle.subscribe(
7 | LifeCycleEventType.RUM_EVENT_COLLECTED,
8 | function (serverRumEvent) {
9 | if (serverRumEvent.type === RumEventType.VIEW) {
10 | batch.upsert(serverRumEvent, serverRumEvent.page.id)
11 | } else {
12 | batch.add(serverRumEvent)
13 | }
14 | },
15 | )
16 | return {
17 | stop: function () {
18 | batch.stop()
19 | },
20 | }
21 | }
22 |
23 | function makeRumBatch(configuration, lifeCycle) {
24 | var primaryBatch = createRumBatch(configuration.datakitUrl, lifeCycle)
25 |
26 | function createRumBatch(endpointUrl, lifeCycle) {
27 | return new Batch(
28 | new HttpRequest(endpointUrl, configuration.batchBytesLimit),
29 | configuration.maxBatchSize,
30 | configuration.batchBytesLimit,
31 | configuration.maxMessageSize,
32 | configuration.flushTimeout,
33 | lifeCycle,
34 | )
35 | }
36 |
37 | var stopped = false
38 | return {
39 | add: function (message) {
40 | if (stopped) {
41 | return
42 | }
43 | primaryBatch.add(message)
44 | },
45 | stop: function () {
46 | stopped = true
47 | },
48 | upsert: function (message, key) {
49 | if (stopped) {
50 | return
51 | }
52 | primaryBatch.upsert(message, key)
53 | },
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const TerserPlugin = require('terser-webpack-plugin')
3 | module.exports = (env, args) => {
4 | let baseConfig = {
5 | mode: args.mode,
6 | entry: './src/index.js',
7 | output: {
8 | filename: 'dataflux-rum-uniapp.js',
9 | path: path.resolve(__dirname, './demo/miniprogram'),
10 | library: {
11 | type: 'commonjs2',
12 | },
13 | },
14 | devtool: args.mode === 'development' ? 'inline-source-map' : 'source-map',
15 | }
16 | if (args.mode !== 'development') {
17 | baseConfig = Object.assign(baseConfig, {
18 | optimization: {
19 | minimize: true,
20 | minimizer: [
21 | new TerserPlugin({
22 | terserOptions: {
23 | compress: {
24 | drop_console: true,
25 | },
26 | },
27 | }),
28 | ],
29 | },
30 | })
31 | } else {
32 | baseConfig = Object.assign(baseConfig, {
33 | watchOptions: {
34 | ignored: /node_modules|demo/, //忽略不用监听变更的目录
35 | aggregateTimeout: 300, // 文件发生改变后多长时间后再重新编译(Add a delay before rebuilding once the first file changed )
36 | poll: 1000, //每秒询问的文件变更的次数
37 | },
38 | })
39 | }
40 | return baseConfig
41 | }
42 |
--------------------------------------------------------------------------------