@xenon-device-management/xenon 2.9.2 → 2.10.0

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 (143) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-B87n4pUu.js → AnnotationOverlay-LzfXiiZJ.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-BzFDp4mK.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-BpgWvmZo.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-CCB-egSH.js} +2 -2
  6. package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-DkUVmANo.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-mbfnfTql.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-BH5usz4Y.js} +1 -1
  9. package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Cbo9mdTk.js} +1 -1
  10. package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-D6XfQHPr.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-CmFPkWqH.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CSlKk2qC.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-BFW9Dlqd.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-C4DioTTi.js} +1 -1
  15. package/lib/public/assets/{Table-D_2lhvRM.js → Table-DrKjlL00.js} +1 -1
  16. package/lib/public/assets/{activity-cNHHOKx5.js → activity-C9bE7Jm8.js} +1 -1
  17. package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DRYic4oO.js} +1 -1
  18. package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-CaQU0g7c.js} +1 -1
  19. package/lib/public/assets/{apps-DCiI3UOb.js → apps-c1gUFmmK.js} +1 -1
  20. package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-DMvAWA8r.js} +1 -1
  21. package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-BEQgxmKT.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-Ba0e2GeU.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-COODQnc7.js} +2 -2
  24. package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-DZQgWBUs.js} +4 -4
  25. package/lib/public/assets/{button-C0caCjJB.js → button-WW9j43wR.js} +1 -1
  26. package/lib/public/assets/{calendar-BqOOkln_.js → calendar-COUs9VxF.js} +1 -1
  27. package/lib/public/assets/{check-DYSXRG79.js → check-D8ifHIJR.js} +1 -1
  28. package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-BjKVUlB_.js} +1 -1
  29. package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-CfP79QE2.js} +1 -1
  30. package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-BZbFCixZ.js} +1 -1
  31. package/lib/public/assets/{clock-XP3iBAhg.js → clock-BuTm35Qg.js} +1 -1
  32. package/lib/public/assets/{copy-D8d9UzWE.js → copy-W00zUBwN.js} +1 -1
  33. package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-BEpS-Y-_.js} +5 -5
  34. package/lib/public/assets/{download-SaN9-umX.js → download-CGz-MS3y.js} +1 -1
  35. package/lib/public/assets/{forgot-password-DfUhEZW5.js → forgot-password-DG7SqVgU.js} +1 -1
  36. package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
  37. package/lib/public/assets/{index-IefGCFQQ.js → index-DWHTp1YP.js} +1 -1
  38. package/lib/public/assets/{index-CzaIyL9t.js → index-EkaQM1DK.js} +2 -2
  39. package/lib/public/assets/{input-BqFzAQT3.js → input-ChaTXeCq.js} +1 -1
  40. package/lib/public/assets/{keycodes-DDV2OlpQ.js → keycodes-BhbgeW_r.js} +10 -5
  41. package/lib/public/assets/line-chart-9P3RxScy.js +1 -0
  42. package/lib/public/assets/list-checks-_23IIEEL.js +11 -0
  43. package/lib/public/assets/{lock-Eye1CG8p.js → lock-B2JaqqRI.js} +1 -1
  44. package/lib/public/assets/{login-Co1IXuen.js → login-BEWIfLfC.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-BGJIJQ-k.js} +1 -1
  46. package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-BrwMY4G0.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-DsWDMyrY.js} +1 -1
  48. package/lib/public/assets/{network-BP_U4iDZ.js → network-DfIhNVJv.js} +1 -1
  49. package/lib/public/assets/overview-B1mePY2_.js +6 -0
  50. package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DoD3TOzZ.js} +1 -1
  51. package/lib/public/assets/{plus-1k0sxXyU.js → plus-BGvWvpBB.js} +1 -1
  52. package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-CvV5aWvl.js} +1 -1
  53. package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-CGVd31LR.js} +1 -1
  54. package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page-Bq2cvwb5.js} +1 -1
  55. package/lib/public/assets/{select-CiRW6v17.js → select-CTcD4jWR.js} +1 -1
  56. package/lib/public/assets/selector-detail-redirect-19odlD4B.js +1 -0
  57. package/lib/public/assets/selector-health-page-LqIsLrjW.js +11 -0
  58. package/lib/public/assets/session-detail-page-DQzb8_fq.js +4 -0
  59. package/lib/public/assets/{settings-DhwZoWv7.js → settings-C-EnGIqs.js} +1 -1
  60. package/lib/public/assets/stat-tile-BY0RDTRm.js +1 -0
  61. package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-B4cFM-5B.js} +1 -1
  62. package/lib/public/assets/{teams-D0IBa2Ov.js → teams-BFe-RpYd.js} +1 -1
  63. package/lib/public/assets/time-D-gjmE1o.js +1 -0
  64. package/lib/public/assets/{trash-2-u8MlXUBD.js → trash-2-CJCKBOP9.js} +1 -1
  65. package/lib/public/assets/{upload-Cq8L3kpw.js → upload-DidWkbld.js} +1 -1
  66. package/lib/public/assets/use-builds-data-fnfxatOd.js +8 -0
  67. package/lib/public/assets/{use-password-reset-mode-Dbr2HEfw.js → use-password-reset-mode-iPen3tmC.js} +1 -1
  68. package/lib/public/assets/{users-CZqCE511.js → users-CrcjmRdf.js} +1 -1
  69. package/lib/public/assets/{users-DxLHYmFm.js → users-DBUv0CJx.js} +1 -1
  70. package/lib/public/assets/{video-off-mh1EEzP9.js → video-off-eh69-FKB.js} +1 -1
  71. package/lib/public/assets/view-state-CP9dE1UI.js +1 -0
  72. package/lib/public/assets/{webhook-settings-BYuOib-Q.js → webhook-settings-CQoSshBn.js} +1 -1
  73. package/lib/public/assets/{zap-BOpUur0M.js → zap-CICtWiLn.js} +1 -1
  74. package/lib/public/index.html +2 -2
  75. package/lib/schema.json +5 -0
  76. package/lib/src/app/routers/dashboard.js +86 -40
  77. package/lib/src/app/routers/selector-health.js +75 -0
  78. package/lib/src/app/swagger-docs.js +77 -8
  79. package/lib/src/dashboard/event-manager.js +15 -99
  80. package/lib/src/generated/client/edge.js +27 -3
  81. package/lib/src/generated/client/index-browser.js +24 -0
  82. package/lib/src/generated/client/index.d.ts +8207 -5283
  83. package/lib/src/generated/client/index.js +27 -3
  84. package/lib/src/generated/client/package.json +1 -1
  85. package/lib/src/generated/client/schema.prisma +43 -9
  86. package/lib/src/generated/client/wasm.js +24 -0
  87. package/lib/src/prisma.js +2 -0
  88. package/lib/src/services/CleanupService.js +1 -0
  89. package/lib/src/services/NotificationService.js +3 -4
  90. package/lib/src/services/SelectorStateService.js +151 -113
  91. package/lib/src/services/SelectorVerificationJob.js +14 -9
  92. package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
  93. package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
  94. package/lib/src/services/metrics/SessionMetricsService.js +238 -0
  95. package/lib/src/services/metrics/androidMetrics.js +94 -0
  96. package/lib/src/services/metrics/iosMetrics.js +26 -0
  97. package/lib/src/services/metrics/metricsBody.js +37 -0
  98. package/lib/src/services/metrics/types.js +19 -0
  99. package/lib/src/services/selector-health/access.js +66 -0
  100. package/lib/src/services/selector-health/healingTrend.js +49 -0
  101. package/lib/src/services/selector-health/people.js +27 -0
  102. package/lib/src/services/selector-health/selectorDetail.js +198 -0
  103. package/lib/src/services/selector-health/selectorKeys.js +36 -0
  104. package/lib/src/services/selector-health/selectorList.js +333 -0
  105. package/lib/src/services/selector-health/stateView.js +67 -0
  106. package/lib/test/helpers/scratch-database.js +2 -0
  107. package/lib/test/helpers/selector-health-fixture.js +216 -0
  108. package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
  109. package/lib/test/unit/CleanupService.spec.js +28 -2
  110. package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
  111. package/lib/test/unit/healing-no-cost.spec.js +134 -0
  112. package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
  113. package/lib/test/unit/healing-trend.spec.js +118 -0
  114. package/lib/test/unit/selector-event-model.spec.js +47 -0
  115. package/lib/test/unit/selector-health-actions.spec.js +106 -0
  116. package/lib/test/unit/selector-health-detail.spec.js +131 -0
  117. package/lib/test/unit/selector-health-list.spec.js +185 -0
  118. package/lib/test/unit/selector-state-service.spec.js +74 -1
  119. package/lib/test/unit/selector-verification-job.spec.js +38 -0
  120. package/lib/test/unit/session-metrics-parse.spec.js +138 -0
  121. package/lib/test/unit/session-metrics-route.spec.js +140 -0
  122. package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
  123. package/lib/test/unit/session-metrics-service.spec.js +215 -0
  124. package/lib/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +1 -1
  126. package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
  127. package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
  128. package/prisma/schema.prisma +43 -9
  129. package/schema.json +5 -0
  130. package/lib/public/assets/chevron-left-BSG6mcpo.js +0 -6
  131. package/lib/public/assets/funnel-OefHE-54.js +0 -6
  132. package/lib/public/assets/layers-a3GNz2DA.js +0 -6
  133. package/lib/public/assets/list-checks-DMrMAFDt.js +0 -6
  134. package/lib/public/assets/overview-CsPyPv10.js +0 -1
  135. package/lib/public/assets/selector-detail-page-pA913STP.js +0 -1
  136. package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
  137. package/lib/public/assets/selector-health-CHLX8KqY.js +0 -16
  138. package/lib/public/assets/selector-health-page-vVkg8Mii.js +0 -16
  139. package/lib/public/assets/session-detail-page-BCD49zDi.js +0 -4
  140. package/lib/public/assets/stat-tile-BaE76cnK.js +0 -8
  141. package/lib/public/assets/time-B8FIgwPE.js +0 -1
  142. package/lib/src/profiling/AndroidAppProfiler.js +0 -213
  143. /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const androidMetrics_1 = require("../../src/services/metrics/androidMetrics");
