@oneuptime/common 7.0.4748 → 7.0.4755

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.
@@ -62,6 +62,7 @@ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
62
62
  import { Dictionary } from "lodash";
63
63
  import MetricType from "../../Models/DatabaseModels/MetricType";
64
64
  import UpdateBy from "../Types/Database/UpdateBy";
65
+ import OnCallDutyPolicy from "../../Models/DatabaseModels/OnCallDutyPolicy";
65
66
 
66
67
  // key is incidentId for this dictionary.
67
68
  type UpdateCarryForward = Dictionary<{
@@ -544,6 +545,7 @@ export class Service extends DatabaseService<Model> {
544
545
  throw new BadDataException("id is required");
545
546
  }
546
547
 
548
+ // Get incident data for feed creation
547
549
  const incident: Model | null = await this.findOneById({
548
550
  id: createdItem.id,
549
551
  select: {
@@ -576,202 +578,343 @@ export class Service extends DatabaseService<Model> {
576
578
  throw new BadDataException("Incident not found");
577
579
  }
578
580
 
579
- // release the mutex.
580
- if (onCreate.carryForward && onCreate.carryForward.mutex) {
581
- const mutex: SemaphoreMutex = onCreate.carryForward.mutex;
582
- const projectId: ObjectID = createdItem.projectId!;
583
-
584
- try {
585
- await Semaphore.release(mutex);
586
- logger.debug(
587
- "Mutex released - IncidentService.incident-create " +
588
- projectId.toString() +
589
- " at " +
590
- OneUptimeDate.getCurrentDateAsFormattedString(),
591
- );
592
- } catch (err) {
593
- logger.debug(
594
- "Mutex release failed - IncidentService.incident-create " +
595
- projectId.toString() +
596
- " at " +
597
- OneUptimeDate.getCurrentDateAsFormattedString(),
598
- );
599
- logger.error(err);
600
- }
581
+ // Execute core operations in parallel first
582
+ const coreOperations: Array<Promise<any>> = [];
583
+
584
+ // Create feed item asynchronously
585
+ coreOperations.push(this.createIncidentFeedAsync(incident, createdItem));
586
+
587
+ // Handle state change asynchronously
588
+ coreOperations.push(this.handleIncidentStateChangeAsync(createdItem));
589
+
590
+ // Handle owner assignment asynchronously
591
+ if (
592
+ onCreate.createBy.miscDataProps &&
593
+ (onCreate.createBy.miscDataProps["ownerTeams"] ||
594
+ onCreate.createBy.miscDataProps["ownerUsers"])
595
+ ) {
596
+ coreOperations.push(
597
+ this.addOwners(
598
+ createdItem.projectId,
599
+ createdItem.id,
600
+ (onCreate.createBy.miscDataProps["ownerUsers"] as Array<ObjectID>) ||
601
+ [],
602
+ (onCreate.createBy.miscDataProps["ownerTeams"] as Array<ObjectID>) ||
603
+ [],
604
+ false,
605
+ onCreate.createBy.props,
606
+ ),
607
+ );
601
608
  }
602
609
 
603
- const createdByUserId: ObjectID | undefined | null =
604
- createdItem.createdByUserId || createdItem.createdByUser?.id;
610
+ // Handle monitor status change and active monitoring asynchronously
611
+ if (createdItem.changeMonitorStatusToId && createdItem.projectId) {
612
+ coreOperations.push(
613
+ this.handleMonitorStatusChangeAsync(createdItem, onCreate),
614
+ );
615
+ }
605
616
 
606
- // send message to workspaces - slack, teams, etc.
607
- const workspaceResult: {
608
- channelsCreated: Array<NotificationRuleWorkspaceChannel>;
609
- } | null =
610
- await IncidentWorkspaceMessages.createChannelsAndInviteUsersToChannels({
611
- projectId: createdItem.projectId,
612
- incidentId: createdItem.id!,
613
- incidentNumber: createdItem.incidentNumber!,
614
- });
617
+ coreOperations.push(
618
+ this.disableActiveMonitoringIfManualIncident(createdItem.id!),
619
+ );
615
620
 
616
- if (workspaceResult && workspaceResult.channelsCreated?.length > 0) {
617
- // update incident with these channels.
618
- await this.updateOneById({
619
- id: createdItem.id!,
620
- data: {
621
- postUpdatesToWorkspaceChannels: workspaceResult.channelsCreated || [],
622
- },
623
- props: {
624
- isRoot: true,
625
- },
621
+ // Release mutex immediately
622
+ this.releaseMutexAsync(onCreate, createdItem.projectId!);
623
+
624
+ // Execute core operations in parallel with error handling
625
+ Promise.allSettled(coreOperations)
626
+ .then((coreResults: any[]) => {
627
+ // Log any errors from core operations
628
+ coreResults.forEach((result: any, index: number) => {
629
+ if (result.status === "rejected") {
630
+ logger.error(
631
+ `Core operation ${index} failed in IncidentService.onCreateSuccess: ${result.reason}`,
632
+ );
633
+ }
634
+ });
635
+
636
+ // Handle on-call duty policies asynchronously
637
+ if (
638
+ createdItem.onCallDutyPolicies?.length &&
639
+ createdItem.onCallDutyPolicies?.length > 0
640
+ ) {
641
+ this.executeOnCallDutyPoliciesAsync(createdItem).catch(
642
+ (error: Error) => {
643
+ logger.error(
644
+ `On-call duty policy execution failed in IncidentService.onCreateSuccess: ${error}`,
645
+ );
646
+ },
647
+ );
648
+ }
649
+
650
+ // Handle workspace operations after core operations complete
651
+ if (createdItem.projectId && createdItem.id) {
652
+ // Run workspace operations in background without blocking response
653
+ this.handleIncidentWorkspaceOperationsAsync(createdItem).catch(
654
+ (error: Error) => {
655
+ logger.error(
656
+ `Workspace operations failed in IncidentService.onCreateSuccess: ${error}`,
657
+ );
658
+ },
659
+ );
660
+ }
661
+ })
662
+ .catch((error: Error) => {
663
+ logger.error(
664
+ `Critical error in IncidentService core operations: ${error}`,
665
+ );
626
666
  });
667
+
668
+ return createdItem;
669
+ }
670
+
671
+ @CaptureSpan()
672
+ private async handleIncidentWorkspaceOperationsAsync(
673
+ createdItem: Model,
674
+ ): Promise<void> {
675
+ try {
676
+ if (!createdItem.projectId || !createdItem.id) {
677
+ throw new BadDataException(
678
+ "projectId and id are required for workspace operations",
679
+ );
680
+ }
681
+
682
+ // send message to workspaces - slack, teams, etc.
683
+ const workspaceResult: {
684
+ channelsCreated: Array<NotificationRuleWorkspaceChannel>;
685
+ } | null =
686
+ await IncidentWorkspaceMessages.createChannelsAndInviteUsersToChannels({
687
+ projectId: createdItem.projectId,
688
+ incidentId: createdItem.id,
689
+ incidentNumber: createdItem.incidentNumber!,
690
+ });
691
+
692
+ if (workspaceResult && workspaceResult.channelsCreated?.length > 0) {
693
+ // update incident with these channels.
694
+ await this.updateOneById({
695
+ id: createdItem.id,
696
+ data: {
697
+ postUpdatesToWorkspaceChannels:
698
+ workspaceResult.channelsCreated || [],
699
+ },
700
+ props: {
701
+ isRoot: true,
702
+ },
703
+ });
704
+ }
705
+ } catch (error) {
706
+ logger.error(`Error in handleIncidentWorkspaceOperationsAsync: ${error}`);
707
+ throw error;
627
708
  }
709
+ }
710
+
711
+ @CaptureSpan()
712
+ private async createIncidentFeedAsync(
713
+ incident: Model,
714
+ createdItem: Model,
715
+ ): Promise<void> {
716
+ try {
717
+ const createdByUserId: ObjectID | undefined | null =
718
+ createdItem.createdByUserId || createdItem.createdByUser?.id;
628
719
 
629
- let feedInfoInMarkdown: string = `#### 🚨 Incident ${createdItem.incidentNumber?.toString()} Created:
630
-
720
+ let feedInfoInMarkdown: string = `#### 🚨 Incident ${createdItem.incidentNumber?.toString()} Created:
721
+
631
722
  **${createdItem.title || "No title provided."}**:
632
723
 
633
724
  ${createdItem.description || "No description provided."}
634
725
 
635
726
  `;
636
727
 
637
- if (incident.currentIncidentState?.name) {
638
- feedInfoInMarkdown += `🔴 **Incident State**: ${incident.currentIncidentState.name} \n\n`;
639
- }
728
+ if (incident.currentIncidentState?.name) {
729
+ feedInfoInMarkdown += `🔴 **Incident State**: ${incident.currentIncidentState.name} \n\n`;
730
+ }
640
731
 
641
- if (incident.incidentSeverity?.name) {
642
- feedInfoInMarkdown += `⚠️ **Severity**: ${incident.incidentSeverity.name} \n\n`;
643
- }
732
+ if (incident.incidentSeverity?.name) {
733
+ feedInfoInMarkdown += `⚠️ **Severity**: ${incident.incidentSeverity.name} \n\n`;
734
+ }
644
735
 
645
- if (incident.monitors && incident.monitors.length > 0) {
646
- feedInfoInMarkdown += `🌎 **Resources Affected**:\n`;
736
+ if (incident.monitors && incident.monitors.length > 0) {
737
+ feedInfoInMarkdown += `🌎 **Resources Affected**:\n`;
647
738
 
648
- for (const monitor of incident.monitors) {
649
- feedInfoInMarkdown += `- [${monitor.name}](${(await MonitorService.getMonitorLinkInDashboard(createdItem.projectId!, monitor.id!)).toString()})\n`;
650
- }
739
+ for (const monitor of incident.monitors) {
740
+ feedInfoInMarkdown += `- [${monitor.name}](${(await MonitorService.getMonitorLinkInDashboard(createdItem.projectId!, monitor.id!)).toString()})\n`;
741
+ }
651
742
 
652
- feedInfoInMarkdown += `\n\n`;
653
- }
743
+ feedInfoInMarkdown += `\n\n`;
744
+ }
654
745
 
655
- if (createdItem.rootCause) {
656
- feedInfoInMarkdown += `\n
746
+ if (createdItem.rootCause) {
747
+ feedInfoInMarkdown += `\n
657
748
  📄 **Root Cause**:
658
749
 
659
750
  ${createdItem.rootCause || "No root cause provided."}
660
751
 
661
752
  `;
662
- }
753
+ }
663
754
 
664
- if (createdItem.remediationNotes) {
665
- feedInfoInMarkdown += `\n
755
+ if (createdItem.remediationNotes) {
756
+ feedInfoInMarkdown += `\n
666
757
  🎯 **Remediation Notes**:
667
758
 
668
759
  ${createdItem.remediationNotes || "No remediation notes provided."}
669
760
 
670
761
 
671
762
  `;
672
- }
763
+ }
764
+
765
+ const incidentCreateMessageBlocks: Array<MessageBlocksByWorkspaceType> =
766
+ await IncidentWorkspaceMessages.getIncidentCreateMessageBlocks({
767
+ incidentId: createdItem.id!,
768
+ projectId: createdItem.projectId!,
769
+ });
673
770
 
674
- const incidentCreateMessageBlocks: Array<MessageBlocksByWorkspaceType> =
675
- await IncidentWorkspaceMessages.getIncidentCreateMessageBlocks({
771
+ await IncidentFeedService.createIncidentFeedItem({
676
772
  incidentId: createdItem.id!,
677
773
  projectId: createdItem.projectId!,
774
+ incidentFeedEventType: IncidentFeedEventType.IncidentCreated,
775
+ displayColor: Red500,
776
+ feedInfoInMarkdown: feedInfoInMarkdown,
777
+ userId: createdByUserId || undefined,
778
+ workspaceNotification: {
779
+ appendMessageBlocks: incidentCreateMessageBlocks,
780
+ sendWorkspaceNotification: true,
781
+ },
678
782
  });
783
+ } catch (error) {
784
+ logger.error(`Error in createIncidentFeedAsync: ${error}`);
785
+ throw error;
786
+ }
787
+ }
679
788
 
680
- await IncidentFeedService.createIncidentFeedItem({
681
- incidentId: createdItem.id!,
682
- projectId: createdItem.projectId!,
683
- incidentFeedEventType: IncidentFeedEventType.IncidentCreated,
684
- displayColor: Red500,
685
- feedInfoInMarkdown: feedInfoInMarkdown,
686
- userId: createdByUserId || undefined,
687
- workspaceNotification: {
688
- appendMessageBlocks: incidentCreateMessageBlocks,
689
- sendWorkspaceNotification: true,
690
- },
691
- });
789
+ @CaptureSpan()
790
+ private async handleIncidentStateChangeAsync(
791
+ createdItem: Model,
792
+ ): Promise<void> {
793
+ try {
794
+ if (!createdItem.currentIncidentStateId) {
795
+ throw new BadDataException("currentIncidentStateId is required");
796
+ }
692
797
 
693
- if (!createdItem.currentIncidentStateId) {
694
- throw new BadDataException("currentIncidentStateId is required");
695
- }
798
+ if (!createdItem.projectId || !createdItem.id) {
799
+ throw new BadDataException(
800
+ "projectId and id are required for state change",
801
+ );
802
+ }
696
803
 
697
- if (createdItem.changeMonitorStatusToId && createdItem.projectId) {
698
- // change status of all the monitors.
699
- await MonitorService.changeMonitorStatus(
700
- createdItem.projectId,
701
- createdItem.monitors?.map((monitor: Monitor) => {
702
- return new ObjectID(monitor._id || "");
703
- }) || [],
704
- createdItem.changeMonitorStatusToId,
705
- true, // notifyMonitorOwners
706
- createdItem.rootCause ||
707
- "Status was changed because Incident #" +
708
- createdItem.incidentNumber?.toString() +
709
- " was created.",
710
- createdItem.createdStateLog,
711
- onCreate.createBy.props,
712
- );
804
+ await this.changeIncidentState({
805
+ projectId: createdItem.projectId,
806
+ incidentId: createdItem.id,
807
+ incidentStateId: createdItem.currentIncidentStateId,
808
+ shouldNotifyStatusPageSubscribers: Boolean(
809
+ createdItem.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
810
+ ),
811
+ isSubscribersNotified: Boolean(
812
+ createdItem.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
813
+ ), // we dont want to notify subscribers when incident state changes because they are already notified when the incident is created.
814
+ notifyOwners: false,
815
+ rootCause: createdItem.rootCause,
816
+ stateChangeLog: createdItem.createdStateLog,
817
+ props: {
818
+ isRoot: true,
819
+ },
820
+ });
821
+ } catch (error) {
822
+ logger.error(`Error in handleIncidentStateChangeAsync: ${error}`);
823
+ throw error;
713
824
  }
825
+ }
714
826
 
715
- await this.changeIncidentState({
716
- projectId: createdItem.projectId,
717
- incidentId: createdItem.id,
718
- incidentStateId: createdItem.currentIncidentStateId,
719
- shouldNotifyStatusPageSubscribers: Boolean(
720
- createdItem.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
721
- ),
722
- isSubscribersNotified: Boolean(
723
- createdItem.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
724
- ), // we dont want to notify subscribers when incident state changes because they are already notified when the incident is created.
725
- notifyOwners: false,
726
- rootCause: createdItem.rootCause,
727
- stateChangeLog: createdItem.createdStateLog,
728
- props: {
729
- isRoot: true,
730
- },
731
- });
732
-
733
- // add owners.
827
+ @CaptureSpan()
828
+ private async executeOnCallDutyPoliciesAsync(
829
+ createdItem: Model,
830
+ ): Promise<void> {
831
+ try {
832
+ if (
833
+ createdItem.onCallDutyPolicies?.length &&
834
+ createdItem.onCallDutyPolicies?.length > 0
835
+ ) {
836
+ // Execute all on-call policies in parallel
837
+ const policyPromises: Promise<void>[] =
838
+ createdItem.onCallDutyPolicies.map((policy: OnCallDutyPolicy) => {
839
+ return OnCallDutyPolicyService.executePolicy(
840
+ new ObjectID(policy["_id"] as string),
841
+ {
842
+ triggeredByIncidentId: createdItem.id!,
843
+ userNotificationEventType:
844
+ UserNotificationEventType.IncidentCreated,
845
+ },
846
+ );
847
+ });
734
848
 
735
- if (
736
- onCreate.createBy.miscDataProps &&
737
- (onCreate.createBy.miscDataProps["ownerTeams"] ||
738
- onCreate.createBy.miscDataProps["ownerUsers"])
739
- ) {
740
- await this.addOwners(
741
- createdItem.projectId,
742
- createdItem.id,
743
- (onCreate.createBy.miscDataProps["ownerUsers"] as Array<ObjectID>) ||
744
- [],
745
- (onCreate.createBy.miscDataProps["ownerTeams"] as Array<ObjectID>) ||
746
- [],
747
- false,
748
- onCreate.createBy.props,
749
- );
849
+ await Promise.allSettled(policyPromises);
850
+ }
851
+ } catch (error) {
852
+ logger.error(`Error in executeOnCallDutyPoliciesAsync: ${error}`);
853
+ throw error;
750
854
  }
855
+ }
751
856
 
752
- if (
753
- createdItem.onCallDutyPolicies?.length &&
754
- createdItem.onCallDutyPolicies?.length > 0
755
- ) {
756
- for (const policy of createdItem.onCallDutyPolicies) {
757
- await OnCallDutyPolicyService.executePolicy(
758
- new ObjectID(policy._id as string),
759
- {
760
- triggeredByIncidentId: createdItem.id!,
761
- userNotificationEventType:
762
- UserNotificationEventType.IncidentCreated,
763
- },
857
+ @CaptureSpan()
858
+ private async handleMonitorStatusChangeAsync(
859
+ createdItem: Model,
860
+ onCreate: OnCreate<Model>,
861
+ ): Promise<void> {
862
+ try {
863
+ if (createdItem.changeMonitorStatusToId && createdItem.projectId) {
864
+ // change status of all the monitors.
865
+ await MonitorService.changeMonitorStatus(
866
+ createdItem.projectId,
867
+ createdItem.monitors?.map((monitor: Monitor) => {
868
+ return new ObjectID(monitor._id || "");
869
+ }) || [],
870
+ createdItem.changeMonitorStatusToId,
871
+ true, // notifyMonitorOwners
872
+ createdItem.rootCause ||
873
+ "Status was changed because Incident #" +
874
+ createdItem.incidentNumber?.toString() +
875
+ " was created.",
876
+ createdItem.createdStateLog,
877
+ onCreate.createBy.props,
764
878
  );
765
879
  }
880
+ } catch (error) {
881
+ logger.error(`Error in handleMonitorStatusChangeAsync: ${error}`);
882
+ throw error;
766
883
  }
884
+ }
767
885
 
768
- // check if the incident is created manaull by a user and if thats the case, then disable active monitoting on that monitor.
769
-
770
- await this.disableActiveMonitoringIfManualIncident(createdItem.id!);
886
+ @CaptureSpan()
887
+ private releaseMutexAsync(
888
+ onCreate: OnCreate<Model>,
889
+ projectId: ObjectID,
890
+ ): void {
891
+ // Release mutex in background without blocking
892
+ if (onCreate.carryForward && onCreate.carryForward.mutex) {
893
+ const mutex: SemaphoreMutex = onCreate.carryForward.mutex;
771
894
 
772
- return createdItem;
895
+ setImmediate(async () => {
896
+ try {
897
+ await Semaphore.release(mutex);
898
+ logger.debug(
899
+ "Mutex released - IncidentService.incident-create " +
900
+ projectId.toString() +
901
+ " at " +
902
+ OneUptimeDate.getCurrentDateAsFormattedString(),
903
+ );
904
+ } catch (err) {
905
+ logger.debug(
906
+ "Mutex release failed - IncidentService.incident-create " +
907
+ projectId.toString() +
908
+ " at " +
909
+ OneUptimeDate.getCurrentDateAsFormattedString(),
910
+ );
911
+ logger.error(err);
912
+ }
913
+ });
914
+ }
773
915
  }
774
916
 
917
+ @CaptureSpan()
775
918
  public async disableActiveMonitoringIfManualIncident(
776
919
  incidentId: ObjectID,
777
920
  ): Promise<void> {
@@ -516,7 +516,7 @@ ${createdItem.description?.trim() || "No description provided."}
516
516
  onCreate.createBy.props,
517
517
  );
518
518
 
519
- // 2. Start heavy operations in parallel that can run asynchronously
519
+ // 2. Start core operations in parallel that can run asynchronously (excluding workspace operations)
520
520
 
521
521
  // Add default probes if needed (can be slow with many probes)
522
522
  if (
@@ -535,21 +535,6 @@ ${createdItem.description?.trim() || "No description provided."}
535
535
  );
536
536
  }
537
537
 
538
- // Workspace operations (can be slow due to external API calls)
539
- parallelOperations.push(
540
- this.handleWorkspaceOperationsAsync({
541
- projectId: createdItem.projectId,
542
- monitorId: createdItem.id!,
543
- monitorName: createdItem.name!,
544
- feedInfoInMarkdown,
545
- createdByUserId,
546
- }).catch((error: Error) => {
547
- logger.error("Error in workspace operations");
548
- logger.error(error);
549
- // Don't fail monitor creation due to workspace issues
550
- }),
551
- );
552
-
553
538
  // Billing operations
554
539
  if (IsBillingEnabled) {
555
540
  parallelOperations.push(
@@ -596,11 +581,27 @@ ${createdItem.description?.trim() || "No description provided."}
596
581
  }),
597
582
  );
598
583
 
599
- // Wait for all parallel operations to complete (but don't block monitor creation)
600
- Promise.allSettled(parallelOperations).catch((error: Error) => {
601
- logger.error("Error in parallel monitor creation operations");
602
- logger.error(error);
603
- });
584
+ // Wait for core operations to complete, then handle workspace operations
585
+ Promise.allSettled(parallelOperations)
586
+ .then(() => {
587
+ // Handle workspace operations after core operations complete
588
+ // Run workspace operations in background without blocking response
589
+ this.handleWorkspaceOperationsAsync({
590
+ projectId: createdItem.projectId!,
591
+ monitorId: createdItem.id!,
592
+ monitorName: createdItem.name!,
593
+ feedInfoInMarkdown,
594
+ createdByUserId,
595
+ }).catch((error: Error) => {
596
+ logger.error("Error in workspace operations");
597
+ logger.error(error);
598
+ // Don't fail monitor creation due to workspace issues
599
+ });
600
+ })
601
+ .catch((error: Error) => {
602
+ logger.error("Error in parallel monitor creation operations");
603
+ logger.error(error);
604
+ });
604
605
 
605
606
  return createdItem;
606
607
  }