@oneuptime/common 11.5.11 → 11.5.13

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.
Files changed (142) hide show
  1. package/Models/AnalyticsModels/NetworkFlow.ts +12 -1
  2. package/Models/DatabaseModels/AIInsight.ts +30 -0
  3. package/Models/DatabaseModels/Project.ts +30 -0
  4. package/Models/DatabaseModels/TelemetryException.ts +128 -0
  5. package/Server/API/AIAgentDataAPI.ts +129 -5
  6. package/Server/API/AIInvestigationAPI.ts +87 -0
  7. package/Server/API/StatusPageAPI.ts +14 -1
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -2
  10. package/Server/Services/AIRunService.ts +8 -0
  11. package/Server/Services/IncidentService.ts +62 -67
  12. package/Server/Services/MonitorGroupService.ts +15 -2
  13. package/Server/Services/MonitorService.ts +91 -3
  14. package/Server/Services/MonitorStatusTimelineService.ts +285 -100
  15. package/Server/Services/StatusPageService.ts +59 -34
  16. package/Server/Services/TelemetryExceptionService.ts +320 -24
  17. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
  18. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
  19. package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
  20. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
  21. package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
  22. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
  23. package/Server/Utils/Monitor/MonitorResource.ts +43 -14
  24. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +32 -7
  25. package/Server/Utils/Monitor/MonitorStatusTimelineReconciler.ts +362 -0
  26. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
  27. package/Tests/Server/Services/MonitorStatusTimelineService.test.ts +235 -0
  28. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
  29. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
  30. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
  31. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
  32. package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
  33. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineReconciler.test.ts +555 -0
  34. package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
  35. package/Tests/Types/Database/CompareOperatorWireSerialization.test.ts +216 -0
  36. package/Tests/Types/Database/GreaterThan.test.ts +5 -1
  37. package/Tests/Types/Database/GreaterThanOrEqual.test.ts +5 -1
  38. package/Tests/Types/Database/GreaterThanOrNull.test.ts +24 -1
  39. package/Tests/Types/Database/LessThan.test.ts +5 -1
  40. package/Tests/Types/Database/LessThanOrEqual.test.ts +24 -1
  41. package/Tests/Types/Database/LessThanOrNull.test.ts +5 -1
  42. package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
  43. package/Tests/Utils/Uptime/UptimeUtil.test.ts +726 -0
  44. package/Types/AI/CodeFixTaskContext.ts +6 -0
  45. package/Types/AI/CodeFixTaskType.ts +38 -1
  46. package/Types/AI/ExceptionAIClassification.ts +27 -0
  47. package/Types/BaseDatabase/GreaterThan.ts +8 -1
  48. package/Types/BaseDatabase/GreaterThanOrEqual.ts +8 -1
  49. package/Types/BaseDatabase/GreaterThanOrNull.ts +9 -1
  50. package/Types/BaseDatabase/LessThan.ts +8 -1
  51. package/Types/BaseDatabase/LessThanOrEqual.ts +9 -1
  52. package/Types/BaseDatabase/LessThanOrNull.ts +8 -1
  53. package/Types/Log/LogScrubPatternType.ts +7 -0
  54. package/Types/Trace/TraceScrubPatternType.ts +7 -0
  55. package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
  56. package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
  57. package/Utils/StatusPage/ResourceUptime.ts +11 -1
  58. package/Utils/Uptime/UptimeUtil.ts +142 -36
  59. package/build/dist/Models/AnalyticsModels/NetworkFlow.js +12 -1
  60. package/build/dist/Models/AnalyticsModels/NetworkFlow.js.map +1 -1
  61. package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
  62. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  63. package/build/dist/Models/DatabaseModels/Project.js +31 -0
  64. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  65. package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
  66. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  67. package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
  68. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  69. package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
  70. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  71. package/build/dist/Server/API/StatusPageAPI.js +12 -2
  72. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -2
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  77. package/build/dist/Server/Services/AIRunService.js +9 -2
  78. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  79. package/build/dist/Server/Services/IncidentService.js +47 -56
  80. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  81. package/build/dist/Server/Services/MonitorGroupService.js +15 -2
  82. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  83. package/build/dist/Server/Services/MonitorService.js +66 -3
  84. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  85. package/build/dist/Server/Services/MonitorStatusTimelineService.js +244 -102
  86. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  87. package/build/dist/Server/Services/StatusPageService.js +57 -33
  88. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  89. package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
  90. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  91. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
  92. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  93. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
  94. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  95. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
  96. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
  97. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
  98. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  99. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
  100. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
  101. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
  102. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
  103. package/build/dist/Server/Utils/Monitor/MonitorResource.js +34 -12
  104. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  105. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +25 -8
  106. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  107. package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js +268 -0
  108. package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js.map +1 -0
  109. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
  110. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
  111. package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
  112. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  113. package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
  114. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
  115. package/build/dist/Types/BaseDatabase/GreaterThan.js +8 -1
  116. package/build/dist/Types/BaseDatabase/GreaterThan.js.map +1 -1
  117. package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js +8 -1
  118. package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js.map +1 -1
  119. package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js +9 -1
  120. package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js.map +1 -1
  121. package/build/dist/Types/BaseDatabase/LessThan.js +8 -1
  122. package/build/dist/Types/BaseDatabase/LessThan.js.map +1 -1
  123. package/build/dist/Types/BaseDatabase/LessThanOrEqual.js +9 -1
  124. package/build/dist/Types/BaseDatabase/LessThanOrEqual.js.map +1 -1
  125. package/build/dist/Types/BaseDatabase/LessThanOrNull.js +8 -1
  126. package/build/dist/Types/BaseDatabase/LessThanOrNull.js.map +1 -1
  127. package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
  128. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
  129. package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
  130. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
  131. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
  132. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  133. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
  134. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
  135. package/build/dist/Utils/StatusPage/ResourceUptime.js +4 -1
  136. package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
  137. package/build/dist/Utils/Uptime/UptimeUtil.js +86 -33
  138. package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
  139. package/package.json +1 -1
  140. package/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.ts +0 -101
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js +0 -40
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js.map +0 -1
@@ -83,6 +83,12 @@ export interface CodeFixTaskContext {
83
83
  performanceFindings?: Array<PerformanceFinding> | undefined;
84
84
  // code.* attribute locations for stack-trace-style repo resolution.
85
85
  codeLocations?: Array<PerformanceCodeLocation> | undefined;
86
+ /*
87
+ * ImproveLogging / ImproveTracing: the telemetry service whose
88
+ * instrumentation the recipe improves (serviceName above carries its
89
+ * name for repository resolution and PR wording).
90
+ */
91
+ telemetryServiceId?: string | undefined;
86
92
  }
