@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
@@ -9,7 +9,9 @@ import MonitorStep from "../../../Types/Monitor/MonitorStep";
9
9
  import MonitorSteps from "../../../Types/Monitor/MonitorSteps";
10
10
  import MonitorType from "../../../Types/Monitor/MonitorType";
11
11
  import ObjectID from "../../../Types/ObjectID";
12
- import NetworkDeviceMonitorTemplateUtil from "../../../Utils/Monitor/NetworkDeviceMonitorTemplateUtil";
12
+ import NetworkDeviceMonitorTemplateUtil, {
13
+ MAX_PROVISIONED_MONITOR_NAME_LENGTH,
14
+ } from "../../../Utils/Monitor/NetworkDeviceMonitorTemplateUtil";
13
15
  import { describe, expect, it } from "@jest/globals";
14
16
 
15
17
  const PROJECT_ID: ObjectID = new ObjectID(
@@ -290,36 +292,202 @@ describe("NetworkDeviceMonitorTemplateUtil.buildMonitor", () => {
290
292
  networkDevice: buildDevice({ name: " ", hostname: " " }),
291
293
  });
292
294
 
293
- expect(monitor.name).toBe(
294
- `Network Device ${DEVICE_ID.toString()} - Monitor`,
295
- );
295
+ expect(monitor.name).toBe(`Network Device ${DEVICE_ID.toString()}`);
296
296
  expect(monitor.name?.trim()).not.toBe("");
297
297
  });
298
298
 
299
- it("caps names at ShortText without leaving half a surrogate pair", () => {
299
+ /*
300
+ * ISSUE #3486, and the reason the column became optional.
301
+ *
302
+ * An auto-import rule names every monitor it provisions after the device.
303
+ * While the template's default monitor name was required there was nothing
304
+ * an operator could type that did not become a suffix on the whole imported
305
+ * estate - "UN0660WANRTR01 - Unit Router" for a router already called
306
+ * UN0660WANRTR01. The three tests below pin the three ways a template can
307
+ * say "no suffix", because all three are reachable: the column is nullable,
308
+ * the dashboard's edit form PUTs an empty string rather than null when the
309
+ * box is cleared, and the API accepts whitespace.
310
+ */
311
+ it("names the monitor after the device alone when the template has no default name", () => {
312
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
313
+ template: buildTemplate({ monitorName: undefined }),
314
+ networkDevice: buildDevice({ name: "UN0660WANRTR01" }),
315
+ });
316
+
317
+ expect(monitor.name).toBe("UN0660WANRTR01");
318
+ expect(monitor.name).not.toContain(" - ");
319
+ });
320
+
321
+ it("treats an empty default monitor name as no default name", () => {
322
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
323
+ template: buildTemplate({ monitorName: "" }),
324
+ networkDevice: buildDevice({ name: "UN0660WANRTR01" }),
325
+ });
326
+
327
+ expect(monitor.name).toBe("UN0660WANRTR01");
328
+ });
329
+
330
+ it("treats a whitespace-only default monitor name as no default name", () => {
300
331
  const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
301
- template: buildTemplate({ monitorName: "🔥 alerting" }),
332
+ template: buildTemplate({ monitorName: " " }),
333
+ networkDevice: buildDevice({ name: "UN0660WANRTR01" }),
334
+ });
335
+
336
+ expect(monitor.name).toBe("UN0660WANRTR01");
337
+ });
338
+
339
+ it("falls back to the hostname, not to a suffix, for an unnamed device on an unnamed template", () => {
340
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
341
+ template: buildTemplate({ monitorName: undefined }),
342
+ networkDevice: buildDevice({ name: " ", hostname: "10.20.30.40" }),
343
+ });
344
+
345
+ expect(monitor.name).toBe("10.20.30.40");
346
+ });
347
+
348
+ /*
349
+ * The other half of the contract. Dropping the suffix when it IS asked for
350
+ * would make two templates provisioning the same device produce two
351
+ * identically named monitors, which is the thing the suffix is for.
352
+ */
353
+ it("keeps the suffix when the template does carry a default name", () => {
354
+ const device: NetworkDevice = buildDevice({ name: "UN0660WANRTR01" });
355
+
356
+ const named: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
357
+ template: buildTemplate({ monitorName: "Unit Router" }),
358
+ networkDevice: device,
359
+ });
360
+ const otherNamed: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
361
+ template: buildTemplate({ monitorName: "Interface Health" }),
362
+ networkDevice: device,
363
+ });
364
+
365
+ expect(named.name).toBe("UN0660WANRTR01 - Unit Router");
366
+ expect(otherNamed.name).toBe("UN0660WANRTR01 - Interface Health");
367
+ expect(named.name).not.toBe(otherNamed.name);
368
+ });
369
+
370
+ it("trims the stored default monitor name before composing", () => {
371
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
372
+ template: buildTemplate({ monitorName: " Unit Router " }),
373
+ networkDevice: buildDevice({ name: "UN0660WANRTR01" }),
374
+ });
375
+
376
+ expect(monitor.name).toBe("UN0660WANRTR01 - Unit Router");
377
+ });
378
+
379
+ /*
380
+ * THE CAP IS THE SLUG'S, NOT THE NAME COLUMN'S.
381
+ *
382
+ * Monitor.name is varchar(100), but Monitor is @SlugifyColumn("name",
383
+ * "slug") and Slug.getSlug appends a dash plus ten random digits into its
384
+ * own varchar(100). A 100-character name therefore produces a
385
+ * 111-character slug and the INSERT throws - the monitor lands in
386
+ * `monitorsFailed` and the device silently never gets one. 88 leaves
387
+ * exactly that 11-character tail.
388
+ */
389
+ it("caps a provisioned name low enough that its generated slug still fits", () => {
390
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
391
+ template: buildTemplate({ monitorName: "t".repeat(60) }),
302
392
  networkDevice: buildDevice({ name: "d".repeat(96) }),
303
393
  });