5
+ const iosMetrics_1 = require("../../src/services/metrics/iosMetrics");
6
+ /**
7
+ * Recorded on the S9+ (Android 10) with the command androidSampleCommand
8
+ * builds; the second reading is the first plus 1000 jiffies, 800 of them
9
+ * idle, and 50 for the app.
10
+ */
11
+ const FIRST = [
12
+ 'cpu 4842746 1486246 9435122 410569805 191176 0 106298 0 0 0',
13
+ 'MemTotal: 5755748 kB',
14
+ 'MemAvailable: 2839168 kB',
15
+ 'pid=1404',
16
+ 'stat=211391 93546',
17
+ 'VmRSS:\t 334480 kB',
18
+ ].join('\n');
19
+ const SECOND = [
20
+ 'cpu 4842846 1486246 9435222 410570595 191186 0 106298 0 0 0',
21
+ 'MemTotal: 5755748 kB',
22
+ 'MemAvailable: 2838144 kB',
23
+ 'pid=1404',
24
+ 'stat=211421 93566',
25
+ 'VmRSS:\t 335504 kB',
26
+ ].join('\n');
27
+ /** A reading the test knows is readable. */
28
+ function read(out) {
29
+ const r = (0, androidMetrics_1.parseAndroidReading)(out);
30
+ if (!r)
31
+ throw new Error('unreadable sample');
32
+ return r;
33
+ }
34
+ describe('session metrics: Android readings', () => {
35
+ it('reads device CPU jiffies, memory, and the app process', () => {
36
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)(FIRST)).to.deep.equal({
37
+ cpuTotal: 4842746 + 1486246 + 9435122 + 410569805 + 191176 + 0 + 106298 + 0,
38
+ cpuIdle: 410569805 + 191176,
39
+ memTotalKb: 5755748,
40
+ memAvailableKb: 2839168,
41
+ pid: 1404,
42
+ appJiffies: 211391 + 93546,
43
+ appRssKb: 334480,
44
+ });
45
+ });
46
+ it('reads a device with no app process as device figures only', () => {
47
+ const out = [FIRST.split('\n')[0], FIRST.split('\n')[1], FIRST.split('\n')[2], 'pid='].join('\n');
48
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)(out)).to.include({ pid: null, appJiffies: null, appRssKb: null });
49
+ });
50
+ it('reads output with CRLF line ends', () => {
51
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)(FIRST.replace(/\n/g, '\r\n'))).to.include({
52
+ pid: 1404,
53
+ appRssKb: 334480,
54
+ });
55
+ });
56
+ it('gives up on output without the cpu line', () => {
57
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)('MemTotal: 5755748 kB')).to.equal(null);
58
+ });
59
+ it('records no CPU on the first sample, only memory', () => {
60
+ const s = (0, androidMetrics_1.androidSample)(null, read(FIRST), 'com.android.systemui', 1000);
61
+ (0, chai_1.expect)(s).to.deep.equal({
62
+ at: 1000,
63
+ deviceCpuPct: null,
64
+ deviceMemMb: 2848.2,
65
+ deviceMemTotalMb: 5620.8,
66
+ appCpuPct: null,
67
+ appMemMb: 326.6,
68
+ appId: 'com.android.systemui',
69
+ });
70
+ });
71
+ it('turns two readings into device and app CPU as shares of the whole device', () => {
72
+ const s = (0, androidMetrics_1.androidSample)((0, androidMetrics_1.parseAndroidReading)(FIRST), read(SECOND), 'com.android.systemui', 3000);
73
+ (0, chai_1.expect)(s.deviceCpuPct).to.equal(20);
74
+ (0, chai_1.expect)(s.appCpuPct).to.equal(5);
75
+ (0, chai_1.expect)(s.appMemMb).to.equal(327.6);
76
+ });
77
+ it('records no app CPU, not a spike, when the app restarted between samples', () => {
78
+ const restarted = SECOND.replace('pid=1404', 'pid=2210').replace('stat=211421 93566', 'stat=12 8');
79
+ const s = (0, androidMetrics_1.androidSample)((0, androidMetrics_1.parseAndroidReading)(FIRST), read(restarted), 'com.android.systemui', 3000);
80
+ (0, chai_1.expect)(s.appCpuPct).to.equal(null);
81
+ (0, chai_1.expect)(s.deviceCpuPct).to.equal(20);
82
+ (0, chai_1.expect)(s.appMemMb).to.equal(327.6);
83
+ });
84
+ it('names no app when the app has no process', () => {
85
+ const none = [SECOND.split('\n')[0], SECOND.split('\n')[1], SECOND.split('\n')[2], 'pid='].join('\n');
86
+ const s = (0, androidMetrics_1.androidSample)((0, androidMetrics_1.parseAndroidReading)(FIRST), read(none), 'com.acme.shop', 3000);
87
+ (0, chai_1.expect)(s).to.include({ appId: null, appCpuPct: null, appMemMb: null, deviceCpuPct: 20 });
88
+ });
89
+ });
90
+ describe('session metrics: Android package names', () => {
91
+ it('accepts package names and nothing else', () => {
92
+ for (const ok of ['com.acme.shop', 'com.sec.android.app.launcher', 'io.x_y.App2']) {
93
+ (0, chai_1.expect)((0, androidMetrics_1.isSafePackage)(ok), ok).to.equal(true);
94
+ }
95
+ for (const bad of [
96
+ '',
97
+ 'shop',
98
+ 'com.acme;reboot',
99
+ '$(id).x',
100
+ 'com.acme shop',
101
+ 'com..x',
102
+ '1com.x',
103
+ undefined,
104
+ 42,
105
+ ]) {
106
+ (0, chai_1.expect)((0, androidMetrics_1.isSafePackage)(bad), String(bad)).to.equal(false);
107
+ }
108
+ });
109
+ it("reads a session's appPackage from either capability spelling", () => {
110
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)({ 'appium:appPackage': 'com.a.b' })).to.equal('com.a.b');
111
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)({ appPackage: 'com.c.d' })).to.equal('com.c.d');
112
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)({})).to.equal(undefined);
113
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)(undefined)).to.equal(undefined);
114
+ });
115
+ it('builds a device-only command with no package, and refuses anything but a package', () => {
116
+ (0, chai_1.expect)((0, androidMetrics_1.androidSampleCommand)(null)).to.not.include('pidof');
117
+ (0, chai_1.expect)((0, androidMetrics_1.androidSampleCommand)('com.acme.shop')).to.include('pidof com.acme.shop');
118
+ (0, chai_1.expect)(() => (0, androidMetrics_1.androidSampleCommand)('com.acme;reboot')).to.throw(/not a package name/);
119
+ });
120
+ it('reads the foreground app from dumpsys, before and after Android 10', () => {
121
+ (0, chai_1.expect)((0, androidMetrics_1.parseForegroundPackage)(' mResumedActivity: ActivityRecord{c51fd83 u0 com.sec.android.app.launcher/.activities.LauncherActivity t2}')).to.equal('com.sec.android.app.launcher');
122
+ (0, chai_1.expect)((0, androidMetrics_1.parseForegroundPackage)(' topResumedActivity=ActivityRecord{a1b2c3 u0 com.acme.shop/.MainActivity t12}')).to.equal('com.acme.shop');
123
+ (0, chai_1.expect)((0, androidMetrics_1.parseForegroundPackage)('nothing resumed')).to.equal(null);
124
+ });
125
+ });
126
+ describe('session metrics: iPhone sysmontap lines', () => {
127
+ it('reads device CPU, the load divided over the cores', () => {
128
+ const line = '{"time":"2026-10-02T11:56:46.322036+05:30","level":"INFO","msg":"received CPU usage data","cpu_count":6,"enabled_cpus":6,"end_time":1301586447413,"cpu_total_load":77.48226950354608}';
129
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)(line)).to.equal(12.9);
130
+ });
131
+ it('reads nothing from other lines', () => {
132
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)('{"level":"WARN","msg":"go-ios agent is not running."}')).to.equal(null);
133
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)('not json')).to.equal(null);
134
+ });
135
+ it('never reports more than 100%', () => {
136
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)('{"enabled_cpus":2,"cpu_total_load":450}')).to.equal(100);
137
+ });
138
+ });
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const express_1 = __importDefault(require("express"));
18
+ const supertest_1 = __importDefault(require("supertest"));
19
+ const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
20
+ const metricsBody_1 = require("../../src/services/metrics/metricsBody");
21
+ const scratch_database_1 = require("../helpers/scratch-database");
22
+ const row = (at, over = {}) => (Object.assign({ at, device_cpu_pct: 20, device_mem_mb: 2800, device_mem_total: 5620.8, app_cpu_pct: 5, app_mem_mb: 300, app_id: 'com.acme.shop' }, over));
23
+ describe('session metrics: the response', () => {
24
+ it('says what each platform can record', () => {
25
+ (0, chai_1.expect)((0, metricsBody_1.seriesFor)('Android')).to.deep.equal({
26
+ deviceCpu: true,
27
+ deviceMem: true,
28
+ appCpu: true,
29
+ appMem: true,
30
+ });
31
+ (0, chai_1.expect)((0, metricsBody_1.seriesFor)('ios')).to.deep.equal({
32
+ deviceCpu: true,
33
+ deviceMem: false,
34
+ appCpu: false,
35
+ appMem: false,
36
+ });
37
+ (0, chai_1.expect)((0, metricsBody_1.seriesFor)('')).to.deep.equal({
38
+ deviceCpu: false,
39
+ deviceMem: false,
40
+ appCpu: false,
41
+ appMem: false,
42
+ });
43
+ });
44
+ it('passes on whether a running session is being sampled, and nothing for an ended one', () => {
45
+ (0, chai_1.expect)((0, metricsBody_1.sessionMetricsBody)('android', [], 'off').recording).to.equal('off');
46
+ (0, chai_1.expect)((0, metricsBody_1.sessionMetricsBody)('android', []).recording).to.equal(null);
47
+ });
48
+ it('answers samples in time order, named by the last app recorded', () => {
49
+ const body = (0, metricsBody_1.sessionMetricsBody)('android', [
50
+ row(4000, { app_id: null, app_cpu_pct: null, app_mem_mb: null }),
51
+ row(0, { app_id: 'com.old.app' }),
52
+ row(2000),
53
+ ]);
54
+ (0, chai_1.expect)(body.samples.map((s) => s.t)).to.deep.equal([0, 2000, 4000]);
55
+ (0, chai_1.expect)(body.appId).to.equal('com.acme.shop');
56
+ (0, chai_1.expect)(body.intervalMs).to.equal(2000);
57
+ (0, chai_1.expect)(body.samples[1]).to.deep.equal({
58
+ t: 2000,
59
+ deviceCpu: 20,
60
+ deviceMemMb: 2800,
61
+ deviceMemTotalMb: 5620.8,
62
+ appCpu: 5,
63
+ appMemMb: 300,
64
+ app: 'com.acme.shop',
65
+ });
66
+ });
67
+ it("names each sample's app, since the foreground app can change", () => {
68
+ const body = (0, metricsBody_1.sessionMetricsBody)('android', [
69
+ row(0, { app_id: 'com.a.one' }),
70
+ row(2000, { app_id: 'com.b.two' }),
71
+ ]);
72
+ (0, chai_1.expect)(body.samples.map((s) => s.app)).to.deep.equal(['com.a.one', 'com.b.two']);
73
+ });
74
+ });
75
+ describe('GET /session/:sessionId/metrics', function () {
76
+ this.timeout(60000);
77
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
78
+ function app() {
79
+ const a = (0, express_1.default)();
80
+ a.use((req, _res, next) => {
81
+ req.auth = {
82
+ kind: 'user',
83
+ userId: 'sa',
84
+ role: 'SUPER_ADMIN',
85
+ scopes: ['admin', 'devices', 'sessions', 'read'],
86
+ teamIds: undefined,
87
+ };
88
+ next();
89
+ });
90
+ dashboard_1.default.register(a);
91
+ return a;
92
+ }
93
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
94
+ yield scratch.db.sessionMetric.deleteMany({});
95
+ yield scratch.db.session.deleteMany({});
96
+ }));
97
+ it("answers a session's samples", () => __awaiter(this, void 0, void 0, function* () {
98
+ yield scratch.db.session.create({
99
+ data: {
100
+ id: 'metrics-1',
101
+ desired_capabilities: '{}',
102
+ session_capabilities: '{}',
103
+ node_id: 'n',
104
+ has_live_video: false,
105
+ device_udid: 'phone-m',
106
+ device_platform: 'android',
107
+ device_version: '10',
108
+ },
109
+ });
110
+ yield scratch.db.sessionMetric.createMany({
111
+ data: [
112
+ Object.assign({ session_id: 'metrics-1' }, row(2000)),
113
+ Object.assign({ session_id: 'metrics-1' }, row(0)),
114
+ ],
115
+ });
116
+ const res = yield (0, supertest_1.default)(app()).get('/session/metrics-1/metrics');
117
+ (0, chai_1.expect)(res.status).to.equal(200);
118
+ (0, chai_1.expect)(res.body.platform).to.equal('android');
119
+ (0, chai_1.expect)(res.body.samples.map((s) => s.t)).to.deep.equal([0, 2000]);
120
+ (0, chai_1.expect)(res.body.series.appMem).to.equal(true);
121
+ }));
122
+ it('says a running session nothing samples is not recorded', () => __awaiter(this, void 0, void 0, function* () {
123
+ yield scratch.db.session.create({
124
+ data: {
125
+ id: 'metrics-2',
126
+ status: 'running',
127
+ desired_capabilities: '{}',
128
+ session_capabilities: '{}',
129
+ node_id: 'n',
130
+ has_live_video: false,
131
+ device_udid: 'sim-1',
132
+ device_platform: 'ios',
133
+ device_version: '18.0',
134
+ },
135
+ });
136
+ const res = yield (0, supertest_1.default)(app()).get('/session/metrics-2/metrics');
137
+ (0, chai_1.expect)(res.status).to.equal(200);
138
+ (0, chai_1.expect)(res.body.recording).to.equal('off');
139
+ }));
140
+ });
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const chai_1 = require("chai");
16
+ const events_1 = require("events");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const AndroidMetricsSampler_1 = require("../../src/services/metrics/AndroidMetricsSampler");
19
+ const IOSMetricsSampler_1 = require("../../src/services/metrics/IOSMetricsSampler");
20
+ const types_1 = require("../../src/services/metrics/types");
21
+ const reading = (jiffies, idle, app) => [
22
+ `cpu ${jiffies - idle} 0 0 ${idle} 0 0 0 0 0 0`,
23
+ 'MemTotal: 5755748 kB',
24
+ 'MemAvailable: 2839168 kB',
25
+ 'pid=1404',
26
+ `stat=${app} 0`,
27
+ 'VmRSS:\t 334480 kB',
28
+ ].join('\n');
29
+ describe('session metrics: AndroidMetricsSampler', () => {
30
+ let clock;
31
+ let samples;
32
+ let gaveUp;
33
+ const hooks = () => ({
34
+ onSample: (s) => samples.push(s),
35
+ onGiveUp: (r) => gaveUp.push(r),
36
+ });
37
+ beforeEach(() => {
38
+ clock = sinon_1.default.useFakeTimers({ now: 1000000 });
39
+ samples = [];
40
+ gaveUp = [];
41
+ });
42
+ afterEach(() => clock.restore());
43
+ it("samples the session's app every 2 s, with CPU from the second sample", () => __awaiter(void 0, void 0, void 0, function* () {
44
+ const commands = [];
45
+ let n = 0;
46
+ const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
47
+ appPackage: 'com.acme.shop',
48
+ hooks: hooks(),
49
+ shell: (c) => __awaiter(void 0, void 0, void 0, function* () {
50
+ commands.push(c);
51
+ n += 1;
52
+ return reading(1000 * n, 800 * n, 50 * n);
53
+ }),
54
+ });
55
+ s.start();
56
+ yield clock.tickAsync(0);
57
+ yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS);
58
+ yield s.stop();
59
+ (0, chai_1.expect)(commands).to.have.length(2);
60
+ (0, chai_1.expect)(commands.every((c) => c.includes('pidof com.acme.shop'))).to.equal(true);
61
+ (0, chai_1.expect)(samples.map((x) => x.deviceCpuPct)).to.deep.equal([null, 20]);
62
+ (0, chai_1.expect)(samples.map((x) => x.appCpuPct)).to.deep.equal([null, 5]);
63
+ (0, chai_1.expect)(samples[1].appId).to.equal('com.acme.shop');
64
+ }));
65
+ it('with no appPackage, samples the foreground app, looked up once per 10 s', () => __awaiter(void 0, void 0, void 0, function* () {
66
+ const commands = [];
67
+ const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
68
+ appPackage: undefined,
69
+ hooks: hooks(),
70
+ shell: (c) => __awaiter(void 0, void 0, void 0, function* () {
71
+ commands.push(c);
72
+ return c.startsWith('dumpsys')
73
+ ? ' topResumedActivity=ActivityRecord{a1 u0 com.acme.shop/.Main t3}'
74
+ : reading(1000, 800, 50);
75
+ }),
76
+ });
77
+ s.start();
78
+ yield clock.tickAsync(0);
79
+ yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS);
80
+ yield clock.tickAsync(types_1.FOREGROUND_REFRESH_MS);
81
+ yield s.stop();
82
+ const lookups = commands.filter((c) => c.startsWith('dumpsys'));
83
+ (0, chai_1.expect)(lookups).to.have.length(2);
84
+ (0, chai_1.expect)(commands.filter((c) => c.includes('pidof com.acme.shop')).length).to.be.greaterThan(2);
85
+ }));
86
+ it("never puts an appPackage that isn't a package name into a command", () => __awaiter(void 0, void 0, void 0, function* () {
87
+ const commands = [];
88
+ const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
89
+ appPackage: 'com.acme;reboot',
90
+ hooks: hooks(),
91
+ shell: (c) => __awaiter(void 0, void 0, void 0, function* () {
92
+ commands.push(c);
93
+ return c.startsWith('dumpsys') ? '' : reading(1000, 800, 50);
94
+ }),
95
+ });
96
+ s.start();
97
+ yield clock.tickAsync(0);
98
+ yield s.stop();
99
+ (0, chai_1.expect)(commands.some((c) => c.includes('reboot'))).to.equal(false);
100
+ (0, chai_1.expect)(samples[0].appId).to.equal(null);
101
+ }));
102
+ it('stops itself after 5 failures in a row, and says so once', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ let calls = 0;
104
+ const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
105
+ appPackage: 'com.acme.shop',
106
+ hooks: hooks(),
107
+ shell: () => __awaiter(void 0, void 0, void 0, function* () {
108
+ calls += 1;
109
+ throw new Error('device offline');
110
+ }),
111
+ });
112
+ s.start();
113
+ yield clock.tickAsync(0);
114
+ yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * (types_1.MAX_CONSECUTIVE_FAILURES + 3));
115
+ (0, chai_1.expect)(calls).to.equal(types_1.MAX_CONSECUTIVE_FAILURES);
116
+ (0, chai_1.expect)(gaveUp).to.deep.equal(['device offline']);
117
+ }));
118
+ it('starts counting failures again after a good sample', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ let n = 0;
120
+ const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
121
+ appPackage: 'com.acme.shop',
122
+ hooks: hooks(),
123
+ shell: () => __awaiter(void 0, void 0, void 0, function* () {
124
+ n += 1;
125
+ if (n % 4 === 0)
126
+ return reading(1000 * n, 800 * n, 50 * n);
127
+ throw new Error('flaky');
128
+ }),
129
+ });
130
+ s.start();
131
+ yield clock.tickAsync(0);
132
+ yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * 12);
133
+ yield s.stop();
134
+ (0, chai_1.expect)(gaveUp).to.deep.equal([]);
135
+ (0, chai_1.expect)(samples.length).to.equal(3);
136
+ }));
137
+ it('reports nothing that was in flight when it stopped', () => __awaiter(void 0, void 0, void 0, function* () {
138
+ let release = () => undefined;
139
+ const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
140
+ appPackage: 'com.acme.shop',
141
+ hooks: hooks(),
142
+ shell: () => new Promise((r) => (release = r)),
143
+ });
144
+ s.start();
145
+ yield clock.tickAsync(0);
146
+ yield s.stop();
147
+ release(reading(1000, 800, 50));
148
+ yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * 3);
149
+ (0, chai_1.expect)(samples).to.deep.equal([]);
150
+ }));
151
+ });
152
+ class FakeChild extends events_1.EventEmitter {
153
+ constructor() {
154
+ super(...arguments);
155
+ this.stdout = new events_1.EventEmitter();
156
+ this.stderr = new events_1.EventEmitter();
157
+ this.pid = 4321;
158
+ this.killed = [];
159
+ }
160
+ kill(sig) {
161
+ this.killed.push(sig);
162
+ return true;
163
+ }
164
+ }
165
+ describe('session metrics: IOSMetricsSampler', () => {
166
+ let clock;
167
+ let samples;
168
+ let gaveUp;
169
+ let children;
170
+ let envs;
171
+ let borrows;
172
+ const line = (load) => JSON.stringify({ msg: 'received CPU usage data', enabled_cpus: 6, cpu_total_load: load }) +
173
+ '\n';
174
+ function sampler() {
175
+ return new IOSMetricsSampler_1.IOSMetricsSampler({
176
+ udid: 'iphone-1',
177
+ tunnels: {
178
+ borrow: () => __awaiter(this, void 0, void 0, function* () {
179
+ borrows += 1;
180
+ return 12100;
181
+ }),
182
+ envFor: () => ({ GO_IOS_AGENT_PORT: '12100' }),
183
+ },
184
+ spawnSysmontap: (env) => {
185
+ envs.push(env);
186
+ const c = new FakeChild();
187
+ children.push(c);
188
+ return c;
189
+ },
190
+ hooks: { onSample: (s) => samples.push(s), onGiveUp: (r) => gaveUp.push(r) },
191
+ });
192
+ }
193
+ beforeEach(() => {
194
+ clock = sinon_1.default.useFakeTimers({ now: 1000000 });
195
+ samples = [];
196
+ gaveUp = [];
197
+ children = [];
198
+ envs = [];
199
+ borrows = 0;
200
+ });
201
+ afterEach(() => clock.restore());
202
+ it("runs sysmontap through the phone's tunnel and reports device CPU every 2 s", () => __awaiter(void 0, void 0, void 0, function* () {
203
+ const s = sampler();
204
+ s.start();
205
+ yield clock.tickAsync(0);
206
+ (0, chai_1.expect)(envs).to.deep.equal([{ GO_IOS_AGENT_PORT: '12100' }]);
207
+ children[0].stderr.emit('data', line(60) + line(120));
208
+ yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS);
209
+ yield s.stop();
210
+ (0, chai_1.expect)(samples).to.have.length(1);
211
+ (0, chai_1.expect)(samples[0]).to.include({
212
+ deviceCpuPct: 20,
213
+ deviceMemMb: null,
214
+ appCpuPct: null,
215
+ appMemMb: null,
216
+ appId: null,
217
+ });
218
+ (0, chai_1.expect)(children[0].killed).to.deep.equal(['SIGTERM']);
219
+ }));
220
+ it('reports nothing when no reading came for 4 s', () => __awaiter(void 0, void 0, void 0, function* () {
221
+ const s = sampler();
222
+ s.start();
223
+ yield clock.tickAsync(0);
224
+ children[0].stdout.emit('data', line(60));
225
+ yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * 4);
226
+ yield s.stop();
227
+ (0, chai_1.expect)(samples.length).to.be.lessThan(3);
228
+ }));
229
+ it('asks for the tunnel again every 30 s', () => __awaiter(void 0, void 0, void 0, function* () {
230
+ const s = sampler();
231
+ s.start();
232
+ yield clock.tickAsync(0);
233
+ yield clock.tickAsync(types_1.TUNNEL_RENEW_MS * 2);
234
+ yield s.stop();
235
+ (0, chai_1.expect)(borrows).to.equal(3);
236
+ }));
237
+ it('starts sysmontap again when it exits, and gives up after 5 exits with no reading', () => __awaiter(void 0, void 0, void 0, function* () {
238
+ const s = sampler();
239
+ s.start();
240
+ for (let i = 0; i < types_1.MAX_CONSECUTIVE_FAILURES; i += 1) {
241
+ yield clock.tickAsync(0);
242
+ children[children.length - 1].emit('exit', 1);
243
+ yield clock.tickAsync(30000);
244
+ }
245
+ (0, chai_1.expect)(children).to.have.length(types_1.MAX_CONSECUTIVE_FAILURES);
246
+ (0, chai_1.expect)(gaveUp).to.deep.equal(['ios sysmontap kept exiting']);
247
+ }));
248
+ });