@oneuptime/common 9.4.9 → 9.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/Models/DatabaseModels/Alert.ts +1 -0
  2. package/Models/DatabaseModels/AlertFeed.ts +1 -0
  3. package/Models/DatabaseModels/Project.ts +29 -0
  4. package/Server/API/BillingAPI.ts +78 -1
  5. package/Server/BillingConfig.ts +3 -0
  6. package/Server/EnvironmentConfig.ts +1 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1769599843642-MigrationName.ts +29 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  9. package/Server/Services/AIBillingService.ts +10 -0
  10. package/Server/Services/AlertEpisodeMemberService.ts +49 -1
  11. package/Server/Services/AlertGroupingEngineService.ts +3 -168
  12. package/Server/Services/BillingService.ts +351 -1
  13. package/Server/Services/NotificationService.ts +10 -0
  14. package/Server/Services/ProjectService.ts +33 -2
  15. package/Server/Services/UserNotificationRuleService.ts +62 -33
  16. package/Server/Services/UserService.ts +45 -1
  17. package/Server/Types/Database/Permissions/TenantPermission.ts +20 -0
  18. package/Types/Database/TableColumn.ts +1 -0
  19. package/Types/Email/EmailTemplateType.ts +1 -0
  20. package/Utils/Schema/ModelSchema.ts +2 -0
  21. package/build/dist/Models/DatabaseModels/Alert.js +1 -0
  22. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  23. package/build/dist/Models/DatabaseModels/AlertFeed.js +1 -0
  24. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  25. package/build/dist/Models/DatabaseModels/Project.js +30 -0
  26. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  27. package/build/dist/Server/API/BillingAPI.js +44 -1
  28. package/build/dist/Server/API/BillingAPI.js.map +1 -1
  29. package/build/dist/Server/BillingConfig.js +2 -0
  30. package/build/dist/Server/BillingConfig.js.map +1 -1
  31. package/build/dist/Server/EnvironmentConfig.js +1 -0
  32. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769599843642-MigrationName.js +16 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769599843642-MigrationName.js.map +1 -0
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  36. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  37. package/build/dist/Server/Services/AIBillingService.js +10 -1
  38. package/build/dist/Server/Services/AIBillingService.js.map +1 -1
  39. package/build/dist/Server/Services/AlertEpisodeMemberService.js +43 -1
  40. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
  41. package/build/dist/Server/Services/AlertGroupingEngineService.js +8 -137
  42. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  43. package/build/dist/Server/Services/BillingService.js +225 -5
  44. package/build/dist/Server/Services/BillingService.js.map +1 -1
  45. package/build/dist/Server/Services/NotificationService.js +10 -1
  46. package/build/dist/Server/Services/NotificationService.js.map +1 -1
  47. package/build/dist/Server/Services/ProjectService.js +16 -3
  48. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  49. package/build/dist/Server/Services/UserNotificationRuleService.js +50 -39
  50. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  51. package/build/dist/Server/Services/UserService.js +40 -0
  52. package/build/dist/Server/Services/UserService.js.map +1 -1
  53. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +17 -0
  54. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  55. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  56. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  57. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  58. package/build/dist/Utils/Schema/ModelSchema.js +2 -0
  59. package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
  60. package/package.json +1 -1
@@ -1044,6 +1044,7 @@ export default class Alert extends BaseModel {
1044
1044
  required: false,
1045
1045
  type: TableColumnType.Number,
1046
1046
  computed: true,
1047
+ canReadOnRelationQuery: true,
1047
1048
  title: "Alert Number",
1048
1049
  description: "Alert Number",
1049
1050
  example: 42,
@@ -39,6 +39,7 @@ export enum AlertFeedEventType {
39
39
  OnCallPolicy = "OnCallPolicy",
40
40
  OnCallNotification = "OnCallNotification",
41
41
  AddedToEpisode = "AddedToEpisode",
42
+ RemovedFromEpisode = "RemovedFromEpisode",
42
43
  }
43
44
 
44
45
  @EnableDocumentation()
@@ -1105,6 +1105,35 @@ export default class Project extends TenantModel {
1105
1105
  })
