@oneuptime/common 10.3.0 → 10.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/Models/DatabaseModels/IncomingCallPolicyLabelRule.ts +522 -0
  2. package/Models/DatabaseModels/IncomingCallPolicyOwnerRule.ts +606 -0
  3. package/Models/DatabaseModels/IncomingCallPolicyOwnerTeam.ts +508 -0
  4. package/Models/DatabaseModels/IncomingCallPolicyOwnerUser.ts +507 -0
  5. package/Models/DatabaseModels/Index.ts +24 -0
  6. package/Models/DatabaseModels/OnCallDutyPolicyLabelRule.ts +522 -0
  7. package/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.ts +606 -0
  8. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.ts +522 -0
  9. package/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.ts +606 -0
  10. package/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam.ts +508 -0
  11. package/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser.ts +507 -0
  12. package/Server/API/TelemetryAPI.ts +69 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1778929624633-AddOnCallDutyPolicyScheduleOwners.ts +153 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1778931537020-AddOnCallIncomingCallOwnersAndRules.ts +823 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  16. package/Server/Services/IncomingCallPolicyLabelRuleEngineService.ts +197 -0
  17. package/Server/Services/IncomingCallPolicyLabelRuleService.ts +10 -0
  18. package/Server/Services/IncomingCallPolicyOwnerRuleEngineService.ts +220 -0
  19. package/Server/Services/IncomingCallPolicyOwnerRuleService.ts +10 -0
  20. package/Server/Services/IncomingCallPolicyOwnerTeamService.ts +9 -0
  21. package/Server/Services/IncomingCallPolicyOwnerUserService.ts +9 -0
  22. package/Server/Services/IncomingCallPolicyService.ts +35 -0
  23. package/Server/Services/OnCallDutyPolicyLabelRuleEngineService.ts +191 -0
  24. package/Server/Services/OnCallDutyPolicyLabelRuleService.ts +10 -0
  25. package/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.ts +214 -0
  26. package/Server/Services/OnCallDutyPolicyOwnerRuleService.ts +10 -0
  27. package/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.ts +200 -0
  28. package/Server/Services/OnCallDutyPolicyScheduleLabelRuleService.ts +10 -0
  29. package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.ts +223 -0
  30. package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleService.ts +10 -0
  31. package/Server/Services/OnCallDutyPolicyScheduleOwnerTeamService.ts +9 -0
  32. package/Server/Services/OnCallDutyPolicyScheduleOwnerUserService.ts +9 -0
  33. package/Server/Services/OnCallDutyPolicyScheduleService.ts +32 -1
  34. package/Server/Services/OnCallDutyPolicyService.ts +21 -0
  35. package/Server/Services/ProfileAggregationService.ts +119 -0
  36. package/Server/Types/Database/Permissions/QueryPermission.ts +36 -0
  37. package/Types/Monitor/MonitorStep.ts +66 -0
  38. package/Types/Permission.ts +475 -0
  39. package/build/dist/Models/DatabaseModels/IncomingCallPolicyLabelRule.js +532 -0
  40. package/build/dist/Models/DatabaseModels/IncomingCallPolicyLabelRule.js.map +1 -0
  41. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerRule.js +615 -0
  42. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerRule.js.map +1 -0
  43. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerTeam.js +524 -0
  44. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerTeam.js.map +1 -0
  45. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerUser.js +523 -0
  46. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerUser.js.map +1 -0
  47. package/build/dist/Models/DatabaseModels/Index.js +22 -0
  48. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyLabelRule.js +532 -0
  50. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyLabelRule.js.map +1 -0
  51. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.js +615 -0
  52. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.js.map +1 -0
  53. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.js +532 -0
  54. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.js.map +1 -0
  55. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.js +615 -0
  56. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.js.map +1 -0
  57. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam.js +524 -0
  58. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam.js.map +1 -0
  59. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser.js +523 -0
  60. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser.js.map +1 -0
  61. package/build/dist/Server/API/TelemetryAPI.js +34 -0
  62. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778929624633-AddOnCallDutyPolicyScheduleOwners.js +58 -0
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778929624633-AddOnCallDutyPolicyScheduleOwners.js.map +1 -0
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778931537020-AddOnCallIncomingCallOwnersAndRules.js +292 -0
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778931537020-AddOnCallIncomingCallOwnersAndRules.js.map +1 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  69. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js +158 -0
  70. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js.map +1 -0
  71. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleService.js +9 -0
  72. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleService.js.map +1 -0
  73. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js +181 -0
  74. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js.map +1 -0
  75. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleService.js +9 -0
  76. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleService.js.map +1 -0
  77. package/build/dist/Server/Services/IncomingCallPolicyOwnerTeamService.js +9 -0
  78. package/build/dist/Server/Services/IncomingCallPolicyOwnerTeamService.js.map +1 -0
  79. package/build/dist/Server/Services/IncomingCallPolicyOwnerUserService.js +9 -0
  80. package/build/dist/Server/Services/IncomingCallPolicyOwnerUserService.js.map +1 -0
  81. package/build/dist/Server/Services/IncomingCallPolicyService.js +38 -0
  82. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  83. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js +157 -0
  84. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js.map +1 -0
  85. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleService.js +9 -0
  86. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleService.js.map +1 -0
  87. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js +180 -0
  88. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js.map +1 -0
  89. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleService.js +9 -0
  90. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleService.js.map +1 -0
  91. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js +158 -0
  92. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js.map +1 -0
  93. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleService.js +9 -0
  94. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleService.js.map +1 -0
  95. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js +181 -0
  96. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js.map +1 -0
  97. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleService.js +9 -0
  98. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleService.js.map +1 -0
  99. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerTeamService.js +9 -0
  100. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerTeamService.js.map +1 -0
  101. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerUserService.js +9 -0
  102. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerUserService.js.map +1 -0
  103. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +21 -0
  104. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  105. package/build/dist/Server/Services/OnCallDutyPolicyService.js +17 -3
  106. package/build/dist/Server/Services/OnCallDutyPolicyService.js.map +1 -1
  107. package/build/dist/Server/Services/ProfileAggregationService.js +80 -0
  108. package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
  109. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +25 -0
  110. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
  111. package/build/dist/Types/Monitor/MonitorStep.js +42 -0
  112. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  113. package/build/dist/Types/Permission.js +415 -0
  114. package/build/dist/Types/Permission.js.map +1 -1
  115. package/package.json +1 -1
