@oneuptime/common 11.3.20 → 11.3.22
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/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.ts +2 -0
- package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
- package/Models/AnalyticsModels/Log.ts +1 -0
- package/Models/AnalyticsModels/Metric.ts +1 -0
- package/Models/AnalyticsModels/MonitorLog.ts +1 -0
- package/Models/AnalyticsModels/Span.ts +1 -0
- package/Models/DatabaseModels/Alert.ts +100 -0
- package/Models/DatabaseModels/AlertInternalNote.ts +2 -0
- package/Models/DatabaseModels/AlertReminderRule.ts +597 -0
- package/Models/DatabaseModels/AlertSeverity.ts +2 -0
- package/Models/DatabaseModels/AlertState.ts +2 -0
- package/Models/DatabaseModels/AlertStateTimeline.ts +2 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +173 -0
- package/Models/DatabaseModels/GlobalConfig.ts +19 -0
- package/Models/DatabaseModels/Incident.ts +100 -0
- package/Models/DatabaseModels/IncidentInternalNote.ts +2 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +2 -0
- package/Models/DatabaseModels/IncidentReminderRule.ts +597 -0
- package/Models/DatabaseModels/IncidentSeverity.ts +2 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +2 -0
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/Label.ts +2 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +2 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +100 -0
- package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +585 -0
- package/Models/DatabaseModels/ScheduledMaintenanceState.ts +2 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +2 -0
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +2 -0
- package/Server/API/DashboardAPI.ts +221 -0
- package/Server/API/EnterpriseLicenseAPI.ts +279 -12
- package/Server/API/GlobalConfigAPI.ts +81 -16
- package/Server/API/StatusPageAPI.ts +894 -791
- package/Server/EnvironmentConfig.ts +9 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.ts +211 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.ts +113 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.ts +20 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.ts +69 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Services/AlertReminderRuleService.ts +253 -0
- package/Server/Services/AlertService.ts +106 -0
- package/Server/Services/EnterpriseLicenseInstanceService.ts +10 -0
- package/Server/Services/IncidentReminderRuleService.ts +256 -0
- package/Server/Services/IncidentService.ts +143 -0
- package/Server/Services/Index.ts +8 -0
- package/Server/Services/IoTDeviceService.ts +99 -4
- package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +236 -0
- package/Server/Services/ScheduledMaintenanceService.ts +127 -0
- package/Server/Services/StatusPageService.ts +104 -45
- package/Server/Services/TelemetryExceptionService.ts +123 -0
- package/Server/Services/UserNotificationSettingService.ts +24 -0
- package/Server/Utils/AnalyticsDatabase/Statement.ts +7 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +35 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +6 -4
- package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +7 -6
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +6 -4
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +45 -7
- package/Server/Utils/WhatsAppTemplateUtil.ts +10 -0
- package/ServiceRoute.ts +4 -0
- package/Tests/Server/Services/IoTDeviceService.test.ts +280 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +49 -0
- package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +390 -0
- package/Tests/Server/Utils/Telemetry/IoTSnapshotScan.test.ts +202 -0
- package/Tests/Utils/EnterpriseLicenseUsage.test.ts +165 -0
- package/Types/Email/EmailTemplateType.ts +3 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +18 -0
- package/Types/Monitor/MonitorStep.ts +32 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +3 -0
- package/Types/Permission.ts +144 -0
- package/Types/Reminder/ReminderStopState.ts +6 -0
- package/Types/Reminder/ScheduledMaintenanceReminderStopState.ts +6 -0
- package/Types/WhatsApp/WhatsAppTemplates.ts +14 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +172 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +125 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +1 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
- package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +1 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Alert.js +103 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertInternalNote.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertReminderRule.js +605 -0
- package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertSeverity.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +197 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +18 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +103 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +605 -0
- package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +8 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Label.js +2 -0
- package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +103 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +596 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +166 -16
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +203 -13
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +62 -15
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +74 -31
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js +44 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js +228 -0
- package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/AlertService.js +87 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +9 -0
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -0
- package/build/dist/Server/Services/IncidentReminderRuleService.js +231 -0
- package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +116 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +8 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +66 -4
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +214 -0
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +91 -43
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +101 -0
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +6 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -5
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +33 -2
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +6 -0
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
- package/build/dist/ServiceRoute.js +1 -0
- package/build/dist/ServiceRoute.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +3 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js +2 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStep.js +25 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/Permission.js +126 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Reminder/ReminderStopState.js +7 -0
- package/build/dist/Types/Reminder/ReminderStopState.js.map +1 -0
- package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js +7 -0
- package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js.map +1 -0
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +9 -0
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +92 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +83 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import MetricMonitorCriteria from "../../../../../Server/Utils/Monitor/Criteria/MetricMonitorCriteria";
|
|
2
|
+
import MonitorCriteriaDataExtractor from "../../../../../Server/Utils/Monitor/MonitorCriteriaDataExtractor";
|
|
3
|
+
import AggregateModel from "../../../../../Types/BaseDatabase/AggregatedModel";
|
|
4
|
+
import AggregatedResult from "../../../../../Types/BaseDatabase/AggregatedResult";
|
|
5
|
+
import {
|
|
6
|
+
CheckOn,
|
|
7
|
+
CriteriaFilter,
|
|
8
|
+
EvaluateOverTimeType,
|
|
9
|
+
FilterType,
|
|
10
|
+
} from "../../../../../Types/Monitor/CriteriaFilter";
|
|
11
|
+
import MetricFormulaConfigData from "../../../../../Types/Metrics/MetricFormulaConfigData";
|
|
12
|
+
import MetricQueryConfigData from "../../../../../Types/Metrics/MetricQueryConfigData";
|
|
13
|
+
import MetricQueryData from "../../../../../Types/Metrics/MetricQueryData";
|
|
14
|
+
import MetricsViewConfig from "../../../../../Types/Metrics/MetricsViewConfig";
|
|
15
|
+
import MetricMonitorResponse from "../../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
|
16
|
+
import MonitorStep, {
|
|
17
|
+
MonitorStepType,
|
|
18
|
+
} from "../../../../../Types/Monitor/MonitorStep";
|
|
19
|
+
import { MonitorStepCephMonitorUtil } from "../../../../../Types/Monitor/MonitorStepCephMonitor";
|
|
20
|
+
import { MonitorStepDockerMonitorUtil } from "../../../../../Types/Monitor/MonitorStepDockerMonitor";
|
|
21
|
+
import { MonitorStepDockerSwarmMonitorUtil } from "../../../../../Types/Monitor/MonitorStepDockerSwarmMonitor";
|
|
22
|
+
import { MonitorStepHostMonitorUtil } from "../../../../../Types/Monitor/MonitorStepHostMonitor";
|
|
23
|
+
import { MonitorStepKubernetesMonitorUtil } from "../../../../../Types/Monitor/MonitorStepKubernetesMonitor";
|
|
24
|
+
import { MonitorStepPodmanMonitorUtil } from "../../../../../Types/Monitor/MonitorStepPodmanMonitor";
|
|
25
|
+
import { MonitorStepProxmoxMonitorUtil } from "../../../../../Types/Monitor/MonitorStepProxmoxMonitor";
|
|
26
|
+
import RollingTime from "../../../../../Types/RollingTime/RollingTime";
|
|
27
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* Criteria evaluation resolves `metricAlias` → result slot via the query
|
|
31
|
+
* configs on the monitor step. Telemetry monitors other than the plain
|
|
32
|
+
* Metric monitor (IoT, Kubernetes, Docker, ...) store their query configs
|
|
33
|
+
* under their own step shape (e.g. `data.iotMonitor.metricViewConfig`), so
|
|
34
|
+
* alias resolution must read from whichever shape the step carries — not
|
|
35
|
+
* just `data.metricMonitor`. These tests pin that behavior: a criteria
|
|
36
|
+
* referencing the second query's alias must evaluate the second result
|
|
37
|
+
* slot, never silently fall back to slot 0.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
function buildQueryConfig(input: {
|
|
41
|
+
alias: string;
|
|
42
|
+
metricName: string;
|
|
43
|
+
}): MetricQueryConfigData {
|
|
44
|
+
return {
|
|
45
|
+
metricAliasData: {
|
|
46
|
+
metricVariable: input.alias,
|
|
47
|
+
title: input.metricName,
|
|
48
|
+
description: undefined,
|
|
49
|
+
legend: undefined,
|
|
50
|
+
legendUnit: undefined,
|
|
51
|
+
},
|
|
52
|
+
metricQueryData: {
|
|
53
|
+
filterData: {
|
|
54
|
+
metricName: input.metricName,
|
|
55
|
+
},
|
|
56
|
+
} as unknown as MetricQueryData,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Two queries — "a" (iot_battery_percent) and "b"
|
|
62
|
+
* (iot_signal_strength_dbm) — plus an optional formula "f" = a + b.
|
|
63
|
+
* Result slots: 0 → a, 1 → b, 2 → f.
|
|
64
|
+
*/
|
|
65
|
+
function buildViewConfig(input?: { withFormula?: boolean }): MetricsViewConfig {
|
|
66
|
+
const formulaConfigs: Array<MetricFormulaConfigData> = input?.withFormula
|
|
67
|
+
? [
|
|
68
|
+
{
|
|
69
|
+
metricAliasData: {
|
|
70
|
+
metricVariable: "f",
|
|
71
|
+
title: "Battery + Signal",
|
|
72
|
+
description: undefined,
|
|
73
|
+
legend: undefined,
|
|
74
|
+
legendUnit: undefined,
|
|
75
|
+
},
|
|
76
|
+
metricFormulaData: {
|
|
77
|
+
metricFormula: "a + b",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
: [];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
queryConfigs: [
|
|
85
|
+
buildQueryConfig({ alias: "a", metricName: "iot_battery_percent" }),
|
|
86
|
+
buildQueryConfig({ alias: "b", metricName: "iot_signal_strength_dbm" }),
|
|
87
|
+
],
|
|
88
|
+
formulaConfigs,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Bare step whose telemetry shape is attached by `assignShape` — the
|
|
94
|
+
* point of these tests is that `data.metricMonitor` stays undefined.
|
|
95
|
+
*/
|
|
96
|
+
function buildStep(
|
|
97
|
+
assignShape: (data: MonitorStepType, viewConfig: MetricsViewConfig) => void,
|
|
98
|
+
viewConfig: MetricsViewConfig,
|
|
99
|
+
): MonitorStep {
|
|
100
|
+
const monitorStep: MonitorStep = new MonitorStep();
|
|
101
|
+
monitorStep.data = {
|
|
102
|
+
id: ObjectID.generate().toString(),
|
|
103
|
+
monitorCriteria: { data: undefined } as never,
|
|
104
|
+
} as unknown as MonitorStep["data"];
|
|
105
|
+
assignShape(monitorStep.data!, viewConfig);
|
|
106
|
+
return monitorStep;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildIoTStep(viewConfig: MetricsViewConfig): MonitorStep {
|
|
110
|
+
return buildStep((data: MonitorStepType, v: MetricsViewConfig) => {
|
|
111
|
+
data.iotMonitor = {
|
|
112
|
+
fleetIdentifier: "plant-a-fleet",
|
|
113
|
+
resourceFilters: {},
|
|
114
|
+
metricViewConfig: v,
|
|
115
|
+
rollingTime: RollingTime.Past1Minute,
|
|
116
|
+
};
|
|
117
|
+
}, viewConfig);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* One aggregated result per result slot, in query/formula order.
|
|
122
|
+
*/
|
|
123
|
+
function buildResponse(input: {
|
|
124
|
+
viewConfig: MetricsViewConfig;
|
|
125
|
+
resultSlots: Array<Array<number>>;
|
|
126
|
+
}): MetricMonitorResponse {
|
|
127
|
+
const metricResult: Array<AggregatedResult> = input.resultSlots.map(
|
|
128
|
+
(values: Array<number>) => {
|
|
129
|
+
return {
|
|
130
|
+
data: values.map((v: number) => {
|
|
131
|
+
return {
|
|
132
|
+
timestamp: new Date(),
|
|
133
|
+
value: v,
|
|
134
|
+
} as AggregateModel;
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
projectId: ObjectID.generate(),
|
|
142
|
+
metricResult,
|
|
143
|
+
metricViewConfig: input.viewConfig,
|
|
144
|
+
monitorId: ObjectID.generate(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function greaterThanFilter(input: {
|
|
149
|
+
alias: string;
|
|
150
|
+
threshold: string;
|
|
151
|
+
}): CriteriaFilter {
|
|
152
|
+
return {
|
|
153
|
+
checkOn: CheckOn.MetricValue,
|
|
154
|
+
filterType: FilterType.GreaterThan,
|
|
155
|
+
value: input.threshold,
|
|
156
|
+
metricMonitorOptions: {
|
|
157
|
+
metricAlias: input.alias,
|
|
158
|
+
metricAggregationType: EvaluateOverTimeType.AnyValue,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
describe("MetricMonitorCriteria — IoT monitor step alias resolution", () => {
|
|
164
|
+
test("criteria on the second alias evaluates the second series", async () => {
|
|
165
|
+
const viewConfig: MetricsViewConfig = buildViewConfig();
|
|
166
|
+
const criteriaFilter: CriteriaFilter = greaterThanFilter({
|
|
167
|
+
alias: "b",
|
|
168
|
+
threshold: "30",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Slot 0 (alias a) stays under the threshold; slot 1 (alias b) breaches.
|
|
172
|
+
const message: string | null =
|
|
173
|
+
await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
174
|
+
dataToProcess: buildResponse({
|
|
175
|
+
viewConfig,
|
|
176
|
+
resultSlots: [[10, 12], [55]],
|
|
177
|
+
}),
|
|
178
|
+
criteriaFilter,
|
|
179
|
+
monitorStep: buildIoTStep(viewConfig),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(message).toBeTruthy();
|
|
183
|
+
expect(message).toContain("55");
|
|
184
|
+
expect(criteriaFilter.metricCriteriaContext?.metricName).toBe(
|
|
185
|
+
"iot_signal_strength_dbm",
|
|
186
|
+
);
|
|
187
|
+
expect(criteriaFilter.metricCriteriaContext?.breachingSample?.value).toBe(
|
|
188
|
+
55,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("criteria on the second alias ignores a breach in the first series", async () => {
|
|
193
|
+
const viewConfig: MetricsViewConfig = buildViewConfig();
|
|
194
|
+
|
|
195
|
+
// Slot 0 (alias a) breaches, but the criteria targets alias b.
|
|
196
|
+
const message: string | null =
|
|
197
|
+
await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
198
|
+
dataToProcess: buildResponse({
|
|
199
|
+
viewConfig,
|
|
200
|
+
resultSlots: [[90], [10]],
|
|
201
|
+
}),
|
|
202
|
+
criteriaFilter: greaterThanFilter({ alias: "b", threshold: "30" }),
|
|
203
|
+
monitorStep: buildIoTStep(viewConfig),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(message).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("formula alias resolves to the formula result slot", async () => {
|
|
210
|
+
const viewConfig: MetricsViewConfig = buildViewConfig({
|
|
211
|
+
withFormula: true,
|
|
212
|
+
});
|
|
213
|
+
const criteriaFilter: CriteriaFilter = greaterThanFilter({
|
|
214
|
+
alias: "f",
|
|
215
|
+
threshold: "100",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Slots: a → 10, b → 20, f (formula) → 150. Only the formula breaches.
|
|
219
|
+
const message: string | null =
|
|
220
|
+
await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
221
|
+
dataToProcess: buildResponse({
|
|
222
|
+
viewConfig,
|
|
223
|
+
resultSlots: [[10], [20], [150]],
|
|
224
|
+
}),
|
|
225
|
+
criteriaFilter,
|
|
226
|
+
monitorStep: buildIoTStep(viewConfig),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(message).toBeTruthy();
|
|
230
|
+
expect(message).toContain("150");
|
|
231
|
+
expect(criteriaFilter.metricCriteriaContext?.isFormula).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("MonitorCriteriaDataExtractor — IoT monitor step alias resolution", () => {
|
|
236
|
+
test("extractMetricValues returns the aliased series, not slot 0", () => {
|
|
237
|
+
const viewConfig: MetricsViewConfig = buildViewConfig();
|
|
238
|
+
|
|
239
|
+
const extracted: { alias: string | null; values: Array<number> } | null =
|
|
240
|
+
MonitorCriteriaDataExtractor.extractMetricValues({
|
|
241
|
+
criteriaFilter: greaterThanFilter({ alias: "b", threshold: "30" }),
|
|
242
|
+
dataToProcess: buildResponse({
|
|
243
|
+
viewConfig,
|
|
244
|
+
resultSlots: [
|
|
245
|
+
[10, 12],
|
|
246
|
+
[55, 60],
|
|
247
|
+
],
|
|
248
|
+
}),
|
|
249
|
+
monitorStep: buildIoTStep(viewConfig),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(extracted).not.toBeNull();
|
|
253
|
+
expect(extracted?.alias).toBe("b");
|
|
254
|
+
expect(extracted?.values).toEqual([55, 60]);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("MetricMonitorCriteria — other telemetry step shapes", () => {
|
|
259
|
+
type ShapeCase = {
|
|
260
|
+
name: string;
|
|
261
|
+
assignShape: (data: MonitorStepType, viewConfig: MetricsViewConfig) => void;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const shapeCases: Array<ShapeCase> = [
|
|
265
|
+
{
|
|
266
|
+
name: "kubernetesMonitor",
|
|
267
|
+
assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
|
|
268
|
+
data.kubernetesMonitor = {
|
|
269
|
+
...MonitorStepKubernetesMonitorUtil.getDefault(),
|
|
270
|
+
metricViewConfig: v,
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "dockerMonitor",
|
|
276
|
+
assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
|
|
277
|
+
data.dockerMonitor = {
|
|
278
|
+
...MonitorStepDockerMonitorUtil.getDefault(),
|
|
279
|
+
metricViewConfig: v,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "dockerSwarmMonitor",
|
|
285
|
+
assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
|
|
286
|
+
data.dockerSwarmMonitor = {
|
|
287
|
+
...MonitorStepDockerSwarmMonitorUtil.getDefault(),
|
|
288
|
+
metricViewConfig: v,
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "hostMonitor",
|
|
294
|
+
assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
|
|
295
|
+
data.hostMonitor = {
|
|
296
|
+
...MonitorStepHostMonitorUtil.getDefault(),
|
|
297
|
+
metricViewConfig: v,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "podmanMonitor",
|
|
303
|
+
assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
|
|
304
|
+
data.podmanMonitor = {
|
|
305
|
+
...MonitorStepPodmanMonitorUtil.getDefault(),
|
|
306
|
+
metricViewConfig: v,
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "proxmoxMonitor",
|
|
312
|
+
assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
|
|
313
|
+
data.proxmoxMonitor = {
|
|
314
|
+
...MonitorStepProxmoxMonitorUtil.getDefault(),
|
|
315
|
+
metricViewConfig: v,
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "cephMonitor",
|
|
321
|
+
assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
|
|
322
|
+
data.cephMonitor = {
|
|
323
|
+
...MonitorStepCephMonitorUtil.getDefault(),
|
|
324
|
+
metricViewConfig: v,
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
test.each(shapeCases)(
|
|
331
|
+
"$name: criteria on the second alias evaluates the second series",
|
|
332
|
+
async (shapeCase: ShapeCase) => {
|
|
333
|
+
const viewConfig: MetricsViewConfig = buildViewConfig();
|
|
334
|
+
const criteriaFilter: CriteriaFilter = greaterThanFilter({
|
|
335
|
+
alias: "b",
|
|
336
|
+
threshold: "30",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const message: string | null =
|
|
340
|
+
await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
341
|
+
dataToProcess: buildResponse({
|
|
342
|
+
viewConfig,
|
|
343
|
+
resultSlots: [[10, 12], [55]],
|
|
344
|
+
}),
|
|
345
|
+
criteriaFilter,
|
|
346
|
+
monitorStep: buildStep(shapeCase.assignShape, viewConfig),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(message).toBeTruthy();
|
|
350
|
+
expect(message).toContain("55");
|
|
351
|
+
expect(criteriaFilter.metricCriteriaContext?.metricName).toBe(
|
|
352
|
+
"iot_signal_strength_dbm",
|
|
353
|
+
);
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("MetricMonitorCriteria — metric monitor step (regression)", () => {
|
|
359
|
+
test("alias resolution still reads data.metricMonitor", async () => {
|
|
360
|
+
const viewConfig: MetricsViewConfig = buildViewConfig();
|
|
361
|
+
const criteriaFilter: CriteriaFilter = greaterThanFilter({
|
|
362
|
+
alias: "b",
|
|
363
|
+
threshold: "30",
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const message: string | null =
|
|
367
|
+
await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
368
|
+
dataToProcess: buildResponse({
|
|
369
|
+
viewConfig,
|
|
370
|
+
resultSlots: [[10, 12], [55]],
|
|
371
|
+
}),
|
|
372
|
+
criteriaFilter,
|
|
373
|
+
monitorStep: buildStep(
|
|
374
|
+
(data: MonitorStepType, v: MetricsViewConfig) => {
|
|
375
|
+
data.metricMonitor = {
|
|
376
|
+
metricViewConfig: v,
|
|
377
|
+
rollingTime: RollingTime.Past1Minute,
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
viewConfig,
|
|
381
|
+
),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(message).toBeTruthy();
|
|
385
|
+
expect(message).toContain("55");
|
|
386
|
+
expect(criteriaFilter.metricCriteriaContext?.metricName).toBe(
|
|
387
|
+
"iot_signal_strength_dbm",
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IoTDeviceBufferEntry,
|
|
3
|
+
IoTFleetSnapshotBufferEntry,
|
|
4
|
+
IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES,
|
|
5
|
+
bufferIoTSnapshotMetric,
|
|
6
|
+
clampIoTTimestamp,
|
|
7
|
+
} from "../../../../Server/Utils/Telemetry/IoTSnapshotScan";
|
|
8
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Malformed-device hardening for the IoT snapshot scan:
|
|
12
|
+
*
|
|
13
|
+
* - a `device.id` longer than the ShortText column limit (100) is
|
|
14
|
+
* truncated at ingest, so one bad device can never fail the
|
|
15
|
+
* 500-row inventory INSERT chunk downstream — and two ids sharing
|
|
16
|
+
* the truncated prefix fold into ONE entry (a duplicate conflict
|
|
17
|
+
* target inside a single INSERT would abort the statement),
|
|
18
|
+
* - a future-skewed device clock is clamped to ingest time, so the
|
|
19
|
+
* `>=` lastSeenAt dominance guard can't get wedged by one bogus
|
|
20
|
+
* future timestamp and then reject every legitimate update.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const FLEET: string = "0a1b2c3d-0000-0000-0000-000000000001";
|
|
24
|
+
const SHORT_TEXT_LIMIT: number = 100;
|
|
25
|
+
|
|
26
|
+
type LabelMap = Record<string, string>;
|
|
27
|
+
|
|
28
|
+
function toNano(ms: number): string {
|
|
29
|
+
return `${ms}000000`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function datapoint(data: {
|
|
33
|
+
value: number;
|
|
34
|
+
atMs?: number;
|
|
35
|
+
labels?: LabelMap;
|
|
36
|
+
asDouble?: boolean;
|
|
37
|
+
}): JSONObject {
|
|
38
|
+
const attributes: Array<JSONObject> = Object.entries(data.labels || {}).map(
|
|
39
|
+
([key, value]: [string, string]) => {
|
|
40
|
+
return { key, value: { stringValue: value } };
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
return {
|
|
44
|
+
...(data.asDouble ? { asDouble: data.value } : { asInt: data.value }),
|
|
45
|
+
timeUnixNano: toNano(data.atMs ?? Date.now()),
|
|
46
|
+
attributes,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface IoTBuffers {
|
|
51
|
+
resourceBuffer: Map<string, Map<string, IoTDeviceBufferEntry>>;
|
|
52
|
+
fleetBuffer: Map<string, IoTFleetSnapshotBufferEntry>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function iotBuffers(): IoTBuffers {
|
|
56
|
+
return { resourceBuffer: new Map(), fleetBuffer: new Map() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function feed(buffers: IoTBuffers, metricName: string, dp: JSONObject): void {
|
|
60
|
+
bufferIoTSnapshotMetric({
|
|
61
|
+
fleetIdStr: FLEET,
|
|
62
|
+
metricName,
|
|
63
|
+
datapoint: dp,
|
|
64
|
+
resourceBuffer: buffers.resourceBuffer,
|
|
65
|
+
fleetBuffer: buffers.fleetBuffer,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function entries(buffers: IoTBuffers): Array<IoTDeviceBufferEntry> {
|
|
70
|
+
return Array.from(buffers.resourceBuffer.get(FLEET)?.values() || []);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("clampIoTTimestamp", () => {
|
|
74
|
+
const now: Date = new Date("2026-07-02T12:00:00.000Z");
|
|
75
|
+
|
|
76
|
+
test("keeps a past timestamp untouched", () => {
|
|
77
|
+
const past: Date = new Date("2026-07-02T11:00:00.000Z");
|
|
78
|
+
expect(clampIoTTimestamp(past, now)).toEqual(past);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("keeps a future timestamp within the skew tolerance", () => {
|
|
82
|
+
const slightlyAhead: Date = new Date(
|
|
83
|
+
now.getTime() + (IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES - 1) * 60 * 1000,
|
|
84
|
+
);
|
|
85
|
+
expect(clampIoTTimestamp(slightlyAhead, now)).toEqual(slightlyAhead);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("clamps a timestamp beyond the skew tolerance back to now", () => {
|
|
89
|
+
const farAhead: Date = new Date(now.getTime() + 60 * 60 * 1000);
|
|
90
|
+
expect(clampIoTTimestamp(farAhead, now)).toEqual(now);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("bufferIoTSnapshotMetric — device.id length hardening", () => {
|
|
95
|
+
test("truncates a device.id longer than the 100-char column limit", () => {
|
|
96
|
+
const longId: string = "x".repeat(150);
|
|
97
|
+
const buffers: IoTBuffers = iotBuffers();
|
|
98
|
+
|
|
99
|
+
feed(buffers, "iot_device_up", {
|
|
100
|
+
...datapoint({ value: 1, labels: { "device.id": longId } }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const buffered: Array<IoTDeviceBufferEntry> = entries(buffers);
|
|
104
|
+
expect(buffered).toHaveLength(1);
|
|
105
|
+
expect(buffered[0]!.externalId).toBe("x".repeat(SHORT_TEXT_LIMIT));
|
|
106
|
+
expect(buffered[0]!.externalId.length).toBe(SHORT_TEXT_LIMIT);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("keeps a device.id exactly at the column limit untouched", () => {
|
|
110
|
+
const exactId: string = "y".repeat(SHORT_TEXT_LIMIT);
|
|
111
|
+
const buffers: IoTBuffers = iotBuffers();
|
|
112
|
+
|
|
113
|
+
feed(buffers, "iot_device_up", {
|
|
114
|
+
...datapoint({ value: 1, labels: { "device.id": exactId } }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(entries(buffers)[0]!.externalId).toBe(exactId);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("two over-long ids sharing the truncated prefix fold into one entry", () => {
|
|
121
|
+
const shared: string = "z".repeat(SHORT_TEXT_LIMIT);
|
|
122
|
+
const buffers: IoTBuffers = iotBuffers();
|
|
123
|
+
|
|
124
|
+
feed(buffers, "iot_device_up", {
|
|
125
|
+
...datapoint({ value: 1, labels: { "device.id": `${shared}-alpha` } }),
|
|
126
|
+
});
|
|
127
|
+
feed(buffers, "iot_device_up", {
|
|
128
|
+
...datapoint({ value: 0, labels: { "device.id": `${shared}-bravo` } }),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// One folded identity — never two rows hitting one conflict target.
|
|
132
|
+
const buffered: Array<IoTDeviceBufferEntry> = entries(buffers);
|
|
133
|
+
expect(buffered).toHaveLength(1);
|
|
134
|
+
expect(buffered[0]!.externalId).toBe(shared);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("bufferIoTSnapshotMetric — future clock-skew hardening", () => {
|
|
139
|
+
test("clamps an observedAt beyond the skew tolerance to ingest time", () => {
|
|
140
|
+
const buffers: IoTBuffers = iotBuffers();
|
|
141
|
+
const before: number = Date.now();
|
|
142
|
+
|
|
143
|
+
feed(buffers, "iot_battery_percent", {
|
|
144
|
+
...datapoint({
|
|
145
|
+
value: 55,
|
|
146
|
+
asDouble: true,
|
|
147
|
+
atMs: Date.now() + 60 * 60 * 1000, // device clock 1h ahead
|
|
148
|
+
labels: { "device.id": "sensor-1" },
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const after: number = Date.now();
|
|
153
|
+
const observedAt: Date = entries(buffers)[0]!.observedAt;
|
|
154
|
+
expect(observedAt.getTime()).toBeGreaterThanOrEqual(before);
|
|
155
|
+
expect(observedAt.getTime()).toBeLessThanOrEqual(after);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("keeps an observedAt within the skew tolerance untouched", () => {
|
|
159
|
+
const buffers: IoTBuffers = iotBuffers();
|
|
160
|
+
const slightlyAheadMs: number =
|
|
161
|
+
Date.now() + (IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES - 1) * 60 * 1000;
|
|
162
|
+
|
|
163
|
+
feed(buffers, "iot_battery_percent", {
|
|
164
|
+
...datapoint({
|
|
165
|
+
value: 55,
|
|
166
|
+
asDouble: true,
|
|
167
|
+
atMs: slightlyAheadMs,
|
|
168
|
+
labels: { "device.id": "sensor-1" },
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(entries(buffers)[0]!.observedAt.getTime()).toBe(
|
|
173
|
+
Math.trunc(slightlyAheadMs),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a clamped future point no longer wedges newest-wins folding", () => {
|
|
178
|
+
const buffers: IoTBuffers = iotBuffers();
|
|
179
|
+
|
|
180
|
+
// Skewed device stamps 1h ahead — clamped to ~now at ingest.
|
|
181
|
+
feed(buffers, "iot_battery_percent", {
|
|
182
|
+
...datapoint({
|
|
183
|
+
value: 50,
|
|
184
|
+
asDouble: true,
|
|
185
|
+
atMs: Date.now() + 60 * 60 * 1000,
|
|
186
|
+
labels: { "device.id": "sensor-1" },
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// A legitimate later reading must still win the fold.
|
|
191
|
+
feed(buffers, "iot_battery_percent", {
|
|
192
|
+
...datapoint({
|
|
193
|
+
value: 80,
|
|
194
|
+
asDouble: true,
|
|
195
|
+
atMs: Date.now() + 1000,
|
|
196
|
+
labels: { "device.id": "sensor-1" },
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(entries(buffers)[0]!.latestBatteryPercent).toBe(80);
|
|
201
|
+
});
|
|
202
|
+
});
|