@oneuptime/common 11.5.13 → 11.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
  2. package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
  5. package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
  6. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
  7. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
  8. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  9. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  10. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  11. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  12. package/Server/API/GlobalConfigAPI.ts +62 -1
  13. package/Server/EnvironmentConfig.ts +22 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -46
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/WorkflowService.ts +100 -39
  20. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  21. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  22. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
  23. package/Server/Utils/Express.ts +13 -0
  24. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  25. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  26. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  27. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  28. package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
  29. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  30. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  31. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  32. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
  33. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
  34. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  35. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  36. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  37. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  38. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  39. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  40. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  41. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  42. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  43. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  44. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  45. package/Tests/Types/HashCode.test.ts +41 -0
  46. package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
  47. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  48. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  49. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  50. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  51. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  52. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  53. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  54. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  55. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  56. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  57. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  58. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  59. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  60. package/Tests/Utils/Array.test.ts +156 -0
  61. package/Tests/Utils/CronTab.test.ts +199 -0
  62. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
  63. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  64. package/Tests/Utils/Number.test.ts +179 -0
  65. package/Tests/Utils/ValueFormatter.test.ts +305 -0
  66. package/Tests/Utils/VersionUtil.test.ts +348 -0
  67. package/Tests/jest.setup.ts +1 -0
  68. package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
  69. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  70. package/Types/Monitor/MonitorStep.ts +24 -4
  71. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  72. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  73. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  74. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  75. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  76. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  77. package/Utils/CronTab.ts +823 -0
  78. package/Utils/ModelImportExport.ts +21 -6
  79. package/Utils/VersionUtil.ts +260 -0
  80. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
  81. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
  83. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
  85. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
  86. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
  87. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
  88. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
  89. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
  90. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
  91. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
  92. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
  93. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  100. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  101. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  102. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  103. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +17 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
  113. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/DatabaseService.js +53 -1
  115. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  116. package/build/dist/Server/Services/WorkflowService.js +80 -30
  117. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  118. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  119. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  120. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  121. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/Express.js +12 -0
  125. package/build/dist/Server/Utils/Express.js.map +1 -1
  126. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  127. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  128. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  129. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  131. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  133. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  134. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  135. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  136. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  137. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  139. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  140. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  141. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  142. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  143. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  144. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  145. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  146. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  147. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  148. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  149. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  150. package/build/dist/Utils/CronTab.js +609 -0
  151. package/build/dist/Utils/CronTab.js.map +1 -0
  152. package/build/dist/Utils/ModelImportExport.js +20 -6
  153. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  154. package/build/dist/Utils/VersionUtil.js +193 -0
  155. package/build/dist/Utils/VersionUtil.js.map +1 -0
  156. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import NumberUtil from "../../Utils/Number";
