@xenon-device-management/xenon 2.10.1 → 2.11.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.
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-defSfi9B.js → AnnotationOverlay-BQPQ5bPF.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DS7dJ2bU.js → ApiKeyGate-Bd7Pumwv.js} +1 -1
- package/lib/public/assets/{BugReportButton-CeD8SUnQ.js → BugReportButton-iC7fxPxl.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Dgue7t7s.js → DeviceMosaicView-CNdES7nm.js} +1 -1
- package/lib/public/assets/{EmptyState-Bo-vvxiy.js → EmptyState-wZeJbGhN.js} +1 -1
- package/lib/public/assets/{FieldGroup-DdhTNAXZ.js → FieldGroup-tpRfmhlE.js} +1 -1
- package/lib/public/assets/{FilterMenu-J3F_3fDw.js → FilterMenu-BZwOppId.js} +1 -1
- package/lib/public/assets/{Menu-Bxluaw2m.js → Menu-CahoAx8L.js} +1 -1
- package/lib/public/assets/{Modal-qgMVFrqV.js → Modal-mcM5W_Ji.js} +1 -1
- package/lib/public/assets/{RecordingPage-BUT7WgJB.js → RecordingPage-B4dFBndn.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CY0OIgCo.js → RecordingsPage-BOez3f_9.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Cq-fybZ2.js → SegmentedControl-qMSiMN1d.js} +1 -1
- package/lib/public/assets/{SettingCard-CASWJr7h.js → SettingCard-rcPtJ7HG.js} +1 -1
- package/lib/public/assets/{Table-BSO_dZzu.js → Table-J3uognu0.js} +1 -1
- package/lib/public/assets/{activity-B_-MNwwR.js → activity-_5pNSfKT.js} +1 -1
- package/lib/public/assets/{ai-settings-DFhemyo8.js → ai-settings-DTPRbkei.js} +1 -1
- package/lib/public/assets/{api-keys-DV1oB8Tf.js → api-keys-CxkIUo76.js} +1 -1
- package/lib/public/assets/apps-BQKfuN3J.css +1 -0
- package/lib/public/assets/{apps-B5f-VuLN.js → apps-Bv0pAfxz.js} +2 -2
- package/lib/public/assets/{arrow-left-D_xIcVqg.js → arrow-left-uY7F6z1K.js} +1 -1
- package/lib/public/assets/{arrow-right-CLGlL_Un.js → arrow-right-BbAm_2Wa.js} +1 -1
- package/lib/public/assets/{arrow-up-right-CuIgWFj2.js → arrow-up-right-x4H5NuvE.js} +1 -1
- package/lib/public/assets/{auth-shell-SgAkrFQg.js → auth-shell-CtMFhbnC.js} +2 -2
- package/lib/public/assets/{builds-page-h9ZJ8n74.js → builds-page-B_O5uBSZ.js} +1 -1
- package/lib/public/assets/{button-DKl94fzV.js → button-CQoMQEj_.js} +1 -1
- package/lib/public/assets/{calendar-BNLTjBsT.js → calendar-BZkpCqlL.js} +1 -1
- package/lib/public/assets/{check-1u_3Zsx5.js → check-CAB6RYuc.js} +1 -1
- package/lib/public/assets/{chevron-right-Cn2BixQX.js → chevron-right-ec0unu3u.js} +1 -1
- package/lib/public/assets/{circle-check-OymVCeGU.js → circle-check-Dfg-gpjg.js} +1 -1
- package/lib/public/assets/{circle-x-D3ZyFWOd.js → circle-x-cbPq6Btp.js} +1 -1
- package/lib/public/assets/{clock-DwzGN-aa.js → clock-DU9cBLi8.js} +1 -1
- package/lib/public/assets/{copy-De43WY6T.js → copy-VM9kWPpE.js} +1 -1
- package/lib/public/assets/{device-explorer-CtvEmCC5.js → device-explorer-ai0usQ8m.js} +1 -1
- package/lib/public/assets/{download-DcSdKSvR.js → download-WgW4AVid.js} +1 -1
- package/lib/public/assets/{forgot-password-DDjM8J7Z.js → forgot-password-BGRpjTtt.js} +1 -1
- package/lib/public/assets/{index-Bqt1dmMA.js → index-C33GjwPr.js} +2 -2
- package/lib/public/assets/{index-CSNs0EE4.js → index-DNDdHt3h.js} +1 -1
- package/lib/public/assets/{input-fRGFwm1K.js → input-DwiVcO3Q.js} +1 -1
- package/lib/public/assets/{keycodes-Dj4m3JV7.js → keycodes-CvCtpVJD.js} +1 -1
- package/lib/public/assets/{line-chart-DHSZ8Sm4.js → line-chart-PE5e9PeN.js} +1 -1
- package/lib/public/assets/{list-checks-Co1gqqDn.js → list-checks-Bgn9GEEO.js} +1 -1
- package/lib/public/assets/{lock-BzutP2Yf.js → lock-Cb7My6hc.js} +1 -1
- package/lib/public/assets/{login-DSVZLgW2.js → login-CDmyzSpl.js} +1 -1
- package/lib/public/assets/{maintenance-settings-PYFNp_tc.js → maintenance-settings-Bq6pKoBt.js} +1 -1
- package/lib/public/assets/{monitor-DdKVA3ze.js → monitor-BZSwrnPV.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C8UpXByt.js → mouse-pointer-2-BbFj8y8i.js} +1 -1
- package/lib/public/assets/{network-BkzHrr2L.js → network-DpwYGB0-.js} +1 -1
- package/lib/public/assets/{overview-0YFo6f97.js → overview-B4_IO-X8.js} +1 -1
- package/lib/public/assets/{page-header-DkmHEtLJ.js → page-header-BpLEuo7i.js} +1 -1
- package/lib/public/assets/{plus-BH6Y7G7y.js → plus-DHZS0EGU.js} +1 -1
- package/lib/public/assets/{profile-page-C1IDBP4l.js → profile-page-DsHDIq-Y.js} +1 -1
- package/lib/public/assets/{reset-password-yNMsl5fZ.js → reset-password-DuUjhw_l.js} +1 -1
- package/lib/public/assets/{runbook-page--fH5nToN.js → runbook-page-BI_ayvLd.js} +1 -1
- package/lib/public/assets/{select-DqjhZ6h2.js → select-xiba9PSx.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CrYIFrUj.js → selector-detail-redirect-Emz2obK_.js} +1 -1
- package/lib/public/assets/{selector-health-page-BnRUmiS2.js → selector-health-page-zpwrgalC.js} +1 -1
- package/lib/public/assets/session-detail-page-BhlTJ1Oq.js +4 -0
- package/lib/public/assets/{settings-BcxYFxnE.js → settings-B-mabJFm.js} +1 -1
- package/lib/public/assets/{stat-tile-DxVLBgA7.js → stat-tile-lT8WcPgu.js} +1 -1
- package/lib/public/assets/{tablet-C_uKWfUK.js → tablet-DM8dp75P.js} +1 -1
- package/lib/public/assets/{teams-C9G2WisD.js → teams-CRkb5-Fe.js} +1 -1
- package/lib/public/assets/{trash-2-C4cJO0CG.js → trash-2-Dxgryn4o.js} +1 -1
- package/lib/public/assets/{upload-BN3PMMvP.js → upload-Ci0vz0BP.js} +1 -1
- package/lib/public/assets/{use-builds-data-CtelVuo_.js → use-builds-data-DaaqMjjL.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CClcmiRh.js → use-password-reset-mode-BvIyZtaF.js} +1 -1
- package/lib/public/assets/{users-OdnOb63Y.js → users-BT8qx3ea.js} +1 -1
- package/lib/public/assets/{users-BK7fwGGA.js → users-CWYHk75q.js} +1 -1
- package/lib/public/assets/{video-off-DUAYoXTQ.js → video-off-BvY08g6I.js} +1 -1
- package/lib/public/assets/{webhook-settings-BRyvDWHI.js → webhook-settings-aHIlSJFP.js} +1 -1
- package/lib/public/assets/{zap-BiceX7DC.js → zap-qG2Li6rI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/dashboard/event-manager.js +4 -6
- package/lib/src/gateway/nodeSessionMetrics.js +39 -0
- package/lib/src/gateway/nodeSessionStatus.js +32 -23
- package/lib/src/plugin.js +3 -0
- package/lib/src/services/SessionLifecycleService.js +16 -0
- package/lib/src/services/metrics/NodeMetricsCollector.js +114 -0
- package/lib/src/services/metrics/NodeMetricsStore.js +76 -0
- package/lib/src/services/metrics/SessionMetricsService.js +71 -14
- package/lib/src/services/metrics/nodeMetrics.js +92 -0
- package/lib/src/sessions/RemoteSession.js +32 -0
- package/lib/src/sessions/SessionManager.js +63 -0
- package/lib/test/unit/dashboard-events-metrics.spec.js +10 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +330 -0
- package/lib/test/unit/node-metrics-collector.spec.js +226 -0
- package/lib/test/unit/node-metrics-store.spec.js +70 -0
- package/lib/test/unit/node-session-metrics-route.spec.js +97 -0
- package/lib/test/unit/session-metrics-lifecycle.spec.js +55 -0
- package/lib/test/unit/session-metrics-service.spec.js +97 -0
- package/lib/test/unit/session-recovery-metrics.spec.js +94 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/lib/public/assets/apps-DMhpZuKd.css +0 -1
- package/lib/public/assets/session-detail-page-BNh_QfHv.js +0 -4
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("reflect-metadata");
|
|
4
|
+
const chai_1 = require("chai");
|
|
5
|
+
const NodeMetricsStore_1 = require("../../src/services/metrics/NodeMetricsStore");
|
|
6
|
+
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
7
|
+
const types_1 = require("../../src/services/metrics/types");
|
|
8
|
+
const sample = (at) => ({
|
|
9
|
+
at,
|
|
10
|
+
deviceCpuPct: 10,
|
|
11
|
+
deviceMemMb: 100,
|
|
12
|
+
deviceMemTotalMb: 4000,
|
|
13
|
+
appCpuPct: null,
|
|
14
|
+
appMemMb: null,
|
|
15
|
+
appId: null,
|
|
16
|
+
});
|
|
17
|
+
describe('NodeMetricsStore: a node holds its figures for the hub', () => {
|
|
18
|
+
let now;
|
|
19
|
+
let store;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
now = 1000000;
|
|
22
|
+
store = new NodeMetricsStore_1.NodeMetricsStore();
|
|
23
|
+
store.now = () => now;
|
|
24
|
+
});
|
|
25
|
+
it('answers every sample without `after`, and only newer ones with it', () => {
|
|
26
|
+
store.begin('s1', 'Android');
|
|
27
|
+
[1, 2, 3].forEach((t) => store.add('s1', sample(t)));
|
|
28
|
+
(0, chai_1.expect)(store.read('s1', null)).to.deep.equal({
|
|
29
|
+
platform: 'android',
|
|
30
|
+
state: 'sampling',
|
|
31
|
+
samples: [sample(1), sample(2), sample(3)],
|
|
32
|
+
});
|
|
33
|
+
(0, chai_1.expect)(store.read('s1', 2).samples.map((s) => s.at)).to.deep.equal([3]);
|
|
34
|
+
});
|
|
35
|
+
it('drops what the hub has collected', () => {
|
|
36
|
+
store.begin('s1', 'android');
|
|
37
|
+
[1, 2, 3].forEach((t) => store.add('s1', sample(t)));
|
|
38
|
+
store.read('s1', 2);
|
|
39
|
+
(0, chai_1.expect)(store.read('s1', null).samples.map((s) => s.at)).to.deep.equal([3]);
|
|
40
|
+
});
|
|
41
|
+
it('keeps the newest 900 samples', () => {
|
|
42
|
+
store.begin('s1', 'android');
|
|
43
|
+
for (let t = 1; t <= types_1.MAX_BUFFERED_SAMPLES + 5; t++)
|
|
44
|
+
store.add('s1', sample(t));
|
|
45
|
+
const ats = store.read('s1', null).samples.map((s) => s.at);
|
|
46
|
+
(0, chai_1.expect)(ats).to.have.length(types_1.MAX_BUFFERED_SAMPLES);
|
|
47
|
+
(0, chai_1.expect)(ats[0]).to.equal(6);
|
|
48
|
+
});
|
|
49
|
+
it('says a sampler that gave up stopped, and an ended session ended', () => {
|
|
50
|
+
store.begin('s1', 'android');
|
|
51
|
+
store.gaveUp('s1');
|
|
52
|
+
(0, chai_1.expect)(store.read('s1', null).state).to.equal('stopped');
|
|
53
|
+
store.end('s1');
|
|
54
|
+
(0, chai_1.expect)(store.read('s1', null).state).to.equal('ended');
|
|
55
|
+
});
|
|
56
|
+
it('keeps an ended session 10 minutes for the last collection, then forgets it', () => {
|
|
57
|
+
store.begin('s1', 'android');
|
|
58
|
+
store.add('s1', sample(1));
|
|
59
|
+
store.end('s1');
|
|
60
|
+
now += nodeMetrics_1.KEEP_AFTER_END_MS - 1;
|
|
61
|
+
(0, chai_1.expect)(store.read('s1', null).samples).to.have.length(1);
|
|
62
|
+
now += 1;
|
|
63
|
+
(0, chai_1.expect)(store.read('s1', null)).to.deep.equal({ platform: '', state: 'off', samples: [] });
|
|
64
|
+
});
|
|
65
|
+
it('says off for a session it has nothing for', () => {
|
|
66
|
+
(0, chai_1.expect)(store.read('nope', null)).to.deep.equal({ platform: '', state: 'off', samples: [] });
|
|
67
|
+
store.add('nope', sample(1));
|
|
68
|
+
(0, chai_1.expect)(store.read('nope', null).samples).to.deep.equal([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
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 sinon_1 = __importDefault(require("sinon"));
|
|
20
|
+
const nodeSessionMetrics_1 = require("../../src/gateway/nodeSessionMetrics");
|
|
21
|
+
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
22
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
23
|
+
const commandAuth_1 = require("../../src/middleware/commandAuth");
|
|
24
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
25
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
26
|
+
const ANSWER = { platform: 'android', state: 'sampling', samples: [] };
|
|
27
|
+
describe("a node's session metrics route", () => {
|
|
28
|
+
let verify;
|
|
29
|
+
let read;
|
|
30
|
+
let enforced;
|
|
31
|
+
const app = () => {
|
|
32
|
+
const a = (0, express_1.default)();
|
|
33
|
+
a.get('/xenon/api/node/sessions/:sessionId/metrics', (0, nodeSessionMetrics_1.nodeSessionMetricsHandler)({
|
|
34
|
+
hubTokens: { verify },
|
|
35
|
+
enforced: () => enforced,
|
|
36
|
+
read,
|
|
37
|
+
logger: quiet,
|
|
38
|
+
}));
|
|
39
|
+
return a;
|
|
40
|
+
};
|
|
41
|
+
const get = (q = '') => (0, supertest_1.default)(app()).get(`/xenon/api/node/sessions/s1/metrics${q}`);
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
verify = sinon_1.default.stub();
|
|
44
|
+
read = sinon_1.default.stub().returns(ANSWER);
|
|
45
|
+
enforced = false;
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => sinon_1.default.restore());
|
|
48
|
+
it('answers the figures, with no credential when per-command auth is off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
const res = yield get('?after=1500');
|
|
50
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
51
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: ANSWER });
|
|
52
|
+
(0, chai_1.expect)(res.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal('1');
|
|
53
|
+
(0, chai_1.expect)(read.calledOnceWithExactly('s1', 1500)).to.equal(true);
|
|
54
|
+
}));
|
|
55
|
+
it("with per-command auth on, answers only the hub's token for that session", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
enforced = true;
|
|
57
|
+
verify.withArgs('good', 's1').resolves(true);
|
|
58
|
+
verify.withArgs('other', 's1').resolves(false);
|
|
59
|
+
const missing = yield get();
|
|
60
|
+
const wrong = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'other');
|
|
61
|
+
const right = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
62
|
+
(0, chai_1.expect)([missing.status, missing.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
63
|
+
(0, chai_1.expect)([wrong.status, wrong.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
64
|
+
(0, chai_1.expect)(right.status).to.equal(200);
|
|
65
|
+
(0, chai_1.expect)(read.calledOnce).to.equal(true);
|
|
66
|
+
for (const r of [missing, wrong, right])
|
|
67
|
+
(0, chai_1.expect)(r.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal('1');
|
|
68
|
+
}));
|
|
69
|
+
it("answers 503 when it can't check the hub's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
enforced = true;
|
|
71
|
+
verify.rejects(new Error('JWKS unreachable'));
|
|
72
|
+
const res = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
73
|
+
(0, chai_1.expect)([res.status, res.body]).to.deep.equal([503, commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY]);
|
|
74
|
+
(0, chai_1.expect)(read.called).to.equal(false);
|
|
75
|
+
}));
|
|
76
|
+
it('reads anything but a time as no `after`', () => {
|
|
77
|
+
(0, chai_1.expect)((0, nodeSessionMetrics_1.parseAfter)('1500')).to.equal(1500);
|
|
78
|
+
(0, chai_1.expect)((0, nodeSessionMetrics_1.parseAfter)('1500.5')).to.equal(1500.5);
|
|
79
|
+
for (const bad of [undefined, '', 'abc', '-5', '1e3', ['1']]) {
|
|
80
|
+
(0, chai_1.expect)((0, nodeSessionMetrics_1.parseAfter)(bad), String(bad)).to.equal(null);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
it('exists only on a node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
const mount = (hub) => {
|
|
85
|
+
const a = (0, express_1.default)();
|
|
86
|
+
const r = express_1.default.Router();
|
|
87
|
+
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(r, Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub }));
|
|
88
|
+
a.use('/xenon/api', r);
|
|
89
|
+
return a;
|
|
90
|
+
};
|
|
91
|
+
const onHub = yield (0, supertest_1.default)(mount()).get('/xenon/api/node/sessions/s1/metrics');
|
|
92
|
+
const onNode = yield (0, supertest_1.default)(mount('http://127.0.0.1:1')).get('/xenon/api/node/sessions/s1/metrics');
|
|
93
|
+
(0, chai_1.expect)(onHub.status).to.equal(404);
|
|
94
|
+
(0, chai_1.expect)(onHub.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal(undefined);
|
|
95
|
+
(0, chai_1.expect)(onNode.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal('1');
|
|
96
|
+
}));
|
|
97
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
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 typedi_1 = require("typedi");
|
|
19
|
+
const plugin_1 = require("../../src/plugin");
|
|
20
|
+
const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
|
|
21
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
22
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
23
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
24
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
25
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
26
|
+
/**
|
|
27
|
+
* A session's sampling stops however the session ends on this server, its
|
|
28
|
+
* dashboard on or off: on a node that ends the figures it holds for the hub.
|
|
29
|
+
*/
|
|
30
|
+
describe('session sampling stops however a session ends', function () {
|
|
31
|
+
this.timeout(30000);
|
|
32
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
33
|
+
let restore;
|
|
34
|
+
let stop;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService);
|
|
37
|
+
stop = sinon_1.default.stub().resolves();
|
|
38
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop });
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
sinon_1.default.restore();
|
|
42
|
+
restore();
|
|
43
|
+
});
|
|
44
|
+
it('at shutdown', () => __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
|
|
46
|
+
yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
|
|
47
|
+
(0, chai_1.expect)(stop.calledWith('s-down')).to.equal(true);
|
|
48
|
+
}));
|
|
49
|
+
it('when the driver shuts down unexpectedly on a node', () => __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').resolves();
|
|
51
|
+
const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
|
|
52
|
+
yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-crash', caps: {} }, null);
|
|
53
|
+
(0, chai_1.expect)(stop.calledWith('s-crash')).to.equal(true);
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
@@ -32,6 +32,23 @@ class FakeSampler {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
class FakeStore {
|
|
36
|
+
constructor() {
|
|
37
|
+
this.calls = [];
|
|
38
|
+
}
|
|
39
|
+
begin(id, platform) {
|
|
40
|
+
this.calls.push(`begin ${id} ${platform}`);
|
|
41
|
+
}
|
|
42
|
+
add(id, s) {
|
|
43
|
+
this.calls.push(`add ${id} ${s.at}`);
|
|
44
|
+
}
|
|
45
|
+
gaveUp(id) {
|
|
46
|
+
this.calls.push(`gaveUp ${id}`);
|
|
47
|
+
}
|
|
48
|
+
end(id) {
|
|
49
|
+
this.calls.push(`end ${id}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
35
52
|
class TestMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
36
53
|
constructor() {
|
|
37
54
|
super(...arguments);
|
|
@@ -40,6 +57,19 @@ class TestMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
|
40
57
|
this.failWrites = 0;
|
|
41
58
|
this.samplers = [];
|
|
42
59
|
this.caps = [];
|
|
60
|
+
this.store = new FakeStore();
|
|
61
|
+
this.collected = [];
|
|
62
|
+
this.collectorState = 'sampling';
|
|
63
|
+
}
|
|
64
|
+
nodeStore() {
|
|
65
|
+
return this.store;
|
|
66
|
+
}
|
|
67
|
+
collectorFor(source, hooks, after) {
|
|
68
|
+
this.collected.push({ source, after });
|
|
69
|
+
const f = new FakeSampler(hooks);
|
|
70
|
+
f.state = () => this.collectorState;
|
|
71
|
+
this.samplers.push(f);
|
|
72
|
+
return f;
|
|
43
73
|
}
|
|
44
74
|
context() {
|
|
45
75
|
return {
|
|
@@ -159,6 +189,73 @@ describe('SessionMetricsService', () => {
|
|
|
159
189
|
m.start({ sessionId: 's2', device: phone({ nodeId: 'node-2' }), capabilities: {} });
|
|
160
190
|
(0, chai_1.expect)(m.samplers).to.deep.equal([]);
|
|
161
191
|
});
|
|
192
|
+
it('on a node, holds the figures for the hub instead of writing them', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
m.args = { hub: 'http://hub:4723' };
|
|
194
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: {} });
|
|
195
|
+
const hooks = m.samplers[0].hooks;
|
|
196
|
+
[1, 2].forEach((t) => hooks.onSample(sample(t)));
|
|
197
|
+
hooks.onGiveUp('adb gone');
|
|
198
|
+
yield clock.tickAsync(types_1.FLUSH_INTERVAL_MS * 2);
|
|
199
|
+
yield m.stop('s1');
|
|
200
|
+
(0, chai_1.expect)(m.written).to.deep.equal([]);
|
|
201
|
+
(0, chai_1.expect)(m.store.calls).to.deep.equal([
|
|
202
|
+
'begin s1 android',
|
|
203
|
+
'add s1 1',
|
|
204
|
+
'add s1 2',
|
|
205
|
+
'gaveUp s1',
|
|
206
|
+
'end s1',
|
|
207
|
+
]);
|
|
208
|
+
(0, chai_1.expect)(m.samplers[0].stopped).to.equal(true);
|
|
209
|
+
}));
|
|
210
|
+
it('on a node with sessionMetrics off, holds nothing', () => {
|
|
211
|
+
m.args = { hub: 'http://hub:4723', sessionMetrics: false };
|
|
212
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: {} });
|
|
213
|
+
(0, chai_1.expect)(m.samplers).to.have.length(0);
|
|
214
|
+
(0, chai_1.expect)(m.store.calls).to.deep.equal([]);
|
|
215
|
+
});
|
|
216
|
+
const source = {
|
|
217
|
+
nodeOrigin: () => 'http://node',
|
|
218
|
+
nodeMetrics: () => __awaiter(void 0, void 0, void 0, function* () { return ({ kind: 'refused' }); }),
|
|
219
|
+
};
|
|
220
|
+
it("on a hub, collects a node's phone from the node, never a cloud phone or without a session", () => {
|
|
221
|
+
const nodePhone = phone({ nodeId: 'node-2', host: 'http://node:4723' });
|
|
222
|
+
(0, chai_1.expect)(m.appliesTo(nodePhone, source)).to.equal(true);
|
|
223
|
+
(0, chai_1.expect)(m.appliesTo(nodePhone)).to.equal(false);
|
|
224
|
+
(0, chai_1.expect)(m.appliesTo(Object.assign(Object.assign({}, nodePhone), { cloud: 'browserstack' }), source)).to.equal(false);
|
|
225
|
+
(0, chai_1.expect)(m.appliesTo(phone({ nodeId: 'node-2', platform: 'ios', realDevice: false }), source)).to.equal(false);
|
|
226
|
+
m.args = { sessionMetrics: false };
|
|
227
|
+
(0, chai_1.expect)(m.appliesTo(nodePhone, source)).to.equal(false);
|
|
228
|
+
m.args = { hub: 'http://hub:4723' };
|
|
229
|
+
(0, chai_1.expect)(m.appliesTo(nodePhone, source)).to.equal(false);
|
|
230
|
+
});
|
|
231
|
+
it('collects from the node, from where it left off, and writes the figures as its own', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
232
|
+
m.start({
|
|
233
|
+
sessionId: 's1',
|
|
234
|
+
device: phone({ nodeId: 'node-2' }),
|
|
235
|
+
capabilities: {},
|
|
236
|
+
source,
|
|
237
|
+
after: 40,
|
|
238
|
+
});
|
|
239
|
+
(0, chai_1.expect)(m.collected).to.deep.equal([{ source, after: 40 }]);
|
|
240
|
+
m.samplers[0].hooks.onSample(sample(41));
|
|
241
|
+
yield m.stop('s1');
|
|
242
|
+
(0, chai_1.expect)(m.written).to.deep.equal([{ sessionId: 's1', ats: [41] }]);
|
|
243
|
+
}));
|
|
244
|
+
it("says what the node says about a running session's sampling", () => {
|
|
245
|
+
m.start({ sessionId: 's1', device: phone({ nodeId: 'node-2' }), capabilities: {}, source });
|
|
246
|
+
(0, chai_1.expect)(m.recordingState('s1')).to.equal('sampling');
|
|
247
|
+
m.collectorState = 'off';
|
|
248
|
+
(0, chai_1.expect)(m.recordingState('s1')).to.equal('off');
|
|
249
|
+
});
|
|
250
|
+
it('writes the samples a sampler gives while it stops: the collector asks the node one last time', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
251
|
+
m.start({ sessionId: 's1', device: phone({ nodeId: 'node-2' }), capabilities: {}, source });
|
|
252
|
+
const last = m.samplers[0];
|
|
253
|
+
last.stop = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
254
|
+
last.hooks.onSample(sample(99));
|
|
255
|
+
});
|
|
256
|
+
yield m.stop('s1');
|
|
257
|
+
(0, chai_1.expect)(m.written).to.deep.equal([{ sessionId: 's1', ats: [99] }]);
|
|
258
|
+
}));
|
|
162
259
|
});
|
|
163
260
|
/** The real Android wiring, down to the adb process it runs. */
|
|
164
261
|
class WiredMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
@@ -0,0 +1,94 @@
|
|
|
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 typedi_1 = require("typedi");
|
|
19
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
20
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
21
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
22
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
23
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
24
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
25
|
+
/**
|
|
26
|
+
* After a hub restart, a node session whose figures the hub was collecting
|
|
27
|
+
* goes on being collected, from the newest sample the hub stored: the node's
|
|
28
|
+
* memory fills the gap.
|
|
29
|
+
*/
|
|
30
|
+
describe('a hub restart resumes collecting a node session’s figures', function () {
|
|
31
|
+
this.timeout(30000);
|
|
32
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
33
|
+
let restore;
|
|
34
|
+
let start;
|
|
35
|
+
let savedStore;
|
|
36
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService);
|
|
38
|
+
start = sinon_1.default.stub();
|
|
39
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { start });
|
|
40
|
+
savedStore = device_store_1.DeviceStoreFactory._deviceStore;
|
|
41
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
42
|
+
yield scratch.db.sessionMetric.deleteMany({});
|
|
43
|
+
yield scratch.db.session.deleteMany({});
|
|
44
|
+
yield scratch.db.device.deleteMany({});
|
|
45
|
+
yield scratch.db.device.create({
|
|
46
|
+
data: {
|
|
47
|
+
udid: 'node-phone',
|
|
48
|
+
host: 'http://127.0.0.1:1',
|
|
49
|
+
nodeId: 'node-2',
|
|
50
|
+
platform: 'android',
|
|
51
|
+
name: 'S9',
|
|
52
|
+
sdk: '10',
|
|
53
|
+
busy: true,
|
|
54
|
+
userBlocked: false,
|
|
55
|
+
offline: false,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const row = (id, profiled) => scratch.db.session.create({
|
|
59
|
+
data: {
|
|
60
|
+
id,
|
|
61
|
+
device_udid: 'node-phone',
|
|
62
|
+
device_platform: 'android',
|
|
63
|
+
device_version: '10',
|
|
64
|
+
desired_capabilities: '{}',
|
|
65
|
+
session_capabilities: '{}',
|
|
66
|
+
node_id: 'node-2',
|
|
67
|
+
has_live_video: false,
|
|
68
|
+
status: 'running',
|
|
69
|
+
is_profiling_available: profiled,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
yield row('collected', true);
|
|
73
|
+
yield row('fresh', true);
|
|
74
|
+
yield row('never', false);
|
|
75
|
+
yield scratch.db.sessionMetric.createMany({
|
|
76
|
+
data: [100, 200].map((at) => ({ session_id: 'collected', at })),
|
|
77
|
+
});
|
|
78
|
+
}));
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
81
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
82
|
+
device_store_1.DeviceStoreFactory._deviceStore = savedStore;
|
|
83
|
+
sinon_1.default.restore();
|
|
84
|
+
restore();
|
|
85
|
+
});
|
|
86
|
+
it('resumes each from its newest stored sample, and never one the hub did not collect', () => __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
yield new SessionManager_1.SessionManager().recoverActiveSessions('hub-1', '/wd/hub');
|
|
88
|
+
const byId = new Map(start.getCalls().map((c) => [c.args[0].sessionId, c.args[0]]));
|
|
89
|
+
(0, chai_1.expect)(Array.from(byId.keys()).sort()).to.deep.equal(['collected', 'fresh']);
|
|
90
|
+
(0, chai_1.expect)(byId.get('collected').after).to.equal(200);
|
|
91
|
+
(0, chai_1.expect)(byId.get('fresh').after).to.equal(null);
|
|
92
|
+
(0, chai_1.expect)(typeof byId.get('collected').source.nodeMetrics).to.equal('function');
|
|
93
|
+
}));
|
|
94
|
+
});
|