394
+ const slugTailLength: number = "-1234567890".length;
395
+
396
+ expect(monitor.name).toHaveLength(MAX_PROVISIONED_MONITOR_NAME_LENGTH);
397
+ expect(monitor.name!.length + slugTailLength).toBeLessThanOrEqual(
398
+ ColumnLength.Slug,
399
+ );
400
+ expect(monitor.name!.length).toBeLessThanOrEqual(ColumnLength.ShortText);
401
+ });
402
+
403
+ it("caps an unsuffixed device name at the same ceiling", () => {
404
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
405
+ template: buildTemplate({ monitorName: undefined }),
406
+ networkDevice: buildDevice({ name: "d".repeat(120) }),
407
+ });
408
+
409
+ expect(monitor.name).toBe("d".repeat(MAX_PROVISIONED_MONITOR_NAME_LENGTH));
410
+ });
411
+
412
+ it("caps names without leaving half a surrogate pair", () => {
413
+ /*
414
+ * 80 + " - " + 4 characters puts the cap exactly on the leading half of
415
+ * the astral character, the only cut that can produce an unpaired code
416
+ * unit.
417
+ */
418
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
419
+ template: buildTemplate({ monitorName: "tttt🔥 alerting" }),
420
+ networkDevice: buildDevice({ name: "d".repeat(80) }),
421
+ });
304
422
  const loneSurrogate: RegExp =
305
423
  /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
306
424
 
307
- expect(monitor.name!.length).toBeLessThanOrEqual(ColumnLength.ShortText);
308
- expect(monitor.name!.length).toBe(99);
425
+ expect(monitor.name!.length).toBeLessThanOrEqual(
426
+ MAX_PROVISIONED_MONITOR_NAME_LENGTH,
427
+ );
428
+ expect(monitor.name).toBe(`${"d".repeat(80)} - tttt`);
309
429
  expect(loneSurrogate.test(monitor.name!)).toBe(false);
310
- expect(monitor.name!.startsWith("d".repeat(96))).toBe(true);
311
430
  });
312
431
 
313
- it("keeps a composed name exactly at the column limit", () => {
314
- const deviceName: string = "d".repeat(80);
315
- const templateName: string = "t".repeat(17);
432
+ it("keeps an astral character in the device identity whole", () => {
433
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
434
+ template: buildTemplate(),
435
+ networkDevice: buildDevice({ name: `${"d".repeat(87)}🔥` }),
436
+ });
437
+ const loneSurrogate: RegExp =
438
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
439
+
440
+ expect(monitor.name).toBe("d".repeat(87));
441
+ expect(loneSurrogate.test(monitor.name!)).toBe(false);
442
+ });
443
+
444
+ it("keeps a composed name exactly at the cap", () => {
445
+ const deviceName: string = "d".repeat(70);
446
+ const templateName: string = "t".repeat(15);
316
447
  const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
317
448
  template: buildTemplate({ monitorName: templateName }),
318
449
  networkDevice: buildDevice({ name: deviceName }),
319
450
  });
