@xenon-device-management/xenon 2.10.1 → 2.11.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-defSfi9B.js → AnnotationOverlay-Dt5AmXCR.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DS7dJ2bU.js → ApiKeyGate-CS05jXd7.js} +1 -1
- package/lib/public/assets/{BugReportButton-CeD8SUnQ.js → BugReportButton-ejWk5lof.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Dgue7t7s.js → DeviceMosaicView-C9AypMcW.js} +1 -1
- package/lib/public/assets/{EmptyState-Bo-vvxiy.js → EmptyState-DyojFa86.js} +1 -1
- package/lib/public/assets/{FieldGroup-DdhTNAXZ.js → FieldGroup-Dm-U3Ucr.js} +1 -1
- package/lib/public/assets/{FilterMenu-J3F_3fDw.js → FilterMenu-B3hgDfy_.js} +1 -1
- package/lib/public/assets/{Menu-Bxluaw2m.js → Menu-DFNsTx2i.js} +1 -1
- package/lib/public/assets/{Modal-qgMVFrqV.js → Modal-BdWYrwfK.js} +1 -1
- package/lib/public/assets/{RecordingPage-BUT7WgJB.js → RecordingPage-FIRd8b5d.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CY0OIgCo.js → RecordingsPage-FAUS-RwG.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Cq-fybZ2.js → SegmentedControl-EBzIUjnM.js} +1 -1
- package/lib/public/assets/{SettingCard-CASWJr7h.js → SettingCard-BQ8wMHrz.js} +1 -1
- package/lib/public/assets/{Table-BSO_dZzu.js → Table-DDahAwGb.js} +1 -1
- package/lib/public/assets/{activity-B_-MNwwR.js → activity-CFEfukVD.js} +1 -1
- package/lib/public/assets/{ai-settings-DFhemyo8.js → ai-settings-iPPNZMnS.js} +1 -1
- package/lib/public/assets/{api-keys-DV1oB8Tf.js → api-keys-DS979X_e.js} +1 -1
- package/lib/public/assets/apps-BQKfuN3J.css +1 -0
- package/lib/public/assets/{apps-B5f-VuLN.js → apps-DQnxhScG.js} +2 -2
- package/lib/public/assets/{arrow-left-D_xIcVqg.js → arrow-left-CIqcStpJ.js} +1 -1
- package/lib/public/assets/{arrow-right-CLGlL_Un.js → arrow-right-Pw0XgjBJ.js} +1 -1
- package/lib/public/assets/{arrow-up-right-CuIgWFj2.js → arrow-up-right-DWaEiSQf.js} +1 -1
- package/lib/public/assets/{auth-shell-SgAkrFQg.js → auth-shell-DvJ8qKdR.js} +2 -2
- package/lib/public/assets/{builds-page-h9ZJ8n74.js → builds-page-BIWuhGph.js} +1 -1
- package/lib/public/assets/{button-DKl94fzV.js → button-B1XDl_TT.js} +1 -1
- package/lib/public/assets/{calendar-BNLTjBsT.js → calendar-BPrY2NZL.js} +1 -1
- package/lib/public/assets/{check-1u_3Zsx5.js → check-DjbzNVbd.js} +1 -1
- package/lib/public/assets/{chevron-right-Cn2BixQX.js → chevron-right-J_WfelGu.js} +1 -1
- package/lib/public/assets/{circle-check-OymVCeGU.js → circle-check-CGwn46BE.js} +1 -1
- package/lib/public/assets/{circle-x-D3ZyFWOd.js → circle-x-Rfn2Zmq3.js} +1 -1
- package/lib/public/assets/{clock-DwzGN-aa.js → clock-BAD4KA9j.js} +1 -1
- package/lib/public/assets/{copy-De43WY6T.js → copy-CcNlsrXH.js} +1 -1
- package/lib/public/assets/{device-explorer-CtvEmCC5.js → device-explorer-Dl1QGKQN.js} +1 -1
- package/lib/public/assets/{download-DcSdKSvR.js → download-CoBA2mT_.js} +1 -1
- package/lib/public/assets/{forgot-password-DDjM8J7Z.js → forgot-password-BrV3pKRK.js} +1 -1
- package/lib/public/assets/{index-CSNs0EE4.js → index-BHI_dBXi.js} +1 -1
- package/lib/public/assets/{index-Bqt1dmMA.js → index-DklX18ye.js} +2 -2
- package/lib/public/assets/{input-fRGFwm1K.js → input-Bo2b9153.js} +1 -1
- package/lib/public/assets/{keycodes-Dj4m3JV7.js → keycodes-B5mWBGMd.js} +1 -1
- package/lib/public/assets/{line-chart-DHSZ8Sm4.js → line-chart-X7ZCpjWB.js} +1 -1
- package/lib/public/assets/{list-checks-Co1gqqDn.js → list-checks-Cqso6ROH.js} +1 -1
- package/lib/public/assets/{lock-BzutP2Yf.js → lock-D9WyaPyU.js} +1 -1
- package/lib/public/assets/{login-DSVZLgW2.js → login-DqIYeZ2q.js} +1 -1
- package/lib/public/assets/{maintenance-settings-PYFNp_tc.js → maintenance-settings-C7pWE0--.js} +1 -1
- package/lib/public/assets/{monitor-DdKVA3ze.js → monitor-ZC2MoScA.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C8UpXByt.js → mouse-pointer-2-DXh-LIn9.js} +1 -1
- package/lib/public/assets/{network-BkzHrr2L.js → network-CivILLOW.js} +1 -1
- package/lib/public/assets/{overview-0YFo6f97.js → overview-ClDfJNL5.js} +1 -1
- package/lib/public/assets/{page-header-DkmHEtLJ.js → page-header-CV-WOQ7B.js} +1 -1
- package/lib/public/assets/{plus-BH6Y7G7y.js → plus-DOp8RUCs.js} +1 -1
- package/lib/public/assets/{profile-page-C1IDBP4l.js → profile-page-Ckx1bcSx.js} +1 -1
- package/lib/public/assets/{reset-password-yNMsl5fZ.js → reset-password-CZDSfFZ6.js} +1 -1
- package/lib/public/assets/{runbook-page--fH5nToN.js → runbook-page-BXe7SL5K.js} +1 -1
- package/lib/public/assets/{select-DqjhZ6h2.js → select-Bo_2fqDM.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CrYIFrUj.js → selector-detail-redirect-D_aNBNim.js} +1 -1
- package/lib/public/assets/{selector-health-page-BnRUmiS2.js → selector-health-page-BP4XhSI4.js} +1 -1
- package/lib/public/assets/session-detail-page-DN8MPDsL.js +4 -0
- package/lib/public/assets/{settings-BcxYFxnE.js → settings-rFQ6qW9Q.js} +1 -1
- package/lib/public/assets/{stat-tile-DxVLBgA7.js → stat-tile-D8taj4dL.js} +1 -1
- package/lib/public/assets/{tablet-C_uKWfUK.js → tablet-Cz9pTLGh.js} +1 -1
- package/lib/public/assets/{teams-C9G2WisD.js → teams-DkFhPU6N.js} +1 -1
- package/lib/public/assets/{trash-2-C4cJO0CG.js → trash-2-DMkIlO0b.js} +1 -1
- package/lib/public/assets/{upload-BN3PMMvP.js → upload-Dg76vQUo.js} +1 -1
- package/lib/public/assets/{use-builds-data-CtelVuo_.js → use-builds-data-iSxxYh9G.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CClcmiRh.js → use-password-reset-mode-DsNcDXiv.js} +1 -1
- package/lib/public/assets/{users-OdnOb63Y.js → users-BShiNYd9.js} +1 -1
- package/lib/public/assets/{users-BK7fwGGA.js → users-Bu1HulwN.js} +1 -1
- package/lib/public/assets/{video-off-DUAYoXTQ.js → video-off-BJ_s_mLK.js} +1 -1
- package/lib/public/assets/{webhook-settings-BRyvDWHI.js → webhook-settings-CJxMc7-Q.js} +1 -1
- package/lib/public/assets/{zap-BiceX7DC.js → zap-BXZlQKN8.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 +42 -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 +116 -0
- package/lib/src/services/metrics/NodeMetricsStore.js +76 -0
- package/lib/src/services/metrics/SessionMetricsService.js +75 -14
- package/lib/src/services/metrics/nodeMetrics.js +122 -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 +11 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +400 -0
- package/lib/test/unit/node-metrics-collector.spec.js +279 -0
- package/lib/test/unit/node-metrics-store.spec.js +70 -0
- package/lib/test/unit/node-session-metrics-route.spec.js +98 -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,279 @@
|
|
|
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 NodeMetricsCollector_1 = require("../../src/services/metrics/NodeMetricsCollector");
|
|
19
|
+
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
20
|
+
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
21
|
+
const CloudSession_1 = require("../../src/sessions/CloudSession");
|
|
22
|
+
const sample = (at) => ({
|
|
23
|
+
at,
|
|
24
|
+
deviceCpuPct: 5,
|
|
25
|
+
deviceMemMb: 1,
|
|
26
|
+
deviceMemTotalMb: 2,
|
|
27
|
+
appCpuPct: null,
|
|
28
|
+
appMemMb: null,
|
|
29
|
+
appId: null,
|
|
30
|
+
});
|
|
31
|
+
const answer = (state, ats) => ({
|
|
32
|
+
kind: 'answer',
|
|
33
|
+
answer: { platform: 'android', state, samples: ats.map(sample) },
|
|
34
|
+
});
|
|
35
|
+
describe('NodeMetricsCollector: the hub collects a node session’s figures', () => {
|
|
36
|
+
let clock;
|
|
37
|
+
let replies;
|
|
38
|
+
let asked;
|
|
39
|
+
let got;
|
|
40
|
+
let warned;
|
|
41
|
+
let support;
|
|
42
|
+
/** While set, an ask waits on it: a node slow to answer. */
|
|
43
|
+
let gate;
|
|
44
|
+
const source = {
|
|
45
|
+
nodeOrigin: () => 'http://node:4723',
|
|
46
|
+
nodeMetrics: (after) => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
var _a;
|
|
48
|
+
asked.push(after);
|
|
49
|
+
if (gate)
|
|
50
|
+
yield gate;
|
|
51
|
+
return (_a = replies.shift()) !== null && _a !== void 0 ? _a : answer('sampling', []);
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
const make = (after = null) => new NodeMetricsCollector_1.NodeMetricsCollector({
|
|
55
|
+
source,
|
|
56
|
+
hooks: { onSample: (s) => got.push(s.at), onGiveUp: () => undefined },
|
|
57
|
+
support,
|
|
58
|
+
logger: { info: () => undefined, warn: (m) => warned.push(m) },
|
|
59
|
+
after,
|
|
60
|
+
});
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
clock = sinon_1.default.useFakeTimers({ now: 0 });
|
|
63
|
+
replies = [];
|
|
64
|
+
asked = [];
|
|
65
|
+
got = [];
|
|
66
|
+
warned = [];
|
|
67
|
+
gate = null;
|
|
68
|
+
support = new nodeMetrics_1.NodeMetricsSupport();
|
|
69
|
+
support.now = () => clock.now;
|
|
70
|
+
support.logger = { warn: (m) => warned.push(m) };
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => clock.restore());
|
|
73
|
+
it('asks every 10 s from the newest sample it has, and passes the samples on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
replies = [answer('sampling', [1, 2]), answer('sampling', [3])];
|
|
75
|
+
const c = make();
|
|
76
|
+
c.start();
|
|
77
|
+
yield clock.tickAsync(10000);
|
|
78
|
+
yield clock.tickAsync(10000);
|
|
79
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
80
|
+
(0, chai_1.expect)(got).to.deep.equal([1, 2, 3]);
|
|
81
|
+
(0, chai_1.expect)(c.state()).to.equal('sampling');
|
|
82
|
+
}));
|
|
83
|
+
it('starts from the newest sample stored, after a hub restart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
const c = make(500);
|
|
85
|
+
c.start();
|
|
86
|
+
yield clock.tickAsync(10000);
|
|
87
|
+
(0, chai_1.expect)(asked).to.deep.equal([500]);
|
|
88
|
+
}));
|
|
89
|
+
it('asks once more when the session ends, and takes the last samples', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
replies = [answer('sampling', [1]), answer('ended', [2, 3])];
|
|
91
|
+
const c = make();
|
|
92
|
+
c.start();
|
|
93
|
+
yield clock.tickAsync(10000);
|
|
94
|
+
yield c.stop();
|
|
95
|
+
(0, chai_1.expect)(got).to.deep.equal([1, 2, 3]);
|
|
96
|
+
(0, chai_1.expect)(c.state()).to.equal('stopped');
|
|
97
|
+
yield clock.tickAsync(30000);
|
|
98
|
+
(0, chai_1.expect)(asked).to.have.length(2);
|
|
99
|
+
}));
|
|
100
|
+
it('stops asking when the node says off, or that its sampler stopped', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
for (const [state, ats, expected] of [
|
|
102
|
+
['off', [], 'off'],
|
|
103
|
+
['stopped', [7], 'stopped'],
|
|
104
|
+
]) {
|
|
105
|
+
asked = [];
|
|
106
|
+
replies = [answer(state, [...ats])];
|
|
107
|
+
const c = make();
|
|
108
|
+
c.start();
|
|
109
|
+
yield clock.tickAsync(30000);
|
|
110
|
+
(0, chai_1.expect)(asked, state).to.have.length(1);
|
|
111
|
+
(0, chai_1.expect)(c.state(), state).to.equal(expected);
|
|
112
|
+
yield c.stop();
|
|
113
|
+
(0, chai_1.expect)(asked, `${state}: no last ask`).to.have.length(1);
|
|
114
|
+
}
|
|
115
|
+
}));
|
|
116
|
+
it('stops asking a node without the route, says so once, and asks it again later', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
replies = [{ kind: 'unsupported', status: 404 }];
|
|
118
|
+
const first = make();
|
|
119
|
+
first.start();
|
|
120
|
+
yield clock.tickAsync(10000);
|
|
121
|
+
(0, chai_1.expect)(first.state()).to.equal('off');
|
|
122
|
+
const second = make();
|
|
123
|
+
second.start();
|
|
124
|
+
yield clock.tickAsync(10000);
|
|
125
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
126
|
+
(0, chai_1.expect)(second.state()).to.equal('off');
|
|
127
|
+
(0, chai_1.expect)(warned.filter((m) => m.includes('http://node:4723'))).to.have.length(1);
|
|
128
|
+
yield clock.tickAsync(10 * 60000);
|
|
129
|
+
const third = make();
|
|
130
|
+
third.start();
|
|
131
|
+
yield clock.tickAsync(10000);
|
|
132
|
+
(0, chai_1.expect)(asked).to.have.length(2);
|
|
133
|
+
}));
|
|
134
|
+
it('keeps the figures it has when the node later says off or refuses: it says stopped', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
for (const last of [answer('off', []), { kind: 'refused' }]) {
|
|
136
|
+
asked = [];
|
|
137
|
+
replies = [answer('sampling', [1]), last];
|
|
138
|
+
const c = make();
|
|
139
|
+
c.start();
|
|
140
|
+
yield clock.tickAsync(20000);
|
|
141
|
+
(0, chai_1.expect)(asked, last.kind).to.have.length(2);
|
|
142
|
+
(0, chai_1.expect)(c.state(), last.kind).to.equal('stopped');
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
it("stops asking when the node refuses the hub's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
replies = [{ kind: 'refused' }];
|
|
147
|
+
const c = make();
|
|
148
|
+
c.start();
|
|
149
|
+
yield clock.tickAsync(30000);
|
|
150
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
151
|
+
(0, chai_1.expect)(c.state()).to.equal('off');
|
|
152
|
+
}));
|
|
153
|
+
it('keeps asking an unreachable node, saying so once per outage', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
|
+
replies = [
|
|
155
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
156
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
157
|
+
answer('sampling', [1]),
|
|
158
|
+
{ kind: 'unavailable', reason: 'timeout' },
|
|
159
|
+
];
|
|
160
|
+
const c = make();
|
|
161
|
+
c.start();
|
|
162
|
+
yield clock.tickAsync(40000);
|
|
163
|
+
(0, chai_1.expect)(asked).to.have.length(4);
|
|
164
|
+
(0, chai_1.expect)(got).to.deep.equal([1]);
|
|
165
|
+
(0, chai_1.expect)(warned).to.have.length(2);
|
|
166
|
+
(0, chai_1.expect)(c.state()).to.equal('sampling');
|
|
167
|
+
}));
|
|
168
|
+
it("doesn't wait on a last ask when the node already wasn't answering", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
169
|
+
// A dark node's sessions are ended one after another by the heartbeat;
|
|
170
|
+
// each would wait up to 5 s more for an answer that won't come.
|
|
171
|
+
replies = [{ kind: 'unavailable', reason: 'ECONNREFUSED' }];
|
|
172
|
+
const c = make();
|
|
173
|
+
c.start();
|
|
174
|
+
yield clock.tickAsync(10000);
|
|
175
|
+
yield c.stop();
|
|
176
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
177
|
+
}));
|
|
178
|
+
it('finishes stopping when the node is unreachable', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
179
|
+
replies = [{ kind: 'unavailable', reason: 'timeout' }];
|
|
180
|
+
const c = make();
|
|
181
|
+
c.start();
|
|
182
|
+
yield c.stop();
|
|
183
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
184
|
+
}));
|
|
185
|
+
it('never asks twice at once, or takes a sample twice', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
186
|
+
let release;
|
|
187
|
+
gate = new Promise((r) => (release = r));
|
|
188
|
+
replies = [answer('sampling', [1, 2]), answer('sampling', [2, 3])];
|
|
189
|
+
const c = make();
|
|
190
|
+
c.start();
|
|
191
|
+
yield clock.tickAsync(10000); // the first ask, held by the node
|
|
192
|
+
yield clock.tickAsync(20000); // two more ticks while it is held
|
|
193
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
194
|
+
gate = null;
|
|
195
|
+
release();
|
|
196
|
+
yield clock.tickAsync(10000); // the first answer arrives; the next ask goes
|
|
197
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
198
|
+
(0, chai_1.expect)(got).to.deep.equal([1, 2, 3]);
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
describe("readNodeMetricsReply: what a node's answer means", () => {
|
|
202
|
+
const h = { [nodeMetrics_1.NODE_METRICS_HEADER]: '1' };
|
|
203
|
+
it('keeps only well-formed samples: one bad sample would block every later write', () => {
|
|
204
|
+
const bad = [
|
|
205
|
+
null,
|
|
206
|
+
'x',
|
|
207
|
+
Object.assign(Object.assign({}, sample(5)), { at: '6' }),
|
|
208
|
+
Object.assign(Object.assign({}, sample(7)), { at: Number.NaN }),
|
|
209
|
+
Object.assign(Object.assign({}, sample(8)), { deviceCpuPct: '12' }),
|
|
210
|
+
Object.assign(Object.assign({}, sample(9)), { appId: 3 }),
|
|
211
|
+
];
|
|
212
|
+
const reply = (0, nodeMetrics_1.readNodeMetricsReply)(200, h, {
|
|
213
|
+
value: {
|
|
214
|
+
platform: 'android',
|
|
215
|
+
state: 'sampling',
|
|
216
|
+
samples: [sample(1), ...bad, Object.assign(Object.assign({}, sample(2)), { extra: 1 })],
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
(0, chai_1.expect)(reply).to.deep.equal({
|
|
220
|
+
kind: 'answer',
|
|
221
|
+
answer: { platform: 'android', state: 'sampling', samples: [sample(1), sample(2)] },
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
it('reads each answer', () => {
|
|
225
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(404, {}, undefined)).to.deep.equal({
|
|
226
|
+
kind: 'unsupported',
|
|
227
|
+
status: 404,
|
|
228
|
+
});
|
|
229
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(404, h, {})).to.deep.equal({ kind: 'refused' });
|
|
230
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(200, h, {
|
|
231
|
+
value: { platform: 'ios', state: 'sampling', samples: [sample(1)] },
|
|
232
|
+
})).to.deep.equal({
|
|
233
|
+
kind: 'answer',
|
|
234
|
+
answer: { platform: 'ios', state: 'sampling', samples: [sample(1)] },
|
|
235
|
+
});
|
|
236
|
+
// Without the header: an older node only for what an older node answers
|
|
237
|
+
// (its login's 401, an unknown route's 404, a catch-all's 2xx). A proxy's
|
|
238
|
+
// 502 or 503 in front of a new node is an outage, not an older node.
|
|
239
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(401, {}, {})).to.deep.equal({ kind: 'unsupported', status: 401 });
|
|
240
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(200, {}, '<html>')).to.deep.equal({
|
|
241
|
+
kind: 'unsupported',
|
|
242
|
+
status: 200,
|
|
243
|
+
});
|
|
244
|
+
for (const status of [500, 502, 503, 504]) {
|
|
245
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(status, {}, 'Bad Gateway'), String(status)).to.deep.equal({
|
|
246
|
+
kind: 'unavailable',
|
|
247
|
+
reason: `answered ${status}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(503, h, {})).to.deep.equal({
|
|
251
|
+
kind: 'unavailable',
|
|
252
|
+
reason: 'answered 503',
|
|
253
|
+
});
|
|
254
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(200, h, { value: { state: 'weird', samples: [] } })).to.deep.equal({
|
|
255
|
+
kind: 'unavailable',
|
|
256
|
+
reason: 'answered 200',
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe('nodeMetricsSourceOf: only a session a node runs is collected from', () => {
|
|
261
|
+
const options = (cloud) => ({
|
|
262
|
+
sessionId: 's1',
|
|
263
|
+
device: { udid: 'p1', host: 'http://node:4723', platform: 'android', cloud },
|
|
264
|
+
sessionResponse: {},
|
|
265
|
+
xenonOption: {},
|
|
266
|
+
baseUrl: 'http://node:4723/wd/hub',
|
|
267
|
+
});
|
|
268
|
+
it("takes a node's session, never a cloud provider's or this server's own", () => {
|
|
269
|
+
const remote = new RemoteSession_1.RemoteSession(options());
|
|
270
|
+
(0, chai_1.expect)((0, nodeMetrics_1.nodeMetricsSourceOf)(remote)).to.equal(remote);
|
|
271
|
+
// Both inherit nodeMetrics from RemoteSession; neither runs on a node.
|
|
272
|
+
(0, chai_1.expect)((0, nodeMetrics_1.nodeMetricsSourceOf)(new CloudSession_1.CloudSession(options('browserstack')))).to.equal(undefined);
|
|
273
|
+
const local = Object.assign(Object.create(RemoteSession_1.RemoteSession.prototype), {
|
|
274
|
+
getType: () => 'local',
|
|
275
|
+
});
|
|
276
|
+
(0, chai_1.expect)((0, nodeMetrics_1.nodeMetricsSourceOf)(local)).to.equal(undefined);
|
|
277
|
+
(0, chai_1.expect)((0, nodeMetrics_1.nodeMetricsSourceOf)({ nodeOrigin: () => 'x', nodeMetrics: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) })).to.equal(undefined);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -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,98 @@
|
|
|
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
|
+
// A 400-digit number is Infinity: it would drop every sample the node holds.
|
|
80
|
+
for (const bad of [undefined, '', 'abc', '-5', '1e3', ['1'], '9'.repeat(400)]) {
|
|
81
|
+
(0, chai_1.expect)((0, nodeSessionMetrics_1.parseAfter)(bad), String(bad)).to.equal(null);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
it('exists only on a node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
const mount = (hub) => {
|
|
86
|
+
const a = (0, express_1.default)();
|
|
87
|
+
const r = express_1.default.Router();
|
|
88
|
+
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(r, Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub }));
|
|
89
|
+
a.use('/xenon/api', r);
|
|
90
|
+
return a;
|
|
91
|
+
};
|
|
92
|
+
const onHub = yield (0, supertest_1.default)(mount()).get('/xenon/api/node/sessions/s1/metrics');
|
|
93
|
+
const onNode = yield (0, supertest_1.default)(mount('http://127.0.0.1:1')).get('/xenon/api/node/sessions/s1/metrics');
|
|
94
|
+
(0, chai_1.expect)(onHub.status).to.equal(404);
|
|
95
|
+
(0, chai_1.expect)(onHub.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal(undefined);
|
|
96
|
+
(0, chai_1.expect)(onNode.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal('1');
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
@@ -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 {
|