@oneuptime/common 12.0.27 → 12.0.29

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 (204) hide show
  1. package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
  2. package/Models/DatabaseModels/CloudResource.ts +17 -1
  3. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  4. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  5. package/Models/DatabaseModels/RumApplication.ts +35 -2
  6. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  7. package/Server/API/DashboardDomainAPI.ts +74 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  9. package/Server/API/StatusPageDomainAPI.ts +74 -0
  10. package/Server/API/TelemetryAPI.ts +104 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  14. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  15. package/Server/Services/DashboardDomainService.ts +131 -0
  16. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  17. package/Server/Services/MonitorTemplateService.ts +70 -11
  18. package/Server/Services/StatusPageDomainService.ts +163 -3
  19. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  20. package/Server/Types/Domain.ts +17 -0
  21. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  22. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  23. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  24. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  25. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  26. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
  27. package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
  28. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
  29. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  30. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  31. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  32. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  34. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  35. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  36. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  37. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  38. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
  39. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  40. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  41. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  42. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  43. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  44. package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
  45. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  46. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  47. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  48. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  49. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  50. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  51. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  52. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  53. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  54. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  55. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  56. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  57. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  58. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  59. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  60. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  61. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  62. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  63. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  64. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  65. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  66. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  67. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  68. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  69. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  70. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  71. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  72. package/Tests/Types/API/Route.test.ts +36 -4
  73. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  74. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  75. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  76. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  77. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  78. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  79. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  80. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  81. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  82. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  83. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  84. package/Tests/Utils/Array.test.ts +100 -0
  85. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  86. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  87. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  88. package/Types/API/Route.ts +20 -5
  89. package/Types/Date.ts +143 -13
  90. package/Types/Rum/SessionReplay.ts +76 -1
  91. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  92. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  93. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  94. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  95. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  96. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  97. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  98. package/UI/Utils/Navigation.ts +42 -0
  99. package/Utils/Array.ts +37 -0
  100. package/Utils/CertificateReissue.ts +128 -0
  101. package/Utils/Grok/Grok.ts +461 -0
  102. package/Utils/Grok/GrokPatterns.ts +118 -0
  103. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  104. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
  105. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
  107. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  109. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  111. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
  113. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  115. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  116. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  117. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  118. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  119. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  120. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  121. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  122. package/build/dist/Server/API/TelemetryAPI.js +67 -14
  123. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  130. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  131. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  132. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  133. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  134. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  135. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  136. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  137. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  138. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  139. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  140. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  141. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  142. package/build/dist/Server/Types/Domain.js +16 -0
  143. package/build/dist/Server/Types/Domain.js.map +1 -1
  144. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  145. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  146. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  147. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  148. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  149. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  150. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  151. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  152. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  153. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  154. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  155. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
  156. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
  157. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
  158. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  160. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  163. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  164. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  165. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  166. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  167. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  168. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  169. package/build/dist/Types/API/Route.js +16 -5
  170. package/build/dist/Types/API/Route.js.map +1 -1
  171. package/build/dist/Types/Date.js +115 -13
  172. package/build/dist/Types/Date.js.map +1 -1
  173. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  174. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  175. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  176. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  177. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  178. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  179. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  180. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  181. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  182. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  183. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  184. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  185. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  186. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  187. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  188. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  189. package/build/dist/UI/Utils/Navigation.js +30 -0
  190. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  191. package/build/dist/Utils/Array.js +25 -0
  192. package/build/dist/Utils/Array.js.map +1 -1
  193. package/build/dist/Utils/CertificateReissue.js +97 -0
  194. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  195. package/build/dist/Utils/Grok/Grok.js +324 -0
  196. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  197. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  198. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  199. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  200. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  201. package/package.json +1 -1
  202. package/UI/Utils/JsonWebToken.ts +0 -14
  203. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  204. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,212 @@
