@xenon-device-management/xenon 2.9.2 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-B87n4pUu.js → AnnotationOverlay-LzfXiiZJ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-BzFDp4mK.js} +1 -1
- package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-BpgWvmZo.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-CCB-egSH.js} +2 -2
- package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-DkUVmANo.js} +1 -1
- package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-mbfnfTql.js} +1 -1
- package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-BH5usz4Y.js} +1 -1
- package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Cbo9mdTk.js} +1 -1
- package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-D6XfQHPr.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-CmFPkWqH.js} +1 -1
- package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CSlKk2qC.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-BFW9Dlqd.js} +1 -1
- package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-C4DioTTi.js} +1 -1
- package/lib/public/assets/{Table-D_2lhvRM.js → Table-DrKjlL00.js} +1 -1
- package/lib/public/assets/{activity-cNHHOKx5.js → activity-C9bE7Jm8.js} +1 -1
- package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DRYic4oO.js} +1 -1
- package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-CaQU0g7c.js} +1 -1
- package/lib/public/assets/{apps-DCiI3UOb.js → apps-c1gUFmmK.js} +1 -1
- package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-DMvAWA8r.js} +1 -1
- package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-BEQgxmKT.js} +1 -1
- package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-Ba0e2GeU.js} +1 -1
- package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-COODQnc7.js} +2 -2
- package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-DZQgWBUs.js} +4 -4
- package/lib/public/assets/{button-C0caCjJB.js → button-WW9j43wR.js} +1 -1
- package/lib/public/assets/{calendar-BqOOkln_.js → calendar-COUs9VxF.js} +1 -1
- package/lib/public/assets/{check-DYSXRG79.js → check-D8ifHIJR.js} +1 -1
- package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-BjKVUlB_.js} +1 -1
- package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-CfP79QE2.js} +1 -1
- package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-BZbFCixZ.js} +1 -1
- package/lib/public/assets/{clock-XP3iBAhg.js → clock-BuTm35Qg.js} +1 -1
- package/lib/public/assets/{copy-D8d9UzWE.js → copy-W00zUBwN.js} +1 -1
- package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-BEpS-Y-_.js} +5 -5
- package/lib/public/assets/{download-SaN9-umX.js → download-CGz-MS3y.js} +1 -1
- package/lib/public/assets/{forgot-password-DfUhEZW5.js → forgot-password-DG7SqVgU.js} +1 -1
- package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
- package/lib/public/assets/{index-IefGCFQQ.js → index-DWHTp1YP.js} +1 -1
- package/lib/public/assets/{index-CzaIyL9t.js → index-EkaQM1DK.js} +2 -2
- package/lib/public/assets/{input-BqFzAQT3.js → input-ChaTXeCq.js} +1 -1
- package/lib/public/assets/{keycodes-DDV2OlpQ.js → keycodes-BhbgeW_r.js} +10 -5
- package/lib/public/assets/line-chart-9P3RxScy.js +1 -0
- package/lib/public/assets/list-checks-_23IIEEL.js +11 -0
- package/lib/public/assets/{lock-Eye1CG8p.js → lock-B2JaqqRI.js} +1 -1
- package/lib/public/assets/{login-Co1IXuen.js → login-BEWIfLfC.js} +1 -1
- package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-BGJIJQ-k.js} +1 -1
- package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-BrwMY4G0.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-DsWDMyrY.js} +1 -1
- package/lib/public/assets/{network-BP_U4iDZ.js → network-DfIhNVJv.js} +1 -1
- package/lib/public/assets/overview-B1mePY2_.js +6 -0
- package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DoD3TOzZ.js} +1 -1
- package/lib/public/assets/{plus-1k0sxXyU.js → plus-BGvWvpBB.js} +1 -1
- package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-CvV5aWvl.js} +1 -1
- package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-CGVd31LR.js} +1 -1
- package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page-Bq2cvwb5.js} +1 -1
- package/lib/public/assets/{select-CiRW6v17.js → select-CTcD4jWR.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-19odlD4B.js +1 -0
- package/lib/public/assets/selector-health-page-LqIsLrjW.js +11 -0
- package/lib/public/assets/session-detail-page-DQzb8_fq.js +4 -0
- package/lib/public/assets/{settings-DhwZoWv7.js → settings-C-EnGIqs.js} +1 -1
- package/lib/public/assets/stat-tile-BY0RDTRm.js +1 -0
- package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-B4cFM-5B.js} +1 -1
- package/lib/public/assets/{teams-D0IBa2Ov.js → teams-BFe-RpYd.js} +1 -1
- package/lib/public/assets/time-D-gjmE1o.js +1 -0
- package/lib/public/assets/{trash-2-u8MlXUBD.js → trash-2-CJCKBOP9.js} +1 -1
- package/lib/public/assets/{upload-Cq8L3kpw.js → upload-DidWkbld.js} +1 -1
- package/lib/public/assets/use-builds-data-fnfxatOd.js +8 -0
- package/lib/public/assets/{use-password-reset-mode-Dbr2HEfw.js → use-password-reset-mode-iPen3tmC.js} +1 -1
- package/lib/public/assets/{users-CZqCE511.js → users-CrcjmRdf.js} +1 -1
- package/lib/public/assets/{users-DxLHYmFm.js → users-DBUv0CJx.js} +1 -1
- package/lib/public/assets/{video-off-mh1EEzP9.js → video-off-eh69-FKB.js} +1 -1
- package/lib/public/assets/view-state-CP9dE1UI.js +1 -0
- package/lib/public/assets/{webhook-settings-BYuOib-Q.js → webhook-settings-CQoSshBn.js} +1 -1
- package/lib/public/assets/{zap-BOpUur0M.js → zap-CICtWiLn.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 +43 -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 +14 -9
- 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 +198 -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 +2 -0
- 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 +131 -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 +38 -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/schema.prisma +43 -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,333 @@
|
|
|
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.SORTS = exports.TABS = void 0;
|
|
13
|
+
exports.parseSelectorListQuery = parseSelectorListQuery;
|
|
14
|
+
exports.listSelectors = listSelectors;
|
|
15
|
+
const prisma_1 = require("../../prisma");
|
|
16
|
+
const access_1 = require("./access");
|
|
17
|
+
const healingTrend_1 = require("./healingTrend");
|
|
18
|
+
const people_1 = require("./people");
|
|
19
|
+
const selectorKeys_1 = require("./selectorKeys");
|
|
20
|
+
const stateView_1 = require("./stateView");
|
|
21
|
+
exports.TABS = ['fix', 'verifying', 'fixed', 'muted'];
|
|
22
|
+
exports.SORTS = ['heals', 'recent', 'time'];
|
|
23
|
+
/** Statuses that take a selector off "To fix". */
|
|
24
|
+
const OTHER_STATUSES = ['pending', 'resolved', 'muted'];
|
|
25
|
+
function pick(raw, allowed, fallback) {
|
|
26
|
+
return typeof raw === 'string' && allowed.includes(raw)
|
|
27
|
+
? raw
|
|
28
|
+
: fallback;
|
|
29
|
+
}
|
|
30
|
+
function whole(raw, fallback, min, max) {
|
|
31
|
+
if (typeof raw !== 'string' || !/^\d+$/.test(raw))
|
|
32
|
+
return fallback;
|
|
33
|
+
return Math.min(Math.max(Number(raw), min), max);
|
|
34
|
+
}
|
|
35
|
+
function text(raw, max) {
|
|
36
|
+
return typeof raw === 'string' ? raw.trim().slice(0, max) : '';
|
|
37
|
+
}
|
|
38
|
+
function parseSelectorListQuery(q) {
|
|
39
|
+
return {
|
|
40
|
+
tab: pick(q.tab, exports.TABS, 'fix'),
|
|
41
|
+
days: whole(q.days, 30, 1, 365),
|
|
42
|
+
q: text(q.q, 200),
|
|
43
|
+
platform: text(q.platform, 40) || null,
|
|
44
|
+
method: text(q.method, 40) || null,
|
|
45
|
+
sort: pick(q.sort, exports.SORTS, 'heals'),
|
|
46
|
+
page: whole(q.page, 1, 1, 100000),
|
|
47
|
+
pageSize: whole(q.pageSize, 50, 1, 100),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Heals in the period, in the caller's sessions, on a platform and by a method if given. */
|
|
51
|
+
function healWhere(since, scope, platform, method) {
|
|
52
|
+
return {
|
|
53
|
+
AND: [
|
|
54
|
+
{ is_healed: true, createdAt: { gte: since }, original_selector: { not: null } },
|
|
55
|
+
method ? { healing_tier: method } : {},
|
|
56
|
+
(0, selectorKeys_1.sessionScope)(scope, platform),
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const forKeys = (where, keys) => ({
|
|
61
|
+
AND: [where, { OR: keys.map((k) => (0, selectorKeys_1.tupleWhere)(k.strategy, k.selector)) }],
|
|
62
|
+
});
|
|
63
|
+
/** Heals, latest heal and time spent per selector, counted by the database: no heal is left out. */
|
|
64
|
+
function healGroups(where) {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
var _a, _b, _c, _d, _e, _f;
|
|
67
|
+
const groups = yield prisma_1.prisma.sessionLog.groupBy({
|
|
68
|
+
by: ['original_strategy', 'original_selector'],
|
|
69
|
+
where,
|
|
70
|
+
_count: { _all: true },
|
|
71
|
+
_max: { createdAt: true },
|
|
72
|
+
_sum: { duration: true },
|
|
73
|
+
});
|
|
74
|
+
const out = new Map();
|
|
75
|
+
for (const g of groups) {
|
|
76
|
+
const selector = g.original_selector;
|
|
77
|
+
if (!selector)
|
|
78
|
+
continue;
|
|
79
|
+
const key = (0, selectorKeys_1.keyOf)(g.original_strategy, selector);
|
|
80
|
+
const prev = out.get(key);
|
|
81
|
+
const last = (_a = g._max.createdAt) !== null && _a !== void 0 ? _a : null;
|
|
82
|
+
const prevLast = (_b = prev === null || prev === void 0 ? void 0 : prev.lastHealedAt) !== null && _b !== void 0 ? _b : null;
|
|
83
|
+
out.set(key, {
|
|
84
|
+
key,
|
|
85
|
+
strategy: (_c = g.original_strategy) !== null && _c !== void 0 ? _c : '',
|
|
86
|
+
selector,
|
|
87
|
+
heals: ((_d = prev === null || prev === void 0 ? void 0 : prev.heals) !== null && _d !== void 0 ? _d : 0) + g._count._all,
|
|
88
|
+
lastHealedAt: prevLast && last ? (prevLast > last ? prevLast : last) : (prevLast !== null && prevLast !== void 0 ? prevLast : last),
|
|
89
|
+
timeSpentMs: ((_e = prev === null || prev === void 0 ? void 0 : prev.timeSpentMs) !== null && _e !== void 0 ? _e : 0) + ((_f = g._sum.duration) !== null && _f !== void 0 ? _f : 0),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** The value counted most often; the first one seen on a tie. */
|
|
96
|
+
function topOf(counts) {
|
|
97
|
+
let top = null;
|
|
98
|
+
let best = -1;
|
|
99
|
+
for (const [value, n] of counts) {
|
|
100
|
+
if (n > best) {
|
|
101
|
+
top = value;
|
|
102
|
+
best = n;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return top;
|
|
106
|
+
}
|
|
107
|
+
/** Sessions, top method and top suggestion, for one page's selectors only. */
|
|
108
|
+
function pageExtras(where, keys) {
|
|
109
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
111
|
+
const out = new Map();
|
|
112
|
+
if (keys.length === 0)
|
|
113
|
+
return out;
|
|
114
|
+
const scoped = forKeys(where, keys);
|
|
115
|
+
const [bySession, byMethod, byFix] = yield Promise.all([
|
|
116
|
+
prisma_1.prisma.sessionLog.groupBy({
|
|
117
|
+
by: ['original_strategy', 'original_selector', 'session_id'],
|
|
118
|
+
where: scoped,
|
|
119
|
+
}),
|
|
120
|
+
prisma_1.prisma.sessionLog.groupBy({
|
|
121
|
+
by: ['original_strategy', 'original_selector', 'healing_tier'],
|
|
122
|
+
where: scoped,
|
|
123
|
+
_count: { _all: true },
|
|
124
|
+
}),
|
|
125
|
+
prisma_1.prisma.sessionLog.groupBy({
|
|
126
|
+
by: ['original_strategy', 'original_selector', 'healed_selector', 'healed_strategy'],
|
|
127
|
+
where: { AND: [scoped, { healed_selector: { not: null } }] },
|
|
128
|
+
_count: { _all: true },
|
|
129
|
+
}),
|
|
130
|
+
]);
|
|
131
|
+
const entry = (key) => {
|
|
132
|
+
let e = out.get(key);
|
|
133
|
+
if (!e) {
|
|
134
|
+
e = { sessions: 0, topMethod: null, suggestion: null };
|
|
135
|
+
out.set(key, e);
|
|
136
|
+
}
|
|
137
|
+
return e;
|
|
138
|
+
};
|
|
139
|
+
for (const g of bySession)
|
|
140
|
+
entry((0, selectorKeys_1.keyOf)(g.original_strategy, (_a = g.original_selector) !== null && _a !== void 0 ? _a : '')).sessions += 1;
|
|
141
|
+
const methods = new Map();
|
|
142
|
+
for (const g of byMethod) {
|
|
143
|
+
if (!g.healing_tier)
|
|
144
|
+
continue;
|
|
145
|
+
const key = (0, selectorKeys_1.keyOf)(g.original_strategy, (_b = g.original_selector) !== null && _b !== void 0 ? _b : '');
|
|
146
|
+
const m = (_c = methods.get(key)) !== null && _c !== void 0 ? _c : new Map();
|
|
147
|
+
m.set(g.healing_tier, ((_d = m.get(g.healing_tier)) !== null && _d !== void 0 ? _d : 0) + g._count._all);
|
|
148
|
+
methods.set(key, m);
|
|
149
|
+
}
|
|
150
|
+
for (const [key, m] of methods)
|
|
151
|
+
entry(key).topMethod = topOf(m);
|
|
152
|
+
const fixes = new Map();
|
|
153
|
+
const fixOf = new Map();
|
|
154
|
+
for (const g of byFix) {
|
|
155
|
+
if (!g.healed_selector)
|
|
156
|
+
continue;
|
|
157
|
+
const key = (0, selectorKeys_1.keyOf)(g.original_strategy, (_e = g.original_selector) !== null && _e !== void 0 ? _e : '');
|
|
158
|
+
const fixKey = (0, selectorKeys_1.keyOf)(g.healed_strategy, g.healed_selector);
|
|
159
|
+
fixOf.set(fixKey, { selector: g.healed_selector, strategy: (_f = g.healed_strategy) !== null && _f !== void 0 ? _f : null });
|
|
160
|
+
const m = (_g = fixes.get(key)) !== null && _g !== void 0 ? _g : new Map();
|
|
161
|
+
m.set(fixKey, ((_h = m.get(fixKey)) !== null && _h !== void 0 ? _h : 0) + g._count._all);
|
|
162
|
+
fixes.set(key, m);
|
|
163
|
+
}
|
|
164
|
+
for (const [key, m] of fixes) {
|
|
165
|
+
const top = topOf(m);
|
|
166
|
+
const fix = top ? fixOf.get(top) : undefined;
|
|
167
|
+
if (top && fix)
|
|
168
|
+
entry(key).suggestion = Object.assign(Object.assign({}, fix), { count: (_j = m.get(top)) !== null && _j !== void 0 ? _j : 0 });
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Selectors whose suggested fix contains `needle` (lower case), compared here so `%` and `_` are plain text. */
|
|
174
|
+
function keysWithFixMatching(where, needle) {
|
|
175
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
176
|
+
var _a;
|
|
177
|
+
const pairs = yield prisma_1.prisma.sessionLog.groupBy({
|
|
178
|
+
by: ['original_strategy', 'original_selector', 'healed_selector'],
|
|
179
|
+
where: { AND: [where, { healed_selector: { not: null } }] },
|
|
180
|
+
});
|
|
181
|
+
const out = new Set();
|
|
182
|
+
for (const p of pairs) {
|
|
183
|
+
if (p.original_selector && ((_a = p.healed_selector) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(needle))) {
|
|
184
|
+
out.add((0, selectorKeys_1.keyOf)(p.original_strategy, p.original_selector));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const time = (d) => { var _a; return (_a = d === null || d === void 0 ? void 0 : d.getTime()) !== null && _a !== void 0 ? _a : 0; };
|
|
191
|
+
const byKey = (a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
192
|
+
const byLast = (a, b) => time(b.lastHealedAt) - time(a.lastHealedAt);
|
|
193
|
+
const SORTERS = {
|
|
194
|
+
heals: (a, b) => b.heals - a.heals || byLast(a, b) || byKey(a, b),
|
|
195
|
+
recent: (a, b) => byLast(a, b) || b.heals - a.heals || byKey(a, b),
|
|
196
|
+
time: (a, b) => b.timeSpentMs - a.timeSpentMs || b.heals - a.heals || byKey(a, b),
|
|
197
|
+
};
|
|
198
|
+
const STATUS_OF = {
|
|
199
|
+
verifying: 'pending',
|
|
200
|
+
fixed: 'resolved',
|
|
201
|
+
muted: 'muted',
|
|
202
|
+
};
|
|
203
|
+
const NEWEST_FIRST = {
|
|
204
|
+
verifying: (a, b) => time(b.fixed_at) - time(a.fixed_at),
|
|
205
|
+
fixed: (a, b) => time(b.resolved_at) - time(a.resolved_at),
|
|
206
|
+
muted: (a, b) => time(b.muted_at) - time(a.muted_at),
|
|
207
|
+
};
|
|
208
|
+
/** `page` of `all`, or its last page when `page` is past the end. */
|
|
209
|
+
function pageOf(all, page, size) {
|
|
210
|
+
const last = Math.max(1, Math.ceil(all.length / size));
|
|
211
|
+
const p = Math.min(page, last);
|
|
212
|
+
return { page: p, slice: all.slice((p - 1) * size, p * size) };
|
|
213
|
+
}
|
|
214
|
+
const keyOfState = (s) => (0, selectorKeys_1.keyOf)(s.original_strategy, s.original_selector);
|
|
215
|
+
const toKey = (s) => ({
|
|
216
|
+
strategy: s.original_strategy,
|
|
217
|
+
selector: s.original_selector,
|
|
218
|
+
});
|
|
219
|
+
function stateRows(keys) {
|
|
220
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const part of (0, selectorKeys_1.chunk)(keys)) {
|
|
223
|
+
out.push(...(yield prisma_1.prisma.selectorState.findMany({
|
|
224
|
+
where: {
|
|
225
|
+
OR: part.map((k) => ({ original_strategy: k.strategy, original_selector: k.selector })),
|
|
226
|
+
},
|
|
227
|
+
})));
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function stateViews(rows) {
|
|
233
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
234
|
+
var _a;
|
|
235
|
+
const latest = yield (0, stateView_1.latestEvents)(rows.map(toKey));
|
|
236
|
+
const people = yield (0, people_1.peopleById)(Array.from(latest.values()).flatMap((l) => { var _a, _b; return [(_a = l.fixed) === null || _a === void 0 ? void 0 : _a.user_id, (_b = l.muted) === null || _b === void 0 ? void 0 : _b.user_id]; }));
|
|
237
|
+
const out = new Map();
|
|
238
|
+
for (const r of rows) {
|
|
239
|
+
const view = (0, stateView_1.stateView)(r, (_a = latest.get(keyOfState(r))) !== null && _a !== void 0 ? _a : {}, people);
|
|
240
|
+
if (view)
|
|
241
|
+
out.set(keyOfState(r), view);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function item(k, g, extras, state) {
|
|
247
|
+
var _a, _b, _c, _d;
|
|
248
|
+
const heals = (_a = g === null || g === void 0 ? void 0 : g.heals) !== null && _a !== void 0 ? _a : 0;
|
|
249
|
+
const s = extras === null || extras === void 0 ? void 0 : extras.suggestion;
|
|
250
|
+
return {
|
|
251
|
+
strategy: k.strategy,
|
|
252
|
+
selector: k.selector,
|
|
253
|
+
heals,
|
|
254
|
+
sessions: (_b = extras === null || extras === void 0 ? void 0 : extras.sessions) !== null && _b !== void 0 ? _b : 0,
|
|
255
|
+
lastHealedAt: (g === null || g === void 0 ? void 0 : g.lastHealedAt) ? g.lastHealedAt.toISOString() : null,
|
|
256
|
+
timeSpentMs: (_c = g === null || g === void 0 ? void 0 : g.timeSpentMs) !== null && _c !== void 0 ? _c : 0,
|
|
257
|
+
topMethod: (_d = extras === null || extras === void 0 ? void 0 : extras.topMethod) !== null && _d !== void 0 ? _d : null,
|
|
258
|
+
suggestion: s && heals > 0
|
|
259
|
+
? { selector: s.selector, strategy: s.strategy, share: s.count / heals }
|
|
260
|
+
: null,
|
|
261
|
+
state,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* One tab of the list. "To fix" groups the period's heals by selector and
|
|
266
|
+
* leaves out those with another status; the other tabs start from the
|
|
267
|
+
* selectors with their status. Only selectors the caller may see.
|
|
268
|
+
*/
|
|
269
|
+
function listSelectors(query_1, scope_1) {
|
|
270
|
+
return __awaiter(this, arguments, void 0, function* (query, scope, now = new Date()) {
|
|
271
|
+
const since = new Date(now.getTime() - query.days * healingTrend_1.DAY_MS);
|
|
272
|
+
const states = yield prisma_1.prisma.selectorState.findMany({ where: { status: { in: OTHER_STATUSES } } });
|
|
273
|
+
const visible = yield (0, access_1.visibleKeys)(states.map(toKey), scope);
|
|
274
|
+
const seen = states.filter((s) => visible.has(keyOfState(s)));
|
|
275
|
+
const inTab = {
|
|
276
|
+
verifying: seen.filter((s) => s.status === STATUS_OF.verifying),
|
|
277
|
+
fixed: seen.filter((s) => s.status === STATUS_OF.fixed && !!s.resolved_at && s.resolved_at >= since),
|
|
278
|
+
muted: seen.filter((s) => s.status === STATUS_OF.muted),
|
|
279
|
+
};
|
|
280
|
+
const notToFix = new Set(states.map(keyOfState));
|
|
281
|
+
const allHeals = healWhere(since, scope, null, null);
|
|
282
|
+
const base = yield healGroups(allHeals);
|
|
283
|
+
const counts = {
|
|
284
|
+
fix: Array.from(base.keys()).filter((k) => !notToFix.has(k)).length,
|
|
285
|
+
verifying: inTab.verifying.length,
|
|
286
|
+
fixed: inTab.fixed.length,
|
|
287
|
+
muted: inTab.muted.length,
|
|
288
|
+
};
|
|
289
|
+
const answer = (total, page, items) => ({
|
|
290
|
+
tab: query.tab,
|
|
291
|
+
days: query.days,
|
|
292
|
+
page,
|
|
293
|
+
pageSize: query.pageSize,
|
|
294
|
+
total,
|
|
295
|
+
counts,
|
|
296
|
+
items,
|
|
297
|
+
});
|
|
298
|
+
const needle = query.q.toLowerCase();
|
|
299
|
+
if (query.tab === 'fix') {
|
|
300
|
+
const filtered = !!(query.platform || query.method);
|
|
301
|
+
const where = filtered ? healWhere(since, scope, query.platform, query.method) : allHeals;
|
|
302
|
+
const groups = filtered ? yield healGroups(where) : base;
|
|
303
|
+
let candidates = Array.from(groups.values()).filter((g) => !notToFix.has(g.key));
|
|
304
|
+
if (needle) {
|
|
305
|
+
const viaFix = yield keysWithFixMatching(where, needle);
|
|
306
|
+
candidates = candidates.filter((g) => g.selector.toLowerCase().includes(needle) || viaFix.has(g.key));
|
|
307
|
+
}
|
|
308
|
+
candidates.sort(SORTERS[query.sort]);
|
|
309
|
+
const { page, slice } = pageOf(candidates, query.page, query.pageSize);
|
|
310
|
+
const [extras, rows] = yield Promise.all([pageExtras(where, slice), stateRows(slice)]);
|
|
311
|
+
const views = yield stateViews(rows);
|
|
312
|
+
return answer(candidates.length, page, slice.map((g) => { var _a; return item(g, g, extras.get(g.key), (_a = views.get(g.key)) !== null && _a !== void 0 ? _a : null); }));
|
|
313
|
+
}
|
|
314
|
+
let rows = inTab[query.tab];
|
|
315
|
+
if (needle)
|
|
316
|
+
rows = rows.filter((s) => s.original_selector.toLowerCase().includes(needle));
|
|
317
|
+
rows = rows.slice().sort(NEWEST_FIRST[query.tab]);
|
|
318
|
+
const { page, slice } = pageOf(rows, query.page, query.pageSize);
|
|
319
|
+
const keys = slice.map(toKey);
|
|
320
|
+
const [groups, extras, views] = yield Promise.all([
|
|
321
|
+
keys.length > 0
|
|
322
|
+
? healGroups(forKeys(allHeals, keys))
|
|
323
|
+
: Promise.resolve(new Map()),
|
|
324
|
+
pageExtras(allHeals, keys),
|
|
325
|
+
stateViews(slice),
|
|
326
|
+
]);
|
|
327
|
+
return answer(rows.length, page, keys.map((k) => {
|
|
328
|
+
var _a;
|
|
329
|
+
const key = (0, selectorKeys_1.keyOf)(k.strategy, k.selector);
|
|
330
|
+
return item(k, groups.get(key), extras.get(key), (_a = views.get(key)) !== null && _a !== void 0 ? _a : null);
|
|
331
|
+
}));
|
|
332
|
+
});
|
|
333
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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.stateView = stateView;
|
|
13
|
+
exports.latestEvents = latestEvents;
|
|
14
|
+
const prisma_1 = require("../../prisma");
|
|
15
|
+
const selectorKeys_1 = require("./selectorKeys");
|
|
16
|
+
const iso = (d) => (d ? d.toISOString() : null);
|
|
17
|
+
function stateView(row, latest, people) {
|
|
18
|
+
var _a, _b, _c, _d;
|
|
19
|
+
if (!row)
|
|
20
|
+
return null;
|
|
21
|
+
const person = (id) => { var _a; return id ? ((_a = people.get(id)) !== null && _a !== void 0 ? _a : { id, name: null }) : null; };
|
|
22
|
+
const muted = row.status === 'muted';
|
|
23
|
+
return {
|
|
24
|
+
status: row.status,
|
|
25
|
+
cleanBuilds: row.clean_builds_count,
|
|
26
|
+
fixedAt: iso(row.fixed_at),
|
|
27
|
+
fixedBy: row.fixed_at ? person((_a = latest.fixed) === null || _a === void 0 ? void 0 : _a.user_id) : null,
|
|
28
|
+
resolvedAt: iso(row.resolved_at),
|
|
29
|
+
mutedAt: iso(row.muted_at),
|
|
30
|
+
mutedBy: muted ? person((_b = latest.muted) === null || _b === void 0 ? void 0 : _b.user_id) : null,
|
|
31
|
+
muteReason: muted ? ((_d = (_c = latest.muted) === null || _c === void 0 ? void 0 : _c.reason) !== null && _d !== void 0 ? _d : null) : null,
|
|
32
|
+
brokeAgain: row.regression_count,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The latest "marked fixed" and "muted" events of each selector, by key. */
|
|
36
|
+
function latestEvents(keys) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
var _a;
|
|
39
|
+
const out = new Map();
|
|
40
|
+
for (const part of (0, selectorKeys_1.chunk)(keys)) {
|
|
41
|
+
const events = yield prisma_1.prisma.selectorEvent.findMany({
|
|
42
|
+
where: {
|
|
43
|
+
action: { in: ['marked_fixed', 'muted'] },
|
|
44
|
+
OR: part.map((k) => ({ original_strategy: k.strategy, original_selector: k.selector })),
|
|
45
|
+
},
|
|
46
|
+
orderBy: { createdAt: 'desc' },
|
|
47
|
+
select: {
|
|
48
|
+
original_strategy: true,
|
|
49
|
+
original_selector: true,
|
|
50
|
+
action: true,
|
|
51
|
+
user_id: true,
|
|
52
|
+
reason: true,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
for (const e of events) {
|
|
56
|
+
const key = (0, selectorKeys_1.keyOf)(e.original_strategy, e.original_selector);
|
|
57
|
+
const cur = (_a = out.get(key)) !== null && _a !== void 0 ? _a : {};
|
|
58
|
+
if (e.action === 'marked_fixed' && !cur.fixed)
|
|
59
|
+
cur.fixed = { user_id: e.user_id };
|
|
60
|
+
if (e.action === 'muted' && !cur.muted)
|
|
61
|
+
cur.muted = { user_id: e.user_id, reason: e.reason };
|
|
62
|
+
out.set(key, cur);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
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.READ_ONLY_A = exports.MEMBER_A = exports.ADMIN = exports.FIX = exports.SEL = exports.BUILD = exports.PHONE = exports.USER = exports.TEAM = exports.DAY = exports.HOUR = void 0;
|
|
16
|
+
exports.selectorHealthApp = selectorHealthApp;
|
|
17
|
+
exports.seedSelectorHealth = seedSelectorHealth;
|
|
18
|
+
const express_1 = __importDefault(require("express"));
|
|
19
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
20
|
+
exports.HOUR = 60 * 60 * 1000;
|
|
21
|
+
exports.DAY = 24 * exports.HOUR;
|
|
22
|
+
exports.TEAM = { a: 'sh-team-a', b: 'sh-team-b' };
|
|
23
|
+
exports.USER = { priya: 'sh-u-priya', alex: 'sh-u-alex', gone: 'sh-u-gone' };
|
|
24
|
+
exports.PHONE = { shared: 'sh-phone-shared', a: 'sh-phone-a', b: 'sh-phone-b' };
|
|
25
|
+
exports.BUILD = { id: 'sh-build-1', name: 'nightly-2026-10-01' };
|
|
26
|
+
/** What each selector stands for is in seedSelectorHealth. */
|
|
27
|
+
exports.SEL = {
|
|
28
|
+
hot: '//android.widget.Button[@text="Confirm"]',
|
|
29
|
+
warm: 'com.acme:id/cart_total',
|
|
30
|
+
bOnly: '//team-b/only',
|
|
31
|
+
pending: '//being/verified',
|
|
32
|
+
fixed: '//fixed/recently',
|
|
33
|
+
fixedOld: '//fixed/long/ago',
|
|
34
|
+
muted: '//muted/by/priya',
|
|
35
|
+
mutedB: '//muted/team-b',
|
|
36
|
+
legacy: '//legacy/no-strategy',
|
|
37
|
+
odd: '//*[@text=\'50% off & "free" #1? [x]\']',
|
|
38
|
+
};
|
|
39
|
+
exports.FIX = {
|
|
40
|
+
hot: '//android.widget.Button[@content-desc="Confirm order"]',
|
|
41
|
+
hotLlm: 'confirm_order',
|
|
42
|
+
warm: 'com.acme:id/cart_total_v2',
|
|
43
|
+
};
|
|
44
|
+
exports.ADMIN = {
|
|
45
|
+
kind: 'user',
|
|
46
|
+
userId: 'sh-u-admin',
|
|
47
|
+
role: 'SUPER_ADMIN',
|
|
48
|
+
scopes: 'admin,devices,sessions,read',
|
|
49
|
+
teamIds: undefined,
|
|
50
|
+
};
|
|
51
|
+
exports.MEMBER_A = {
|
|
52
|
+
kind: 'user',
|
|
53
|
+
userId: exports.USER.priya,
|
|
54
|
+
role: 'MEMBER',
|
|
55
|
+
scopes: 'devices,sessions,read',
|
|
56
|
+
teamIds: [exports.TEAM.a],
|
|
57
|
+
};
|
|
58
|
+
exports.READ_ONLY_A = {
|
|
59
|
+
kind: 'api-key',
|
|
60
|
+
userId: exports.USER.priya,
|
|
61
|
+
role: 'MEMBER',
|
|
62
|
+
scopes: 'read',
|
|
63
|
+
teamIds: [exports.TEAM.a],
|
|
64
|
+
};
|
|
65
|
+
/** The dashboard routes, with `auth` as the signed-in caller. */
|
|
66
|
+
function selectorHealthApp(auth) {
|
|
67
|
+
const a = (0, express_1.default)();
|
|
68
|
+
a.use(express_1.default.json());
|
|
69
|
+
a.use((req, _res, next) => {
|
|
70
|
+
req.auth = auth;
|
|
71
|
+
next();
|
|
72
|
+
});
|
|
73
|
+
dashboard_1.default.register(a);
|
|
74
|
+
return a;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Two teams, a shared phone and one phone each, four sessions, and heals:
|
|
78
|
+
* - hot: 4 heals on the shared phone (3 Visual AI, 1 LLM), the latest an hour
|
|
79
|
+
* ago; it broke again twice after being fixed;
|
|
80
|
+
* - warm: 2 heals on team A's phone, 3 days ago, the slowest (20 s each);
|
|
81
|
+
* - bOnly: 3 heals on team B's iPhone only;
|
|
82
|
+
* - pending: being verified (2 of 3 clean builds), marked fixed by Priya;
|
|
83
|
+
* - fixed: verified 2 days ago, marked fixed by a user who has been deleted;
|
|
84
|
+
* - fixedOld: verified 60 days ago;
|
|
85
|
+
* - muted: muted by Priya, with a reason; mutedB: muted by Alex, team B only;
|
|
86
|
+
* - legacy: a heal with no strategy; odd: a selector full of punctuation.
|
|
87
|
+
*/
|
|
88
|
+
function seedSelectorHealth(db_1) {
|
|
89
|
+
return __awaiter(this, arguments, void 0, function* (db, now = Date.now()) {
|
|
90
|
+
yield db.team.create({ data: { id: exports.TEAM.a, name: 'Team A' } });
|
|
91
|
+
yield db.team.create({ data: { id: exports.TEAM.b, name: 'Team B' } });
|
|
92
|
+
for (const [id, name] of [
|
|
93
|
+
[exports.USER.priya, 'Priya'],
|
|
94
|
+
[exports.USER.alex, 'Alex'],
|
|
95
|
+
]) {
|
|
96
|
+
yield db.user.create({
|
|
97
|
+
data: { id, name, email: `${id}@xenon.local`, passwordHash: 'x', accessKey: `ak-${id}` },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const phones = [
|
|
101
|
+
[exports.PHONE.shared, 'Shared Pixel', 'android', null],
|
|
102
|
+
[exports.PHONE.a, 'Team A Galaxy', 'android', exports.TEAM.a],
|
|
103
|
+
[exports.PHONE.b, 'Team B iPhone', 'ios', exports.TEAM.b],
|
|
104
|
+
];
|
|
105
|
+
for (const [udid, name, platform, teamId] of phones) {
|
|
106
|
+
yield db.device.create({ data: { udid, host: 'localhost', name, platform, teamId } });
|
|
107
|
+
}
|
|
108
|
+
yield db.build.create({ data: { id: exports.BUILD.id, name: exports.BUILD.name } });
|
|
109
|
+
const session = (id, udid, name, platform, buildId) => db.session.create({
|
|
110
|
+
data: {
|
|
111
|
+
id,
|
|
112
|
+
device_udid: udid,
|
|
113
|
+
device_name: name,
|
|
114
|
+
device_platform: platform,
|
|
115
|
+
build_id: buildId,
|
|
116
|
+
desired_capabilities: '{}',
|
|
117
|
+
session_capabilities: '{}',
|
|
118
|
+
node_id: 'localhost',
|
|
119
|
+
has_live_video: false,
|
|
120
|
+
device_version: '14',
|
|
121
|
+
status: 'passed',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
yield session('sh-s-shared-1', exports.PHONE.shared, 'Shared Pixel', 'android', exports.BUILD.id);
|
|
125
|
+
yield session('sh-s-shared-2', exports.PHONE.shared, 'Shared Pixel', 'android', null);
|
|
126
|
+
yield session('sh-s-a-1', exports.PHONE.a, 'Team A Galaxy', 'android', exports.BUILD.id);
|
|
127
|
+
yield session('sh-s-b-1', exports.PHONE.b, 'Team B iPhone', 'ios', null);
|
|
128
|
+
const heal = (sessionId, selector, ageMs, over = {}) => db.sessionLog.create({
|
|
129
|
+
data: Object.assign({ session_id: sessionId, url: '/element', method: 'POST', title: 'findElement', response: '{}', command_name: 'findElement', is_healed: true, original_strategy: 'xpath', original_selector: selector, healed_strategy: 'xpath', healed_selector: `${selector}-healed`, healing_tier: 'Fuzzy XML', healing_confidence: 0.8, duration: 1000, createdAt: new Date(now - ageMs) }, over),
|
|
130
|
+
});
|
|
131
|
+
for (const age of [1, 1.5, 3]) {
|
|
132
|
+
yield heal('sh-s-shared-1', exports.SEL.hot, age * exports.HOUR, {
|
|
133
|
+
healed_selector: exports.FIX.hot,
|
|
134
|
+
healing_tier: 'Visual AI',
|
|
135
|
+
healing_confidence: 0.9,
|
|
136
|
+
duration: 3000,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
yield heal('sh-s-shared-2', exports.SEL.hot, 4 * exports.HOUR, {
|
|
140
|
+
healed_selector: exports.FIX.hotLlm,
|
|
141
|
+
healed_strategy: 'accessibility id',
|
|
142
|
+
healing_tier: 'LLM',
|
|
143
|
+
healing_confidence: 0.5,
|
|
144
|
+
duration: 9000,
|
|
145
|
+
});
|
|
146
|
+
// A lookup of the same selector that needed no healing is never counted.
|
|
147
|
+
yield db.sessionLog.create({
|
|
148
|
+
data: {
|
|
149
|
+
session_id: 'sh-s-shared-1',
|
|
150
|
+
url: '/element',
|
|
151
|
+
method: 'POST',
|
|
152
|
+
title: 'findElement',
|
|
153
|
+
response: '{}',
|
|
154
|
+
command_name: 'findElement',
|
|
155
|
+
is_healed: false,
|
|
156
|
+
original_strategy: 'xpath',
|
|
157
|
+
original_selector: exports.SEL.hot,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
for (const age of [3, 3.1]) {
|
|
161
|
+
yield heal('sh-s-a-1', exports.SEL.warm, age * exports.DAY, {
|
|
162
|
+
original_strategy: 'id',
|
|
163
|
+
healed_strategy: 'id',
|
|
164
|
+
healed_selector: exports.FIX.warm,
|
|
165
|
+
duration: 20000,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
for (const age of [1, 1.1, 1.2])
|
|
169
|
+
yield heal('sh-s-b-1', exports.SEL.bOnly, age * exports.DAY);
|
|
170
|
+
yield heal('sh-s-shared-1', exports.SEL.pending, 10 * exports.DAY);
|
|
171
|
+
yield heal('sh-s-a-1', exports.SEL.fixed, 20 * exports.DAY);
|
|
172
|
+
yield heal('sh-s-shared-1', exports.SEL.fixedOld, 89 * exports.DAY);
|
|
173
|
+
yield heal('sh-s-a-1', exports.SEL.muted, 1 * exports.DAY);
|
|
174
|
+
yield heal('sh-s-b-1', exports.SEL.mutedB, 1 * exports.DAY);
|
|
175
|
+
yield heal('sh-s-shared-2', exports.SEL.legacy, 5 * exports.HOUR, { original_strategy: null });
|
|
176
|
+
yield heal('sh-s-shared-2', exports.SEL.odd, 6 * exports.HOUR);
|
|
177
|
+
const state = (selector, data) => db.selectorState.create({
|
|
178
|
+
data: Object.assign({ original_strategy: 'xpath', original_selector: selector }, data),
|
|
179
|
+
});
|
|
180
|
+
yield state(exports.SEL.hot, { status: 'active', regression_count: 2 });
|
|
181
|
+
yield state(exports.SEL.pending, {
|
|
182
|
+
status: 'pending',
|
|
183
|
+
fixed_at: new Date(now - 5 * exports.DAY),
|
|
184
|
+
clean_builds_count: 2,
|
|
185
|
+
});
|
|
186
|
+
yield state(exports.SEL.fixed, {
|
|
187
|
+
status: 'resolved',
|
|
188
|
+
fixed_at: new Date(now - 9 * exports.DAY),
|
|
189
|
+
resolved_at: new Date(now - 2 * exports.DAY),
|
|
190
|
+
clean_builds_count: 3,
|
|
191
|
+
});
|
|
192
|
+
yield state(exports.SEL.fixedOld, {
|
|
193
|
+
status: 'resolved',
|
|
194
|
+
fixed_at: new Date(now - 70 * exports.DAY),
|
|
195
|
+
resolved_at: new Date(now - 60 * exports.DAY),
|
|
196
|
+
clean_builds_count: 3,
|
|
197
|
+
});
|
|
198
|
+
yield state(exports.SEL.muted, { status: 'muted', muted_at: new Date(now - 1 * exports.DAY) });
|
|
199
|
+
yield state(exports.SEL.mutedB, { status: 'muted', muted_at: new Date(now - 2 * exports.DAY) });
|
|
200
|
+
const event = (selector, action, userId, ageMs, reason = null) => db.selectorEvent.create({
|
|
201
|
+
data: {
|
|
202
|
+
original_strategy: 'xpath',
|
|
203
|
+
original_selector: selector,
|
|
204
|
+
action,
|
|
205
|
+
user_id: userId,
|
|
206
|
+
reason,
|
|
207
|
+
createdAt: new Date(now - ageMs),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
yield event(exports.SEL.pending, 'marked_fixed', exports.USER.priya, 5 * exports.DAY);
|
|
211
|
+
yield event(exports.SEL.fixed, 'marked_fixed', exports.USER.gone, 9 * exports.DAY);
|
|
212
|
+
yield event(exports.SEL.fixed, 'verified', null, 2 * exports.DAY);
|
|
213
|
+
yield event(exports.SEL.muted, 'muted', exports.USER.priya, 1 * exports.DAY, 'Screen being redesigned');
|
|
214
|
+
yield event(exports.SEL.mutedB, 'muted', exports.USER.alex, 2 * exports.DAY);
|
|
215
|
+
});
|
|
216
|
+
}
|