@arms/rum-miniapp 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -9
- package/dist/miniapp-sdk.js +1 -1
- package/docs/SDK/347/211/210/346/234/254/350/257/264/346/230/216.md +71 -0
- package/docs/SDK/351/205/215/347/275/256/345/217/202/350/200/203.md +523 -0
- package/docs//346/216/245/345/205/245/345/260/217/347/250/213/345/272/217/345/272/224/347/224/250.md +272 -0
- package/es/collector/action/index.d.ts +14 -0
- package/es/collector/action/index.js +1 -0
- package/es/collector/application/index.d.ts +15 -0
- package/es/collector/application/index.js +5 -0
- package/es/collector/exception/index.d.ts +29 -0
- package/es/collector/exception/index.js +7 -0
- package/es/collector/longtask/render-block.d.ts +15 -0
- package/es/collector/longtask/render-block.js +1 -0
- package/es/collector/resource/api.d.ts +28 -0
- package/es/collector/resource/api.js +19 -0
- package/es/collector/resource/static-resource.d.ts +10 -0
- package/es/collector/resource/static-resource.js +148 -0
- package/es/collector/view/perf.d.ts +26 -0
- package/es/collector/view/perf.js +10 -0
- package/es/collector/view/pv.d.ts +12 -0
- package/es/collector/view/pv.js +2 -0
- package/es/configManager.d.ts +13 -0
- package/es/configManager.js +7 -0
- package/es/empty.js +1 -0
- package/es/index.d.ts +3 -0
- package/es/index.js +1 -0
- package/es/processor/default-processor.d.ts +5 -0
- package/es/processor/default-processor.js +1 -0
- package/es/processor/session-processor.d.ts +8 -0
- package/es/processor/session-processor.js +1 -0
- package/es/reporter/index.d.ts +10 -0
- package/es/reporter/index.js +3 -0
- package/es/shell.d.ts +42 -0
- package/es/shell.js +10 -0
- package/es/style.js +1 -0
- package/es/types/client.d.ts +39 -0
- package/es/types/client.js +1 -0
- package/es/utils/api.d.ts +43 -0
- package/es/utils/api.js +3 -0
- package/es/utils/base.d.ts +19 -0
- package/es/utils/base.js +16 -0
- package/es/utils/constants.d.ts +1 -0
- package/es/utils/constants.js +1 -0
- package/es/utils/hackApp.d.ts +3 -0
- package/es/utils/hackApp.js +2 -0
- package/es/utils/hackComponent.d.ts +3 -0
- package/es/utils/hackComponent.js +5 -0
- package/es/utils/hackPage.d.ts +3 -0
- package/es/utils/hackPage.js +2 -0
- package/es/utils/network.d.ts +1 -0
- package/es/utils/network.js +1 -0
- package/es/utils/platform.d.ts +41 -0
- package/es/utils/platform.js +6 -0
- package/es/utils/session.d.ts +26 -0
- package/es/utils/session.js +4 -0
- package/es/utils/sse.d.ts +23 -0
- package/es/utils/sse.js +14 -0
- package/es/utils/url.d.ts +29 -0
- package/es/utils/url.js +16 -0
- package/es/utils/view.d.ts +32 -0
- package/es/utils/view.js +31 -0
- package/lib/collector/action/index.js +1 -1
- package/lib/collector/application/index.js +2 -2
- package/lib/collector/exception/index.js +3 -3
- package/lib/collector/longtask/render-block.js +1 -1
- package/lib/collector/resource/api.d.ts +1 -0
- package/lib/collector/resource/api.js +13 -7
- package/lib/collector/resource/static-resource.js +10 -10
- package/lib/collector/view/perf.js +6 -5
- package/lib/collector/view/pv.js +2 -2
- package/lib/configManager.js +6 -5
- package/lib/empty.js +2 -0
- package/lib/processor/default-processor.js +1 -1
- package/lib/processor/session-processor.js +1 -1
- package/lib/reporter/index.js +2 -2
- package/lib/shell.js +6 -6
- package/lib/types/client.d.ts +21 -2
- package/lib/utils/api.d.ts +13 -0
- package/lib/utils/api.js +3 -1
- package/lib/utils/base.js +1 -1
- package/lib/utils/hackApp.js +2 -2
- package/lib/utils/hackComponent.js +2 -2
- package/lib/utils/hackPage.js +2 -2
- package/lib/utils/platform.js +1 -1
- package/lib/utils/session.js +4 -2
- package/lib/utils/sse.d.ts +23 -0
- package/lib/utils/sse.js +14 -0
- package/lib/utils/url.d.ts +15 -9
- package/lib/utils/url.js +10 -19
- package/package.json +12 -3
package/docs//346/216/245/345/205/245/345/260/217/347/250/213/345/272/217/345/272/224/347/224/250.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# 将小程序接入用户体验监控
|
|
2
|
+
|
|
3
|
+
阿里云用户体验监控支持监控微信、支付宝、百度、字节、快手、钉钉、京东等常见小程序平台,并兼容 Taro、Uniapp 等跨端框架。集成 `@arms/rum-miniapp` SDK 后,可自动采集页面 PV、性能指标(含启动与渲染性能)、JS 异常、API 请求(含 SSE 流式接口)、用户行为、页面卡顿等监控数据,并支持分布式链路追踪。
|
|
4
|
+
|
|
5
|
+
SDK 架构概览:
|
|
6
|
+
|
|
7
|
+
- **采集层**:内置多个采集器(PV / 性能 / 异常 / Console 错误 / API(含 SSE)/ 用户行为 / 卡顿监控),按配置启用
|
|
8
|
+
- **处理层**:默认事件处理 + 会话采样处理,自动注入 view / session / user 上下文
|
|
9
|
+
- **上报层**:批量缓冲 + 定时上报,支持配置上报间隔与单次上报数量
|
|
10
|
+
- **配置层**:本地配置 + 可选远程配置(`remoteConfig`),动态调整采样率与采集器开关
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 前提条件
|
|
15
|
+
|
|
16
|
+
| 项目 | 要求 |
|
|
17
|
+
|------|------|
|
|
18
|
+
| 小程序平台 | 微信 / 支付宝 / 百度 / 字节 / 快手 / 钉钉 / 京东 |
|
|
19
|
+
| 跨端框架 | Taro / Uniapp(编译目标为小程序) |
|
|
20
|
+
| 上报地址 | 已开通用户体验监控并在控制台创建应用获取 `endpoint` |
|
|
21
|
+
| SSE 流式监控 | 仅微信小程序(基础库 >= 2.20.2)支持 |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 创建应用
|
|
26
|
+
|
|
27
|
+
1. 登录 [云监控控制台](https://cmsnext.console.aliyun.com/)。
|
|
28
|
+
2. 在左侧导航栏选择 **用户体验监控** > **应用列表**,并在顶部菜单选择目标地域。
|
|
29
|
+
3. 单击 **添加应用**,在 **创建应用** 面板选择 **小程序**。
|
|
30
|
+
4. 输入应用名称与描述后单击 **创建**。应用创建成功后会自动生成 `pid` 和 `endpoint`,记录用于初始化。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 安装 SDK
|
|
35
|
+
|
|
36
|
+
小程序端仅支持 NPM 包接入方式。
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @arms/rum-miniapp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> 安装完成后,需要在对应小程序开发工具中执行**构建 npm**(如微信开发者工具中「工具 > 构建 npm」)。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 初始化
|
|
47
|
+
|
|
48
|
+
在小程序入口文件(如 `app.js` / `app.ts`)中尽早引入并初始化 SDK:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import ArmsRum from '@arms/rum-miniapp';
|
|
52
|
+
|
|
53
|
+
ArmsRum.init({
|
|
54
|
+
pid: '<your-pid>',
|
|
55
|
+
endpoint: '<your-endpoint>',
|
|
56
|
+
env: 'prod',
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> **重要** SDK 通过代理小程序原生 `request` / `httpRequest` 实现 API 采集,`init()` 务必在业务发起任何网络请求**之前**调用。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 设置安全域名
|
|
66
|
+
|
|
67
|
+
将应用对应的 `endpoint` 地址添加到小程序服务器域名白名单中:
|
|
68
|
+
|
|
69
|
+
- 微信:「小程序管理后台 > 开发管理 > 开发设置 > 服务器域名 > request 合法域名」
|
|
70
|
+
- 支付宝:「小程序管理中心 > 开发设置 > 服务器域名白名单」
|
|
71
|
+
- 其他平台类似操作
|
|
72
|
+
|
|
73
|
+
Endpoint 地址示例:`https://aokcd*****-default-cn.rum.aliyuncs.com`
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 验证接入
|
|
78
|
+
|
|
79
|
+
启动小程序后做一些操作(浏览页面、发起请求、触发异常等),可通过 `beforeReport` 在控制台查看待上报数据:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
ArmsRum.init({
|
|
83
|
+
pid: '<your-pid>',
|
|
84
|
+
endpoint: '<your-endpoint>',
|
|
85
|
+
beforeReport(bundle) {
|
|
86
|
+
console.log('[RUM]', bundle);
|
|
87
|
+
return bundle;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
约 1–2 分钟后,可在阿里云云监控控制台 **用户体验监控** > **应用列表** 中确认数据上报情况。
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 高级用法
|
|
97
|
+
|
|
98
|
+
### 标识用户与应用维度
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
ArmsRum.init({
|
|
102
|
+
pid: '<your-pid>',
|
|
103
|
+
endpoint: '<your-endpoint>',
|
|
104
|
+
env: 'prod',
|
|
105
|
+
version: '1.0.0',
|
|
106
|
+
user: {
|
|
107
|
+
name: '张三',
|
|
108
|
+
tags: 'vip,enterprise',
|
|
109
|
+
},
|
|
110
|
+
properties: {
|
|
111
|
+
department: 'engineering',
|
|
112
|
+
region: 'cn-hangzhou',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`user` 用于按用户维度排查问题;`properties` 是全局自定义属性,会附加到所有上报事件,便于按业务标签聚合。
|
|
118
|
+
|
|
119
|
+
> **说明** 如需关联业务自有账号体系,建议使用 `user.name` 或 `user.tags`,不建议覆盖 `user.id`(会影响 UV 计算)。
|
|
120
|
+
|
|
121
|
+
### 自定义页面名 / 资源名
|
|
122
|
+
|
|
123
|
+
默认页面名取小程序页面路由 path。需要按业务语义归类时通过 `parseViewName` / `parseResourceName` 覆写:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
ArmsRum.init({
|
|
127
|
+
pid: '<your-pid>',
|
|
128
|
+
endpoint: '<your-endpoint>',
|
|
129
|
+
parseViewName: (url) => {
|
|
130
|
+
// pages/detail/index?id=123 → 'detail'
|
|
131
|
+
return url.split('/')[1] || url;
|
|
132
|
+
},
|
|
133
|
+
parseResourceName: (url) => {
|
|
134
|
+
return new URL(url).pathname.replace(/\/\d+(\/|$)/g, '/:id$1');
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`parseResourceName` 把数字 ID、UUID 这类高基数路径归并成模板,避免资源聚合噪声。
|
|
140
|
+
|
|
141
|
+
### 启用分布式链路追踪
|
|
142
|
+
|
|
143
|
+
通过 `tracing` 启用后,SDK 在 outbound 请求上自动注入追踪头,与后端服务关联:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
ArmsRum.init({
|
|
147
|
+
pid: '<your-pid>',
|
|
148
|
+
endpoint: '<your-endpoint>',
|
|
149
|
+
tracing: {
|
|
150
|
+
enable: true,
|
|
151
|
+
sample: 10, // 10% 采样
|
|
152
|
+
propagatorTypes: ['tracecontext', 'b3'],
|
|
153
|
+
allowedUrls: [
|
|
154
|
+
'https://api.example.com',
|
|
155
|
+
/\/api\/v\d+\//,
|
|
156
|
+
{ match: 'https://payment.example.com', sampling: 100 },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`propagatorTypes` 支持 `tracecontext` / `b3` / `b3multi` / `jaeger` / `sw8`。**`allowedUrls` 未配置时不会向任何 URL 注入追踪头**。
|
|
163
|
+
|
|
164
|
+
### 自定义 API 事件 payload
|
|
165
|
+
|
|
166
|
+
需要把请求/响应内容写入事件用于排查时,通过 `evaluateApi` 回调按需提取。SDK 自动裁剪至 5KB:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
ArmsRum.init({
|
|
170
|
+
pid: '<your-pid>',
|
|
171
|
+
endpoint: '<your-endpoint>',
|
|
172
|
+
evaluateApi: async (options, response, error) => {
|
|
173
|
+
const respText = JSON.stringify(response);
|
|
174
|
+
return {
|
|
175
|
+
success: error ? 0 : 1,
|
|
176
|
+
snapshots: JSON.stringify({
|
|
177
|
+
params: options.data,
|
|
178
|
+
response: respText.substring(0, 2000),
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
> **字段约定** `snapshots` 留给 SDK / `evaluateApi` 写采集元数据,`properties` 保留给用户业务标签。回调如抛错会被静默捕获,事件回退到原始字段。
|
|
186
|
+
|
|
187
|
+
### 关闭 / 精细化采集器
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
ArmsRum.init({
|
|
191
|
+
pid: '<your-pid>',
|
|
192
|
+
endpoint: '<your-endpoint>',
|
|
193
|
+
collectors: {
|
|
194
|
+
action: false, // 关闭用户行为采集
|
|
195
|
+
longTask: false, // 关闭卡顿监控
|
|
196
|
+
api: {
|
|
197
|
+
enable: true,
|
|
198
|
+
filters: [/\.internal\.example\.com/],
|
|
199
|
+
sse: { timeout: 30000 }, // SSE 流 30s 无消息判超时
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
完整字段表与 API 见 [`SDK配置参考.md`](./SDK配置参考.md)。
|
|
206
|
+
|
|
207
|
+
### 启用远程配置
|
|
208
|
+
|
|
209
|
+
通过控制台远程下发配置,可在不发版的情况下调整采样率、关闭采集器:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
ArmsRum.init({
|
|
213
|
+
pid: '<your-pid>',
|
|
214
|
+
endpoint: '<your-endpoint>',
|
|
215
|
+
remoteConfig: {
|
|
216
|
+
enable: true,
|
|
217
|
+
mode: 'launch-first',
|
|
218
|
+
cacheTimeout: 3600000,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
> 需要 SDK 版本 >= 0.0.37。`launch-first` 模式不阻塞启动;远端配置异步生效并缓存到本地存储。
|
|
224
|
+
|
|
225
|
+
### SSE 流式接口监控(微信小程序)
|
|
226
|
+
|
|
227
|
+
微信小程序(基础库 >= 2.20.2)支持 SSE 流式监控。SDK 自动识别 `Accept: text/event-stream` 请求并启用 `enableChunked` 模式:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
ArmsRum.init({
|
|
231
|
+
pid: '<your-pid>',
|
|
232
|
+
endpoint: '<your-endpoint>',
|
|
233
|
+
collectors: {
|
|
234
|
+
api: {
|
|
235
|
+
sse: {
|
|
236
|
+
enabled: true, // 默认启用
|
|
237
|
+
timeout: 60000, // 60s 无消息判超时
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
SSE 监控输出以下指标:
|
|
245
|
+
|
|
246
|
+
| 指标 | 说明 |
|
|
247
|
+
|------|------|
|
|
248
|
+
| `time_to_first_token` | 首 Token 延迟(从请求发起到首条 SSE 消息) |
|
|
249
|
+
| `inter_token_latency_avg` | Token 间平均延迟 |
|
|
250
|
+
| `inter_token_latency_max` | Token 间最大延迟 |
|
|
251
|
+
| `message_count` | SSE 消息事件总数 |
|
|
252
|
+
|
|
253
|
+
> 非微信平台或不支持 `enableChunked` 时自动静默降级为普通 API 监控,不抛异常、不打日志。
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 注意事项
|
|
258
|
+
|
|
259
|
+
1. **`init()` 务必尽早执行**:SDK 通过代理 `request` / `httpRequest` 实现 API 采集,初始化之前发出的请求采不到。
|
|
260
|
+
2. **构建 npm**:安装 SDK 后需在小程序开发工具中执行构建 npm,否则运行时找不到包。
|
|
261
|
+
3. **安全域名必须配置**:未添加 `endpoint` 到域名白名单会导致上报请求被拦截。
|
|
262
|
+
4. **`evaluateApi` 内禁止同步阻塞**:回调里处理大数据请限制大小,否则会拖慢上报队列。
|
|
263
|
+
5. **`tracing.allowedUrls` 显式声明**:未配置时不会注入追踪头,避免向不可控第三方域名泄漏内部 traceId。
|
|
264
|
+
6. **卡顿监控依赖 `setUpdatePerformanceListener`**:部分低版本基础库不支持,SDK 会自动降级跳过。
|
|
265
|
+
7. **SSE 监控平台限制**:仅微信小程序(基础库 >= 2.20.2)支持,其他平台静默降级。
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 相关文档
|
|
270
|
+
|
|
271
|
+
- [`SDK配置参考.md`](./SDK配置参考.md) — 完整配置 + API 参考
|
|
272
|
+
- [`SDK版本说明.md`](./SDK版本说明.md) — 版本变更历史
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ICollector, IContext, RumActionEvent, RumEvent } from '@arms/rum-core';
|
|
2
|
+
export default class ActionCollector implements ICollector {
|
|
3
|
+
name: string;
|
|
4
|
+
private DELAY_TIME;
|
|
5
|
+
ctx: IContext;
|
|
6
|
+
sendEvent: (payload: RumEvent) => void;
|
|
7
|
+
events: string[];
|
|
8
|
+
lastEvent: RumActionEvent;
|
|
9
|
+
timer: any;
|
|
10
|
+
setup(ctx: IContext, sendEvent: (payload: RumEvent) => void): void;
|
|
11
|
+
onEvent: (e: any) => void;
|
|
12
|
+
sendAction: () => void;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RumEventType}from"@arms/rum-core";import{addPageListener,removePageListener}from"../../utils/hackPage";import{checkEnable}from"../../configManager";export default class ActionCollector{constructor(){var a=this;this.name="action-collector",this.DELAY_TIME=300,this.ctx=void 0,this.sendEvent=void 0,this.events=["tap"],this.lastEvent=void 0,this.timer=void 0,this.onEvent=function(b){if(b&&a.events.includes(b.type)&&b.target){var c=b.target,d=b.currentTarget,e=b.type,f=d.dataset,g=d.id;if(g||d===c||(g=c.id,f=c.dataset),!g)return;var h=a.ctx.session.getBaseEvent(),i={...h,event_type:RumEventType.ACTION,type:e,name:g,snapshots:JSON.stringify({dataset:f}).substring(0,1e3)};clearTimeout(a.timer),a.lastEvent?a.lastEvent.name===i.name?a.lastEvent.times+=1:a.sendAction():a.lastEvent=i,a.timer=setTimeout(a.sendAction,a.DELAY_TIME)}},this.sendAction=function(){a.sendEvent(a.lastEvent),a.lastEvent=null}}setup(a,b){checkEnable(a,"action")&&(this.ctx=a,this.sendEvent=b,addPageListener("*",this.onEvent))}destroy(){removePageListener("*",this.onEvent)}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ICollector, IContext, RumEvent } from '@arms/rum-core';
|
|
2
|
+
/**
|
|
3
|
+
* 参考资料:
|
|
4
|
+
* - https://developers.weixin.qq.com/miniprogram/dev/api/base/performance/PerformanceEntry.html
|
|
5
|
+
* - https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/foundation/performance/tt-performance/performance-entry
|
|
6
|
+
*/
|
|
7
|
+
export default class ApplicationCollector implements ICollector {
|
|
8
|
+
name: string;
|
|
9
|
+
ctx: IContext;
|
|
10
|
+
sendEvent: (payload: RumEvent) => void;
|
|
11
|
+
perfObserver: any;
|
|
12
|
+
setup(ctx: IContext, sendEvent: (payload: RumEvent) => void): void;
|
|
13
|
+
observerHandler: (ev: any) => void;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{isArray,isFunction,RumEventType}from"@arms/rum-core";import sdk,{alipayName,appName,bytedanceName,getPerformance,ksName,wechatName}from"../../utils/platform";import{checkEnable}from"../../configManager";/**
|
|
2
|
+
* 参考资料:
|
|
3
|
+
* - https://developers.weixin.qq.com/miniprogram/dev/api/base/performance/PerformanceEntry.html
|
|
4
|
+
* - https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/foundation/performance/tt-performance/performance-entry
|
|
5
|
+
*/export default class ApplicationCollector{constructor(){var a=this;this.name="application-collector",this.ctx=void 0,this.sendEvent=void 0,this.perfObserver=void 0,this.observerHandler=function(b){var c,d=isArray(b)?b:b.getEntries();if(isFunction(sdk.getLaunchOptionsSync)){var e;c=null===(e=sdk.getLaunchOptionsSync())||void 0===e?void 0:e.scene}if(Array.isArray(d)&&0<d.length)for(var f,g=0;g<d.length;g++)if(f=d[g],"number"==typeof f.duration&&0<f.duration){var h=a.ctx.session.getBaseEvent(),j={...h,event_type:RumEventType.APPLICATION,state:"",duration:0,snapshots:JSON.stringify(f),scene:c};switch(f.name){case"appLaunch":case"app-click":j.state="cold_lunch",j.duration=f.duration;break;case"downloadPackage":case"miniprogram-package":case"download":j.state="download_package",j.duration=f.duration;break;case"evaluateScript":case"app-service":var k=f.moduleName||f.packageName;"__APP__"===k&&(j.state="evaluate_script",j.duration=f.duration);break;default:}j.state&&0<j.duration&&6e5>j.duration&&a.sendEvent(j)}}}setup(a,b){if(checkEnable(a,"application")){this.ctx=a,this.sendEvent=b;var c=getPerformance();if(c&&isFunction(c.createObserver)){var d=[];if(appName===wechatName||appName===alipayName)d=["navigation","script","loadPackage"];else if(appName===bytedanceName)d=["launch","evaluate","resource"];else if(appName===ksName)d=["navigation","resource"];else return;this.observerHandler(c.getEntries().filter(function(a){return d.includes(a.entryType)})),this.perfObserver=c.createObserver(this.observerHandler),this.perfObserver.observe({entryTypes:d})}}}destroy(){this.perfObserver&&this.perfObserver.disconnect()}}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ICollector, IContext, RumEvent } from '@arms/rum-core';
|
|
2
|
+
interface MiniError extends Error {
|
|
3
|
+
reason?: Error;
|
|
4
|
+
}
|
|
5
|
+
export default class ExceptionCollector implements ICollector {
|
|
6
|
+
name: string;
|
|
7
|
+
ctx: IContext;
|
|
8
|
+
sendEvent: (payload: RumEvent) => void;
|
|
9
|
+
private origin;
|
|
10
|
+
private events;
|
|
11
|
+
setup(ctx: IContext, sendEvent: (payload: RumEvent) => void): void;
|
|
12
|
+
hackOrigin(): void;
|
|
13
|
+
onUnhandled: (event: MiniError) => void;
|
|
14
|
+
onError: (e: Error | string, stack?: string) => void;
|
|
15
|
+
errorHandle: (error: any, source?: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* 根据stack获取Error信息
|
|
18
|
+
* TODO: 增加基于Stack解析filename和lineNumber的能力
|
|
19
|
+
* @param stack
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
getErrorByStack(stack?: string): {
|
|
23
|
+
name: string;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
errorFilter(err: Error): boolean;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{isArray,isFunction,isString,interceptFunction,matchList,RumEventType}from"@arms/rum-core";import{addAppListener,removeAppListener}from"../../utils/hackApp";import{sdk,appName}from"../../utils/platform";import{checkEnable}from"../../configManager";export default class ExceptionCollector{constructor(){var a=this;this.name="exception-collector",this.ctx=void 0,this.sendEvent=void 0,this.origin={consoleError:console.error},this.events={onError:null,onUnhandledRejection:null,onPageNotFound:null,onLazyLoadError:null},this.onUnhandled=function(b){b&&b.reason&&a.errorHandle(b.reason,"onUnhandledRejection")},this.onError=function(b,c){var d=b;isString(d)&&isString(c)&&(d=c),a.errorHandle(d,"onError")},this.errorHandle=function(b,c=""){var d,e,f,g,h,i;if(b instanceof Object)d=b.name,e=b.message,f=b.filename||b.fileName,g=b.stack,h=b.lineno||b.lineNumber,i=b.colno||b.columnNumber;else if("string"==typeof b){g=b;var j=a.getErrorByStack(b);j&&(d=j.name,e=j.message)}if(d&&e&&!a.errorFilter(b)){var k=a.ctx.session.getBaseEvent();a.sendEvent({...k,event_type:RumEventType.EXCEPTION,source:c,type:"error",name:d,message:e,file:f,stack:g,line:h,column:i})}}}setup(a,b){this.ctx=a,this.sendEvent=b,this.hackOrigin()}hackOrigin(){var a=this,b=this;if(checkEnable(this.ctx,"jsError")){// 由于 uniapp 对组件的异常进行了劫持,无法通过全局监听异常,需要排除
|
|
2
|
+
var c=isFunction(sdk.onError),d=isFunction(sdk.onUnhandledRejection);(c||d)&&"undefined"==typeof uni?(c&&sdk.onError(this.onError),d&&sdk.onUnhandledRejection(this.onUnhandled)):Object.keys(this.events).forEach(function(c){a.events[c]=function(a){a.reason&&a.reason instanceof Error&&(a=a.reason),b.errorHandle(a,c)},addAppListener(c,a.events[c])})}checkEnable(this.ctx,"consoleError")&&("swan"===appName&&Object.defineProperty(console,"error",{writable:!0}),interceptFunction(console,"error",function(a){b.errorHandle(a,"console.error")}))}/**
|
|
3
|
+
* 根据stack获取Error信息
|
|
4
|
+
* TODO: 增加基于Stack解析filename和lineNumber的能力
|
|
5
|
+
* @param stack
|
|
6
|
+
* @returns
|
|
7
|
+
*/getErrorByStack(a=""){var b=a.split("\n");if(!(2>b.length))for(var c=1;c<b.length;c++)if(b[c].trim().startsWith("at ")){var d=b[c-1],e=d.indexOf(": ");return{name:d.substring(0,e),message:d.substring(e+2)}}}errorFilter(a){var b=this.ctx.getConfig().filters||{},c=b.exception;return!!c&&(isArray(c)||(c=[c]),matchList(c,a.name,!0,a)||matchList(c,a.message,!0,a)||matchList(c,a.stack,!0,a))}destroy(){var a=this,b=this.origin;b.consoleError&&(console.error=b.consoleError),isFunction(sdk.offError)&&sdk.offError(this.onError),Object.keys(this.events).forEach(function(b){isFunction(a.events[b])&&removeAppListener(b,a.events[b])})}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ICollector, IContext, RumEvent } from '@arms/rum-core';
|
|
2
|
+
export default class RenderBlockCollector implements ICollector {
|
|
3
|
+
name: string;
|
|
4
|
+
ctx: IContext;
|
|
5
|
+
sendEvent: (payload: RumEvent) => void;
|
|
6
|
+
private _onSetData;
|
|
7
|
+
private maxEventCount;
|
|
8
|
+
private renderThreshold;
|
|
9
|
+
private renderBlockReportLimitMap;
|
|
10
|
+
setup(ctx: IContext, sendEvent: (payload: RumEvent) => void): void;
|
|
11
|
+
onLoad: () => void;
|
|
12
|
+
onSetData: (instance: any) => void;
|
|
13
|
+
onUnload: () => void;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function _createForOfIteratorHelperLoose(a,b){var c="undefined"!=typeof Symbol&&a[Symbol.iterator]||a["@@iterator"];if(c)return(c=c.call(a)).next.bind(c);if(Array.isArray(a)||(c=_unsupportedIterableToArray(a))||b&&a&&"number"==typeof a.length){c&&(a=c);var d=0;return function(){return d>=a.length?{done:!0}:{done:!1,value:a[d++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(b,c){if(b){if("string"==typeof b)return _arrayLikeToArray(b,c);var a={}.toString.call(b).slice(8,-1);return"Object"===a&&b.constructor&&(a=b.constructor.name),"Map"===a||"Set"===a?Array.from(b):"Arguments"===a||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?_arrayLikeToArray(b,c):void 0}}function _arrayLikeToArray(b,c){(null==c||c>b.length)&&(c=b.length);for(var d=0,f=Array(c);d<c;d++)f[d]=b[d];return f}import{isFunction,isNumber,RumEventType}from"@arms/rum-core";import{addPageListener,removePageListener}from"../../utils/hackPage";import{addComponentListener,removeComponentListener}from"../../utils/hackComponent";import{getCurPage,getRumViewId,getViewById}from"../../utils/view";import{getCurrentTime}from"../../utils/base";import{checkEnable}from"../../configManager";var defaultValues={maxEventCount:5,renderThreshold:50};export default class RenderBlockCollector{constructor(){var a=this;this.name="render-block-collector",this.ctx=void 0,this.sendEvent=void 0,this._onSetData=void 0,this.maxEventCount=void 0,this.renderThreshold=void 0,this.renderBlockReportLimitMap=new Map,this.onLoad=function(){var b,c=getCurPage(),d=getRumViewId(c,a.ctx.session),e=null===(b=getViewById(d,a.ctx))||void 0===b?void 0:b.name,f=a.renderBlockReportLimitMap;d&&e&&!f.has(d)&&f.set(d,{count:0,name:e})},this.onSetData=function(b){return b&&isFunction(b.setUpdatePerformanceListener)?void b.setUpdatePerformanceListener({withDataPaths:!0},function(c){var d=c||{},e=d.updateEndTimestamp,f=d.pendingStartTimestamp,g=d.dataPaths,h=void 0===g?[]:g,i=e-f;if(isNumber(i)&&!(i<a.renderThreshold)){var j=getCurPage(),k=getRumViewId(j,a.ctx.session);if(a.renderBlockReportLimitMap.has(k)){var l=a.renderBlockReportLimitMap.get(k),m=l.count,n=l.name;if(!(m>a.maxEventCount)){a.renderBlockReportLimitMap.set(k,{count:m+1,name:n});var o=a.ctx.session.getBaseEvent(),p=b.data,q=formatDataPath(h,p);a.sendEvent({...o,event_type:RumEventType.LONG_TASK,type:"render",duration:i,message:b.is||b.route||"",snapshots:JSON.stringify({...formatSetDataOriginalPefData(c),...(q?{data:q}:{})}),timestamp:f||getCurrentTime(),source:"setData",view:{id:k,name:n}})}}}}):void a.destroy()},this.onUnload=function(){var b=getCurPage(),c=getRumViewId(b,a.ctx.session);a.renderBlockReportLimitMap["delete"](c)}}setup(a,b){var c=a.getConfig(),d=c.longTaskConfig,e=void 0===d?{}:d;if(checkEnable(a,"longTask")){var f=this;this.ctx=a,this.sendEvent=b;var g=fixRenderBlockConfig(e),h=g.maxEventCount,i=g.renderThreshold;this.maxEventCount=h,this.renderThreshold=i,this._onSetData=function(){f.onSetData(this)},addPageListener("onLoad",this.onLoad),addPageListener("onReady",this._onSetData,!0),addPageListener("onUnload",this.onUnload),addComponentListener("attached",this._onSetData)}}destroy(){removePageListener("onLoad",this.onLoad),removePageListener("onReady",this._onSetData,!0),removePageListener("onUnload",this.onUnload),removeComponentListener("attached",this._onSetData)}}var fixRenderBlockConfig=function(a){var b=a||{},c=b.maxEventCount,d=b.renderThreshold;return(!c||!isNumber(c)||1>c||c>defaultValues.renderThreshold)&&(a.maxEventCount=defaultValues.maxEventCount),d&&isNumber(d)&&!(d<defaultValues.renderThreshold)||(a.renderThreshold=defaultValues.renderThreshold),a},formatDataPath=function(a,b){try{return a.map(function(a){for(var c,d="",e=b,f=_createForOfIteratorHelperLoose(a);!(c=f()).done;){var g,h=c.value;d+=d?"."+h:h,e=null===(g=e)||void 0===g?void 0:g[h]}return{dataKey:d,dataLength:JSON.stringify(e).length}})}catch(a){return""}},formatSetDataOriginalPefData=function(a){if(!a)return a;var b=a.updateStartTimestamp,c=a.updateEndTimestamp,d=a.pendingStartTimestamp;return a.updateEndTimestamp=c-d,a.updateStartTimestamp=b-d,a.pendingStartTimestamp=0,a};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ICollector, IContext, ITracingHeaders, RumEvent } from '@arms/rum-core';
|
|
2
|
+
import { IApiAttr, IMeasure, Options } from "../../utils/api";
|
|
3
|
+
export default class ApiCollector implements ICollector {
|
|
4
|
+
name: string;
|
|
5
|
+
ctx: IContext;
|
|
6
|
+
sendEvent: (payload: RumEvent) => void;
|
|
7
|
+
origin: {
|
|
8
|
+
request: (options: any) => void;
|
|
9
|
+
httpRequest: (options: any) => void;
|
|
10
|
+
};
|
|
11
|
+
setup(ctx: IContext, sendEvent: (payload: RumEvent) => void): void;
|
|
12
|
+
injectTracing(apiAttr: IApiAttr, inject: (tracingHeaders: ITracingHeaders) => void): void;
|
|
13
|
+
hackRequest(key: any): void;
|
|
14
|
+
rebuildRequestOptions(options: Options): Options;
|
|
15
|
+
sendApi: (apiAttr: any, resp: any, options: any) => Promise<void>;
|
|
16
|
+
private getSseConfig;
|
|
17
|
+
private getResourceName;
|
|
18
|
+
/**
|
|
19
|
+
* 解析小程序的性能数据
|
|
20
|
+
* 参考:https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html
|
|
21
|
+
* https://opendocs.alipay.com/mini/api/owycmh?pathHash=c91640f8
|
|
22
|
+
* TODO:抖音 https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/network/http/tt-request/#Profile-%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E
|
|
23
|
+
* @param profile
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
parseProfile(profile: IMeasure): IMeasure;
|
|
27
|
+
private filter;
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import{find,generateGUID,generateSpanId,generateTraceId,isFunction,makeTracingHeaders,matchList,parseTracingOptions,performDraw,RumEventType,urlMatch,ONE_HOUR,isObject,isArray}from"@arms/rum-core";import sdk,{appName,HEADER_KEY}from"../../utils/platform";import{getPathByURL,isEndpoint}from"../../utils/url";import{getCurrentTime}from"../../utils/base";import{getCurView}from"../../utils/view";import{reviseApiAttr}from"../../utils/api";import{checkEnable}from"../../configManager";import{SseMetricsTracker,arrayBufferToString,parseSseChunk,isSseRequest,isSseSupported}from"../../utils/sse";export default class ApiCollector{constructor(){var a=this;this.name="api-collector",this.ctx=void 0,this.sendEvent=void 0,this.origin={request:sdk.request,httpRequest:sdk.httpRequest},this.sendApi=async function(b,c,d){var e,f,g=a.ctx.getConfig(),h=g.evaluateApi,i=c.statusCode,j=c.status,k=c.errMsg,l=c.errorMessage,m=c.message,n=c.profile;if(n&&(e=a.parseProfile(n),f=JSON.stringify(n)),isFunction(h))try{var o;0===b.success&&(o=new Error("request error"));var p=await h(d,c,o);b={...b,...reviseApiAttr(p)}}catch(a){//
|
|
2
|
+
}var q=a.ctx.session.getBaseEvent(),r={...q,event_type:RumEventType.RESOURCE,type:"api",status_code:j||i,message:l||k||m,duration:getCurrentTime()-b.timestamp,...b,...e,timing_data:f};// 对超大的耗时进行过滤
|
|
3
|
+
r.duration>ONE_HOUR||a.sendEvent(r)}}setup(a,b){var c=this;checkEnable(a,"api")&&(this.ctx=a,this.sendEvent=b,Object.keys(this.origin).forEach(function(a){return c.hackRequest(a)}))}injectTracing(a,b){var c,d=this.ctx.session,e=this.ctx.getConfig(),f=e.tracing,g=e.pid,h=e.version,i=void 0===h?"1.0.0":h,j=parseTracingOptions(f),k=j.enable,l=j.sample,m=j.propagatorTypes,n=j.allowedUrls,o=j.tracestate,p=j.baggage;if(k){var q=find(n,function(b){return matchList([b.match],a.url)});if(q){var r=q.propagatorTypes;0===r.length&&(r=m);// skywalking
|
|
4
|
+
var s=!1;r.includes("sw8")&&(r=["sw8"],s=!0);var t=s?generateGUID():generateTraceId(),u=s?generateGUID():generateSpanId(),v=performDraw(l),w=d.getUserId(),x=d.getSessionId(),y=!(void 0!==o)||o?"rum=v2&miniapp&"+g+"&"+x+"&"+w:void 0,z=void 0!==p&&p?"rum=v2,appType=miniapp,pid="+g+",sid="+x+",uid="+w:void 0,A=makeTracingHeaders(t,u,v,r,{tracestate:y,baggage:z,appId:g,appVersion:i,viewName:null===(c=a.view)||void 0===c?void 0:c.name,host:"miniapp_"+appName});v&&(a.trace_id=t,a.trace_data=JSON.stringify({spanId:u,sample:l,sampled:v,headers:A})),b(A)}}}hackRequest(a){var b=this;isFunction(sdk[a])&&Object.defineProperty(sdk,a,{configurable:!0,enumerable:!0,writable:!0,value:function(...c){c[0]=b.rebuildRequestOptions(c[0]);var d=c[0].__onRequestTask;d&&delete c[0].__onRequestTask;var e=b.origin[a].apply(this,c);return d&&e&&d(e),e}})}rebuildRequestOptions(a){var b=this,c=a.url,d=a.success,e=a.fail,f=getCurrentTime();// SDK自己的请求不监控 & 过滤不需要监控的url
|
|
5
|
+
if(isEndpoint(this.ctx,c)||b.filter(c))return a;var g={...a},h={view:getCurView(this.ctx),timestamp:f,url:c,name:this.getResourceName(c),method:a.method||"GET"};this.injectTracing(h,function(a){var b=[];// 已存在header配置情况
|
|
6
|
+
["headers","header"].forEach(function(c){c in g&&isObject(g[c])&&(g[c]={...g[c],...a},b.push(c))}),0===b.length&&(g[HEADER_KEY]={...g[HEADER_KEY],...a})});// SSE 流式监控逻辑
|
|
7
|
+
var i=this.getSseConfig(),j=!i||!1!==i.enabled,k=isSseSupported();if(j&&k&&isSseRequest(a)){g.enableChunked=!0;var l=new SseMetricsTracker;l.start();var m="",n=null,o=i&&i.timeout||6e4,p=function(a){n&&clearTimeout(n),n=setTimeout(function(){h.sse||(h.sse=l.finish("timeout")),a.abort()},o)};// 通过 __onRequestTask 回调在 hackRequest 中绑定 onChunkReceived
|
|
8
|
+
g.__onRequestTask=function(a){p(a),a&&isFunction(a.onChunkReceived)&&a.onChunkReceived(function(b){p(a);var c=arrayBufferToString(b.data),d=parseSseChunk(c,m);m=d.remaining;for(var e=0;e<d.events.length;e++)l.onMessage()})},g.success=function(...a){n&&clearTimeout(n),h.sse||(h.sse=l.finish("success")),h.success=1,b.sendApi(h,a[0],g),d&&d.apply(this,a)},g.fail=function(...a){if(n&&clearTimeout(n),!h.sse){// 根据 errMsg 区分 abort/timeout/error
|
|
9
|
+
var c=a[0]&&a[0].errMsg||"",d="error";-1===c.indexOf("abort")?-1!==c.indexOf("timeout")&&(d="timeout"):d="abort",h.sse=l.finish(d)}h.success=0,b.sendApi(h,a[0],g),e&&e.apply(this,a)}}else// 非 SSE 请求走原有逻辑
|
|
10
|
+
g.success=function(...a){h.success=1,b.sendApi(h,a[0],g),d&&d.apply(this,a)},g.fail=function(...a){h.success=0,b.sendApi(h,a[0],g),e&&e.apply(this,a)};return g}getSseConfig(){var a=this.ctx.getConfig(),b=a.collectors&&a.collectors.api;return isObject(b)&&b.sse?b.sse:void 0}getResourceName(a){var b,c=this.ctx.getConfig(),d=c.parseResourceName;return b=isFunction(d)?d(a):getPathByURL(a),b}/**
|
|
11
|
+
* 解析小程序的性能数据
|
|
12
|
+
* 参考:https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html
|
|
13
|
+
* https://opendocs.alipay.com/mini/api/owycmh?pathHash=c91640f8
|
|
14
|
+
* TODO:抖音 https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/network/http/tt-request/#Profile-%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E
|
|
15
|
+
* @param profile
|
|
16
|
+
* @returns
|
|
17
|
+
*/parseProfile(a){function b(b,c){b in a&&0<a[b]&&(d[c]=a[b])}// 以下针对类alipay小程序的情况
|
|
18
|
+
function c(b,c,e){if(b in a&&c in a){var f=a[c]-a[b];0<f&&(d[e]=f)}}// 以下针对wx小程序相对标准的情况
|
|
19
|
+
var d={};return b("domainLookup","dns_duration"),b("connect","connect_duration"),b("SSLconnection","ssl_duration"),b("Waiting","first_byte_duration"),b("totalTime","duration"),b("receivedBytedCount","size"),c("connectStart","connectEnd","connect_duration"),c("SSLconnectionStart","SSLconnectionEnd","ssl_duration"),c("domainLookUpStart","domainLookUpEnd","dns_duration"),c("redirectStart","redirectEnd","redirect_duration"),c("responseStart","requestStart","first_byte_duration"),c("responseStart","requestEnd","download_duration"),c("redirectStart","responseEnd","duration"),Object.keys(d).forEach(function(a){d[a]>ONE_HOUR&&delete d[a]}),d}filter(a){var b,c,d=this.ctx.getConfig(),e=(null===d||void 0===d||null===(b=d.filters)||void 0===b?void 0:b.resource)||[];isArray(e)||(e=[e]);var f=null===d||void 0===d||null===(c=d.collectors)||void 0===c?void 0:c.api;return isObject(f)&&isArray(f.filters)&&(e=[...e,...f.filters]),urlMatch(a,e)}}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ICollector, IContext, RumEvent } from '@arms/rum-core';
|
|
2
|
+
export default class PerfCollector implements ICollector {
|
|
3
|
+
name: string;
|
|
4
|
+
ctx: IContext;
|
|
5
|
+
sendEvent: (payload: RumEvent) => void;
|
|
6
|
+
perfObserver: any;
|
|
7
|
+
setup(ctx: IContext, sendEvent: (payload: RumEvent) => void): void;
|
|
8
|
+
resourceHandler: (ev: any) => void;
|
|
9
|
+
destroy(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import{isArray}from"@arms/rum-core";import{appName,getPerformance}from"../../utils/platform";import{checkEnable}from"../../configManager";/*
|
|
2
|
+
* 参考资料:
|
|
3
|
+
* https://developers.weixin.qq.com/miniprogram/dev/api/base/performance/PerformanceEntry.html
|
|
4
|
+
* https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/foundation/performance/tt-performance/performance-entry
|
|
5
|
+
* https://opendocs.alipay.com/mini/api/performanceentry
|
|
6
|
+
* */var performance=getPerformance();export default class PerfCollector{constructor(){this.name="static-resource-collector",this.ctx=void 0,this.sendEvent=void 0,this.perfObserver=void 0,this.resourceHandler=function(a){isArray(a)?a:a.getEntries();// TODO
|
|
7
|
+
}}// perfMap: PerfMap;
|
|
8
|
+
//
|
|
9
|
+
// currentViewId = '';
|
|
10
|
+
//
|
|
11
|
+
// pageIdToViewId: PerfMap = {}; // 仅微信生效
|
|
12
|
+
//
|
|
13
|
+
// onUnload: Function;
|
|
14
|
+
setup(a,b){if(checkEnable(a,"staticResource")&&(this.ctx=a,this.sendEvent=b,performance&&performance.getEntries)){// this.perfMap = {};
|
|
15
|
+
var c;if("wechat"===appName||"alipay"===appName)c=["navigation","render"];else if("bytedance"===appName)c=["launch","paint","navigation"];else return;this.resourceHandler(performance.getEntries().filter(function(a){return c.includes(a.entryType)})),this.perfObserver=performance.createObserver(this.resourceHandler),this.perfObserver.observe({entryTypes:c})}}// delayHandler = (ev) => {
|
|
16
|
+
// const entries = isArray(ev) ? ev : ev.getEntries();
|
|
17
|
+
// if (!Array.isArray(entries) || entries.length === 0) return;
|
|
18
|
+
// setTimeout(() => {
|
|
19
|
+
// entries.forEach(item => {
|
|
20
|
+
// if (item.pageId) { // 仅微信生效
|
|
21
|
+
// const vid = this.pageIdToViewId[`pageId:${item.pageId}`];
|
|
22
|
+
// if (vid && this.perfMap[vid]) {
|
|
23
|
+
// this.perfMap[vid].entries.push(item);
|
|
24
|
+
// return
|
|
25
|
+
// }
|
|
26
|
+
// }
|
|
27
|
+
// if (!this.currentViewId) return;
|
|
28
|
+
// const perfData = this.perfMap[this.currentViewId];
|
|
29
|
+
// if (!perfData) return;
|
|
30
|
+
// let path = item.path;
|
|
31
|
+
// if (!path && item.entryType === 'navigation') {
|
|
32
|
+
// path = item.name
|
|
33
|
+
// }
|
|
34
|
+
// if (perfData.path !== path) return;
|
|
35
|
+
// perfData.entries.push(item);
|
|
36
|
+
// })
|
|
37
|
+
// // console.log('delayHandlerCache', this.perfMap, this.currentViewId);
|
|
38
|
+
// }, 200)
|
|
39
|
+
// }
|
|
40
|
+
//
|
|
41
|
+
// onLoad = () => {
|
|
42
|
+
// const page = getCurPage();
|
|
43
|
+
// const view = getCurView(this.ctx);
|
|
44
|
+
// const vid = page.__rum_view_id;
|
|
45
|
+
// const pageId = isFunction(page.getPageId) ? page.getPageId() : null;
|
|
46
|
+
// if (vid in this.perfMap) return;
|
|
47
|
+
// this.currentViewId = vid;
|
|
48
|
+
// pageId && (this.pageIdToViewId[pageId.toString()] = vid);
|
|
49
|
+
// this.perfMap[vid] = {
|
|
50
|
+
// view,
|
|
51
|
+
// path: page.is || page.route,
|
|
52
|
+
// pageId,
|
|
53
|
+
// entries: [],
|
|
54
|
+
// timer: setTimeout(() => {
|
|
55
|
+
// this.flushByViewId(vid, 'timeout');
|
|
56
|
+
// }, 10000)
|
|
57
|
+
// };
|
|
58
|
+
// }
|
|
59
|
+
//
|
|
60
|
+
// flushByViewId = (vid: string, type = 'normal') => {
|
|
61
|
+
// const perfData = this.perfMap[vid];
|
|
62
|
+
// if (!perfData) return;
|
|
63
|
+
// this.perfMap[vid] = false;
|
|
64
|
+
// clearTimeout(perfData.timer);
|
|
65
|
+
// // console.log('flushByViewId', vid, perfData, type);
|
|
66
|
+
// if (!perfData.entries.length) return;
|
|
67
|
+
// this.parsePerf(perfData)
|
|
68
|
+
// }
|
|
69
|
+
//
|
|
70
|
+
// parsePerf(perfData) {
|
|
71
|
+
// const { entries, view } = perfData;
|
|
72
|
+
// let navData;
|
|
73
|
+
// let frData;
|
|
74
|
+
// let fpData;
|
|
75
|
+
// let fcpData;
|
|
76
|
+
// let lcpData;
|
|
77
|
+
// entries.forEach(item => {
|
|
78
|
+
// if (!item.startTime) return;
|
|
79
|
+
// switch (item.name) {
|
|
80
|
+
// case 'firstRender':
|
|
81
|
+
// case 'first-render':
|
|
82
|
+
// frData = item;
|
|
83
|
+
// break;
|
|
84
|
+
// case 'firstPaint':
|
|
85
|
+
// case 'first-paint':
|
|
86
|
+
// fpData = item;
|
|
87
|
+
// break;
|
|
88
|
+
// case 'firstContentfulPaint':
|
|
89
|
+
// case 'first-contentful-paint':
|
|
90
|
+
// fcpData = item;
|
|
91
|
+
// break
|
|
92
|
+
// case 'largestContentfulPaint':
|
|
93
|
+
// case 'largest-contentful-paint':
|
|
94
|
+
// if (lcpData && lcpData.startTime > item.startTime) {
|
|
95
|
+
// break
|
|
96
|
+
// }
|
|
97
|
+
// lcpData = item;
|
|
98
|
+
// break;
|
|
99
|
+
// default:
|
|
100
|
+
// if (item.entryType === 'navigation') {
|
|
101
|
+
// navData = item;
|
|
102
|
+
// }
|
|
103
|
+
// break;
|
|
104
|
+
// }
|
|
105
|
+
// });
|
|
106
|
+
// const measures: Measures = {};
|
|
107
|
+
// if (!navData) {
|
|
108
|
+
// return
|
|
109
|
+
// }
|
|
110
|
+
// if (frData) {
|
|
111
|
+
// measures.first_render = frData.duration ? frData.duration : (frData.startTime - navData.startTime);
|
|
112
|
+
// }
|
|
113
|
+
// if (fpData) {
|
|
114
|
+
// measures.first_paint = fpData.startTime - navData.startTime;
|
|
115
|
+
// }
|
|
116
|
+
// if (fcpData) {
|
|
117
|
+
// measures.first_contentful_paint = fcpData.startTime - navData.startTime;
|
|
118
|
+
// }
|
|
119
|
+
// if (lcpData) {
|
|
120
|
+
// measures.largest_contentful_paint = lcpData.startTime - navData.startTime;
|
|
121
|
+
// }
|
|
122
|
+
// this.sendPerf(measures, {
|
|
123
|
+
// view,
|
|
124
|
+
// timestamp: navData.startTime,
|
|
125
|
+
// timing_data: JSON.stringify(navData)
|
|
126
|
+
// })
|
|
127
|
+
// }
|
|
128
|
+
//
|
|
129
|
+
// sendPerf(measures: Measures, event: PerfMap) {
|
|
130
|
+
// const keys = Object.keys(measures);
|
|
131
|
+
// for (let i = 0; i < keys.length; i++) {
|
|
132
|
+
// const key = keys[i];
|
|
133
|
+
// const value = measures[key];
|
|
134
|
+
// if (value <= 0 || isNaN(value) || value > 6e5) {
|
|
135
|
+
// delete measures[key];
|
|
136
|
+
// }
|
|
137
|
+
// }
|
|
138
|
+
// if (!Object.keys(measures)) return;
|
|
139
|
+
// const baseRumEvent = this.ctx.session.getBaseEvent();
|
|
140
|
+
// this.sendEvent({
|
|
141
|
+
// ...baseRumEvent,
|
|
142
|
+
// event_type: RumEventType.VIEW,
|
|
143
|
+
// type: 'perf',
|
|
144
|
+
// ...event,
|
|
145
|
+
// ...measures
|
|
146
|
+
// })
|
|
147
|
+
// }
|
|
148
|
+
destroy(){this.perfObserver&&this.perfObserver.disconnect()}}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ICollector, IContext, RumEvent } from '@arms/rum-core';
|
|
2
|
+
interface PerfMap {
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
}
|
|
5
|
+
interface Measures {
|
|
6
|
+
[key: string]: number;
|
|
7
|
+
}
|
|
8
|
+
export default class PerfCollector implements ICollector {
|
|
9
|
+
name: string;
|
|
10
|
+
ctx: IContext;
|
|
11
|
+
sendEvent: (payload: RumEvent) => void;
|
|
12
|
+
perfObserver: any;
|
|
13
|
+
perfCache: PerfMap;
|
|
14
|
+
currentViewId: string;
|
|
15
|
+
pageIdToViewId: PerfMap;
|
|
16
|
+
onUnload: Function;
|
|
17
|
+
setup(ctx: IContext, sendEvent: (payload: RumEvent) => void): void;
|
|
18
|
+
delayHandler: (ev: any) => void;
|
|
19
|
+
onLoad: () => void;
|
|
20
|
+
onReady: () => void;
|
|
21
|
+
flushByViewId: (vid: string) => void;
|
|
22
|
+
parsePerf(perfData: PerfMap): void;
|
|
23
|
+
sendPerf(measures: Measures, event: PerfMap): void;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import{isArray,isFunction,RumEventType,ONE_MINUTE}from"@arms/rum-core";import{alipayName,appName,bytedanceName,getPerformance,ksName,wechatName}from"../../utils/platform";import{getCurPage,getCurView,getRumViewId}from"../../utils/view";import{addPageListener,removePageListener}from"../../utils/hackPage";import{getCurrentTime}from"../../utils/base";import{COMMON_DELAY}from"../../utils/constants";import{checkEnable}from"../../configManager";/*
|
|
2
|
+
* 参考资料:
|
|
3
|
+
* https://developers.weixin.qq.com/miniprogram/dev/api/base/performance/PerformanceEntry.html
|
|
4
|
+
* https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/foundation/performance/tt-performance/performance-entry
|
|
5
|
+
* https://opendocs.alipay.com/mini/api/performanceentry
|
|
6
|
+
* */var performance=getPerformance();export default class PerfCollector{constructor(){var a=this;// 仅微信生效
|
|
7
|
+
this.name="perf-collector",this.ctx=void 0,this.sendEvent=void 0,this.perfObserver=void 0,this.perfCache=void 0,this.currentViewId="",this.pageIdToViewId={},this.onUnload=void 0,this.delayHandler=function(b){var c=isArray(b)?b:b.getEntries();Array.isArray(c)&&0!==c.length&&setTimeout(function(){c.forEach(function(b){if(b.pageId){// 仅微信生效
|
|
8
|
+
var c=a.pageIdToViewId["pageId:"+b.pageId];if(c&&a.perfCache[c])return void a.perfCache[c].entries.push(b)}if(a.currentViewId){var d=a.perfCache[a.currentViewId];if(d){var e=b.path;e||"navigation"!==b.entryType||(e=b.name),d.path!==e||d.entries.push(b)}}})},COMMON_DELAY)},this.onLoad=function(){var b=getCurPage(),c=getCurView(a.ctx),d=getRumViewId(b,a.ctx.session),e=isFunction(b.getPageId)?b.getPageId():null;d in a.perfCache||(a.currentViewId=d,e&&(a.pageIdToViewId[e.toString()]=d),a.perfCache[d]={view:c,path:b.is||b.route,pageId:e,entries:[],timer:setTimeout(function(){a.flushByViewId(d)},ONE_MINUTE),loadTime:getCurrentTime()})},this.onReady=function(){var b=a.currentViewId;if(b){var c=a.perfCache[b];c&&(c.dom_complete=getCurrentTime()-c.loadTime)}},this.flushByViewId=function(b){var c=a.perfCache[b];c&&(a.perfCache[b]=!1,clearTimeout(c.timer),c.entries.length&&a.parsePerf(c))}}setup(a,b){if(checkEnable(a,"perf")){var c=this;if(this.ctx=a,this.sendEvent=b,performance&&performance.getEntries){this.perfCache={};// appType_1 是针对微信和支付宝的情况,该类型的Performance对象支持 pageId,且性能数据类型为render
|
|
9
|
+
var d,e=appName===wechatName||appName===alipayName;if(e)d=["navigation","render"];else if(appName===bytedanceName)d=["launch","paint","navigation"];else if(appName===ksName)d=["paint","navigation"];else return;this.delayHandler(performance.getEntries().filter(function(a){return d.includes(a.entryType)})),this.perfObserver=performance.createObserver(this.delayHandler),this.perfObserver.observe({entryTypes:d}),this.onUnload=function(){if(!e){// 不支持pageId的情况下,无法直接关联性能和页面的对应关系,需要Hide时结束
|
|
10
|
+
var b=getRumViewId(this,a.session);b&&c.flushByViewId(b)}},addPageListener("onLoad",this.onLoad),addPageListener("onReady",this.onReady),addPageListener("onHide",this.onUnload),addPageListener("onUnload",this.onUnload)}}}parsePerf(a){var b,c,d,e,f,g=a.entries,h=a.view,i=a.dom_complete;g.forEach(function(a){if(a.startTime)switch(a.name){case"firstRender":case"first-render":c=a;break;case"firstPaint":case"first-paint":d=a;break;case"firstContentfulPaint":case"first-contentful-paint":e=a;break;case"largestContentfulPaint":case"largest-contentful-paint":if(f&&f.startTime>a.startTime)break;f=a;break;default:"navigation"===a.entryType&&(b=a)}});var j={};b&&(c&&(j.first_render=c.duration?c.duration:c.startTime-b.startTime),d&&(j.first_paint=d.startTime-b.startTime),e&&(j.first_contentful_paint=e.startTime-b.startTime),f&&(j.largest_contentful_paint=f.startTime-b.startTime),i&&(j.dom_complete=i),this.sendPerf(j,{view:h,timestamp:b.startTime,timing_data:JSON.stringify(b)}))}sendPerf(a,b){for(var c=Object.keys(a),d=0;d<c.length;d++){var e=c[d],f=a[e];(0>=f||isNaN(f)||6e5<f)&&delete a[e]}if(0!==Object.keys(a).length){var g=this.ctx.session.getBaseEvent();this.sendEvent({...g,event_type:RumEventType.VIEW,type:"perf",...b,...a})}}destroy(){this.perfObserver&&this.perfObserver.disconnect(),removePageListener("onLoad",this.onLoad),removePageListener("onReady",this.onReady),removePageListener("onHide",this.onUnload),removePageListener("onUnload",this.onUnload)}}
|