1106
1106
  public enableAutoRechargeAiBalance?: boolean = undefined;
1107
1107
 
1108
+ @ColumnAccessControl({
1109
+ create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
1110
+ read: [
1111
+ Permission.ProjectOwner,
1112
+ Permission.ProjectAdmin,
1113
+ Permission.ProjectMember,
1114
+ Permission.ReadProject,
1115
+ Permission.UnAuthorizedSsoUser,
1116
+ Permission.ProjectUser,
1117
+ ],
1118
+ update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
1119
+ })
1120
+ @TableColumn({
1121
+ required: true,
1122
+ isDefaultValueColumn: true,
1123
+ type: TableColumnType.Boolean,
1124
+ title: "Send Invoices by Email",
1125
+ description:
1126
+ "When enabled, invoices will be automatically sent to the finance/accounting email when they are generated.",
1127
+ defaultValue: false,
1128
+ example: true,
1129
+ })
1130
+ @Column({
1131
+ nullable: false,
1132
+ default: false,
1133
+ type: ColumnType.Boolean,
1134
+ })
1135
+ public sendInvoicesByEmail?: boolean = undefined;
1136
+
1108
1137
  @ColumnAccessControl({
1109
1138
  create: [],
1110
1139
  read: [],
@@ -1,4 +1,5 @@
1
- import { IsBillingEnabled } from "../EnvironmentConfig";
1
+ import { BillingWebhookSecret, IsBillingEnabled } from "../EnvironmentConfig";
2
+ import Stripe from "stripe";
2
3
  import UserMiddleware from "../Middleware/UserAuthorization";
3
4
  import BillingService from "../Services/BillingService";
4
5
  import ProjectService from "../Services/ProjectService";
@@ -16,6 +17,7 @@ import Project from "../../Models/DatabaseModels/Project";
16
17
  import CommonAPI from "./CommonAPI";
17
18
  import ObjectID from "../../Types/ObjectID";
18
19
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
20
+ import logger from "../Utils/Logger";
19
21
 
20
22
  export default class BillingAPI {
21
23
  public router: ExpressRouter;
@@ -23,6 +25,81 @@ export default class BillingAPI {
23
25
  public constructor() {
24
26
  this.router = Express.getRouter();
25
27
 
28
+ // Stripe webhook endpoint - uses raw body captured by JSON parser for signature verification
29
+ this.router.post(
30
+ `/billing/webhook`,
31
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
32
+ try {
33
+ logger.debug(
34
+ `[Invoice Email] Webhook endpoint hit - /billing/webhook`,
35
+ );
36
+
37
+ if (!IsBillingEnabled) {
38
+ logger.debug(
39
+ `[Invoice Email] Billing not enabled, returning early`,
40
+ );
41
+ return Response.sendJsonObjectResponse(req, res, {
42
+ message: "Billing is not enabled",
43
+ });
44
+ }
45
+
46
+ if (!BillingWebhookSecret) {
47
+ logger.error(
48
+ `[Invoice Email] Billing webhook secret is not configured`,
49
+ );
50
+ throw new BadDataException(
51
+ "Billing webhook secret is not configured",
52
+ );
53
+ }
54
+
55
+ const signature: string = req.headers["stripe-signature"] as string;
56
+ logger.debug(
57
+ `[Invoice Email] Stripe signature header present: ${Boolean(signature)}`,
58
+ );
59
+
60
+ if (!signature) {
61
+ logger.error(`[Invoice Email] Missing Stripe signature header`);
62
+ throw new BadDataException("Missing Stripe signature header");
63
+ }
64
+
65
+ const rawBody: string | undefined = (req as OneUptimeRequest).rawBody;
66
+ logger.debug(
67
+ `[Invoice Email] Raw body present: ${Boolean(rawBody)}, length: ${rawBody?.length || 0}`,
68
+ );
69
+
70
+ if (!rawBody) {
71
+ logger.error(
72
+ `[Invoice Email] Missing raw body for webhook verification`,
73
+ );
74
+ throw new BadDataException(
75
+ "Missing raw body for webhook verification",
76
+ );
77
+ }
78
+
79
+ logger.debug(`[Invoice Email] Verifying webhook signature...`);
80
+ const event: Stripe.Event = BillingService.verifyWebhookSignature(
81
+ rawBody,
82
+ signature,
83
+ );
84
+ logger.debug(
85
+ `[Invoice Email] Webhook signature verified successfully, event type: ${event.type}`,
86
+ );
87
+
88
+ // Handle the event asynchronously
89
+ logger.debug(`[Invoice Email] Handling webhook event...`);
90
+ await BillingService.handleWebhookEvent(event);
91
+ logger.debug(`[Invoice Email] Webhook event handled successfully`);
92
+
93
+ return Response.sendJsonObjectResponse(req, res, {
94
+ received: true,
95
+ });
96
+ } catch (err) {
97
+ logger.error(`[Invoice Email] Stripe webhook error: ${err}`);
98
+ next(err);
99
+ }
100
+ },
101
+ );
102
+
26
103
  this.router.get(
27
104
  `/billing/customer-balance`,
28
105
  UserMiddleware.getUserMiddleware,
@@ -1,9 +1,12 @@
1
1
  const IsBillingEnabled: boolean = process.env["BILLING_ENABLED"] === "true";
2
2
  const BillingPublicKey: string = process.env["BILLING_PUBLIC_KEY"] || "";
3
3
  const BillingPrivateKey: string = process.env["BILLING_PRIVATE_KEY"] || "";
4
+ const BillingWebhookSecret: string =
5
+ process.env["BILLING_WEBHOOK_SECRET"] || "";
4
6
 
5
7
  export default {
6
8
  IsBillingEnabled,
7
9
  BillingPublicKey,
8
10
  BillingPrivateKey,
11
+ BillingWebhookSecret,
9
12
  };
@@ -102,6 +102,7 @@ const parsePositiveNumberFromEnv: (
102
102
  export const IsBillingEnabled: boolean = BillingConfig.IsBillingEnabled;
103
103
  export const BillingPublicKey: string = BillingConfig.BillingPublicKey;
104
104
  export const BillingPrivateKey: string = BillingConfig.BillingPrivateKey;
105
+ export const BillingWebhookSecret: string = BillingConfig.BillingWebhookSecret;
105
106
 
106
107
  export const DatabaseHost: Hostname = Hostname.fromString(
107
108
  process.env["DATABASE_HOST"] || "postgres",
@@ -0,0 +1,29 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1769599843642 implements MigrationInterface {
4
+ public name = "MigrationName1769599843642";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "Project" ADD "sendInvoicesByEmail" boolean NOT NULL DEFAULT false`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
12
+ );
13
+ await queryRunner.query(
14
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
15
+ );
16
+ }
17
+
18
+ public async down(queryRunner: QueryRunner): Promise<void> {
19
+ await queryRunner.query(
20
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
21
+ );
22
+ await queryRunner.query(
23
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "Project" DROP COLUMN "sendInvoicesByEmail"`,
27
+ );
28
+ }
29
+ }
@@ -236,6 +236,7 @@ import { MigrationName1769428619414 } from "./1769428619414-MigrationName";
236
236
  import { MigrationName1769428821686 } from "./1769428821686-MigrationName";
237
237
  import { MigrationName1769469813786 } from "./1769469813786-MigrationName";
238
238
  import { RenameNotificationRuleTypes1769517677937 } from "./1769517677937-RenameNotificationRuleTypes";
239
+ import { MigrationName1769599843642 } from "./1769599843642-MigrationName";
239
240
 
240
241
  export default [
241
242
  InitialMigration,
@@ -476,4 +477,5 @@ export default [
476
477
  MigrationName1769428821686,
477
478
  MigrationName1769469813786,
478
479
  RenameNotificationRuleTypes1769517677937,
480
+ MigrationName1769599843642,
479
481
  ];
@@ -7,6 +7,7 @@ import BaseService from "./BaseService";
7
7
  import BillingService from "./BillingService";
8
8
  import ProjectService from "./ProjectService";
9
9
  import BadDataException from "../../Types/Exception/BadDataException";
10
+ import Email from "../../Types/Email";
10
11
  import ObjectID from "../../Types/ObjectID";
11
12
  import Project from "../../Models/DatabaseModels/Project";
12
13
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
@@ -39,6 +40,8 @@ export class AIBillingService extends BaseService {
39
40
  paymentProviderCustomerId: true,
40
41
  name: true,
41
42
  failedAiBalanceChargeNotificationSentToOwners: true,
43
+ sendInvoicesByEmail: true,
44
+ financeAccountingEmail: true,
42
45
  },
43
46
  props: {
44
47
  isRoot: true,
@@ -89,6 +92,13 @@ export class AIBillingService extends BaseService {
89
92
  project.paymentProviderCustomerId!,
90
93
  "AI Balance Recharge",
91
94
  amountInUSD,
95
+ {
96
+ sendInvoiceByEmail: project.sendInvoicesByEmail || false,
97
+ recipientEmail: project.financeAccountingEmail
98
+ ? new Email(project.financeAccountingEmail)
99
+ : undefined,
100
+ projectId: project.id || undefined,
101
+ },
92
102
  );
93
103
 
94
104
  await ProjectService.updateOneById({
@@ -6,11 +6,14 @@ import BadDataException from "../../Types/Exception/BadDataException";
6
6
  import ObjectID from "../../Types/ObjectID";
7
7
  import Model from "../../Models/DatabaseModels/AlertEpisodeMember";
8
8
  import Alert from "../../Models/DatabaseModels/Alert";
9
+ import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
9
10
  import { IsBillingEnabled } from "../EnvironmentConfig";
10
11
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
11
12
  import logger from "../Utils/Logger";
12
13
  import AlertEpisodeFeedService from "./AlertEpisodeFeedService";
14
+ import AlertFeedService from "./AlertFeedService";
13
15
  import { AlertEpisodeFeedEventType } from "../../Models/DatabaseModels/AlertEpisodeFeed";
16
+ import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
14
17
  import { Yellow500, Green500 } from "../../Types/BrandColors";
15
18
  import OneUptimeDate from "../../Types/Date";
16
19
  import AlertService from "./AlertService";
@@ -124,7 +127,19 @@ export class Service extends DatabaseService<Model> {
124
127
  },
125
128
  });
126
129
 
127
- // Create feed item
130
+ // Get episode details for feed
131
+ const episode: AlertEpisode | null = await AlertEpisodeService.findOneById({
132
+ id: createdItem.alertEpisodeId,
133
+ select: {
134
+ episodeNumber: true,
135
+ title: true,
136
+ },
137
+ props: {
138
+ isRoot: true,
139
+ },
140
+ });
141
+
142
+ // Create feed item on episode
128
143
  await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
129
144
  alertEpisodeId: createdItem.alertEpisodeId,
130
145
  projectId: createdItem.projectId,
@@ -134,6 +149,16 @@ export class Service extends DatabaseService<Model> {
134
149
  userId: createdItem.addedByUserId || undefined,
135
150
  });
136
151
 
152
+ // Create feed item on alert
153
+ await AlertFeedService.createAlertFeedItem({
154
+ alertId: createdItem.alertId,
155
+ projectId: createdItem.projectId,
156
+ alertFeedEventType: AlertFeedEventType.AddedToEpisode,
157
+ displayColor: Yellow500,
158
+ feedInfoInMarkdown: `Added to **Episode #${episode?.episodeNumber || "N/A"}**: ${episode?.title || "No title"}`,
159
+ userId: createdItem.addedByUserId || undefined,
160
+ });
161
+
137
162
  return createdItem;
138
163
  }
139
164
 
@@ -197,6 +222,20 @@ export class Service extends DatabaseService<Model> {
197
222
 
198
223
  // Create feed item for removal
199
224
  if (member.alertEpisodeId && member.projectId) {
225
+ // Get episode details for feed
226
+ const episode: AlertEpisode | null =
227
+ await AlertEpisodeService.findOneById({
228
+ id: member.alertEpisodeId,
229
+ select: {
230
+ episodeNumber: true,
231
+ title: true,
232
+ },
233
+ props: {
234
+ isRoot: true,
235
+ },
236
+ });
237
+
238
+ // Create feed item on episode
200
239
  await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
201
240
  alertEpisodeId: member.alertEpisodeId,
202
241
  projectId: member.projectId,
@@ -204,6 +243,15 @@ export class Service extends DatabaseService<Model> {
204
243
  displayColor: Green500,
205
244
  feedInfoInMarkdown: `**Alert #${alert?.alertNumber || "N/A"}** removed from episode: ${alert?.title || "No title"}`,
206
245
  });
246
+
247
+ // Create feed item on alert
248
+ await AlertFeedService.createAlertFeedItem({
249
+ alertId: member.alertId,
250
+ projectId: member.projectId,
251
+ alertFeedEventType: AlertFeedEventType.RemovedFromEpisode,
252
+ displayColor: Green500,
253
+ feedInfoInMarkdown: `Removed from **Episode #${episode?.episodeNumber || "N/A"}**: ${episode?.title || "No title"}`,
254
+ });
207
255
  }
208
256
  }
209
257
 
@@ -21,10 +21,8 @@ import MonitorService from "./MonitorService";
21
21
  import ServiceMonitorService from "./ServiceMonitorService";
22
22
  import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
23
23
  import AlertEpisodeFeedService from "./AlertEpisodeFeedService";
24
- import AlertFeedService from "./AlertFeedService";
25
24
  import { AlertEpisodeFeedEventType } from "../../Models/DatabaseModels/AlertEpisodeFeed";
26
- import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
27
- import { Green500, Purple500 } from "../../Types/BrandColors";
25
+ import { Green500 } from "../../Types/BrandColors";
28
26
 
29
27
  export interface GroupingResult {
30
28
  grouped: boolean;
@@ -395,9 +393,6 @@ class AlertGroupingEngineServiceClass {
395
393
  existingEpisode.id,
396
394
  AlertEpisodeMemberAddedBy.Rule,
397
395
  rule.id!,
398
- rule,
399
- false, // isNewEpisode
400
- false, // wasReopened
401
396
  );
402
397
 
403
398
  // Update episode severity if alert has higher severity
@@ -440,9 +435,6 @@ class AlertGroupingEngineServiceClass {
440
435
  recentlyResolvedEpisode.id,
441
436
  AlertEpisodeMemberAddedBy.Rule,
442
437
  rule.id!,
443
- rule,
444
- false, // isNewEpisode
445
- true, // wasReopened
446
438
  );
447
439
 
448
440
  // Update episode severity if alert has higher severity
@@ -478,9 +470,6 @@ class AlertGroupingEngineServiceClass {
478
470
  newEpisode.id,
479
471
  AlertEpisodeMemberAddedBy.Rule,
480
472
  rule.id!,
481
- rule,
482
- true, // isNewEpisode
483
- false, // wasReopened
484
473
  );
485
474
 
486
475
  return { grouped: true, episodeId: newEpisode.id, isNewEpisode: true };
@@ -894,9 +883,6 @@ class AlertGroupingEngineServiceClass {
894
883
  episodeId: ObjectID,
895
884
  addedBy: AlertEpisodeMemberAddedBy,
896
885
  ruleId?: ObjectID,
897
- rule?: AlertGroupingRule,
898
- isNewEpisode?: boolean,
899
- wasReopened?: boolean,
900
886
  ): Promise<void> {
901
887
  const member: AlertEpisodeMember = new AlertEpisodeMember();
902
888
  member.projectId = alert.projectId!;
@@ -916,15 +902,7 @@ class AlertGroupingEngineServiceClass {
916
902
  },
917
903
  });
918
904
 
919
- // Create feed entries for alert being added to episode
920
- await this.createAlertAddedFeedEntries({
921
- alert,
922
- episodeId,
923
- addedBy,
924
- rule,
925
- isNewEpisode,
926
- wasReopened,
927
- });
905
+ // Feed entries are created by AlertEpisodeMemberService.onCreateSuccess
928
906
  } catch (error) {
929
907
  // Check if it's a duplicate error (alert already in episode)
930
908
  if (
@@ -938,143 +916,6 @@ class AlertGroupingEngineServiceClass {
938
916
  }
939
917
  }
940
918
 
941
- @CaptureSpan()
942
- private async createAlertAddedFeedEntries(data: {
943
- alert: Alert;
944
- episodeId: ObjectID;
945
- addedBy: AlertEpisodeMemberAddedBy;
946
- rule?: AlertGroupingRule | undefined;
947
- isNewEpisode?: boolean | undefined;
948
- wasReopened?: boolean | undefined;
949
- addedByUserId?: ObjectID | undefined;
950
- }): Promise<void> {
951
- const {
952
- alert,
953
- episodeId,
954
- addedBy,
955
- rule,
956
- isNewEpisode,
957
- wasReopened,
958
- addedByUserId,
959
- } = data;
960
-
961
- // Fetch episode number for feed entry
962
- const episode: AlertEpisode | null = await AlertEpisodeService.findOneById({
963
- id: episodeId,
964
- select: {
965
- episodeNumber: true,
966
- },
967
- props: {
968
- isRoot: true,
969
- },
970
- });
971
-
972
- const episodeNumber: number = episode?.episodeNumber || 0;
973
- const alertNumber: number = alert.alertNumber || 0;
974
-
975
- // Build explanation of why the alert was added
976
- let matchReason: string = "";
977
- let alertFeedMessage: string = "";
978
- let episodeFeedMessage: string = "";
979
-
980
- if (addedBy === AlertEpisodeMemberAddedBy.Rule && rule) {
981
- const matchCriteria: Array<string> = [];
982
-
983
- if (rule.monitors && rule.monitors.length > 0) {
984
- matchCriteria.push("monitor matches rule criteria");
985
- }
986
- if (rule.alertSeverities && rule.alertSeverities.length > 0) {
987
- matchCriteria.push("severity matches rule criteria");
988
- }
989
- if (rule.alertLabels && rule.alertLabels.length > 0) {
990
- matchCriteria.push("alert labels match rule criteria");
991
- }
992
- if (rule.monitorLabels && rule.monitorLabels.length > 0) {
993
- matchCriteria.push("monitor labels match rule criteria");
994
- }
995
- if (rule.alertTitlePattern) {
996
- matchCriteria.push(
997
- `alert title matches pattern \`${rule.alertTitlePattern}\``,
998
- );
999
- }
1000
- if (rule.alertDescriptionPattern) {
1001
- matchCriteria.push(
1002
- `alert description matches pattern \`${rule.alertDescriptionPattern}\``,
1003
- );
1004
- }
1005
- if (rule.monitorNamePattern) {
1006
- matchCriteria.push(
1007
- `monitor name matches pattern \`${rule.monitorNamePattern}\``,
1008
- );
1009
- }
1010
- if (rule.monitorDescriptionPattern) {
1011
- matchCriteria.push(
1012
- `monitor description matches pattern \`${rule.monitorDescriptionPattern}\``,
1013
- );
1014
- }
1015
-
1016
- if (matchCriteria.length === 0) {
1017
- matchCriteria.push("all criteria matched (rule matches all alerts)");
1018
- }
1019
-
1020
- matchReason = `**Match Criteria:**\n- ${matchCriteria.join("\n- ")}`;
1021
-
1022
- if (wasReopened) {
1023
- alertFeedMessage = `➕ **Added to Episode #${episodeNumber}** (reopened) by rule **${rule.name || "Unnamed Rule"}**`;
1024
- episodeFeedMessage = `➕ **Alert #${alertNumber}** added (episode reopened) by rule **${rule.name || "Unnamed Rule"}**`;
1025
- } else if (isNewEpisode) {
1026
- alertFeedMessage = `➕ **Added to new Episode #${episodeNumber}** by rule **${rule.name || "Unnamed Rule"}**`;
1027
- episodeFeedMessage = `➕ **Alert #${alertNumber}** added (initial alert) by rule **${rule.name || "Unnamed Rule"}**`;
1028
- } else {
1029
- alertFeedMessage = `➕ **Added to Episode #${episodeNumber}** by rule **${rule.name || "Unnamed Rule"}**`;
1030
- episodeFeedMessage = `➕ **Alert #${alertNumber}** added by rule **${rule.name || "Unnamed Rule"}**`;
1031
- }
1032
- } else {
1033
- // Manual addition
1034
- alertFeedMessage = `➕ **Manually added to Episode #${episodeNumber}**`;
1035
- episodeFeedMessage = `➕ **Alert #${alertNumber}** manually added`;
1036
- matchReason = "**Reason:** Manually added by user";
1037
- }
1038
-
1039
- // Create alert feed entry
1040
- try {
1041
- await AlertFeedService.createAlertFeedItem({
1042
- alertId: alert.id!,
1043
- projectId: alert.projectId!,
1044
- alertFeedEventType: AlertFeedEventType.AddedToEpisode,
1045
- displayColor: Purple500,
1046
- feedInfoInMarkdown: alertFeedMessage,
1047
- moreInformationInMarkdown: matchReason,
1048
- userId: addedByUserId,
1049
- });
1050
- } catch (feedError) {
1051
- logger.error(
1052
- `Error creating alert feed for alert added to episode: ${feedError}`,
1053
- );
1054
- }
1055
-
1056
- // Create episode feed entry
1057
- try {
1058
- let moreInfo: string = `**Alert:** #${alertNumber}`;
1059
- if (alert.title) {
1060
- moreInfo += ` - ${alert.title}`;
1061
- }
1062
- moreInfo += `\n\n${matchReason}`;
1063
-
1064
- await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
1065
- alertEpisodeId: episodeId,
1066
- projectId: alert.projectId!,
1067
- alertEpisodeFeedEventType: AlertEpisodeFeedEventType.AlertAdded,
1068
- displayColor: Purple500,
1069
- feedInfoInMarkdown: episodeFeedMessage,
1070
- moreInformationInMarkdown: moreInfo,
1071
- userId: addedByUserId,
1072
- });
1073
- } catch (feedError) {
1074
- logger.error(`Error creating episode feed for alert added: ${feedError}`);
1075
- }
1076
- }
1077
-
1078
919
  @CaptureSpan()
1079
920
  public async addAlertToEpisodeManually(
1080
921
  alert: Alert,
@@ -1098,13 +939,7 @@ class AlertGroupingEngineServiceClass {
1098
939
  },
1099
940
  });
1100
941
 
1101
- // Create feed entries for manual addition
1102
- await this.createAlertAddedFeedEntries({
1103
- alert,
1104
- episodeId,
1105
- addedBy: AlertEpisodeMemberAddedBy.Manual,
1106
- addedByUserId,
1107
- });
942
+ // Feed entries are created by AlertEpisodeMemberService.onCreateSuccess
1108
943
 
1109
944
  // Update episode severity if needed
1110
945
  if (alert.alertSeverityId) {