@arms/rum-miniapp 0.1.6 → 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.
Files changed (89) hide show
  1. package/README.md +146 -9
  2. package/dist/miniapp-sdk.js +1 -1
  3. package/docs/SDK/347/211/210/346/234/254/350/257/264/346/230/216.md +71 -0
  4. package/docs/SDK/351/205/215/347/275/256/345/217/202/350/200/203.md +523 -0
  5. 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
  6. package/es/collector/action/index.d.ts +14 -0
  7. package/es/collector/action/index.js +1 -0
  8. package/es/collector/application/index.d.ts +15 -0
  9. package/es/collector/application/index.js +5 -0
  10. package/es/collector/exception/index.d.ts +29 -0
  11. package/es/collector/exception/index.js +7 -0
  12. package/es/collector/longtask/render-block.d.ts +15 -0
  13. package/es/collector/longtask/render-block.js +1 -0
  14. package/es/collector/resource/api.d.ts +28 -0
  15. package/es/collector/resource/api.js +19 -0
  16. package/es/collector/resource/static-resource.d.ts +10 -0
  17. package/es/collector/resource/static-resource.js +148 -0
  18. package/es/collector/view/perf.d.ts +26 -0
  19. package/es/collector/view/perf.js +10 -0
  20. package/es/collector/view/pv.d.ts +12 -0
  21. package/es/collector/view/pv.js +2 -0
  22. package/es/configManager.d.ts +13 -0
  23. package/es/configManager.js +7 -0
  24. package/es/empty.js +1 -0
  25. package/es/index.d.ts +3 -0
  26. package/es/index.js +1 -0
  27. package/es/processor/default-processor.d.ts +5 -0
  28. package/es/processor/default-processor.js +1 -0
  29. package/es/processor/session-processor.d.ts +8 -0
  30. package/es/processor/session-processor.js +1 -0
  31. package/es/reporter/index.d.ts +10 -0
  32. package/es/reporter/index.js +3 -0
  33. package/es/shell.d.ts +42 -0
  34. package/es/shell.js +10 -0
  35. package/es/style.js +1 -0
  36. package/es/types/client.d.ts +39 -0
  37. package/es/types/client.js +1 -0
  38. package/es/utils/api.d.ts +43 -0
  39. package/es/utils/api.js +3 -0
  40. package/es/utils/base.d.ts +19 -0
  41. package/es/utils/base.js +16 -0
  42. package/es/utils/constants.d.ts +1 -0
  43. package/es/utils/constants.js +1 -0
  44. package/es/utils/hackApp.d.ts +3 -0
  45. package/es/utils/hackApp.js +2 -0
  46. package/es/utils/hackComponent.d.ts +3 -0
  47. package/es/utils/hackComponent.js +5 -0
  48. package/es/utils/hackPage.d.ts +3 -0
  49. package/es/utils/hackPage.js +2 -0
  50. package/es/utils/network.d.ts +1 -0
  51. package/es/utils/network.js +1 -0
  52. package/es/utils/platform.d.ts +41 -0
  53. package/es/utils/platform.js +6 -0
  54. package/es/utils/session.d.ts +26 -0
  55. package/es/utils/session.js +4 -0
  56. package/es/utils/sse.d.ts +23 -0
  57. package/es/utils/sse.js +14 -0
  58. package/es/utils/url.d.ts +29 -0
  59. package/es/utils/url.js +16 -0
  60. package/es/utils/view.d.ts +32 -0
  61. package/es/utils/view.js +31 -0
  62. package/lib/collector/action/index.js +1 -1
  63. package/lib/collector/application/index.js +2 -2
  64. package/lib/collector/exception/index.js +3 -3
  65. package/lib/collector/longtask/render-block.js +1 -1
  66. package/lib/collector/resource/api.d.ts +1 -0
  67. package/lib/collector/resource/api.js +13 -7
  68. package/lib/collector/resource/static-resource.js +10 -10
  69. package/lib/collector/view/perf.js +6 -5
  70. package/lib/collector/view/pv.js +2 -2
  71. package/lib/configManager.js +6 -5
  72. package/lib/empty.js +2 -0
  73. package/lib/processor/default-processor.js +1 -1
  74. package/lib/processor/session-processor.js +1 -1
  75. package/lib/reporter/index.js +2 -2
  76. package/lib/shell.js +6 -6
  77. package/lib/types/client.d.ts +21 -2
  78. package/lib/utils/api.d.ts +13 -0
  79. package/lib/utils/api.js +3 -1
  80. package/lib/utils/base.js +1 -1
  81. package/lib/utils/hackApp.js +2 -2
  82. package/lib/utils/hackComponent.js +2 -2
  83. package/lib/utils/hackPage.js +2 -2
  84. package/lib/utils/platform.js +1 -1
  85. package/lib/utils/session.js +4 -2
  86. package/lib/utils/sse.d.ts +23 -0
  87. package/lib/utils/sse.js +14 -0
  88. package/lib/utils/url.js +2 -2
  89. package/package.json +12 -3
@@ -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)}}