@@ -0,0 +1,223 @@
1
+ import Label from "../../Models/DatabaseModels/Label";
2
+ import OnCallDutyPolicySchedule from "../../Models/DatabaseModels/OnCallDutyPolicySchedule";
3
+ import OnCallDutyPolicyScheduleOwnerRule from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule";
4
+ import OnCallDutyPolicyScheduleOwnerUser from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser";
5
+ import OnCallDutyPolicyScheduleOwnerTeam from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam";
6
+ import OnCallDutyPolicyScheduleOwnerRuleService from "./OnCallDutyPolicyScheduleOwnerRuleService";
7
+ import OnCallDutyPolicyScheduleOwnerUserService from "./OnCallDutyPolicyScheduleOwnerUserService";
8
+ import OnCallDutyPolicyScheduleOwnerTeamService from "./OnCallDutyPolicyScheduleOwnerTeamService";
9
+ import OnCallDutyPolicyScheduleService from "./OnCallDutyPolicyScheduleService";
10
+ import ObjectID from "../../Types/ObjectID";
11
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
12
+ import logger, { LogAttributes } from "../Utils/Logger";
13
+
14
+ class OnCallDutyPolicyScheduleOwnerRuleEngineServiceClass {
15
+ @CaptureSpan()
16
+ public async applyRulesToSchedule(
17
+ schedule: OnCallDutyPolicySchedule,
18
+ ): Promise<void> {
19
+ if (!schedule.id || !schedule.projectId) {
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const rules: Array<OnCallDutyPolicyScheduleOwnerRule> =
25
+ await OnCallDutyPolicyScheduleOwnerRuleService.findBy({
26
+ query: {
27
+ projectId: schedule.projectId,
28
+ isEnabled: true,
29
+ },
30
+ props: { isRoot: true },
31
+ select: {
32
+ _id: true,
33
+ name: true,
34
+ notifyOwners: true,
35
+ onCallDutyPolicyScheduleLabels: { _id: true },
36
+ onCallDutyPolicyScheduleNamePattern: true,
37
+ onCallDutyPolicyScheduleDescriptionPattern: true,
38
+ ownerUsers: { _id: true },
39
+ ownerTeams: { _id: true },
40
+ },
41
+ limit: 100,
42
+ skip: 0,
43
+ });
44
+
45
+ if (rules.length === 0) {
46
+ return;
47
+ }
48
+
49
+ const scheduleWithDetails: OnCallDutyPolicySchedule | null =
50
+ await OnCallDutyPolicyScheduleService.findOneById({
51
+ id: schedule.id,
52
+ select: {
53
+ name: true,
54
+ description: true,
55
+ labels: { _id: true },
56
+ },
57
+ props: { isRoot: true },
58
+ });
59
+
60
+ if (!scheduleWithDetails) {
61
+ return;
62
+ }
63
+
64
+ const usersByNotify: Map<boolean, Set<string>> = new Map([
65
+ [true, new Set()],
66
+ [false, new Set()],
67
+ ]);
68
+ const teamsByNotify: Map<boolean, Set<string>> = new Map([
69
+ [true, new Set()],
70
+ [false, new Set()],
71
+ ]);
72
+
73
+ const matchedRules: Array<OnCallDutyPolicyScheduleOwnerRule> = [];
74
+
75
+ for (const rule of rules) {
76
+ const matches: boolean = this.doesScheduleMatchRule(
77
+ scheduleWithDetails,
78
+ rule,
79
+ );
80
+ if (!matches) {
81
+ continue;
82
+ }
83
+ let ruleAddedAny: boolean = false;
84
+ const notify: boolean = rule.notifyOwners !== false;
85
+ for (const user of rule.ownerUsers || []) {
86
+ if (user.id) {
87
+ usersByNotify.get(notify)!.add(user.id.toString());
88
+ ruleAddedAny = true;
89
+ }
90
+ }
91
+ for (const team of rule.ownerTeams || []) {
92
+ if (team.id) {
93
+ teamsByNotify.get(notify)!.add(team.id.toString());
94
+ ruleAddedAny = true;
95
+ }
96
+ }
97
+ if (ruleAddedAny) {
98
+ matchedRules.push(rule);
99
+ }
100
+ }
101
+
102
+ if (matchedRules.length === 0) {
103
+ return;
104
+ }
105
+
106
+ for (const notify of [true, false]) {
107
+ const userIds: Set<string> = usersByNotify.get(notify)!;
108
+ const teamIds: Set<string> = teamsByNotify.get(notify)!;
109
+
110
+ for (const userId of userIds) {
111
+ const owner: OnCallDutyPolicyScheduleOwnerUser =
112
+ new OnCallDutyPolicyScheduleOwnerUser();
113
+ owner.onCallDutyPolicyScheduleId = schedule.id;
114
+ owner.projectId = schedule.projectId;
115
+ owner.userId = new ObjectID(userId);
116
+ owner.isOwnerNotified = !notify;
117
+ await OnCallDutyPolicyScheduleOwnerUserService.create({
118
+ data: owner,
119
+ props: { isRoot: true },
120
+ });
121
+ }
122
+
123
+ for (const teamId of teamIds) {
124
+ const owner: OnCallDutyPolicyScheduleOwnerTeam =
125
+ new OnCallDutyPolicyScheduleOwnerTeam();
126
+ owner.onCallDutyPolicyScheduleId = schedule.id;
127
+ owner.projectId = schedule.projectId;
128
+ owner.teamId = new ObjectID(teamId);
129
+ owner.isOwnerNotified = !notify;
130
+ await OnCallDutyPolicyScheduleOwnerTeamService.create({
131
+ data: owner,
132
+ props: { isRoot: true },
133
+ });
134
+ }
135
+ }
136
+
137
+ logger.debug(
138
+ `OnCallDutyPolicyScheduleOwnerRuleEngine added owners to schedule ${schedule.id}`,
139
+ { projectId: schedule.projectId.toString() } as LogAttributes,
140
+ );
141
+ } catch (error) {
142
+ logger.error(
143
+ `Error applying on-call duty schedule owner rules: ${error}`,
144
+ {
145
+ projectId: schedule.projectId?.toString(),
146
+ onCallDutyPolicyScheduleId: schedule.id?.toString(),
147
+ } as LogAttributes,
148
+ );
149
+ }
150
+ }
151
+
152
+ private doesScheduleMatchRule(
153
+ schedule: OnCallDutyPolicySchedule,
154
+ rule: OnCallDutyPolicyScheduleOwnerRule,
155
+ ): boolean {
156
+ if (
157
+ rule.onCallDutyPolicyScheduleLabels &&
158
+ rule.onCallDutyPolicyScheduleLabels.length > 0
159
+ ) {
160
+ if (!schedule.labels || schedule.labels.length === 0) {
161
+ return false;
162
+ }
163
+ const ruleLabelIds: Array<string> =
164
+ rule.onCallDutyPolicyScheduleLabels.map((l: Label) => {
165
+ return l.id?.toString() || "";
166
+ });
167
+ const labelIds: Array<string> = schedule.labels.map((l: Label) => {
168
+ return l.id?.toString() || "";
169
+ });
170
+ if (
171
+ !ruleLabelIds.some((id: string) => {
172
+ return labelIds.includes(id);
173
+ })
174
+ ) {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ if (
180
+ rule.onCallDutyPolicyScheduleNamePattern &&
181
+ (!schedule.name ||
182
+ !this.testRegex(
183
+ rule.onCallDutyPolicyScheduleNamePattern,
184
+ schedule.name,
185
+ rule,
186
+ ))
187
+ ) {
188
+ return false;
189
+ }
190
+
191
+ if (
192
+ rule.onCallDutyPolicyScheduleDescriptionPattern &&
193
+ (!schedule.description ||
194
+ !this.testRegex(
195
+ rule.onCallDutyPolicyScheduleDescriptionPattern,
196
+ schedule.description,
197
+ rule,
198
+ ))
199
+ ) {
200
+ return false;
201
+ }
202
+
203
+ return true;
204
+ }
205
+
206
+ private testRegex(
207
+ pattern: string,
208
+ value: string,
209
+ rule: OnCallDutyPolicyScheduleOwnerRule,
210
+ ): boolean {
211
+ try {
212
+ const regex: RegExp = new RegExp(pattern, "i");
213
+ return regex.test(value);
214
+ } catch {
215
+ logger.warn(
216
+ `Invalid regex in on-call duty schedule owner rule ${rule.id}: ${pattern}`,
217
+ );
218
+ return false;
219
+ }
220
+ }
221
+ }
222
+
223
+ export default new OnCallDutyPolicyScheduleOwnerRuleEngineServiceClass();
@@ -0,0 +1,10 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule";
3
+
4
+ export class Service extends DatabaseService<Model> {
5
+ public constructor() {
6
+ super(Model);
7
+ }
8
+ }
9
+
10
+ export default new Service();
@@ -0,0 +1,9 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerTeam";
3
+
4
+ export class Service extends DatabaseService<Model> {
5
+ public constructor() {
6
+ super(Model);
7
+ }
8
+ }
9
+ export default new Service();
@@ -0,0 +1,9 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerUser";
3
+
4
+ export class Service extends DatabaseService<Model> {
5
+ public constructor() {
6
+ super(Model);
7
+ }
8
+ }
9
+ export default new Service();
@@ -38,8 +38,10 @@ import OnCallDutyPolicyFeedService from "./OnCallDutyPolicyFeedService";
38
38
  import { OnCallDutyPolicyFeedEventType } from "../../Models/DatabaseModels/OnCallDutyPolicyFeed";
39
39
  import { Green500 } from "../../Types/BrandColors";
40
40
  import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
41
+ import OnCallDutyPolicyScheduleLabelRuleEngineService from "./OnCallDutyPolicyScheduleLabelRuleEngineService";
42
+ import OnCallDutyPolicyScheduleOwnerRuleEngineService from "./OnCallDutyPolicyScheduleOwnerRuleEngineService";
41
43
  import DeleteBy from "../Types/Database/DeleteBy";
42
- import { OnDelete } from "../Types/Database/Hooks";
44
+ import { OnCreate, OnDelete } from "../Types/Database/Hooks";
43
45
  import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
44
46
  import PushNotificationUtil from "../Utils/PushNotificationUtil";
45
47
  import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
@@ -52,6 +54,35 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
52
54
  super(OnCallDutyPolicySchedule);
53
55
  }
54
56
 
57
+ protected override async onCreateSuccess(
58
+ _onCreate: OnCreate<OnCallDutyPolicySchedule>,
59
+ createdItem: OnCallDutyPolicySchedule,
60
+ ): Promise<OnCallDutyPolicySchedule> {
61
+ if (createdItem.projectId && createdItem.id) {
62
+ Promise.resolve()
63
+ .then(async () => {
64
+ await OnCallDutyPolicyScheduleLabelRuleEngineService.applyRulesToSchedule(
65
+ createdItem,
66
+ );
67
+ })
68
+ .then(async () => {
69
+ await OnCallDutyPolicyScheduleOwnerRuleEngineService.applyRulesToSchedule(
70
+ createdItem,
71
+ );
72
+ })
73
+ .catch((error: Error) => {
74
+ logger.error(
75
+ `Error applying on-call schedule rules in OnCallDutyPolicyScheduleService.onCreateSuccess: ${error}`,
76
+ {
77
+ projectId: createdItem.projectId?.toString(),
78
+ onCallDutyPolicyScheduleId: createdItem.id?.toString(),
79
+ } as LogAttributes,
80
+ );
81
+ });
82
+ }
83
+ return createdItem;
84
+ }
85
+
55
86
  protected override async onBeforeDelete(
56
87
  deleteBy: DeleteBy<OnCallDutyPolicySchedule>,
57
88
  ): Promise<OnDelete<OnCallDutyPolicySchedule>> {
@@ -35,6 +35,8 @@ import OnCallDutyPolicyWorkspaceMessages from "../Utils/Workspace/WorkspaceMessa
35
35
  import OnCallDutyPolicyFeedService from "./OnCallDutyPolicyFeedService";
36
36
  import { OnCallDutyPolicyFeedEventType } from "../../Models/DatabaseModels/OnCallDutyPolicyFeed";
37
37
  import { Green500 } from "../../Types/BrandColors";
38
+ import OnCallDutyPolicyLabelRuleEngineService from "./OnCallDutyPolicyLabelRuleEngineService";
39
+ import OnCallDutyPolicyOwnerRuleEngineService from "./OnCallDutyPolicyOwnerRuleEngineService";
38
40
 
39
41
  export class Service extends DatabaseService<OnCallDutyPolicy> {
40
42
  public constructor() {
@@ -49,6 +51,25 @@ export class Service extends DatabaseService<OnCallDutyPolicy> {
49
51
  throw new BadDataException("On Call Policy id not found.");
50
52
  }
51
53
 
54
+ if (createdItem.projectId) {
55
+ try {
56
+ await OnCallDutyPolicyLabelRuleEngineService.applyRulesToOnCallDutyPolicy(
57
+ createdItem,
58
+ );
59
+ await OnCallDutyPolicyOwnerRuleEngineService.applyRulesToOnCallDutyPolicy(
60
+ createdItem,
61
+ );
62
+ } catch (error) {
63
+ logger.error(
64
+ `Error applying on-call duty policy rules in OnCallDutyPolicyService.onCreateSuccess: ${error}`,
65
+ {
66
+ projectId: createdItem.projectId?.toString(),
67
+ onCallDutyPolicyId: createdItem.id?.toString(),
68
+ } as LogAttributes,
69
+ );
70
+ }
71
+ }
72
+
52
73
  const onCallPolicy: OnCallDutyPolicy | null = await this.findOneById({
53
74
  id: createdItem.id,
54
75
  select: {
@@ -59,6 +59,30 @@ export interface FunctionListRequest {
59
59
  sortBy?: "selfValue" | "totalValue" | "sampleCount";
60
60
  }
61
61
 
62
+ export interface ServiceActivityRequest {
63
+ projectId: ObjectID;
64
+ startTime: Date;
65
+ endTime: Date;
66
+ /**
67
+ * Single profile type to filter on. Kept for backwards compat. When
68
+ * `profileTypes` is also supplied, `profileTypes` wins.
69
+ */
70
+ profileType?: string;
71
+ /**
72
+ * Multiple raw profile-type strings to OR together. The UI maps a
73
+ * user-facing category (e.g. "CPU") to all the raw type strings real
74
+ * agents actually emit (e.g. ["cpu", "samples"]).
75
+ */
76
+ profileTypes?: Array<string>;
77
+ }
78
+
79
+ export interface ServiceActivityItem {
80
+ serviceId: string;
81
+ sampleCount: number;
82
+ profileCount: number;
83
+ totalValue: number;
84
+ }
85
+
62
86
  export interface DiffFlamegraphRequest {
63
87
  projectId: ObjectID;
64
88
  baselineStartTime: Date;
@@ -417,6 +441,42 @@ export class ProfileAggregationService {
417
441
  return node;
418
442
  }
419
443
 
444
+ /**
445
+ * Aggregate sample / profile counts per serviceId for a time window.
446
+ * Drives the "loudest services first" sort on the Profiles dashboard
447
+ * so a developer opening the page lands on the workloads that are
448
+ * actually doing work rather than scrolling past kernel-thread noise.
449
+ */
450
+ @CaptureSpan()
451
+ public static async getServiceActivity(
452
+ request: ServiceActivityRequest,
453
+ ): Promise<Array<ServiceActivityItem>> {
454
+ const statement: Statement =
455
+ ProfileAggregationService.buildServiceActivityQuery(request);
456
+
457
+ const dbResult: Results =
458
+ await ProfileSampleDatabaseService.executeQuery(statement);
459
+ const response: DbJSONResponse = await dbResult.json<{
460
+ data?: Array<JSONObject>;
461
+ }>();
462
+
463
+ const rows: Array<JSONObject> = response.data || [];
464
+ const out: Array<ServiceActivityItem> = [];
465
+ for (const row of rows) {
466
+ const serviceId: string = String(row["serviceId"] || "");
467
+ if (!serviceId) {
468
+ continue;
469
+ }
470
+ out.push({
471
+ serviceId,
472
+ sampleCount: Number(row["sampleCount"] || 0),
473
+ profileCount: Number(row["profileCount"] || 0),
474
+ totalValue: Number(row["totalValue"] || 0),
475
+ });
476
+ }
477
+ return out;
478
+ }
479
+
420
480
  // --- Query builders ---
421
481
 
422
482
  private static buildFlamegraphQuery(request: FlamegraphRequest): Statement {
@@ -506,6 +566,65 @@ export class ProfileAggregationService {
506
566
  return statement;
507
567
  }
508
568
 
569
+ private static buildServiceActivityQuery(
570
+ request: ServiceActivityRequest,
571
+ ): Statement {
572
+ /*
573
+ * sum(value) directly — value is stored as Int128 in ClickHouse, so
574
+ * toFloat64OrZero would fail ("Illegal type Int128"); a plain sum is
575
+ * the right idiom here, and we coerce to Number when serialising in
576
+ * getServiceActivity.
577
+ */
578
+ const statement: Statement = SQL`
579
+ SELECT
580
+ toString(serviceId) AS serviceId,
581
+ count() AS sampleCount,
582
+ uniqExact(profileId) AS profileCount,
583
+ toFloat64(sum(value)) AS totalValue
584
+ FROM ${ProfileAggregationService.TABLE_NAME}
585
+ WHERE projectId = ${{
586
+ type: TableColumnType.ObjectID,
587
+ value: request.projectId,
588
+ }}
589
+ AND time >= ${{
590
+ type: TableColumnType.Date,
591
+ value: request.startTime,
592
+ }}
593
+ AND time <= ${{
594
+ type: TableColumnType.Date,
595
+ value: request.endTime,
596
+ }}
597
+ `;
598
+
599
+ /*
600
+ * profileTypes (array) wins over profileType (single) so the UI
601
+ * can OR together every raw type string in a category.
602
+ */
603
+ if (request.profileTypes && request.profileTypes.length > 0) {
604
+ statement.append(
605
+ SQL` AND profileType IN (${{
606
+ type: TableColumnType.Text,
607
+ value: new Includes(request.profileTypes),
608
+ }})`,
609
+ );
610
+ } else if (request.profileType) {
611
+ statement.append(
612
+ SQL` AND profileType = ${{
613
+ type: TableColumnType.Text,
614
+ value: request.profileType,
615
+ }}`,
616
+ );
617
+ }
618
+
619
+ statement.append(
620
+ SQL` GROUP BY serviceId
621
+ ORDER BY sampleCount DESC
622
+ LIMIT 10000`,
623
+ );
624
+
625
+ return statement;
626
+ }
627
+
509
628
  private static appendCommonFilters(
510
629
  statement: Statement,
511
630
  request: Pick<
@@ -9,6 +9,7 @@ import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/Datab
9
9
  import DatabaseCommonInteractionPropsUtil, {
10
10
  PermissionType,
11
11
  } from "../../../../Types/BaseDatabase/DatabaseCommonInteractionPropsUtil";
12
+ import MultiSearch from "../../../../Types/BaseDatabase/MultiSearch";
12
13
  import Columns from "../../../../Types/Database/Columns";
13
14
  import { TableColumnMetadata } from "../../../../Types/Database/TableColumn";
14
15
  import TableColumnType from "../../../../Types/Database/TableColumnType";
@@ -156,6 +157,41 @@ export default class QueryPermission {
156
157
  continue;
157
158
  }
158
159
 
160
+ /*
161
+ * MultiSearch is a synthetic operator: the key itself is not a column,
162
+ * but the operator carries the list of fields it will ILIKE against
163
+ * (QueryUtil.serializeQuery expands it into an OR over those fields).
164
+ * Validate each inner field the same way we validate any other queried
165
+ * column so a client can't search across columns the user can't read.
166
+ */
167
+ if (query[key] instanceof MultiSearch) {
168
+ const multiSearch: MultiSearch = query[key] as unknown as MultiSearch;
169
+
170
+ for (const field of multiSearch.fields) {
171
+ if (excludedColumnNames.includes(field)) {
172
+ continue;
173
+ }
174
+
175
+ if (!tableColumns.includes(field)) {
176
+ throw new BadDataException(
177
+ `Invalid column on ${model.singularName} - ${field}. Column does not exist.`,
178
+ );
179
+ }
180
+
181
+ if (!canReadOnTheseColumns.columns.includes(field)) {
182
+ throw new NotAuthorizedException(
183
+ `You do not have permissions to query on - ${field}. You need any one of these permissions: ${PermissionHelper.getPermissionTitles(
184
+ model.getColumnAccessControlFor(field)
185
+ ? model.getColumnAccessControlFor(field)!.read
186
+ : [],
187
+ ).join(", ")}`,
188
+ );
189
+ }
190
+ }
191
+
192
+ continue;
193
+ }
194
+
159
195
  if (!canReadOnTheseColumns.columns.includes(key)) {
160
196
  if (!tableColumns.includes(key)) {
161
197
  throw new BadDataException(
@@ -64,6 +64,14 @@ export interface MonitorStepType {
64
64
  doNotFollowRedirects?: boolean | undefined;
65
65
  allowSelfSignedCertificates?: boolean | undefined;
66
66
 
67
+ /*
68
+ * mTLS / client certificate authentication (API and Website monitors).
69
+ * Values can be raw PEM strings or {{monitorSecrets.name}} references.
70
+ */
71
+ tlsClientCertificate?: string | undefined;
72
+ tlsClientKey?: string | undefined;
73
+ tlsClientKeyPassphrase?: string | undefined;
74
+
67
75
  // this is for port monitors.
68
76
  monitorDestinationPort?: Port | undefined;
69
77
 
@@ -122,6 +130,9 @@ export default class MonitorStep extends DatabaseProperty {
122
130
  monitorDestination: undefined,
123
131
  doNotFollowRedirects: undefined,
124
132
  allowSelfSignedCertificates: undefined,
133
+ tlsClientCertificate: undefined,
134
+ tlsClientKey: undefined,
135
+ tlsClientKeyPassphrase: undefined,
125
136
  monitorDestinationPort: undefined,
126
137
  monitorCriteria: new MonitorCriteria(),
127
138
  requestType: HTTPMethod.GET,
@@ -160,6 +171,9 @@ export default class MonitorStep extends DatabaseProperty {
160
171
  monitorDestination: undefined,
161
172
  doNotFollowRedirects: undefined,
162
173
  allowSelfSignedCertificates: undefined,
174
+ tlsClientCertificate: undefined,
175
+ tlsClientKey: undefined,
176
+ tlsClientKeyPassphrase: undefined,
163
177
  monitorDestinationPort: undefined,
164
178
  monitorCriteria: MonitorCriteria.getDefaultMonitorCriteria(arg),
165
179
  requestType: HTTPMethod.GET,
@@ -231,6 +245,25 @@ export default class MonitorStep extends DatabaseProperty {
231
245
  return this;
232
246
  }
233
247
 
248
+ public setTlsClientCertificate(
249
+ tlsClientCertificate: string | undefined,
250
+ ): MonitorStep {
251
+ this.data!.tlsClientCertificate = tlsClientCertificate || undefined;
252
+ return this;
253
+ }
254
+
255
+ public setTlsClientKey(tlsClientKey: string | undefined): MonitorStep {
256
+ this.data!.tlsClientKey = tlsClientKey || undefined;
257
+ return this;
258
+ }
259
+
260
+ public setTlsClientKeyPassphrase(
261
+ tlsClientKeyPassphrase: string | undefined,
262
+ ): MonitorStep {
263
+ this.data!.tlsClientKeyPassphrase = tlsClientKeyPassphrase || undefined;
264
+ return this;
265
+ }
266
+
234
267
  public setPort(monitorDestinationPort: Port): MonitorStep {
235
268
  this.data!.monitorDestinationPort = monitorDestinationPort;
236
269
  return this;
@@ -340,6 +373,9 @@ export default class MonitorStep extends DatabaseProperty {
340
373
  monitorDestination: undefined,
341
374
  doNotFollowRedirects: undefined,
342
375
  allowSelfSignedCertificates: undefined,
376
+ tlsClientCertificate: undefined,
377
+ tlsClientKey: undefined,
378
+ tlsClientKeyPassphrase: undefined,
343
379
  monitorDestinationPort: undefined,
344
380
  monitorCriteria: MonitorCriteria.getNewMonitorCriteriaAsJSON(),
345
381
  requestType: HTTPMethod.GET,
@@ -409,6 +445,25 @@ export default class MonitorStep extends DatabaseProperty {
409
445
  return "Request Type is required";
410
446
  }
411
447
 
448
+ if (
449
+ monitorType === MonitorType.API ||
450
+ monitorType === MonitorType.Website
451
+ ) {
452
+ const hasCert: boolean = Boolean(
453
+ value.data.tlsClientCertificate &&
454
+ value.data.tlsClientCertificate.trim(),
455
+ );
456
+ const hasKey: boolean = Boolean(
457
+ value.data.tlsClientKey && value.data.tlsClientKey.trim(),
458
+ );
459
+ if (hasCert && !hasKey) {
460
+ return "Client private key is required when a client certificate is provided";
461
+ }
462
+ if (hasKey && !hasCert) {
463
+ return "Client certificate is required when a client private key is provided";
464
+ }
465
+ }
466
+
412
467
  if (
413
468
  monitorType === MonitorType.Port &&
414
469
  !value.data.monitorDestinationPort
@@ -497,6 +552,9 @@ export default class MonitorStep extends DatabaseProperty {
497
552
  doNotFollowRedirects: this.data.doNotFollowRedirects || undefined,
498
553
  allowSelfSignedCertificates:
499
554
  this.data.allowSelfSignedCertificates || undefined,
555
+ tlsClientCertificate: this.data.tlsClientCertificate || undefined,
556
+ tlsClientKey: this.data.tlsClientKey || undefined,
557
+ tlsClientKeyPassphrase: this.data.tlsClientKeyPassphrase || undefined,
500
558
  monitorDestinationPort:
501
559
  this.data?.monitorDestinationPort?.toJSON() || undefined,
502
560
  monitorCriteria: this.data.monitorCriteria.toJSON(),
@@ -634,6 +692,11 @@ export default class MonitorStep extends DatabaseProperty {
634
692
  doNotFollowRedirects: json["doNotFollowRedirects"] || undefined,
635
693
  allowSelfSignedCertificates:
636
694
  json["allowSelfSignedCertificates"] || undefined,
695
+ tlsClientCertificate:
696
+ (json["tlsClientCertificate"] as string) || undefined,
697
+ tlsClientKey: (json["tlsClientKey"] as string) || undefined,
698
+ tlsClientKeyPassphrase:
699
+ (json["tlsClientKeyPassphrase"] as string) || undefined,
637
700
  monitorDestinationPort: monitorDestinationPort || undefined,
638
701
  monitorCriteria: MonitorCriteria.fromJSON(
639
702
  json["monitorCriteria"] as JSONObject,
@@ -697,6 +760,9 @@ export default class MonitorStep extends DatabaseProperty {
697
760
  requestBody: Zod.string().optional(),
698
761
  doNotFollowRedirects: Zod.boolean().optional(),
699
762
  allowSelfSignedCertificates: Zod.boolean().optional(),
763
+ tlsClientCertificate: Zod.string().optional(),
764
+ tlsClientKey: Zod.string().optional(),
765
+ tlsClientKeyPassphrase: Zod.string().optional(),
700
766
  monitorDestinationPort: Zod.any().optional(),
701
767
  customCode: Zod.string().optional(),
702
768
  screenSizeTypes: Zod.any().optional(),