320
451
 
321
452
  expect(monitor.name).toBe(`${deviceName} - ${templateName}`);
322
- expect(monitor.name).toHaveLength(ColumnLength.ShortText);
453
+ expect(monitor.name).toHaveLength(MAX_PROVISIONED_MONITOR_NAME_LENGTH);
454
+ });
455
+
456
+ /*
457
+ * A cap landing inside " - " would otherwise ship a name trailing a
458
+ * half-written separator - "core switch -" - which reads as a truncation
459
+ * bug to whoever opens the monitor.
460
+ */
461
+ it("drops a separator the cap cut through", () => {
462
+ // One character into " - ", two characters in, and exactly at its end.
463
+ for (const deviceNameLength of [87, 86, 85]) {
464
+ const deviceName: string = "d".repeat(deviceNameLength);
465
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
466
+ template: buildTemplate({ monitorName: "Unit Router" }),
467
+ networkDevice: buildDevice({ name: deviceName }),
468
+ });
469
+
470
+ expect({
471
+ deviceNameLength: deviceNameLength,
472
+ name: monitor.name,
473
+ }).toEqual({ deviceNameLength: deviceNameLength, name: deviceName });
474
+ expect(monitor.name!.endsWith(" ")).toBe(false);
475
+ expect(monitor.name!.endsWith("-")).toBe(false);
476
+ }
477
+ });
478
+
479
+ /*
480
+ * The mirror of the test above: the remnant rule is a LENGTH check, not a
481
+ * "does it end with a dash" check, so a device whose own name ends in a
482
+ * dash keeps it when nothing was cut.
483
+ */
484
+ it("keeps a device's own trailing dash when nothing was truncated", () => {
485
+ const monitor: Monitor = NetworkDeviceMonitorTemplateUtil.buildMonitor({
486
+ template: buildTemplate({ monitorName: undefined }),
487
+ networkDevice: buildDevice({ name: "Router A -" }),
488
+ });
489
+
490
+ expect(monitor.name).toBe("Router A -");
323
491
  });
324
492
 