87
93
 
88
94
  export default CodeFixTaskContext;
@@ -12,6 +12,16 @@ enum CodeFixTaskType {
12
12
  FixException = "FixException",
13
13
  // Write a failing-then-passing regression test that reproduces the exception.
14
14
  WriteRegressionTest = "WriteRegressionTest",
15
+ /*
16
+ * Improve how the code HANDLES and REPORTS this exception without
17
+ * changing business behavior: parameterize messages that interpolate
18
+ * user data (PII out of error text, stable fingerprints), validate bad
19
+ * input earlier with an actionable error, and mark expected/operational
20
+ * errors as handled in telemetry. The recipe for exceptions the triage
21
+ * classified as user errors or expected denials — where "fix the bug"
22
+ * is the wrong instruction because there is no bug.
23
+ */
24
+ ImproveExceptionHandling = "ImproveExceptionHandling",
15
25
  /*
16
26
  * Add the observability an INCONCLUSIVE AI investigation was missing
17
27
  * (structured logs, spans, metric counters on the implicated code paths).
@@ -32,6 +42,27 @@ enum CodeFixTaskType {
32
42
  * Common/Server/Utils/AI/SRE/FixPerformanceTaskTrigger.ts).
33
43
  */
34
44
  FixPerformance = "FixPerformance",
45
+ /*
46
+ * Improve a SERVICE's logging hygiene: structured/parameterized log
47
+ * messages (no user data interpolated into log text), correct severity
48
+ * levels, trace correlation, proper exception recording instead of raw
49
+ * stack dumps in log bodies, and less noise. Human-triggered from the
50
+ * service's Logs page (POST /ai-investigation/
51
+ * create-telemetry-improvement-task); its context (service id + name) is
52
+ * captured into AIRun.taskContext at trigger time. Instrumentation only —
53
+ * never a behavior change.
54
+ */
55
+ ImproveLogging = "ImproveLogging",
56
+ /*
57
+ * Improve a SERVICE's tracing instrumentation: spans on uninstrumented
58
+ * entry points and significant operations, low-cardinality span names,
59
+ * exceptions recorded on spans with correct status/escaped semantics,
60
+ * code.* and semantic-convention attributes, context propagation across
61
+ * async boundaries. Human-triggered from the service's Traces page (same
62
+ * endpoint as ImproveLogging); context captured into AIRun.taskContext.
63
+ * Instrumentation only — never a behavior change.
64
+ */
65
+ ImproveTracing = "ImproveTracing",
35
66
  /*
36
67
  * Fix the root cause a completed AI investigation identified. Human-
37
68
  * triggered from the investigation panel (POST /ai-investigation/
@@ -77,7 +108,11 @@ export class CodeFixTaskTypeHelper {
77
108
  * (POST /ai-investigation/create-performance-fix-task).
78
109
  */
79
110
  public static getUserTriggerableTaskTypes(): Array<CodeFixTaskType> {
80
- return [CodeFixTaskType.FixException, CodeFixTaskType.WriteRegressionTest];
111
+ return [
112
+ CodeFixTaskType.FixException,
113
+ CodeFixTaskType.WriteRegressionTest,
114
+ CodeFixTaskType.ImproveExceptionHandling,
115
+ ];
81
116
  }
82
117
 
83
118
  public static isUserTriggerable(taskType: CodeFixTaskType): boolean {
@@ -97,6 +132,8 @@ export class CodeFixTaskTypeHelper {
97
132
  case CodeFixTaskType.FixFromIncident:
98
133
  return CodeFixContextKind.IncidentOrAlertSubject;
99
134
  case CodeFixTaskType.FixPerformance:
135
+ case CodeFixTaskType.ImproveLogging:
136
+ case CodeFixTaskType.ImproveTracing:
100
137
  return CodeFixContextKind.TaskContext;
101
138
  default:
102
139
  return CodeFixContextKind.TelemetryException;
@@ -0,0 +1,27 @@
1
+ /*
2
+ * AI triage verdict for a telemetry exception group. Decides how the
3
+ * automatic fix lane treats the group:
4
+ *
5
+ * - CodeFault: a defect in the monitored code — the only class the
6
+ * automatic lane opens fix pull requests for.
7
+ * - UserError: expected consequence of invalid end-user input (bad
8
+ * parameters, malformed values). The right change, if any, is earlier
9
+ * validation and clearer error UX — routed to a human, never auto-fixed.
10
+ * - ExpectedDenial: an intentional check doing its job (auth failure,
11
+ * plan/paywall denial, scanner/fuzzer probe tripping validation).
12
+ * Never auto-fixed; optionally auto-archived.
13
+ * - Infrastructure: environmental conditions (timeouts, connection
14
+ * resets, resource exhaustion) where a code "fix" is usually tuning —
15
+ * routed to a human.
16
+ * - Unknown: triage could not decide — treated conservatively (no
17
+ * automatic fix).
18
+ */
19
+ enum ExceptionAIClassification {
20
+ CodeFault = "code-fault",
21
+ UserError = "user-error",
22
+ ExpectedDenial = "expected-denial",
23
+ Infrastructure = "infrastructure",
24
+ Unknown = "unknown",
25
+ }
26
+
27
+ export default ExceptionAIClassification;
@@ -11,7 +11,14 @@ export default class GreaterThan<T extends CompareType> extends CompareBase<T> {
11
11
  public override toJSON(): JSONObject {
12
12
  return {
13
13
  _type: ObjectType.GreaterThan,
14
- value: (this as GreaterThan<T>).toString(),
14
+ /*
15
+ * The RAW value is serialized (like InBetween), not toString():
16
+ * toString() collapses a Date to a local-timezone date-only string
17
+ * (asDateForDatabaseQuery), shifting Date bounds sent from the browser
18
+ * by up to a day. JSON.stringify emits a raw Date as its full ISO
19
+ * timestamp, which the server binds at full precision.
20
+ */
21
+ value: (this as GreaterThan<T>).value,
15
22
  };
16
23
  }
17
24
 
@@ -13,7 +13,14 @@ export default class GreaterThanOrEqual<
13
13
  public override toJSON(): JSONObject {
14
14
  return {
15
15
  _type: ObjectType.GreaterThanOrEqual,
16
- value: (this as GreaterThanOrEqual<T>).toString(),
16
+ /*
17
+ * The RAW value is serialized (like InBetween), not toString():
18
+ * toString() collapses a Date to a local-timezone date-only string
19
+ * (asDateForDatabaseQuery), shifting Date bounds sent from the browser
20
+ * by up to a day. JSON.stringify emits a raw Date as its full ISO
21
+ * timestamp, which the server binds at full precision.
22
+ */
23
+ value: (this as GreaterThanOrEqual<T>).value,
17
24
  };
18
25
  }
19
26
 
@@ -10,10 +10,18 @@ export default class GreaterThanOrNull<
10
10
  super(value);
11
11
  }
12
12
 
13
+ /*
14
+ * Serializes the RAW value (like InBetween does), not toString(): toString()
15
+ * collapses a Date to a date-only string in the LOCAL timezone
16
+ * (asDateForDatabaseQuery), so a Date bound would arrive at the server as
17
+ * midnight of the browser's calendar date and shift the query bound by up to
18
+ * a day. JSON.stringify turns a raw Date into its full ISO timestamp, which
19
+ * the server binds at full precision.
20
+ */
13
21
  public override toJSON(): JSONObject {
14
22
  return {
15
23
  _type: ObjectType.GreaterThanOrNull,
16
- value: (this as GreaterThanOrNull<T>).toString(),
24
+ value: (this as GreaterThanOrNull<T>).value,
17
25
  };
18
26
  }
19
27
 
@@ -11,7 +11,14 @@ export default class LessThan<T extends CompareType> extends CompareBase<T> {
11
11
  public override toJSON(): JSONObject {
12
12
  return {
13
13
  _type: ObjectType.LessThan,
14
- value: (this as LessThan<T>).toString(),
14
+ /*
15
+ * The RAW value is serialized (like InBetween), not toString():
16
+ * toString() collapses a Date to a local-timezone date-only string
17
+ * (asDateForDatabaseQuery), shifting Date bounds sent from the browser
18
+ * by up to a day. JSON.stringify emits a raw Date as its full ISO
19
+ * timestamp, which the server binds at full precision.
20
+ */
21
+ value: (this as LessThan<T>).value,
15
22
  };
16
23
  }
17
24
 
@@ -10,10 +10,18 @@ export default class LessThanOrEqual<
10
10
  super(value);
11
11
  }
12
12
 
13
+ /*
14
+ * Serializes the RAW value (like InBetween does), not toString(): toString()
15
+ * collapses a Date to a date-only string in the LOCAL timezone
16
+ * (asDateForDatabaseQuery), so a bound of "now" would arrive at the server as
17
+ * midnight of the browser's calendar date and silently exclude every row from
18
+ * today. JSON.stringify turns a raw Date into its full ISO timestamp, which
19
+ * the server binds at full precision.
20
+ */
13
21
  public override toJSON(): JSONObject {
14
22
  return {
15
23
  _type: ObjectType.LessThanOrEqual,
16
- value: (this as LessThanOrEqual<T>).toString(),
24
+ value: (this as LessThanOrEqual<T>).value,
17
25
  };
18
26
  }
19
27
 
@@ -13,7 +13,14 @@ export default class LessThanOrNull<
13
13
  public override toJSON(): JSONObject {
14
14
  return {
15
15
  _type: ObjectType.LessThanOrNull,
16
- value: (this as LessThanOrNull<T>).toString(),
16
+ /*
17
+ * The RAW value is serialized (like InBetween), not toString():
18
+ * toString() collapses a Date to a local-timezone date-only string
19
+ * (asDateForDatabaseQuery), shifting Date bounds sent from the browser
20
+ * by up to a day. JSON.stringify emits a raw Date as its full ISO
21
+ * timestamp, which the server binds at full precision.
22
+ */
23
+ value: (this as LessThanOrNull<T>).value,
17
24
  };
18
25
  }
19
26
 
@@ -4,6 +4,13 @@ enum LogScrubPatternType {
4
4
  SSN = "ssn",
5
5
  PhoneNumber = "phoneNumber",
6
6
  IPAddress = "ipAddress",
7
+ /*
8
+ * Key-targeted: scrubs the WHOLE value of any attribute whose KEY looks
9
+ * sensitive (password, token, apiKey, authorization, cookie, ...) —
10
+ * catches secrets whose values match no shape-based pattern. Applies to
11
+ * attributes only; body text has no keys to match.
12
+ */
13
+ SensitiveKeys = "sensitiveKeys",
7
14
  Custom = "custom",
8
15
  }
9
16
 
@@ -4,6 +4,13 @@ enum TraceScrubPatternType {
4
4
  SSN = "ssn",
5
5
  PhoneNumber = "phoneNumber",
6
6
  IPAddress = "ipAddress",
7
+ /*
8
+ * Key-targeted: scrubs the WHOLE value of any attribute whose KEY looks
9
+ * sensitive (password, token, apiKey, authorization, cookie, ...) —
10
+ * catches secrets whose values match no shape-based pattern. Applies to
11
+ * span/event attributes only; span names have no keys to match.
12
+ */
13
+ SensitiveKeys = "sensitiveKeys",
7
14
  Custom = "custom",
8
15
  }
9
16
 
@@ -53,6 +53,10 @@ import TableColumn from "../Table/Types/Column";
53
53
  import FieldType from "../Types/FieldType";
54
54
  import ModelTableColumn from "./Column";
55
55
  import Columns from "./Columns";
56
+ import {
57
+ getRelationSelectFromColumns,
58
+ getSelectFromColumns,
59
+ } from "./SelectFromColumns";
56
60
  import AnalyticsBaseModel, {
57
61
  AnalyticsBaseModelType,
58
62
  } from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
@@ -664,33 +668,10 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
664
668
 
665
669
  const getRelationSelect: GetRelationSelectFunction =
666
670
  (): Select<TBaseModel> => {
667
- const relationSelect: Select<TBaseModel> = {};
668
-
669
- for (const column of props.columns || []) {
670
- const key: string | null = column.field
671
- ? (Object.keys(column.field)[0] as string)
672
- : null;
673
-
674
- if (key && model.isFileColumn(key)) {
675
- (relationSelect as JSONObject)[key] = {
676
- file: true,
677
- _id: true,
678
- fileType: true,
679
- name: true,
680
- };
681
- } else if (key && model.isEntityColumn(key)) {
682
- if (!(relationSelect as JSONObject)[key]) {
683
- (relationSelect as JSONObject)[key] = {};
684
- }
685
-
686
- (relationSelect as JSONObject)[key] = {
687
- ...((relationSelect as JSONObject)[key] as JSONObject),
688
- ...(column.field as any)[key],
689
- };
690
- }
691
- }
692
-
693
- return relationSelect;
671
+ return getRelationSelectFromColumns<TBaseModel>({
672
+ columns: props.columns || [],
673
+ model: model,
674
+ });
694
675
  };
695
676
 
696
677
  type DeleteItemFunction = (item: TBaseModel) => Promise<void>;
@@ -1226,25 +1207,13 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
1226
1207
  type GetSelectFunction = () => Select<TBaseModel>;
1227
1208
 
1228
1209
  const getSelect: GetSelectFunction = (): Select<TBaseModel> => {
1229
- const selectFields: Select<TBaseModel> = {
1230
- _id: true,
1231
- };
1232
-
1233
- for (const column of props.columns || []) {
1234
- const key: string | null = column.field
1235
- ? (Object.keys(column.field)[0] as string)
1236
- : null;
1237
-
1238
- if (key) {
1239
- if (model.hasColumn(key)) {
1240
- (selectFields as Dictionary<boolean>)[key] = true;
1241
- } else {
1242
- throw new BadDataException(
1243
- `${key} column not found on ${model.singularName}`,
1244
- );
1245
- }
1246
- }
1247
- }
1210
+ const selectFields: Select<TBaseModel> = getSelectFromColumns<TBaseModel>({
1211
+ columns: props.columns || [],
1212
+ model: model,
1213
+ hasPermissionToReadField: (field: string): boolean => {
1214
+ return hasPermissionToReadField(field as keyof TBaseModel);
1215
+ },
1216
+ });
1248
1217
 
1249
1218
  const selectMoreFields: Array<keyof TBaseModel> = props.selectMoreFields
1250
1219
  ? (Object.keys(props.selectMoreFields) as Array<keyof TBaseModel>)
@@ -0,0 +1,129 @@
1
+ import Columns from "./Columns";
2
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
3
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
4
+ import Select from "../../../Types/BaseDatabase/Select";
5
+ import Dictionary from "../../../Types/Dictionary";
6
+ import BadDataException from "../../../Types/Exception/BadDataException";
7
+ import { JSONObject } from "../../../Types/JSON";
8
+ import { Logger } from "../../Utils/Logger";
9
+
10
+ /*
11
+ * Builds the API `select` for a ModelTable from its column definitions.
12
+ *
13
+ * A column may declare MORE THAN ONE field when its cell renders a value
14
+ * composed from several columns — e.g. a "Nodes" cell that renders
15
+ * `{onlineNodeCount}/{nodeCount} online` declares
16
+ * `field: { nodeCount: true, onlineNodeCount: true }`.
17
+ *
18
+ * These used to select only the FIRST key, which left every other declared
19
+ * field `undefined` on the fetched row, so such cells silently rendered
20
+ * their zero/empty fallback — a fully online Proxmox cluster displayed
21
+ * "0/3 online" (OneUptime/oneuptime#2756). ModelDetail has always selected
22
+ * every declared key; these helpers keep ModelTable in sync with it.
23
+ */
24
+
25
+ export function getSelectFromColumns<
26
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
27
+ >(data: {
28
+ columns: Columns<TBaseModel>;
29
+ model: TBaseModel;
30
+ /*
31
+ * Secondary fields are gated on this. The API rejects the WHOLE request
32
+ * with NotAuthorizedException when the select names a column the caller
33
+ * cannot read (Common/Server/Types/Database/Permissions/SelectPermission
34
+ * .checkSelectPermission), so a secondary field the user has no read
35
+ * permission on must be dropped rather than blank the table for them.
36
+ * Primary fields are left exactly as they always were.
37
+ */
38
+ hasPermissionToReadField?: ((field: string) => boolean) | undefined;
39
+ }): Select<TBaseModel> {
40
+ const selectFields: Select<TBaseModel> = {
41
+ _id: true,
42
+ };
43
+
44
+ for (const column of data.columns) {
45
+ if (!column.field) {
46
+ continue;
47
+ }
48
+
49
+ const keys: Array<string> = Object.keys(column.field);
50
+
51
+ for (const key of keys) {
52
+ const isPrimaryField: boolean = key === keys[0];
53
+
54
+ if (!data.model.hasColumn(key)) {
55
+ if (isPrimaryField) {
56
+ /*
57
+ * Only the primary field is a hard error — it is also the key
58
+ * used for sorting and for the default cell renderer, so a bad
59
+ * one is a genuine mistake.
60
+ */
61
+ throw new BadDataException(
62
+ `${key} column not found on ${data.model.singularName}`,
63
+ );
64
+ }
65
+
66
+ /*
67
+ * A stray secondary key degrades to "not selected" rather than
68
+ * throwing, because throwing here blanks the entire page. Log it:
69
+ * silently dropping it is what produced #2756 in the first place.
70
+ */
71
+ Logger.error(
72
+ `ModelTable column "${column.title}" on ${data.model.singularName} declares field "${key}", which is not a column on the model. It will not be fetched, so the cell will render its empty value.`,
73
+ );
74
+ continue;
75
+ }
76
+
77
+ if (
78
+ !isPrimaryField &&
79
+ data.hasPermissionToReadField &&
80
+ !data.hasPermissionToReadField(key)
81
+ ) {
82
+ continue;
83
+ }
84
+
85
+ (selectFields as Dictionary<boolean>)[key] = true;
86
+ }
87
+ }
88
+
89
+ return selectFields;
90
+ }
91
+
92
+ export function getRelationSelectFromColumns<
93
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
94
+ >(data: {
95
+ columns: Columns<TBaseModel>;
96
+ model: TBaseModel;
97
+ }): Select<TBaseModel> {
98
+ const relationSelect: Select<TBaseModel> = {};
99
+
100
+ for (const column of data.columns) {
101
+ if (!column.field) {
102
+ continue;
103
+ }
104
+
105
+ /*
106
+ * Same multi-field contract as getSelectFromColumns: one cell can
107
+ * render several relations (e.g. an alert "Resource" cell spanning
108
+ * hosts / kubernetesClusters / dockerHosts / podmanHosts / services).
109
+ */
110
+ for (const key of Object.keys(column.field)) {
111
+ if (data.model.isFileColumn(key)) {
112
+ (relationSelect as JSONObject)[key] = {
113
+ file: true,
114
+ _id: true,
115
+ fileType: true,
116
+ name: true,
117
+ };
118
+ } else if (data.model.isEntityColumn(key)) {
119
+ (relationSelect as JSONObject)[key] = {
120
+ ...(((relationSelect as JSONObject)[key] as JSONObject) || {}),
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ ...((column.field as any)[key] as JSONObject),
123
+ };
124
+ }
125
+ }
126
+ }
127
+
128
+ return relationSelect;
129
+ }
@@ -6,7 +6,7 @@ import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
6
6
  import Dictionary from "../../Types/Dictionary";
7
7
  import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
8
8
  import StatusPageGroup from "../../Models/DatabaseModels/StatusPageGroup";
9
- import UptimeUtil from "../Uptime/UptimeUtil";
9
+ import UptimeUtil, { UptimeWindow } from "../Uptime/UptimeUtil";
10
10
 
11
11
  export default class StatusPageResourceUptimeUtil {
12
12
  public static getWorstMonitorStatus(data: {
@@ -137,6 +137,8 @@ export default class StatusPageResourceUptimeUtil {
137
137
  precision: UptimePrecision;
138
138
  downtimeMonitorStatuses: Array<MonitorStatus>;
139
139
  monitorsInGroup: Dictionary<Array<ObjectID>>;
140
+ // if supplied, uptime is measured over this window instead of "first event -> now".
141
+ uptimeWindow?: UptimeWindow | undefined;
140
142
  }): number | null {
141
143
  if (!data.statusPageResource.showUptimePercent) {
142
144
  return null;
@@ -156,6 +158,7 @@ export default class StatusPageResourceUptimeUtil {
156
158
  monitorStatusTimelines,
157
159
  data.precision,
158
160
  downtimeMonitorStatuses,
161
+ data.uptimeWindow,
159
162
  );
160
163
 
161
164
  return uptimePercent;
@@ -168,6 +171,8 @@ export default class StatusPageResourceUptimeUtil {
168
171
  downtimeMonitorStatuses: Array<MonitorStatus>;
169
172
  statusPageResources: Array<StatusPageResource>;
170
173
  monitorsInGroup: Dictionary<Array<ObjectID>>;
174
+ // if supplied, uptime is measured over this window instead of "first event -> now".
175
+ uptimeWindow?: UptimeWindow | undefined;
171
176
  }): number | null {
172
177
  if (!data.statusPageGroup.showUptimePercent) {
173
178
  return null;
@@ -193,6 +198,7 @@ export default class StatusPageResourceUptimeUtil {
193
198
  precision: data.precision,
194
199
  downtimeMonitorStatuses: data.downtimeMonitorStatuses,
195
200
  monitorsInGroup: data.monitorsInGroup,
201
+ uptimeWindow: data.uptimeWindow,
196
202
  });
197
203
 
198
204
  if (calculateUptimePercentOfResource !== null) {
@@ -252,6 +258,8 @@ export default class StatusPageResourceUptimeUtil {
252
258
  statusPageResources: Array<StatusPageResource>;
253
259
  resourceGroups: Array<StatusPageGroup>;
254
260
  monitorsInGroup: Dictionary<Array<ObjectID>>;
261
+ // if supplied, uptime is measured over this window instead of "first event -> now".
262
+ uptimeWindow?: UptimeWindow | undefined;
255
263
  }): number | null {
256
264
  const showUptimePercentage: boolean = Boolean(
257
265
  data.statusPageResources.find((item: StatusPageResource) => {
@@ -276,6 +284,7 @@ export default class StatusPageResourceUptimeUtil {
276
284
  downtimeMonitorStatuses: data.downtimeMonitorStatuses,
277
285
  statusPageResources: data.statusPageResources,
278
286
  monitorsInGroup: data.monitorsInGroup,
287
+ uptimeWindow: data.uptimeWindow,
279
288
  });
280
289
 
281
290
  if (calculateAvgUptimePercentOfStatusPageGroup !== null) {
@@ -298,6 +307,7 @@ export default class StatusPageResourceUptimeUtil {
298
307
  precision: data.precision,
299
308
  downtimeMonitorStatuses: data.downtimeMonitorStatuses,
300
309
  monitorsInGroup: data.monitorsInGroup,
310
+ uptimeWindow: data.uptimeWindow,
301
311
  });
302
312
 
303
313
  if (calculateUptimePercentOfResource !== null) {