@oneuptime/common 13.0.8 → 14.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/AuditLog.ts +23 -15
- package/Models/DatabaseModels/GlobalConfig.ts +24 -0
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/Monitor.ts +3 -2
- package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
- package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
- package/Scripts/generate-service-worker.js +65 -4
- package/Server/API/BaseAPI.ts +26 -5
- package/Server/API/GlobalConfigAPI.ts +228 -462
- package/Server/API/ProjectOIDC.ts +25 -16
- package/Server/API/ProjectSSO.ts +24 -16
- package/Server/API/StatusPageAPI.ts +100 -42
- package/Server/API/TeamMemberAPI.ts +10 -1
- package/Server/Enterprise/EnterpriseEdition.ts +649 -0
- package/Server/Enterprise/EnterpriseFeature.ts +48 -0
- package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
- package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
- package/Server/EnvironmentConfig.ts +147 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/UserAuthorization.ts +38 -16
- package/Server/Services/AuditLogService.ts +84 -1079
- package/Server/Services/DatabaseService.ts +10 -0
- package/Server/Services/EnterpriseLicenseService.ts +38 -0
- package/Server/Services/GlobalConfigService.ts +80 -1
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/MonitorProbeService.ts +116 -20
- package/Server/Services/MonitorService.ts +59 -6
- package/Server/Services/MonitorStatusTimelineService.ts +284 -0
- package/Server/Services/MonitorTemplateService.ts +26 -0
- package/Server/Services/ProjectService.ts +284 -0
- package/Server/Services/RumSessionPinService.ts +25 -6
- package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
- package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
- package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
- package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
- package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
- package/Server/Services/StatusPagePrivateUserService.ts +7 -3
- package/Server/Services/StatusPageService.ts +78 -1
- package/Server/Services/TeamMemberService.ts +11 -0
- package/Server/Services/TeamService.ts +10 -0
- package/Server/Services/UserService.ts +10 -17
- package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
- package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
- package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
- package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
- package/Server/Types/Markdown.ts +41 -0
- package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
- package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
- package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
- package/Server/Utils/EditionEnforcement.ts +233 -0
- package/Server/Utils/FrontendEnvironment.ts +41 -2
- package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
- package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
- package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
- package/Server/Utils/Monitor/RootCauseList.ts +139 -0
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
- package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
- package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
- package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
- package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
- package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
- package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
- package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
- package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
- package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
- package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
- package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
- package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
- package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
- package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
- package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
- package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
- package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
- package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
- package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
- package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
- package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
- package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
- package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
- package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
- package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
- package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
- package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
- package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
- package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
- package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
- package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
- package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
- package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
- package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
- package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
- package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
- package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
- package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
- package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
- package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
- package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
- package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
- package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
- package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
- package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
- package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
- package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
- package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
- package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
- package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
- package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
- package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
- package/Tests/Server/Services/AuditLogService.test.ts +460 -990
- package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
- package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
- package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
- package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
- package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
- package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
- package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
- package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
- package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
- package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
- package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
- package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
- package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
- package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
- package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
- package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
- package/Tests/Server/Types/Markdown.test.ts +183 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
- package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
- package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
- package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
- package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
- package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
- package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
- package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
- package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
- package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
- package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
- package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
- package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
- package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
- package/Tests/Types/API/Route.test.ts +56 -0
- package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
- package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
- package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
- package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
- package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
- package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
- package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
- package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
- package/Tests/UI/Components/Icon.test.tsx +26 -0
- package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
- package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
- package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
- package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
- package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
- package/Tests/Utils/HostAddressUtil.test.ts +138 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
- package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
- package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
- package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
- package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
- package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
- package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
- package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
- package/Types/API/Hostname.ts +63 -16
- package/Types/API/Route.ts +19 -2
- package/Types/API/URL.ts +26 -2
- package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
- package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
- package/Types/Database/CanonicalModelInstance.ts +102 -0
- package/Types/Database/TableColumn.ts +20 -6
- package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
- package/Types/IP/IPv6.ts +6 -1
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
- package/Types/Monitor/MonitorStep.ts +6 -1
- package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
- package/Types/Permission.ts +90 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
- package/Types/Rules/RuleRun.ts +4 -0
- package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
- package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
- package/UI/Components/Feed/FeedOptions.ts +427 -0
- package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
- package/UI/Components/Feed/useFeedItems.ts +28 -9
- package/UI/Components/Feed/useFeedOptions.ts +225 -0
- package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
- package/UI/Components/ModelList/ModelList.tsx +11 -1
- package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
- package/UI/esbuild-enterprise.js +263 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
- package/Utils/HostAddressUtil.ts +94 -0
- package/Utils/LabelRuleImportExport.ts +27 -9
- package/Utils/Monitor/MetricValueFormatter.ts +2 -2
- package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
- package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
- package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
- package/Utils/Slo/SloDuration.ts +22 -6
- package/Utils/Slo/SloHealth.ts +8 -7
- package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
- package/Utils/Slo/SloOverviewText.ts +33 -0
- package/Utils/Slo/SloProjection.ts +4 -3
- package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
- package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
- package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
- package/build/dist/Server/API/BaseAPI.js +26 -5
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/ProjectOIDC.js +24 -16
- package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
- package/build/dist/Server/API/ProjectSSO.js +24 -16
- package/build/dist/Server/API/ProjectSSO.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +93 -43
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +8 -1
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
- package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
- package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
- package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
- package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
- package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
- package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
- package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +84 -2
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/AuditLogService.js +71 -740
- package/build/dist/Server/Services/AuditLogService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +7 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/GlobalConfigService.js +63 -0
- package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorProbeService.js +90 -17
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +48 -6
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +214 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RumSessionPinService.js +24 -6
- package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
- package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +71 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +10 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +9 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +10 -16
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
- package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
- package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +31 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
- package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
- package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
- package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
- package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
- package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
- package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
- package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
- package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
- package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +59 -11
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/API/Route.js +18 -2
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/API/URL.js +23 -2
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
- package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
- package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
- package/build/dist/Types/Database/TableColumn.js +19 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
- package/build/dist/Types/IP/IPv6.js +6 -1
- package/build/dist/Types/IP/IPv6.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +5 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
- package/build/dist/Types/Permission.js +84 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
- package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
- package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
- package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
- package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
- package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
- package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
- package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
- package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
- package/build/dist/Utils/HostAddressUtil.js +80 -0
- package/build/dist/Utils/HostAddressUtil.js.map +1 -0
- package/build/dist/Utils/LabelRuleImportExport.js +22 -6
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
- package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
- package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
- package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
- package/build/dist/Utils/Slo/SloDuration.js +13 -5
- package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
- package/build/dist/Utils/Slo/SloHealth.js +8 -7
- package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
- package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
- package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
- package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
- package/jest.config.json +3 -1
- package/package.json +1 -1
- package/test-setup.sh +103 -34
- package/tsconfig.json +13 -0
- package/Server/API/EnterpriseLicenseAPI.ts +0 -925
- package/Server/API/TeamComplianceAPI.ts +0 -158
- package/Server/Middleware/SCIMAuthorization.ts +0 -141
- package/Server/Services/TeamComplianceService.ts +0 -733
- package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
- package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
- package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
- package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
- package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
- package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
- package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
- package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
- package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
- package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
- package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
- package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
- package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
- package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
- package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
- package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
- package/build/dist/Server/Services/TeamComplianceService.js +0 -537
- package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
- package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
- package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Slack rejects a section whose text is over 3000 characters, and a message
|
|
5
|
+
* with more than 50 blocks (100 in a modal view) — and it rejects the whole
|
|
6
|
+
* message, not the one block. chat.postMessage answers ok:false and the
|
|
7
|
+
* caller only logs it, so an "Incident Created" message whose root cause
|
|
8
|
+
* carried a long Affected Resources list silently never reached the channel.
|
|
9
|
+
*
|
|
10
|
+
* These tests pin how SlackUtil turns a markdown payload into sections:
|
|
11
|
+
*
|
|
12
|
+
* - text that fits is one section, byte for byte what it always was,
|
|
13
|
+
* - longer text becomes consecutive sections of at most 3000 characters,
|
|
14
|
+
* split between paragraphs first, then between list items (an item keeps
|
|
15
|
+
* its nested bullets), then between lines, and only inside a line that is
|
|
16
|
+
* longer than the limit on its own,
|
|
17
|
+
* - code blocks cut by a section boundary are closed and reopened,
|
|
18
|
+
* - the message never goes over Slack's block limit: extra sections come out
|
|
19
|
+
* of what the other blocks leave, and text that still does not fit is cut
|
|
20
|
+
* short with a note pointing to OneUptime.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import SlackifyMarkdown from "slackify-markdown";
|
|
24
|
+
import SlackUtil from "../../../../../Server/Utils/Workspace/Slack/Slack";
|
|
25
|
+
import WorkspaceBase, {
|
|
26
|
+
WorkspaceSendMessageResponse,
|
|
27
|
+
} from "../../../../../Server/Utils/Workspace/WorkspaceBase";
|
|
28
|
+
import AffectedResourceList, {
|
|
29
|
+
AffectedResourceListEntry,
|
|
30
|
+
} from "../../../../../Server/Utils/Monitor/AffectedResourceList";
|
|
31
|
+
import logger from "../../../../../Server/Utils/Logger";
|
|
32
|
+
import API from "../../../../../Utils/API";
|
|
33
|
+
import HTTPResponse from "../../../../../Types/API/HTTPResponse";
|
|
34
|
+
import URL from "../../../../../Types/API/URL";
|
|
35
|
+
import { JSONObject } from "../../../../../Types/JSON";
|
|
36
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
37
|
+
import {
|
|
38
|
+
WorkspaceMessageBlock,
|
|
39
|
+
WorkspaceModalBlock,
|
|
40
|
+
WorkspacePayloadButtons,
|
|
41
|
+
WorkspacePayloadDivider,
|
|
42
|
+
WorkspacePayloadHeader,
|
|
43
|
+
WorkspacePayloadMarkdown,
|
|
44
|
+
} from "../../../../../Types/Workspace/WorkspaceMessagePayload";
|
|
45
|
+
import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
|
|
46
|
+
|
|
47
|
+
const MAX_LENGTH: number = SlackUtil.SECTION_TEXT_MAX_LENGTH;
|
|
48
|
+
const NOTE: string = SlackUtil.TRUNCATED_SECTION_NOTE;
|
|
49
|
+
|
|
50
|
+
interface PostCallArgs {
|
|
51
|
+
url: URL;
|
|
52
|
+
data: JSONObject;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function markdown(text: string): WorkspacePayloadMarkdown {
|
|
56
|
+
return {
|
|
57
|
+
_type: "WorkspacePayloadMarkdown",
|
|
58
|
+
text: text,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function divider(): WorkspacePayloadDivider {
|
|
63
|
+
return {
|
|
64
|
+
_type: "WorkspacePayloadDivider",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function header(text: string): WorkspacePayloadHeader {
|
|
69
|
+
return {
|
|
70
|
+
_type: "WorkspacePayloadHeader",
|
|
71
|
+
text: text,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buttons(): WorkspacePayloadButtons {
|
|
76
|
+
return {
|
|
77
|
+
_type: "WorkspacePayloadButtons",
|
|
78
|
+
buttons: [
|
|
79
|
+
{
|
|
80
|
+
_type: "WorkspaceMessagePayloadButton",
|
|
81
|
+
title: "🔗 View Incident",
|
|
82
|
+
value: "incident-id",
|
|
83
|
+
actionId: "ViewIncident",
|
|
84
|
+
url: URL.fromString("https://oneuptime.test/dashboard/incidents/1"),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
_type: "WorkspaceMessagePayloadButton",
|
|
88
|
+
title: "✅ Acknowledge",
|
|
89
|
+
value: "incident-id",
|
|
90
|
+
actionId: "AcknowledgeIncident",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// What a markdown payload rendered to before sections could be split.
|
|
97
|
+
function legacySection(text: string): JSONObject {
|
|
98
|
+
return {
|
|
99
|
+
type: "section",
|
|
100
|
+
text: {
|
|
101
|
+
type: "mrkdwn",
|
|
102
|
+
text: text ? SlackifyMarkdown(text) : "",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isSection(block: JSONObject): boolean {
|
|
108
|
+
return block["type"] === "section";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sectionText(block: JSONObject): string {
|
|
112
|
+
return (block["text"] as JSONObject)["text"] as string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sectionTexts(blocks: Array<JSONObject>): Array<string> {
|
|
116
|
+
return blocks.filter(isSection).map(sectionText);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function repeatLines(data: {
|
|
120
|
+
count: number;
|
|
121
|
+
line: (index: number) => string;
|
|
122
|
+
}): Array<string> {
|
|
123
|
+
const lines: Array<string> = [];
|
|
124
|
+
|
|
125
|
+
for (let index: number = 0; index < data.count; index++) {
|
|
126
|
+
lines.push(data.line(index));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A string of exactly `length` characters that says where it is.
|
|
133
|
+
function filler(label: string, length: number): string {
|
|
134
|
+
return (label + ":" + "x".repeat(length)).slice(0, length);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasLoneSurrogate(text: string): boolean {
|
|
138
|
+
for (let index: number = 0; index < text.length; index++) {
|
|
139
|
+
const code: number = text.charCodeAt(index);
|
|
140
|
+
|
|
141
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
142
|
+
const next: number = text.charCodeAt(index + 1);
|
|
143
|
+
|
|
144
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
index++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (code >= 0xdc00 && code <= 0xdfff) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The limits Slack enforces on what we send it.
|
|
161
|
+
function expectValidSlackBlocks(
|
|
162
|
+
blocks: Array<JSONObject>,
|
|
163
|
+
maxBlocks: number,
|
|
164
|
+
): void {
|
|
165
|
+
expect(blocks.length).toBeLessThanOrEqual(maxBlocks);
|
|
166
|
+
|
|
167
|
+
for (const text of sectionTexts(blocks)) {
|
|
168
|
+
expect(text.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
169
|
+
expect(hasLoneSurrogate(text)).toBe(false);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* The sections are consecutive, non-overlapping slices of the original with
|
|
175
|
+
* nothing but whitespace (the newlines they were split at) between them —
|
|
176
|
+
* so no content is lost, duplicated or reordered.
|
|
177
|
+
*/
|
|
178
|
+
function expectSlicesOfOriginal(original: string, pieces: Array<string>): void {
|
|
179
|
+
let cursor: number = 0;
|
|
180
|
+
|
|
181
|
+
for (const piece of pieces) {
|
|
182
|
+
expect(piece.length).toBeGreaterThan(0);
|
|
183
|
+
|
|
184
|
+
const index: number = original.indexOf(piece, cursor);
|
|
185
|
+
|
|
186
|
+
expect(index).toBeGreaterThanOrEqual(cursor);
|
|
187
|
+
expect(original.slice(cursor, index).trim()).toBe("");
|
|
188
|
+
|
|
189
|
+
cursor = index + piece.length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
expect(original.slice(cursor).trim()).toBe("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
* A root cause the way the Kubernetes monitor writes one when containers
|
|
197
|
+
* are crash-looping: cluster details, the ranked Affected Resources list
|
|
198
|
+
* (ten container-level entries, the longest kind), then the analysis.
|
|
199
|
+
*/
|
|
200
|
+
function buildKubernetesRootCause(): string {
|
|
201
|
+
const entries: Array<AffectedResourceListEntry> = [];
|
|
202
|
+
|
|
203
|
+
for (let index: number = 0; index < 10; index++) {
|
|
204
|
+
entries.push({
|
|
205
|
+
kind: "Container",
|
|
206
|
+
name: `${AffectedResourceList.code(`checkout-worker-${index}`)} in pod ${AffectedResourceList.code(`checkout-service-7d9f8b6c5d-x2k9q${index}`)}`,
|
|
207
|
+
value: `**${20 - index}**`,
|
|
208
|
+
details: [
|
|
209
|
+
{
|
|
210
|
+
label: "Namespace",
|
|
211
|
+
value: AffectedResourceList.code("payments-production"),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
label: "Deployment",
|
|
215
|
+
value: AffectedResourceList.code("checkout-service"),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
label: "Node",
|
|
219
|
+
value: AffectedResourceList.code(
|
|
220
|
+
`gke-gke-prod-cluster-default-pool-662f6819-c6w${index}`,
|
|
221
|
+
),
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const clusterDetails: string = [
|
|
228
|
+
"**Kubernetes Cluster Details**",
|
|
229
|
+
"- Cluster: gke-prod-cluster",
|
|
230
|
+
"- Metric: Container Restarts (`k8s.container.restarts`)",
|
|
231
|
+
"- Namespace: payments-production",
|
|
232
|
+
].join("\n");
|
|
233
|
+
|
|
234
|
+
const affectedResources: string = AffectedResourceList.render({
|
|
235
|
+
heading: "Affected Resources",
|
|
236
|
+
overflowNoun: "affected resources",
|
|
237
|
+
totalCount: 83,
|
|
238
|
+
entries: entries,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const analysis: string = [
|
|
242
|
+
"**Root Cause Analysis**",
|
|
243
|
+
"Container `checkout-worker-0` in pod `checkout-service-7d9f8b6c5d-x2k9q0` has restarted 20 times in the evaluation window. " +
|
|
244
|
+
"Frequent restarts usually mean the container is crashing on start (CrashLoopBackOff), being OOMKilled because it exceeds its memory limit, " +
|
|
245
|
+
"or failing its liveness probe. Check the previous container logs with `kubectl logs --previous` and the pod events with `kubectl describe pod`.",
|
|
246
|
+
].join("\n");
|
|
247
|
+
|
|
248
|
+
return `${clusterDetails}${affectedResources}\n\n${analysis}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// The feed text IncidentService.createIncidentFeedAsync posts to Slack.
|
|
252
|
+
function buildIncidentCreatedFeed(rootCause: string): string {
|
|
253
|
+
return `#### 🚨 Incident #4211 Created:
|
|
254
|
+
|
|
255
|
+
**Container restarts in payments-production**:
|
|
256
|
+
|
|
257
|
+
Containers in the checkout service are restarting repeatedly on gke-prod-cluster.
|
|
258
|
+
|
|
259
|
+
🔴 **Incident State**: Created
|
|
260
|
+
|
|
261
|
+
⚠️ **Severity**: Critical
|
|
262
|
+
|
|
263
|
+
🌎 **Resources Affected**:
|
|
264
|
+
- [Kubernetes: gke-prod-cluster container restarts](https://oneuptime.test/dashboard/${ObjectID.generate().toString()}/monitors/${ObjectID.generate().toString()})
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
\n
|
|
269
|
+
📄 **Root Cause**:
|
|
270
|
+
|
|
271
|
+
${rootCause}
|
|
272
|
+
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/*
|
|
277
|
+
* Many paragraphs — far more than MAX_SECTIONS_PER_MARKDOWN_BLOCK sections
|
|
278
|
+
* could ever hold.
|
|
279
|
+
*/
|
|
280
|
+
function buildAbsurdlyLongMarkdown(): string {
|
|
281
|
+
return repeatLines({
|
|
282
|
+
count: 2000,
|
|
283
|
+
line: (index: number): string => {
|
|
284
|
+
return `Paragraph ${index}: ${"lorem ipsum dolor sit amet ".repeat(4)}`;
|
|
285
|
+
},
|
|
286
|
+
}).join("\n\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
describe("SlackUtil.splitSectionText", () => {
|
|
290
|
+
test("returns text that fits unchanged, as the only section", () => {
|
|
291
|
+
const text: string = "*Incident created*\n\nSomething happened.\n";
|
|
292
|
+
|
|
293
|
+
expect(SlackUtil.splitSectionText({ text: text })).toEqual([text]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("returns empty text as a single empty section", () => {
|
|
297
|
+
expect(SlackUtil.splitSectionText({ text: "" })).toEqual([""]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("keeps text of exactly 3000 characters whole", () => {
|
|
301
|
+
const text: string = `${filler("a", 1500)}\n\n${filler("b", 1498)}`;
|
|
302
|
+
|
|
303
|
+
expect(text.length).toBe(3000);
|
|
304
|
+
expect(SlackUtil.splitSectionText({ text: text })).toEqual([text]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("splits text of 3001 characters at its paragraph break", () => {
|
|
308
|
+
const first: string = filler("a", 1500);
|
|
309
|
+
const second: string = filler("b", 1499);
|
|
310
|
+
const text: string = `${first}\n\n${second}`;
|
|
311
|
+
|
|
312
|
+
expect(text.length).toBe(3001);
|
|
313
|
+
expect(SlackUtil.splitSectionText({ text: text })).toEqual([first, second]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("hard-cuts a single 3001-character line into 3000 + 1", () => {
|
|
317
|
+
const text: string = "x".repeat(3001);
|
|
318
|
+
|
|
319
|
+
expect(SlackUtil.splitSectionText({ text: text })).toEqual([
|
|
320
|
+
"x".repeat(3000),
|
|
321
|
+
"x",
|
|
322
|
+
]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("moves a whole paragraph to the next section rather than splitting it", () => {
|
|
326
|
+
const paragraphA: string = repeatLines({
|
|
327
|
+
count: 4,
|
|
328
|
+
line: (index: number): string => {
|
|
329
|
+
return filler(`a${index}`, 499);
|
|
330
|
+
},
|
|
331
|
+
}).join("\n");
|
|
332
|
+
|
|
333
|
+
const paragraphB: string = repeatLines({
|
|
334
|
+
count: 3,
|
|
335
|
+
line: (index: number): string => {
|
|
336
|
+
return filler(`b${index}`, 499);
|
|
337
|
+
},
|
|
338
|
+
}).join("\n");
|
|
339
|
+
|
|
340
|
+
/*
|
|
341
|
+
* Two of paragraph B's lines would still fit after paragraph A, so a
|
|
342
|
+
* line-by-line fill would split B across the two sections.
|
|
343
|
+
*/
|
|
344
|
+
expect(paragraphA.length + 1 + 2 * 500).toBeLessThanOrEqual(MAX_LENGTH);
|
|
345
|
+
|
|
346
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
347
|
+
text: `${paragraphA}\n\n${paragraphB}`,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(pieces).toEqual([paragraphA, paragraphB]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("packs several short paragraphs into one section before starting another", () => {
|
|
354
|
+
const paragraphs: Array<string> = repeatLines({
|
|
355
|
+
count: 6,
|
|
356
|
+
line: (index: number): string => {
|
|
357
|
+
return filler(`p${index}`, 900);
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const text: string = paragraphs.join("\n\n");
|
|
362
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
363
|
+
|
|
364
|
+
// 3 × 900 + 2 × 2 = 2704 fits; a fourth paragraph would make 3606.
|
|
365
|
+
expect(pieces).toEqual([
|
|
366
|
+
paragraphs.slice(0, 3).join("\n\n"),
|
|
367
|
+
paragraphs.slice(3).join("\n\n"),
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("splits a paragraph longer than the limit between its lines", () => {
|
|
372
|
+
const lines: Array<string> = repeatLines({
|
|
373
|
+
count: 8,
|
|
374
|
+
line: (index: number): string => {
|
|
375
|
+
return filler(`line${index}`, 900);
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
380
|
+
text: lines.join("\n"),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(pieces).toEqual([
|
|
384
|
+
lines.slice(0, 3).join("\n"),
|
|
385
|
+
lines.slice(3, 6).join("\n"),
|
|
386
|
+
lines.slice(6).join("\n"),
|
|
387
|
+
]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("keeps a list item together with its indented bullets", () => {
|
|
391
|
+
const items: Array<string> = repeatLines({
|
|
392
|
+
count: 6,
|
|
393
|
+
line: (index: number): string => {
|
|
394
|
+
return [
|
|
395
|
+
filler(`${index + 1}. item`, 100),
|
|
396
|
+
...repeatLines({
|
|
397
|
+
count: 3,
|
|
398
|
+
line: (detail: number): string => {
|
|
399
|
+
return filler(` • item ${index + 1} detail ${detail}`, 200);
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
].join("\n");
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Each item is 100 + 3 × 201 = 703 characters; four fit, five do not.
|
|
407
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
408
|
+
text: items.join("\n"),
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(pieces).toEqual([
|
|
412
|
+
items.slice(0, 4).join("\n"),
|
|
413
|
+
items.slice(4).join("\n"),
|
|
414
|
+
]);
|
|
415
|
+
|
|
416
|
+
/*
|
|
417
|
+
* Filling line by line would have put item 5's title in the first
|
|
418
|
+
* section and its bullets in the second.
|
|
419
|
+
*/
|
|
420
|
+
expect(items.slice(0, 4).join("\n").length + 1 + 100 + 1).toBeLessThan(
|
|
421
|
+
MAX_LENGTH,
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("splits a single list item larger than the limit between its lines", () => {
|
|
426
|
+
const itemLines: Array<string> = [
|
|
427
|
+
filler("1. huge item", 80),
|
|
428
|
+
...repeatLines({
|
|
429
|
+
count: 5,
|
|
430
|
+
line: (index: number): string => {
|
|
431
|
+
return filler(` • detail ${index}`, 800);
|
|
432
|
+
},
|
|
433
|
+
}),
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
const text: string = itemLines.join("\n");
|
|
437
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
438
|
+
|
|
439
|
+
expect(pieces.length).toBe(2);
|
|
440
|
+
expectSlicesOfOriginal(text, pieces);
|
|
441
|
+
|
|
442
|
+
// Every line of every piece is a whole line of the original.
|
|
443
|
+
for (const piece of pieces) {
|
|
444
|
+
for (const line of piece.split("\n")) {
|
|
445
|
+
expect(itemLines).toContain(line);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("hard-cuts a 7000-character line without spaces into pieces of at most 3000", () => {
|
|
451
|
+
const text: string = filler("unbroken", 7000);
|
|
452
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
453
|
+
|
|
454
|
+
expect(
|
|
455
|
+
pieces.map((piece: string): number => {
|
|
456
|
+
return piece.length;
|
|
457
|
+
}),
|
|
458
|
+
).toEqual([3000, 3000, 1000]);
|
|
459
|
+
expect(pieces.join("")).toBe(text);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("hard-cuts a long line of words at spaces, keeping every word whole", () => {
|
|
463
|
+
const text: string = "incident ".repeat(800).trim();
|
|
464
|
+
|
|
465
|
+
expect(text.length).toBeGreaterThan(7000);
|
|
466
|
+
|
|
467
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
468
|
+
|
|
469
|
+
expect(pieces.length).toBe(3);
|
|
470
|
+
expect(pieces.join("")).toBe(text);
|
|
471
|
+
|
|
472
|
+
for (const piece of pieces) {
|
|
473
|
+
expect(piece.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
474
|
+
|
|
475
|
+
for (const word of piece.split(" ")) {
|
|
476
|
+
expect(["incident", ""]).toContain(word);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("never cuts between the two halves of an emoji", () => {
|
|
482
|
+
// After the leading "a", every emoji's high surrogate sits at an odd index — 2999 among them.
|
|
483
|
+
const text: string = "a" + "😀".repeat(2000);
|
|
484
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
485
|
+
|
|
486
|
+
expect(pieces.join("")).toBe(text);
|
|
487
|
+
expect(pieces[0]!.length).toBe(2999);
|
|
488
|
+
|
|
489
|
+
for (const piece of pieces) {
|
|
490
|
+
expect(piece.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
491
|
+
expect(hasLoneSurrogate(piece)).toBe(false);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("never starts or ends a section with a newline, and never emits an empty one", () => {
|
|
496
|
+
const text: string = `\n\n\n${filler("a", 2000)}\n\n\n\n\n${filler("b", 2000)}\n\n\n`;
|
|
497
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
498
|
+
|
|
499
|
+
expect(pieces).toEqual([filler("a", 2000), filler("b", 2000)]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("returns a single empty section for text that is nothing but whitespace", () => {
|
|
503
|
+
expect(SlackUtil.splitSectionText({ text: "\n".repeat(4000) })).toEqual([
|
|
504
|
+
"",
|
|
505
|
+
]);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("closes a code block at a section boundary and reopens it in the next section", () => {
|
|
509
|
+
const codeLines: Array<string> = repeatLines({
|
|
510
|
+
count: 120,
|
|
511
|
+
line: (index: number): string => {
|
|
512
|
+
return filler(`2026-09-21T10:00:${index} ERROR checkout`, 60);
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const text: string = `Logs from the failing pod:\n\n\`\`\`\n${codeLines.join("\n")}\n\`\`\`\n\nRestart the deployment once fixed.`;
|
|
517
|
+
|
|
518
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
519
|
+
|
|
520
|
+
/*
|
|
521
|
+
* The code block is a paragraph longer than a section, so it starts a
|
|
522
|
+
* section of its own and is split between its lines across three.
|
|
523
|
+
*/
|
|
524
|
+
expect(pieces.length).toBe(4);
|
|
525
|
+
expect(pieces[0]).toBe("Logs from the failing pod:");
|
|
526
|
+
|
|
527
|
+
for (let index: number = 1; index < pieces.length; index++) {
|
|
528
|
+
const piece: string = pieces[index]!;
|
|
529
|
+
|
|
530
|
+
expect(piece.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
531
|
+
|
|
532
|
+
// Every section opens and closes its own code blocks.
|
|
533
|
+
expect((piece.split("```").length - 1) % 2).toBe(0);
|
|
534
|
+
expect(piece.startsWith("```\n")).toBe(true);
|
|
535
|
+
|
|
536
|
+
if (index < pieces.length - 1) {
|
|
537
|
+
expect(piece.endsWith("\n```")).toBe(true);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Every log line survives, in order.
|
|
542
|
+
const allLines: Array<string> = pieces.join("\n").split("\n");
|
|
543
|
+
const loggedLines: Array<string> = allLines.filter((line: string) => {
|
|
544
|
+
return line.includes("ERROR checkout");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(loggedLines).toEqual(codeLines);
|
|
548
|
+
expect(pieces[0]!.startsWith("Logs from the failing pod:")).toBe(true);
|
|
549
|
+
expect(
|
|
550
|
+
pieces[pieces.length - 1]!.endsWith(
|
|
551
|
+
"```\n\nRestart the deployment once fixed.",
|
|
552
|
+
),
|
|
553
|
+
).toBe(true);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("leaves a code block alone when the original never closes it", () => {
|
|
557
|
+
const text: string = `\`\`\`\n${repeatLines({
|
|
558
|
+
count: 80,
|
|
559
|
+
line: (index: number): string => {
|
|
560
|
+
return filler(`unterminated ${index}`, 60);
|
|
561
|
+
},
|
|
562
|
+
}).join("\n")}`;
|
|
563
|
+
|
|
564
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({ text: text });
|
|
565
|
+
const lastPiece: string = pieces[pieces.length - 1]!;
|
|
566
|
+
|
|
567
|
+
expect(pieces.length).toBe(2);
|
|
568
|
+
expect(pieces[0]!.endsWith("\n```")).toBe(true);
|
|
569
|
+
expect(lastPiece.startsWith("```\n")).toBe(true);
|
|
570
|
+
// Nothing is added after the original's last line.
|
|
571
|
+
expect(lastPiece.endsWith(filler("unterminated 79", 60))).toBe(true);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("stops at maxSections and ends the last section with the truncation note", () => {
|
|
575
|
+
const paragraphs: Array<string> = repeatLines({
|
|
576
|
+
count: 30,
|
|
577
|
+
line: (index: number): string => {
|
|
578
|
+
return filler(`p${index}`, 1000);
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const text: string = paragraphs.join("\n\n");
|
|
583
|
+
const untruncated: Array<string> = SlackUtil.splitSectionText({
|
|
584
|
+
text: text,
|
|
585
|
+
maxSections: 100,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Two 1000-character paragraphs per section.
|
|
589
|
+
expect(untruncated.length).toBe(15);
|
|
590
|
+
|
|
591
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
592
|
+
text: text,
|
|
593
|
+
maxSections: 3,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
expect(pieces).toEqual([
|
|
597
|
+
untruncated[0],
|
|
598
|
+
untruncated[1],
|
|
599
|
+
untruncated[2] + NOTE,
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
for (const piece of pieces) {
|
|
603
|
+
expect(piece.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("shortens the last section at a paragraph break to make room for the note", () => {
|
|
608
|
+
const first: string = filler("first", 1400);
|
|
609
|
+
const second: string = filler("second", 1590);
|
|
610
|
+
const third: string = filler("third", 1000);
|
|
611
|
+
|
|
612
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
613
|
+
text: [first, second, third].join("\n\n"),
|
|
614
|
+
maxSections: 1,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// first + second (2992) fits a section but leaves no room for the note.
|
|
618
|
+
expect(pieces).toEqual([first + NOTE]);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("hard-cuts a single long line to make room for the note", () => {
|
|
622
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
623
|
+
text: filler("unbroken", 7000),
|
|
624
|
+
maxSections: 1,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
expect(pieces.length).toBe(1);
|
|
628
|
+
expect(pieces[0]!.length).toBe(MAX_LENGTH);
|
|
629
|
+
expect(pieces[0]!.endsWith(NOTE)).toBe(true);
|
|
630
|
+
expect(pieces[0]!.startsWith(filler("unbroken", 2000))).toBe(true);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("closes an open code block before the truncation note", () => {
|
|
634
|
+
const text: string = `\`\`\`\n${repeatLines({
|
|
635
|
+
count: 500,
|
|
636
|
+
line: (index: number): string => {
|
|
637
|
+
return filler(`trace ${index}`, 60);
|
|
638
|
+
},
|
|
639
|
+
}).join("\n")}\n\`\`\``;
|
|
640
|
+
|
|
641
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
642
|
+
text: text,
|
|
643
|
+
maxSections: 2,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
expect(pieces.length).toBe(2);
|
|
647
|
+
expect(pieces[1]!.startsWith("```\n")).toBe(true);
|
|
648
|
+
expect(pieces[1]!.endsWith("\n```" + NOTE)).toBe(true);
|
|
649
|
+
|
|
650
|
+
for (const piece of pieces) {
|
|
651
|
+
expect(piece.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
652
|
+
expect((piece.split("```").length - 1) % 2).toBe(0);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("treats a maxSections below one as one", () => {
|
|
657
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
658
|
+
text: filler("zero", 5000),
|
|
659
|
+
maxSections: 0,
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
expect(pieces.length).toBe(1);
|
|
663
|
+
expect(pieces[0]!.endsWith(NOTE)).toBe(true);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("defaults to MAX_SECTIONS_PER_MARKDOWN_BLOCK sections", () => {
|
|
667
|
+
const pieces: Array<string> = SlackUtil.splitSectionText({
|
|
668
|
+
text: buildAbsurdlyLongMarkdown(),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
expect(pieces.length).toBe(SlackUtil.MAX_SECTIONS_PER_MARKDOWN_BLOCK);
|
|
672
|
+
expect(pieces[pieces.length - 1]!.endsWith(NOTE)).toBe(true);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
describe("SlackUtil markdown sections", () => {
|
|
677
|
+
test("renders short markdown as exactly one section, identical to the SlackifyMarkdown output", () => {
|
|
678
|
+
const text: string =
|
|
679
|
+
"#### 🚨 Incident #12 Created:\n\n**Checkout is down**\n\n- [Checkout API](https://oneuptime.test/monitors/1)\n";
|
|
680
|
+
|
|
681
|
+
expect(
|
|
682
|
+
SlackUtil.getMarkdownBlocks({ payloadMarkdownBlock: markdown(text) }),
|
|
683
|
+
).toEqual([legacySection(text)]);
|
|
684
|
+
|
|
685
|
+
expect(
|
|
686
|
+
SlackUtil.getMarkdownBlock({ payloadMarkdownBlock: markdown(text) }),
|
|
687
|
+
).toEqual(legacySection(text));
|
|
688
|
+
|
|
689
|
+
expect(
|
|
690
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
691
|
+
messageBlocks: [markdown(text)],
|
|
692
|
+
}),
|
|
693
|
+
).toEqual([legacySection(text)]);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test("renders empty text as one section with empty text, as before", () => {
|
|
697
|
+
const expected: Array<JSONObject> = [
|
|
698
|
+
{ type: "section", text: { type: "mrkdwn", text: "" } },
|
|
699
|
+
];
|
|
700
|
+
|
|
701
|
+
expect(
|
|
702
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
703
|
+
messageBlocks: [markdown("")],
|
|
704
|
+
}),
|
|
705
|
+
).toEqual(expected);
|
|
706
|
+
|
|
707
|
+
expect(
|
|
708
|
+
SlackUtil.getMarkdownBlock({ payloadMarkdownBlock: markdown("") }),
|
|
709
|
+
).toEqual(expected[0]);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("keeps markdown that slackifies to exactly 3000 characters in one section", () => {
|
|
713
|
+
const text: string = `${"a".repeat(1500)}\n\n${"b".repeat(1497)}`;
|
|
714
|
+
|
|
715
|
+
// slackify-markdown ends its output with a newline.
|
|
716
|
+
expect(SlackifyMarkdown(text).length).toBe(3000);
|
|
717
|
+
|
|
718
|
+
expect(
|
|
719
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
720
|
+
messageBlocks: [markdown(text)],
|
|
721
|
+
}),
|
|
722
|
+
).toEqual([legacySection(text)]);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("splits markdown that slackifies to 3001 characters into two sections", () => {
|
|
726
|
+
const text: string = `${"a".repeat(1500)}\n\n${"b".repeat(1498)}`;
|
|
727
|
+
|
|
728
|
+
expect(SlackifyMarkdown(text).length).toBe(3001);
|
|
729
|
+
|
|
730
|
+
expect(
|
|
731
|
+
sectionTexts(
|
|
732
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
733
|
+
messageBlocks: [markdown(text)],
|
|
734
|
+
}),
|
|
735
|
+
),
|
|
736
|
+
).toEqual(["a".repeat(1500), "b".repeat(1498)]);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("hard-cuts a 7000-character line into sections of at most 3000", () => {
|
|
740
|
+
const line: string = "z".repeat(7000);
|
|
741
|
+
|
|
742
|
+
const texts: Array<string> = sectionTexts(
|
|
743
|
+
SlackUtil.getMarkdownBlocks({ payloadMarkdownBlock: markdown(line) }),
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
expect(
|
|
747
|
+
texts.map((text: string): number => {
|
|
748
|
+
return text.length;
|
|
749
|
+
}),
|
|
750
|
+
).toEqual([3000, 3000, 1000]);
|
|
751
|
+
expect(texts.join("")).toBe(line);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("getMarkdownBlock cuts long text short with a note instead of returning an invalid section", () => {
|
|
755
|
+
const block: JSONObject = SlackUtil.getMarkdownBlock({
|
|
756
|
+
payloadMarkdownBlock: markdown(buildAbsurdlyLongMarkdown()),
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
expect(block["type"]).toBe("section");
|
|
760
|
+
expect(sectionText(block).length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
761
|
+
expect(sectionText(block).endsWith(NOTE)).toBe(true);
|
|
762
|
+
expect(sectionText(block).startsWith("Paragraph 0:")).toBe(true);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
describe("a realistic Incident Created feed with an Affected Resources list", () => {
|
|
766
|
+
const rootCause: string = buildKubernetesRootCause();
|
|
767
|
+
const feed: string = buildIncidentCreatedFeed(rootCause);
|
|
768
|
+
const slackified: string = SlackifyMarkdown(feed);
|
|
769
|
+
|
|
770
|
+
const texts: Array<string> = sectionTexts(
|
|
771
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
772
|
+
messageBlocks: [markdown(feed)],
|
|
773
|
+
}),
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
test("is too long for one section in the first place", () => {
|
|
777
|
+
expect(slackified.length).toBeGreaterThan(MAX_LENGTH);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("becomes several sections, each within Slack's limit", () => {
|
|
781
|
+
expect(texts.length).toBeGreaterThan(1);
|
|
782
|
+
|
|
783
|
+
for (const text of texts) {
|
|
784
|
+
expect(text.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("keeps all the content, in order", () => {
|
|
789
|
+
expectSlicesOfOriginal(slackified, texts);
|
|
790
|
+
|
|
791
|
+
expect(texts[0]!.startsWith("*🚨 Incident #4211 Created:*")).toBe(true);
|
|
792
|
+
expect(texts.join("\n")).toContain("Root Cause Analysis");
|
|
793
|
+
expect(texts.join("\n")).toContain("and 73 more affected resources");
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("never splits inside a line", () => {
|
|
797
|
+
const originalLines: Set<string> = new Set(slackified.split("\n"));
|
|
798
|
+
|
|
799
|
+
for (const text of texts) {
|
|
800
|
+
for (const line of text.split("\n")) {
|
|
801
|
+
expect(originalLines.has(line)).toBe(true);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("keeps the whole ranked list in one section, every item with its bullets", () => {
|
|
807
|
+
const listSection: string | undefined = texts.find((text: string) => {
|
|
808
|
+
return text.includes("checkout-worker-0");
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
expect(listSection).toBeDefined();
|
|
812
|
+
|
|
813
|
+
for (let index: number = 0; index < 10; index++) {
|
|
814
|
+
expect(listSection).toContain(`\`checkout-worker-${index}\``);
|
|
815
|
+
expect(listSection).toContain(
|
|
816
|
+
`gke-gke-prod-cluster-default-pool-662f6819-c6w${index}`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// No section opens with a nested bullet cut off from its item.
|
|
821
|
+
for (const text of texts) {
|
|
822
|
+
expect(text.startsWith(" ")).toBe(false);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
describe("SlackUtil.getBlocksFromWorkspaceMessagePayload", () => {
|
|
829
|
+
const longText: string = repeatLines({
|
|
830
|
+
count: 40,
|
|
831
|
+
line: (index: number): string => {
|
|
832
|
+
return `Paragraph ${index}: ${"the checkout service is failing health checks ".repeat(4)}`;
|
|
833
|
+
},
|
|
834
|
+
}).join("\n\n");
|
|
835
|
+
|
|
836
|
+
test("renders every other block type exactly as the base class does", () => {
|
|
837
|
+
const messageBlocks: Array<WorkspaceMessageBlock> = [
|
|
838
|
+
header("Incident #12 created"),
|
|
839
|
+
markdown("**Checkout is down**"),
|
|
840
|
+
divider(),
|
|
841
|
+
buttons(),
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
const expected: Array<JSONObject> =
|
|
845
|
+
WorkspaceBase.getBlocksFromWorkspaceMessagePayload.call(SlackUtil, {
|
|
846
|
+
messageBlocks: messageBlocks,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
expect(
|
|
850
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
851
|
+
messageBlocks: messageBlocks,
|
|
852
|
+
}),
|
|
853
|
+
).toEqual(expected);
|
|
854
|
+
|
|
855
|
+
expect(expected[1]).toEqual(legacySection("**Checkout is down**"));
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("keeps the header, divider and buttons in place around a split markdown block", () => {
|
|
859
|
+
const blocks: Array<JSONObject> =
|
|
860
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
861
|
+
messageBlocks: [
|
|
862
|
+
header("Incident #12 created"),
|
|
863
|
+
markdown(longText),
|
|
864
|
+
divider(),
|
|
865
|
+
buttons(),
|
|
866
|
+
],
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const sectionCount: number = sectionTexts(blocks).length;
|
|
870
|
+
|
|
871
|
+
expect(sectionCount).toBeGreaterThan(1);
|
|
872
|
+
expect(blocks.length).toBe(sectionCount + 3);
|
|
873
|
+
|
|
874
|
+
expect(blocks[0]).toEqual(
|
|
875
|
+
SlackUtil.getHeaderBlock({
|
|
876
|
+
payloadHeaderBlock: header("Incident #12 created"),
|
|
877
|
+
}),
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
for (let index: number = 1; index <= sectionCount; index++) {
|
|
881
|
+
expect(blocks[index]!["type"]).toBe("section");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
expect(blocks[sectionCount + 1]).toEqual(SlackUtil.getDividerBlock());
|
|
885
|
+
expect(blocks[sectionCount + 2]).toEqual(
|
|
886
|
+
SlackUtil.getButtonsBlock({ payloadButtonsBlock: buttons() }),
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
expectSlicesOfOriginal(SlackifyMarkdown(longText), sectionTexts(blocks));
|
|
890
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
test("keeps several markdown blocks in their own sections and order", () => {
|
|
894
|
+
const blocks: Array<JSONObject> =
|
|
895
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
896
|
+
messageBlocks: [
|
|
897
|
+
markdown(longText),
|
|
898
|
+
divider(),
|
|
899
|
+
markdown("**Second**"),
|
|
900
|
+
markdown(longText),
|
|
901
|
+
],
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
const firstSplit: Array<string> = SlackUtil.splitSectionText({
|
|
905
|
+
text: SlackifyMarkdown(longText),
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const types: Array<string> = blocks.map((block: JSONObject): string => {
|
|
909
|
+
return block["type"] as string;
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
expect(types).toEqual([
|
|
913
|
+
...firstSplit.map((): string => {
|
|
914
|
+
return "section";
|
|
915
|
+
}),
|
|
916
|
+
"divider",
|
|
917
|
+
"section",
|
|
918
|
+
...firstSplit.map((): string => {
|
|
919
|
+
return "section";
|
|
920
|
+
}),
|
|
921
|
+
]);
|
|
922
|
+
|
|
923
|
+
expect(sectionText(blocks[firstSplit.length + 1]!)).toBe(
|
|
924
|
+
SlackifyMarkdown("**Second**"),
|
|
925
|
+
);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test("caps one absurdly long markdown block at MAX_SECTIONS_PER_MARKDOWN_BLOCK sections", () => {
|
|
929
|
+
const blocks: Array<JSONObject> =
|
|
930
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
931
|
+
messageBlocks: [
|
|
932
|
+
markdown(buildAbsurdlyLongMarkdown()),
|
|
933
|
+
divider(),
|
|
934
|
+
buttons(),
|
|
935
|
+
],
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
const texts: Array<string> = sectionTexts(blocks);
|
|
939
|
+
|
|
940
|
+
expect(texts.length).toBe(SlackUtil.MAX_SECTIONS_PER_MARKDOWN_BLOCK);
|
|
941
|
+
expect(blocks.length).toBe(SlackUtil.MAX_SECTIONS_PER_MARKDOWN_BLOCK + 2);
|
|
942
|
+
expect(texts[texts.length - 1]!.endsWith(NOTE)).toBe(true);
|
|
943
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("never goes over 50 blocks, however much markdown the message carries", () => {
|
|
947
|
+
const absurd: string = buildAbsurdlyLongMarkdown();
|
|
948
|
+
|
|
949
|
+
const blocks: Array<JSONObject> =
|
|
950
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
951
|
+
messageBlocks: repeatLines({
|
|
952
|
+
count: 8,
|
|
953
|
+
line: (): string => {
|
|
954
|
+
return absurd;
|
|
955
|
+
},
|
|
956
|
+
}).map(markdown),
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
/*
|
|
960
|
+
* 8 sections are owed up front, leaving 42 extra: the first four
|
|
961
|
+
* payloads take 9 each, the fifth the 6 left, the rest keep one.
|
|
962
|
+
*/
|
|
963
|
+
expect(blocks.length).toBe(SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
964
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
965
|
+
|
|
966
|
+
const texts: Array<string> = sectionTexts(blocks);
|
|
967
|
+
const payloadStarts: Array<number> = [];
|
|
968
|
+
|
|
969
|
+
texts.forEach((text: string, index: number): void => {
|
|
970
|
+
if (text.startsWith("Paragraph 0:")) {
|
|
971
|
+
payloadStarts.push(index);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
expect(payloadStarts).toEqual([0, 10, 20, 30, 40, 47, 48, 49]);
|
|
976
|
+
|
|
977
|
+
// Every payload was cut short, and says so.
|
|
978
|
+
const notedSections: number = texts.filter((text: string): boolean => {
|
|
979
|
+
return text.endsWith(NOTE);
|
|
980
|
+
}).length;
|
|
981
|
+
|
|
982
|
+
expect(notedSections).toBe(8);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test("gives a long markdown block only the blocks the rest of the message leaves", () => {
|
|
986
|
+
const blocks: Array<JSONObject> =
|
|
987
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
988
|
+
messageBlocks: [
|
|
989
|
+
markdown(buildAbsurdlyLongMarkdown()),
|
|
990
|
+
...repeatLines({
|
|
991
|
+
count: 47,
|
|
992
|
+
line: (): string => {
|
|
993
|
+
return "";
|
|
994
|
+
},
|
|
995
|
+
}).map(divider),
|
|
996
|
+
],
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
expect(blocks.length).toBe(SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
1000
|
+
expect(sectionTexts(blocks).length).toBe(3);
|
|
1001
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("keeps one section per markdown block when the message is already over the limit", () => {
|
|
1005
|
+
const blocks: Array<JSONObject> =
|
|
1006
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
1007
|
+
messageBlocks: [
|
|
1008
|
+
...repeatLines({
|
|
1009
|
+
count: 55,
|
|
1010
|
+
line: (): string => {
|
|
1011
|
+
return "";
|
|
1012
|
+
},
|
|
1013
|
+
}).map(divider),
|
|
1014
|
+
markdown(longText),
|
|
1015
|
+
],
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// As many blocks as before — sendMessage posts the overflow separately.
|
|
1019
|
+
expect(blocks.length).toBe(56);
|
|
1020
|
+
|
|
1021
|
+
const texts: Array<string> = sectionTexts(blocks);
|
|
1022
|
+
|
|
1023
|
+
expect(texts.length).toBe(1);
|
|
1024
|
+
expect(texts[0]!.length).toBeLessThanOrEqual(MAX_LENGTH);
|
|
1025
|
+
expect(texts[0]!.endsWith(NOTE)).toBe(true);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("honours a larger maxBlocks", () => {
|
|
1029
|
+
const blocks: Array<JSONObject> =
|
|
1030
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
1031
|
+
messageBlocks: [
|
|
1032
|
+
...repeatLines({
|
|
1033
|
+
count: 60,
|
|
1034
|
+
line: (): string => {
|
|
1035
|
+
return "";
|
|
1036
|
+
},
|
|
1037
|
+
}).map(divider),
|
|
1038
|
+
markdown(longText),
|
|
1039
|
+
],
|
|
1040
|
+
maxBlocks: SlackUtil.MAX_BLOCKS_PER_MODAL,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
expect(sectionTexts(blocks).length).toBeGreaterThan(1);
|
|
1044
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MODAL);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test("still drops and logs a block of an unknown type", () => {
|
|
1048
|
+
const errorSpy: jest.SpyInstance = jest
|
|
1049
|
+
.spyOn(logger, "error")
|
|
1050
|
+
.mockImplementation((): void => {});
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
const blocks: Array<JSONObject> =
|
|
1054
|
+
SlackUtil.getBlocksFromWorkspaceMessagePayload({
|
|
1055
|
+
messageBlocks: [
|
|
1056
|
+
{ _type: "SomethingSlackHasNeverHeardOf" },
|
|
1057
|
+
markdown("**Hello**"),
|
|
1058
|
+
],
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
expect(blocks).toEqual([legacySection("**Hello**")]);
|
|
1062
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1063
|
+
"Unknown block type: SomethingSlackHasNeverHeardOf",
|
|
1064
|
+
{ blockType: "SomethingSlackHasNeverHeardOf" },
|
|
1065
|
+
);
|
|
1066
|
+
} finally {
|
|
1067
|
+
errorSpy.mockRestore();
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
describe("Slack callers of getBlocksFromWorkspaceMessagePayload", () => {
|
|
1073
|
+
const rootCause: string = buildKubernetesRootCause();
|
|
1074
|
+
const feed: string = buildIncidentCreatedFeed(rootCause);
|
|
1075
|
+
|
|
1076
|
+
afterEach(() => {
|
|
1077
|
+
jest.restoreAllMocks();
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
function mockSlackPost(): jest.SpyInstance {
|
|
1081
|
+
return jest.spyOn(API, "post").mockResolvedValue({
|
|
1082
|
+
jsonData: { ok: true, ts: "1726912800.000100" },
|
|
1083
|
+
} as unknown as HTTPResponse<JSONObject>);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function postedData(postSpy: jest.SpyInstance, index: number): JSONObject {
|
|
1087
|
+
return (postSpy.mock.calls[index]![0] as PostCallArgs).data;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
test("sendMessage posts the Incident Created message once, with every section within the limit", async () => {
|
|
1091
|
+
jest
|
|
1092
|
+
.spyOn(SlackUtil, "getWorkspaceChannelFromChannelId")
|
|
1093
|
+
.mockResolvedValue({
|
|
1094
|
+
id: "C0INCIDENTS",
|
|
1095
|
+
name: "incidents",
|
|
1096
|
+
workspaceType: WorkspaceType.Slack,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const postSpy: jest.SpyInstance = mockSlackPost();
|
|
1100
|
+
|
|
1101
|
+
const response: WorkspaceSendMessageResponse = await SlackUtil.sendMessage({
|
|
1102
|
+
workspaceMessagePayload: {
|
|
1103
|
+
_type: "WorkspaceMessagePayload",
|
|
1104
|
+
channelNames: [],
|
|
1105
|
+
channelIds: ["C0INCIDENTS"],
|
|
1106
|
+
messageBlocks: [markdown(feed), divider(), buttons()],
|
|
1107
|
+
workspaceType: WorkspaceType.Slack,
|
|
1108
|
+
},
|
|
1109
|
+
authToken: "xoxb-test-token",
|
|
1110
|
+
userId: "",
|
|
1111
|
+
projectId: ObjectID.generate(),
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
expect(response.errors).toEqual([]);
|
|
1115
|
+
expect(response.threads.length).toBe(1);
|
|
1116
|
+
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
1117
|
+
|
|
1118
|
+
const args: PostCallArgs = postSpy.mock.calls[0]![0] as PostCallArgs;
|
|
1119
|
+
|
|
1120
|
+
expect(args.url.toString()).toBe("https://slack.com/api/chat.postMessage");
|
|
1121
|
+
|
|
1122
|
+
const blocks: Array<JSONObject> = args.data["blocks"] as Array<JSONObject>;
|
|
1123
|
+
|
|
1124
|
+
expect(sectionTexts(blocks).length).toBeGreaterThan(1);
|
|
1125
|
+
expect(blocks[blocks.length - 2]).toEqual({ type: "divider" });
|
|
1126
|
+
expect(blocks[blocks.length - 1]!["type"]).toBe("actions");
|
|
1127
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test("sendDirectMessageToUser posts valid blocks", async () => {
|
|
1131
|
+
const postSpy: jest.SpyInstance = mockSlackPost();
|
|
1132
|
+
|
|
1133
|
+
await SlackUtil.sendDirectMessageToUser({
|
|
1134
|
+
authToken: "xoxb-test-token",
|
|
1135
|
+
workspaceUserId: "U0ONCALL",
|
|
1136
|
+
messageBlocks: [markdown(buildAbsurdlyLongMarkdown())],
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
1140
|
+
|
|
1141
|
+
const blocks: Array<JSONObject> = postedData(postSpy, 0)[
|
|
1142
|
+
"blocks"
|
|
1143
|
+
] as Array<JSONObject>;
|
|
1144
|
+
|
|
1145
|
+
expect(blocks.length).toBe(SlackUtil.MAX_SECTIONS_PER_MARKDOWN_BLOCK);
|
|
1146
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test("sendEphemeralMessageToChannel stays within 50 blocks", async () => {
|
|
1150
|
+
const postSpy: jest.SpyInstance = mockSlackPost();
|
|
1151
|
+
|
|
1152
|
+
await SlackUtil.sendEphemeralMessageToChannel({
|
|
1153
|
+
authToken: "xoxb-test-token",
|
|
1154
|
+
channelId: "C0INCIDENTS",
|
|
1155
|
+
userId: "U0ONCALL",
|
|
1156
|
+
messageBlocks: [
|
|
1157
|
+
markdown(buildAbsurdlyLongMarkdown()),
|
|
1158
|
+
...repeatLines({
|
|
1159
|
+
count: 45,
|
|
1160
|
+
line: (): string => {
|
|
1161
|
+
return "";
|
|
1162
|
+
},
|
|
1163
|
+
}).map(divider),
|
|
1164
|
+
],
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
const blocks: Array<JSONObject> = postedData(postSpy, 0)[
|
|
1168
|
+
"blocks"
|
|
1169
|
+
] as Array<JSONObject>;
|
|
1170
|
+
|
|
1171
|
+
expect(blocks.length).toBe(SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
1172
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MESSAGE);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
test("getModalBlock splits within a view's 100-block limit", () => {
|
|
1176
|
+
const modalBlock: WorkspaceModalBlock = {
|
|
1177
|
+
_type: "WorkspaceModalBlock",
|
|
1178
|
+
title: "Incident details",
|
|
1179
|
+
submitButtonTitle: "Close",
|
|
1180
|
+
cancelButtonTitle: "Cancel",
|
|
1181
|
+
actionId: "ViewIncident",
|
|
1182
|
+
actionValue: "incident-id",
|
|
1183
|
+
blocks: [
|
|
1184
|
+
...repeatLines({
|
|
1185
|
+
count: 60,
|
|
1186
|
+
line: (): string => {
|
|
1187
|
+
return "";
|
|
1188
|
+
},
|
|
1189
|
+
}).map(divider),
|
|
1190
|
+
markdown(feed),
|
|
1191
|
+
],
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const view: JSONObject = SlackUtil.getModalBlock({
|
|
1195
|
+
payloadModalBlock: modalBlock,
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const blocks: Array<JSONObject> = view["blocks"] as Array<JSONObject>;
|
|
1199
|
+
|
|
1200
|
+
// Over a message's 50 blocks already, but a view still has room to split.
|
|
1201
|
+
expect(sectionTexts(blocks).length).toBeGreaterThan(1);
|
|
1202
|
+
expect(
|
|
1203
|
+
sectionTexts(blocks).some((text: string): boolean => {
|
|
1204
|
+
return text.endsWith(NOTE);
|
|
1205
|
+
}),
|
|
1206
|
+
).toBe(false);
|
|
1207
|
+
expectValidSlackBlocks(blocks, SlackUtil.MAX_BLOCKS_PER_MODAL);
|
|
1208
|
+
});
|
|
1209
|
+
});
|