@xenon-device-management/xenon 2.13.2 → 2.14.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/README.md +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +19 -21
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +12 -2
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +62 -2
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +150 -8
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +96 -40
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +94 -8
- package/lib/src/services/SessionLifecycleService.js +47 -49
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -21
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
|
@@ -9,10 +9,15 @@ exports.resolveStreamType = resolveStreamType;
|
|
|
9
9
|
* MJPEG path so the recording pipeline (which reads the MJPEG server) is
|
|
10
10
|
* unaffected (spec phase-1 rule). iOS/tvOS always use MJPEG (WDA emits it
|
|
11
11
|
* directly).
|
|
12
|
+
*
|
|
13
|
+
* `clientCanPlayH264` is false when the page that starts the stream shows
|
|
14
|
+
* MJPEG: device control, or a browser without WebCodecs (exposed only on
|
|
15
|
+
* https and localhost). Starting an H.264 capture for it would run a second
|
|
16
|
+
* capture beside the screencap loop its `<img>` starts.
|
|
12
17
|
*/
|
|
13
|
-
function resolveStreamType(platform, flagOn, recording) {
|
|
18
|
+
function resolveStreamType(platform, flagOn, recording, clientCanPlayH264 = true) {
|
|
14
19
|
const isAndroid = platform === 'android' || platform === 'androidtv' || platform === 'android-tv';
|
|
15
|
-
if (!isAndroid || !flagOn || recording)
|
|
20
|
+
if (!isAndroid || !flagOn || recording || !clientCanPlayH264)
|
|
16
21
|
return 'mjpeg';
|
|
17
22
|
return 'h264';
|
|
18
23
|
}
|
|
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const express_1 = require("express");
|
|
16
16
|
const NotificationService_1 = require("../../services/NotificationService");
|
|
17
|
+
const webhookEvents_1 = require("../../services/webhookEvents");
|
|
17
18
|
const typedi_1 = require("typedi");
|
|
18
19
|
const logger_1 = __importDefault(require("../../logger"));
|
|
19
20
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
@@ -63,17 +64,20 @@ function deleteConfig(req, res) {
|
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
|
-
// Sends a sample event the way the webhook's real events go out (its
|
|
67
|
-
// and template), and says whether it was delivered.
|
|
67
|
+
// Sends a sample of an event the way the webhook's real events go out (its
|
|
68
|
+
// type and template), and says whether it was delivered.
|
|
68
69
|
function testWebhook(req, res) {
|
|
69
70
|
return __awaiter(this, void 0, void 0, function* () {
|
|
70
71
|
var _a, _b;
|
|
71
|
-
const { url, type, payloadTemplate } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
|
|
72
|
+
const { url, type, payloadTemplate, event } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
|
|
72
73
|
if (!url || typeof url !== 'string') {
|
|
73
74
|
return res.status(400).json({ error: 'url is required' });
|
|
74
75
|
}
|
|
76
|
+
if (event !== undefined && !(0, webhookEvents_1.isEventType)(event)) {
|
|
77
|
+
return res.status(400).json({ error: `event must be one of: ${webhookEvents_1.EVENT_TYPES.join(', ')}` });
|
|
78
|
+
}
|
|
75
79
|
try {
|
|
76
|
-
yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate);
|
|
80
|
+
yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate, event);
|
|
77
81
|
res.json({ success: true });
|
|
78
82
|
}
|
|
79
83
|
catch (err) {
|
package/lib/src/config.js
CHANGED
|
@@ -32,14 +32,20 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var _a;
|
|
36
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.config = void 0;
|
|
36
|
+
exports.config = exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = void 0;
|
|
38
37
|
exports.updateConfig = updateConfig;
|
|
39
38
|
exports.resolveAuthDisabled = resolveAuthDisabled;
|
|
39
|
+
exports.resolveMaxConcurrentRecordings = resolveMaxConcurrentRecordings;
|
|
40
|
+
exports.resolveRecordingsAssetsPath = resolveRecordingsAssetsPath;
|
|
41
|
+
exports.recordingConfigFrom = recordingConfigFrom;
|
|
40
42
|
const os = __importStar(require("os"));
|
|
41
43
|
const path = __importStar(require("path"));
|
|
42
44
|
const basePath = path.join(os.homedir(), '.cache', 'xenon');
|
|
45
|
+
/** Where free-form recordings go unless the option or the environment says otherwise. */
|
|
46
|
+
const DEFAULT_RECORDINGS_ASSETS_PATH = path.join(basePath, 'assets', 'sessions', 'recordings');
|
|
47
|
+
/** The server-wide cap on simultaneous free-form recordings, unless set. */
|
|
48
|
+
exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = 4;
|
|
43
49
|
exports.config = {
|
|
44
50
|
cacheDir: basePath,
|
|
45
51
|
databaseProvider: process.env.XENON_DB_PROVIDER || 'sqlite',
|
|
@@ -87,9 +93,8 @@ exports.config = {
|
|
|
87
93
|
passwordResetLogFallback: process.env.XENON_PASSWORD_RESET_LOG_FALLBACK === 'true',
|
|
88
94
|
resetRateLimitAttempts: Number(process.env.XENON_RESET_RATE_LIMIT_ATTEMPTS) || 3,
|
|
89
95
|
resetRateLimitWindowMs: Number(process.env.XENON_RESET_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
|
90
|
-
recordingsAssetsPath: process.env.XENON_RECORDINGS_ASSETS_PATH
|
|
91
|
-
|
|
92
|
-
maxConcurrentRecordings: Number((_a = process.env.XENON_MAX_CONCURRENT_RECORDINGS) !== null && _a !== void 0 ? _a : 4),
|
|
96
|
+
recordingsAssetsPath: resolveRecordingsAssetsPath(undefined, process.env.XENON_RECORDINGS_ASSETS_PATH),
|
|
97
|
+
maxConcurrentRecordings: resolveMaxConcurrentRecordings(undefined, process.env.XENON_MAX_CONCURRENT_RECORDINGS),
|
|
93
98
|
};
|
|
94
99
|
function updateConfig(newConfig) {
|
|
95
100
|
Object.assign(exports.config, newConfig);
|
|
@@ -113,3 +118,43 @@ function updateConfig(newConfig) {
|
|
|
113
118
|
function resolveAuthDisabled(pluginArgValue, envDisabled) {
|
|
114
119
|
return pluginArgValue === true || envDisabled === true;
|
|
115
120
|
}
|
|
121
|
+
function wholeNumberAtLeastOne(value) {
|
|
122
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 ? value : undefined;
|
|
123
|
+
}
|
|
124
|
+
function nonBlankString(value) {
|
|
125
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The cap on simultaneous free-form recordings: the `maxConcurrentRecordings`
|
|
129
|
+
* plugin option when it is set, else XENON_MAX_CONCURRENT_RECORDINGS, else 4.
|
|
130
|
+
* Both options are `schema.json` options Appium accepts, but only the variable
|
|
131
|
+
* was ever read. A value that is not a whole number of at least 1 is ignored,
|
|
132
|
+
* so the next source answers: the variable used to be taken as `Number(env)`,
|
|
133
|
+
* and `many` made that NaN, which no recording count exceeds (no cap at all).
|
|
134
|
+
* Pure, so the precedence is testable without booting a server.
|
|
135
|
+
*/
|
|
136
|
+
function resolveMaxConcurrentRecordings(optionValue, envValue) {
|
|
137
|
+
var _a, _b;
|
|
138
|
+
const fromEnv = (envValue === null || envValue === void 0 ? void 0 : envValue.trim()) ? Number(envValue) : undefined;
|
|
139
|
+
return ((_b = (_a = wholeNumberAtLeastOne(optionValue)) !== null && _a !== void 0 ? _a : wholeNumberAtLeastOne(fromEnv)) !== null && _b !== void 0 ? _b : exports.DEFAULT_MAX_CONCURRENT_RECORDINGS);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Where free-form recordings are stored: the `recordingsAssetsPath` plugin
|
|
143
|
+
* option when it is set, else XENON_RECORDINGS_ASSETS_PATH, else
|
|
144
|
+
* `~/.cache/xenon/assets/sessions/recordings`. A blank value counts as unset.
|
|
145
|
+
*/
|
|
146
|
+
function resolveRecordingsAssetsPath(optionValue, envValue) {
|
|
147
|
+
var _a, _b;
|
|
148
|
+
return (_b = (_a = nonBlankString(optionValue)) !== null && _a !== void 0 ? _a : nonBlankString(envValue)) !== null && _b !== void 0 ? _b : DEFAULT_RECORDINGS_ASSETS_PATH;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The part of `config` the recording plugin options set, from the options the
|
|
152
|
+
* server started with and the environment as it is now. Applied at boot,
|
|
153
|
+
* before the artifact store and the concurrency gate read them.
|
|
154
|
+
*/
|
|
155
|
+
function recordingConfigFrom(pluginArgs, env = process.env) {
|
|
156
|
+
return {
|
|
157
|
+
maxConcurrentRecordings: resolveMaxConcurrentRecordings(pluginArgs.maxConcurrentRecordings, env.XENON_MAX_CONCURRENT_RECORDINGS),
|
|
158
|
+
recordingsAssetsPath: resolveRecordingsAssetsPath(pluginArgs.recordingsAssetsPath, env.XENON_RECORDINGS_ASSETS_PATH),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -45,56 +45,123 @@ 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.dashboardCommands = exports.DashboardCommands = void 0;
|
|
48
|
+
exports.dashboardCommands = exports.DashboardCommands = exports.SESSION_DETAILS_COMMANDS = void 0;
|
|
49
|
+
exports.sessionDetailsCommandOf = sessionDetailsCommandOf;
|
|
49
50
|
const session_service_1 = require("./services/session-service");
|
|
50
51
|
const prisma_1 = require("../prisma");
|
|
51
52
|
const logger_1 = __importDefault(require("../logger"));
|
|
52
|
-
const lodash_1 = __importDefault(require("lodash"));
|
|
53
53
|
const SessionManager_1 = require("../sessions/SessionManager");
|
|
54
54
|
const asset_manager_1 = require("./asset-manager");
|
|
55
|
+
const xenonScripts_1 = require("../interceptors/xenonScripts");
|
|
56
|
+
/**
|
|
57
|
+
* The execute commands that write to a session's record (its Session row):
|
|
58
|
+
* `driver.execute('xenon: setSessionName', 'Checkout')` and the like.
|
|
59
|
+
*
|
|
60
|
+
* Each answers `{ recorded: true }` when it wrote, or `{ recorded: false,
|
|
61
|
+
* message }` saying why nothing was written: no record of the session on this
|
|
62
|
+
* server (its dashboard is off, or it is a node, which keeps none for the
|
|
63
|
+
* hub's sessions), an argument it can't use, or a failed write. None of them
|
|
64
|
+
* fails the test's command. They are bookkeeping, and a test shouldn't die
|
|
65
|
+
* because the server it ran on keeps no dashboard. Through 2.13.2
|
|
66
|
+
* setSessionName, setSessionStatus and debug failed the command there
|
|
67
|
+
* (Prisma's P2025, a foreign-key error), and addTag and captureEvidence
|
|
68
|
+
* answered `null` as if they had worked.
|
|
69
|
+
*/
|
|
70
|
+
exports.SESSION_DETAILS_COMMANDS = [
|
|
71
|
+
'setSessionName',
|
|
72
|
+
'setSessionStatus',
|
|
73
|
+
'debug',
|
|
74
|
+
'addTag',
|
|
75
|
+
'captureEvidence',
|
|
76
|
+
];
|
|
77
|
+
const RECORDED = { recorded: true };
|
|
78
|
+
const notRecorded = (message) => ({ recorded: false, message });
|
|
79
|
+
/** The session-details command a script names (`xenon: addTag`), or null for any other script. */
|
|
80
|
+
function sessionDetailsCommandOf(script) {
|
|
81
|
+
const name = (0, xenonScripts_1.xenonScriptName)(script);
|
|
82
|
+
return name !== null && exports.SESSION_DETAILS_COMMANDS.includes(name)
|
|
83
|
+
? name
|
|
84
|
+
: null;
|
|
85
|
+
}
|
|
86
|
+
/** The script's arguments as a list, as both the W3C body and Appium's executeScript give them. */
|
|
87
|
+
function argumentList(args) {
|
|
88
|
+
if (Array.isArray(args))
|
|
89
|
+
return args;
|
|
90
|
+
return args === undefined || args === null ? [] : [args];
|
|
91
|
+
}
|
|
92
|
+
function isObject(value) {
|
|
93
|
+
return typeof value === 'object' && value !== null;
|
|
94
|
+
}
|
|
95
|
+
/** A string argument, or the named field of an object argument. */
|
|
96
|
+
function stringArg(value, field) {
|
|
97
|
+
if (typeof value === 'string')
|
|
98
|
+
return value;
|
|
99
|
+
if (isObject(value) && typeof value[field] === 'string')
|
|
100
|
+
return value[field];
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
55
103
|
class DashboardCommands {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Answers a session-details command on an Express-style response, as
|
|
106
|
+
* `{ value: <answer> }`, and returns the answer. The hub's gateway passes
|
|
107
|
+
* the client's response; CommandInterceptor passes one that keeps it.
|
|
108
|
+
*/
|
|
59
109
|
process(sessionId, request, response) {
|
|
60
110
|
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
var _a;
|
|
112
|
+
const { script, args } = (_a = request.body) !== null && _a !== void 0 ? _a : {};
|
|
113
|
+
const command = sessionDetailsCommandOf(script);
|
|
114
|
+
const answer = command
|
|
115
|
+
? yield this.run(sessionId, command, args)
|
|
116
|
+
: notRecorded(`"${script}" is not a command that records session details.`);
|
|
117
|
+
response.status(200).json({ value: answer });
|
|
118
|
+
return answer;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Runs one command. Never throws: a failure is `{ recorded: false, message }`. */
|
|
122
|
+
run(sessionId, command, args) {
|
|
123
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
var _a;
|
|
125
|
+
logger_1.default.info(`[DashboardCommands] ${command} for session ${sessionId}`);
|
|
126
|
+
const list = argumentList(args);
|
|
127
|
+
let answer;
|
|
128
|
+
try {
|
|
129
|
+
const row = yield prisma_1.prisma.session.findUnique({
|
|
130
|
+
where: { id: sessionId },
|
|
131
|
+
select: { id: true, tags: true },
|
|
132
|
+
});
|
|
133
|
+
if (!row) {
|
|
134
|
+
answer = notRecorded(`This server keeps no record of session ${sessionId}, so nothing was saved. ` +
|
|
135
|
+
'Sessions are recorded on a hub with its dashboard turned on.');
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
switch (command) {
|
|
139
|
+
case 'setSessionName':
|
|
140
|
+
answer = yield this.setSessionName(sessionId, list);
|
|
141
|
+
break;
|
|
142
|
+
case 'setSessionStatus':
|
|
143
|
+
answer = yield this.setSessionStatus(sessionId, list);
|
|
144
|
+
break;
|
|
145
|
+
case 'debug':
|
|
146
|
+
answer = yield this.debug(sessionId, list);
|
|
147
|
+
break;
|
|
148
|
+
case 'captureEvidence':
|
|
149
|
+
answer = yield this.captureEvidence(sessionId, list);
|
|
150
|
+
break;
|
|
151
|
+
case 'addTag':
|
|
152
|
+
answer = yield this.addTag(sessionId, list, row.tags);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
90
155
|
}
|
|
91
156
|
}
|
|
92
|
-
|
|
157
|
+
catch (err) {
|
|
158
|
+
answer = notRecorded(`Xenon could not save it: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
159
|
+
}
|
|
160
|
+
if (!answer.recorded)
|
|
161
|
+
logger_1.default.warn(`[DashboardCommands] ${command}: ${answer.message}`);
|
|
162
|
+
return answer;
|
|
93
163
|
});
|
|
94
164
|
}
|
|
95
|
-
sendSuccessResponse(response) {
|
|
96
|
-
return response.status(200).json({ value: null });
|
|
97
|
-
}
|
|
98
165
|
/* Commands */
|
|
99
166
|
/**
|
|
100
167
|
* Set the name of current test(session)
|
|
@@ -103,63 +170,50 @@ class DashboardCommands {
|
|
|
103
170
|
* or
|
|
104
171
|
* driver.executeScript("xenon: setSessionName", {"name": "MyTestName"})
|
|
105
172
|
*/
|
|
106
|
-
setSessionName(sessionId,
|
|
173
|
+
setSessionName(sessionId, args) {
|
|
107
174
|
return __awaiter(this, void 0, void 0, function* () {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
name
|
|
111
|
-
}
|
|
112
|
-
|
|
175
|
+
const name = stringArg(args[0], 'name');
|
|
176
|
+
if (!name || !name.trim()) {
|
|
177
|
+
return notRecorded('setSessionName needs a name, as a string or { name }.');
|
|
178
|
+
}
|
|
179
|
+
yield (0, session_service_1.updateSessionDetails)(sessionId, { name });
|
|
180
|
+
return RECORDED;
|
|
113
181
|
});
|
|
114
182
|
}
|
|
115
183
|
/**
|
|
116
184
|
* Update the status of the session
|
|
117
185
|
*
|
|
118
186
|
* driver.executeScript("xenon: setSessionStatus", {"status": "passed/failed", "reason": "optional reason"})
|
|
187
|
+
* or, positionally, ("xe:setSessionStatus", "failed", "reason")
|
|
119
188
|
*/
|
|
120
|
-
setSessionStatus(sessionId,
|
|
189
|
+
setSessionStatus(sessionId, args) {
|
|
121
190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
-
const { args
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (typeof args[0] === 'object') {
|
|
126
|
-
statusArg = args[0];
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
// Handle positional: executeScript("xe:setSessionStatus", "failed", "reason")
|
|
130
|
-
statusArg = {
|
|
131
|
-
status: args[0],
|
|
132
|
-
reason: args[1],
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
else if (typeof args === 'object') {
|
|
137
|
-
statusArg = args;
|
|
138
|
-
}
|
|
139
|
-
// Normalize input
|
|
140
|
-
const inputStatus = (statusArg.status || '').toLowerCase();
|
|
191
|
+
const statusArg = isObject(args[0]) ? args[0] : { status: args[0], reason: args[1] };
|
|
192
|
+
const given = statusArg.status;
|
|
193
|
+
const inputStatus = typeof given === 'string' ? given.toLowerCase() : '';
|
|
141
194
|
const validStatuses = ['success', 'failed', 'passed'];
|
|
142
195
|
if (!validStatuses.includes(inputStatus)) {
|
|
143
|
-
|
|
144
|
-
|
|
196
|
+
return notRecorded('setSessionStatus takes "passed" or "failed" ("success" also means passed); ' +
|
|
197
|
+
`it was given ${JSON.stringify(given !== null && given !== void 0 ? given : null)}.`);
|
|
145
198
|
}
|
|
146
199
|
// Normalize 'passed' to 'success' for consistency with SessionStatus enum
|
|
147
200
|
const normalizedStatus = inputStatus === 'passed' ? 'success' : inputStatus;
|
|
201
|
+
const reason = typeof statusArg.reason === 'string' ? statusArg.reason : undefined;
|
|
148
202
|
const updated = yield (0, session_service_1.updateSessionDetails)(sessionId, {
|
|
149
203
|
status: normalizedStatus,
|
|
150
|
-
failure_reason:
|
|
204
|
+
failure_reason: reason || undefined,
|
|
151
205
|
});
|
|
152
206
|
// --- REAL-TIME UI UPDATE ---
|
|
153
|
-
//
|
|
207
|
+
// Emit a socket event so the dashboard row updates immediately.
|
|
154
208
|
const { SocketServer } = yield Promise.resolve().then(() => __importStar(require('../services/SocketServer')));
|
|
155
209
|
const { Container } = yield Promise.resolve().then(() => __importStar(require('typedi')));
|
|
156
210
|
const { SocketEvents } = yield Promise.resolve().then(() => __importStar(require('../enums/SocketEvents')));
|
|
157
211
|
void Container.get(SocketServer).emitToDashboardForDevices(SocketEvents.SESSION_STOPPED, {
|
|
158
212
|
id: sessionId,
|
|
159
213
|
status: normalizedStatus,
|
|
160
|
-
failure_reason:
|
|
214
|
+
failure_reason: reason || undefined,
|
|
161
215
|
}, { udid: updated === null || updated === void 0 ? void 0 : updated.device_udid });
|
|
162
|
-
return
|
|
216
|
+
return RECORDED;
|
|
163
217
|
});
|
|
164
218
|
}
|
|
165
219
|
/**
|
|
@@ -169,12 +223,18 @@ class DashboardCommands {
|
|
|
169
223
|
* or
|
|
170
224
|
* driver.executeScript("xenon: debug", "Debug message")
|
|
171
225
|
*/
|
|
172
|
-
debug(sessionId,
|
|
226
|
+
debug(sessionId, args) {
|
|
173
227
|
return __awaiter(this, void 0, void 0, function* () {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
228
|
+
const logData = args[0];
|
|
229
|
+
let message;
|
|
230
|
+
if (typeof logData === 'string')
|
|
231
|
+
message = logData;
|
|
232
|
+
else if (isObject(logData)) {
|
|
233
|
+
message = typeof logData.message === 'string' ? logData.message : JSON.stringify(logData);
|
|
234
|
+
}
|
|
235
|
+
if (!message) {
|
|
236
|
+
return notRecorded('debug needs a message, as a string or { message }.');
|
|
237
|
+
}
|
|
178
238
|
yield prisma_1.prisma.log.create({
|
|
179
239
|
data: {
|
|
180
240
|
session_id: sessionId,
|
|
@@ -182,7 +242,7 @@ class DashboardCommands {
|
|
|
182
242
|
message: message,
|
|
183
243
|
},
|
|
184
244
|
});
|
|
185
|
-
return
|
|
245
|
+
return RECORDED;
|
|
186
246
|
});
|
|
187
247
|
}
|
|
188
248
|
/**
|
|
@@ -192,44 +252,43 @@ class DashboardCommands {
|
|
|
192
252
|
* or
|
|
193
253
|
* driver.executeScript("xenon: captureEvidence", "Checkpoint reached")
|
|
194
254
|
*/
|
|
195
|
-
captureEvidence(sessionId,
|
|
255
|
+
captureEvidence(sessionId, args) {
|
|
196
256
|
return __awaiter(this, void 0, void 0, function* () {
|
|
197
|
-
|
|
198
|
-
const evidenceData =
|
|
199
|
-
const reason =
|
|
200
|
-
const label =
|
|
257
|
+
var _a;
|
|
258
|
+
const evidenceData = args[0];
|
|
259
|
+
const reason = stringArg(evidenceData, 'reason') || 'Manual capture';
|
|
260
|
+
const label = isObject(evidenceData) ? evidenceData.label : undefined;
|
|
261
|
+
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
262
|
+
if (!session) {
|
|
263
|
+
return notRecorded(`Session ${sessionId} isn't running here, so no screenshot was taken.`);
|
|
264
|
+
}
|
|
265
|
+
let screenshotBase64;
|
|
201
266
|
try {
|
|
202
|
-
|
|
203
|
-
if (!session) {
|
|
204
|
-
logger_1.default.warn(`[DashboardCommands] captureEvidence: Session ${sessionId} not found`);
|
|
205
|
-
return this.sendSuccessResponse(response);
|
|
206
|
-
}
|
|
207
|
-
const screenshotBase64 = yield session.getScreenShot();
|
|
208
|
-
if (screenshotBase64) {
|
|
209
|
-
const screenshotPath = (0, asset_manager_1.saveScreenShot)(sessionId, screenshotBase64);
|
|
210
|
-
// Log the evidence capture
|
|
211
|
-
yield prisma_1.prisma.sessionLog.create({
|
|
212
|
-
data: {
|
|
213
|
-
session_id: sessionId,
|
|
214
|
-
command_name: 'captureEvidence',
|
|
215
|
-
body: JSON.stringify({ reason, label }),
|
|
216
|
-
response: JSON.stringify({ captured: true }),
|
|
217
|
-
is_success: true,
|
|
218
|
-
is_error: false,
|
|
219
|
-
method: 'POST',
|
|
220
|
-
title: 'Evidence Captured',
|
|
221
|
-
subtitle: reason,
|
|
222
|
-
screenshot: screenshotPath,
|
|
223
|
-
url: '/execute/sync',
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
logger_1.default.info(`[DashboardCommands] Evidence captured for ${sessionId}: ${reason}`);
|
|
227
|
-
}
|
|
267
|
+
screenshotBase64 = yield session.getScreenShot();
|
|
228
268
|
}
|
|
229
269
|
catch (err) {
|
|
230
|
-
|
|
270
|
+
return notRecorded(`No screenshot could be taken: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
231
271
|
}
|
|
232
|
-
|
|
272
|
+
if (!screenshotBase64)
|
|
273
|
+
return notRecorded('No screenshot could be taken.');
|
|
274
|
+
const screenshotPath = (0, asset_manager_1.saveScreenShot)(sessionId, screenshotBase64);
|
|
275
|
+
yield prisma_1.prisma.sessionLog.create({
|
|
276
|
+
data: {
|
|
277
|
+
session_id: sessionId,
|
|
278
|
+
command_name: 'captureEvidence',
|
|
279
|
+
body: JSON.stringify({ reason, label }),
|
|
280
|
+
response: JSON.stringify({ captured: true }),
|
|
281
|
+
is_success: true,
|
|
282
|
+
is_error: false,
|
|
283
|
+
method: 'POST',
|
|
284
|
+
title: 'Evidence Captured',
|
|
285
|
+
subtitle: reason,
|
|
286
|
+
screenshot: screenshotPath,
|
|
287
|
+
url: '/execute/sync',
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
logger_1.default.info(`[DashboardCommands] Evidence captured for ${sessionId}: ${reason}`);
|
|
291
|
+
return RECORDED;
|
|
233
292
|
});
|
|
234
293
|
}
|
|
235
294
|
/**
|
|
@@ -239,44 +298,29 @@ class DashboardCommands {
|
|
|
239
298
|
* or
|
|
240
299
|
* driver.executeScript("xenon: addTag", "smoke-test")
|
|
241
300
|
*/
|
|
242
|
-
addTag(sessionId,
|
|
301
|
+
addTag(sessionId, args, tags) {
|
|
243
302
|
return __awaiter(this, void 0, void 0, function* () {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (!newTag) {
|
|
248
|
-
logger_1.default.warn('[DashboardCommands] addTag: No tag provided');
|
|
249
|
-
return this.sendSuccessResponse(response);
|
|
303
|
+
const newTag = stringArg(args[0], 'tag');
|
|
304
|
+
if (!newTag || !newTag.trim()) {
|
|
305
|
+
return notRecorded('addTag needs a tag, as a string or { tag }.');
|
|
250
306
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// Parse existing tags or initialize empty array
|
|
258
|
-
let existingTags = [];
|
|
259
|
-
if (session === null || session === void 0 ? void 0 : session.tags) {
|
|
260
|
-
try {
|
|
261
|
-
existingTags = JSON.parse(session.tags);
|
|
262
|
-
}
|
|
263
|
-
catch (_a) {
|
|
264
|
-
existingTags = [];
|
|
265
|
-
}
|
|
307
|
+
let existingTags = [];
|
|
308
|
+
if (tags) {
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(tags);
|
|
311
|
+
if (Array.isArray(parsed))
|
|
312
|
+
existingTags = parsed;
|
|
266
313
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
existingTags.push(newTag);
|
|
270
|
-
yield (0, session_service_1.updateSessionDetails)(sessionId, {
|
|
271
|
-
tags: JSON.stringify(existingTags),
|
|
272
|
-
});
|
|
273
|
-
logger_1.default.info(`[DashboardCommands] Tag added to ${sessionId}: ${newTag}`);
|
|
314
|
+
catch (_a) {
|
|
315
|
+
existingTags = [];
|
|
274
316
|
}
|
|
275
317
|
}
|
|
276
|
-
|
|
277
|
-
|
|
318
|
+
if (!existingTags.includes(newTag)) {
|
|
319
|
+
existingTags.push(newTag);
|
|
320
|
+
yield (0, session_service_1.updateSessionDetails)(sessionId, { tags: JSON.stringify(existingTags) });
|
|
321
|
+
logger_1.default.info(`[DashboardCommands] Tag added to ${sessionId}: ${newTag}`);
|
|
278
322
|
}
|
|
279
|
-
return
|
|
323
|
+
return RECORDED;
|
|
280
324
|
});
|
|
281
325
|
}
|
|
282
326
|
}
|
|
@@ -78,6 +78,7 @@ const SocketEvents_1 = require("../enums/SocketEvents");
|
|
|
78
78
|
const types_1 = require("../services/healing/types");
|
|
79
79
|
const SelectorStateService_1 = require("../services/SelectorStateService");
|
|
80
80
|
const recording_store_1 = require("../services/recording/recording-store");
|
|
81
|
+
const NotificationService_1 = require("../services/NotificationService");
|
|
81
82
|
const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
|
|
82
83
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
83
84
|
const typedi_2 = require("typedi");
|
|
@@ -190,8 +191,16 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
190
191
|
}
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Closes a session's record: its end time, its final status, the device's
|
|
196
|
+
* release. Every way a session ends comes here (the client's delete, an
|
|
197
|
+
* inactivity timeout, a driver crash, a heartbeat timeout, a shutdown), so
|
|
198
|
+
* this is where a failed one is announced to the `session_failed` webhooks.
|
|
199
|
+
* `notify: false` records the failure without announcing it: a server
|
|
200
|
+
* shutting down fails the sessions it drains, which no test caused.
|
|
201
|
+
*/
|
|
202
|
+
onSessionStopped(sessionId_1, status_1, failureReason_1) {
|
|
203
|
+
return __awaiter(this, arguments, void 0, function* (sessionId, status, failureReason, options = {}) {
|
|
195
204
|
var _a, _b, _c, _d;
|
|
196
205
|
// Idempotency Guard: If this session is already being stopped by another actor
|
|
197
206
|
// (heartbeat, stream watchdog, plugin.deleteSession), skip to avoid double-cleanup.
|
|
@@ -341,6 +350,12 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
341
350
|
else if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
|
|
342
351
|
typedi_1.Container.get(MetricsService_1.MetricsService).incrementSessionFailure();
|
|
343
352
|
}
|
|
353
|
+
// Webhooks: not awaited, since a slow or dead webhook must not hold up
|
|
354
|
+
// the end of the session. The service sends once per session even
|
|
355
|
+
// though a session can end twice (a crash, then the client's delete).
|
|
356
|
+
if (updateData.status === SessionStatus_1.SessionStatus.FAILED && options.notify !== false) {
|
|
357
|
+
this.announceFailure(Object.assign(Object.assign({}, sessionEntry), updateData));
|
|
358
|
+
}
|
|
344
359
|
// Principal Triage: If session failed, perform intelligent failure analysis
|
|
345
360
|
if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
|
|
346
361
|
try {
|
|
@@ -363,6 +378,18 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
363
378
|
}
|
|
364
379
|
});
|
|
365
380
|
}
|
|
381
|
+
/** Starts the `session_failed` webhooks; never throws and never waits for them. */
|
|
382
|
+
announceFailure(session) {
|
|
383
|
+
var _a;
|
|
384
|
+
try {
|
|
385
|
+
void typedi_1.Container.get(NotificationService_1.NotificationService)
|
|
386
|
+
.notifySessionFailed(session)
|
|
387
|
+
.catch((err) => { var _a; return logger_1.default.warn(`session_failed webhook for ${session.id} not sent: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
logger_1.default.warn(`session_failed webhook for ${session.id} not sent: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
366
393
|
beforeSessionCommand(sessionId, commandName, request, response) {
|
|
367
394
|
return __awaiter(this, void 0, void 0, function* () {
|
|
368
395
|
var _a;
|
|
@@ -370,16 +397,22 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
370
397
|
this.commandStartTime.set(`${sessionId}:${commandName}`, Date.now());
|
|
371
398
|
logger_1.default.debug(`[EventManager] beforeSessionCommand: sessionId=${sessionId}, commandName=${commandName}`);
|
|
372
399
|
}
|
|
373
|
-
//
|
|
400
|
+
// The session-details commands (`xenon: setSessionName`, ...) are answered
|
|
401
|
+
// here, from this server's record of the session, whether or not the
|
|
402
|
+
// session is in memory. Every other `xenon:` script goes on: to the
|
|
403
|
+
// plugin's CommandInterceptor on the server that drives the phone, which
|
|
404
|
+
// a hub reaches by forwarding it. Taking them all here answered a node
|
|
405
|
+
// phone's autowait, Omni-Vision and network-capture scripts with null on
|
|
406
|
+
// the hub, and they never reached the node.
|
|
374
407
|
if (commandName === 'execute') {
|
|
375
408
|
const script = ((_a = request.body) === null || _a === void 0 ? void 0 : _a.script) || (Array.isArray(request.body) ? request.body[0] : undefined);
|
|
376
|
-
if (
|
|
409
|
+
if ((0, commands_1.sessionDetailsCommandOf)(script)) {
|
|
377
410
|
logger_1.default.info(`[EventManager] Intercepting Xenon command: ${script} for session ${sessionId}`);
|
|
378
411
|
yield commands_1.dashboardCommands.process(sessionId, request, response);
|
|
379
412
|
return false;
|
|
380
413
|
}
|
|
381
|
-
else if (script && script.includes(':')) {
|
|
382
|
-
logger_1.default.debug(`[EventManager]
|
|
414
|
+
else if (typeof script === 'string' && script.includes(':')) {
|
|
415
|
+
logger_1.default.debug(`[EventManager] Script ${script} is not a session-details command; passing it on.`);
|
|
383
416
|
}
|
|
384
417
|
}
|
|
385
418
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|