@oneuptime/common 14.0.7 → 14.0.8
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/DatabaseModels/Incident.ts +267 -1
- package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
- package/Server/Infrastructure/Semaphore.ts +10 -0
- package/Server/Middleware/IdentityRateLimit.ts +108 -3
- package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
- package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentCustomFieldService.ts +276 -1
- package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
- package/Server/Services/IncidentEpisodeService.ts +15 -0
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
- package/Server/Services/IncidentPublicNoteService.ts +30 -1
- package/Server/Services/IncidentService.ts +1535 -65
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +68 -18
- package/Server/Services/SmsService.ts +10 -0
- package/Server/Services/StatusPageService.ts +65 -8
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
- package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
- package/Server/Services/StatusPageSubscriberService.ts +1161 -64
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/SSRFProtection.ts +76 -16
- package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
- package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
- package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
- package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
- package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
- package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
- package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
- package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
- package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
- package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
- package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
- package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
- package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
- package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
- package/Server/Utils/StatusPageResource.ts +51 -20
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
- package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
- package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
- package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
- package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
- package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
- package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +242 -5
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
- package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
- package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
- package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
- package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
- package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
- package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
- package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
- package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
- package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
- package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
- package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
- package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
- package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
- package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
- package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
- package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
- package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
- package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
- package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
- package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
- package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
- package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
- package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
- package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
- package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
- package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
- package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
- package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
- package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
- package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
- package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
- package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
- package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
- package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
- package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
- package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
- package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
- package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
- package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
- package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
- package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
- package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
- package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
- package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
- package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
- package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
- package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
- package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
- package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
- package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
- package/Tests/Types/SafeHtml.test.ts +102 -0
- package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
- package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
- package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
- package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
- package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
- package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
- package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
- package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
- package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
- package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
- package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
- package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
- package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
- package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
- package/Types/CustomField/CustomFieldOrder.ts +55 -0
- package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
- package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
- package/Types/CustomField/CustomFieldType.ts +14 -0
- package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
- package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
- package/Types/SafeHtml.ts +93 -0
- package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
- package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
- package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
- package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
- package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
- package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
- package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
- package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
- package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
- package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
- package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
- package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
- package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
- package/UI/Components/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- package/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
- package/Utils/Markdown/MarkdownEscape.ts +65 -0
- package/Utils/ProbeNetworkFailureUtil.ts +673 -0
- package/build/dist/Models/DatabaseModels/Incident.js +267 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +417 -176
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
- package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1052 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +47 -17
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/SmsService.js +2 -22
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +47 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +110 -6
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +48 -6
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageResource.js +39 -7
- package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
- package/build/dist/Types/SafeHtml.js +84 -0
- package/build/dist/Types/SafeHtml.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +1 -1
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
- package/build/dist/Utils/API.js +177 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
- package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
- package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -10,6 +10,14 @@ import QueryHelper from "../Types/Database/QueryHelper";
|
|
|
10
10
|
import DatabaseService from "./DatabaseService";
|
|
11
11
|
import IncidentCustomField from "../../Models/DatabaseModels/IncidentCustomField";
|
|
12
12
|
import CustomFieldMappingService from "./CustomFieldMappingService";
|
|
13
|
+
import IncidentCustomFieldService from "./IncidentCustomFieldService";
|
|
14
|
+
import { CustomFieldDefinition } from "../../Types/CustomField/CustomFieldDefinition";
|
|
15
|
+
import { mergeTemplateCustomFields } from "../../Types/CustomField/CustomFieldTemplateMerge";
|
|
16
|
+
import {
|
|
17
|
+
CustomFieldValueValidationError,
|
|
18
|
+
formatCustomFieldValueValidationErrors,
|
|
19
|
+
validateCustomFieldValues,
|
|
20
|
+
} from "../../Types/CustomField/CustomFieldValueValidator";
|
|
13
21
|
import AIRunService from "./AIRunService";
|
|
14
22
|
import AIRunType from "../../Types/AI/AIRunType";
|
|
15
23
|
import AIRunStatus from "../../Types/AI/AIRunStatus";
|
|
@@ -48,6 +56,18 @@ import DatabaseBaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/Dat
|
|
|
48
56
|
import SloRecordReferenceValidator from "../Utils/Slo/SloRecordReferenceValidator";
|
|
49
57
|
import UserNotificationEventType from "../../Types/UserNotification/UserNotificationEventType";
|
|
50
58
|
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
|
59
|
+
import IncidentCreatedRenotify from "../../Types/StatusPage/IncidentCreatedRenotify";
|
|
60
|
+
import IncidentCreatedResend from "../../Types/StatusPage/IncidentCreatedResend";
|
|
61
|
+
import IncidentScopeAddedPagesNotification, {
|
|
62
|
+
IncidentScopeAddedPagesNotificationAction,
|
|
63
|
+
StatusPageScopeChange,
|
|
64
|
+
} from "../../Types/StatusPage/IncidentScopeAddedPagesNotification";
|
|
65
|
+
import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
|
|
66
|
+
import StatusPage from "../../Models/DatabaseModels/StatusPage";
|
|
67
|
+
import StatusPageService from "./StatusPageService";
|
|
68
|
+
import StatusPageReadAccess from "../Utils/StatusPage/StatusPageReadAccess";
|
|
69
|
+
import SubscriberNotificationResendAccess from "../Utils/StatusPage/SubscriberNotificationResendAccess";
|
|
70
|
+
import Select from "../Types/Database/Select";
|
|
51
71
|
import DockerHost from "../../Models/DatabaseModels/DockerHost";
|
|
52
72
|
import PodmanHost from "../../Models/DatabaseModels/PodmanHost";
|
|
53
73
|
import Host from "../../Models/DatabaseModels/Host";
|
|
@@ -73,6 +93,7 @@ import ServiceType from "../../Types/Telemetry/ServiceType";
|
|
|
73
93
|
import OneUptimeDate from "../../Types/Date";
|
|
74
94
|
import TelemetryUtil from "../Utils/Telemetry/Telemetry";
|
|
75
95
|
import MetricResourceAttributeUtil from "../../Utils/Metrics/MetricResourceAttributeUtil";
|
|
96
|
+
import { escapeMarkdownInline } from "../../Utils/Markdown/MarkdownEscape";
|
|
76
97
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
77
98
|
import ProductAnalytics from "../Utils/ProductAnalytics";
|
|
78
99
|
import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
|
|
@@ -123,12 +144,38 @@ import {
|
|
|
123
144
|
INCIDENT_ALERT_IDS_TO_LINK_KEY,
|
|
124
145
|
} from "../../Types/Incident/IncidentAlertLink";
|
|
125
146
|
|
|
147
|
+
/*
|
|
148
|
+
* How an update changed an incident's status page scope, for its feed item.
|
|
149
|
+
* notificationQueued: the update also queued the 'created' notification for
|
|
150
|
+
* the added pages (IncidentScopeAddedPagesNotification).
|
|
151
|
+
* notificationAlreadyQueued: that notification was Pending or being sent, so
|
|
152
|
+
* the update left it alone; onUpdateSuccess checks it did not finish without
|
|
153
|
+
* the added pages in the meantime.
|
|
154
|
+
*/
|
|
155
|
+
type StatusPageScopeCarryForward = StatusPageScopeChange & {
|
|
156
|
+
isScoped: boolean;
|
|
157
|
+
notificationQueued: boolean;
|
|
158
|
+
notificationAlreadyQueued?: boolean | undefined;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* What adding status pages does to one matched incident's 'created'
|
|
163
|
+
* notification, and the record of told pages to write if it is queued with
|
|
164
|
+
* none (IncidentScopeAddedPagesNotification.getRecordToSeedOnQueue).
|
|
165
|
+
*/
|
|
166
|
+
interface AddedStatusPagesDecision {
|
|
167
|
+
incidentId: string;
|
|
168
|
+
action: IncidentScopeAddedPagesNotificationAction;
|
|
169
|
+
recordToSeed: Array<string> | undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
126
172
|
// key is incidentId for this dictionary.
|
|
127
173
|
type UpdateCarryForward = Dictionary<{
|
|
128
174
|
monitorsRemoved: Array<Monitor>;
|
|
129
175
|
monitorsAdded: Array<Monitor>;
|
|
130
176
|
oldChangeMonitorStatusIdTo: ObjectID | undefined;
|
|
131
177
|
newMonitorChangeStatusIdTo: ObjectID | undefined;
|
|
178
|
+
statusPageScopeChange?: StatusPageScopeCarryForward | undefined;
|
|
132
179
|
}>;
|
|
133
180
|
|
|
134
181
|
/*
|
|
@@ -490,6 +537,19 @@ export class Service extends DatabaseService<Model> {
|
|
|
490
537
|
updateBy.data.isVisibleOnStatusPage = false;
|
|
491
538
|
}
|
|
492
539
|
|
|
540
|
+
this.stripServiceOwnedScopeColumns(updateBy);
|
|
541
|
+
|
|
542
|
+
/*
|
|
543
|
+
* Before anything below adds values of its own to customFields (the
|
|
544
|
+
* mapped values copied from monitors), so only what the caller wrote is
|
|
545
|
+
* checked.
|
|
546
|
+
*/
|
|
547
|
+
await this.validateCustomFieldValuesOnUpdate(updateBy);
|
|
548
|
+
|
|
549
|
+
// Before the hooks below add a Pending of their own.
|
|
550
|
+
const isCreatedNotificationResendRequested: boolean =
|
|
551
|
+
this.isCreatedNotificationResendRequested(updateBy);
|
|
552
|
+
|
|
493
553
|
await this.validateProjectScopedReferences(updateBy);
|
|
494
554
|
|
|
495
555
|
const carryForward: UpdateCarryForward = {};
|
|
@@ -540,78 +600,1247 @@ export class Service extends DatabaseService<Model> {
|
|
|
540
600
|
}
|
|
541
601
|
}
|
|
542
602
|
|
|
543
|
-
if (isRemoved) {
|
|
544
|
-
carryForward[incident.id!.toString()]?.monitorsRemoved?.push(
|
|
545
|
-
monitor,
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
603
|
+
if (isRemoved) {
|
|
604
|
+
carryForward[incident.id!.toString()]?.monitorsRemoved?.push(
|
|
605
|
+
monitor,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (updateBy.data.monitors && updateBy.data.monitors.length > 0) {
|
|
611
|
+
for (const monitor of updateBy.data
|
|
612
|
+
?.monitors as unknown as Array<Monitor>) {
|
|
613
|
+
// check if this monitor is actually added.
|
|
614
|
+
let isAdded: boolean = true;
|
|
615
|
+
|
|
616
|
+
for (const existingMonitor of incident.monitors || []) {
|
|
617
|
+
if (
|
|
618
|
+
existingMonitor._id &&
|
|
619
|
+
existingMonitor._id.toString() === monitor._id?.toString()
|
|
620
|
+
) {
|
|
621
|
+
isAdded = false;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (isAdded) {
|
|
627
|
+
// this monitor is added.
|
|
628
|
+
carryForward[incident.id!.toString()]?.monitorsAdded?.push(
|
|
629
|
+
monitor,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Set notification status based on shouldStatusPageSubscribersBeNotifiedOnIncidentCreated if it's being updated
|
|
638
|
+
if (
|
|
639
|
+
updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated !==
|
|
640
|
+
undefined
|
|
641
|
+
) {
|
|
642
|
+
if (
|
|
643
|
+
updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
|
|
644
|
+
false
|
|
645
|
+
) {
|
|
646
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated =
|
|
647
|
+
StatusPageSubscriberNotificationStatus.Skipped;
|
|
648
|
+
updateBy.data.subscriberNotificationStatusMessage =
|
|
649
|
+
"Notifications skipped as subscribers are not to be notified for this incident.";
|
|
650
|
+
} else if (
|
|
651
|
+
updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
|
|
652
|
+
true
|
|
653
|
+
) {
|
|
654
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated =
|
|
655
|
+
StatusPageSubscriberNotificationStatus.Pending;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
await this.queueCreatedNotificationResendToAllStatusPagesIfRequested(
|
|
660
|
+
updateBy,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
/*
|
|
664
|
+
* Retry or Resend - a user's Pending - over a notification that is
|
|
665
|
+
* being sent would let a second run send it alongside, or be
|
|
666
|
+
* overwritten when the send settles (SubscriberNotificationResendAccess).
|
|
667
|
+
* 'Resend to all pages' above has refused that already, with its own
|
|
668
|
+
* reason; the hooks below queue their own Pending only from a settled
|
|
669
|
+
* state.
|
|
670
|
+
*/
|
|
671
|
+
await SubscriberNotificationResendAccess.assertNotQueuedWhileBeingSent({
|
|
672
|
+
modelType: Model,
|
|
673
|
+
service: this,
|
|
674
|
+
updateBy: updateBy,
|
|
675
|
+
statusColumns: [
|
|
676
|
+
"subscriberNotificationStatusOnIncidentCreated",
|
|
677
|
+
"subscriberNotificationStatusOnPostmortemPublished",
|
|
678
|
+
],
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
await this.clearCreatedNotificationRecordOnResend({
|
|
682
|
+
updateBy: updateBy,
|
|
683
|
+
isResendRequested: isCreatedNotificationResendRequested,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
await this.queueCreatedNotificationOnPublishIfRequested(updateBy);
|
|
687
|
+
|
|
688
|
+
await this.applyStatusPageScopeToUpdate(updateBy, carryForward);
|
|
689
|
+
|
|
690
|
+
/*
|
|
691
|
+
* Re-apply mapped custom field values. Covers the Custom Fields modal
|
|
692
|
+
* saving the whole bag back over a mapped value, and the incident's
|
|
693
|
+
* monitors being added or removed — both change what a mapped field should
|
|
694
|
+
* hold, and folding the answer into this payload keeps it one write with a
|
|
695
|
+
* truthful audit entry.
|
|
696
|
+
*/
|
|
697
|
+
await CustomFieldMappingService.applyMappingsToUpdate({
|
|
698
|
+
definitionModelType: IncidentCustomField,
|
|
699
|
+
updateBy: updateBy,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
updateBy: updateBy,
|
|
704
|
+
carryForward: carryForward,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/*
|
|
709
|
+
* The incidents an update will write to, as they are stored, for the hooks
|
|
710
|
+
* below that decide what else joins the update.
|
|
711
|
+
*
|
|
712
|
+
* Read as root: the answer only decides which columns join this update, and
|
|
713
|
+
* the update itself is still checked against the caller's permissions
|
|
714
|
+
* afterwards. Reading with the caller's props would fail the whole edit for
|
|
715
|
+
* a role that may edit incidents but not read these columns. The query
|
|
716
|
+
* already carries the caller's privacy filter (applyIncidentSelfPrivacyFilter),
|
|
717
|
+
* but not their tenant: the update's permission check adds it only after
|
|
718
|
+
* this hook runs. So a non-root caller's read is limited to their own
|
|
719
|
+
* project here, or a request for another project's incident id would be
|
|
720
|
+
* answered with that incident's state (a refusal that depends on it)
|
|
721
|
+
* instead of the usual "nothing updated".
|
|
722
|
+
*/
|
|
723
|
+
private async findIncidentsForUpdateHook(data: {
|
|
724
|
+
updateBy: UpdateBy<Model>;
|
|
725
|
+
select: Select<Model>;
|
|
726
|
+
}): Promise<Array<Model>> {
|
|
727
|
+
const updateBy: UpdateBy<Model> = data.updateBy;
|
|
728
|
+
|
|
729
|
+
const query: Query<Model> =
|
|
730
|
+
!updateBy.props.isRoot && updateBy.props.tenantId
|
|
731
|
+
? {
|
|
732
|
+
...updateBy.query,
|
|
733
|
+
projectId: updateBy.props.tenantId,
|
|
734
|
+
}
|
|
735
|
+
: updateBy.query;
|
|
736
|
+
|
|
737
|
+
return await this.findBy({
|
|
738
|
+
query: query,
|
|
739
|
+
select: data.select,
|
|
740
|
+
limit: LIMIT_MAX,
|
|
741
|
+
skip: 0,
|
|
742
|
+
props: {
|
|
743
|
+
isRoot: true,
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/*
|
|
749
|
+
* CUSTOM FIELD VALUES must fit their fields: a Number field holds a number,
|
|
750
|
+
* a Dropdown one of its options (CustomFieldValueValidator has the rules).
|
|
751
|
+
*
|
|
752
|
+
* Only for writes a user or an API key makes. Root writes - the server's
|
|
753
|
+
* own, such as values copied from monitors or an incident template - are
|
|
754
|
+
* trusted, and the mapped values are added after this runs, so they never
|
|
755
|
+
* reach it. Only the keys a write changes are checked, so a value stored
|
|
756
|
+
* before these checks, or a dropdown option removed since, does not stop
|
|
757
|
+
* someone saving the rest of the card.
|
|
758
|
+
*
|
|
759
|
+
* "Required on create" is not enforced here: incidents created by
|
|
760
|
+
* monitors, the API, Slack, Microsoft Teams or AI cannot fill in a form.
|
|
761
|
+
*/
|
|
762
|
+
private async getCustomFieldDefinitionsForValidation(
|
|
763
|
+
projectId: ObjectID,
|
|
764
|
+
): Promise<Array<CustomFieldDefinition>> {
|
|
765
|
+
const definitions: Array<IncidentCustomField> =
|
|
766
|
+
await IncidentCustomFieldService.findBy({
|
|
767
|
+
query: {
|
|
768
|
+
projectId: projectId,
|
|
769
|
+
},
|
|
770
|
+
select: {
|
|
771
|
+
name: true,
|
|
772
|
+
customFieldType: true,
|
|
773
|
+
dropdownOptions: true,
|
|
774
|
+
},
|
|
775
|
+
limit: LIMIT_PER_PROJECT,
|
|
776
|
+
skip: 0,
|
|
777
|
+
props: {
|
|
778
|
+
isRoot: true,
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const result: Array<CustomFieldDefinition> = [];
|
|
783
|
+
|
|
784
|
+
for (const definition of definitions) {
|
|
785
|
+
if (typeof definition.name !== "string") {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
result.push({
|
|
790
|
+
name: definition.name,
|
|
791
|
+
customFieldType: definition.customFieldType,
|
|
792
|
+
dropdownOptions: definition.dropdownOptions,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private hasCustomFieldValuesToValidate(customFields: unknown): boolean {
|
|
800
|
+
return (
|
|
801
|
+
customFields !== null &&
|
|
802
|
+
typeof customFields === "object" &&
|
|
803
|
+
!Array.isArray(customFields) &&
|
|
804
|
+
Object.keys(customFields as JSONObject).length > 0
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private throwIfCustomFieldValuesInvalid(
|
|
809
|
+
errors: Array<CustomFieldValueValidationError>,
|
|
810
|
+
): void {
|
|
811
|
+
if (errors.length > 0) {
|
|
812
|
+
throw new BadDataException(
|
|
813
|
+
formatCustomFieldValueValidationErrors(errors),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private async validateCustomFieldValuesOnCreate(data: {
|
|
819
|
+
createBy: CreateBy<Model>;
|
|
820
|
+
projectId: ObjectID;
|
|
821
|
+
}): Promise<void> {
|
|
822
|
+
const customFields: unknown = data.createBy.data.customFields;
|
|
823
|
+
|
|
824
|
+
if (
|
|
825
|
+
data.createBy.props.isRoot ||
|
|
826
|
+
!data.projectId ||
|
|
827
|
+
!this.hasCustomFieldValuesToValidate(customFields)
|
|
828
|
+
) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
this.throwIfCustomFieldValuesInvalid(
|
|
833
|
+
validateCustomFieldValues({
|
|
834
|
+
definitions: await this.getCustomFieldDefinitionsForValidation(
|
|
835
|
+
data.projectId,
|
|
836
|
+
),
|
|
837
|
+
customFields: customFields,
|
|
838
|
+
storedCustomFields: {},
|
|
839
|
+
}),
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/*
|
|
844
|
+
* Checked against each incident the update writes, because what counts as
|
|
845
|
+
* changed depends on what that incident already holds.
|
|
846
|
+
*/
|
|
847
|
+
private async validateCustomFieldValuesOnUpdate(
|
|
848
|
+
updateBy: UpdateBy<Model>,
|
|
849
|
+
): Promise<void> {
|
|
850
|
+
const customFields: unknown = updateBy.data.customFields;
|
|
851
|
+
|
|
852
|
+
if (
|
|
853
|
+
updateBy.props.isRoot ||
|
|
854
|
+
!this.hasCustomFieldValuesToValidate(customFields)
|
|
855
|
+
) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
|
|
860
|
+
updateBy: updateBy,
|
|
861
|
+
select: {
|
|
862
|
+
_id: true,
|
|
863
|
+
projectId: true,
|
|
864
|
+
customFields: true,
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
const definitionsByProject: Map<
|
|
869
|
+
string,
|
|
870
|
+
Array<CustomFieldDefinition>
|
|
871
|
+
> = new Map<string, Array<CustomFieldDefinition>>();
|
|
872
|
+
|
|
873
|
+
for (const incident of incidents) {
|
|
874
|
+
if (!incident.projectId) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const projectKey: string = incident.projectId.toString();
|
|
879
|
+
|
|
880
|
+
if (!definitionsByProject.has(projectKey)) {
|
|
881
|
+
definitionsByProject.set(
|
|
882
|
+
projectKey,
|
|
883
|
+
await this.getCustomFieldDefinitionsForValidation(incident.projectId),
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
this.throwIfCustomFieldValuesInvalid(
|
|
888
|
+
validateCustomFieldValues({
|
|
889
|
+
definitions: definitionsByProject.get(projectKey)!,
|
|
890
|
+
customFields: customFields,
|
|
891
|
+
storedCustomFields: incident.customFields,
|
|
892
|
+
}),
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/*
|
|
898
|
+
* isScopedToStatusPages and statusPagesNotifiedOnCreation are this
|
|
899
|
+
* service's to write. A client value for either is dropped before any hook
|
|
900
|
+
* runs, so what the hooks write is all that is written: the flag is derived
|
|
901
|
+
* from statusPages (applyStatusPageScopeToUpdate), and the record is the
|
|
902
|
+
* notification job's - a client must not be able to seed it and so keep
|
|
903
|
+
* pages from ever hearing about the incident, or clear it and have them
|
|
904
|
+
* told twice. Root (internal) callers keep the record they send.
|
|
905
|
+
*/
|
|
906
|
+
private stripServiceOwnedScopeColumns(updateBy: UpdateBy<Model>): void {
|
|
907
|
+
const data: Dictionary<unknown> = updateBy.data as Dictionary<unknown>;
|
|
908
|
+
|
|
909
|
+
delete data["isScopedToStatusPages"];
|
|
910
|
+
|
|
911
|
+
if (!updateBy.props.isRoot) {
|
|
912
|
+
delete data["statusPagesNotifiedOnCreation"];
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/*
|
|
917
|
+
* Whether an update itself asks for the 'created' notification to go out
|
|
918
|
+
* again: it sets the status to Pending - the API route of resending it,
|
|
919
|
+
* and the dashboard's Retry - turns notifying on creation on, which
|
|
920
|
+
* onBeforeUpdate maps to Pending, or asks for it to be sent to every
|
|
921
|
+
* status page again (IncidentCreatedResend, the dashboard's Resend). Read
|
|
922
|
+
* before any hook adds a Pending of its own.
|
|
923
|
+
*/
|
|
924
|
+
private isCreatedNotificationResendRequested(
|
|
925
|
+
updateBy: UpdateBy<Model>,
|
|
926
|
+
): boolean {
|
|
927
|
+
return (
|
|
928
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated ===
|
|
929
|
+
StatusPageSubscriberNotificationStatus.Pending ||
|
|
930
|
+
updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
|
|
931
|
+
true ||
|
|
932
|
+
IncidentCreatedResend.isRequested(updateBy.miscDataProps)
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/*
|
|
937
|
+
* 'Resend to all pages' (see IncidentCreatedResend): the 'created'
|
|
938
|
+
* notification goes back to Pending with a message saying so, and
|
|
939
|
+
* clearCreatedNotificationRecordOnResend empties its record of told pages
|
|
940
|
+
* in the same write, so the job sends it to every page the incident
|
|
941
|
+
* reaches now - after a failure too, where a plain Retry would resume.
|
|
942
|
+
*
|
|
943
|
+
* Everything is written into the caller's own update, so it lands with the
|
|
944
|
+
* rest of it or not at all, and the column check that runs after this hook
|
|
945
|
+
* still decides whether the caller may write the status. That check is
|
|
946
|
+
* also made here first, before the incidents are read: the refusals below
|
|
947
|
+
* depend on the notification's state, and a caller who may not send it
|
|
948
|
+
* again is told only that. The incidents are then read with the caller's
|
|
949
|
+
* own permissions (and the privacy filter already on the query), so an
|
|
950
|
+
* incident they cannot see never decides the answer; when they can see
|
|
951
|
+
* none of those the update matches, nothing is queued.
|
|
952
|
+
*
|
|
953
|
+
* Refused - with the reason, rather than silently dropped, because asking
|
|
954
|
+
* for it is the whole point of the request - when any matched incident's
|
|
955
|
+
* notification did not go out (Skipped) or is on its way (Pending,
|
|
956
|
+
* InProgress), and when the same update sets the status to anything but
|
|
957
|
+
* Pending.
|
|
958
|
+
*/
|
|
959
|
+
private async queueCreatedNotificationResendToAllStatusPagesIfRequested(
|
|
960
|
+
updateBy: UpdateBy<Model>,
|
|
961
|
+
): Promise<void> {
|
|
962
|
+
if (!IncidentCreatedResend.isRequested(updateBy.miscDataProps)) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const requestedStatus: unknown =
|
|
967
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated;
|
|
968
|
+
|
|
969
|
+
if (
|
|
970
|
+
requestedStatus !== undefined &&
|
|
971
|
+
requestedStatus !== StatusPageSubscriberNotificationStatus.Pending
|
|
972
|
+
) {
|
|
973
|
+
throw new BadDataException(
|
|
974
|
+
IncidentCreatedResend.conflictingStatusMessage,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
SubscriberNotificationResendAccess.assertCallerMayUpdateColumns({
|
|
979
|
+
modelType: Model,
|
|
980
|
+
columns: {
|
|
981
|
+
subscriberNotificationStatusOnIncidentCreated:
|
|
982
|
+
StatusPageSubscriberNotificationStatus.Pending,
|
|
983
|
+
subscriberNotificationStatusMessage:
|
|
984
|
+
IncidentCreatedResend.queuedMessage,
|
|
985
|
+
statusPagesNotifiedOnCreation: [],
|
|
986
|
+
},
|
|
987
|
+
props: updateBy.props,
|
|
988
|
+
refusal: IncidentCreatedResend.noPermissionMessage,
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
const incidents: Array<Model> = await this.findBy({
|
|
992
|
+
query: updateBy.query,
|
|
993
|
+
select: {
|
|
994
|
+
_id: true,
|
|
995
|
+
subscriberNotificationStatusOnIncidentCreated: true,
|
|
996
|
+
},
|
|
997
|
+
limit: LIMIT_MAX,
|
|
998
|
+
skip: 0,
|
|
999
|
+
props: updateBy.props,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
for (const incident of incidents) {
|
|
1003
|
+
const refusal: string | null = IncidentCreatedResend.getRefusalReason(
|
|
1004
|
+
incident.subscriberNotificationStatusOnIncidentCreated,
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
if (refusal) {
|
|
1008
|
+
throw new BadDataException(refusal);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (incidents.length === 0) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated =
|
|
1017
|
+
StatusPageSubscriberNotificationStatus.Pending;
|
|
1018
|
+
|
|
1019
|
+
if (updateBy.data.subscriberNotificationStatusMessage === undefined) {
|
|
1020
|
+
updateBy.data.subscriberNotificationStatusMessage =
|
|
1021
|
+
IncidentCreatedResend.queuedMessage;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/*
|
|
1026
|
+
* A resend asked for by the update itself (see
|
|
1027
|
+
* isCreatedNotificationResendRequested) goes to every status page, as it
|
|
1028
|
+
* always did. The job skips the pages in its record of told pages
|
|
1029
|
+
* (Incident.statusPagesNotifiedOnCreation), so without this a resend of a
|
|
1030
|
+
* notification that went out would reach nobody: the record is emptied in
|
|
1031
|
+
* the same write.
|
|
1032
|
+
*
|
|
1033
|
+
* It is kept in three cases:
|
|
1034
|
+
* - Failed: Retry resumes where the failed send stopped, so the pages it
|
|
1035
|
+
* told are not told twice - unless the update asks for it to go to every
|
|
1036
|
+
* page again (IncidentCreatedResend), which is what that request is for;
|
|
1037
|
+
* - Pending or InProgress: the notification is on its way already, and its
|
|
1038
|
+
* record is what keeps a send queued for added pages from reaching the
|
|
1039
|
+
* pages told before (IncidentCreatedResend refuses these);
|
|
1040
|
+
* - a root caller that writes the record itself.
|
|
1041
|
+
*
|
|
1042
|
+
* An update matching several incidents empties the record when any of them
|
|
1043
|
+
* would otherwise be resent to nobody, because the write applies the same
|
|
1044
|
+
* data to all of them.
|
|
1045
|
+
*/
|
|
1046
|
+
private async clearCreatedNotificationRecordOnResend(data: {
|
|
1047
|
+
updateBy: UpdateBy<Model>;
|
|
1048
|
+
isResendRequested: boolean;
|
|
1049
|
+
}): Promise<void> {
|
|
1050
|
+
const updateBy: UpdateBy<Model> = data.updateBy;
|
|
1051
|
+
|
|
1052
|
+
if (!data.isResendRequested) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (
|
|
1057
|
+
(updateBy.data as Dictionary<unknown>)[
|
|
1058
|
+
"statusPagesNotifiedOnCreation"
|
|
1059
|
+
] !== undefined
|
|
1060
|
+
) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (IncidentCreatedResend.isRequested(updateBy.miscDataProps)) {
|
|
1065
|
+
/*
|
|
1066
|
+
* Queued for every page by
|
|
1067
|
+
* queueCreatedNotificationResendToAllStatusPagesIfRequested, which
|
|
1068
|
+
* left the status alone when the caller can see none of the incidents
|
|
1069
|
+
* the update matches: nothing to read to decide.
|
|
1070
|
+
*/
|
|
1071
|
+
if (
|
|
1072
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated ===
|
|
1073
|
+
StatusPageSubscriberNotificationStatus.Pending
|
|
1074
|
+
) {
|
|
1075
|
+
updateBy.data.statusPagesNotifiedOnCreation = [];
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
|
|
1082
|
+
updateBy: updateBy,
|
|
1083
|
+
select: {
|
|
1084
|
+
_id: true,
|
|
1085
|
+
subscriberNotificationStatusOnIncidentCreated: true,
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const someIncidentWouldReachNobody: boolean = incidents.some(
|
|
1090
|
+
(incident: Model): boolean => {
|
|
1091
|
+
return ![
|
|
1092
|
+
StatusPageSubscriberNotificationStatus.Failed,
|
|
1093
|
+
StatusPageSubscriberNotificationStatus.Pending,
|
|
1094
|
+
StatusPageSubscriberNotificationStatus.InProgress,
|
|
1095
|
+
].includes(
|
|
1096
|
+
incident.subscriberNotificationStatusOnIncidentCreated as StatusPageSubscriberNotificationStatus,
|
|
1097
|
+
);
|
|
1098
|
+
},
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
if (someIncidentWouldReachNobody) {
|
|
1102
|
+
updateBy.data.statusPagesNotifiedOnCreation = [];
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/*
|
|
1107
|
+
* Publishing a hidden incident can announce it: when the editor turns
|
|
1108
|
+
* 'Visible on Status Page' on and ticks "Notify subscribers that this
|
|
1109
|
+
* incident was created" (see IncidentCreatedRenotify), the incident's
|
|
1110
|
+
* 'created' notification goes back to Pending, and the
|
|
1111
|
+
* Incident:SendNotificationToSubscribers job sends it now that the incident
|
|
1112
|
+
* is visible - to the pages its record of told pages does not list, so an
|
|
1113
|
+
* incident whose notification went out before it was hidden tells only the
|
|
1114
|
+
* pages added while it was hidden.
|
|
1115
|
+
*
|
|
1116
|
+
* It only ever re-queues a notification with someone left to tell, for an
|
|
1117
|
+
* incident that is hidden today, not private, and meant to notify on
|
|
1118
|
+
* creation, so a stray request cannot email subscribers twice or break a
|
|
1119
|
+
* deliberate "don't notify". The status is written into this same update,
|
|
1120
|
+
* so it lands together with the visibility change or not at all. The
|
|
1121
|
+
* column (and its message) are computed, but their update ACL lists the
|
|
1122
|
+
* incident roles, so a non-root edit still passes the column check that
|
|
1123
|
+
* runs after this hook.
|
|
1124
|
+
*
|
|
1125
|
+
* An update that sets the status itself - the API route of resetting it to
|
|
1126
|
+
* Pending, or the notify-on-create flag above - is left alone. An update
|
|
1127
|
+
* that matches several incidents queues only when every one of them
|
|
1128
|
+
* qualifies, because the write applies the same data to all of them.
|
|
1129
|
+
*/
|
|
1130
|
+
private async queueCreatedNotificationOnPublishIfRequested(
|
|
1131
|
+
updateBy: UpdateBy<Model>,
|
|
1132
|
+
): Promise<void> {
|
|
1133
|
+
if (!IncidentCreatedRenotify.isRequested(updateBy.miscDataProps)) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (updateBy.data.isVisibleOnStatusPage !== true) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (
|
|
1142
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated !== undefined
|
|
1143
|
+
) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
|
|
1148
|
+
updateBy: updateBy,
|
|
1149
|
+
select: {
|
|
1150
|
+
_id: true,
|
|
1151
|
+
isVisibleOnStatusPage: true,
|
|
1152
|
+
isPrivate: true,
|
|
1153
|
+
subscriberNotificationStatusOnIncidentCreated: true,
|
|
1154
|
+
shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
|
|
1155
|
+
statusPagesNotifiedOnCreation: true,
|
|
1156
|
+
statusPages: {
|
|
1157
|
+
_id: true,
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
if (incidents.length === 0) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const everyIncidentQualifies: boolean = incidents.every(
|
|
1167
|
+
(incident: Model): boolean => {
|
|
1168
|
+
return IncidentCreatedRenotify.canRenotifyOnPublish({
|
|
1169
|
+
isVisibleOnStatusPage: incident.isVisibleOnStatusPage,
|
|
1170
|
+
isPrivate:
|
|
1171
|
+
updateBy.data.isPrivate !== undefined &&
|
|
1172
|
+
updateBy.data.isPrivate !== null
|
|
1173
|
+
? (updateBy.data.isPrivate as boolean)
|
|
1174
|
+
: incident.isPrivate,
|
|
1175
|
+
subscriberNotificationStatusOnIncidentCreated:
|
|
1176
|
+
incident.subscriberNotificationStatusOnIncidentCreated,
|
|
1177
|
+
shouldStatusPageSubscribersBeNotifiedOnIncidentCreated:
|
|
1178
|
+
incident.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
|
|
1179
|
+
// The pages it is limited to once this update is written.
|
|
1180
|
+
statusPages:
|
|
1181
|
+
updateBy.data.statusPages !== undefined
|
|
1182
|
+
? updateBy.data.statusPages
|
|
1183
|
+
: incident.statusPages,
|
|
1184
|
+
statusPagesNotifiedOnCreation: incident.statusPagesNotifiedOnCreation,
|
|
1185
|
+
});
|
|
1186
|
+
},
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
if (!everyIncidentQualifies) {
|
|
1190
|
+
logger.debug(
|
|
1191
|
+
`Not re-queueing the incident created notification on publish: ${incidents.length} incident(s) matched and not all of them were hidden, set to notify subscribers and had subscribers left to tell.`,
|
|
1192
|
+
);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated =
|
|
1197
|
+
StatusPageSubscriberNotificationStatus.Pending;
|
|
1198
|
+
updateBy.data.subscriberNotificationStatusMessage =
|
|
1199
|
+
IncidentCreatedRenotify.queuedMessage;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/*
|
|
1203
|
+
* The status pages a new incident is limited to (Incident.statusPages), and
|
|
1204
|
+
* the two columns that follow from them.
|
|
1205
|
+
*
|
|
1206
|
+
* isScopedToStatusPages is derived from the list, whatever the caller sent
|
|
1207
|
+
* for it: the column is computed, which exempts it from the column check on
|
|
1208
|
+
* create, so a client value would otherwise be stored as is - and a scoped
|
|
1209
|
+
* flag with no pages hides the incident from every status page.
|
|
1210
|
+
*
|
|
1211
|
+
* statusPagesNotifiedOnCreation is the created-notification job's record of
|
|
1212
|
+
* the pages it told. A client must not be able to seed it and so keep pages
|
|
1213
|
+
* from ever hearing about the incident; only root (internal) callers keep
|
|
1214
|
+
* what they send.
|
|
1215
|
+
*
|
|
1216
|
+
* The pages a non-root caller picks must be pages they can read (see
|
|
1217
|
+
* StatusPageReadAccess), except the pages of an incident template they can
|
|
1218
|
+
* read: the dashboard fills a template's pages in for everyone who declares
|
|
1219
|
+
* from it, which is what the template is for. Pages copied from a template
|
|
1220
|
+
* on the server are not the caller's pick. That is checked once the pages
|
|
1221
|
+
* are known to be the project's (assertCallerCanPickStatusPagesOnCreate).
|
|
1222
|
+
*
|
|
1223
|
+
* `isScopedToNothingByTemplate`: declared through the API from a template
|
|
1224
|
+
* whose status pages were all deleted. The incident is scoped with no
|
|
1225
|
+
* pages - hidden from every status page - rather than unscoped.
|
|
1226
|
+
*/
|
|
1227
|
+
private applyStatusPageScopeToCreate(data: {
|
|
1228
|
+
createBy: CreateBy<Model>;
|
|
1229
|
+
isScopedToNothingByTemplate?: boolean | undefined;
|
|
1230
|
+
}): void {
|
|
1231
|
+
const createBy: CreateBy<Model> = data.createBy;
|
|
1232
|
+
|
|
1233
|
+
if (!createBy.props.isRoot) {
|
|
1234
|
+
delete createBy.data.statusPagesNotifiedOnCreation;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const statusPageIds: Array<string> =
|
|
1238
|
+
IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
|
|
1239
|
+
createBy.data.statusPages,
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
if (
|
|
1243
|
+
createBy.data.statusPages !== undefined &&
|
|
1244
|
+
createBy.data.statusPages !== null
|
|
1245
|
+
) {
|
|
1246
|
+
createBy.data.statusPages = this.toStatusPageStubs(statusPageIds);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
createBy.data.isScopedToStatusPages =
|
|
1250
|
+
statusPageIds.length > 0 || data.isScopedToNothingByTemplate === true;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/*
|
|
1254
|
+
* The pages a non-root caller picks for a new incident must be pages they
|
|
1255
|
+
* can read (see applyStatusPageScopeToCreate). Runs after the project check,
|
|
1256
|
+
* so another project's page, or one that does not exist, is reported as
|
|
1257
|
+
* such rather than as a page the caller cannot read.
|
|
1258
|
+
*/
|
|
1259
|
+
private async assertCallerCanPickStatusPagesOnCreate(data: {
|
|
1260
|
+
createBy: CreateBy<Model>;
|
|
1261
|
+
projectId: ObjectID;
|
|
1262
|
+
statusPagesFromCaller: boolean;
|
|
1263
|
+
}): Promise<void> {
|
|
1264
|
+
const createBy: CreateBy<Model> = data.createBy;
|
|
1265
|
+
|
|
1266
|
+
const statusPageIds: Array<string> =
|
|
1267
|
+
IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
|
|
1268
|
+
createBy.data.statusPages,
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
if (
|
|
1272
|
+
createBy.props.isRoot ||
|
|
1273
|
+
!data.statusPagesFromCaller ||
|
|
1274
|
+
statusPageIds.length === 0
|
|
1275
|
+
) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const readableStatusPageIds: Array<string> =
|
|
1280
|
+
await StatusPageReadAccess.getReadableStatusPageIds({
|
|
1281
|
+
statusPageIds: statusPageIds,
|
|
1282
|
+
props: createBy.props,
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
let unreadableStatusPageIds: Array<string> = statusPageIds.filter(
|
|
1286
|
+
(id: string): boolean => {
|
|
1287
|
+
return !readableStatusPageIds.includes(id);
|
|
1288
|
+
},
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
if (unreadableStatusPageIds.length > 0) {
|
|
1292
|
+
const templateStatusPageIds: Array<string> =
|
|
1293
|
+
await this.getStatusPageIdsOfTemplatesReadableByCaller({
|
|
1294
|
+
projectId: data.projectId,
|
|
1295
|
+
props: createBy.props,
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
unreadableStatusPageIds = unreadableStatusPageIds.filter(
|
|
1299
|
+
(id: string): boolean => {
|
|
1300
|
+
return !templateStatusPageIds.includes(id);
|
|
1301
|
+
},
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (unreadableStatusPageIds.length > 0) {
|
|
1306
|
+
throw new NotAuthorizedException(
|
|
1307
|
+
StatusPageReadAccess.getRefusalMessage("incident"),
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/*
|
|
1313
|
+
* The status pages the incident templates the caller can read are limited
|
|
1314
|
+
* to. Such a page may be picked for a new incident without reading it:
|
|
1315
|
+
* declaring from the template picks it anyway. The templates are read with
|
|
1316
|
+
* the caller's permissions (and labels); their pages as root. A caller who
|
|
1317
|
+
* cannot read templates at all gets none.
|
|
1318
|
+
*/
|
|
1319
|
+
private async getStatusPageIdsOfTemplatesReadableByCaller(data: {
|
|
1320
|
+
projectId: ObjectID;
|
|
1321
|
+
props: DatabaseCommonInteractionProps;
|
|
1322
|
+
}): Promise<Array<string>> {
|
|
1323
|
+
let templates: Array<IncidentTemplate> = [];
|
|
1324
|
+
|
|
1325
|
+
try {
|
|
1326
|
+
templates = await IncidentTemplateService.findBy({
|
|
1327
|
+
query: {
|
|
1328
|
+
projectId: data.projectId,
|
|
1329
|
+
},
|
|
1330
|
+
select: {
|
|
1331
|
+
_id: true,
|
|
1332
|
+
},
|
|
1333
|
+
limit: LIMIT_MAX,
|
|
1334
|
+
skip: 0,
|
|
1335
|
+
props: data.props,
|
|
1336
|
+
});
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
// No access to templates (or not on a plan that has them): no pages.
|
|
1339
|
+
logger.debug(
|
|
1340
|
+
`Could not read the caller's incident templates while checking the status pages of a new incident: ${err}`,
|
|
1341
|
+
);
|
|
1342
|
+
return [];
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (templates.length === 0) {
|
|
1346
|
+
return [];
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const templatesWithPages: Array<IncidentTemplate> =
|
|
1350
|
+
await IncidentTemplateService.findBy({
|
|
1351
|
+
query: {
|
|
1352
|
+
_id: QueryHelper.any(
|
|
1353
|
+
templates.map((template: IncidentTemplate): string => {
|
|
1354
|
+
return template.id!.toString();
|
|
1355
|
+
}),
|
|
1356
|
+
),
|
|
1357
|
+
projectId: data.projectId,
|
|
1358
|
+
},
|
|
1359
|
+
select: {
|
|
1360
|
+
_id: true,
|
|
1361
|
+
statusPages: {
|
|
1362
|
+
_id: true,
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1365
|
+
limit: LIMIT_MAX,
|
|
1366
|
+
skip: 0,
|
|
1367
|
+
props: {
|
|
1368
|
+
isRoot: true,
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
return IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
|
|
1373
|
+
templatesWithPages.flatMap(
|
|
1374
|
+
(template: IncidentTemplate): Array<StatusPage> => {
|
|
1375
|
+
return template.statusPages || [];
|
|
1376
|
+
},
|
|
1377
|
+
),
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/*
|
|
1382
|
+
* The status pages an incident is limited to, on update. Everything that
|
|
1383
|
+
* follows from a write to Incident.statusPages happens here, in the
|
|
1384
|
+
* caller's own update, so the list, the flag and a re-queued notification
|
|
1385
|
+
* land together or not at all:
|
|
1386
|
+
*
|
|
1387
|
+
* - isScopedToStatusPages is derived from the list the update writes (a
|
|
1388
|
+
* value the caller sent for it was dropped by
|
|
1389
|
+
* stripServiceOwnedScopeColumns). Nothing else ever sets it, and it is
|
|
1390
|
+
* never recomputed when join rows disappear: deleting the only page an
|
|
1391
|
+
* incident is scoped to leaves it scoped to nothing, hidden from every
|
|
1392
|
+
* page, rather than widened to all of them;
|
|
1393
|
+
* - a non-root caller's list is topped up with the pages the incident is
|
|
1394
|
+
* scoped to that the caller cannot read. Status pages are label-scoped,
|
|
1395
|
+
* so an editor may see only some of an incident's pages, and a list write
|
|
1396
|
+
* replaces the whole list (the dashboard sends back what the editor
|
|
1397
|
+
* sees). Without this, saving the incident would silently drop every page
|
|
1398
|
+
* the editor was not shown;
|
|
1399
|
+
* - the pages a non-root caller adds must be pages they can read (see
|
|
1400
|
+
* StatusPageReadAccess);
|
|
1401
|
+
* - with IncidentScopeAddedPagesNotification requested, the 'created'
|
|
1402
|
+
* notification is queued again for the pages the update adds;
|
|
1403
|
+
* - what was added and removed is carried forward for the feed item
|
|
1404
|
+
* onUpdateSuccess writes, and so is whether the notification was on its
|
|
1405
|
+
* way already, which onUpdateSuccess checks again once the pages are
|
|
1406
|
+
* written.
|
|
1407
|
+
*
|
|
1408
|
+
* The flag and the notification columns are computed, but their update
|
|
1409
|
+
* access control lists every role that may edit statusPages, so a non-root
|
|
1410
|
+
* edit passes the column check that runs after this hook.
|
|
1411
|
+
*/
|
|
1412
|
+
private async applyStatusPageScopeToUpdate(
|
|
1413
|
+
updateBy: UpdateBy<Model>,
|
|
1414
|
+
carryForward: UpdateCarryForward,
|
|
1415
|
+
): Promise<void> {
|
|
1416
|
+
const data: Dictionary<unknown> = updateBy.data as Dictionary<unknown>;
|
|
1417
|
+
|
|
1418
|
+
if (data["statusPages"] === undefined) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const incidents: Array<Model> = await this.findIncidentsForUpdateHook({
|
|
1423
|
+
updateBy: updateBy,
|
|
1424
|
+
select: {
|
|
1425
|
+
_id: true,
|
|
1426
|
+
statusPages: {
|
|
1427
|
+
_id: true,
|
|
1428
|
+
},
|
|
1429
|
+
isVisibleOnStatusPage: true,
|
|
1430
|
+
isPrivate: true,
|
|
1431
|
+
subscriberNotificationStatusOnIncidentCreated: true,
|
|
1432
|
+
shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
|
|
1433
|
+
statusPagesNotifiedOnCreation: true,
|
|
1434
|
+
},
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
const requestedStatusPageIds: Array<string> =
|
|
1438
|
+
IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
|
|
1439
|
+
data["statusPages"],
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
const hiddenStatusPageIds: Array<string> = updateBy.props.isRoot
|
|
1443
|
+
? []
|
|
1444
|
+
: await this.getScopedStatusPageIdsHiddenFromCaller({
|
|
1445
|
+
incidents: incidents,
|
|
1446
|
+
props: updateBy.props,
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
const statusPageIds: Array<string> = [
|
|
1450
|
+
...requestedStatusPageIds,
|
|
1451
|
+
...hiddenStatusPageIds.filter((id: string) => {
|
|
1452
|
+
return !requestedStatusPageIds.includes(id);
|
|
1453
|
+
}),
|
|
1454
|
+
];
|
|
1455
|
+
|
|
1456
|
+
if (!updateBy.props.isRoot) {
|
|
1457
|
+
await StatusPageReadAccess.assertCallerCanPickStatusPages({
|
|
1458
|
+
statusPageIds: this.getStatusPageIdsAddedToAny({
|
|
1459
|
+
incidents: incidents,
|
|
1460
|
+
statusPageIds: statusPageIds,
|
|
1461
|
+
}),
|
|
1462
|
+
props: updateBy.props,
|
|
1463
|
+
subject: "incident",
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
data["statusPages"] = this.toStatusPageStubs(statusPageIds);
|
|
1468
|
+
data["isScopedToStatusPages"] = statusPageIds.length > 0;
|
|
1469
|
+
|
|
1470
|
+
const decisions: Array<AddedStatusPagesDecision> = [];
|
|
1471
|
+
|
|
1472
|
+
for (const incident of incidents) {
|
|
1473
|
+
const incidentId: string = incident.id!.toString();
|
|
1474
|
+
|
|
1475
|
+
const change: StatusPageScopeChange =
|
|
1476
|
+
IncidentScopeAddedPagesNotification.getScopeChange({
|
|
1477
|
+
before: incident.statusPages,
|
|
1478
|
+
after: statusPageIds,
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
const action: IncidentScopeAddedPagesNotificationAction =
|
|
1482
|
+
IncidentScopeAddedPagesNotification.getAction({
|
|
1483
|
+
addedStatusPageIds: change.addedStatusPageIds,
|
|
1484
|
+
incident: {
|
|
1485
|
+
subscriberNotificationStatusOnIncidentCreated:
|
|
1486
|
+
incident.subscriberNotificationStatusOnIncidentCreated,
|
|
1487
|
+
shouldStatusPageSubscribersBeNotifiedOnIncidentCreated:
|
|
1488
|
+
incident.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
|
|
1489
|
+
isVisibleOnStatusPage: this.getValueAfterUpdate(
|
|
1490
|
+
updateBy.data.isVisibleOnStatusPage,
|
|
1491
|
+
incident.isVisibleOnStatusPage,
|
|
1492
|
+
),
|
|
1493
|
+
isPrivate: this.getValueAfterUpdate(
|
|
1494
|
+
updateBy.data.isPrivate,
|
|
1495
|
+
incident.isPrivate,
|
|
1496
|
+
),
|
|
1497
|
+
statusPagesNotifiedOnCreation:
|
|
1498
|
+
incident.statusPagesNotifiedOnCreation,
|
|
1499
|
+
},
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
carryForward[incidentId] = {
|
|
1503
|
+
monitorsRemoved: [],
|
|
1504
|
+
monitorsAdded: [],
|
|
1505
|
+
oldChangeMonitorStatusIdTo: undefined,
|
|
1506
|
+
newMonitorChangeStatusIdTo: undefined,
|
|
1507
|
+
...carryForward[incidentId],
|
|
1508
|
+
statusPageScopeChange: {
|
|
1509
|
+
...change,
|
|
1510
|
+
isScoped: statusPageIds.length > 0,
|
|
1511
|
+
notificationQueued: false,
|
|
1512
|
+
/*
|
|
1513
|
+
* On its way already, and nothing in this update sets the status:
|
|
1514
|
+
* onUpdateSuccess makes sure the send did not finish without the
|
|
1515
|
+
* added pages in the meantime.
|
|
1516
|
+
*/
|
|
1517
|
+
notificationAlreadyQueued:
|
|
1518
|
+
action ===
|
|
1519
|
+
IncidentScopeAddedPagesNotificationAction.AlreadyQueued &&
|
|
1520
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated ===
|
|
1521
|
+
undefined,
|
|
1522
|
+
},
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
decisions.push({
|
|
1526
|
+
incidentId: incidentId,
|
|
1527
|
+
action: action,
|
|
1528
|
+
recordToSeed:
|
|
1529
|
+
action === IncidentScopeAddedPagesNotificationAction.Queue
|
|
1530
|
+
? IncidentScopeAddedPagesNotification.getRecordToSeedOnQueue({
|
|
1531
|
+
statusPagesNotifiedOnCreation:
|
|
1532
|
+
incident.statusPagesNotifiedOnCreation,
|
|
1533
|
+
statusPageIdsBeforeUpdate: incident.statusPages,
|
|
1534
|
+
})
|
|
1535
|
+
: undefined,
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
this.queueCreatedNotificationForAddedStatusPagesIfRequested({
|
|
1540
|
+
updateBy: updateBy,
|
|
1541
|
+
decisions: decisions,
|
|
1542
|
+
carryForward: carryForward,
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/*
|
|
1547
|
+
* The pages an update writing `statusPageIds` adds to at least one of the
|
|
1548
|
+
* incidents it matches.
|
|
1549
|
+
*/
|
|
1550
|
+
private getStatusPageIdsAddedToAny(data: {
|
|
1551
|
+
incidents: Array<Model>;
|
|
1552
|
+
statusPageIds: Array<string>;
|
|
1553
|
+
}): Array<string> {
|
|
1554
|
+
const added: Array<string> = [];
|
|
1555
|
+
|
|
1556
|
+
for (const incident of data.incidents) {
|
|
1557
|
+
for (const id of IncidentScopeAddedPagesNotification.getScopeChange({
|
|
1558
|
+
before: incident.statusPages,
|
|
1559
|
+
after: data.statusPageIds,
|
|
1560
|
+
}).addedStatusPageIds) {
|
|
1561
|
+
if (!added.includes(id)) {
|
|
1562
|
+
added.push(id);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
return added;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/*
|
|
1571
|
+
* Adding status pages to an incident's scope can tell them the incident was
|
|
1572
|
+
* created: with IncidentScopeAddedPagesNotification requested, the 'created'
|
|
1573
|
+
* notification goes back to Pending, and the job sends it to the pages it
|
|
1574
|
+
* has no record of telling (Incident.statusPagesNotifiedOnCreation). See
|
|
1575
|
+
* IncidentScopeAddedPagesNotification.getAction for when it applies.
|
|
1576
|
+
*
|
|
1577
|
+
* A notification that was never sent has no record yet, so the pages the
|
|
1578
|
+
* incident was limited to before are written in as the record in the same
|
|
1579
|
+
* write (getRecordToSeedOnQueue): the job then tells only the added pages.
|
|
1580
|
+
*
|
|
1581
|
+
* An update that sets the status itself - the API route of resetting it to
|
|
1582
|
+
* Pending, the notify-on-create flag, or publishing a hidden incident - is
|
|
1583
|
+
* left alone. An update that matches several incidents queues only when it
|
|
1584
|
+
* may for every one of them, because the write applies the same data to
|
|
1585
|
+
* all. For the same reason it cannot write a record that differs between
|
|
1586
|
+
* them, or overwrite the record of an incident that has one, and is refused
|
|
1587
|
+
* instead.
|
|
1588
|
+
*/
|
|
1589
|
+
private queueCreatedNotificationForAddedStatusPagesIfRequested(data: {
|
|
1590
|
+
updateBy: UpdateBy<Model>;
|
|
1591
|
+
decisions: Array<AddedStatusPagesDecision>;
|
|
1592
|
+
carryForward: UpdateCarryForward;
|
|
1593
|
+
}): void {
|
|
1594
|
+
const updateBy: UpdateBy<Model> = data.updateBy;
|
|
1595
|
+
|
|
1596
|
+
if (
|
|
1597
|
+
!IncidentScopeAddedPagesNotification.isRequested(updateBy.miscDataProps)
|
|
1598
|
+
) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (
|
|
1603
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated !== undefined
|
|
1604
|
+
) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const actions: Array<IncidentScopeAddedPagesNotificationAction> =
|
|
1609
|
+
data.decisions.map(
|
|
1610
|
+
(
|
|
1611
|
+
decision: AddedStatusPagesDecision,
|
|
1612
|
+
): IncidentScopeAddedPagesNotificationAction => {
|
|
1613
|
+
return decision.action;
|
|
1614
|
+
},
|
|
1615
|
+
);
|
|
1616
|
+
|
|
1617
|
+
if (!actions.includes(IncidentScopeAddedPagesNotificationAction.Queue)) {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const everyIncidentMayQueue: boolean = actions.every(
|
|
1622
|
+
(action: IncidentScopeAddedPagesNotificationAction): boolean => {
|
|
1623
|
+
return (
|
|
1624
|
+
action === IncidentScopeAddedPagesNotificationAction.Queue ||
|
|
1625
|
+
action === IncidentScopeAddedPagesNotificationAction.AlreadyQueued
|
|
1626
|
+
);
|
|
1627
|
+
},
|
|
1628
|
+
);
|
|
1629
|
+
|
|
1630
|
+
if (!everyIncidentMayQueue) {
|
|
1631
|
+
logger.debug(
|
|
1632
|
+
`Not queueing the incident created notification for added status pages: ${actions.length} incident(s) matched and not all of them gained status pages and may be announced.`,
|
|
1633
|
+
);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const recordsToSeed: Array<Array<string>> = data.decisions
|
|
1638
|
+
.map((decision: AddedStatusPagesDecision): Array<string> | undefined => {
|
|
1639
|
+
return decision.recordToSeed;
|
|
1640
|
+
})
|
|
1641
|
+
.filter((record: Array<string> | undefined): boolean => {
|
|
1642
|
+
return record !== undefined;
|
|
1643
|
+
}) as Array<Array<string>>;
|
|
1644
|
+
|
|
1645
|
+
if (recordsToSeed.length > 0) {
|
|
1646
|
+
const recordToSeed: Array<string> = recordsToSeed[0]!;
|
|
549
1647
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
1648
|
+
const everyIncidentTakesTheSameRecord: boolean =
|
|
1649
|
+
recordsToSeed.length === data.decisions.length &&
|
|
1650
|
+
recordsToSeed.every((record: Array<string>): boolean => {
|
|
1651
|
+
return (
|
|
1652
|
+
[...record].sort().join(",") === [...recordToSeed].sort().join(",")
|
|
1653
|
+
);
|
|
1654
|
+
});
|
|
555
1655
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
isAdded = false;
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
1656
|
+
if (!everyIncidentTakesTheSameRecord) {
|
|
1657
|
+
throw new BadDataException(
|
|
1658
|
+
"These incidents cannot be sent the incident-created notification for their added status pages together: they were limited to different status pages, or not all of them were ever announced. Change them one at a time.",
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
565
1661
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1662
|
+
updateBy.data.statusPagesNotifiedOnCreation = recordToSeed;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
updateBy.data.subscriberNotificationStatusOnIncidentCreated =
|
|
1666
|
+
StatusPageSubscriberNotificationStatus.Pending;
|
|
1667
|
+
updateBy.data.subscriberNotificationStatusMessage =
|
|
1668
|
+
IncidentScopeAddedPagesNotification.queuedMessage;
|
|
1669
|
+
|
|
1670
|
+
for (const decision of data.decisions) {
|
|
1671
|
+
const scopeChange: StatusPageScopeCarryForward | undefined =
|
|
1672
|
+
data.carryForward[decision.incidentId]?.statusPageScopeChange;
|
|
1673
|
+
|
|
1674
|
+
if (
|
|
1675
|
+
scopeChange &&
|
|
1676
|
+
decision.action === IncidentScopeAddedPagesNotificationAction.Queue
|
|
1677
|
+
) {
|
|
1678
|
+
scopeChange.notificationQueued = true;
|
|
574
1679
|
}
|
|
575
1680
|
}
|
|
1681
|
+
}
|
|
576
1682
|
|
|
577
|
-
|
|
1683
|
+
/*
|
|
1684
|
+
* An update that added pages while the 'created' notification was Pending
|
|
1685
|
+
* or being sent left the status alone: the queued send reads the scope when
|
|
1686
|
+
* it goes out, and a running send reads it again once it has finished (see
|
|
1687
|
+
* the Incident:SendNotificationToSubscribers job). One case slips between
|
|
1688
|
+
* the two: the whole send started after this update read the status and
|
|
1689
|
+
* finished before it wrote the pages. Once the pages are written, this
|
|
1690
|
+
* checks for exactly that - the send finished, and an added page is not in
|
|
1691
|
+
* its record - and queues the notification again for the pages it missed.
|
|
1692
|
+
*
|
|
1693
|
+
* Never throws: the update is already written.
|
|
1694
|
+
*/
|
|
1695
|
+
private async requeueCreatedNotificationIfAddedPagesWereMissed(data: {
|
|
1696
|
+
incidentId: ObjectID;
|
|
1697
|
+
change: StatusPageScopeCarryForward | undefined;
|
|
1698
|
+
}): Promise<void> {
|
|
578
1699
|
if (
|
|
579
|
-
|
|
580
|
-
|
|
1700
|
+
!data.change?.notificationAlreadyQueued ||
|
|
1701
|
+
data.change.addedStatusPageIds.length === 0
|
|
581
1702
|
) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
try {
|
|
1707
|
+
const incident: Model | null = await this.findOneById({
|
|
1708
|
+
id: data.incidentId,
|
|
1709
|
+
select: {
|
|
1710
|
+
subscriberNotificationStatusOnIncidentCreated: true,
|
|
1711
|
+
statusPagesNotifiedOnCreation: true,
|
|
1712
|
+
},
|
|
1713
|
+
props: {
|
|
1714
|
+
isRoot: true,
|
|
1715
|
+
},
|
|
1716
|
+
});
|
|
1717
|
+
|
|
582
1718
|
if (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
) {
|
|
586
|
-
updateBy.data.subscriberNotificationStatusOnIncidentCreated =
|
|
587
|
-
StatusPageSubscriberNotificationStatus.Skipped;
|
|
588
|
-
updateBy.data.subscriberNotificationStatusMessage =
|
|
589
|
-
"Notifications skipped as subscribers are not to be notified for this incident.";
|
|
590
|
-
} else if (
|
|
591
|
-
updateBy.data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated ===
|
|
592
|
-
true
|
|
1719
|
+
incident?.subscriberNotificationStatusOnIncidentCreated !==
|
|
1720
|
+
StatusPageSubscriberNotificationStatus.Success
|
|
593
1721
|
) {
|
|
594
|
-
|
|
595
|
-
|
|
1722
|
+
// Still on its way (it will see the pages), or settled otherwise.
|
|
1723
|
+
return;
|
|
596
1724
|
}
|
|
1725
|
+
|
|
1726
|
+
const recorded: Array<string> =
|
|
1727
|
+
IncidentScopeAddedPagesNotification.getRecordedStatusPageIds(
|
|
1728
|
+
incident.statusPagesNotifiedOnCreation,
|
|
1729
|
+
) || [];
|
|
1730
|
+
|
|
1731
|
+
const missed: boolean = data.change.addedStatusPageIds.some(
|
|
1732
|
+
(id: string): boolean => {
|
|
1733
|
+
return !recorded.includes(id);
|
|
1734
|
+
},
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
if (!missed) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
await this.updateOneById({
|
|
1742
|
+
id: data.incidentId,
|
|
1743
|
+
data: {
|
|
1744
|
+
subscriberNotificationStatusOnIncidentCreated:
|
|
1745
|
+
StatusPageSubscriberNotificationStatus.Pending,
|
|
1746
|
+
subscriberNotificationStatusMessage:
|
|
1747
|
+
IncidentScopeAddedPagesNotification.queuedMessage,
|
|
1748
|
+
},
|
|
1749
|
+
props: {
|
|
1750
|
+
isRoot: true,
|
|
1751
|
+
ignoreHooks: true,
|
|
1752
|
+
},
|
|
1753
|
+
});
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
logger.error(
|
|
1756
|
+
`Failed to check whether the incident created notification reached the status pages added to the incident: ${err}`,
|
|
1757
|
+
{
|
|
1758
|
+
incidentId: data.incidentId?.toString(),
|
|
1759
|
+
} as LogAttributes,
|
|
1760
|
+
);
|
|
597
1761
|
}
|
|
1762
|
+
}
|
|
598
1763
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1764
|
+
/*
|
|
1765
|
+
* The pages the matched incidents are scoped to that the caller cannot
|
|
1766
|
+
* read, which a list write from them must keep (see
|
|
1767
|
+
* applyStatusPageScopeToUpdate). A bulk update writes the same list onto
|
|
1768
|
+
* every incident it matches, so it can only keep them when every matched
|
|
1769
|
+
* incident holds the same hidden pages; otherwise it would scope some of
|
|
1770
|
+
* them to pages they were never scoped to, and it is refused.
|
|
1771
|
+
*/
|
|
1772
|
+
private async getScopedStatusPageIdsHiddenFromCaller(data: {
|
|
1773
|
+
incidents: Array<Model>;
|
|
1774
|
+
props: DatabaseCommonInteractionProps;
|
|
1775
|
+
}): Promise<Array<string>> {
|
|
1776
|
+
const heldByIncident: Array<Array<string>> = data.incidents.map(
|
|
1777
|
+
(incident: Model) => {
|
|
1778
|
+
return IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
|
|
1779
|
+
incident.statusPages,
|
|
1780
|
+
);
|
|
1781
|
+
},
|
|
1782
|
+
);
|
|
1783
|
+
|
|
1784
|
+
const heldIds: Array<string> =
|
|
1785
|
+
IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
|
|
1786
|
+
heldByIncident.flat(),
|
|
1787
|
+
);
|
|
1788
|
+
|
|
1789
|
+
if (heldIds.length === 0) {
|
|
1790
|
+
return [];
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const readableIds: Array<string> =
|
|
1794
|
+
await StatusPageReadAccess.getReadableStatusPageIds({
|
|
1795
|
+
statusPageIds: heldIds,
|
|
1796
|
+
props: data.props,
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
const hiddenByIncident: Array<Array<string>> = heldByIncident.map(
|
|
1800
|
+
(held: Array<string>) => {
|
|
1801
|
+
return held
|
|
1802
|
+
.filter((id: string) => {
|
|
1803
|
+
return !readableIds.includes(id);
|
|
1804
|
+
})
|
|
1805
|
+
.sort();
|
|
1806
|
+
},
|
|
1807
|
+
);
|
|
1808
|
+
|
|
1809
|
+
const hiddenIds: Array<string> = hiddenByIncident[0] || [];
|
|
1810
|
+
|
|
1811
|
+
const everyIncidentHidesTheSame: boolean = hiddenByIncident.every(
|
|
1812
|
+
(hidden: Array<string>) => {
|
|
1813
|
+
return hidden.join(",") === hiddenIds.join(",");
|
|
1814
|
+
},
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
if (!everyIncidentHidesTheSame) {
|
|
1818
|
+
throw new BadDataException(
|
|
1819
|
+
"These incidents are limited to different status pages that you do not have access to, so their status pages cannot be changed together. Change them one at a time.",
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
return hiddenIds;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
private toStatusPageStubs(statusPageIds: Array<string>): Array<StatusPage> {
|
|
1827
|
+
return statusPageIds.map((statusPageId: string) => {
|
|
1828
|
+
const statusPage: StatusPage = new StatusPage();
|
|
1829
|
+
statusPage._id = statusPageId;
|
|
1830
|
+
return statusPage;
|
|
609
1831
|
});
|
|
1832
|
+
}
|
|
610
1833
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1834
|
+
// A column's value once an update is written: the update's, else the stored.
|
|
1835
|
+
private getValueAfterUpdate<T>(
|
|
1836
|
+
updatedValue: unknown,
|
|
1837
|
+
storedValue: T | undefined,
|
|
1838
|
+
): T | undefined {
|
|
1839
|
+
if (updatedValue !== undefined && updatedValue !== null) {
|
|
1840
|
+
return updatedValue as T;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
return storedValue;
|
|
615
1844
|
}
|
|
616
1845
|
|
|
617
1846
|
/*
|
|
@@ -764,6 +1993,16 @@ export class Service extends DatabaseService<Model> {
|
|
|
764
1993
|
modelName: "On-Call Policy",
|
|
765
1994
|
service: OnCallDutyPolicyService,
|
|
766
1995
|
},
|
|
1996
|
+
/*
|
|
1997
|
+
* The status pages the incident is limited to. Another project's page
|
|
1998
|
+
* here would put that project's page name into this incident's feed and
|
|
1999
|
+
* scope it to a page it can never show on.
|
|
2000
|
+
*/
|
|
2001
|
+
{
|
|
2002
|
+
column: "statusPages",
|
|
2003
|
+
modelName: "Status Page",
|
|
2004
|
+
service: StatusPageService,
|
|
2005
|
+
},
|
|
767
2006
|
...getAffectedResourceRelations(this.getModel()),
|
|
768
2007
|
];
|
|
769
2008
|
}
|
|
@@ -811,6 +2050,23 @@ export class Service extends DatabaseService<Model> {
|
|
|
811
2050
|
const projectId: ObjectID =
|
|
812
2051
|
createBy.props.tenantId || createBy.data.projectId!;
|
|
813
2052
|
|
|
2053
|
+
/*
|
|
2054
|
+
* The values the caller sent, before a template or a mapping adds any:
|
|
2055
|
+
* those are not the caller's to get wrong.
|
|
2056
|
+
*/
|
|
2057
|
+
await this.validateCustomFieldValuesOnCreate({
|
|
2058
|
+
createBy: createBy,
|
|
2059
|
+
projectId: projectId,
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
/*
|
|
2063
|
+
* Status pages the caller picked, as opposed to ones copied from a
|
|
2064
|
+
* template below (see applyStatusPageScopeToCreate).
|
|
2065
|
+
*/
|
|
2066
|
+
const statusPagesFromCaller: boolean =
|
|
2067
|
+
createBy.data.statusPages !== undefined &&
|
|
2068
|
+
createBy.data.statusPages !== null;
|
|
2069
|
+
|
|
814
2070
|
/*
|
|
815
2071
|
* Declaring the incident from alerts. The alert ids are checked here,
|
|
816
2072
|
* before the incident number is taken and before anything is written, so
|
|
@@ -886,6 +2142,9 @@ export class Service extends DatabaseService<Model> {
|
|
|
886
2142
|
// Determine the initial incident state
|
|
887
2143
|
let initialIncidentStateId: ObjectID | undefined = undefined;
|
|
888
2144
|
|
|
2145
|
+
// Declared from a template whose status pages were all deleted.
|
|
2146
|
+
let isScopedToNothingByTemplate: boolean = false;
|
|
2147
|
+
|
|
889
2148
|
// If currentIncidentStateId is already provided (manual selection), use it
|
|
890
2149
|
if (createBy.data.currentIncidentStateId) {
|
|
891
2150
|
initialIncidentStateId = createBy.data.currentIncidentStateId;
|
|
@@ -941,6 +2200,9 @@ export class Service extends DatabaseService<Model> {
|
|
|
941
2200
|
services: { _id: true },
|
|
942
2201
|
onCallDutyPolicies: { _id: true },
|
|
943
2202
|
labels: { _id: true },
|
|
2203
|
+
statusPages: { _id: true },
|
|
2204
|
+
isScopedToStatusPages: true,
|
|
2205
|
+
customFields: true,
|
|
944
2206
|
},
|
|
945
2207
|
props: {
|
|
946
2208
|
isRoot: true,
|
|
@@ -1092,9 +2354,50 @@ export class Service extends DatabaseService<Model> {
|
|
|
1092
2354
|
createBy.data.labels = stubs;
|
|
1093
2355
|
}
|
|
1094
2356
|
}
|
|
2357
|
+
// Applying a template that has status pages scopes the incident.
|
|
2358
|
+
if (createBy.data.statusPages === undefined) {
|
|
2359
|
+
const stubs: Array<StatusPage> | undefined = stubBy(
|
|
2360
|
+
StatusPage,
|
|
2361
|
+
incidentTemplate.statusPages,
|
|
2362
|
+
);
|
|
2363
|
+
if (stubs && stubs.length > 0) {
|
|
2364
|
+
createBy.data.statusPages = stubs;
|
|
2365
|
+
} else if (incidentTemplate.isScopedToStatusPages) {
|
|
2366
|
+
/*
|
|
2367
|
+
* A template limited to status pages that have all been deleted
|
|
2368
|
+
* since. Its incidents stay limited - to nothing - like an
|
|
2369
|
+
* incident whose pages were deleted, rather than reaching every
|
|
2370
|
+
* page that lists their monitors.
|
|
2371
|
+
*/
|
|
2372
|
+
isScopedToNothingByTemplate = true;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
/*
|
|
2377
|
+
* The template's custom field values fill in the fields the caller
|
|
2378
|
+
* left out; a value the caller sent always wins. Merged, not
|
|
2379
|
+
* replaced, so the caller's own values are never dropped. They are
|
|
2380
|
+
* the template's, so the value check above (the caller's values
|
|
2381
|
+
* only) does not see them, and the mapping below still has the last
|
|
2382
|
+
* word on a mapped field.
|
|
2383
|
+
*/
|
|
2384
|
+
const customFieldsWithTemplate: JSONObject | undefined =
|
|
2385
|
+
mergeTemplateCustomFields({
|
|
2386
|
+
templateCustomFields: incidentTemplate.customFields,
|
|
2387
|
+
customFields: createBy.data.customFields,
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
if (customFieldsWithTemplate) {
|
|
2391
|
+
createBy.data.customFields = customFieldsWithTemplate;
|
|
2392
|
+
}
|
|
1095
2393
|
}
|
|
1096
2394
|
}
|
|
1097
2395
|
|
|
2396
|
+
this.applyStatusPageScopeToCreate({
|
|
2397
|
+
createBy: createBy,
|
|
2398
|
+
isScopedToNothingByTemplate: isScopedToNothingByTemplate,
|
|
2399
|
+
});
|
|
2400
|
+
|
|
1098
2401
|
// If no custom state is provided or found, fall back to default created state
|
|
1099
2402
|
if (!initialIncidentStateId) {
|
|
1100
2403
|
const incidentState: IncidentState | null =
|
|
@@ -1182,6 +2485,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
1182
2485
|
},
|
|
1183
2486
|
);
|
|
1184
2487
|
|
|
2488
|
+
// Before the counter increment too, like the checks above.
|
|
2489
|
+
await this.assertCallerCanPickStatusPagesOnCreate({
|
|
2490
|
+
createBy: createBy,
|
|
2491
|
+
projectId: projectId,
|
|
2492
|
+
statusPagesFromCaller: statusPagesFromCaller,
|
|
2493
|
+
});
|
|
2494
|
+
|
|
1185
2495
|
const incidentCounterResult: {
|
|
1186
2496
|
counter: number;
|
|
1187
2497
|
prefix: string | undefined;
|
|
@@ -1313,6 +2623,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
1313
2623
|
* The resources named under "Resources Affected" are read by the feed
|
|
1314
2624
|
* builder itself (LinkedAffectedResources), one relation at a time.
|
|
1315
2625
|
*/
|
|
2626
|
+
// The created feed item names the status pages a scoped incident is limited to.
|
|
2627
|
+
isScopedToStatusPages: true,
|
|
1316
2628
|
},
|
|
1317
2629
|
props: {
|
|
1318
2630
|
isRoot: true,
|
|
@@ -1975,6 +3287,44 @@ export class Service extends DatabaseService<Model> {
|
|
|
1975
3287
|
}
|
|
1976
3288
|
}
|
|
1977
3289
|
|
|
3290
|
+
/*
|
|
3291
|
+
* The status pages a scoped incident is limited to, as a Markdown list for
|
|
3292
|
+
* its created feed item. Read on their own rather than in onCreateSuccess's
|
|
3293
|
+
* select: a find that selects several many-to-many lists returns a row for
|
|
3294
|
+
* every combination of them.
|
|
3295
|
+
*/
|
|
3296
|
+
private async getScopedStatusPagesMarkdown(incident: Model): Promise<string> {
|
|
3297
|
+
const incidentWithStatusPages: Model | null = await this.findOneById({
|
|
3298
|
+
id: incident.id!,
|
|
3299
|
+
select: {
|
|
3300
|
+
statusPages: {
|
|
3301
|
+
_id: true,
|
|
3302
|
+
name: true,
|
|
3303
|
+
},
|
|
3304
|
+
},
|
|
3305
|
+
props: {
|
|
3306
|
+
isRoot: true,
|
|
3307
|
+
},
|
|
3308
|
+
});
|
|
3309
|
+
|
|
3310
|
+
const statusPages: Array<StatusPage> =
|
|
3311
|
+
incidentWithStatusPages?.statusPages || [];
|
|
3312
|
+
|
|
3313
|
+
if (statusPages.length === 0) {
|
|
3314
|
+
// Scoped to pages that have all been deleted since: hidden everywhere.
|
|
3315
|
+
return `- None: the status pages it was limited to have been deleted, so it is not shown on any status page.\n`;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
let markdown: string = "";
|
|
3319
|
+
|
|
3320
|
+
for (const statusPage of statusPages) {
|
|
3321
|
+
// A page name is free text: kept from turning into a link or an image.
|
|
3322
|
+
markdown += `- [${escapeMarkdownInline(statusPage.name || "Untitled status page")}](${(await StatusPageService.getStatusPageLinkInDashboard(incident.projectId!, statusPage.id!)).toString()})\n`;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
return markdown;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
1978
3328
|
@CaptureSpan()
|
|
1979
3329
|
private async createIncidentFeedAsync(incident: Model): Promise<void> {
|
|
1980
3330
|
try {
|
|
@@ -2029,6 +3379,12 @@ ${incident.description || "No description provided."}
|
|
|
2029
3379
|
feedInfoInMarkdown += `\n\n`;
|
|
2030
3380
|
}
|
|
2031
3381
|
|
|
3382
|
+
if (incident.isScopedToStatusPages) {
|
|
3383
|
+
feedInfoInMarkdown += `📣 **Limited to Status Pages**:\n`;
|
|
3384
|
+
feedInfoInMarkdown += await this.getScopedStatusPagesMarkdown(incident);
|
|
3385
|
+
feedInfoInMarkdown += `\n\n`;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
2032
3388
|
if (incident.rootCause) {
|
|
2033
3389
|
feedInfoInMarkdown += `\n
|
|
2034
3390
|
📄 **Root Cause**:
|
|
@@ -2969,14 +4325,8 @@ ${incidentSeverity.name}
|
|
|
2969
4325
|
onUpdate.carryForward;
|
|
2970
4326
|
|
|
2971
4327
|
if (carryForward) {
|
|
2972
|
-
const incidentCarryForward:
|
|
2973
|
-
|
|
2974
|
-
monitorsRemoved: Array<Monitor>;
|
|
2975
|
-
monitorsAdded: Array<Monitor>;
|
|
2976
|
-
oldChangeMonitorStatusIdTo: ObjectID | undefined;
|
|
2977
|
-
newMonitorChangeStatusIdTo: ObjectID | undefined;
|
|
2978
|
-
}
|
|
2979
|
-
| undefined = carryForward[incidentId.toString()];
|
|
4328
|
+
const incidentCarryForward: UpdateCarryForward[string] | undefined =
|
|
4329
|
+
carryForward[incidentId.toString()];
|
|
2980
4330
|
|
|
2981
4331
|
if (incidentCarryForward) {
|
|
2982
4332
|
if (incidentCarryForward.monitorsRemoved.length > 0) {
|
|
@@ -3050,6 +4400,23 @@ ${incidentSeverity.name}
|
|
|
3050
4400
|
shouldAddIncidentFeed = true;
|
|
3051
4401
|
}
|
|
3052
4402
|
|
|
4403
|
+
const statusPageScopeMarkdown: string =
|
|
4404
|
+
await this.getStatusPageScopeFeedMarkdown({
|
|
4405
|
+
projectId: projectId,
|
|
4406
|
+
incidentId: incidentId,
|
|
4407
|
+
change: incidentCarryForward.statusPageScopeChange,
|
|
4408
|
+
});
|
|
4409
|
+
|
|
4410
|
+
if (statusPageScopeMarkdown) {
|
|
4411
|
+
feedInfoInMarkdown += statusPageScopeMarkdown;
|
|
4412
|
+
shouldAddIncidentFeed = true;
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
await this.requeueCreatedNotificationIfAddedPagesWereMissed({
|
|
4416
|
+
incidentId: incidentId,
|
|
4417
|
+
change: incidentCarryForward.statusPageScopeChange,
|
|
4418
|
+
});
|
|
4419
|
+
|
|
3053
4420
|
if (
|
|
3054
4421
|
incidentCarryForward.oldChangeMonitorStatusIdTo &&
|
|
3055
4422
|
incidentCarryForward.newMonitorChangeStatusIdTo
|
|
@@ -3147,6 +4514,109 @@ ${incidentSeverity.name}
|
|
|
3147
4514
|
return onUpdate;
|
|
3148
4515
|
}
|
|
3149
4516
|
|
|
4517
|
+
/*
|
|
4518
|
+
* The part of an update's feed item that records a change of the status
|
|
4519
|
+
* pages the incident is limited to: the pages added and removed, whether
|
|
4520
|
+
* the incident is now limited at all, and whether the added pages will be
|
|
4521
|
+
* sent the 'created' notification. Empty when the scope did not change.
|
|
4522
|
+
*
|
|
4523
|
+
* Page names are read as root, like the monitor names above. The pages were
|
|
4524
|
+
* validated as this project's when they were written, and the read is
|
|
4525
|
+
* filtered on the project anyway. Never throws: the update is already
|
|
4526
|
+
* written, and a missing line in the feed must not turn it into an error.
|
|
4527
|
+
*/
|
|
4528
|
+
private async getStatusPageScopeFeedMarkdown(data: {
|
|
4529
|
+
projectId: ObjectID;
|
|
4530
|
+
incidentId: ObjectID;
|
|
4531
|
+
change: StatusPageScopeCarryForward | undefined;
|
|
4532
|
+
}): Promise<string> {
|
|
4533
|
+
const change: StatusPageScopeCarryForward | undefined = data.change;
|
|
4534
|
+
|
|
4535
|
+
if (
|
|
4536
|
+
!change ||
|
|
4537
|
+
(change.addedStatusPageIds.length === 0 &&
|
|
4538
|
+
change.removedStatusPageIds.length === 0)
|
|
4539
|
+
) {
|
|
4540
|
+
return "";
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
try {
|
|
4544
|
+
const statusPages: Array<StatusPage> = await StatusPageService.findBy({
|
|
4545
|
+
query: {
|
|
4546
|
+
_id: QueryHelper.any([
|
|
4547
|
+
...change.addedStatusPageIds,
|
|
4548
|
+
...change.removedStatusPageIds,
|
|
4549
|
+
]),
|
|
4550
|
+
projectId: data.projectId,
|
|
4551
|
+
},
|
|
4552
|
+
select: {
|
|
4553
|
+
_id: true,
|
|
4554
|
+
name: true,
|
|
4555
|
+
},
|
|
4556
|
+
limit: LIMIT_MAX,
|
|
4557
|
+
skip: 0,
|
|
4558
|
+
props: {
|
|
4559
|
+
isRoot: true,
|
|
4560
|
+
},
|
|
4561
|
+
});
|
|
4562
|
+
|
|
4563
|
+
const statusPageLines: (ids: Array<string>) => Promise<string> = async (
|
|
4564
|
+
ids: Array<string>,
|
|
4565
|
+
): Promise<string> => {
|
|
4566
|
+
let lines: string = "";
|
|
4567
|
+
|
|
4568
|
+
for (const id of ids) {
|
|
4569
|
+
const statusPage: StatusPage | undefined = statusPages.find(
|
|
4570
|
+
(page: StatusPage) => {
|
|
4571
|
+
return page.id?.toString().toLowerCase() === id;
|
|
4572
|
+
},
|
|
4573
|
+
);
|
|
4574
|
+
|
|
4575
|
+
if (!statusPage) {
|
|
4576
|
+
// Deleted since: its name is gone with it.
|
|
4577
|
+
lines += `- A deleted status page\n`;
|
|
4578
|
+
continue;
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
// A page name is free text: kept from turning into a link or an image.
|
|
4582
|
+
lines += `- [${escapeMarkdownInline(statusPage.name || "Untitled status page")}](${(await StatusPageService.getStatusPageLinkInDashboard(data.projectId, statusPage.id!)).toString()})\n`;
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
return lines;
|
|
4586
|
+
};
|
|
4587
|
+
|
|
4588
|
+
let markdown: string = "";
|
|
4589
|
+
|
|
4590
|
+
if (change.addedStatusPageIds.length > 0) {
|
|
4591
|
+
markdown += `\n\n**📣 Status Pages Added**:\n${await statusPageLines(change.addedStatusPageIds)}`;
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
if (change.removedStatusPageIds.length > 0) {
|
|
4595
|
+
markdown += `\n\n**🔕 Status Pages Removed**:\n${await statusPageLines(change.removedStatusPageIds)}`;
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
markdown += change.isScoped
|
|
4599
|
+
? `\n\nThis incident is shown on, and notifies the subscribers of, only its selected status pages that list its monitors.\n`
|
|
4600
|
+
: `\n\nThis incident is no longer limited to specific status pages: it is shown on every status page that lists its monitors, except pages that only show incidents limited to them.\n`;
|
|
4601
|
+
|
|
4602
|
+
if (change.notificationQueued) {
|
|
4603
|
+
markdown += `\nSubscribers of the added status pages will be sent the notification that this incident was created.\n`;
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
return markdown;
|
|
4607
|
+
} catch (err) {
|
|
4608
|
+
logger.error(
|
|
4609
|
+
`Failed to describe the status page scope change in the incident feed: ${err}`,
|
|
4610
|
+
{
|
|
4611
|
+
projectId: data.projectId?.toString(),
|
|
4612
|
+
incidentId: data.incidentId?.toString(),
|
|
4613
|
+
} as LogAttributes,
|
|
4614
|
+
);
|
|
4615
|
+
|
|
4616
|
+
return "";
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
|
|
3150
4620
|
@CaptureSpan()
|
|
3151
4621
|
public async doesMonitorHasMoreActiveManualIncidents(
|
|
3152
4622
|
monitorId: ObjectID,
|