2
+
3
+ describe("NumberUtil", () => {
4
+ describe("parseNumberWithDefault", () => {
5
+ it("parses a plain integer string", () => {
6
+ expect(
7
+ NumberUtil.parseNumberWithDefault({ value: "42", defaultValue: 0 }),
8
+ ).toBe(42);
9
+ });
10
+
11
+ it("falls back to the default for undefined, null and empty input", () => {
12
+ expect(
13
+ NumberUtil.parseNumberWithDefault({
14
+ value: undefined,
15
+ defaultValue: 7,
16
+ }),
17
+ ).toBe(7);
18
+ expect(
19
+ NumberUtil.parseNumberWithDefault({
20
+ value: null as unknown as undefined,
21
+ defaultValue: 7,
22
+ }),
23
+ ).toBe(7);
24
+ expect(
25
+ NumberUtil.parseNumberWithDefault({ value: "", defaultValue: 7 }),
26
+ ).toBe(7);
27
+ });
28
+
29
+ it("falls back to the default for non-numeric input", () => {
30
+ expect(
31
+ NumberUtil.parseNumberWithDefault({
32
+ value: "not-a-number",
33
+ defaultValue: 5,
34
+ }),
35
+ ).toBe(5);
36
+ });
37
+
38
+ it("parses the leading integer of a mixed string, like parseInt", () => {
39
+ expect(
40
+ NumberUtil.parseNumberWithDefault({ value: "12px", defaultValue: 0 }),
41
+ ).toBe(12);
42
+ });
43
+
44
+ it("truncates a decimal string to its integer part", () => {
45
+ expect(
46
+ NumberUtil.parseNumberWithDefault({ value: "3.9", defaultValue: 0 }),
47
+ ).toBe(3);
48
+ });
49
+
50
+ it("returns the default when the parsed value is below min", () => {
51
+ expect(
52
+ NumberUtil.parseNumberWithDefault({
53
+ value: "2",
54
+ defaultValue: 10,
55
+ min: 5,
56
+ }),
57
+ ).toBe(10);
58
+ });
59
+
60
+ it("returns the default when the parsed value is above max", () => {
61
+ expect(
62
+ NumberUtil.parseNumberWithDefault({
63
+ value: "200",
64
+ defaultValue: 10,
65
+ max: 100,
66
+ }),
67
+ ).toBe(10);
68
+ });
69
+
70
+ it("accepts a value exactly on the min and max boundaries", () => {
71
+ expect(
72
+ NumberUtil.parseNumberWithDefault({
73
+ value: "5",
74
+ defaultValue: 0,
75
+ min: 5,
76
+ max: 5,
77
+ }),
78
+ ).toBe(5);
79
+ });
80
+
81
+ it("accepts negative values within range", () => {
82
+ expect(
83
+ NumberUtil.parseNumberWithDefault({
84
+ value: "-3",
85
+ defaultValue: 0,
86
+ min: -10,
87
+ }),
88
+ ).toBe(-3);
89
+ });
90
+ });
91
+
92
+ describe("getRandomNumber", () => {
93
+ it("stays within the inclusive range across many draws", () => {
94
+ for (let i: number = 0; i < 1000; i++) {
95
+ const n: number = NumberUtil.getRandomNumber(3, 7);
96
+ expect(n).toBeGreaterThanOrEqual(3);
97
+ expect(n).toBeLessThanOrEqual(7);
98
+ expect(Number.isInteger(n)).toBe(true);
99
+ }
100
+ });
101
+
102
+ it("returns the single value when min equals max", () => {
103
+ expect(NumberUtil.getRandomNumber(4, 4)).toBe(4);
104
+ });
105
+ });
106
+
107
+ describe("convertToTwoDecimalPlaces", () => {
108
+ it("rounds to two decimal places", () => {
109
+ expect(NumberUtil.convertToTwoDecimalPlaces(3.14159)).toBe(3.14);
110
+ expect(NumberUtil.convertToTwoDecimalPlaces(2.005)).toBe(2.01);
111
+ });
112
+
113
+ it("leaves whole numbers unchanged", () => {
114
+ expect(NumberUtil.convertToTwoDecimalPlaces(10)).toBe(10);
115
+ });
116
+
117
+ it("rounds negative numbers", () => {
118
+ expect(NumberUtil.convertToTwoDecimalPlaces(-1.239)).toBe(-1.24);
119
+ });
120
+ });
121
+
122
+ describe("isNumber", () => {
123
+ it("is true for numeric values and numeric strings", () => {
124
+ expect(NumberUtil.isNumber(5)).toBe(true);
125
+ expect(NumberUtil.isNumber("5")).toBe(true);
126
+ expect(NumberUtil.isNumber(0)).toBe(true);
127
+ });
128
+
129
+ it("is false for non-numeric strings", () => {
130
+ expect(NumberUtil.isNumber("abc")).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe("convertToNumber / canBeConvertedToNumber", () => {
135
+ it("converts numeric strings", () => {
136
+ expect(NumberUtil.convertToNumber("42")).toBe(42);
137
+ expect(NumberUtil.convertToNumber("3.5")).toBe(3.5);
138
+ });
139
+
140
+ it("reports whether a value can be converted", () => {
141
+ expect(NumberUtil.canBeConvertedToNumber("42")).toBe(true);
142
+ expect(NumberUtil.canBeConvertedToNumber("nope")).toBe(false);
143
+ });
144
+ });
145
+
146
+ describe("isInteger / isFloat", () => {
147
+ it("distinguishes integers from floats", () => {
148
+ expect(NumberUtil.isInteger(4)).toBe(true);
149
+ expect(NumberUtil.isInteger(4.5)).toBe(false);
150
+ expect(NumberUtil.isFloat(4.5)).toBe(true);
151
+ expect(NumberUtil.isFloat(4)).toBe(false);
152
+ });
153
+ });
154
+
155
+ describe("sign helpers", () => {
156
+ it("classifies positive, negative and zero", () => {
157
+ expect(NumberUtil.isPositive(1)).toBe(true);
158
+ expect(NumberUtil.isPositive(0)).toBe(false);
159
+ expect(NumberUtil.isNegative(-1)).toBe(true);
160
+ expect(NumberUtil.isNegative(0)).toBe(false);
161
+ expect(NumberUtil.isZero(0)).toBe(true);
162
+ expect(NumberUtil.isZero(1)).toBe(false);
163
+ });
164
+ });
165
+
166
+ describe("parity helpers", () => {
167
+ it("classifies even and odd", () => {
168
+ expect(NumberUtil.isEven(2)).toBe(true);
169
+ expect(NumberUtil.isEven(3)).toBe(false);
170
+ expect(NumberUtil.isOdd(3)).toBe(true);
171
+ expect(NumberUtil.isOdd(2)).toBe(false);
172
+ });
173
+
174
+ it("treats zero as even", () => {
175
+ expect(NumberUtil.isEven(0)).toBe(true);
176
+ expect(NumberUtil.isOdd(0)).toBe(false);
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,305 @@
1
+ import ValueFormatter from "../../Utils/ValueFormatter";
2
+
3
+ describe("ValueFormatter", () => {
4
+ describe("formatValue - bytes", () => {
5
+ test("scales bytes up to the right unit", () => {
6
+ expect(ValueFormatter.formatValue(1048576, "bytes")).toBe("1.05 MB");
7
+ expect(ValueFormatter.formatValue(1e6, "bytes")).toBe("1 MB");
8
+ expect(ValueFormatter.formatValue(1e9, "bytes")).toBe("1 GB");
9
+ expect(ValueFormatter.formatValue(1e12, "bytes")).toBe("1 TB");
10
+ expect(ValueFormatter.formatValue(1e15, "bytes")).toBe("1 PB");
11
+ });
12
+
13
+ test("keeps small byte values in B", () => {
14
+ expect(ValueFormatter.formatValue(512, "bytes")).toBe("512 B");
15
+ expect(ValueFormatter.formatValue(1, "bytes")).toBe("1 B");
16
+ });
17
+
18
+ test("recognizes UCUM byte aliases", () => {
19
+ expect(ValueFormatter.formatValue(2000, "By")).toBe("2 KB");
20
+ expect(ValueFormatter.formatValue(2000, "b")).toBe("2 KB");
21
+ expect(ValueFormatter.formatValue(2000, "byte")).toBe("2 KB");
22
+ });
23
+
24
+ test("renders 0 bytes with the natural byte unit", () => {
25
+ expect(ValueFormatter.formatValue(0, "bytes")).toBe("0 B");
26
+ });
27
+
28
+ test("handles negative byte values", () => {
29
+ expect(ValueFormatter.formatValue(-2000, "bytes")).toBe("-2 KB");
30
+ });
31
+ });
32
+
33
+ describe("formatValue - seconds", () => {
34
+ test("scales seconds into larger units", () => {
35
+ expect(ValueFormatter.formatValue(60, "seconds")).toBe("1 min");
36
+ expect(ValueFormatter.formatValue(3600, "seconds")).toBe("1 hours");
37
+ expect(ValueFormatter.formatValue(86400, "seconds")).toBe("1 days");
38
+ });
39
+
40
+ test("scales sub-second values down", () => {
41
+ expect(ValueFormatter.formatValue(0.5, "seconds")).toBe("500 ms");
42
+ expect(ValueFormatter.formatValue(0.0005, "seconds")).toBe("500 µs");
43
+ });
44
+
45
+ test("renders 0 seconds with the natural second unit, not ns", () => {
46
+ expect(ValueFormatter.formatValue(0, "seconds")).toBe("0 sec");
47
+ });
48
+
49
+ test("recognizes second aliases", () => {
50
+ expect(ValueFormatter.formatValue(120, "s")).toBe("2 min");
51
+ expect(ValueFormatter.formatValue(120, "sec")).toBe("2 min");
52
+ });
53
+ });
54
+
55
+ describe("formatValue - milliseconds", () => {
56
+ test("scales milliseconds into seconds and beyond", () => {
57
+ expect(ValueFormatter.formatValue(1000, "ms")).toBe("1 sec");
58
+ expect(ValueFormatter.formatValue(60000, "ms")).toBe("1 min");
59
+ });
60
+
61
+ test("keeps small ms values", () => {
62
+ expect(ValueFormatter.formatValue(5, "ms")).toBe("5 ms");
63
+ });
64
+
65
+ test("renders 0 ms with the natural ms unit", () => {
66
+ expect(ValueFormatter.formatValue(0, "ms")).toBe("0 ms");
67
+ });
68
+ });
69
+
70
+ describe("formatValue - percent", () => {
71
+ test("renders percent inline with two decimals", () => {
72
+ expect(ValueFormatter.formatValue(42, "%")).toBe("42.00%");
73
+ expect(ValueFormatter.formatValue(100, "%")).toBe("100.00%");
74
+ expect(ValueFormatter.formatValue(2.04, "%")).toBe("2.04%");
75
+ });
76
+
77
+ test("treats spelled-out percent variants the same", () => {
78
+ expect(ValueFormatter.formatValue(50, "percent")).toBe("50.00%");
79
+ expect(ValueFormatter.formatValue(50, "percentage")).toBe("50.00%");
80
+ expect(ValueFormatter.formatValue(50, "pct")).toBe("50.00%");
81
+ });
82
+
83
+ test("non-finite percent falls back to 0.00", () => {
84
+ expect(ValueFormatter.formatValue(Infinity, "%")).toBe("0.00%");
85
+ expect(ValueFormatter.formatValue(NaN, "%")).toBe("0.00%");
86
+ });
87
+ });
88
+
89
+ describe("formatValue - dimensionless and fraction metrics", () => {
90
+ test("dimensionless unit '1' renders a bare number", () => {
91
+ expect(ValueFormatter.formatValue(1234, "1")).toBe("1.23K");
92
+ expect(ValueFormatter.formatValue(5, "1")).toBe("5");
93
+ });
94
+
95
+ test("empty unit renders a bare number", () => {
96
+ expect(ValueFormatter.formatValue(5, "")).toBe("5");
97
+ });
98
+
99
+ test("fraction metric with unit '1' renders as a percentage", () => {
100
+ expect(
101
+ ValueFormatter.formatValue(0.25, "1", {
102
+ metricName: "system.cpu.utilization",
103
+ }),
104
+ ).toBe("25.00%");
105
+ expect(
106
+ ValueFormatter.formatValue(0.5, "1", {
107
+ metricName: "db.client.connection.usage_ratio",
108
+ }),
109
+ ).toBe("50.00%");
110
+ });
111
+
112
+ test("non-fraction metric with unit '1' stays a bare number", () => {
113
+ expect(
114
+ ValueFormatter.formatValue(0.25, "1", {
115
+ metricName: "http.server.request.count",
116
+ }),
117
+ ).toBe("0.25");
118
+ });
119
+ });
120
+
121
+ describe("formatValue - annotation-only units", () => {
122
+ test("renders bare number for UCUM annotation units", () => {
123
+ expect(ValueFormatter.formatValue(42, "{thread}")).toBe("42");
124
+ expect(ValueFormatter.formatValue(1500, "{packets}")).toBe("1.5K");
125
+ });
126
+ });
127
+
128
+ describe("formatValue - compound rate units", () => {
129
+ test("scales the numerator and keeps a compact denominator", () => {
130
+ expect(ValueFormatter.formatValue(1500000, "By/s")).toBe("1.5 MB/s");
131
+ expect(ValueFormatter.formatValue(2000, "bytes/s")).toBe("2 KB/s");
132
+ });
133
+
134
+ test("falls back to readable form for unknown numerator", () => {
135
+ expect(ValueFormatter.formatValue(5, "requests/s")).toBe(
136
+ "5 requests per Second",
137
+ );
138
+ });
139
+ });
140
+
141
+ describe("formatValue - unknown units", () => {
142
+ test("formats number and appends a readable unit name", () => {
143
+ expect(ValueFormatter.formatValue(10, "hz")).toBe("10 Hertz");
144
+ expect(ValueFormatter.formatValue(5, "v")).toBe("5 Volts");
145
+ });
146
+
147
+ test("keeps the original unit when unrecognized", () => {
148
+ expect(ValueFormatter.formatValue(5, "widgets")).toBe("5 widgets");
149
+ });
150
+ });
151
+
152
+ describe("formatValue - large number abbreviation", () => {
153
+ test("abbreviates thousands, millions, billions", () => {
154
+ expect(ValueFormatter.formatValue(5000, "widgets")).toBe("5K widgets");
155
+ expect(ValueFormatter.formatValue(1500000, "widgets")).toBe(
156
+ "1.5M widgets",
157
+ );
158
+ expect(ValueFormatter.formatValue(2000000000, "widgets")).toBe(
159
+ "2B widgets",
160
+ );
161
+ });
162
+
163
+ test("trims trailing zeros but keeps distinguishing decimals", () => {
164
+ expect(ValueFormatter.formatValue(119250000000, "widgets")).toBe(
165
+ "119.25B widgets",
166
+ );
167
+ });
168
+ });
169
+
170
+ describe("formatValue - small number precision", () => {
171
+ test("keeps two significant digits below 1", () => {
172
+ expect(ValueFormatter.formatValue(0.004, "widgets")).toBe(
173
+ "0.004 widgets",
174
+ );
175
+ expect(ValueFormatter.formatValue(0.006, "widgets")).toBe(
176
+ "0.006 widgets",
177
+ );
178
+ });
179
+
180
+ test("expands very small numbers out of exponential notation", () => {
181
+ const result: string = ValueFormatter.formatValue(0.00000012, "widgets");
182
+ // Should not fall back to exponential notation like "1.2e-7".
183
+ expect(result).not.toMatch(/e[+-]/i);
184
+ expect(result).toContain("0.00000012");
185
+ });
186
+ });
187
+
188
+ describe("isPercentUnit", () => {
189
+ test("returns true for percent variants", () => {
190
+ expect(ValueFormatter.isPercentUnit("%")).toBe(true);
191
+ expect(ValueFormatter.isPercentUnit("percent")).toBe(true);
192
+ expect(ValueFormatter.isPercentUnit("PERCENTAGE")).toBe(true);
193
+ expect(ValueFormatter.isPercentUnit(" pct ")).toBe(true);
194
+ });
195
+
196
+ test("returns false otherwise", () => {
197
+ expect(ValueFormatter.isPercentUnit("bytes")).toBe(false);
198
+ expect(ValueFormatter.isPercentUnit(undefined)).toBe(false);
199
+ expect(ValueFormatter.isPercentUnit("")).toBe(false);
200
+ });
201
+ });
202
+
203
+ describe("isFractionMetric", () => {
204
+ test("matches utilization/ratio/fraction/percent suffixes", () => {
205
+ expect(ValueFormatter.isFractionMetric("system.cpu.utilization")).toBe(
206
+ true,
207
+ );
208
+ expect(ValueFormatter.isFractionMetric("some_ratio")).toBe(true);
209
+ expect(ValueFormatter.isFractionMetric("mem.fraction")).toBe(true);
210
+ expect(ValueFormatter.isFractionMetric("disk_percentage")).toBe(true);
211
+ });
212
+
213
+ test("does not match unrelated names", () => {
214
+ expect(ValueFormatter.isFractionMetric("request.count")).toBe(false);
215
+ expect(ValueFormatter.isFractionMetric(undefined)).toBe(false);
216
+ expect(ValueFormatter.isFractionMetric("")).toBe(false);
217
+ });
218
+ });
219
+
220
+ describe("isHigherWorseMetric", () => {
221
+ test("returns false for higher-is-better metrics", () => {
222
+ expect(
223
+ ValueFormatter.isHigherWorseMetric("oneuptime.monitor.online"),
224
+ ).toBe(false);
225
+ expect(ValueFormatter.isHigherWorseMetric("service.uptime")).toBe(false);
226
+ expect(ValueFormatter.isHigherWorseMetric("availability_percent")).toBe(
227
+ false,
228
+ );
229
+ });
230
+
231
+ test("returns true for higher-is-worse metrics", () => {
232
+ expect(ValueFormatter.isHigherWorseMetric("http.error.count")).toBe(true);
233
+ expect(ValueFormatter.isHigherWorseMetric("incident.count")).toBe(true);
234
+ expect(ValueFormatter.isHigherWorseMetric("request.latency")).toBe(true);
235
+ expect(ValueFormatter.isHigherWorseMetric("cpu.utilization")).toBe(true);
236
+ expect(ValueFormatter.isHigherWorseMetric("pod.restart")).toBe(true);
237
+ });
238
+
239
+ test("returns false for neutral counters and undefined", () => {
240
+ expect(ValueFormatter.isHigherWorseMetric("network.bytes")).toBe(false);
241
+ expect(ValueFormatter.isHigherWorseMetric(undefined)).toBe(false);
242
+ });
243
+
244
+ test("higher-is-better token wins over higher-is-worse token", () => {
245
+ // Contains both "uptime" (better) and would otherwise be neutral.
246
+ expect(ValueFormatter.isHigherWorseMetric("system.uptime.seconds")).toBe(
247
+ false,
248
+ );
249
+ });
250
+ });
251
+
252
+ describe("isScalableUnit", () => {
253
+ test("true for known scalable units", () => {
254
+ expect(ValueFormatter.isScalableUnit("bytes")).toBe(true);
255
+ expect(ValueFormatter.isScalableUnit("ms")).toBe(true);
256
+ expect(ValueFormatter.isScalableUnit("By")).toBe(true);
257
+ });
258
+
259
+ test("false for empty or unknown units", () => {
260
+ expect(ValueFormatter.isScalableUnit("")).toBe(false);
261
+ expect(ValueFormatter.isScalableUnit(" ")).toBe(false);
262
+ expect(ValueFormatter.isScalableUnit("%")).toBe(false);
263
+ expect(ValueFormatter.isScalableUnit("widgets")).toBe(false);
264
+ });
265
+ });
266
+
267
+ describe("getReadableUnit", () => {
268
+ test("maps UCUM codes to readable names", () => {
269
+ expect(ValueFormatter.getReadableUnit("By")).toBe("Bytes");
270
+ expect(ValueFormatter.getReadableUnit("s")).toBe("Seconds");
271
+ expect(ValueFormatter.getReadableUnit("ms")).toBe("Milliseconds");
272
+ });
273
+
274
+ test("dimensionless returns empty string", () => {
275
+ expect(ValueFormatter.getReadableUnit("1")).toBe("");
276
+ expect(ValueFormatter.getReadableUnit("")).toBe("");
277
+ });
278
+
279
+ test("fraction metric with '1' returns Percent", () => {
280
+ expect(
281
+ ValueFormatter.getReadableUnit("1", {
282
+ metricName: "cpu.utilization",
283
+ }),
284
+ ).toBe("Percent");
285
+ });
286
+
287
+ test("percent variants return Percent", () => {
288
+ expect(ValueFormatter.getReadableUnit("%")).toBe("Percent");
289
+ expect(ValueFormatter.getReadableUnit("pct")).toBe("Percent");
290
+ });
291
+
292
+ test("annotation-only unit is capitalized", () => {
293
+ expect(ValueFormatter.getReadableUnit("{thread}")).toBe("Thread");
294
+ expect(ValueFormatter.getReadableUnit("{packets}")).toBe("Packets");
295
+ });
296
+
297
+ test("compound rate unit reads as 'X per Y' with singular denominator", () => {
298
+ expect(ValueFormatter.getReadableUnit("By/s")).toBe("Bytes per Second");
299
+ });
300
+
301
+ test("unknown unit is returned unchanged", () => {
302
+ expect(ValueFormatter.getReadableUnit("widgets")).toBe("widgets");
303
+ });
304
+ });
305
+ });