@xenon-device-management/xenon 2.9.2 → 2.10.1
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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-B87n4pUu.js → AnnotationOverlay-defSfi9B.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-DS7dJ2bU.js} +1 -1
- package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-CeD8SUnQ.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-Dgue7t7s.js} +2 -2
- package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-Bo-vvxiy.js} +1 -1
- package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-DdhTNAXZ.js} +1 -1
- package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-J3F_3fDw.js} +1 -1
- package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Bxluaw2m.js} +1 -1
- package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-qgMVFrqV.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-BUT7WgJB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CY0OIgCo.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-Cq-fybZ2.js} +1 -1
- package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-CASWJr7h.js} +1 -1
- package/lib/public/assets/{Table-D_2lhvRM.js → Table-BSO_dZzu.js} +1 -1
- package/lib/public/assets/{activity-cNHHOKx5.js → activity-B_-MNwwR.js} +1 -1
- package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DFhemyo8.js} +1 -1
- package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-DV1oB8Tf.js} +1 -1
- package/lib/public/assets/{apps-DCiI3UOb.js → apps-B5f-VuLN.js} +1 -1
- package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-D_xIcVqg.js} +1 -1
- package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-CLGlL_Un.js} +1 -1
- package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-CuIgWFj2.js} +1 -1
- package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-SgAkrFQg.js} +2 -2
- package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-h9ZJ8n74.js} +4 -4
- package/lib/public/assets/{button-C0caCjJB.js → button-DKl94fzV.js} +1 -1
- package/lib/public/assets/{calendar-BqOOkln_.js → calendar-BNLTjBsT.js} +1 -1
- package/lib/public/assets/{check-DYSXRG79.js → check-1u_3Zsx5.js} +1 -1
- package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-Cn2BixQX.js} +1 -1
- package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-OymVCeGU.js} +1 -1
- package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-D3ZyFWOd.js} +1 -1
- package/lib/public/assets/{clock-XP3iBAhg.js → clock-DwzGN-aa.js} +1 -1
- package/lib/public/assets/{copy-D8d9UzWE.js → copy-De43WY6T.js} +1 -1
- package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-CtvEmCC5.js} +5 -5
- package/lib/public/assets/{download-SaN9-umX.js → download-DcSdKSvR.js} +1 -1
- package/lib/public/assets/{forgot-password-DfUhEZW5.js → forgot-password-DDjM8J7Z.js} +1 -1
- package/lib/public/assets/{index-CzaIyL9t.js → index-Bqt1dmMA.js} +2 -2
- package/lib/public/assets/{index-IefGCFQQ.js → index-CSNs0EE4.js} +1 -1
- package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
- package/lib/public/assets/{input-BqFzAQT3.js → input-fRGFwm1K.js} +1 -1
- package/lib/public/assets/{keycodes-DDV2OlpQ.js → keycodes-Dj4m3JV7.js} +10 -5
- package/lib/public/assets/line-chart-DHSZ8Sm4.js +1 -0
- package/lib/public/assets/list-checks-Co1gqqDn.js +11 -0
- package/lib/public/assets/{lock-Eye1CG8p.js → lock-BzutP2Yf.js} +1 -1
- package/lib/public/assets/{login-Co1IXuen.js → login-DSVZLgW2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-PYFNp_tc.js} +1 -1
- package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-DdKVA3ze.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-C8UpXByt.js} +1 -1
- package/lib/public/assets/{network-BP_U4iDZ.js → network-BkzHrr2L.js} +1 -1
- package/lib/public/assets/overview-0YFo6f97.js +6 -0
- package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DkmHEtLJ.js} +1 -1
- package/lib/public/assets/{plus-1k0sxXyU.js → plus-BH6Y7G7y.js} +1 -1
- package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-C1IDBP4l.js} +1 -1
- package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-yNMsl5fZ.js} +1 -1
- package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page--fH5nToN.js} +1 -1
- package/lib/public/assets/{select-CiRW6v17.js → select-DqjhZ6h2.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-CrYIFrUj.js +1 -0
- package/lib/public/assets/selector-health-page-BnRUmiS2.js +11 -0
- package/lib/public/assets/session-detail-page-BNh_QfHv.js +4 -0
- package/lib/public/assets/{settings-DhwZoWv7.js → settings-BcxYFxnE.js} +1 -1
- package/lib/public/assets/stat-tile-DxVLBgA7.js +1 -0
- package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-C_uKWfUK.js} +1 -1
- package/lib/public/assets/{teams-D0IBa2Ov.js → teams-C9G2WisD.js} +1 -1
- package/lib/public/assets/time-D-gjmE1o.js +1 -0
- package/lib/public/assets/{trash-2-u8MlXUBD.js → trash-2-C4cJO0CG.js} +1 -1
- package/lib/public/assets/{upload-Cq8L3kpw.js → upload-BN3PMMvP.js} +1 -1
- package/lib/public/assets/use-builds-data-CtelVuo_.js +8 -0
- package/lib/public/assets/{use-password-reset-mode-Dbr2HEfw.js → use-password-reset-mode-CClcmiRh.js} +1 -1
- package/lib/public/assets/{users-DxLHYmFm.js → users-BK7fwGGA.js} +1 -1
- package/lib/public/assets/{users-CZqCE511.js → users-OdnOb63Y.js} +1 -1
- package/lib/public/assets/{video-off-mh1EEzP9.js → video-off-DUAYoXTQ.js} +1 -1
- package/lib/public/assets/view-state-83Z1gFME.js +1 -0
- package/lib/public/assets/{webhook-settings-BYuOib-Q.js → webhook-settings-BRyvDWHI.js} +1 -1
- package/lib/public/assets/{zap-BOpUur0M.js → zap-BiceX7DC.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +5 -0
- package/lib/src/app/routers/dashboard.js +86 -40
- package/lib/src/app/routers/selector-health.js +75 -0
- package/lib/src/app/swagger-docs.js +77 -8
- package/lib/src/dashboard/event-manager.js +15 -99
- package/lib/src/generated/client/edge.js +27 -3
- package/lib/src/generated/client/index-browser.js +24 -0
- package/lib/src/generated/client/index.d.ts +8207 -5283
- package/lib/src/generated/client/index.js +27 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +49 -9
- package/lib/src/generated/client/wasm.js +24 -0
- package/lib/src/prisma.js +2 -0
- package/lib/src/services/CleanupService.js +1 -0
- package/lib/src/services/NotificationService.js +3 -4
- package/lib/src/services/SelectorStateService.js +151 -113
- package/lib/src/services/SelectorVerificationJob.js +64 -14
- package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
- package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
- package/lib/src/services/metrics/SessionMetricsService.js +238 -0
- package/lib/src/services/metrics/androidMetrics.js +94 -0
- package/lib/src/services/metrics/iosMetrics.js +26 -0
- package/lib/src/services/metrics/metricsBody.js +37 -0
- package/lib/src/services/metrics/types.js +19 -0
- package/lib/src/services/selector-health/access.js +66 -0
- package/lib/src/services/selector-health/healingTrend.js +49 -0
- package/lib/src/services/selector-health/people.js +27 -0
- package/lib/src/services/selector-health/selectorDetail.js +214 -0
- package/lib/src/services/selector-health/selectorKeys.js +36 -0
- package/lib/src/services/selector-health/selectorList.js +333 -0
- package/lib/src/services/selector-health/stateView.js +67 -0
- package/lib/test/helpers/scratch-database.js +19 -3
- package/lib/test/helpers/selector-health-fixture.js +216 -0
- package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
- package/lib/test/unit/CleanupService.spec.js +28 -2
- package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
- package/lib/test/unit/healing-no-cost.spec.js +134 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
- package/lib/test/unit/healing-trend.spec.js +118 -0
- package/lib/test/unit/selector-event-model.spec.js +47 -0
- package/lib/test/unit/selector-health-actions.spec.js +106 -0
- package/lib/test/unit/selector-health-detail.spec.js +188 -0
- package/lib/test/unit/selector-health-list.spec.js +185 -0
- package/lib/test/unit/selector-state-service.spec.js +74 -1
- package/lib/test/unit/selector-verification-job.spec.js +41 -0
- package/lib/test/unit/selector-verification-missed-regression.spec.js +132 -0
- package/lib/test/unit/session-log-indexes.spec.js +147 -0
- package/lib/test/unit/session-metrics-parse.spec.js +138 -0
- package/lib/test/unit/session-metrics-route.spec.js +140 -0
- package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
- package/lib/test/unit/session-metrics-service.spec.js +215 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
- package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
- package/prisma/migrations/20261002180000_session_log_indexes/migration.sql +5 -0
- package/prisma/migrations/20261002190000_log_profiling_session_indexes/migration.sql +5 -0
- package/prisma/schema.prisma +49 -9
- package/schema.json +5 -0
- package/lib/public/assets/chevron-left-BSG6mcpo.js +0 -6
- package/lib/public/assets/funnel-OefHE-54.js +0 -6
- package/lib/public/assets/layers-a3GNz2DA.js +0 -6
- package/lib/public/assets/list-checks-DMrMAFDt.js +0 -6
- package/lib/public/assets/overview-CsPyPv10.js +0 -1
- package/lib/public/assets/selector-detail-page-pA913STP.js +0 -1
- package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
- package/lib/public/assets/selector-health-CHLX8KqY.js +0 -16
- package/lib/public/assets/selector-health-page-vVkg8Mii.js +0 -16
- package/lib/public/assets/session-detail-page-BCD49zDi.js +0 -4
- package/lib/public/assets/stat-tile-BaE76cnK.js +0 -8
- package/lib/public/assets/time-B8FIgwPE.js +0 -1
- package/lib/src/profiling/AndroidAppProfiler.js +0 -213
- /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
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 sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
19
|
+
const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
|
|
20
|
+
const HOUR = 60 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* A heal of a selector someone marked fixed turns it back to "To fix" at once
|
|
23
|
+
* (SelectorStateService.onHealRecorded, called after the heal is written). That
|
|
24
|
+
* call can fail, say on a busy database, and nothing tried again: the selector
|
|
25
|
+
* stayed "being verified", and was even promoted to fixed once three other
|
|
26
|
+
* builds ran clean. The verification job now catches such a heal.
|
|
27
|
+
*/
|
|
28
|
+
describe('SelectorVerificationJob catches a heal the live check missed', function () {
|
|
29
|
+
this.timeout(90000);
|
|
30
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
31
|
+
let socket;
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const fixedAt = new Date(now - 48 * HOUR);
|
|
34
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
for (const b of ['mr-b1', 'mr-b2', 'mr-b3', 'mr-b4']) {
|
|
36
|
+
yield scratch.db.build.create({ data: { id: b, name: b } });
|
|
37
|
+
yield scratch.db.session.create({
|
|
38
|
+
data: {
|
|
39
|
+
id: `mr-s-${b}`,
|
|
40
|
+
build_id: b,
|
|
41
|
+
device_udid: 'mr-phone',
|
|
42
|
+
device_platform: 'android',
|
|
43
|
+
device_version: '14',
|
|
44
|
+
desired_capabilities: '{}',
|
|
45
|
+
session_capabilities: '{}',
|
|
46
|
+
node_id: 'localhost',
|
|
47
|
+
has_live_video: false,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
yield scratch.db.selectorEvent.deleteMany();
|
|
54
|
+
yield scratch.db.selectorState.deleteMany();
|
|
55
|
+
yield scratch.db.sessionLog.deleteMany();
|
|
56
|
+
socket = { emitToDashboard: sinon_1.default.stub() };
|
|
57
|
+
}));
|
|
58
|
+
const find = (build, selector, healed, at) => scratch.db.sessionLog.create({
|
|
59
|
+
data: {
|
|
60
|
+
session_id: `mr-s-${build}`,
|
|
61
|
+
command_name: 'findElement',
|
|
62
|
+
url: '/element',
|
|
63
|
+
method: 'POST',
|
|
64
|
+
title: 'Find element',
|
|
65
|
+
response: '{}',
|
|
66
|
+
is_healed: healed,
|
|
67
|
+
original_strategy: 'xpath',
|
|
68
|
+
original_selector: selector,
|
|
69
|
+
createdAt: new Date(at),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const state = (selector, status) => scratch.db.selectorState.create({
|
|
73
|
+
data: {
|
|
74
|
+
original_strategy: 'xpath',
|
|
75
|
+
original_selector: selector,
|
|
76
|
+
status,
|
|
77
|
+
fixed_at: fixedAt,
|
|
78
|
+
resolved_at: status === 'resolved' ? new Date(now - 24 * HOUR) : null,
|
|
79
|
+
clean_builds_count: status === 'resolved' ? 3 : 0,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const run = () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run();
|
|
83
|
+
const rowOf = (selector) => scratch.db.selectorState.findUniqueOrThrow({
|
|
84
|
+
where: {
|
|
85
|
+
original_strategy_original_selector: {
|
|
86
|
+
original_strategy: 'xpath',
|
|
87
|
+
original_selector: selector,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const actionsOf = (selector) => __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
return (yield scratch.db.selectorEvent.findMany({
|
|
93
|
+
where: { original_selector: selector },
|
|
94
|
+
orderBy: { createdAt: 'asc' },
|
|
95
|
+
})).map((e) => e.action);
|
|
96
|
+
});
|
|
97
|
+
it('sends a selector being verified back to fix, rather than promoting it', () => __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
yield state('//pending/broke', 'pending');
|
|
99
|
+
// Three clean builds since it was marked fixed, and one that healed.
|
|
100
|
+
for (const b of ['mr-b1', 'mr-b2', 'mr-b3'])
|
|
101
|
+
yield find(b, '//pending/broke', false, now - HOUR);
|
|
102
|
+
yield find('mr-b4', '//pending/broke', true, now - 2 * HOUR);
|
|
103
|
+
yield run();
|
|
104
|
+
const row = yield rowOf('//pending/broke');
|
|
105
|
+
(0, chai_1.expect)(row).to.include({ status: 'active', regression_count: 1, clean_builds_count: 0 });
|
|
106
|
+
(0, chai_1.expect)(row.fixed_at).to.equal(null);
|
|
107
|
+
(0, chai_1.expect)(yield actionsOf('//pending/broke')).to.deep.equal(['broke_again']);
|
|
108
|
+
(0, chai_1.expect)(socket.emitToDashboard.firstCall.args[0]).to.equal('selector_regressed');
|
|
109
|
+
}));
|
|
110
|
+
it('sends a fixed selector back to fix', () => __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
yield state('//resolved/broke', 'resolved');
|
|
112
|
+
yield find('mr-b1', '//resolved/broke', true, now - HOUR);
|
|
113
|
+
yield run();
|
|
114
|
+
const row = yield rowOf('//resolved/broke');
|
|
115
|
+
(0, chai_1.expect)(row).to.include({ status: 'active', regression_count: 1 });
|
|
116
|
+
(0, chai_1.expect)(row.resolved_at).to.equal(null);
|
|
117
|
+
(0, chai_1.expect)(yield actionsOf('//resolved/broke')).to.deep.equal(['broke_again']);
|
|
118
|
+
}));
|
|
119
|
+
it('leaves alone a selector that healed only before it was marked fixed', () => __awaiter(this, void 0, void 0, function* () {
|
|
120
|
+
yield state('//pending/clean', 'pending');
|
|
121
|
+
yield state('//resolved/clean', 'resolved');
|
|
122
|
+
yield find('mr-b1', '//pending/clean', true, fixedAt.getTime() - HOUR);
|
|
123
|
+
yield find('mr-b1', '//resolved/clean', true, fixedAt.getTime() - HOUR);
|
|
124
|
+
for (const b of ['mr-b1', 'mr-b2', 'mr-b3'])
|
|
125
|
+
yield find(b, '//pending/clean', false, now - HOUR);
|
|
126
|
+
yield run();
|
|
127
|
+
(0, chai_1.expect)((yield rowOf('//pending/clean')).status).to.equal('resolved');
|
|
128
|
+
(0, chai_1.expect)((yield rowOf('//resolved/clean')).status).to.equal('resolved');
|
|
129
|
+
(0, chai_1.expect)(yield actionsOf('//pending/clean')).to.deep.equal(['verified']);
|
|
130
|
+
(0, chai_1.expect)(yield actionsOf('//resolved/clean')).to.deep.equal([]);
|
|
131
|
+
}));
|
|
132
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
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 supertest_1 = __importDefault(require("supertest"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const CleanupService_1 = require("../../src/services/CleanupService");
|
|
20
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
21
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
22
|
+
/** The SQL statements `run` makes. */
|
|
23
|
+
function sqlOf(db, run) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
db.queries.length = 0;
|
|
26
|
+
yield run();
|
|
27
|
+
return db.queries.slice();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** How SQLite reads `table` in these statements: one line per step. */
|
|
31
|
+
function stepsOn(db, table, queries) {
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
var _a;
|
|
34
|
+
const steps = [];
|
|
35
|
+
for (const q of queries) {
|
|
36
|
+
if (!q.query.includes(`\`${table}\``))
|
|
37
|
+
continue;
|
|
38
|
+
// SQLite plans without looking at the values, and Prisma's logged
|
|
39
|
+
// parameters aren't JSON, so every placeholder gets null.
|
|
40
|
+
const placeholders = ((_a = q.query.match(/\?/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
41
|
+
const plan = yield db.db.$queryRawUnsafe(`EXPLAIN QUERY PLAN ${q.query}`, ...new Array(placeholders).fill(null));
|
|
42
|
+
steps.push(...plan.map((p) => p.detail).filter((d) => new RegExp(`\\b${table}\\b`).test(d)));
|
|
43
|
+
}
|
|
44
|
+
return steps;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** The steps that walk the whole table, or a whole index of it. */
|
|
48
|
+
const scansOf = (table, steps) => steps.filter((s) => new RegExp(`^SCAN (main\\.)?${table}\\b`).test(s));
|
|
49
|
+
const getAs = (auth, path) => () => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
const res = yield (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(auth)).get(path);
|
|
51
|
+
(0, chai_1.expect)(res.status, `${path}: ${res.text}`).to.equal(200);
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* SessionLog holds every command of every session, so a read that scans it
|
|
55
|
+
* costs as much as the lab's whole history: on a million commands, the
|
|
56
|
+
* "To fix" list took 2.5 s and a session's own commands 80 ms. Each read
|
|
57
|
+
* these pages make must find its rows through an index.
|
|
58
|
+
*/
|
|
59
|
+
describe('SessionLog is read through an index', function () {
|
|
60
|
+
this.timeout(30000);
|
|
61
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)({ captureQueries: true });
|
|
62
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
|
|
64
|
+
}));
|
|
65
|
+
const cases = [
|
|
66
|
+
['the summary', selector_health_fixture_1.ADMIN, '/healing/summary?windowDays=365&tz=330'],
|
|
67
|
+
["a member's summary", selector_health_fixture_1.MEMBER_A, '/healing/summary?windowDays=30'],
|
|
68
|
+
['"To fix"', selector_health_fixture_1.ADMIN, '/healing/selectors?tab=fix&days=365'],
|
|
69
|
+
['"To fix" with a search', selector_health_fixture_1.ADMIN, '/healing/selectors?tab=fix&days=30&q=confirm'],
|
|
70
|
+
['"To fix" on one platform', selector_health_fixture_1.ADMIN, '/healing/selectors?tab=fix&days=30&platform=android'],
|
|
71
|
+
['a member\'s "To fix"', selector_health_fixture_1.MEMBER_A, '/healing/selectors?tab=fix&days=30'],
|
|
72
|
+
['"Fixed"', selector_health_fixture_1.ADMIN, '/healing/selectors?tab=fixed&days=30'],
|
|
73
|
+
['a member\'s "Fixed"', selector_health_fixture_1.MEMBER_A, '/healing/selectors?tab=fixed&days=30'],
|
|
74
|
+
[
|
|
75
|
+
'the panel',
|
|
76
|
+
selector_health_fixture_1.ADMIN,
|
|
77
|
+
`/healing/selectors/detail?strategy=xpath&selector=${encodeURIComponent(selector_health_fixture_1.SEL.hot)}&days=365`,
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
"a member's panel",
|
|
81
|
+
selector_health_fixture_1.MEMBER_A,
|
|
82
|
+
`/healing/selectors/detail?strategy=xpath&selector=${encodeURIComponent(selector_health_fixture_1.SEL.hot)}&days=30`,
|
|
83
|
+
],
|
|
84
|
+
["a session's commands", selector_health_fixture_1.ADMIN, '/session/sh-s-shared-1/session_log'],
|
|
85
|
+
];
|
|
86
|
+
for (const [name, auth, path] of cases) {
|
|
87
|
+
it(`reads ${name} without a scan`, () => __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
const steps = yield stepsOn(scratch, 'SessionLog', yield sqlOf(scratch, getAs(auth, path)));
|
|
89
|
+
(0, chai_1.expect)(steps, 'no SessionLog statement was captured').to.not.be.empty;
|
|
90
|
+
(0, chai_1.expect)(scansOf('SessionLog', steps), steps.join('\n')).to.deep.equal([]);
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* A session's device logs (hundreds of lines a session) and the profiling
|
|
96
|
+
* rows sessions wrote before 2.10 are read by session on every session page,
|
|
97
|
+
* and deleted by session in cleanup. Neither table had an index on it.
|
|
98
|
+
*/
|
|
99
|
+
describe("A session's logs and profiling are read through an index", function () {
|
|
100
|
+
this.timeout(30000);
|
|
101
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)({ captureQueries: true });
|
|
102
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
103
|
+
yield scratch.db.build.create({ data: { id: 'ix-build', name: 'ix' } });
|
|
104
|
+
yield scratch.db.session.create({
|
|
105
|
+
data: {
|
|
106
|
+
id: 'ix-session',
|
|
107
|
+
build_id: 'ix-build',
|
|
108
|
+
device_udid: 'ix-phone',
|
|
109
|
+
device_platform: 'android',
|
|
110
|
+
device_version: '14',
|
|
111
|
+
desired_capabilities: '{}',
|
|
112
|
+
session_capabilities: '{}',
|
|
113
|
+
node_id: 'localhost',
|
|
114
|
+
has_live_video: false,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
yield scratch.db.log.createMany({
|
|
118
|
+
data: [
|
|
119
|
+
{ session_id: 'ix-session', log_type: 'DEVICE', message: 'I/ActivityManager: start' },
|
|
120
|
+
{ session_id: 'ix-session', log_type: 'DEBUG', message: 'step 1' },
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
yield scratch.db.profiling.create({
|
|
124
|
+
data: { session_id: 'ix-session', cpu: '12', memory: '200', timestamp: new Date() },
|
|
125
|
+
});
|
|
126
|
+
}));
|
|
127
|
+
const reads = [
|
|
128
|
+
['device logs', 'Log', '/session/ix-session/logs/device'],
|
|
129
|
+
['debug logs', 'Log', '/session/ix-session/logs/debug'],
|
|
130
|
+
['profiling', 'Profiling', '/session/ix-session/profiling'],
|
|
131
|
+
];
|
|
132
|
+
for (const [name, table, path] of reads) {
|
|
133
|
+
it(`reads a session's ${name} without a scan`, () => __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
const steps = yield stepsOn(scratch, table, yield sqlOf(scratch, getAs(selector_health_fixture_1.ADMIN, path)));
|
|
135
|
+
(0, chai_1.expect)(steps, `no ${table} statement was captured`).to.not.be.empty;
|
|
136
|
+
(0, chai_1.expect)(scansOf(table, steps), steps.join('\n')).to.deep.equal([]);
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
it("deletes a session's commands, logs and profiling without a scan", () => __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
const ran = yield sqlOf(scratch, () => typedi_1.Container.get(CleanupService_1.CleanupService).purgeBuild('ix-build', false));
|
|
141
|
+
for (const table of ['SessionLog', 'Log', 'Profiling']) {
|
|
142
|
+
const steps = yield stepsOn(scratch, table, ran);
|
|
143
|
+
(0, chai_1.expect)(steps, `no ${table} statement was captured`).to.not.be.empty;
|
|
144
|
+
(0, chai_1.expect)(scansOf(table, steps), `${table}:\n${steps.join('\n')}`).to.deep.equal([]);
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
});
|
|
@@ -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
|
+
});
|