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