@xenon-device-management/xenon 2.9.2 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-B87n4pUu.js → AnnotationOverlay-defSfi9B.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-DS7dJ2bU.js} +1 -1
- package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-CeD8SUnQ.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-Dgue7t7s.js} +2 -2
- package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-Bo-vvxiy.js} +1 -1
- package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-DdhTNAXZ.js} +1 -1
- package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-J3F_3fDw.js} +1 -1
- package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Bxluaw2m.js} +1 -1
- package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-qgMVFrqV.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-BUT7WgJB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CY0OIgCo.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-Cq-fybZ2.js} +1 -1
- package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-CASWJr7h.js} +1 -1
- package/lib/public/assets/{Table-D_2lhvRM.js → Table-BSO_dZzu.js} +1 -1
- package/lib/public/assets/{activity-cNHHOKx5.js → activity-B_-MNwwR.js} +1 -1
- package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DFhemyo8.js} +1 -1
- package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-DV1oB8Tf.js} +1 -1
- package/lib/public/assets/{apps-DCiI3UOb.js → apps-B5f-VuLN.js} +1 -1
- package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-D_xIcVqg.js} +1 -1
- package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-CLGlL_Un.js} +1 -1
- package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-CuIgWFj2.js} +1 -1
- package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-SgAkrFQg.js} +2 -2
- package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-h9ZJ8n74.js} +4 -4
- package/lib/public/assets/{button-C0caCjJB.js → button-DKl94fzV.js} +1 -1
- package/lib/public/assets/{calendar-BqOOkln_.js → calendar-BNLTjBsT.js} +1 -1
- package/lib/public/assets/{check-DYSXRG79.js → check-1u_3Zsx5.js} +1 -1
- package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-Cn2BixQX.js} +1 -1
- package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-OymVCeGU.js} +1 -1
- package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-D3ZyFWOd.js} +1 -1
- package/lib/public/assets/{clock-XP3iBAhg.js → clock-DwzGN-aa.js} +1 -1
- package/lib/public/assets/{copy-D8d9UzWE.js → copy-De43WY6T.js} +1 -1
- package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-CtvEmCC5.js} +5 -5
- package/lib/public/assets/{download-SaN9-umX.js → download-DcSdKSvR.js} +1 -1
- package/lib/public/assets/{forgot-password-DfUhEZW5.js → forgot-password-DDjM8J7Z.js} +1 -1
- package/lib/public/assets/{index-CzaIyL9t.js → index-Bqt1dmMA.js} +2 -2
- package/lib/public/assets/{index-IefGCFQQ.js → index-CSNs0EE4.js} +1 -1
- package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
- package/lib/public/assets/{input-BqFzAQT3.js → input-fRGFwm1K.js} +1 -1
- package/lib/public/assets/{keycodes-DDV2OlpQ.js → keycodes-Dj4m3JV7.js} +10 -5
- package/lib/public/assets/line-chart-DHSZ8Sm4.js +1 -0
- package/lib/public/assets/list-checks-Co1gqqDn.js +11 -0
- package/lib/public/assets/{lock-Eye1CG8p.js → lock-BzutP2Yf.js} +1 -1
- package/lib/public/assets/{login-Co1IXuen.js → login-DSVZLgW2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-PYFNp_tc.js} +1 -1
- package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-DdKVA3ze.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-C8UpXByt.js} +1 -1
- package/lib/public/assets/{network-BP_U4iDZ.js → network-BkzHrr2L.js} +1 -1
- package/lib/public/assets/overview-0YFo6f97.js +6 -0
- package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DkmHEtLJ.js} +1 -1
- package/lib/public/assets/{plus-1k0sxXyU.js → plus-BH6Y7G7y.js} +1 -1
- package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-C1IDBP4l.js} +1 -1
- package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-yNMsl5fZ.js} +1 -1
- package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page--fH5nToN.js} +1 -1
- package/lib/public/assets/{select-CiRW6v17.js → select-DqjhZ6h2.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-CrYIFrUj.js +1 -0
- package/lib/public/assets/selector-health-page-BnRUmiS2.js +11 -0
- package/lib/public/assets/session-detail-page-BNh_QfHv.js +4 -0
- package/lib/public/assets/{settings-DhwZoWv7.js → settings-BcxYFxnE.js} +1 -1
- package/lib/public/assets/stat-tile-DxVLBgA7.js +1 -0
- package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-C_uKWfUK.js} +1 -1
- package/lib/public/assets/{teams-D0IBa2Ov.js → teams-C9G2WisD.js} +1 -1
- package/lib/public/assets/time-D-gjmE1o.js +1 -0
- package/lib/public/assets/{trash-2-u8MlXUBD.js → trash-2-C4cJO0CG.js} +1 -1
- package/lib/public/assets/{upload-Cq8L3kpw.js → upload-BN3PMMvP.js} +1 -1
- package/lib/public/assets/use-builds-data-CtelVuo_.js +8 -0
- package/lib/public/assets/{use-password-reset-mode-Dbr2HEfw.js → use-password-reset-mode-CClcmiRh.js} +1 -1
- package/lib/public/assets/{users-DxLHYmFm.js → users-BK7fwGGA.js} +1 -1
- package/lib/public/assets/{users-CZqCE511.js → users-OdnOb63Y.js} +1 -1
- package/lib/public/assets/{video-off-mh1EEzP9.js → video-off-DUAYoXTQ.js} +1 -1
- package/lib/public/assets/view-state-83Z1gFME.js +1 -0
- package/lib/public/assets/{webhook-settings-BYuOib-Q.js → webhook-settings-BRyvDWHI.js} +1 -1
- package/lib/public/assets/{zap-BOpUur0M.js → zap-BiceX7DC.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +5 -0
- package/lib/src/app/routers/dashboard.js +86 -40
- package/lib/src/app/routers/selector-health.js +75 -0
- package/lib/src/app/swagger-docs.js +77 -8
- package/lib/src/dashboard/event-manager.js +15 -99
- package/lib/src/generated/client/edge.js +27 -3
- package/lib/src/generated/client/index-browser.js +24 -0
- package/lib/src/generated/client/index.d.ts +8207 -5283
- package/lib/src/generated/client/index.js +27 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +49 -9
- package/lib/src/generated/client/wasm.js +24 -0
- package/lib/src/prisma.js +2 -0
- package/lib/src/services/CleanupService.js +1 -0
- package/lib/src/services/NotificationService.js +3 -4
- package/lib/src/services/SelectorStateService.js +151 -113
- package/lib/src/services/SelectorVerificationJob.js +64 -14
- package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
- package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
- package/lib/src/services/metrics/SessionMetricsService.js +238 -0
- package/lib/src/services/metrics/androidMetrics.js +94 -0
- package/lib/src/services/metrics/iosMetrics.js +26 -0
- package/lib/src/services/metrics/metricsBody.js +37 -0
- package/lib/src/services/metrics/types.js +19 -0
- package/lib/src/services/selector-health/access.js +66 -0
- package/lib/src/services/selector-health/healingTrend.js +49 -0
- package/lib/src/services/selector-health/people.js +27 -0
- package/lib/src/services/selector-health/selectorDetail.js +214 -0
- package/lib/src/services/selector-health/selectorKeys.js +36 -0
- package/lib/src/services/selector-health/selectorList.js +333 -0
- package/lib/src/services/selector-health/stateView.js +67 -0
- package/lib/test/helpers/scratch-database.js +19 -3
- package/lib/test/helpers/selector-health-fixture.js +216 -0
- package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
- package/lib/test/unit/CleanupService.spec.js +28 -2
- package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
- package/lib/test/unit/healing-no-cost.spec.js +134 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
- package/lib/test/unit/healing-trend.spec.js +118 -0
- package/lib/test/unit/selector-event-model.spec.js +47 -0
- package/lib/test/unit/selector-health-actions.spec.js +106 -0
- package/lib/test/unit/selector-health-detail.spec.js +188 -0
- package/lib/test/unit/selector-health-list.spec.js +185 -0
- package/lib/test/unit/selector-state-service.spec.js +74 -1
- package/lib/test/unit/selector-verification-job.spec.js +41 -0
- package/lib/test/unit/selector-verification-missed-regression.spec.js +132 -0
- package/lib/test/unit/session-log-indexes.spec.js +147 -0
- package/lib/test/unit/session-metrics-parse.spec.js +138 -0
- package/lib/test/unit/session-metrics-route.spec.js +140 -0
- package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
- package/lib/test/unit/session-metrics-service.spec.js +215 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
- package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
- package/prisma/migrations/20261002180000_session_log_indexes/migration.sql +5 -0
- package/prisma/migrations/20261002190000_log_profiling_session_indexes/migration.sql +5 -0
- package/prisma/schema.prisma +49 -9
- package/schema.json +5 -0
- package/lib/public/assets/chevron-left-BSG6mcpo.js +0 -6
- package/lib/public/assets/funnel-OefHE-54.js +0 -6
- package/lib/public/assets/layers-a3GNz2DA.js +0 -6
- package/lib/public/assets/list-checks-DMrMAFDt.js +0 -6
- package/lib/public/assets/overview-CsPyPv10.js +0 -1
- package/lib/public/assets/selector-detail-page-pA913STP.js +0 -1
- package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
- package/lib/public/assets/selector-health-CHLX8KqY.js +0 -16
- package/lib/public/assets/selector-health-page-vVkg8Mii.js +0 -16
- package/lib/public/assets/session-detail-page-BCD49zDi.js +0 -4
- package/lib/public/assets/stat-tile-BaE76cnK.js +0 -8
- package/lib/public/assets/time-B8FIgwPE.js +0 -1
- package/lib/src/profiling/AndroidAppProfiler.js +0 -213
- /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSysmontapCpu = parseSysmontapCpu;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
/**
|
|
6
|
+
* Device CPU % from one line of `ios sysmontap`, or null for any other line.
|
|
7
|
+
* go-ios logs JSON lines; a reading is {"msg":"received CPU usage data",
|
|
8
|
+
* "enabled_cpus":6,"cpu_total_load":77.5,...}. The load is summed over the
|
|
9
|
+
* cores (an idle 6-core iPhone 14 Plus read 77 to 125), so it is divided by
|
|
10
|
+
* them.
|
|
11
|
+
*/
|
|
12
|
+
function parseSysmontapCpu(line) {
|
|
13
|
+
var _a;
|
|
14
|
+
let o;
|
|
15
|
+
try {
|
|
16
|
+
o = JSON.parse(line);
|
|
17
|
+
}
|
|
18
|
+
catch (_b) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (!o || typeof o.cpu_total_load !== 'number' || !Number.isFinite(o.cpu_total_load)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const cores = (_a = [o.enabled_cpus, o.cpu_count].find((n) => typeof n === 'number' && n > 0)) !== null && _a !== void 0 ? _a : 1;
|
|
25
|
+
return (0, types_1.round1)((0, types_1.clamp)(o.cpu_total_load / cores, 0, 100));
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.seriesFor = seriesFor;
|
|
4
|
+
exports.sessionMetricsBody = sessionMetricsBody;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
/** What a session on this platform can have: Android everything, an iPhone device CPU. */
|
|
7
|
+
function seriesFor(platform) {
|
|
8
|
+
const p = platform.toLowerCase();
|
|
9
|
+
const android = p === 'android';
|
|
10
|
+
return {
|
|
11
|
+
deviceCpu: android || p === 'ios',
|
|
12
|
+
deviceMem: android,
|
|
13
|
+
appCpu: android,
|
|
14
|
+
appMem: android,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function sessionMetricsBody(platform, rows, recording = null) {
|
|
18
|
+
var _a;
|
|
19
|
+
const sorted = [...rows].sort((a, b) => a.at - b.at);
|
|
20
|
+
const lastApp = [...sorted].reverse().find((r) => r.app_id);
|
|
21
|
+
return {
|
|
22
|
+
platform: platform.toLowerCase(),
|
|
23
|
+
intervalMs: types_1.SAMPLE_INTERVAL_MS,
|
|
24
|
+
appId: (_a = lastApp === null || lastApp === void 0 ? void 0 : lastApp.app_id) !== null && _a !== void 0 ? _a : null,
|
|
25
|
+
series: seriesFor(platform),
|
|
26
|
+
recording,
|
|
27
|
+
samples: sorted.map((r) => ({
|
|
28
|
+
t: r.at,
|
|
29
|
+
deviceCpu: r.device_cpu_pct,
|
|
30
|
+
deviceMemMb: r.device_mem_mb,
|
|
31
|
+
deviceMemTotalMb: r.device_mem_total,
|
|
32
|
+
appCpu: r.app_cpu_pct,
|
|
33
|
+
appMemMb: r.app_mem_mb,
|
|
34
|
+
app: r.app_id,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clamp = exports.round1 = exports.MAX_BUFFERED_SAMPLES = exports.TUNNEL_RENEW_MS = exports.FOREGROUND_REFRESH_MS = exports.MAX_CONSECUTIVE_FAILURES = exports.FLUSH_INTERVAL_MS = exports.SAMPLE_INTERVAL_MS = void 0;
|
|
4
|
+
/** How often a session's phone is sampled. */
|
|
5
|
+
exports.SAMPLE_INTERVAL_MS = 2000;
|
|
6
|
+
/** How often a session's samples are written; a crash loses at most this much. */
|
|
7
|
+
exports.FLUSH_INTERVAL_MS = 10000;
|
|
8
|
+
/** A sampler that fails this many times in a row stops for its session. */
|
|
9
|
+
exports.MAX_CONSECUTIVE_FAILURES = 5;
|
|
10
|
+
/** How long a looked-up foreground app is trusted (Android, no appPackage). */
|
|
11
|
+
exports.FOREGROUND_REFRESH_MS = 10000;
|
|
12
|
+
/** How often an iPhone sampler asks for its phone's tunnel again. */
|
|
13
|
+
exports.TUNNEL_RENEW_MS = 30000;
|
|
14
|
+
/** Samples waiting for a write that keeps failing: the newest 30 minutes. */
|
|
15
|
+
exports.MAX_BUFFERED_SAMPLES = 900;
|
|
16
|
+
const round1 = (n) => Math.round(n * 10) / 10;
|
|
17
|
+
exports.round1 = round1;
|
|
18
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
19
|
+
exports.clamp = clamp;
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SELECTOR_NOT_FOUND = void 0;
|
|
13
|
+
exports.visibleKeys = visibleKeys;
|
|
14
|
+
exports.canSeeSelector = canSeeSelector;
|
|
15
|
+
exports.canActOnSelectors = canActOnSelectors;
|
|
16
|
+
const prisma_1 = require("../../prisma");
|
|
17
|
+
const selectorKeys_1 = require("./selectorKeys");
|
|
18
|
+
/** The answer for a selector the caller may not see, the same as for one that doesn't exist. */
|
|
19
|
+
exports.SELECTOR_NOT_FOUND = { error: 'not_found', message: 'Selector not found' };
|
|
20
|
+
/**
|
|
21
|
+
* The keys among `keys` the caller may see: those healed, at any time, in a
|
|
22
|
+
* session the caller may see. All of them for an admin or with auth disabled.
|
|
23
|
+
*/
|
|
24
|
+
function visibleKeys(keys, scope) {
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
if (!scope)
|
|
27
|
+
return new Set(keys.map((k) => (0, selectorKeys_1.keyOf)(k.strategy, k.selector)));
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
for (const part of (0, selectorKeys_1.chunk)(keys)) {
|
|
30
|
+
const groups = yield prisma_1.prisma.sessionLog.groupBy({
|
|
31
|
+
by: ['original_strategy', 'original_selector'],
|
|
32
|
+
where: {
|
|
33
|
+
AND: [
|
|
34
|
+
{ is_healed: true },
|
|
35
|
+
(0, selectorKeys_1.sessionScope)(scope),
|
|
36
|
+
{ OR: part.map((k) => (0, selectorKeys_1.tupleWhere)(k.strategy, k.selector)) },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
for (const g of groups) {
|
|
41
|
+
if (g.original_selector)
|
|
42
|
+
seen.add((0, selectorKeys_1.keyOf)(g.original_strategy, g.original_selector));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return seen;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Whether the caller may see one selector, by the same rule. */
|
|
49
|
+
function canSeeSelector(key, scope) {
|
|
50
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
if (!scope)
|
|
52
|
+
return true;
|
|
53
|
+
const row = yield prisma_1.prisma.sessionLog.findFirst({
|
|
54
|
+
where: {
|
|
55
|
+
AND: [{ is_healed: true }, (0, selectorKeys_1.tupleWhere)(key.strategy, key.selector), (0, selectorKeys_1.sessionScope)(scope)],
|
|
56
|
+
},
|
|
57
|
+
select: { id: true },
|
|
58
|
+
});
|
|
59
|
+
return row !== null;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Mark fixed, mute, unmute and cancel need the `sessions` scope; `admin` has every scope. */
|
|
63
|
+
function canActOnSelectors(scopes) {
|
|
64
|
+
const owned = new Set((Array.isArray(scopes) ? scopes : (scopes !== null && scopes !== void 0 ? scopes : '').split(',')).map((s) => s.trim()));
|
|
65
|
+
return owned.has('admin') || owned.has('sessions');
|
|
66
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AI_METHODS = exports.DAY_MS = void 0;
|
|
4
|
+
exports.parseTzOffset = parseTzOffset;
|
|
5
|
+
exports.localDayStart = localDayStart;
|
|
6
|
+
exports.dailyHeals = dailyHeals;
|
|
7
|
+
/** One day, in milliseconds. */
|
|
8
|
+
exports.DAY_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
/** Heals found by a screenshot model or a language model: the slowest and least certain. */
|
|
10
|
+
exports.AI_METHODS = new Set(['Visual AI', 'LLM']);
|
|
11
|
+
/** The furthest any time zone is from UTC, in minutes. */
|
|
12
|
+
const MAX_OFFSET_MIN = 14 * 60;
|
|
13
|
+
/**
|
|
14
|
+
* The caller's offset from UTC in minutes, east positive, from `?tz=` (the
|
|
15
|
+
* browser's `-getTimezoneOffset()`). 0 when absent or not a whole number of
|
|
16
|
+
* minutes within ±14 hours.
|
|
17
|
+
*/
|
|
18
|
+
function parseTzOffset(raw) {
|
|
19
|
+
if (typeof raw !== 'string' || !/^-?\d+$/.test(raw))
|
|
20
|
+
return 0;
|
|
21
|
+
const n = Number(raw);
|
|
22
|
+
return Math.abs(n) <= MAX_OFFSET_MIN ? n : 0;
|
|
23
|
+
}
|
|
24
|
+
/** The instant the caller's local day holding `ms` began. */
|
|
25
|
+
function localDayStart(ms, tzOffsetMin) {
|
|
26
|
+
const shift = tzOffsetMin * 60000;
|
|
27
|
+
return Math.floor((ms + shift) / exports.DAY_MS) * exports.DAY_MS - shift;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Heals per local day, from the day holding `since` to the day holding `now`,
|
|
31
|
+
* days with none included. One fixed offset for the whole period: across a
|
|
32
|
+
* daylight-saving change a heal near midnight can land on the next day.
|
|
33
|
+
*/
|
|
34
|
+
function dailyHeals(heals, since, now, tzOffsetMin) {
|
|
35
|
+
const first = localDayStart(since.getTime(), tzOffsetMin);
|
|
36
|
+
const last = localDayStart(now.getTime(), tzOffsetMin);
|
|
37
|
+
const days = [];
|
|
38
|
+
for (let t = first; t <= last; t += exports.DAY_MS)
|
|
39
|
+
days.push({ t, heals: 0, aiHeals: 0 });
|
|
40
|
+
for (const h of heals) {
|
|
41
|
+
const i = Math.round((localDayStart(h.at.getTime(), tzOffsetMin) - first) / exports.DAY_MS);
|
|
42
|
+
if (i < 0 || i >= days.length)
|
|
43
|
+
continue;
|
|
44
|
+
days[i].heals += 1;
|
|
45
|
+
if (h.method && exports.AI_METHODS.has(h.method))
|
|
46
|
+
days[i].aiHeals += 1;
|
|
47
|
+
}
|
|
48
|
+
return days;
|
|
49
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.peopleById = peopleById;
|
|
13
|
+
const prisma_1 = require("../../prisma");
|
|
14
|
+
/** The people behind these user ids, by id. */
|
|
15
|
+
function peopleById(ids) {
|
|
16
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
const wanted = Array.from(new Set(ids.filter((id) => !!id)));
|
|
18
|
+
if (wanted.length === 0)
|
|
19
|
+
return new Map();
|
|
20
|
+
const users = yield prisma_1.prisma.user.findMany({
|
|
21
|
+
where: { id: { in: wanted } },
|
|
22
|
+
select: { id: true, name: true },
|
|
23
|
+
});
|
|
24
|
+
const names = new Map(users.map((u) => [u.id, u.name]));
|
|
25
|
+
return new Map(wanted.map((id) => { var _a; return [id, { id, name: (_a = names.get(id)) !== null && _a !== void 0 ? _a : null }]; }));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.parseSelectorDetailQuery = parseSelectorDetailQuery;
|
|
13
|
+
exports.selectorDetail = selectorDetail;
|
|
14
|
+
const prisma_1 = require("../../prisma");
|
|
15
|
+
const access_1 = require("./access");
|
|
16
|
+
const healingTrend_1 = require("./healingTrend");
|
|
17
|
+
const people_1 = require("./people");
|
|
18
|
+
const selectorKeys_1 = require("./selectorKeys");
|
|
19
|
+
const stateView_1 = require("./stateView");
|
|
20
|
+
/** A selector longer than this is no selector anyone wrote. */
|
|
21
|
+
const MAX_SELECTOR = 4000;
|
|
22
|
+
function parseSelectorDetailQuery(q) {
|
|
23
|
+
const selector = typeof q.selector === 'string' ? q.selector : '';
|
|
24
|
+
if (!selector || selector.length > MAX_SELECTOR)
|
|
25
|
+
return null;
|
|
26
|
+
const strategy = typeof q.strategy === 'string' ? q.strategy.slice(0, 200) : '';
|
|
27
|
+
const days = typeof q.days === 'string' && /^\d+$/.test(q.days)
|
|
28
|
+
? Math.min(Math.max(Number(q.days), 1), 365)
|
|
29
|
+
: 30;
|
|
30
|
+
return { strategy, selector, days, tz: (0, healingTrend_1.parseTzOffset)(q.tz) };
|
|
31
|
+
}
|
|
32
|
+
const TOP = 5;
|
|
33
|
+
const RECENT = 20;
|
|
34
|
+
const ACTIVITY = 50;
|
|
35
|
+
function bump(m, key, make) {
|
|
36
|
+
var _a;
|
|
37
|
+
const v = (_a = m.get(key)) !== null && _a !== void 0 ? _a : make();
|
|
38
|
+
v.count += 1;
|
|
39
|
+
m.set(key, v);
|
|
40
|
+
}
|
|
41
|
+
const topCounts = (m) => Array.from(m.values())
|
|
42
|
+
.sort((a, b) => b.count - a.count)
|
|
43
|
+
.slice(0, TOP);
|
|
44
|
+
/** The sessions behind these ids, with their phone and build, by id. */
|
|
45
|
+
function sessionsById(ids) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
const parts = yield Promise.all((0, selectorKeys_1.chunk)(ids).map((part) => prisma_1.prisma.session.findMany({
|
|
48
|
+
where: { id: { in: part } },
|
|
49
|
+
select: {
|
|
50
|
+
id: true,
|
|
51
|
+
build_id: true,
|
|
52
|
+
device_udid: true,
|
|
53
|
+
device_name: true,
|
|
54
|
+
device_platform: true,
|
|
55
|
+
build: { select: { name: true } },
|
|
56
|
+
},
|
|
57
|
+
})));
|
|
58
|
+
return new Map(parts.flat().map((s) => [s.id, s]));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Everything the panel shows for one selector over the period, or null when
|
|
63
|
+
* the caller may not see it (a member, and no session they can see healed
|
|
64
|
+
* it). Status and activity are lab-wide: one row per selector.
|
|
65
|
+
*/
|
|
66
|
+
function selectorDetail(query_1, scope_1) {
|
|
67
|
+
return __awaiter(this, arguments, void 0, function* (query, scope, now = new Date()) {
|
|
68
|
+
var _a, _b, _c, _d, _e;
|
|
69
|
+
if (!(yield (0, access_1.canSeeSelector)({ strategy: query.strategy, selector: query.selector }, scope))) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const since = new Date(now.getTime() - query.days * healingTrend_1.DAY_MS);
|
|
73
|
+
const rows = yield prisma_1.prisma.sessionLog.findMany({
|
|
74
|
+
where: {
|
|
75
|
+
AND: [
|
|
76
|
+
{ is_healed: true, createdAt: { gte: since } },
|
|
77
|
+
(0, selectorKeys_1.tupleWhere)(query.strategy, query.selector),
|
|
78
|
+
(0, selectorKeys_1.sessionScope)(scope),
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
|
|
82
|
+
// Each heal's own fields only. Attaching its session to every heal took
|
|
83
|
+
// most of a second for a selector healed 50,000 times in a year; each
|
|
84
|
+
// session is read once instead.
|
|
85
|
+
select: {
|
|
86
|
+
id: true,
|
|
87
|
+
session_id: true,
|
|
88
|
+
createdAt: true,
|
|
89
|
+
healed_selector: true,
|
|
90
|
+
healed_strategy: true,
|
|
91
|
+
healing_tier: true,
|
|
92
|
+
healing_confidence: true,
|
|
93
|
+
duration: true,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const sessionOf = yield sessionsById(Array.from(new Set(rows.map((r) => r.session_id))));
|
|
97
|
+
const sessions = new Set();
|
|
98
|
+
let timeSpentMs = 0;
|
|
99
|
+
const fixes = new Map();
|
|
100
|
+
const platforms = new Map();
|
|
101
|
+
const builds = new Map();
|
|
102
|
+
const devices = new Map();
|
|
103
|
+
for (const r of rows) {
|
|
104
|
+
sessions.add(r.session_id);
|
|
105
|
+
timeSpentMs += (_a = r.duration) !== null && _a !== void 0 ? _a : 0;
|
|
106
|
+
if (r.healed_selector) {
|
|
107
|
+
const fixKey = `${(_b = r.healed_strategy) !== null && _b !== void 0 ? _b : ''}\u0000${r.healed_selector}`;
|
|
108
|
+
const f = (_c = fixes.get(fixKey)) !== null && _c !== void 0 ? _c : {
|
|
109
|
+
selector: r.healed_selector,
|
|
110
|
+
strategy: (_d = r.healed_strategy) !== null && _d !== void 0 ? _d : null,
|
|
111
|
+
count: 0,
|
|
112
|
+
methods: new Set(),
|
|
113
|
+
confidenceSum: 0,
|
|
114
|
+
confidenceCount: 0,
|
|
115
|
+
};
|
|
116
|
+
f.count += 1;
|
|
117
|
+
if (r.healing_tier)
|
|
118
|
+
f.methods.add(r.healing_tier);
|
|
119
|
+
if (typeof r.healing_confidence === 'number') {
|
|
120
|
+
f.confidenceSum += r.healing_confidence;
|
|
121
|
+
f.confidenceCount += 1;
|
|
122
|
+
}
|
|
123
|
+
fixes.set(fixKey, f);
|
|
124
|
+
}
|
|
125
|
+
const session = sessionOf.get(r.session_id);
|
|
126
|
+
if (!session)
|
|
127
|
+
continue;
|
|
128
|
+
const platform = session.device_platform || 'unknown';
|
|
129
|
+
bump(platforms, platform, () => ({ name: platform, count: 0 }));
|
|
130
|
+
const buildId = (_e = session.build_id) !== null && _e !== void 0 ? _e : null;
|
|
131
|
+
bump(builds, buildId !== null && buildId !== void 0 ? buildId : '', () => {
|
|
132
|
+
var _a;
|
|
133
|
+
return ({
|
|
134
|
+
id: buildId,
|
|
135
|
+
name: ((_a = session.build) === null || _a === void 0 ? void 0 : _a.name) || buildId || 'No build',
|
|
136
|
+
count: 0,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
const udid = session.device_udid;
|
|
140
|
+
if (udid)
|
|
141
|
+
bump(devices, udid, () => ({ udid, name: session.device_name || udid, count: 0 }));
|
|
142
|
+
}
|
|
143
|
+
const [state, events] = yield Promise.all([
|
|
144
|
+
prisma_1.prisma.selectorState.findUnique({
|
|
145
|
+
where: {
|
|
146
|
+
original_strategy_original_selector: {
|
|
147
|
+
original_strategy: query.strategy,
|
|
148
|
+
original_selector: query.selector,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
prisma_1.prisma.selectorEvent.findMany({
|
|
153
|
+
where: { original_strategy: query.strategy, original_selector: query.selector },
|
|
154
|
+
orderBy: { createdAt: 'desc' },
|
|
155
|
+
take: ACTIVITY,
|
|
156
|
+
}),
|
|
157
|
+
]);
|
|
158
|
+
const people = yield (0, people_1.peopleById)(events.map((e) => e.user_id));
|
|
159
|
+
const fixedEvent = events.find((e) => e.action === 'marked_fixed');
|
|
160
|
+
const mutedEvent = events.find((e) => e.action === 'muted');
|
|
161
|
+
return {
|
|
162
|
+
strategy: query.strategy,
|
|
163
|
+
selector: query.selector,
|
|
164
|
+
days: query.days,
|
|
165
|
+
heals: rows.length,
|
|
166
|
+
sessions: sessions.size,
|
|
167
|
+
timeSpentMs,
|
|
168
|
+
firstHealedAt: rows.length > 0 ? rows[rows.length - 1].createdAt.toISOString() : null,
|
|
169
|
+
lastHealedAt: rows.length > 0 ? rows[0].createdAt.toISOString() : null,
|
|
170
|
+
daily: (0, healingTrend_1.dailyHeals)(rows.map((r) => ({ at: r.createdAt, method: r.healing_tier })), since, now, query.tz).map((d) => ({ t: d.t, heals: d.heals })),
|
|
171
|
+
suggestions: Array.from(fixes.values())
|
|
172
|
+
.sort((a, b) => b.count - a.count)
|
|
173
|
+
.map((f) => ({
|
|
174
|
+
selector: f.selector,
|
|
175
|
+
strategy: f.strategy,
|
|
176
|
+
count: f.count,
|
|
177
|
+
share: rows.length > 0 ? f.count / rows.length : 0,
|
|
178
|
+
methods: Array.from(f.methods),
|
|
179
|
+
averageConfidence: f.confidenceCount > 0 ? f.confidenceSum / f.confidenceCount : null,
|
|
180
|
+
})),
|
|
181
|
+
platforms: topCounts(platforms),
|
|
182
|
+
builds: topCounts(builds),
|
|
183
|
+
devices: topCounts(devices),
|
|
184
|
+
recent: rows.slice(0, RECENT).map((r) => {
|
|
185
|
+
var _a, _b, _c, _d;
|
|
186
|
+
const session = sessionOf.get(r.session_id);
|
|
187
|
+
return {
|
|
188
|
+
id: r.id,
|
|
189
|
+
sessionId: r.session_id,
|
|
190
|
+
buildId: (_a = session === null || session === void 0 ? void 0 : session.build_id) !== null && _a !== void 0 ? _a : null,
|
|
191
|
+
at: r.createdAt.toISOString(),
|
|
192
|
+
device: (session === null || session === void 0 ? void 0 : session.device_name) || (session === null || session === void 0 ? void 0 : session.device_udid) || null,
|
|
193
|
+
platform: (session === null || session === void 0 ? void 0 : session.device_platform) || null,
|
|
194
|
+
method: (_b = r.healing_tier) !== null && _b !== void 0 ? _b : null,
|
|
195
|
+
confidence: (_c = r.healing_confidence) !== null && _c !== void 0 ? _c : null,
|
|
196
|
+
healedSelector: (_d = r.healed_selector) !== null && _d !== void 0 ? _d : null,
|
|
197
|
+
};
|
|
198
|
+
}),
|
|
199
|
+
state: (0, stateView_1.stateView)(state, {
|
|
200
|
+
fixed: fixedEvent ? { user_id: fixedEvent.user_id } : undefined,
|
|
201
|
+
muted: mutedEvent ? { user_id: mutedEvent.user_id, reason: mutedEvent.reason } : undefined,
|
|
202
|
+
}, people),
|
|
203
|
+
activity: events.map((e) => {
|
|
204
|
+
var _a, _b;
|
|
205
|
+
return ({
|
|
206
|
+
action: e.action,
|
|
207
|
+
at: e.createdAt.toISOString(),
|
|
208
|
+
by: e.user_id ? ((_a = people.get(e.user_id)) !== null && _a !== void 0 ? _a : { id: e.user_id, name: null }) : null,
|
|
209
|
+
reason: (_b = e.reason) !== null && _b !== void 0 ? _b : null,
|
|
210
|
+
});
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.keyOf = void 0;
|
|
4
|
+
exports.tupleWhere = tupleWhere;
|
|
5
|
+
exports.sessionScope = sessionScope;
|
|
6
|
+
exports.chunk = chunk;
|
|
7
|
+
/** One map key per selector; a heal with no strategy files under ''. */
|
|
8
|
+
const keyOf = (strategy, selector) => `${strategy !== null && strategy !== void 0 ? strategy : ''}\u0000${selector}`;
|
|
9
|
+
exports.keyOf = keyOf;
|
|
10
|
+
/** Heal rows of one selector. Heals with no strategy match the empty one. */
|
|
11
|
+
function tupleWhere(strategy, selector) {
|
|
12
|
+
return strategy
|
|
13
|
+
? { original_strategy: strategy, original_selector: selector }
|
|
14
|
+
: {
|
|
15
|
+
original_selector: selector,
|
|
16
|
+
OR: [{ original_strategy: null }, { original_strategy: '' }],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Heal rows in the caller's sessions, on one platform if given. */
|
|
20
|
+
function sessionScope(scope, platform) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (platform)
|
|
23
|
+
parts.push({ device_platform: platform });
|
|
24
|
+
if (scope)
|
|
25
|
+
parts.push(scope);
|
|
26
|
+
if (parts.length === 0)
|
|
27
|
+
return {};
|
|
28
|
+
return { session: { is: parts.length === 1 ? parts[0] : { AND: parts } } };
|
|
29
|
+
}
|
|
30
|
+
/** `items` in runs of at most `size`, so no `OR` list grows without bound. */
|
|
31
|
+
function chunk(items, size = 200) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = 0; i < items.length; i += size)
|
|
34
|
+
out.push(items.slice(i, i + size));
|
|
35
|
+
return out;
|
|
36
|
+
}
|