@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,259 @@
1
+ /** @timezone America/Los_Angeles */
2
+ /**
3
+ * The clock reading behind every timestamp on the Logs / Traces / Metrics
4
+ * explorers - histogram x-axis ticks, histogram tooltips, span rows.
5
+ *
6
+ * Those sites used to call `date.toLocaleTimeString([], { hour12: false })`
7
+ * directly, which pinned them to a 24-hour clock and to the zone the browser
8
+ * process happens to run in. So a user whose computer keeps an AM/PM clock
9
+ * read "14:30" off a chart, and a user who had set a timezone in User
10
+ * Settings read a chart that disagreed with the log rows underneath it - in
11
+ * one Logs explorer the table's Time column and the histogram directly above
12
+ * it were formatted by two different rules.
13
+ *
14
+ * The docblock above pins the *browser* zone to Los Angeles and every case
15
+ * below configures a different zone, so a helper that quietly fell back to the
16
+ * process zone fails rather than passing by coincidence.
17
+ */
18
+ import OneUptimeDate from "../../Types/Date";
19
+ import Timezone from "../../Types/Timezone";
20
+ import { afterEach, describe, expect, it, jest } from "@jest/globals";
21
+
22
+ const UTC: Timezone = Timezone.UTC;
23
+ const NEW_YORK: Timezone = Timezone.AmericaNew_York;
24
+ const KOLKATA: Timezone = Timezone.AsiaKolkata;
25
+
26
+ /** 14:30 UTC - an afternoon, so a 12-hour clock has real work to do. */
27
+ const AFTERNOON: Date = new Date("2024-03-01T14:30:45.000Z");
28
+
29
+ type Options = {
30
+ includeMinutes?: boolean;
31
+ includeSeconds?: boolean;
32
+ use12HourFormat?: boolean | undefined;
33
+ };
34
+
35
+ function format(date: Date, options?: Options): string {
36
+ return OneUptimeDate.getLocalTimeString(date, options);
37
+ }
38
+
39
+ afterEach(() => {
40
+ // Never leak a pinned zone or a mocked preference into the rest of the suite.
41
+ OneUptimeDate.setUserTimezone(null);
42
+ jest.restoreAllMocks();
43
+ });
44
+
45
+ describe("OneUptimeDate.getLocalTimeString", () => {
46
+ /*
47
+ * The chart x-axes in UI/Components/Charts/Utils/XAxis.ts label ticks with
48
+ * this helper and are laid out for the fixed width of a 24-hour reading.
49
+ * They pass no preference, so the default has to stay where it was - these
50
+ * two cases are what makes the new option opt-in rather than a behaviour
51
+ * change for all nine of its existing callers.
52
+ */
53
+ describe("with no preference passed", () => {
54
+ it("keeps the 24-hour reading it has always produced", () => {
55
+ OneUptimeDate.setUserTimezone(UTC);
56
+
57
+ expect(format(AFTERNOON)).toBe("14:30");
58
+ });
59
+
60
+ it("does not consult the machine's clock preference at all", () => {
61
+ OneUptimeDate.setUserTimezone(UTC);
62
+
63
+ jest.spyOn(OneUptimeDate, "getUserPrefers12HourFormat");
64
+
65
+ format(AFTERNOON);
66
+
67
+ expect(OneUptimeDate.getUserPrefers12HourFormat).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it("zero-pads the hour so ticks stay the same width", () => {
71
+ OneUptimeDate.setUserTimezone(UTC);
72
+
73
+ expect(format(new Date("2024-03-01T09:05:00.000Z"))).toBe("09:05");
74
+ expect(format(new Date("2024-03-01T00:00:00.000Z"))).toBe("00:00");
75
+ });
76
+
77
+ it("appends seconds only when asked", () => {
78
+ OneUptimeDate.setUserTimezone(UTC);
79
+
80
+ expect(format(AFTERNOON, { includeSeconds: true })).toBe("14:30:45");
81
+ });
82
+
83
+ it("returns the bare hour when minutes are turned off", () => {
84
+ OneUptimeDate.setUserTimezone(UTC);
85
+
86
+ expect(format(AFTERNOON, { includeMinutes: false })).toBe("14");
87
+ });
88
+ });
89
+
90
+ describe("on a 12-hour clock", () => {
91
+ it("marks an afternoon time PM", () => {
92
+ OneUptimeDate.setUserTimezone(UTC);
93
+
94
+ expect(format(AFTERNOON, { use12HourFormat: true })).toBe("2:30 PM");
95
+ });
96
+
97
+ it("marks a morning time AM", () => {
98
+ OneUptimeDate.setUserTimezone(UTC);
99
+
100
+ expect(
101
+ format(new Date("2024-03-01T09:05:00.000Z"), {
102
+ use12HourFormat: true,
103
+ }),
104
+ ).toBe("9:05 AM");
105
+ });
106
+
107
+ // The two readings a naive `hours % 12` gets wrong.
108
+ it("writes midnight as 12 AM rather than 0 AM", () => {
109
+ OneUptimeDate.setUserTimezone(UTC);
110
+
111
+ expect(
112
+ format(new Date("2024-03-01T00:00:00.000Z"), {
113
+ use12HourFormat: true,
114
+ }),
115
+ ).toBe("12:00 AM");
116
+ });
117
+
118
+ it("writes noon as 12 PM rather than 0 PM", () => {
119
+ OneUptimeDate.setUserTimezone(UTC);
120
+
121
+ expect(
122
+ format(new Date("2024-03-01T12:00:00.000Z"), {
123
+ use12HourFormat: true,
124
+ }),
125
+ ).toBe("12:00 PM");
126
+ });
127
+
128
+ it("keeps the marker after the seconds", () => {
129
+ OneUptimeDate.setUserTimezone(UTC);
130
+
131
+ expect(
132
+ format(AFTERNOON, { use12HourFormat: true, includeSeconds: true }),
133
+ ).toBe("2:30:45 PM");
134
+ });
135
+
136
+ it("keeps the marker on a bare hour", () => {
137
+ OneUptimeDate.setUserTimezone(UTC);
138
+
139
+ expect(
140
+ format(AFTERNOON, { use12HourFormat: true, includeMinutes: false }),
141
+ ).toBe("2 PM");
142
+ });
143
+ });
144
+
145
+ /*
146
+ * The browser here is in Los Angeles (see the docblock). None of the
147
+ * readings below are the Los Angeles ones, which is the point.
148
+ */
149
+ describe("reading the configured timezone rather than the browser's", () => {
150
+ it("shifts a whole-hour offset", () => {
151
+ OneUptimeDate.setUserTimezone(NEW_YORK);
152
+
153
+ expect(format(AFTERNOON)).toBe("09:30");
154
+ expect(format(AFTERNOON, { use12HourFormat: true })).toBe("9:30 AM");
155
+ });
156
+
157
+ it("shifts a half-hour offset", () => {
158
+ OneUptimeDate.setUserTimezone(KOLKATA);
159
+
160
+ expect(format(AFTERNOON)).toBe("20:00");
161
+ expect(format(AFTERNOON, { use12HourFormat: true })).toBe("8:00 PM");
162
+ });
163
+
164
+ it("crosses the date line into the next day", () => {
165
+ OneUptimeDate.setUserTimezone(KOLKATA);
166
+
167
+ // 23:00 UTC is already 04:30 the following morning in Kolkata.
168
+ expect(format(new Date("2024-03-01T23:00:00.000Z"))).toBe("04:30");
169
+ });
170
+
171
+ it("falls back to the browser zone when nothing is configured", () => {
172
+ OneUptimeDate.setUserTimezone(null);
173
+
174
+ // 14:30 UTC is 06:30 in Los Angeles on a March morning (PST, UTC-8).
175
+ expect(format(AFTERNOON)).toBe("06:30");
176
+ });
177
+ });
178
+ });
179
+
180
+ /*
181
+ * "Is this bucket today?" is what the histogram tooltips ask before deciding
182
+ * whether a timestamp needs its date spelled out. They used to ask it with
183
+ * `date.toDateString() === now.toDateString()`, which compares calendar days
184
+ * in the browser's zone - so within a few hours of midnight in the configured
185
+ * zone the date got dropped from buckets that needed it and added to buckets
186
+ * that did not.
187
+ */
188
+ describe("OneUptimeDate.areOnTheSameLocalDay", () => {
189
+ const BEFORE_MIDNIGHT_IN_KOLKATA: Date = new Date("2024-03-01T18:00:00.000Z");
190
+ const AFTER_MIDNIGHT_IN_KOLKATA: Date = new Date("2024-03-01T19:00:00.000Z");
191
+
192
+ it("separates two instants that straddle midnight in the configured zone", () => {
193
+ OneUptimeDate.setUserTimezone(KOLKATA);
194
+
195
+ /*
196
+ * 23:30 and 00:30 in Kolkata - two different days there, one hour apart.
197
+ * Both are still the same Friday morning in Los Angeles, which is exactly
198
+ * the disagreement the browser-zone comparison could not see.
199
+ */
200
+ expect(
201
+ OneUptimeDate.areOnTheSameLocalDay(
202
+ BEFORE_MIDNIGHT_IN_KOLKATA,
203
+ AFTER_MIDNIGHT_IN_KOLKATA,
204
+ ),
205
+ ).toBe(false);
206
+
207
+ expect(
208
+ OneUptimeDate.areOnTheSameDay(
209
+ BEFORE_MIDNIGHT_IN_KOLKATA,
210
+ AFTER_MIDNIGHT_IN_KOLKATA,
211
+ ),
212
+ ).toBe(true);
213
+ });
214
+
215
+ it("joins two instants the browser's zone splits across midnight", () => {
216
+ OneUptimeDate.setUserTimezone(KOLKATA);
217
+
218
+ // 07:30 and 21:30 on the same Kolkata day; Feb 29 and Mar 1 in Los Angeles.
219
+ const morning: Date = new Date("2024-03-01T02:00:00.000Z");
220
+ const evening: Date = new Date("2024-03-01T16:00:00.000Z");
221
+
222
+ expect(OneUptimeDate.areOnTheSameLocalDay(morning, evening)).toBe(true);
223
+ expect(OneUptimeDate.areOnTheSameDay(morning, evening)).toBe(false);
224
+ });
225
+
226
+ it("accepts the string form the histogram buckets arrive as", () => {
227
+ OneUptimeDate.setUserTimezone(UTC);
228
+
229
+ expect(
230
+ OneUptimeDate.areOnTheSameLocalDay(
231
+ "2024-03-01T00:30:00.000Z",
232
+ "2024-03-01T23:30:00.000Z",
233
+ ),
234
+ ).toBe(true);
235
+ });
236
+
237
+ it("falls back to the browser zone when nothing is configured", () => {
238
+ OneUptimeDate.setUserTimezone(null);
239
+
240
+ /*
241
+ * 16:00 and 20:00 UTC are 08:00 and 12:00 the same Los Angeles day, but
242
+ * two different UTC-relative readings - the browser's zone is the one
243
+ * that gets to answer when the user has configured none.
244
+ */
245
+ expect(
246
+ OneUptimeDate.areOnTheSameLocalDay(
247
+ new Date("2024-03-01T16:00:00.000Z"),
248
+ new Date("2024-03-01T20:00:00.000Z"),
249
+ ),
250
+ ).toBe(true);
251
+
252
+ expect(
253
+ OneUptimeDate.areOnTheSameLocalDay(
254
+ new Date("2024-03-01T02:00:00.000Z"),
255
+ new Date("2024-03-01T16:00:00.000Z"),
256
+ ),
257
+ ).toBe(false);
258
+ });
259
+ });
@@ -0,0 +1,316 @@
1
+ /**
2
+ * How OneUptime decides whether to write times on a 12- or a 24-hour clock.
3
+ *
4
+ * The answer has to come from the machine the person is sitting at: a browser
5
+ * resolves its default locale against the operating system's own clock setting,
6
+ * so toggling macOS's "24-Hour Time" (or the Windows/Linux equivalent) is meant
7
+ * to flip every time in the product.
8
+ *
9
+ * The bug these lock in: the check used to format a time and look for the
10
+ * letters "am"/"pm" in it. That is only how Latin-script locales mark the day
11
+ * period - ko-KR writes 오후, ar-EG writes م - so 12-hour machines in those
12
+ * locales were misread as 24-hour and got 24-hour times back. Intl reports the
13
+ * hour cycle directly, so it is asked first and the string probe is kept only
14
+ * as a fallback for an engine without a usable Intl.
15
+ */
16
+ import OneUptimeDate from "../../Types/Date";
17
+ import Timezone from "../../Types/Timezone";
18
+ import { afterEach, describe, expect, it, jest } from "@jest/globals";
19
+
20
+ type ResolvedOptionsStub = {
21
+ hour12?: boolean | undefined;
22
+ hourCycle?: string | undefined;
23
+ };
24
+
25
+ /*
26
+ * Stand in for the browser's resolved locale. `locales` records what the probe
27
+ * asked for so a test can assert it requested an hour at all - Intl only
28
+ * reports `hour12` when the format includes one, so a probe that forgot to ask
29
+ * would silently fall through to the string check on every engine.
30
+ */
31
+ type IntlProbeCall = {
32
+ locales: Intl.LocalesArgument;
33
+ options: Intl.DateTimeFormatOptions | undefined;
34
+ };
35
+
36
+ const probeCalls: Array<IntlProbeCall> = [];
37
+
38
+ type StubIntlFunction = (resolved: ResolvedOptionsStub | (() => never)) => void;
39
+
40
+ const stubIntl: StubIntlFunction = (
41
+ resolved: ResolvedOptionsStub | (() => never),
42
+ ): void => {
43
+ jest
44
+ .spyOn(Intl, "DateTimeFormat")
45
+ .mockImplementation(
46
+ (
47
+ locales?: Intl.LocalesArgument,
48
+ options?: Intl.DateTimeFormatOptions,
49
+ ) => {
50
+ probeCalls.push({ locales: locales, options: options });
51
+
52
+ if (typeof resolved === "function") {
53
+ resolved();
54
+ }
55
+
56
+ return {
57
+ resolvedOptions: () => {
58
+ return resolved as Intl.ResolvedDateTimeFormatOptions;
59
+ },
60
+ } as unknown as Intl.DateTimeFormat;
61
+ },
62
+ );
63
+ };
64
+
65
+ type StubFormattedTimeFunction = (formatted: string) => void;
66
+
67
+ const stubFormattedTime: StubFormattedTimeFunction = (
68
+ formatted: string,
69
+ ): void => {
70
+ jest.spyOn(Date.prototype, "toLocaleTimeString").mockImplementation(() => {
71
+ return formatted;
72
+ });
73
+ };
74
+
75
+ /*
76
+ * Delegate to the real Intl but pin the locale, so a case can assert what a
77
+ * machine actually configured to that locale reports rather than a hand-made
78
+ * stub of it.
79
+ */
80
+ type StubIntlWithLocaleFunction = (locale: string) => void;
81
+
82
+ const stubIntlWithLocale: StubIntlWithLocaleFunction = (
83
+ locale: string,
84
+ ): void => {
85
+ const realDateTimeFormat: typeof Intl.DateTimeFormat = Intl.DateTimeFormat;
86
+
87
+ jest
88
+ .spyOn(Intl, "DateTimeFormat")
89
+ .mockImplementation(
90
+ (
91
+ _locales?: Intl.LocalesArgument,
92
+ options?: Intl.DateTimeFormatOptions,
93
+ ) => {
94
+ return new realDateTimeFormat(locale, options);
95
+ },
96
+ );
97
+
98
+ /*
99
+ * Hold the legacy string probe to a bare 24-hour reading. Without this the
100
+ * fallback still sees the HOST's locale, so on a US runner the old am/pm
101
+ * substring check would answer "12-hour" by luck and the cases below would
102
+ * pass against the very implementation they exist to rule out.
103
+ */
104
+ stubFormattedTime("14:30:00");
105
+ };
106
+
107
+ describe("OneUptimeDate.getUserPrefers12HourFormat", () => {
108
+ afterEach(() => {
109
+ probeCalls.length = 0;
110
+ jest.restoreAllMocks();
111
+ OneUptimeDate.setUserTimezone(null);
112
+ });
113
+
114
+ describe("reading the machine's clock setting from Intl", () => {
115
+ it("reports a 12-hour machine as 12-hour", () => {
116
+ stubIntl({ hour12: true, hourCycle: "h12" });
117
+
118
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
119
+ });
120
+
121
+ it("reports a 24-hour machine as 24-hour", () => {
122
+ stubIntl({ hour12: false, hourCycle: "h23" });
123
+
124
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
125
+ });
126
+
127
+ it("asks for an hour, without which Intl reports no clock at all", () => {
128
+ stubIntl({ hour12: true });
129
+
130
+ OneUptimeDate.getUserPrefers12HourFormat();
131
+
132
+ expect(probeCalls).toHaveLength(1);
133
+ expect(probeCalls[0]?.options?.hour).toBeDefined();
134
+ });
135
+
136
+ it("asks for the machine's own locale rather than pinning one", () => {
137
+ stubIntl({ hour12: true });
138
+
139
+ OneUptimeDate.getUserPrefers12HourFormat();
140
+
141
+ /*
142
+ * Assert the probe ran before reading what it asked for - without this,
143
+ * an implementation that never called Intl would satisfy the line below
144
+ * on an empty array.
145
+ */
146
+ expect(probeCalls).toHaveLength(1);
147
+ // `undefined` is what makes Intl resolve the browser's default locale.
148
+ expect(probeCalls[0]?.locales).toBeUndefined();
149
+ });
150
+ });
151
+
152
+ describe("engines that report the cycle but not the boolean", () => {
153
+ it("treats h12 as a 12-hour clock", () => {
154
+ stubIntl({ hourCycle: "h12" });
155
+
156
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
157
+ });
158
+
159
+ it("treats h11 as a 12-hour clock", () => {
160
+ stubIntl({ hourCycle: "h11" });
161
+
162
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
163
+ });
164
+
165
+ it("treats h23 as a 24-hour clock", () => {
166
+ stubIntl({ hourCycle: "h23" });
167
+
168
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
169
+ });
170
+
171
+ it("treats h24 as a 24-hour clock", () => {
172
+ stubIntl({ hourCycle: "h24" });
173
+
174
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
175
+ });
176
+
177
+ it("prefers the boolean when both are reported and they disagree", () => {
178
+ /*
179
+ * hour12 is the value the spec says to honour; hourCycle is only read
180
+ * because older engines omit hour12 entirely.
181
+ */
182
+ stubIntl({ hour12: false, hourCycle: "h12" });
183
+
184
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
185
+ });
186
+ });
187
+
188
+ describe("real locales", () => {
189
+ it("reads en-US as a 12-hour machine", () => {
190
+ stubIntlWithLocale("en-US");
191
+
192
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
193
+ });
194
+
195
+ it("reads en-GB as a 24-hour machine", () => {
196
+ stubIntlWithLocale("en-GB");
197
+
198
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
199
+ });
200
+
201
+ it("reads de-DE as a 24-hour machine", () => {
202
+ stubIntlWithLocale("de-DE");
203
+
204
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
205
+ });
206
+
207
+ it("honours an explicit 24-hour override on a 12-hour locale", () => {
208
+ // What a US machine with "24-Hour Time" switched on resolves to.
209
+ stubIntlWithLocale("en-US-u-hc-h23");
210
+
211
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
212
+ });
213
+
214
+ it("honours an explicit 12-hour override on a 24-hour locale", () => {
215
+ stubIntlWithLocale("de-DE-u-hc-h12");
216
+
217
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
218
+ });
219
+
220
+ it("reads ko-KR as a 12-hour machine even though it writes 오후, not PM", () => {
221
+ /*
222
+ * The regression this whole change exists for: the old string probe saw
223
+ * "오후 2:30:00", found no "am"/"pm", and told a Korean user on a
224
+ * 12-hour machine they were on a 24-hour one.
225
+ */
226
+ stubIntlWithLocale("ko-KR");
227
+
228
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
229
+ });
230
+
231
+ it("reads ar-EG as a 12-hour machine even though it writes م, not PM", () => {
232
+ stubIntlWithLocale("ar-EG");
233
+
234
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
235
+ });
236
+ });
237
+
238
+ describe("falling back when Intl cannot answer", () => {
239
+ it("reads a Latin AM/PM out of a formatted time when Intl throws", () => {
240
+ stubIntl(() => {
241
+ throw new Error("Intl unavailable");
242
+ });
243
+ stubFormattedTime("2:30:00 PM");
244
+
245
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
246
+ });
247
+
248
+ it("reads a bare 24-hour time out of a formatted time when Intl throws", () => {
249
+ stubIntl(() => {
250
+ throw new Error("Intl unavailable");
251
+ });
252
+ stubFormattedTime("14:30:00");
253
+
254
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(false);
255
+ });
256
+
257
+ it("falls back when Intl answers with neither a boolean nor a cycle", () => {
258
+ stubIntl({});
259
+ stubFormattedTime("2:30:00 AM");
260
+
261
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
262
+ });
263
+
264
+ it("matches a lowercase day period", () => {
265
+ stubIntl({});
266
+ stubFormattedTime("2:30:00 pm");
267
+
268
+ expect(OneUptimeDate.getUserPrefers12HourFormat()).toBe(true);
269
+ });
270
+
271
+ it("does not throw when Intl is broken and the time has no day period", () => {
272
+ stubIntl(() => {
273
+ throw new Error("Intl unavailable");
274
+ });
275
+ stubFormattedTime("14:30:00");
276
+
277
+ expect(() => {
278
+ return OneUptimeDate.getUserPrefers12HourFormat();
279
+ }).not.toThrow();
280
+ });
281
+ });
282
+
283
+ describe("as a decision the rest of the product reads", () => {
284
+ it("drives the clock of the time range picker's custom label", () => {
285
+ /*
286
+ * The user-visible consequence, end to end: the same instant is written
287
+ * two different ways purely on the strength of this one answer.
288
+ */
289
+ /*
290
+ * Pin the zone: without it this reads 2:30 PM on a UTC runner and 6:30 AM
291
+ * on a Los Angeles one, and the assertion is about the clock, not the
292
+ * zone.
293
+ */
294
+ OneUptimeDate.setUserTimezone(Timezone.UTC);
295
+
296
+ stubIntlWithLocale("en-US");
297
+ const on12HourMachine: string =
298
+ OneUptimeDate.getDateAsLocalShortDateTimeString(
299
+ new Date("2024-03-01T14:30:00.000Z"),
300
+ { use12HourFormat: OneUptimeDate.getUserPrefers12HourFormat() },
301
+ );
302
+
303
+ jest.restoreAllMocks();
304
+
305
+ stubIntlWithLocale("en-GB");
306
+ const on24HourMachine: string =
307
+ OneUptimeDate.getDateAsLocalShortDateTimeString(
308
+ new Date("2024-03-01T14:30:00.000Z"),
309
+ { use12HourFormat: OneUptimeDate.getUserPrefers12HourFormat() },
310
+ );
311
+
312
+ expect(on12HourMachine).toBe("Mar 1, 2:30 PM");
313
+ expect(on24HourMachine).toBe("Mar 1, 14:30");
314
+ });
315
+ });
316
+ });
@@ -3,9 +3,10 @@
3
3
  import LogsHistogram from "../../../UI/Components/LogsViewer/components/LogsHistogram";
4
4
  import { HistogramBucket } from "../../../UI/Components/LogsViewer/types";
5
5
  import LogSeverity from "../../../Types/Log/LogSeverity";
6
+ import OneUptimeDate from "../../../Types/Date";
6
7
  import { render, screen } from "@testing-library/react";
7
8
  import React from "react";
8
- import { describe, expect, test } from "@jest/globals";
9
+ import { afterEach, describe, expect, test } from "@jest/globals";
9
10
 
10
11
  /*
11
12
  * ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so the
@@ -39,7 +40,27 @@ function seriesCount(container: HTMLElement): number {
39
40
  return container.querySelectorAll(".recharts-bar").length;
40
41
  }
41
42
 
42
- const CLOCK_LABEL: RegExp = /^\d{2}:\d{2}$/;
43
+ /*
44
+ * Matches a tick on either clock. The axis follows the machine's 12/24-hour
45
+ * preference now, so a pattern that only knew "HH:mm" would quietly classify
46
+ * every tick as a count label on an AM/PM machine and leave the assertions
47
+ * below comparing empty arrays.
48
+ */
49
+ const CLOCK_LABEL: RegExp = /^\d{1,2}:\d{2}( [AP]M)?$/;
50
+
51
+ /*
52
+ * The tick labels are the machine's business, not the test's, so every case
53
+ * that reads them says which clock it is asking about.
54
+ */
55
+ function pinClock(use12HourFormat: boolean): void {
56
+ jest
57
+ .spyOn(OneUptimeDate, "getUserPrefers12HourFormat")
58
+ .mockReturnValue(use12HourFormat);
59
+ }
60
+
61
+ afterEach(() => {
62
+ jest.restoreAllMocks();
63
+ });
43
64
 
44
65
  /*
45
66
  * Recharts portals tick labels out of their axis group, so the two axes are
@@ -126,6 +147,8 @@ describe("LogsHistogram", () => {
126
147
  });
127
148
 
128
149
  test("labels the time axis in the reader's clock", () => {
150
+ pinClock(false);
151
+
129
152
  const { container } = render(
130
153
  <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
131
154
  );
@@ -133,6 +156,20 @@ describe("LogsHistogram", () => {
133
156
  expect(axisLabels(container, "time")).toContain("12:00");
134
157
  });
135
158
 
159
+ /*
160
+ * Noon is where the two clocks are hardest to tell apart - "12:00" is a
161
+ * valid reading on both - so the marker is the whole assertion.
162
+ */
163
+ test("labels the time axis with AM/PM when that is the reader's clock", () => {
164
+ pinClock(true);
165
+
166
+ const { container } = render(
167
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
168
+ );
169
+
170
+ expect(axisLabels(container, "time")).toContain("12:00 PM");
171
+ });
172
+
136
173
  test("lists only the severities present in the window", () => {
137
174
  render(
138
175
  <LogsHistogram
@@ -176,6 +213,8 @@ describe("LogsHistogram", () => {
176
213
  });
177
214
 
178
215
  test("follows the window as it slides off the oldest bucket", () => {
216
+ pinClock(false);
217
+
179
218
  const view: ReturnType<typeof render> = render(
180
219
  <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
181
220
  );