1
+ /** @timezone UTC */
2
+ /**
3
+ * The sentence the logs explorer shows when a search comes back empty repeats
4
+ * the window that was searched, so the reader can tell "nothing matched" apart
5
+ * from "you are looking at the wrong hour".
6
+ *
7
+ * It had the same defect as the picker button above it, in mirror image: the
8
+ * window was formatted with a hardcoded `toLocaleString("en-US", ...)`, so it
9
+ * always came out on a 12-hour clock even for a user whose machine is set to
10
+ * 24-hour, and always in the browser's own zone rather than the one configured
11
+ * in User Settings. The picker meanwhile always said 24-hour. One screen, one
12
+ * window, two disagreeing renderings, neither of them the user's.
13
+ */
14
+ import "@testing-library/jest-dom";
15
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
16
+ import getJestMockFunction, { MockFunction } from "../../MockType";
17
+
18
+ /*
19
+ * Declared before jest.mock but dereferenced inside the factories: ts-jest
20
+ * hoists the jest.mock calls above these initializers, so naming the mocks
21
+ * directly in a factory would capture undefined.
22
+ */
23
+ const getListMock: MockFunction = getJestMockFunction();
24
+ const postMock: MockFunction = getJestMockFunction();
25
+
26
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
27
+ return {
28
+ __esModule: true,
29
+ default: {
30
+ getList: (...args: Array<any>) => {
31
+ return getListMock(...args);
32
+ },
33
+ getCommonHeaders: () => {
34
+ return {};
35
+ },
36
+ },
37
+ };
38
+ });
39
+
40
+ jest.mock("../../../UI/Utils/API/API", () => {
41
+ return {
42
+ __esModule: true,
43
+ default: {
44
+ post: (...args: Array<any>) => {
45
+ return postMock(...args);
46
+ },
47
+ getFriendlyErrorMessage: (error: Error) => {
48
+ return error.message;
49
+ },
50
+ getFriendlyMessage: (error: Error) => {
51
+ return error.message;
52
+ },
53
+ },
54
+ };
55
+ });
56
+
57
+ getListMock.mockImplementation(() => {
58
+ return Promise.resolve({ data: [], count: 0, skip: 0, limit: 0 });
59
+ });
60
+
61
+ postMock.mockImplementation(() => {
62
+ return Promise.resolve({ data: {} });
63
+ });
64
+
65
+ // Imported after the mocks so the module picks the mocked API surfaces up.
66
+ import { getEmptyMessageWithTimeRange } from "../../../UI/Components/LogsViewer/LogsViewer";
67
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
68
+ import OneUptimeDate from "../../../Types/Date";
69
+ import TimeRange from "../../../Types/Time/TimeRange";
70
+ import Timezone from "../../../Types/Timezone";
71
+
72
+ const AFTERNOON: InBetween<Date> = new InBetween<Date>(
73
+ new Date("2024-03-01T14:30:00.000Z"),
74
+ new Date("2024-03-01T18:45:00.000Z"),
75
+ );
76
+
77
+ function pin(use12HourFormat: boolean): void {
78
+ jest
79
+ .spyOn(OneUptimeDate, "getUserPrefers12HourFormat")
80
+ .mockReturnValue(use12HourFormat);
81
+ }
82
+
83
+ afterEach(() => {
84
+ jest.restoreAllMocks();
85
+ OneUptimeDate.setUserTimezone(null);
86
+ });
87
+
88
+ describe("getEmptyMessageWithTimeRange", () => {
89
+ describe("a custom window", () => {
90
+ test("is written with AM/PM on a 12-hour machine", () => {
91
+ pin(true);
92
+
93
+ expect(
94
+ getEmptyMessageWithTimeRange({
95
+ range: TimeRange.CUSTOM,
96
+ startAndEndDate: AFTERNOON,
97
+ }),
98
+ ).toBe(
99
+ "Time range: Mar 1, 2:30 PM – Mar 1, 6:45 PM. Try adjusting filters or expanding the time range.",
100
+ );
101
+ });
102
+
103
+ test("is written on a 24-hour clock on a 24-hour machine", () => {
104
+ pin(false);
105
+
106
+ const message: string = getEmptyMessageWithTimeRange({
107
+ range: TimeRange.CUSTOM,
108
+ startAndEndDate: AFTERNOON,
109
+ });
110
+
111
+ expect(message).toBe(
112
+ "Time range: Mar 1, 14:30 – Mar 1, 18:45. Try adjusting filters or expanding the time range.",
113
+ );
114
+ expect(message).not.toMatch(/AM|PM/);
115
+ });
116
+
117
+ test("no longer forces AM/PM on a 24-hour machine", () => {
118
+ /*
119
+ * The exact regression: `toLocaleString("en-US", ...)` pinned the locale,
120
+ * so this sentence said "02:30 PM" to a reader whose whole machine - and
121
+ * whose picker button - was on a 24-hour clock.
122
+ */
123
+ pin(false);
124
+
125
+ expect(
126
+ getEmptyMessageWithTimeRange({
127
+ range: TimeRange.CUSTOM,
128
+ startAndEndDate: AFTERNOON,
129
+ }),
130
+ ).not.toContain("02:30 PM");
131
+ });
132
+
133
+ test("is read in the timezone configured in User Settings", () => {
134
+ pin(true);
135
+ OneUptimeDate.setUserTimezone(Timezone.AsiaKolkata);
136
+
137
+ expect(
138
+ getEmptyMessageWithTimeRange({
139
+ range: TimeRange.CUSTOM,
140
+ startAndEndDate: AFTERNOON,
141
+ }),
142
+ ).toContain("Mar 1, 8:00 PM – Mar 2, 12:15 AM");
143
+ });
144
+
145
+ test("matches the label the picker button puts on the same window", () => {
146
+ /*
147
+ * The two sit on the same screen describing the same window, so they must
148
+ * not disagree. Both now go through one formatter - this pins that they
149
+ * still do.
150
+ */
151
+ pin(true);
152
+ OneUptimeDate.setUserTimezone(Timezone.AmericaNew_York);
153
+
154
+ const start: string = OneUptimeDate.getDateAsLocalShortDateTimeString(
155
+ AFTERNOON.startValue,
156
+ );
157
+ const end: string = OneUptimeDate.getDateAsLocalShortDateTimeString(
158
+ AFTERNOON.endValue,
159
+ );
160
+
161
+ expect(
162
+ getEmptyMessageWithTimeRange({
163
+ range: TimeRange.CUSTOM,
164
+ startAndEndDate: AFTERNOON,
165
+ }),
166
+ ).toContain(`${start} – ${end}`);
167
+ });
168
+ });
169
+
170
+ describe("a preset window", () => {
171
+ test("names the preset rather than any clock time", () => {
172
+ pin(true);
173
+
174
+ const message: string = getEmptyMessageWithTimeRange({
175
+ range: TimeRange.PAST_ONE_HOUR,
176
+ });
177
+
178
+ expect(message).toBe(
179
+ "Time range: past 1 hour. Try adjusting filters or expanding the time range.",
180
+ );
181
+ expect(message).not.toMatch(/\d{1,2}:\d{2}/);
182
+ });
183
+
184
+ test("reads the same on either clock", () => {
185
+ pin(true);
186
+ const on12Hour: string = getEmptyMessageWithTimeRange({
187
+ range: TimeRange.PAST_THIRTY_MINS,
188
+ });
189
+
190
+ pin(false);
191
+ const on24Hour: string = getEmptyMessageWithTimeRange({
192
+ range: TimeRange.PAST_THIRTY_MINS,
193
+ });
194
+
195
+ expect(on12Hour).toBe(on24Hour);
196
+ });
197
+ });
198
+
199
+ describe("no window at all", () => {
200
+ test("falls back to a message with no range in it", () => {
201
+ expect(getEmptyMessageWithTimeRange(undefined)).toBe(
202
+ "Adjust filters or check again later.",
203
+ );
204
+ });
205
+
206
+ test("falls back when a custom range carries no dates", () => {
207
+ expect(getEmptyMessageWithTimeRange({ range: TimeRange.CUSTOM })).toBe(
208
+ "Time range: custom. Try adjusting filters or expanding the time range.",
209
+ );
210
+ });
211
+ });
212
+ });
@@ -0,0 +1,359 @@
1
+ /** @timezone America/Los_Angeles */
2
+ /**
3
+ * The reported bug, one layer below the time range picker: every timestamp the
4
+ * Logs / Traces / Metrics explorers draw *inside* the chart - the histogram's
5
+ * x-axis ticks and the tooltip that follows the cursor - was formatted with a
6
+ * bare `date.toLocaleTimeString([], { hour12: false })`.
7
+ *
8
+ * That hardcodes a 24-hour clock over the user's own preference, and passes no
9
+ * `timeZone`, so the digits come from the browser process instead of the zone
10
+ * set in User Settings. Inside a single Logs explorer that produced two
11
+ * different rules on one screen: LogsTable's Time column already goes through
12
+ * OneUptimeDate (honouring both), while the histogram directly above it did
13
+ * not.
14
+ *
15
+ * These render the real components rather than calling the formatters,
16
+ * because a correct formatter is worth nothing if the chart stops reading from
17
+ * it. The browser zone is pinned to Los Angeles by the docblock and every case
18
+ * configures a different zone, so nothing here can pass by coincidence.
19
+ */
20
+ import HistogramTooltip from "../../../UI/Components/LogsViewer/components/HistogramTooltip";
21
+ import LogsHistogram from "../../../UI/Components/LogsViewer/components/LogsHistogram";
22
+ import {
23
+ formatTickTime as formatLogsAnalyticsTick,
24
+ formatTooltipLabel as formatLogsAnalyticsTooltip,
25
+ } from "../../../UI/Components/LogsViewer/components/LogsAnalyticsView";
26
+ import TelemetryHistogram from "../../../UI/Components/TelemetryViewer/components/TelemetryHistogram";
27
+ import TelemetryHistogramTooltip from "../../../UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip";
28
+ import { HistogramBucket as LogsBucket } from "../../../UI/Components/LogsViewer/types";
29
+ import {
30
+ HistogramBucket as TelemetryBucket,
31
+ HistogramSeriesOption,
32
+ } from "../../../UI/Components/TelemetryViewer/types";
33
+ import LogSeverity from "../../../Types/Log/LogSeverity";
34
+ import OneUptimeDate from "../../../Types/Date";
35
+ import Timezone from "../../../Types/Timezone";
36
+ import { cleanup, render } from "@testing-library/react";
37
+ import React from "react";
38
+ import { afterEach, describe, expect, test } from "@jest/globals";
39
+
40
+ /*
41
+ * ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so
42
+ * the chart draws no ticks at all without a fixed size.
43
+ */
44
+ jest.mock("recharts", () => {
45
+ const actual: Record<string, any> = jest.requireActual("recharts");
46
+ return {
47
+ ...actual,
48
+ ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
49
+ return React.cloneElement(children, { width: 600, height: 300 });
50
+ },
51
+ };
52
+ });
53
+
54
+ /*
55
+ * Three consecutive minutes straddling noon UTC. Noon is deliberate: it is the
56
+ * reading a naive `hours % 12` turns into "0:00 PM", and it is where a 24-hour
57
+ * "12:00" and a 12-hour "12:00 PM" differ only by the marker.
58
+ */
59
+ const BUCKET_TIMES: Array<string> = [
60
+ "2026-08-05T11:58:00Z",
61
+ "2026-08-05T11:59:00Z",
62
+ "2026-08-05T12:00:00Z",
63
+ ];
64
+
65
+ const LOG_BUCKETS: Array<LogsBucket> = BUCKET_TIMES.map(
66
+ (time: string, index: number): LogsBucket => {
67
+ return { time, severity: LogSeverity.Error, count: index + 3 };
68
+ },
69
+ );
70
+
71
+ const TELEMETRY_SERIES: Array<HistogramSeriesOption> = [
72
+ { key: "ok", label: "OK", color: "#10b981" },
73
+ ];
74
+
75
+ const TELEMETRY_BUCKETS: Array<TelemetryBucket> = BUCKET_TIMES.map(
76
+ (time: string, index: number): TelemetryBucket => {
77
+ return { time, series: "ok", count: index + 3 };
78
+ },
79
+ );
80
+
81
+ /** Matches a clock reading on either clock, so it selects the time axis without assuming the fix. */
82
+ const CLOCK_LABEL: RegExp = /\d{1,2}:\d{2}/;
83
+
84
+ function timeAxisLabels(container: HTMLElement): Array<string> {
85
+ return Array.from(
86
+ container.querySelectorAll(".recharts-cartesian-axis-tick-value"),
87
+ )
88
+ .map((tick: Element): string => {
89
+ return tick.textContent || "";
90
+ })
91
+ .filter((label: string): boolean => {
92
+ return CLOCK_LABEL.test(label);
93
+ });
94
+ }
95
+
96
+ /** The tooltip prints its timestamp in the first paragraph, above the counts. */
97
+ function tooltipTimestamp(container: HTMLElement): string {
98
+ return (container.querySelector("p")?.textContent || "").trim();
99
+ }
100
+
101
+ function pin(use12HourFormat: boolean, timezone: Timezone): void {
102
+ jest
103
+ .spyOn(OneUptimeDate, "getUserPrefers12HourFormat")
104
+ .mockReturnValue(use12HourFormat);
105
+ OneUptimeDate.setUserTimezone(timezone);
106
+ }
107
+
108
+ afterEach(() => {
109
+ cleanup();
110
+ jest.restoreAllMocks();
111
+ OneUptimeDate.setUserTimezone(null);
112
+ });
113
+
114
+ interface HistogramSurface {
115
+ name: string;
116
+ render: () => HTMLElement;
117
+ }
118
+
119
+ const HISTOGRAMS: Array<HistogramSurface> = [
120
+ {
121
+ name: "logs",
122
+ render: (): HTMLElement => {
123
+ return render(<LogsHistogram buckets={LOG_BUCKETS} isLoading={false} />)
124
+ .container;
125
+ },
126
+ },
127
+ {
128
+ name: "telemetry",
129
+ render: (): HTMLElement => {
130
+ return render(
131
+ <TelemetryHistogram
132
+ buckets={TELEMETRY_BUCKETS}
133
+ series={TELEMETRY_SERIES}
134
+ isLoading={false}
135
+ />,
136
+ ).container;
137
+ },
138
+ },
139
+ ];
140
+
141
+ describe.each(HISTOGRAMS)(
142
+ "the $name explorer histogram x-axis",
143
+ (surface: HistogramSurface) => {
144
+ test("labels its ticks with AM/PM on a machine set to a 12-hour clock", () => {
145
+ pin(true, Timezone.UTC);
146
+
147
+ const labels: Array<string> = timeAxisLabels(surface.render());
148
+
149
+ expect(labels).toContain("12:00 PM");
150
+ expect(labels).toContain("11:58 AM");
151
+ });
152
+
153
+ test("never leaves a bare 24-hour tick on a 12-hour machine", () => {
154
+ pin(true, Timezone.UTC);
155
+
156
+ const labels: Array<string> = timeAxisLabels(surface.render());
157
+
158
+ expect(labels).not.toContain("12:00");
159
+ expect(labels).not.toContain("11:58");
160
+ });
161
+
162
+ test("keeps 24-hour ticks on a machine set to a 24-hour clock", () => {
163
+ pin(false, Timezone.UTC);
164
+
165
+ const labels: Array<string> = timeAxisLabels(surface.render());
166
+
167
+ expect(labels).toContain("12:00");
168
+ expect(labels).toContain("11:58");
169
+ expect(labels.join(" ")).not.toContain("PM");
170
+ });
171
+
172
+ /*
173
+ * Los Angeles is the browser's zone here; Kolkata is the configured one.
174
+ * 12:00 UTC is 05:00 in Los Angeles and 17:30 in Kolkata, so the two are
175
+ * impossible to confuse.
176
+ */
177
+ test("draws the ticks in the configured timezone, not the browser's", () => {
178
+ pin(false, Timezone.AsiaKolkata);
179
+
180
+ const labels: Array<string> = timeAxisLabels(surface.render());
181
+
182
+ expect(labels).toContain("17:30");
183
+ expect(labels).not.toContain("05:00");
184
+ expect(labels).not.toContain("12:00");
185
+ });
186
+
187
+ test("carries the configured timezone into the 12-hour reading too", () => {
188
+ pin(true, Timezone.AsiaKolkata);
189
+
190
+ expect(timeAxisLabels(surface.render())).toContain("5:30 PM");
191
+ });
192
+ },
193
+ );
194
+
195
+ interface TooltipSurface {
196
+ name: string;
197
+ render: (label: string) => HTMLElement;
198
+ }
199
+
200
+ const TOOLTIPS: Array<TooltipSurface> = [
201
+ {
202
+ name: "logs histogram",
203
+ render: (label: string): HTMLElement => {
204
+ return render(
205
+ <HistogramTooltip
206
+ active={true}
207
+ label={label}
208
+ payload={[
209
+ {
210
+ dataKey: LogSeverity.Error,
211
+ value: 7,
212
+ payload: {},
213
+ },
214
+ ]}
215
+ />,
216
+ ).container;
217
+ },
218
+ },
219
+ {
220
+ name: "telemetry histogram",
221
+ render: (label: string): HTMLElement => {
222
+ return render(
223
+ <TelemetryHistogramTooltip
224
+ active={true}
225
+ label={label}
226
+ payload={[{ dataKey: "ok", value: 7, payload: {} }]}
227
+ seriesByKey={{ ok: TELEMETRY_SERIES[0] as HistogramSeriesOption }}
228
+ />,
229
+ ).container;
230
+ },
231
+ },
232
+ ];
233
+
234
+ describe.each(TOOLTIPS)("the $name tooltip", (surface: TooltipSurface) => {
235
+ /*
236
+ * A bucket from a day that is definitely not today, so the branch under test
237
+ * is the one that spells the date out.
238
+ */
239
+ const OLD_BUCKET: string = "2020-03-01T14:30:45.000Z";
240
+
241
+ test("writes an older bucket with AM/PM on a 12-hour machine", () => {
242
+ pin(true, Timezone.UTC);
243
+
244
+ expect(tooltipTimestamp(surface.render(OLD_BUCKET))).toBe(
245
+ "Mar 1, 2:30:45 PM",
246
+ );
247
+ });
248
+
249
+ test("writes an older bucket on a 24-hour clock when that is the preference", () => {
250
+ pin(false, Timezone.UTC);
251
+
252
+ expect(tooltipTimestamp(surface.render(OLD_BUCKET))).toBe(
253
+ "Mar 1, 14:30:45",
254
+ );
255
+ });
256
+
257
+ test("reads the bucket in the configured timezone", () => {
258
+ pin(false, Timezone.AsiaKolkata);
259
+
260
+ // 14:30:45 UTC is 20:00:45 the same evening in Kolkata.
261
+ expect(tooltipTimestamp(surface.render(OLD_BUCKET))).toBe(
262
+ "Mar 1, 20:00:45",
263
+ );
264
+ });
265
+
266
+ /*
267
+ * Today's buckets drop the date - the explorer's range picker already says
268
+ * which day is on screen. Which day counts as "today" has to be answered in
269
+ * the configured zone; the old `toDateString()` comparison asked Los Angeles
270
+ * instead, so for several hours either side of midnight in Kolkata the two
271
+ * disagreed. "Now" is pinned in both cases below, because a comparison that
272
+ * happens to be right at the hour the suite runs proves nothing.
273
+ */
274
+ function pinNow(instant: string): void {
275
+ jest
276
+ .spyOn(OneUptimeDate, "getCurrentDate")
277
+ .mockReturnValue(new Date(instant));
278
+ }
279
+
280
+ test("drops the date from a bucket on today's date in the configured timezone", () => {
281
+ pin(false, Timezone.AsiaKolkata);
282
+ // 21:30 on 1 March in Kolkata; still 08:00 that morning in Los Angeles.
283
+ pinNow("2024-03-01T16:00:00.000Z");
284
+
285
+ /*
286
+ * 07:30 the same Kolkata morning - but the evening of 29 February in Los
287
+ * Angeles, so the browser-zone comparison would have dated this bucket.
288
+ */
289
+ expect(tooltipTimestamp(surface.render("2024-03-01T02:00:00.000Z"))).toBe(
290
+ "07:30:00",
291
+ );
292
+ });
293
+
294
+ test("dates a bucket the browser's zone would have called today", () => {
295
+ pin(false, Timezone.AsiaKolkata);
296
+ // 00:30 on 2 March in Kolkata; still 11:00 on 1 March in Los Angeles.
297
+ pinNow("2024-03-01T19:00:00.000Z");
298
+
299
+ /*
300
+ * 21:30 on 1 March in Kolkata - yesterday there, so it needs its date -
301
+ * while Los Angeles puts it on the same day as "now" and would have
302
+ * dropped it.
303
+ */
304
+ expect(tooltipTimestamp(surface.render("2024-03-01T16:00:00.000Z"))).toBe(
305
+ "Mar 1, 21:30:00",
306
+ );
307
+ });
308
+
309
+ test("spells the date out on a bucket from another day", () => {
310
+ pin(false, Timezone.UTC);
311
+
312
+ expect(tooltipTimestamp(surface.render(OLD_BUCKET))).toContain("Mar 1,");
313
+ });
314
+
315
+ test("leaves an unparseable label alone", () => {
316
+ pin(true, Timezone.UTC);
317
+
318
+ expect(tooltipTimestamp(surface.render("not-a-date"))).toBe("not-a-date");
319
+ });
320
+ });
321
+
322
+ /*
323
+ * The Logs explorer's analytics tab draws its own chart from the same buckets.
324
+ * Its formatters are module-level so they can be checked directly - rendering
325
+ * the view itself would need the whole telemetry API behind it.
326
+ */
327
+ describe("the logs analytics chart", () => {
328
+ test("labels x-axis ticks on the machine's clock and configured zone", () => {
329
+ pin(true, Timezone.AsiaKolkata);
330
+ expect(formatLogsAnalyticsTick("2026-08-05T12:00:00Z")).toBe("5:30 PM");
331
+
332
+ jest.restoreAllMocks();
333
+
334
+ pin(false, Timezone.AsiaKolkata);
335
+ expect(formatLogsAnalyticsTick("2026-08-05T12:00:00Z")).toBe("17:30");
336
+ });
337
+
338
+ test("dates its tooltip label on the same clock", () => {
339
+ pin(true, Timezone.UTC);
340
+ expect(formatLogsAnalyticsTooltip("2026-08-05T12:00:00Z")).toBe(
341
+ "Aug 5, 12:00 PM",
342
+ );
343
+
344
+ jest.restoreAllMocks();
345
+
346
+ pin(false, Timezone.UTC);
347
+ expect(formatLogsAnalyticsTooltip("2026-08-05T12:00:00Z")).toBe(
348
+ "Aug 5, 12:00",
349
+ );
350
+ });
351
+
352
+ test("leaves unparseable input alone", () => {
353
+ pin(true, Timezone.UTC);
354
+
355
+ expect(formatLogsAnalyticsTick("not-a-date")).toBe("not-a-date");
356
+ expect(formatLogsAnalyticsTooltip("not-a-date")).toBe("not-a-date");
357
+ expect(formatLogsAnalyticsTooltip(undefined)).toBe("");
358
+ });
359
+ });