@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
|
@@ -18,28 +18,28 @@ model Build {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
model Session {
|
|
21
|
-
id String
|
|
21
|
+
id String @id
|
|
22
22
|
build_id String?
|
|
23
23
|
name String?
|
|
24
|
-
status String
|
|
24
|
+
status String @default("running")
|
|
25
25
|
desired_capabilities String
|
|
26
26
|
session_capabilities String
|
|
27
27
|
node_id String
|
|
28
28
|
has_live_video Boolean
|
|
29
|
-
video_recording_enabled Boolean
|
|
29
|
+
video_recording_enabled Boolean @default(true)
|
|
30
30
|
video_recording String?
|
|
31
|
-
startTime DateTime
|
|
31
|
+
startTime DateTime @default(now())
|
|
32
32
|
endTime DateTime?
|
|
33
33
|
failure_reason String?
|
|
34
|
-
is_profiling_available Boolean
|
|
34
|
+
is_profiling_available Boolean @default(false)
|
|
35
35
|
device_info String?
|
|
36
36
|
device_udid String
|
|
37
37
|
device_platform String
|
|
38
38
|
device_version String
|
|
39
39
|
device_name String?
|
|
40
|
-
createdAt DateTime
|
|
41
|
-
updatedAt DateTime
|
|
42
|
-
performance_trace String?
|
|
40
|
+
createdAt DateTime @default(now())
|
|
41
|
+
updatedAt DateTime @updatedAt
|
|
42
|
+
performance_trace String? @map("performance_trace")
|
|
43
43
|
failure_category String?
|
|
44
44
|
ai_analysis String?
|
|
45
45
|
tags String?
|
|
@@ -51,7 +51,8 @@ model Session {
|
|
|
51
51
|
user_id String?
|
|
52
52
|
Log Log[]
|
|
53
53
|
Profiling Profiling[]
|
|
54
|
-
|
|
54
|
+
SessionMetric SessionMetric[]
|
|
55
|
+
build Build? @relation(fields: [build_id], references: [id])
|
|
55
56
|
SessionLog SessionLog[]
|
|
56
57
|
Recording Recording[]
|
|
57
58
|
|
|
@@ -118,6 +119,24 @@ model Profiling {
|
|
|
118
119
|
session Session @relation(fields: [session_id], references: [id])
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/// CPU and memory sampled every 2 s while a session runs (SessionMetricsService).
|
|
123
|
+
model SessionMetric {
|
|
124
|
+
id Int @id @default(autoincrement())
|
|
125
|
+
session_id String
|
|
126
|
+
/// epoch ms
|
|
127
|
+
at Float
|
|
128
|
+
device_cpu_pct Float?
|
|
129
|
+
device_mem_mb Float?
|
|
130
|
+
device_mem_total Float?
|
|
131
|
+
app_cpu_pct Float?
|
|
132
|
+
app_mem_mb Float?
|
|
133
|
+
/// The package the app figures are for (Android), else null.
|
|
134
|
+
app_id String?
|
|
135
|
+
session Session @relation(fields: [session_id], references: [id], onDelete: Cascade)
|
|
136
|
+
|
|
137
|
+
@@index([session_id, at])
|
|
138
|
+
}
|
|
139
|
+
|
|
121
140
|
model App {
|
|
122
141
|
id String @id @default(uuid())
|
|
123
142
|
name String
|
|
@@ -351,6 +370,21 @@ model SelectorState {
|
|
|
351
370
|
@@index([fixed_at])
|
|
352
371
|
}
|
|
353
372
|
|
|
373
|
+
// What happened to a selector, and who did it: one row per status change.
|
|
374
|
+
// No relation to SelectorState (unmute and cancel can delete that row; the
|
|
375
|
+
// history stays) or to User (a deleted user's actions stay, without a name).
|
|
376
|
+
model SelectorEvent {
|
|
377
|
+
id String @id @default(uuid())
|
|
378
|
+
original_strategy String
|
|
379
|
+
original_selector String
|
|
380
|
+
action String
|
|
381
|
+
user_id String?
|
|
382
|
+
reason String?
|
|
383
|
+
createdAt DateTime @default(now())
|
|
384
|
+
|
|
385
|
+
@@index([original_strategy, original_selector, createdAt])
|
|
386
|
+
}
|
|
387
|
+
|
|
354
388
|
model Recording {
|
|
355
389
|
id String @id @default(uuid())
|
|
356
390
|
group_id String
|
|
@@ -211,6 +211,18 @@ exports.Prisma.ProfilingScalarFieldEnum = {
|
|
|
211
211
|
updatedAt: 'updatedAt'
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
exports.Prisma.SessionMetricScalarFieldEnum = {
|
|
215
|
+
id: 'id',
|
|
216
|
+
session_id: 'session_id',
|
|
217
|
+
at: 'at',
|
|
218
|
+
device_cpu_pct: 'device_cpu_pct',
|
|
219
|
+
device_mem_mb: 'device_mem_mb',
|
|
220
|
+
device_mem_total: 'device_mem_total',
|
|
221
|
+
app_cpu_pct: 'app_cpu_pct',
|
|
222
|
+
app_mem_mb: 'app_mem_mb',
|
|
223
|
+
app_id: 'app_id'
|
|
224
|
+
};
|
|
225
|
+
|
|
214
226
|
exports.Prisma.AppScalarFieldEnum = {
|
|
215
227
|
id: 'id',
|
|
216
228
|
name: 'name',
|
|
@@ -399,6 +411,16 @@ exports.Prisma.SelectorStateScalarFieldEnum = {
|
|
|
399
411
|
updatedAt: 'updatedAt'
|
|
400
412
|
};
|
|
401
413
|
|
|
414
|
+
exports.Prisma.SelectorEventScalarFieldEnum = {
|
|
415
|
+
id: 'id',
|
|
416
|
+
original_strategy: 'original_strategy',
|
|
417
|
+
original_selector: 'original_selector',
|
|
418
|
+
action: 'action',
|
|
419
|
+
user_id: 'user_id',
|
|
420
|
+
reason: 'reason',
|
|
421
|
+
createdAt: 'createdAt'
|
|
422
|
+
};
|
|
423
|
+
|
|
402
424
|
exports.Prisma.RecordingScalarFieldEnum = {
|
|
403
425
|
id: 'id',
|
|
404
426
|
group_id: 'group_id',
|
|
@@ -510,6 +532,7 @@ exports.Prisma.ModelName = {
|
|
|
510
532
|
SessionLog: 'SessionLog',
|
|
511
533
|
Log: 'Log',
|
|
512
534
|
Profiling: 'Profiling',
|
|
535
|
+
SessionMetric: 'SessionMetric',
|
|
513
536
|
App: 'App',
|
|
514
537
|
Device: 'Device',
|
|
515
538
|
PendingSession: 'PendingSession',
|
|
@@ -522,6 +545,7 @@ exports.Prisma.ModelName = {
|
|
|
522
545
|
ApiKey: 'ApiKey',
|
|
523
546
|
Team: 'Team',
|
|
524
547
|
SelectorState: 'SelectorState',
|
|
548
|
+
SelectorEvent: 'SelectorEvent',
|
|
525
549
|
Recording: 'Recording',
|
|
526
550
|
Bookmark: 'Bookmark',
|
|
527
551
|
Annotation: 'Annotation',
|
package/lib/src/prisma.js
CHANGED
|
@@ -38,6 +38,8 @@ const MODEL_DELEGATES = new Set([
|
|
|
38
38
|
'lease', 'portLease', 'apiKey', 'selectorState', 'user', 'userSession',
|
|
39
39
|
'passwordResetToken', 'team', 'teamMember', 'eventLog', 'project', 'recording', 'bookmark',
|
|
40
40
|
'annotation',
|
|
41
|
+
'sessionMetric',
|
|
42
|
+
'selectorEvent',
|
|
41
43
|
]);
|
|
42
44
|
/** Cache of plain-object wrappers, keyed by model name. */
|
|
43
45
|
const _modelWrappers = new Map();
|
|
@@ -308,6 +308,7 @@ let CleanupService = class CleanupService {
|
|
|
308
308
|
prisma_1.prisma.sessionLog.deleteMany({ where: { session_id: sessionId } }),
|
|
309
309
|
prisma_1.prisma.log.deleteMany({ where: { session_id: sessionId } }),
|
|
310
310
|
prisma_1.prisma.profiling.deleteMany({ where: { session_id: sessionId } }),
|
|
311
|
+
prisma_1.prisma.sessionMetric.deleteMany({ where: { session_id: sessionId } }),
|
|
311
312
|
]);
|
|
312
313
|
});
|
|
313
314
|
}
|
|
@@ -114,7 +114,7 @@ let NotificationService = class NotificationService {
|
|
|
114
114
|
}
|
|
115
115
|
sendSlackMessage(url, eventType, payload) {
|
|
116
116
|
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
-
var _a, _b, _c
|
|
117
|
+
var _a, _b, _c;
|
|
118
118
|
let text = '';
|
|
119
119
|
let color = '#36a64f'; // green
|
|
120
120
|
switch (eventType) {
|
|
@@ -135,15 +135,14 @@ let NotificationService = class NotificationService {
|
|
|
135
135
|
// formatting end-to-end.
|
|
136
136
|
const totalHeals = (_a = payload.totalHeals) !== null && _a !== void 0 ? _a : 0;
|
|
137
137
|
const distinctSelectors = (_b = payload.distinctSelectors) !== null && _b !== void 0 ? _b : 0;
|
|
138
|
-
const
|
|
139
|
-
const windowDays = (_d = payload.windowDays) !== null && _d !== void 0 ? _d : 30;
|
|
138
|
+
const windowDays = (_c = payload.windowDays) !== null && _c !== void 0 ? _c : 30;
|
|
140
139
|
const top = Array.isArray(payload.hotspots) ? payload.hotspots : [];
|
|
141
140
|
const lines = top
|
|
142
141
|
.slice(0, 5)
|
|
143
142
|
.map((h, i) => `${i + 1}. *${h.healCount}× healed* — \`${h.originalSelector}\`` +
|
|
144
143
|
(h.suggestedRewrite ? `\n ↳ rewrite: \`${h.suggestedRewrite}\`` : ''))
|
|
145
144
|
.join('\n');
|
|
146
|
-
const summary = `🩺 *Selector Health digest* — last ${windowDays}d\n${totalHeals} heals across ${distinctSelectors} selectors
|
|
145
|
+
const summary = `🩺 *Selector Health digest* — last ${windowDays}d\n${totalHeals} heals across ${distinctSelectors} selectors`;
|
|
147
146
|
const body = {
|
|
148
147
|
text: summary,
|
|
149
148
|
attachments: [
|
|
@@ -22,6 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
22
22
|
};
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.SelectorStateService = exports.SelectorStateConflictError = void 0;
|
|
25
|
+
exports.selectorEventData = selectorEventData;
|
|
25
26
|
const typedi_1 = require("typedi");
|
|
26
27
|
const prisma_1 = require("../prisma");
|
|
27
28
|
const SocketServer_1 = require("./SocketServer");
|
|
@@ -50,6 +51,18 @@ function whereTuple(strategy, selector) {
|
|
|
50
51
|
},
|
|
51
52
|
};
|
|
52
53
|
}
|
|
54
|
+
/** The SelectorEvent row for one change. */
|
|
55
|
+
function selectorEventData(tuple, action, userId, reason) {
|
|
56
|
+
return {
|
|
57
|
+
data: {
|
|
58
|
+
original_strategy: tuple.strategy,
|
|
59
|
+
original_selector: tuple.selector,
|
|
60
|
+
action,
|
|
61
|
+
user_id: userId !== null && userId !== void 0 ? userId : null,
|
|
62
|
+
reason: reason !== null && reason !== void 0 ? reason : null,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
53
66
|
/**
|
|
54
67
|
* Convert a SelectorState row into a plain object with ISO-8601 timestamps
|
|
55
68
|
* suitable for socket.io broadcast and JSON serialization.
|
|
@@ -76,7 +89,8 @@ function serialize(row) {
|
|
|
76
89
|
* Owns SelectorState lifecycle transitions. Every transition is idempotent
|
|
77
90
|
* where the spec allows, emits the corresponding `SELECTOR_*` socket event,
|
|
78
91
|
* and uses lazy row creation — rows only exist once a user has taken an
|
|
79
|
-
* action against the selector.
|
|
92
|
+
* action against the selector. Each change and its SelectorEvent are one
|
|
93
|
+
* transaction, so the status and its history can't disagree.
|
|
80
94
|
*
|
|
81
95
|
* The constructor accepts both prisma + socket as parameters (defaulting to
|
|
82
96
|
* the real implementations) so tests can pass Sinon stubs without going
|
|
@@ -94,33 +108,37 @@ let SelectorStateService = class SelectorStateService {
|
|
|
94
108
|
*/
|
|
95
109
|
markFixed(ctx) {
|
|
96
110
|
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
const row = yield this.prisma.$transaction((tx) => __awaiter(this, void 0, void 0, function* () {
|
|
112
|
+
const existing = yield tx.selectorState.findUnique({
|
|
113
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
114
|
+
});
|
|
115
|
+
if (existing && existing.status === 'muted') {
|
|
116
|
+
throw new SelectorStateConflictError(`Cannot markFixed on a muted selector (${ctx.strategy}=${ctx.selector})`, existing.status);
|
|
117
|
+
}
|
|
118
|
+
const now = new Date();
|
|
119
|
+
const upserted = yield tx.selectorState.upsert({
|
|
120
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
121
|
+
create: {
|
|
122
|
+
original_strategy: ctx.strategy,
|
|
123
|
+
original_selector: ctx.selector,
|
|
124
|
+
status: 'pending',
|
|
125
|
+
fixed_at: now,
|
|
126
|
+
fixed_by_api_key: ctx.apiKeyId,
|
|
127
|
+
clean_builds_count: 0,
|
|
128
|
+
last_event_at: now,
|
|
129
|
+
},
|
|
130
|
+
update: {
|
|
131
|
+
status: 'pending',
|
|
132
|
+
fixed_at: now,
|
|
133
|
+
fixed_by_api_key: ctx.apiKeyId,
|
|
134
|
+
clean_builds_count: 0,
|
|
135
|
+
resolved_at: null,
|
|
136
|
+
last_event_at: now,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
yield tx.selectorEvent.create(selectorEventData(ctx, 'marked_fixed', ctx.userId));
|
|
140
|
+
return upserted;
|
|
141
|
+
}));
|
|
124
142
|
scopedLog.info(`markFixed: ${ctx.strategy}=${ctx.selector} → pending (api_key=${ctx.apiKeyId})`);
|
|
125
143
|
this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_FIXED, serialize(row));
|
|
126
144
|
return row;
|
|
@@ -128,29 +146,33 @@ let SelectorStateService = class SelectorStateService {
|
|
|
128
146
|
}
|
|
129
147
|
/**
|
|
130
148
|
* Silence a selector. Idempotent — re-muting an already-muted selector
|
|
131
|
-
* just refreshes muted_at / muted_by_api_key.
|
|
149
|
+
* just refreshes muted_at / muted_by_api_key, and records the new reason.
|
|
132
150
|
*/
|
|
133
151
|
mute(ctx) {
|
|
134
152
|
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const row = yield this.prisma.$transaction((tx) => __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
const now = new Date();
|
|
155
|
+
const upserted = yield tx.selectorState.upsert({
|
|
156
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
157
|
+
create: {
|
|
158
|
+
original_strategy: ctx.strategy,
|
|
159
|
+
original_selector: ctx.selector,
|
|
160
|
+
status: 'muted',
|
|
161
|
+
muted_at: now,
|
|
162
|
+
muted_by_api_key: ctx.apiKeyId,
|
|
163
|
+
last_event_at: now,
|
|
164
|
+
},
|
|
165
|
+
update: {
|
|
166
|
+
// Preserve any prior fixed_at / resolved_at history (Pending → Muted spec).
|
|
167
|
+
status: 'muted',
|
|
168
|
+
muted_at: now,
|
|
169
|
+
muted_by_api_key: ctx.apiKeyId,
|
|
170
|
+
last_event_at: now,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
yield tx.selectorEvent.create(selectorEventData(ctx, 'muted', ctx.userId, ctx.reason));
|
|
174
|
+
return upserted;
|
|
175
|
+
}));
|
|
154
176
|
scopedLog.info(`mute: ${ctx.strategy}=${ctx.selector} → muted (api_key=${ctx.apiKeyId})`);
|
|
155
177
|
this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_MUTED, serialize(row));
|
|
156
178
|
return row;
|
|
@@ -167,37 +189,45 @@ let SelectorStateService = class SelectorStateService {
|
|
|
167
189
|
* - `resolved_at != null` (it has reached the resolved terminal state).
|
|
168
190
|
*
|
|
169
191
|
* Calling `unmute` on a row whose status is not `'muted'` (including a
|
|
170
|
-
* missing row) is a silent no-op: the existing row is returned unchanged
|
|
171
|
-
* and no socket event is emitted.
|
|
192
|
+
* missing row) is a silent no-op: the existing row is returned unchanged,
|
|
193
|
+
* nothing is recorded and no socket event is emitted.
|
|
172
194
|
*/
|
|
173
195
|
unmute(ctx) {
|
|
174
196
|
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
});
|
|
178
|
-
if (!existing || existing.status !== 'muted') {
|
|
179
|
-
return existing;
|
|
180
|
-
}
|
|
181
|
-
const hasOtherHistory = existing.regression_count > 0 || existing.fixed_at !== null || existing.resolved_at !== null;
|
|
182
|
-
let result;
|
|
183
|
-
if (!hasOtherHistory) {
|
|
184
|
-
yield this.prisma.selectorState.delete({
|
|
197
|
+
const outcome = yield this.prisma.$transaction((tx) => __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
const existing = yield tx.selectorState.findUnique({
|
|
185
199
|
where: whereTuple(ctx.strategy, ctx.selector),
|
|
186
200
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
+
if (!existing || existing.status !== 'muted') {
|
|
202
|
+
return { changed: false, row: existing };
|
|
203
|
+
}
|
|
204
|
+
const hasOtherHistory = existing.regression_count > 0 ||
|
|
205
|
+
existing.fixed_at !== null ||
|
|
206
|
+
existing.resolved_at !== null;
|
|
207
|
+
let result;
|
|
208
|
+
if (!hasOtherHistory) {
|
|
209
|
+
yield tx.selectorState.delete({
|
|
210
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
211
|
+
});
|
|
212
|
+
result = null;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
result = yield tx.selectorState.update({
|
|
216
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
217
|
+
data: {
|
|
218
|
+
status: 'active',
|
|
219
|
+
muted_at: null,
|
|
220
|
+
muted_by_api_key: null,
|
|
221
|
+
last_event_at: new Date(),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
yield tx.selectorEvent.create(selectorEventData(ctx, 'unmuted', ctx.userId));
|
|
226
|
+
return { changed: true, row: result };
|
|
227
|
+
}));
|
|
228
|
+
if (!outcome.changed)
|
|
229
|
+
return outcome.row;
|
|
230
|
+
const result = outcome.row;
|
|
201
231
|
scopedLog.info(`unmute: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
|
|
202
232
|
// Emit the post-state. When the row was deleted, surface the prior tuple
|
|
203
233
|
// so the dashboard can drop it from the muted list.
|
|
@@ -218,33 +248,36 @@ let SelectorStateService = class SelectorStateService {
|
|
|
218
248
|
*/
|
|
219
249
|
cancelVerification(ctx) {
|
|
220
250
|
return __awaiter(this, void 0, void 0, function* () {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
});
|
|
224
|
-
if (!existing || existing.status !== 'pending') {
|
|
225
|
-
throw new SelectorStateConflictError(`cancelVerification requires status=pending (${ctx.strategy}=${ctx.selector})`, existing ? existing.status : 'none');
|
|
226
|
-
}
|
|
227
|
-
const hasOtherHistory = existing.regression_count > 0 || existing.resolved_at !== null;
|
|
228
|
-
let result;
|
|
229
|
-
if (!hasOtherHistory) {
|
|
230
|
-
yield this.prisma.selectorState.delete({
|
|
231
|
-
where: whereTuple(ctx.strategy, ctx.selector),
|
|
232
|
-
});
|
|
233
|
-
result = null;
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
const now = new Date();
|
|
237
|
-
result = yield this.prisma.selectorState.update({
|
|
251
|
+
const result = yield this.prisma.$transaction((tx) => __awaiter(this, void 0, void 0, function* () {
|
|
252
|
+
const existing = yield tx.selectorState.findUnique({
|
|
238
253
|
where: whereTuple(ctx.strategy, ctx.selector),
|
|
239
|
-
data: {
|
|
240
|
-
status: 'active',
|
|
241
|
-
fixed_at: null,
|
|
242
|
-
fixed_by_api_key: null,
|
|
243
|
-
clean_builds_count: 0,
|
|
244
|
-
last_event_at: now,
|
|
245
|
-
},
|
|
246
254
|
});
|
|
247
|
-
|
|
255
|
+
if (!existing || existing.status !== 'pending') {
|
|
256
|
+
throw new SelectorStateConflictError(`cancelVerification requires status=pending (${ctx.strategy}=${ctx.selector})`, existing ? existing.status : 'none');
|
|
257
|
+
}
|
|
258
|
+
const hasOtherHistory = existing.regression_count > 0 || existing.resolved_at !== null;
|
|
259
|
+
let row;
|
|
260
|
+
if (!hasOtherHistory) {
|
|
261
|
+
yield tx.selectorState.delete({
|
|
262
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
263
|
+
});
|
|
264
|
+
row = null;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
row = yield tx.selectorState.update({
|
|
268
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
269
|
+
data: {
|
|
270
|
+
status: 'active',
|
|
271
|
+
fixed_at: null,
|
|
272
|
+
fixed_by_api_key: null,
|
|
273
|
+
clean_builds_count: 0,
|
|
274
|
+
last_event_at: new Date(),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
yield tx.selectorEvent.create(selectorEventData(ctx, 'verification_cancelled', ctx.userId));
|
|
279
|
+
return row;
|
|
280
|
+
}));
|
|
248
281
|
scopedLog.info(`cancelVerification: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
|
|
249
282
|
this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, result
|
|
250
283
|
? serialize(result)
|
|
@@ -260,8 +293,8 @@ let SelectorStateService = class SelectorStateService {
|
|
|
260
293
|
* Heal-write hook called from the heal write path when `is_healed=true` is
|
|
261
294
|
* about to be persisted. If the (strategy, selector) row is in `'pending'`
|
|
262
295
|
* or `'resolved'` state, transitions it back to `'active'`, increments
|
|
263
|
-
* `regression_count`, and emits `SELECTOR_REGRESSED`.
|
|
264
|
-
* absent, muted, or already active.
|
|
296
|
+
* `regression_count`, records `broke_again`, and emits `SELECTOR_REGRESSED`.
|
|
297
|
+
* No-op if the row is absent, muted, or already active.
|
|
265
298
|
*
|
|
266
299
|
* Return value: provided for testing/integration scenarios. Production
|
|
267
300
|
* callers in the heal write path (`event-manager.ts`) should treat this
|
|
@@ -270,6 +303,8 @@ let SelectorStateService = class SelectorStateService {
|
|
|
270
303
|
*/
|
|
271
304
|
onHealRecorded(ctx) {
|
|
272
305
|
return __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
// Read outside a transaction: this runs on every heal, and almost every
|
|
307
|
+
// heal changes nothing.
|
|
273
308
|
const existing = yield this.prisma.selectorState.findUnique({
|
|
274
309
|
where: whereTuple(ctx.strategy, ctx.selector),
|
|
275
310
|
});
|
|
@@ -278,18 +313,21 @@ let SelectorStateService = class SelectorStateService {
|
|
|
278
313
|
if (existing.status !== 'pending' && existing.status !== 'resolved') {
|
|
279
314
|
return existing;
|
|
280
315
|
}
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
316
|
+
const row = yield this.prisma.$transaction((tx) => __awaiter(this, void 0, void 0, function* () {
|
|
317
|
+
const updated = yield tx.selectorState.update({
|
|
318
|
+
where: whereTuple(ctx.strategy, ctx.selector),
|
|
319
|
+
data: {
|
|
320
|
+
status: 'active',
|
|
321
|
+
fixed_at: null,
|
|
322
|
+
resolved_at: null,
|
|
323
|
+
regression_count: { increment: 1 },
|
|
324
|
+
clean_builds_count: 0,
|
|
325
|
+
last_event_at: new Date(),
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
yield tx.selectorEvent.create(selectorEventData(ctx, 'broke_again'));
|
|
329
|
+
return updated;
|
|
330
|
+
}));
|
|
293
331
|
scopedLog.warn(`onHealRecorded: ${ctx.strategy}=${ctx.selector} regressed from ${existing.status} (session=${ctx.sessionId})`);
|
|
294
332
|
this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, serialize(row));
|
|
295
333
|
return row;
|
|
@@ -27,6 +27,7 @@ const prisma_1 = require("../prisma");
|
|
|
27
27
|
const SocketServer_1 = require("./SocketServer");
|
|
28
28
|
const SocketEvents_1 = require("../enums/SocketEvents");
|
|
29
29
|
const logger_1 = __importDefault(require("../logger"));
|
|
30
|
+
const SelectorStateService_1 = require("./SelectorStateService");
|
|
30
31
|
const log = logger_1.default.scope('SelectorVerification');
|
|
31
32
|
/**
|
|
32
33
|
* Threshold of distinct clean build_ids required to promote a Pending
|
|
@@ -109,15 +110,19 @@ let SelectorVerificationJob = class SelectorVerificationJob {
|
|
|
109
110
|
promoteToResolved(row) {
|
|
110
111
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
112
|
const now = new Date();
|
|
112
|
-
const updated = yield this.prisma
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
const updated = yield this.prisma.$transaction((tx) => __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const promoted = yield tx.selectorState.update({
|
|
115
|
+
where: { id: row.id },
|
|
116
|
+
data: {
|
|
117
|
+
status: 'resolved',
|
|
118
|
+
resolved_at: now,
|
|
119
|
+
clean_builds_count: CLEAN_BUILDS_TO_RESOLVE,
|
|
120
|
+
last_event_at: now,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
yield tx.selectorEvent.create((0, SelectorStateService_1.selectorEventData)({ strategy: row.original_strategy, selector: row.original_selector }, 'verified'));
|
|
124
|
+
return promoted;
|
|
125
|
+
}));
|
|
121
126
|
log.info(`[${row.id}] promoted to resolved (${row.original_strategy}=${row.original_selector})`);
|
|
122
127
|
this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_RESOLVED, this.serialize(updated));
|
|
123
128
|
});
|