325
493
  it("rejects a template without an id", () => {
@@ -5,16 +5,27 @@ import { JSONObject, ObjectType } from "../JSON";
5
5
  import { FindOperator } from "typeorm";
6
6
 
7
7
  export default class Route extends DatabaseProperty {
8
+ private static readonly SCHEME_PREFIX: RegExp = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
9
+
10
+ private static validateRoute(route: string): void {
11
+ if (Route.SCHEME_PREFIX.test(route)) {
12
+ throw new BadDataException(`Invalid route: ${route}`);
13
+ }
14
+
15
+ const matchRouteCharacters: RegExp =
16
+ /^[a-zA-Z_\d\-!#$%&'()*+,./:;=?@[\]]*$/;
17
+
18
+ if (route && !matchRouteCharacters.test(route)) {
19
+ throw new BadDataException(`Invalid route: ${route}`);
20
+ }
21
+ }
22
+
8
23
  private _route: string = "";
9
24
  public get route(): string {
10
25
  return this._route;
11
26
  }
12
27
  public set route(v: string) {
13
- const matchRouteCharacters: RegExp =
14
- /^[a-zA-Z_\d\-!#$%&'()*+,./:;=?@[\]]*$/;
15
- if (v && !matchRouteCharacters.test(v)) {
16
- throw new BadDataException(`Invalid route: ${v}`);
17
- }
28
+ Route.validateRoute(v);
18
29
  this._route = v;
19
30
  }
20
31
 
@@ -24,6 +35,10 @@ export default class Route extends DatabaseProperty {
24
35
  route = route.toString();
25
36
  }
26
37
 
38
+ if (route) {
39
+ Route.validateRoute(route);
40
+ }
41
+
27
42
  route = route?.replace(/\/+/g, "/"); // remove multiple slashes from route and replace with single slash
28
43
 
29
44
  if (route) {
package/Types/Date.ts CHANGED
@@ -194,32 +194,48 @@ export default class OneUptimeDate {
194
194
  return this.getLocalShortMonthName(date);
195
195
  }
196
196
 
197
+ /**
198
+ * The wall clock `date` reads at in the current timezone - "14:30", or
199
+ * "2:30 PM" when the caller asks for a 12-hour clock.
200
+ *
201
+ * `use12HourFormat` is opt-in rather than defaulted to
202
+ * `getUserPrefers12HourFormat()`: the chart x-axes in
203
+ * UI/Components/Charts/Utils/XAxis.ts label ticks with this and are laid out
204
+ * for the fixed width of a 24-hour reading, so they must keep the clock they
205
+ * were built against. Surfaces that show a person a timestamp - histogram
206
+ * ticks and tooltips, span rows - pass
207
+ * `use12HourFormat: OneUptimeDate.getUserPrefers12HourFormat()` explicitly.
208
+ */
197
209
  public static getLocalTimeString(
198
210
  date: Date | string,
199
211
  options?: {
200
212
  includeMinutes?: boolean;
201
213
  includeSeconds?: boolean;
214
+ use12HourFormat?: boolean | undefined;
202
215
  },
203
216
  ): string {
204
217
  date = this.fromString(date);
205
218
 
206
219
  const includeMinutes: boolean = options?.includeMinutes ?? true;
207
220
  const includeSeconds: boolean = options?.includeSeconds ?? false;
208
- const localDate: moment.Moment = this.inCurrentTimezone(date);
209
- const hours: string = this.padDatePart(localDate.hours());
221
+ const use12HourFormat: boolean = options?.use12HourFormat ?? false;
210
222
 
211
- if (!includeMinutes) {
212
- return hours;
213
- }
223
+ // "HH" and "h" are the zero-padded 24-hour and the bare 12-hour readings.
224
+ let timeFormat: string = use12HourFormat ? "h" : "HH";
214
225
 
215
- const minutes: string = this.padDatePart(localDate.minutes());
226
+ if (includeMinutes) {
227
+ timeFormat += ":mm";
228
+
229
+ if (includeSeconds) {
230
+ timeFormat += ":ss";
231
+ }
232
+ }
216
233
 
217
- if (!includeSeconds) {
218
- return `${hours}:${minutes}`;
234
+ if (use12HourFormat) {
235
+ timeFormat += " A";
219
236
  }
220
237
 
221
- const seconds: string = this.padDatePart(localDate.seconds());
222
- return `${hours}:${minutes}:${seconds}`;
238
+ return this.inCurrentTimezone(date).format(timeFormat);
223
239
  }
224
240
 
225
241
  public static getDateAsLocalDayMonthString(date: Date | string): string {
@@ -237,6 +253,41 @@ export default class OneUptimeDate {
237
253
  return `${this.getDateAsLocalDayMonthString(date)}, ${this.getLocalTimeString(date, { includeMinutes: false })}:00`;
238
254
  }
239
255
 
256
+ /**
257
+ * A compact "Mar 1, 14:30" (or "Mar 1, 2:30 PM") label for one instant, used
258
+ * where a whole window has to fit on a button - the time range picker above
259
+ * the metrics, traces and logs explorers, chiefly.
260
+ *
261
+ * Both halves follow the user's machine: the wall clock is resolved in the
262
+ * configured timezone (falling back to the one the browser reports), and the
263
+ * 12- vs 24-hour choice follows the operating system's clock preference
264
+ * unless a caller overrides it.
265
+ */
266
+ public static getDateAsLocalShortDateTimeString(
267
+ date: Date | string,
268
+ options?: {
269
+ use12HourFormat?: boolean | undefined;
270
+ includeSeconds?: boolean | undefined;
271
+ },
272
+ ): string {
273
+ date = this.fromString(date);
274
+
275
+ const use12HourFormat: boolean =
276
+ options?.use12HourFormat ?? this.getUserPrefers12HourFormat();
277
+
278
+ let timeFormat: string = use12HourFormat ? "h:mm" : "HH:mm";
279
+
280
+ if (options?.includeSeconds) {
281
+ timeFormat += ":ss";
282
+ }
283
+
284
+ if (use12HourFormat) {
285
+ timeFormat += " A";
286
+ }
287
+
288
+ return this.inCurrentTimezone(date).format(`MMM D, ${timeFormat}`);
289
+ }
290
+
240
291
  public static getDateAsLocalMonthYearString(date: Date | string): string {
241
292
  date = this.fromString(date);
242
293
 
@@ -857,6 +908,25 @@ export default class OneUptimeDate {
857
908
  return moment(date1).isSame(date2, "day");
858
909
  }
859
910
 
911
+ /**
912
+ * Whether the two instants fall on the same calendar day *in the current
913
+ * timezone*, which is the question a UI is asking when it decides whether a
914
+ * timestamp needs its date spelled out or a bare clock reading will do.
915
+ *
916
+ * `areOnTheSameDay` above answers it in the zone the process happens to run
917
+ * in. Near midnight those two zones disagree, so asking the wrong one drops
918
+ * the date from yesterday's buckets and adds it to today's.
919
+ */
920
+ public static areOnTheSameLocalDay(
921
+ date1: Date | string,
922
+ date2: Date | string,
923
+ ): boolean {
924
+ return this.inCurrentTimezone(date1).isSame(
925
+ this.inCurrentTimezone(date2),
926
+ "day",
927
+ );
928
+ }
929
+
860
930
  public static areOnTheSameMonth(date1: Date, date2: Date): boolean {
861
931
  date1 = this.fromString(date1);
862
932
  date2 = this.fromString(date2);
@@ -1465,15 +1535,75 @@ export default class OneUptimeDate {
1465
1535
  return this.getDateAsFormattedString(new Date(), options);
1466
1536
  }
1467
1537
 
1538
+ /**
1539
+ * Whether the machine this is running on writes the time of day on a 12-hour
1540
+ * clock, read from the browser's resolved default locale - which browsers
1541
+ * derive from the operating system's language and region, so changing those
1542
+ * changes this. An explicit -u-hc- override on the locale is honoured too.
1543
+ *
1544
+ * Note what this cannot see: the standalone "24-Hour Time" switch macOS and
1545
+ * Windows offer separately from the region. Safari and recent Firefox fold
1546
+ * that into the locale they resolve; Chromium does not. A user who wants a
1547
+ * clock that ignores their region would need an explicit preference stored
1548
+ * alongside the timezone in User Settings - there is none today.
1549
+ *
1550
+ * Asked of Intl rather than inferred from a formatted string. The string
1551
+ * probe below only recognises the Latin "AM"/"PM", so it misread every
1552
+ * 12-hour locale that marks the day period some other way - ko-KR writes
1553
+ * the equivalent of "PM" in Hangul, ar-EG in Arabic script - and served
1554
+ * those users a 24-hour clock against their own convention.
1555
+ */
1468
1556
  public static getUserPrefers12HourFormat(): boolean {
1469
1557
  if (typeof window === "undefined") {
1470
1558
  // Server-side: default to 12-hour format for user-friendly display
1471
1559
  return true;
1472
1560
  }
1473
1561
 
1474
- // Client-side: detect user's preferred time format from browser locale
1475
- const testDate: Date = new Date();
1476
- const timeString: string = testDate.toLocaleTimeString();
1562
+ /*
1563
+ * `hour12` is only reported when the format actually asks for an hour, so
1564
+ * the probe has to request one.
1565
+ */
1566
+ try {
1567
+ /*
1568
+ * Typed structurally rather than as Intl.ResolvedDateTimeFormatOptions:
1569
+ * `hourCycle` only appears on that interface from the ES2021 lib, and
1570
+ * this file has to compile against the older one too.
1571
+ */
1572
+ const resolvedOptions: {
1573
+ hour12?: boolean | undefined;
1574
+ hourCycle?: string | undefined;
1575
+ } = new Intl.DateTimeFormat(undefined, {
1576
+ hour: "numeric",
1577
+ }).resolvedOptions();
1578
+
1579
+ if (typeof resolvedOptions.hour12 === "boolean") {
1580
+ return resolvedOptions.hour12;
1581
+ }
1582
+
1583
+ /*
1584
+ * Older engines report the cycle but not the boolean. h11/h12 are the
1585
+ * two 12-hour cycles; h23/h24 are the 24-hour ones.
1586
+ */
1587
+ if (resolvedOptions.hourCycle) {
1588
+ return (
1589
+ resolvedOptions.hourCycle === "h11" ||
1590
+ resolvedOptions.hourCycle === "h12"
1591
+ );
1592
+ }
1593
+ } catch {
1594
+ /*
1595
+ * A missing or broken Intl must not take every timestamp in the UI down
1596
+ * with it - fall through to the string probe below.
1597
+ */
1598
+ }
1599
+
1600
+ /*
1601
+ * Last resort: read a formatted time and look for a day-period marker.
1602
+ * This only recognises the Latin "AM"/"PM" - which is exactly why it is the
1603
+ * fallback and not the primary check, since locales like ko-KR and ar-EG
1604
+ * are 12-hour but mark the day period with characters this cannot see.
1605
+ */
1606
+ const timeString: string = new Date().toLocaleTimeString();
1477
1607
  return (
1478
1608
  timeString.toLowerCase().includes("am") ||
1479
1609
  timeString.toLowerCase().includes("pm")
@@ -296,6 +296,20 @@ export interface SessionReplayChunkEnvelope {
296
296
  /* Already scrubbed client-side: origin + path, no query, no fragment. */
297
297
  url: string;
298
298
 
299
+ /*
300
+ * Distinct scrubbed URLs the page was on while this chunk was open, in
301
+ * chronological order. Optional so an older recorder posting to a newer
302
+ * server is still accepted - the server then falls back to `url` alone,
303
+ * which is what it did before this field existed.
304
+ *
305
+ * `url` says where the chunk was FLUSHED from; this says where the user
306
+ * went while it was filling. The session header's routes[] column is the
307
+ * union of these, and the "sessions that hit /checkout" filter reads it,
308
+ * so without this a navigation that both starts and ends inside one flush
309
+ * window is invisible.
310
+ */
311
+ routes?: Array<string>;
312
+
299
313
  signals: SessionReplaySignalCounts;
300
314
 
301
315
  fidelityNotices: Array<string>;
@@ -313,10 +327,52 @@ export interface SessionReplayChunkEnvelope {
313
327
  traceIds?: Array<string>;
314
328
  }
315
329
 
330
+ /*
331
+ * WHY a config response says enabled:false.
332
+ *
333
+ * The disabled response is deliberately a complete, well-formed config
334
+ * rather than an error, so a recorder that cannot parse a config still
335
+ * refuses to record. The cost of that was one answer for five very
336
+ * different causes: from a browser, an instance-wide kill switch, a
337
+ * deployment whose recorder bundle was never built, an application
338
+ * somebody switched off and a Redis outage were all "enabled: false" and
339
+ * nothing else, and no amount of dashboard configuration fixes three of
340
+ * them.
341
+ *
342
+ * This is a closed vocabulary carrying no project data - it is answered
343
+ * only to a request already bearing a valid ingestion key for that
344
+ * project, and it says strictly less than the Dashboard's own ingest
345
+ * status endpoint already shows the same customer.
346
+ */
347
+ export enum SessionReplayDisabledReason {
348
+ /* SESSION_REPLAY_INGEST_ENABLED=false on this deployment. */
349
+ IngestDisabled = "ingest-disabled",
350
+
351
+ /* SESSION_REPLAY_ENABLED_BY_DEFAULT=false on this deployment. */
352
+ DisabledByDefault = "disabled-by-default",
353
+
354
+ /*
355
+ * No published recorder artifact. The build never ran, or its output is
356
+ * not where the server looks for it - overwhelmingly the answer on a
357
+ * self-hosted install where replay has never worked at all.
358
+ */
359
+ RecorderNotBuilt = "recorder-not-built",
360
+
361
+ /* The policy lookup threw. Fail closed, and say that it failed. */
362
+ PolicyUnavailable = "policy-unavailable",
363
+
364
+ /*
365
+ * No policy for this project/application pair: the application is
366
+ * switched off, the project is not allowed replay, the identifier does
367
+ * not match one, or a kill key is set.
368
+ */
369
+ NotEnabledForApplication = "not-enabled-for-application",
370
+ }
371
+
316
372
  /*
317
373
  * Policy snapshot handed to the recorder at startup. This endpoint is
318
374
  * what makes every server-side privacy control reachable by a live
319
- * recorder without it, flipping a masking setting would never take
375
+ * recorder - without it, flipping a masking setting would never take
320
376
  * effect in a browser that already loaded.
321
377
  */
322
378
  export interface SessionReplayConfigResponse {
@@ -401,6 +457,25 @@ export interface SessionReplayConfigResponse {
401
457
  configEpoch: number;
402
458
 
403
459
  directive: SessionReplayDirective;
460
+
461
+ /*
462
+ * Set only alongside enabled:false. Optional on the wire like every
463
+ * other field added after v1: an older recorder ignores it, a newer one
464
+ * logs it, and neither breaks.
465
+ */
466
+ disabledReason?: SessionReplayDisabledReason;
467
+
468
+ /*
469
+ * Ask every recorder that reads this policy to print its decisions to
470
+ * the browser console (SESSION_REPLAY_DEBUG on the deployment).
471
+ *
472
+ * The switch of last resort, and the only one an operator can reach
473
+ * without editing a page they do not own: the per-browser localStorage
474
+ * and query-string switches need somebody at that browser. It is off by
475
+ * default and must stay that way - a RUM script logging on every visitor
476
+ * of every customer is noise on somebody else's site.
477
+ */
478
+ debug?: boolean;
404
479
  }
405
480
 
406
481
  /* Response to a chunk POST. Deliberately tiny. */
@@ -7,6 +7,7 @@ import React, {
7
7
  useState,
8
8
  } from "react";
9
9
  import InBetween from "../../../Types/BaseDatabase/InBetween";
10
+ import OneUptimeDate from "../../../Types/Date";
10
11
  import IconProp from "../../../Types/Icon/IconProp";
11
12
  import RangeStartAndEndDateTime, {
12
13
  RangeStartAndEndDateTimeUtil,
@@ -54,12 +55,15 @@ export const TIME_RANGE_PRESET_OPTIONS: Array<TimeRangePickerPresetOption> = [
54
55
  { range: TimeRange.PAST_THREE_MONTHS, label: "Past 3 Months" },
55
56
  ];
56
57
 
58
+ /*
59
+ * Both ends of a custom window are written the way the user's machine writes
60
+ * times: on its 12- or 24-hour clock, in the timezone they configured. Reading
61
+ * the digits off the Date itself - which is what this used to do - pinned the
62
+ * label to a 24-hour clock in whatever zone the browser process happened to be
63
+ * in, so a machine set to AM/PM still got "Mar 1, 14:30" back.
64
+ */
57
65
  function formatDateShort(date: Date): string {
58
- const month: string = date.toLocaleString("en-US", { month: "short" });
59
- const day: number = date.getDate();
60
- const hours: string = date.getHours().toString().padStart(2, "0");
61
- const minutes: string = date.getMinutes().toString().padStart(2, "0");
62
- return `${month} ${day}, ${hours}:${minutes}`;
66
+ return OneUptimeDate.getDateAsLocalShortDateTimeString(date);
63
67
  }
64
68
 
65
69
  export function getTimeRangeButtonLabel(
@@ -175,7 +175,11 @@ const severityWeight: Record<string, number> = {
175
175
  trace: 1,
176
176
  };
177
177
 
178
- function getEmptyMessageWithTimeRange(
178
+ /*
179
+ * Exported so the window it reports can be unit-tested directly - rendering the
180
+ * whole viewer to read one sentence is not worth the setup.
181
+ */
182
+ export function getEmptyMessageWithTimeRange(
179
183
  timeRange: RangeStartAndEndDateTime | undefined,
180
184
  ): string {
181
185
  if (!timeRange) {
@@ -185,13 +189,14 @@ function getEmptyMessageWithTimeRange(
185
189
  if (timeRange.range === TimeRange.CUSTOM && timeRange.startAndEndDate) {
186
190
  const startDate: Date = timeRange.startAndEndDate.startValue;
187
191
  const endDate: Date = timeRange.startAndEndDate.endValue;
192
+ /*
193
+ * Same shape, clock and timezone as the label on the picker button that
194
+ * set this window - pinning en-US here handed a US viewer AM/PM even on a
195
+ * 24-hour machine, and read the digits in the browser's zone rather than
196
+ * the one configured in User Settings.
197
+ */
188
198
  const fmt: (d: Date) => string = (d: Date): string => {
189
- return d.toLocaleString("en-US", {
190
- month: "short",
191
- day: "numeric",
192
- hour: "2-digit",
193
- minute: "2-digit",
194
- });
199
+ return OneUptimeDate.getDateAsLocalShortDateTimeString(d);
195
200
  };
196
201
 
197
202
  return `Time range: ${fmt(startDate)} – ${fmt(endDate)}. Try adjusting filters or expanding the time range.`;