@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
|
@@ -45,6 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.MUTE_REASON_MAX = void 0;
|
|
48
49
|
exports.getRecentHealingEvents = getRecentHealingEvents;
|
|
49
50
|
exports.aggregateHotspots = aggregateHotspots;
|
|
50
51
|
exports.getSelectorHealth = getSelectorHealth;
|
|
@@ -59,12 +60,18 @@ const typedi_1 = require("typedi");
|
|
|
59
60
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
60
61
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
61
62
|
const build_export_1 = __importDefault(require("./build-export"));
|
|
63
|
+
const selector_health_1 = __importDefault(require("./selector-health"));
|
|
64
|
+
const actor_1 = require("../../services/device-access/actor");
|
|
65
|
+
const access_1 = require("../../services/selector-health/access");
|
|
62
66
|
const NotificationService_1 = require("../../services/NotificationService");
|
|
63
67
|
const SelectorStateService_1 = require("../../services/SelectorStateService");
|
|
64
68
|
const path_1 = __importDefault(require("path"));
|
|
65
69
|
const config_1 = require("../../config");
|
|
66
70
|
const sessionVisibility_1 = require("../../services/device-access/sessionVisibility");
|
|
67
71
|
const HealEtalonService_1 = require("../../services/healing/HealEtalonService");
|
|
72
|
+
const metricsBody_1 = require("../../services/metrics/metricsBody");
|
|
73
|
+
const healingTrend_1 = require("../../services/selector-health/healingTrend");
|
|
74
|
+
const SessionMetricsService_1 = require("../../services/metrics/SessionMetricsService");
|
|
68
75
|
const sessionInsights_1 = require("./sessionInsights");
|
|
69
76
|
const logger_1 = __importDefault(require("../../logger"));
|
|
70
77
|
const MJPEG_PROXY_CACHE = new Map();
|
|
@@ -324,26 +331,6 @@ function getRecentHealingEvents(request, response) {
|
|
|
324
331
|
return response.status(200).json({ events, todayCount });
|
|
325
332
|
});
|
|
326
333
|
}
|
|
327
|
-
// Rough per-heal cost estimates in USD. Local tiers are effectively free
|
|
328
|
-
// (CPU/inference cost only); LLM is the headline number testers should
|
|
329
|
-
// optimize against. Surfaced so engineering managers can quantify suite
|
|
330
|
-
// hygiene work without reading raw billing data.
|
|
331
|
-
const TIER_COST_USD = {
|
|
332
|
-
Native: 0,
|
|
333
|
-
Resilio: 0,
|
|
334
|
-
'Fuzzy XML': 0,
|
|
335
|
-
OCR: 0.0005,
|
|
336
|
-
'Visual AI': 0.002,
|
|
337
|
-
LLM: 0.04,
|
|
338
|
-
};
|
|
339
|
-
function estimateCost(byTier) {
|
|
340
|
-
var _a;
|
|
341
|
-
let sum = 0;
|
|
342
|
-
for (const [tier, count] of Object.entries(byTier)) {
|
|
343
|
-
sum += ((_a = TIER_COST_USD[tier]) !== null && _a !== void 0 ? _a : 0) * count;
|
|
344
|
-
}
|
|
345
|
-
return sum;
|
|
346
|
-
}
|
|
347
334
|
function parseWindowDays(raw, fallback = 30) {
|
|
348
335
|
const n = parseInt(typeof raw === 'string' ? raw : '', 10);
|
|
349
336
|
if (!Number.isFinite(n))
|
|
@@ -355,6 +342,7 @@ function parseWindowDays(raw, fallback = 30) {
|
|
|
355
342
|
function getHealingSummary(request, response) {
|
|
356
343
|
return __awaiter(this, void 0, void 0, function* () {
|
|
357
344
|
const windowDays = parseWindowDays(request.query.windowDays);
|
|
345
|
+
const tz = (0, healingTrend_1.parseTzOffset)(request.query.tz);
|
|
358
346
|
const now = new Date();
|
|
359
347
|
const since = new Date(now);
|
|
360
348
|
since.setDate(since.getDate() - windowDays);
|
|
@@ -370,6 +358,8 @@ function getHealingSummary(request, response) {
|
|
|
370
358
|
session_id: true,
|
|
371
359
|
original_selector: true,
|
|
372
360
|
healing_tier: true,
|
|
361
|
+
createdAt: true,
|
|
362
|
+
duration: true,
|
|
373
363
|
},
|
|
374
364
|
}),
|
|
375
365
|
prisma_1.prisma.sessionLog.findMany({
|
|
@@ -378,15 +368,20 @@ function getHealingSummary(request, response) {
|
|
|
378
368
|
session_id: true,
|
|
379
369
|
original_selector: true,
|
|
380
370
|
healing_tier: true,
|
|
371
|
+
createdAt: true,
|
|
372
|
+
duration: true,
|
|
381
373
|
},
|
|
382
374
|
}),
|
|
383
375
|
]);
|
|
384
376
|
const aggregate = (rows) => {
|
|
377
|
+
var _a;
|
|
385
378
|
const sessions = new Set();
|
|
386
379
|
const selectors = new Set();
|
|
387
380
|
const byTier = {};
|
|
381
|
+
let timeSpentMs = 0;
|
|
388
382
|
for (const r of rows) {
|
|
389
383
|
sessions.add(r.session_id);
|
|
384
|
+
timeSpentMs += (_a = r.duration) !== null && _a !== void 0 ? _a : 0;
|
|
390
385
|
if (r.original_selector)
|
|
391
386
|
selectors.add(r.original_selector);
|
|
392
387
|
const tier = r.healing_tier || 'Unknown';
|
|
@@ -397,7 +392,8 @@ function getHealingSummary(request, response) {
|
|
|
397
392
|
distinctSelectors: selectors.size,
|
|
398
393
|
sessionsTouched: sessions.size,
|
|
399
394
|
byTier,
|
|
400
|
-
|
|
395
|
+
// The commands that needed healing, start to end: what healing cost the run.
|
|
396
|
+
timeSpentMs,
|
|
401
397
|
};
|
|
402
398
|
};
|
|
403
399
|
const current = aggregate(currentRows);
|
|
@@ -417,6 +413,7 @@ function getHealingSummary(request, response) {
|
|
|
417
413
|
prior,
|
|
418
414
|
resolvedCount,
|
|
419
415
|
pendingCount,
|
|
416
|
+
trend: (0, healingTrend_1.dailyHeals)(currentRows.map((r) => ({ at: r.createdAt, method: r.healing_tier })), since, now, tz),
|
|
420
417
|
});
|
|
421
418
|
});
|
|
422
419
|
}
|
|
@@ -624,7 +621,6 @@ function aggregateHotspots(opts) {
|
|
|
624
621
|
distinctSelectors: buckets.size,
|
|
625
622
|
sessionsTouched: sessionsTouched.size,
|
|
626
623
|
byTier,
|
|
627
|
-
estCostUsd: estimateCost(byTier),
|
|
628
624
|
hotspots,
|
|
629
625
|
};
|
|
630
626
|
});
|
|
@@ -660,7 +656,6 @@ function getHealingViolations(request, response) {
|
|
|
660
656
|
violationCount: agg.hotspots.length,
|
|
661
657
|
totalHeals: agg.totalHeals,
|
|
662
658
|
distinctSelectors: agg.distinctSelectors,
|
|
663
|
-
estCostUsd: agg.estCostUsd,
|
|
664
659
|
violations: agg.hotspots,
|
|
665
660
|
});
|
|
666
661
|
});
|
|
@@ -685,7 +680,6 @@ function sendHealingDigest(request, response) {
|
|
|
685
680
|
windowDays,
|
|
686
681
|
totalHeals: agg.totalHeals,
|
|
687
682
|
distinctSelectors: agg.distinctSelectors,
|
|
688
|
-
estCostUsd: agg.estCostUsd,
|
|
689
683
|
hotspots: agg.hotspots,
|
|
690
684
|
};
|
|
691
685
|
const notifier = typedi_1.Container.get(NotificationService_1.NotificationService);
|
|
@@ -801,7 +795,6 @@ function getHealingSelectorDetail(request, response) {
|
|
|
801
795
|
windowDays,
|
|
802
796
|
healCount: rows.length,
|
|
803
797
|
sessionCount: sessions.size,
|
|
804
|
-
estCostUsd: estimateCost(byTier),
|
|
805
798
|
byTier,
|
|
806
799
|
byPlatform,
|
|
807
800
|
byBuild: Object.entries(byBuild)
|
|
@@ -878,14 +871,24 @@ function getSelectorHealth(request, response) {
|
|
|
878
871
|
}
|
|
879
872
|
// SelectorState lifecycle action endpoint — mark fixed / mute / unmute /
|
|
880
873
|
// cancel verification. The action vocabulary is closed (any value not in
|
|
881
|
-
// VALID_ACTIONS rejects with 400). A
|
|
882
|
-
//
|
|
874
|
+
// VALID_ACTIONS rejects with 400). A member may act on a selector they can
|
|
875
|
+
// see (healed in a session they can see); any other answers 404, as an
|
|
876
|
+
// unknown one. The person acting is recorded (SelectorEvent), with the
|
|
877
|
+
// reason for a mute. A SelectorStateConflictError surfaces as 409 with
|
|
878
|
+
// `currentStatus`; anything else logs and surfaces as 500.
|
|
883
879
|
const VALID_SELECTOR_ACTIONS = ['mark_fixed', 'mute', 'unmute', 'cancel_verification'];
|
|
880
|
+
/** The longest reason a mute may give. */
|
|
881
|
+
exports.MUTE_REASON_MAX = 500;
|
|
884
882
|
function postSelectorStateAction(request, response) {
|
|
885
883
|
return __awaiter(this, void 0, void 0, function* () {
|
|
886
|
-
var _a, _b, _c, _d;
|
|
887
|
-
const { original_strategy, original_selector, action } = ((_a = request.body) !== null && _a !== void 0 ? _a : {});
|
|
888
|
-
|
|
884
|
+
var _a, _b, _c, _d, _e;
|
|
885
|
+
const { original_strategy, original_selector, action, reason } = ((_a = request.body) !== null && _a !== void 0 ? _a : {});
|
|
886
|
+
// The strategy may be '': heals recorded before strategies were.
|
|
887
|
+
if (typeof original_strategy !== 'string' ||
|
|
888
|
+
typeof original_selector !== 'string' ||
|
|
889
|
+
!original_selector ||
|
|
890
|
+
typeof action !== 'string' ||
|
|
891
|
+
!action) {
|
|
889
892
|
return response.status(400).json({
|
|
890
893
|
error: 'original_strategy, original_selector, and action are required',
|
|
891
894
|
});
|
|
@@ -895,10 +898,29 @@ function postSelectorStateAction(request, response) {
|
|
|
895
898
|
error: `action must be one of ${VALID_SELECTOR_ACTIONS.join(', ')}`,
|
|
896
899
|
});
|
|
897
900
|
}
|
|
898
|
-
|
|
899
|
-
|
|
901
|
+
if (reason !== undefined &&
|
|
902
|
+
reason !== null &&
|
|
903
|
+
(typeof reason !== 'string' || reason.length > exports.MUTE_REASON_MAX)) {
|
|
904
|
+
return response.status(400).json({
|
|
905
|
+
error: `reason must be text of at most ${exports.MUTE_REASON_MAX} characters`,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
const muteReason = action === 'mute' && typeof reason === 'string' ? reason.trim() : '';
|
|
909
|
+
const ctx = {
|
|
910
|
+
strategy: original_strategy,
|
|
911
|
+
selector: original_selector,
|
|
912
|
+
apiKeyId: (_c = (_b = request.apiKey) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : '',
|
|
913
|
+
userId: (_d = (0, actor_1.resolveActor)(request).userId) !== null && _d !== void 0 ? _d : null,
|
|
914
|
+
reason: muteReason || null,
|
|
915
|
+
};
|
|
900
916
|
const service = typedi_1.Container.get(SelectorStateService_1.SelectorStateService);
|
|
901
917
|
try {
|
|
918
|
+
// Inside the try: a failed lookup answers 500 rather than leaving the
|
|
919
|
+
// request open (Express 4 doesn't catch a rejected handler).
|
|
920
|
+
const scope = (yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request)));
|
|
921
|
+
if (!(yield (0, access_1.canSeeSelector)({ strategy: original_strategy, selector: original_selector }, scope))) {
|
|
922
|
+
return response.status(404).json(access_1.SELECTOR_NOT_FOUND);
|
|
923
|
+
}
|
|
902
924
|
let row;
|
|
903
925
|
switch (action) {
|
|
904
926
|
case 'mark_fixed':
|
|
@@ -920,7 +942,7 @@ function postSelectorStateAction(request, response) {
|
|
|
920
942
|
if (err instanceof SelectorStateService_1.SelectorStateConflictError) {
|
|
921
943
|
return response.status(409).json({ error: err.message, currentStatus: err.currentStatus });
|
|
922
944
|
}
|
|
923
|
-
logger_1.default.error(`[Dashboard] selector state action failed: ${(
|
|
945
|
+
logger_1.default.error(`[Dashboard] selector state action failed: ${(_e = err === null || err === void 0 ? void 0 : err.message) !== null && _e !== void 0 ? _e : err}`);
|
|
924
946
|
return response.status(500).json({ error: 'internal' });
|
|
925
947
|
}
|
|
926
948
|
});
|
|
@@ -1028,6 +1050,27 @@ function getProfilingData(request, response) {
|
|
|
1028
1050
|
return response.status(200).json(profilingData);
|
|
1029
1051
|
});
|
|
1030
1052
|
}
|
|
1053
|
+
/** A session's CPU and memory samples, for the Performance panel. */
|
|
1054
|
+
function getSessionMetrics(request, response) {
|
|
1055
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1056
|
+
var _a;
|
|
1057
|
+
const sessionId = request.params.sessionId;
|
|
1058
|
+
const [session, rows] = yield Promise.all([
|
|
1059
|
+
prisma_1.prisma.session.findFirst({
|
|
1060
|
+
where: { id: sessionId },
|
|
1061
|
+
select: { device_platform: true, status: true },
|
|
1062
|
+
}),
|
|
1063
|
+
prisma_1.prisma.sessionMetric.findMany({ where: { session_id: sessionId }, orderBy: { at: 'asc' } }),
|
|
1064
|
+
]);
|
|
1065
|
+
// A running session's page says whether it is sampled, not "Collecting…" for one nothing samples.
|
|
1066
|
+
const recording = (session === null || session === void 0 ? void 0 : session.status) === 'running'
|
|
1067
|
+
? typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).recordingState(sessionId)
|
|
1068
|
+
: null;
|
|
1069
|
+
return response
|
|
1070
|
+
.status(200)
|
|
1071
|
+
.json((0, metricsBody_1.sessionMetricsBody)((_a = session === null || session === void 0 ? void 0 : session.device_platform) !== null && _a !== void 0 ? _a : '', rows, recording));
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1031
1074
|
function streamLiveSessionVideo(request, response) {
|
|
1032
1075
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1033
1076
|
var _a;
|
|
@@ -1140,6 +1183,7 @@ function register(router) {
|
|
|
1140
1183
|
router.get('/session/:sessionId/logs/device', getDeviceLogs);
|
|
1141
1184
|
router.get('/session/:sessionId/logs/debug', getDebugLogs);
|
|
1142
1185
|
router.get('/session/:sessionId/profiling', getProfilingData);
|
|
1186
|
+
router.get('/session/:sessionId/metrics', getSessionMetrics);
|
|
1143
1187
|
router.get('/session/:sessionId/asset/:kind/:file', getSessionAsset);
|
|
1144
1188
|
// The healing reads count only heals from sessions the caller may see
|
|
1145
1189
|
// (visibleSessionWhere); the digest below counts every team's.
|
|
@@ -1149,16 +1193,18 @@ function register(router) {
|
|
|
1149
1193
|
router.get('/healing/hotspots/violations', getHealingViolations);
|
|
1150
1194
|
router.get('/healing/selector', getHealingSelectorDetail);
|
|
1151
1195
|
router.get('/healing/selector-health', getSelectorHealth);
|
|
1196
|
+
// The Selector Health page's list and panel (selector-health.ts).
|
|
1197
|
+
selector_health_1.default.register(router);
|
|
1152
1198
|
// Outbound notification — admin only since it can fan out to every
|
|
1153
1199
|
// configured webhook (Slack channels, etc.).
|
|
1154
1200
|
router.post('/healing/digest/send', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), sendHealingDigest);
|
|
1155
|
-
// SelectorState lifecycle:
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
router.post('/healing/selector/state', (0, roleGuard_1.roleGuard)('
|
|
1201
|
+
// SelectorState lifecycle: members act on selectors they can see (the
|
|
1202
|
+
// handler checks, and records who acted); the `sessions` scope is needed,
|
|
1203
|
+
// which `admin` implies. The two reads stay global, not team-scoped: a
|
|
1204
|
+
// selector's mute or fix is one lab-wide row with no team column, shared
|
|
1205
|
+
// by every team whose tests use that selector. The one heal-derived field,
|
|
1206
|
+
// the muted list's `last_healed_at`, is the caller's.
|
|
1207
|
+
router.post('/healing/selector/state', (0, roleGuard_1.roleGuard)('MEMBER'), (0, scopeGuard_1.scopeGuard)(['sessions']), postSelectorStateAction);
|
|
1162
1208
|
router.get('/healing/state/muted', getMutedSelectors);
|
|
1163
1209
|
router.get('/healing/state/:strategy/:value', getSelectorStateByTuple);
|
|
1164
1210
|
router.get('/config', getGlobalConfig);
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
exports.callerOf = void 0;
|
|
16
|
+
exports.scopeOf = scopeOf;
|
|
17
|
+
exports.getSelectorList = getSelectorList;
|
|
18
|
+
exports.getSelectorDetail = getSelectorDetail;
|
|
19
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
20
|
+
const sessionVisibility_1 = require("../../services/device-access/sessionVisibility");
|
|
21
|
+
const access_1 = require("../../services/selector-health/access");
|
|
22
|
+
const selectorDetail_1 = require("../../services/selector-health/selectorDetail");
|
|
23
|
+
const selectorList_1 = require("../../services/selector-health/selectorList");
|
|
24
|
+
const callerOf = (request) => request.auth;
|
|
25
|
+
exports.callerOf = callerOf;
|
|
26
|
+
/** The caller's sessions as a Session `where`; undefined for an admin, or with auth disabled. */
|
|
27
|
+
function scopeOf(request) {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
return (yield (0, sessionVisibility_1.visibleSessionWhere)((0, exports.callerOf)(request)));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** One tab of Selector Health's list, the four tab counts, and whether the caller may act. */
|
|
33
|
+
function getSelectorList(request, response) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
var _a, _b;
|
|
36
|
+
try {
|
|
37
|
+
const query = (0, selectorList_1.parseSelectorListQuery)(request.query);
|
|
38
|
+
const answer = yield (0, selectorList_1.listSelectors)(query, yield scopeOf(request));
|
|
39
|
+
return response
|
|
40
|
+
.status(200)
|
|
41
|
+
.json(Object.assign(Object.assign({}, answer), { canAct: (0, access_1.canActOnSelectors)((_a = (0, exports.callerOf)(request)) === null || _a === void 0 ? void 0 : _a.scopes) }));
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
logger_1.default.error(`[SelectorHealth] list failed: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
|
|
45
|
+
return response.status(500).json({ error: 'internal' });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** Everything the side panel shows for one selector, and whether the caller may act on it. */
|
|
50
|
+
function getSelectorDetail(request, response) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
const query = (0, selectorDetail_1.parseSelectorDetailQuery)(request.query);
|
|
54
|
+
if (!query) {
|
|
55
|
+
return response.status(400).json({ error: 'bad_request', message: 'selector is required' });
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const detail = yield (0, selectorDetail_1.selectorDetail)(query, yield scopeOf(request));
|
|
59
|
+
if (!detail)
|
|
60
|
+
return response.status(404).json(access_1.SELECTOR_NOT_FOUND);
|
|
61
|
+
return response
|
|
62
|
+
.status(200)
|
|
63
|
+
.json(Object.assign(Object.assign({}, detail), { canAct: (0, access_1.canActOnSelectors)((_a = (0, exports.callerOf)(request)) === null || _a === void 0 ? void 0 : _a.scopes) }));
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger_1.default.error(`[SelectorHealth] detail failed: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
|
|
67
|
+
return response.status(500).json({ error: 'internal' });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function register(router) {
|
|
72
|
+
router.get('/healing/selectors', getSelectorList);
|
|
73
|
+
router.get('/healing/selectors/detail', getSelectorDetail);
|
|
74
|
+
}
|
|
75
|
+
exports.default = { register };
|
|
@@ -218,11 +218,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
218
218
|
* post:
|
|
219
219
|
* summary: Apply a lifecycle action to a selector
|
|
220
220
|
* description: |
|
|
221
|
-
* Mutates the SelectorState row for a (strategy, selector) tuple
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
221
|
+
* Mutates the SelectorState row for a (strategy, selector) tuple and
|
|
222
|
+
* records the change, with the person, in SelectorEvent. Actions are
|
|
223
|
+
* bounded — `mark_fixed`, `mute`, `unmute`, `cancel_verification`.
|
|
224
|
+
* Members and admins may act on a selector healed in a session they
|
|
225
|
+
* can see; any other answers 404, as an unknown one. Needs the
|
|
226
|
+
* `sessions` scope (`admin` implies it). `original_strategy` may be
|
|
227
|
+
* empty, for heals recorded with no strategy. Returns 409 with
|
|
228
|
+
* `currentStatus` if the action is incompatible with the row's
|
|
229
|
+
* current state (e.g. mark_fixed on a muted selector).
|
|
226
230
|
* tags: [Selector Health]
|
|
227
231
|
* security:
|
|
228
232
|
* - apiKey: []
|
|
@@ -243,13 +247,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
243
247
|
* action:
|
|
244
248
|
* type: string
|
|
245
249
|
* enum: [mark_fixed, mute, unmute, cancel_verification]
|
|
250
|
+
* reason: { type: string, maxLength: 500, description: 'Why the selector is muted; mute only' }
|
|
246
251
|
* responses:
|
|
247
252
|
* 200:
|
|
248
253
|
* description: Updated SelectorState row
|
|
249
254
|
* 400:
|
|
250
|
-
* description: Missing fields or
|
|
255
|
+
* description: Missing fields, unknown action, or a reason over 500 characters
|
|
251
256
|
* 403:
|
|
252
|
-
* description:
|
|
257
|
+
* description: The caller lacks the `sessions` scope
|
|
258
|
+
* 404:
|
|
259
|
+
* description: '`{ error: "not_found", message: "Selector not found" }`'
|
|
253
260
|
* 409:
|
|
254
261
|
* description: Action conflicts with current state (returns currentStatus)
|
|
255
262
|
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
@@ -1221,6 +1228,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
1221
1228
|
* - in: query
|
|
1222
1229
|
* name: windowDays
|
|
1223
1230
|
* schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
|
|
1231
|
+
* - in: query
|
|
1232
|
+
* name: tz
|
|
1233
|
+
* schema: { type: integer, minimum: -840, maximum: 840, default: 0 }
|
|
1234
|
+
* description: "The caller's offset from UTC in minutes (east positive), so `trend` days are theirs"
|
|
1224
1235
|
* responses:
|
|
1225
1236
|
* 200:
|
|
1226
1237
|
* description: KPI summary
|
|
@@ -1237,15 +1248,73 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
1237
1248
|
* distinctSelectors: { type: integer }
|
|
1238
1249
|
* sessionsTouched: { type: integer }
|
|
1239
1250
|
* byTier: { type: object, additionalProperties: { type: integer } }
|
|
1240
|
-
*
|
|
1251
|
+
* timeSpentMs: { type: integer, description: 'Total duration of the commands that needed healing' }
|
|
1241
1252
|
* prior:
|
|
1242
1253
|
* type: object
|
|
1243
1254
|
* description: 'Same shape as `current` for the immediately preceding window'
|
|
1244
1255
|
* resolvedCount: { type: integer }
|
|
1245
1256
|
* pendingCount: { type: integer }
|
|
1257
|
+
* trend:
|
|
1258
|
+
* type: array
|
|
1259
|
+
* description: 'Heals per day of the period, in the caller''s time zone, days with none included'
|
|
1260
|
+
* items:
|
|
1261
|
+
* type: object
|
|
1262
|
+
* properties:
|
|
1263
|
+
* t: { type: integer, description: 'When the day began, epoch ms' }
|
|
1264
|
+
* heals: { type: integer }
|
|
1265
|
+
* aiHeals: { type: integer, description: 'Heals by Visual AI or an LLM' }
|
|
1246
1266
|
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1247
1267
|
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1248
1268
|
*/
|
|
1269
|
+
/**
|
|
1270
|
+
* @swagger
|
|
1271
|
+
* /api/healing/selectors:
|
|
1272
|
+
* get:
|
|
1273
|
+
* summary: One tab of the Selector Health list
|
|
1274
|
+
* description: |
|
|
1275
|
+
* `fix` groups the period's heals by selector and leaves out selectors
|
|
1276
|
+
* being verified, fixed or muted; the other tabs list the selectors with
|
|
1277
|
+
* that status (`fixed`: verified within the period). Only selectors
|
|
1278
|
+
* healed, at any time, in sessions the caller can see. `counts` follow
|
|
1279
|
+
* the period, not the search or filters. A page past the end answers the
|
|
1280
|
+
* last page.
|
|
1281
|
+
* tags: [Selector Health]
|
|
1282
|
+
* parameters:
|
|
1283
|
+
* - { in: query, name: tab, schema: { type: string, enum: [fix, verifying, fixed, muted], default: fix } }
|
|
1284
|
+
* - { in: query, name: days, schema: { type: integer, minimum: 1, maximum: 365, default: 30 } }
|
|
1285
|
+
* - { in: query, name: q, schema: { type: string }, description: 'Text in the selector, or (fix) in its suggested fix' }
|
|
1286
|
+
* - { in: query, name: platform, schema: { type: string }, description: 'fix only' }
|
|
1287
|
+
* - { in: query, name: method, schema: { type: string }, description: 'Healing method, e.g. LLM; fix only' }
|
|
1288
|
+
* - { in: query, name: sort, schema: { type: string, enum: [heals, recent, time], default: heals }, description: 'fix only' }
|
|
1289
|
+
* - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 } }
|
|
1290
|
+
* - { in: query, name: pageSize, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
|
|
1291
|
+
* responses:
|
|
1292
|
+
* 200: { description: '`{ tab, days, page, pageSize, total, counts, canAct, items }`' }
|
|
1293
|
+
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1294
|
+
*/
|
|
1295
|
+
/**
|
|
1296
|
+
* @swagger
|
|
1297
|
+
* /api/healing/selectors/detail:
|
|
1298
|
+
* get:
|
|
1299
|
+
* summary: Everything the Selector Health side panel shows for one selector
|
|
1300
|
+
* description: |
|
|
1301
|
+
* Its heals in the period (suggested fixes with their share, methods and
|
|
1302
|
+
* confidence; heals per day; platforms, builds and devices; the latest 20
|
|
1303
|
+
* heals), its status, the latest 50 status changes with who made them,
|
|
1304
|
+
* and whether the caller may act. A selector no session the caller can
|
|
1305
|
+
* see has healed answers 404, exactly as an unknown one.
|
|
1306
|
+
* tags: [Selector Health]
|
|
1307
|
+
* parameters:
|
|
1308
|
+
* - { in: query, name: selector, required: true, schema: { type: string } }
|
|
1309
|
+
* - { in: query, name: strategy, schema: { type: string }, description: "Empty for heals recorded with no strategy" }
|
|
1310
|
+
* - { in: query, name: days, schema: { type: integer, minimum: 1, maximum: 365, default: 30 } }
|
|
1311
|
+
* - { in: query, name: tz, schema: { type: integer, minimum: -840, maximum: 840, default: 0 } }
|
|
1312
|
+
* responses:
|
|
1313
|
+
* 200: { description: 'The panel for one selector' }
|
|
1314
|
+
* 400: { description: 'No selector given' }
|
|
1315
|
+
* 404: { description: '`{ error: "not_found", message: "Selector not found" }`' }
|
|
1316
|
+
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1317
|
+
*/
|
|
1249
1318
|
/**
|
|
1250
1319
|
* @swagger
|
|
1251
1320
|
* /api/healing/digest/send:
|
|
@@ -64,8 +64,6 @@ const asset_manager_1 = require("./asset-manager");
|
|
|
64
64
|
const commands_1 = require("./commands");
|
|
65
65
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
66
66
|
const appium_ios_device_1 = require("appium-ios-device");
|
|
67
|
-
const AndroidAppProfiler_1 = require("../profiling/AndroidAppProfiler");
|
|
68
|
-
const appium_adb_1 = require("appium-adb");
|
|
69
67
|
const config_1 = require("../config");
|
|
70
68
|
const helpers_2 = require("../helpers");
|
|
71
69
|
const typedi_1 = require("typedi");
|
|
@@ -80,6 +78,7 @@ const SocketEvents_1 = require("../enums/SocketEvents");
|
|
|
80
78
|
const types_1 = require("../services/healing/types");
|
|
81
79
|
const SelectorStateService_1 = require("../services/SelectorStateService");
|
|
82
80
|
const recording_store_1 = require("../services/recording/recording-store");
|
|
81
|
+
const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
|
|
83
82
|
const typedi_2 = require("typedi");
|
|
84
83
|
/**
|
|
85
84
|
* What a Session row keeps of the capabilities the driver returned. The row
|
|
@@ -105,8 +104,6 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
105
104
|
this.sessionToUdid = new Map();
|
|
106
105
|
// Map session ID to device info
|
|
107
106
|
this.sessionToDevice = new Map();
|
|
108
|
-
// Store app profilers for Android sessions
|
|
109
|
-
this.appProfilers = new Map();
|
|
110
107
|
// Track last log line for each session (for device logs)
|
|
111
108
|
this.lastLogLine = new Map();
|
|
112
109
|
// Track start time for each command to calculate duration
|
|
@@ -124,8 +121,10 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
124
121
|
};
|
|
125
122
|
// create directory to store screenshots, videos and log files for the session
|
|
126
123
|
(0, asset_manager_1.prepareDirectory)(session.getId());
|
|
127
|
-
//
|
|
128
|
-
|
|
124
|
+
// CPU and memory for the session page's Performance panel; sampling
|
|
125
|
+
// starts once the session's row exists, since the samples point at it.
|
|
126
|
+
const metrics = typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService);
|
|
127
|
+
const sampled = metrics.appliesTo(device);
|
|
129
128
|
// If iOS real device, start performance recording (Time Profiler)
|
|
130
129
|
// Note: This only works on real devices with XCUITest driver 4.5+
|
|
131
130
|
let isIosProfilingStarted = false;
|
|
@@ -149,10 +148,17 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
149
148
|
const sessionResponse = lodash_1.default.assign({}, session.getCapabilities());
|
|
150
149
|
const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
|
|
151
150
|
const traceId = tracingService.getTraceId(session.getId());
|
|
152
|
-
const createData = Object.assign(Object.assign({ id: session.getId(), build: build.id ? { connect: { id: build.id } } : undefined, name: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined }, storedSessionCapabilities(sessionResponse)), { node_id: device.nodeId || '', has_live_video: session.getLiveVideoUrl() !== null, video_recording_enabled: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true, is_profiling_available:
|
|
151
|
+
const createData = Object.assign(Object.assign({ id: session.getId(), build: build.id ? { connect: { id: build.id } } : undefined, name: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined }, storedSessionCapabilities(sessionResponse)), { node_id: device.nodeId || '', has_live_video: session.getLiveVideoUrl() !== null, video_recording_enabled: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true, is_profiling_available: sampled || isIosProfilingStarted, device_udid: device.udid || '', device_platform: device.platform || '', device_version: device.sdk || '', device_name: device.name, trace_id: traceId, status: 'running', api_key_id: (_a = session.apiKeyId) !== null && _a !== void 0 ? _a : null, user_id: (_b = session.userId) !== null && _b !== void 0 ? _b : null });
|
|
153
152
|
yield prisma_1.prisma.session.create({
|
|
154
153
|
data: createData,
|
|
155
154
|
});
|
|
155
|
+
if (sampled) {
|
|
156
|
+
metrics.start({
|
|
157
|
+
sessionId: session.getId(),
|
|
158
|
+
device,
|
|
159
|
+
capabilities: session.getCapabilities(),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
156
162
|
// Emit session started event
|
|
157
163
|
void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });
|
|
158
164
|
// Increment Metrics
|
|
@@ -198,14 +204,14 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
198
204
|
this.stoppingSessionIds.add(sessionId);
|
|
199
205
|
try {
|
|
200
206
|
logger_1.default.info(`🟢 onSessionStopped called for session ${sessionId}`);
|
|
207
|
+
// However the session ended, in memory or not, its sampler stops here.
|
|
208
|
+
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
201
209
|
// Video recording is now handled in plugin.ts deleteSession() before the session is deleted
|
|
202
210
|
// This ensures we can call stop_recording_screen while the session is still active
|
|
203
211
|
// Here we just handle the session status update
|
|
204
212
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
205
213
|
if (session) {
|
|
206
214
|
logger_1.default.info(`Session ${sessionId} found in SESSION_MANAGER`);
|
|
207
|
-
// Save Android profiling data before cleanup
|
|
208
|
-
yield this.saveAppProfilingData(sessionId);
|
|
209
215
|
// iOS profiling is now handled in plugin.ts deleteSession() before the session is deleted
|
|
210
216
|
// This ensures we can call mobile: stopPerfRecord while the driver is still alive
|
|
211
217
|
// Clean up syslog service for real iOS devices
|
|
@@ -689,96 +695,6 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
689
695
|
}
|
|
690
696
|
});
|
|
691
697
|
}
|
|
692
|
-
startAppProfiling(sessionId, device, sessionCapabilities) {
|
|
693
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
694
|
-
// Only support Android profiling
|
|
695
|
-
if (device.platform.toLowerCase() !== 'android') {
|
|
696
|
-
return { is_profiling_available: false };
|
|
697
|
-
}
|
|
698
|
-
// Check if we have an app package
|
|
699
|
-
const appPackage = (sessionCapabilities === null || sessionCapabilities === void 0 ? void 0 : sessionCapabilities.appPackage) || (sessionCapabilities === null || sessionCapabilities === void 0 ? void 0 : sessionCapabilities['appium:appPackage']);
|
|
700
|
-
if (!appPackage) {
|
|
701
|
-
logger_1.default.info(`[Profiling] No app package found for session ${sessionId}, skipping profiling`);
|
|
702
|
-
return { is_profiling_available: false };
|
|
703
|
-
}
|
|
704
|
-
try {
|
|
705
|
-
// Principal Intelligence: Use the Properly configured ADB from the manager
|
|
706
|
-
// instead of a naked instance, to avoid 'defaultArgs not iterable' crashes.
|
|
707
|
-
const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
|
|
708
|
-
const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
|
|
709
|
-
let adb;
|
|
710
|
-
if (androidManager) {
|
|
711
|
-
adb = yield androidManager.getAdbForDevice(device.udid);
|
|
712
|
-
}
|
|
713
|
-
else {
|
|
714
|
-
adb = yield appium_adb_1.ADB.createADB({});
|
|
715
|
-
}
|
|
716
|
-
// Create app profiler
|
|
717
|
-
const profiler = new AndroidAppProfiler_1.AndroidAppProfiler({
|
|
718
|
-
adb,
|
|
719
|
-
deviceUDID: device.udid,
|
|
720
|
-
appPackage,
|
|
721
|
-
});
|
|
722
|
-
// Get device info and start capture
|
|
723
|
-
const device_info = yield profiler.getDeviceInfo();
|
|
724
|
-
yield profiler.startCapture();
|
|
725
|
-
// Store profiler for this session
|
|
726
|
-
this.appProfilers.set(sessionId, profiler);
|
|
727
|
-
logger_1.default.info(`[Profiling] Started app profiling for session ${sessionId}`);
|
|
728
|
-
return {
|
|
729
|
-
is_profiling_available: true,
|
|
730
|
-
device_info,
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
catch (err) {
|
|
734
|
-
logger_1.default.error(`[Profiling] Error initializing app profiler for session ${sessionId}: ${err.message}`);
|
|
735
|
-
return { is_profiling_available: false };
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
saveAppProfilingData(sessionId) {
|
|
740
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
741
|
-
const profiler = this.appProfilers.get(sessionId);
|
|
742
|
-
if (!profiler) {
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
try {
|
|
746
|
-
// Stop capture
|
|
747
|
-
yield profiler.stopCapture();
|
|
748
|
-
// Get logs
|
|
749
|
-
const logs = profiler.getLogs();
|
|
750
|
-
if (logs.length === 0) {
|
|
751
|
-
logger_1.default.info(`[Profiling] No profiling data to save for session ${sessionId}`);
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
// Save to database
|
|
755
|
-
const profilingEntries = logs.map((log) => ({
|
|
756
|
-
session_id: sessionId,
|
|
757
|
-
cpu: log.cpu,
|
|
758
|
-
memory: log.memory,
|
|
759
|
-
total_cpu_used: log.total_cpu_used.toString(),
|
|
760
|
-
total_memory_used: log.total_memory_used.toString(),
|
|
761
|
-
raw_cpu_log: log.raw_cpu_log,
|
|
762
|
-
raw_memory_log: log.raw_memory_log,
|
|
763
|
-
timestamp: new Date(log.timestamp),
|
|
764
|
-
}));
|
|
765
|
-
// Batch insert profiling data
|
|
766
|
-
for (const entry of profilingEntries) {
|
|
767
|
-
yield prisma_1.prisma.profiling.create({
|
|
768
|
-
data: entry,
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
logger_1.default.info(`[Profiling] Saved ${profilingEntries.length} profiling entries for session ${sessionId}`);
|
|
772
|
-
}
|
|
773
|
-
catch (err) {
|
|
774
|
-
logger_1.default.error(`[Profiling] Error saving profiling data for session ${sessionId}: ${err.message}`);
|
|
775
|
-
}
|
|
776
|
-
finally {
|
|
777
|
-
// Clean up profiler
|
|
778
|
-
this.appProfilers.delete(sessionId);
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
698
|
// ─── Recording (free-form mosaic) events ─────────────────────────────────
|
|
783
699
|
// Emitted by RecordingOrchestrator. Distinct namespace from SESSION_*; the
|
|
784
700
|
// dashboard listens to both independently. Team-scoped: a group's started
|