@oneuptime/common 10.0.70 → 10.0.72
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/Alert.ts +55 -0
- package/Models/DatabaseModels/Incident.ts +55 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +6 -4
- package/Models/DatabaseModels/Project.ts +5 -5
- package/Models/DatabaseModels/StatusPage.ts +80 -0
- package/Server/API/StatusPageAPI.ts +4 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +6 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/AIBillingService.ts +2 -2
- package/Server/Services/AnalyticsDatabaseService.ts +17 -7
- package/Server/Services/BillingService.ts +116 -48
- package/Server/Services/NotificationService.ts +2 -2
- package/Server/Types/Database/QueryUtil.ts +13 -7
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +175 -29
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +71 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +170 -7
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +171 -2
- package/Server/Utils/Monitor/MonitorIncident.ts +212 -8
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +423 -1
- package/Server/Utils/Monitor/MonitorResource.ts +2 -0
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +99 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +268 -0
- package/Types/BaseDatabase/IncludesNone.ts +1 -4
- package/Types/Email.ts +50 -0
- package/Types/Infrastructure/BasicMetrics.ts +75 -0
- package/Types/Metrics/MetricQueryData.ts +11 -0
- package/Types/Monitor/CriteriaFilter.ts +10 -0
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +11 -0
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +10 -0
- package/Types/Monitor/MetricMonitor/MetricSeriesResult.ts +20 -0
- package/Types/Monitor/MonitorMetricType.ts +34 -0
- package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +8 -0
- package/Types/Probe/ProbeApiIngestResponse.ts +25 -0
- package/Types/StatusPage/StatusPageLanguage.ts +29 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +17 -12
- package/UI/Components/Charts/Bar/BarChart.tsx +16 -11
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -0
- package/UI/Components/Charts/Line/LineChart.tsx +16 -11
- package/UI/Components/Filters/DateFilter.tsx +16 -8
- package/UI/Components/Filters/EntityFilter.tsx +33 -18
- package/UI/Components/Filters/FilterViewer.tsx +7 -5
- package/UI/Components/Filters/FiltersForm.tsx +27 -5
- package/UI/Components/Filters/NumberFilter.tsx +3 -2
- package/UI/Components/Filters/TextFilter.tsx +5 -4
- package/UI/Components/ModelTable/BaseModelTable.tsx +5 -3
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +453 -0
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +229 -0
- package/Utils/Metrics/MetricSeriesFingerprint.ts +97 -0
- package/Utils/Monitor/MonitorMetricType.ts +309 -19
- package/build/dist/Models/DatabaseModels/Alert.js +57 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +57 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +6 -4
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +5 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +4 -0
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +4 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIBillingService.js +2 -2
- package/build/dist/Server/Services/AIBillingService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +14 -4
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +99 -39
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/NotificationService.js +2 -2
- package/build/dist/Server/Services/NotificationService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +13 -7
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +132 -30
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +58 -7
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +134 -12
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +112 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +159 -15
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +373 -0
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +2 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +65 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +199 -0
- package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -1
- package/build/dist/Types/Email.js +42 -0
- package/build/dist/Types/Email.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +10 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js +2 -0
- package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js +28 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageLanguage.js +21 -0
- package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -12
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +12 -11
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +11 -3
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +12 -11
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Filters/DateFilter.js +1 -4
- package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/EntityFilter.js +21 -14
- package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +1 -2
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +7 -3
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/NumberFilter.js +0 -1
- package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/TextFilter.js +5 -4
- package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +5 -3
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +383 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +109 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js +81 -0
- package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js +287 -19
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/package.json +1 -1
|
@@ -138,13 +138,37 @@ export class BillingService extends BaseService {
|
|
|
138
138
|
line2 = rest.substring(0, 200);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/*
|
|
142
|
+
* financeAccountingEmail may be a comma/semicolon-separated list. Parse the first
|
|
143
|
+
* valid address for Stripe (which stores a single email), and keep the full list
|
|
144
|
+
* in metadata so OneUptime can fan out invoice emails to every recipient.
|
|
145
|
+
*/
|
|
146
|
+
const parsedEmails: Array<Email> = financeAccountingEmail
|
|
147
|
+
? (() => {
|
|
148
|
+
try {
|
|
149
|
+
return Email.parseList(financeAccountingEmail);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.error(
|
|
152
|
+
`[Invoice Email] Failed to parse financeAccountingEmail for customer ${id}: ${err}`,
|
|
153
|
+
);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
})()
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
const normalizedEmailList: string = parsedEmails
|
|
160
|
+
.map((e: Email): string => {
|
|
161
|
+
return e.toString();
|
|
162
|
+
})
|
|
163
|
+
.join(", ");
|
|
164
|
+
|
|
141
165
|
const metadata: Record<string, string> = {
|
|
142
166
|
business_details_full: businessDetails.substring(0, 5000),
|
|
143
167
|
};
|
|
144
|
-
if (
|
|
145
|
-
metadata["finance_accounting_email"] =
|
|
168
|
+
if (normalizedEmailList) {
|
|
169
|
+
metadata["finance_accounting_email"] = normalizedEmailList.substring(
|
|
146
170
|
0,
|
|
147
|
-
|
|
171
|
+
500,
|
|
148
172
|
);
|
|
149
173
|
} else {
|
|
150
174
|
// Remove if cleared
|
|
@@ -165,11 +189,11 @@ export class BillingService extends BaseService {
|
|
|
165
189
|
};
|
|
166
190
|
|
|
167
191
|
/*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
192
|
+
* Stripe's customer.email only accepts one address. Use the first parsed email;
|
|
193
|
+
* the rest are delivered by OneUptime's own webhook-driven invoice email path.
|
|
170
194
|
*/
|
|
171
|
-
if (
|
|
172
|
-
updateParams.email =
|
|
195
|
+
if (parsedEmails.length > 0) {
|
|
196
|
+
updateParams.email = parsedEmails[0]!.toString();
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
if (line1) {
|
|
@@ -956,11 +980,16 @@ export class BillingService extends BaseService {
|
|
|
956
980
|
@CaptureSpan()
|
|
957
981
|
public async sendInvoiceByEmail(
|
|
958
982
|
invoiceId: string,
|
|
959
|
-
|
|
983
|
+
recipientEmails?: Array<Email>,
|
|
960
984
|
projectId?: ObjectID,
|
|
961
985
|
): Promise<void> {
|
|
986
|
+
const recipientsForLog: string = (recipientEmails || [])
|
|
987
|
+
.map((e: Email): string => {
|
|
988
|
+
return e.toString();
|
|
989
|
+
})
|
|
990
|
+
.join(", ");
|
|
962
991
|
logger.debug(
|
|
963
|
-
`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId},
|
|
992
|
+
`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId}, recipients: ${recipientsForLog}`,
|
|
964
993
|
);
|
|
965
994
|
|
|
966
995
|
if (!this.isBillingEnabled()) {
|
|
@@ -986,13 +1015,22 @@ export class BillingService extends BaseService {
|
|
|
986
1015
|
return;
|
|
987
1016
|
}
|
|
988
1017
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1018
|
+
/*
|
|
1019
|
+
* Determine recipient list. Fall back to the invoice's customer email when no
|
|
1020
|
+
* explicit recipients were supplied by the caller.
|
|
1021
|
+
*/
|
|
1022
|
+
let toEmails: Array<Email> = recipientEmails || [];
|
|
1023
|
+
if (toEmails.length === 0 && stripeInvoice.customer_email) {
|
|
1024
|
+
try {
|
|
1025
|
+
toEmails = Email.parseList(stripeInvoice.customer_email);
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
logger.error(
|
|
1028
|
+
`[Invoice Email] Failed to parse Stripe customer_email "${stripeInvoice.customer_email}" for invoice ${invoiceId}: ${err}`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
993
1031
|
}
|
|
994
1032
|
|
|
995
|
-
if (
|
|
1033
|
+
if (toEmails.length === 0) {
|
|
996
1034
|
logger.error(
|
|
997
1035
|
`[Invoice Email] No recipient email found for invoice ${invoiceId}`,
|
|
998
1036
|
{ projectId: projectId?.toString() } as LogAttributes,
|
|
@@ -1021,33 +1059,41 @@ export class BillingService extends BaseService {
|
|
|
1021
1059
|
dashboardLink = `${DashboardClientUrl.toString()}/dashboard/${projectId.toString()}/settings/billing`;
|
|
1022
1060
|
}
|
|
1023
1061
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1062
|
+
for (const toEmail of toEmails) {
|
|
1063
|
+
logger.debug(
|
|
1064
|
+
`[Invoice Email] Sending invoice email to ${toEmail.toString()} - Invoice #${invoiceNumber}, Amount: ${amount}`,
|
|
1065
|
+
);
|
|
1066
|
+
try {
|
|
1067
|
+
await MailService.sendMail(
|
|
1068
|
+
{
|
|
1069
|
+
toEmail: toEmail,
|
|
1070
|
+
templateType: EmailTemplateType.Invoice,
|
|
1071
|
+
vars: {
|
|
1072
|
+
invoiceNumber: invoiceNumber,
|
|
1073
|
+
invoiceDate: invoiceDate,
|
|
1074
|
+
amount: amount,
|
|
1075
|
+
description: description || "",
|
|
1076
|
+
invoicePdfUrl: invoicePdfUrl || "",
|
|
1077
|
+
dashboardLink: dashboardLink || "",
|
|
1078
|
+
},
|
|
1079
|
+
subject: `Invoice #${invoiceNumber} from OneUptime`,
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
projectId: projectId,
|
|
1083
|
+
},
|
|
1084
|
+
);
|
|
1047
1085
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1086
|
+
logger.debug(
|
|
1087
|
+
`[Invoice Email] Successfully sent invoice ${invoiceId} email to ${toEmail.toString()}`,
|
|
1088
|
+
);
|
|
1089
|
+
} catch (perRecipientErr) {
|
|
1090
|
+
// Keep going for the remaining recipients if one fails.
|
|
1091
|
+
logger.error(
|
|
1092
|
+
`[Invoice Email] Failed to send invoice ${invoiceId} to ${toEmail.toString()}: ${perRecipientErr}`,
|
|
1093
|
+
{ projectId: projectId?.toString() } as LogAttributes,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1051
1097
|
} catch (err) {
|
|
1052
1098
|
logger.error(
|
|
1053
1099
|
`[Invoice Email] Failed to send invoice ${invoiceId} by email: ${err}`,
|
|
@@ -1108,16 +1154,21 @@ export class BillingService extends BaseService {
|
|
|
1108
1154
|
amountInUsd: number,
|
|
1109
1155
|
options?: {
|
|
1110
1156
|
sendInvoiceByEmail?: boolean | undefined;
|
|
1111
|
-
|
|
1157
|
+
recipientEmails?: Array<Email> | undefined;
|
|
1112
1158
|
projectId?: ObjectID | undefined;
|
|
1113
1159
|
},
|
|
1114
1160
|
): Promise<void> {
|
|
1115
1161
|
const sendInvoiceByEmail: boolean = options?.sendInvoiceByEmail || false;
|
|
1116
|
-
const
|
|
1162
|
+
const recipientEmails: Array<Email> | undefined = options?.recipientEmails;
|
|
1117
1163
|
const projectId: ObjectID | undefined = options?.projectId;
|
|
1118
1164
|
|
|
1165
|
+
const recipientsForLog: string = (recipientEmails || [])
|
|
1166
|
+
.map((e: Email): string => {
|
|
1167
|
+
return e.toString();
|
|
1168
|
+
})
|
|
1169
|
+
.join(", ");
|
|
1119
1170
|
logger.debug(
|
|
1120
|
-
`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail},
|
|
1171
|
+
`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail}, recipients: ${recipientsForLog}, projectId: ${projectId?.toString()}`,
|
|
1121
1172
|
);
|
|
1122
1173
|
|
|
1123
1174
|
const invoice: Stripe.Invoice = await this.stripe.invoices.create({
|
|
@@ -1161,7 +1212,7 @@ export class BillingService extends BaseService {
|
|
|
1161
1212
|
logger.debug(
|
|
1162
1213
|
`[Invoice Email] sendInvoiceByEmail is true, sending invoice ${invoice.id} by email`,
|
|
1163
1214
|
);
|
|
1164
|
-
await this.sendInvoiceByEmail(invoice.id!,
|
|
1215
|
+
await this.sendInvoiceByEmail(invoice.id!, recipientEmails, projectId);
|
|
1165
1216
|
} else {
|
|
1166
1217
|
logger.debug(
|
|
1167
1218
|
`[Invoice Email] sendInvoiceByEmail is false, skipping email for invoice ${invoice.id}`,
|
|
@@ -1356,16 +1407,33 @@ export class BillingService extends BaseService {
|
|
|
1356
1407
|
},
|
|
1357
1408
|
});
|
|
1358
1409
|
|
|
1359
|
-
let
|
|
1410
|
+
let recipientEmails: Array<Email> | undefined = undefined;
|
|
1360
1411
|
let projectId: ObjectID | undefined = undefined;
|
|
1361
1412
|
|
|
1362
1413
|
if (project) {
|
|
1363
1414
|
projectId = project.id || undefined;
|
|
1364
1415
|
if (project.financeAccountingEmail) {
|
|
1365
|
-
|
|
1416
|
+
try {
|
|
1417
|
+
recipientEmails = Email.parseList(
|
|
1418
|
+
project.financeAccountingEmail,
|
|
1419
|
+
);
|
|
1420
|
+
} catch (parseErr) {
|
|
1421
|
+
/*
|
|
1422
|
+
* Log and fall back to Stripe customer email rather than dropping
|
|
1423
|
+
* the invoice delivery entirely due to a malformed address.
|
|
1424
|
+
*/
|
|
1425
|
+
logger.error(
|
|
1426
|
+
`[Invoice Email] Failed to parse financeAccountingEmail for project ${projectId?.toString()}: ${parseErr}`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1366
1429
|
}
|
|
1430
|
+
const parsedForLog: string = (recipientEmails || [])
|
|
1431
|
+
.map((e: Email): string => {
|
|
1432
|
+
return e.toString();
|
|
1433
|
+
})
|
|
1434
|
+
.join(", ");
|
|
1367
1435
|
logger.debug(
|
|
1368
|
-
`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail: ${
|
|
1436
|
+
`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail recipients: ${parsedForLog}`,
|
|
1369
1437
|
);
|
|
1370
1438
|
} else {
|
|
1371
1439
|
logger.debug(
|
|
@@ -1376,7 +1444,7 @@ export class BillingService extends BaseService {
|
|
|
1376
1444
|
logger.debug(
|
|
1377
1445
|
`[Invoice Email] Sending invoice ${invoice.id} by email`,
|
|
1378
1446
|
);
|
|
1379
|
-
await this.sendInvoiceByEmail(invoice.id,
|
|
1447
|
+
await this.sendInvoiceByEmail(invoice.id, recipientEmails, projectId);
|
|
1380
1448
|
logger.debug(
|
|
1381
1449
|
`[Invoice Email] Successfully processed invoice.finalized - sent invoice ${invoice.id} by email`,
|
|
1382
1450
|
);
|
|
@@ -90,8 +90,8 @@ export class NotificationService extends BaseService {
|
|
|
90
90
|
amountInUSD,
|
|
91
91
|
{
|
|
92
92
|
sendInvoiceByEmail: project.sendInvoicesByEmail || false,
|
|
93
|
-
|
|
94
|
-
?
|
|
93
|
+
recipientEmails: project.financeAccountingEmail
|
|
94
|
+
? Email.parseList(project.financeAccountingEmail)
|
|
95
95
|
: undefined,
|
|
96
96
|
projectId: project.id || undefined,
|
|
97
97
|
},
|
|
@@ -212,8 +212,10 @@ export default class QueryUtil {
|
|
|
212
212
|
relationColumnName: manyToManyMeta.relationColumnName,
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
/*
|
|
216
|
+
* Remove the relation-based filter so TypeORM does not create a
|
|
217
|
+
* JOIN that would yield OR semantics.
|
|
218
|
+
*/
|
|
217
219
|
delete query[key];
|
|
218
220
|
|
|
219
221
|
const existingIdFilter: any = (query as any)._id;
|
|
@@ -445,11 +447,13 @@ export default class QueryUtil {
|
|
|
445
447
|
return null;
|
|
446
448
|
}
|
|
447
449
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
+
/*
|
|
451
|
+
* Only the owning side of a many-to-many has join/inverse columns. Follow
|
|
452
|
+
* the inverse relation when needed.
|
|
453
|
+
*/
|
|
450
454
|
const owningRelation: RelationMetadata = relation.isOwning
|
|
451
455
|
? relation
|
|
452
|
-
:
|
|
456
|
+
: relation.inverseRelation ?? relation;
|
|
453
457
|
|
|
454
458
|
const joinTableName: string | undefined =
|
|
455
459
|
owningRelation.junctionEntityMetadata?.tableName;
|
|
@@ -458,8 +462,10 @@ export default class QueryUtil {
|
|
|
458
462
|
return null;
|
|
459
463
|
}
|
|
460
464
|
|
|
461
|
-
|
|
462
|
-
|
|
465
|
+
/*
|
|
466
|
+
* When `modelType` is the owning side, its id lives on joinColumns. When
|
|
467
|
+
* it is the inverse side, its id lives on inverseJoinColumns.
|
|
468
|
+
*/
|
|
463
469
|
const ownerColumns: Array<any> = relation.isOwning
|
|
464
470
|
? owningRelation.joinColumns
|
|
465
471
|
: owningRelation.inverseJoinColumns;
|
|
@@ -8,6 +8,7 @@ import MetricCriteriaContext, {
|
|
|
8
8
|
MetricComponent,
|
|
9
9
|
MetricComponentValue,
|
|
10
10
|
} from "../../../../Types/Monitor/MetricMonitor/MetricCriteriaContext";
|
|
11
|
+
import MetricSeriesResult from "../../../../Types/Monitor/MetricMonitor/MetricSeriesResult";
|
|
11
12
|
import MonitorStep from "../../../../Types/Monitor/MonitorStep";
|
|
12
13
|
import { JSONObject } from "../../../../Types/JSON";
|
|
13
14
|
import DataToProcess from "../DataToProcess";
|
|
@@ -23,6 +24,20 @@ import CaptureSpan from "../../Telemetry/CaptureSpan";
|
|
|
23
24
|
import MetricUnitUtil from "../../../../Utils/MetricUnitUtil";
|
|
24
25
|
import MetricFormulaEvaluator from "../../../../Utils/Metrics/MetricFormulaEvaluator";
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Result of evaluating a single criteria filter against a single metric
|
|
29
|
+
* series. `rootCause` is null when the filter did not match; otherwise
|
|
30
|
+
* it's the human-readable comparison message. `context` always reflects
|
|
31
|
+
* the metric identity for this series (used to render the metric
|
|
32
|
+
* details + breaching samples section of the incident root cause).
|
|
33
|
+
*/
|
|
34
|
+
export interface MetricSeriesEvaluationResult {
|
|
35
|
+
fingerprint: string | undefined;
|
|
36
|
+
labels: JSONObject;
|
|
37
|
+
rootCause: string | null;
|
|
38
|
+
context: MetricCriteriaContext;
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
export default class MetricMonitorCriteria {
|
|
27
42
|
@CaptureSpan()
|
|
28
43
|
public static async isMonitorInstanceCriteriaFilterMet(input: {
|
|
@@ -30,8 +45,50 @@ export default class MetricMonitorCriteria {
|
|
|
30
45
|
criteriaFilter: CriteriaFilter;
|
|
31
46
|
monitorStep: MonitorStep;
|
|
32
47
|
}): Promise<string | null> {
|
|
33
|
-
|
|
48
|
+
const evaluations: Array<MetricSeriesEvaluationResult> =
|
|
49
|
+
MetricMonitorCriteria.evaluateAllSeries(input);
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* Backwards-compat: the scalar entrypoint collapses per-series
|
|
53
|
+
* evaluation down to the first matching series so existing callers
|
|
54
|
+
* (single-incident path) keep working. The per-series code path uses
|
|
55
|
+
* `evaluateAllSeries` directly.
|
|
56
|
+
*/
|
|
57
|
+
const match: MetricSeriesEvaluationResult | undefined = evaluations.find(
|
|
58
|
+
(e: MetricSeriesEvaluationResult) => {
|
|
59
|
+
return e.rootCause !== null;
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Always populate the legacy single-context field so the root-cause
|
|
65
|
+
* renderer can still read metric identity from the criteria filter
|
|
66
|
+
* even when nothing matched. Pick the first evaluation's context.
|
|
67
|
+
*/
|
|
68
|
+
if (evaluations.length > 0) {
|
|
69
|
+
input.criteriaFilter.metricCriteriaContext = (
|
|
70
|
+
match || evaluations[0]!
|
|
71
|
+
).context;
|
|
72
|
+
}
|
|
34
73
|
|
|
74
|
+
return match ? match.rootCause : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Evaluate a single criteria filter against every series produced by
|
|
79
|
+
* the monitor. For monitors without group-by, this returns a single
|
|
80
|
+
* evaluation covering all aggregated results (legacy behavior). For
|
|
81
|
+
* monitors with group-by attributes set, it returns one evaluation
|
|
82
|
+
* per unique series fingerprint — each with its own
|
|
83
|
+
* `MetricCriteriaContext` carrying that series' breaching samples
|
|
84
|
+
* and labels. The caller fans this out into one incident per
|
|
85
|
+
* breaching series.
|
|
86
|
+
*/
|
|
87
|
+
public static evaluateAllSeries(input: {
|
|
88
|
+
dataToProcess: DataToProcess;
|
|
89
|
+
criteriaFilter: CriteriaFilter;
|
|
90
|
+
monitorStep: MonitorStep;
|
|
91
|
+
}): Array<MetricSeriesEvaluationResult> {
|
|
35
92
|
if (
|
|
36
93
|
input.criteriaFilter.metricMonitorOptions &&
|
|
37
94
|
!input.criteriaFilter.metricMonitorOptions.metricAggregationType
|
|
@@ -41,20 +98,14 @@ export default class MetricMonitorCriteria {
|
|
|
41
98
|
}
|
|
42
99
|
|
|
43
100
|
if (input.criteriaFilter.checkOn !== CheckOn.MetricValue) {
|
|
44
|
-
return
|
|
101
|
+
return [];
|
|
45
102
|
}
|
|
46
103
|
|
|
47
|
-
const rawThreshold: number | null = CompareCriteria.convertToNumber(
|
|
48
|
-
input.criteriaFilter.value,
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const metricAlias: string =
|
|
52
|
-
input.criteriaFilter.metricMonitorOptions?.metricAlias || "";
|
|
53
|
-
|
|
54
104
|
const metricResponse: MetricMonitorResponse =
|
|
55
105
|
input.dataToProcess as MetricMonitorResponse;
|
|
56
|
-
|
|
57
|
-
|
|
106
|
+
|
|
107
|
+
const seriesBreakdown: Array<MetricSeriesResult> | undefined =
|
|
108
|
+
metricResponse.seriesBreakdown;
|
|
58
109
|
|
|
59
110
|
const queryConfigs: Array<MetricQueryConfigData> =
|
|
60
111
|
input.monitorStep.data?.metricMonitor?.metricViewConfig?.queryConfigs ||
|
|
@@ -63,6 +114,60 @@ export default class MetricMonitorCriteria {
|
|
|
63
114
|
input.monitorStep.data?.metricMonitor?.metricViewConfig?.formulaConfigs ||
|
|
64
115
|
[];
|
|
65
116
|
|
|
117
|
+
/*
|
|
118
|
+
* Series-less path: one synthetic "all-series" evaluation over the
|
|
119
|
+
* flat metricResult. Preserves the pre-group-by behavior exactly.
|
|
120
|
+
*/
|
|
121
|
+
if (!seriesBreakdown || seriesBreakdown.length === 0) {
|
|
122
|
+
const result: MetricSeriesEvaluationResult =
|
|
123
|
+
MetricMonitorCriteria.evaluateOneSeries({
|
|
124
|
+
criteriaFilter: input.criteriaFilter,
|
|
125
|
+
aggregatedResults: metricResponse.metricResult || [],
|
|
126
|
+
queryConfigs,
|
|
127
|
+
formulaConfigs,
|
|
128
|
+
seriesFingerprint: undefined,
|
|
129
|
+
seriesLabels: {},
|
|
130
|
+
});
|
|
131
|
+
return [result];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return seriesBreakdown.map((series: MetricSeriesResult) => {
|
|
135
|
+
return MetricMonitorCriteria.evaluateOneSeries({
|
|
136
|
+
criteriaFilter: input.criteriaFilter,
|
|
137
|
+
aggregatedResults: series.aggregatedResults,
|
|
138
|
+
queryConfigs,
|
|
139
|
+
formulaConfigs,
|
|
140
|
+
seriesFingerprint: series.fingerprint,
|
|
141
|
+
seriesLabels: series.labels,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Core evaluation loop: compare the samples for one metric series
|
|
148
|
+
* against the criteria threshold. Builds the metric identity context,
|
|
149
|
+
* identifies breaching samples, and assembles the human-readable
|
|
150
|
+
* root-cause message. Factored out so `evaluateAllSeries` can invoke
|
|
151
|
+
* it once per series without duplicating logic.
|
|
152
|
+
*/
|
|
153
|
+
private static evaluateOneSeries(input: {
|
|
154
|
+
criteriaFilter: CriteriaFilter;
|
|
155
|
+
aggregatedResults: Array<AggregatedResult>;
|
|
156
|
+
queryConfigs: Array<MetricQueryConfigData>;
|
|
157
|
+
formulaConfigs: Array<MetricFormulaConfigData>;
|
|
158
|
+
seriesFingerprint: string | undefined;
|
|
159
|
+
seriesLabels: JSONObject;
|
|
160
|
+
}): MetricSeriesEvaluationResult {
|
|
161
|
+
const rawThreshold: number | null = CompareCriteria.convertToNumber(
|
|
162
|
+
input.criteriaFilter.value,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const metricAlias: string =
|
|
166
|
+
input.criteriaFilter.metricMonitorOptions?.metricAlias || "";
|
|
167
|
+
|
|
168
|
+
const metricAggregatedResult: Array<AggregatedResult> =
|
|
169
|
+
input.aggregatedResults;
|
|
170
|
+
|
|
66
171
|
/*
|
|
67
172
|
* Resolve which query/formula the alias refers to. Use explicit index
|
|
68
173
|
* checks (not `findIndex() || -1`, which incorrectly falls back to -1
|
|
@@ -73,25 +178,25 @@ export default class MetricMonitorCriteria {
|
|
|
73
178
|
let aliasIndex: number = -1;
|
|
74
179
|
|
|
75
180
|
if (metricAlias) {
|
|
76
|
-
const qIdx: number = queryConfigs.findIndex(
|
|
181
|
+
const qIdx: number = input.queryConfigs.findIndex(
|
|
77
182
|
(q: MetricQueryConfigData) => {
|
|
78
183
|
return q.metricAliasData?.metricVariable === metricAlias;
|
|
79
184
|
},
|
|
80
185
|
);
|
|
81
186
|
|
|
82
187
|
if (qIdx >= 0) {
|
|
83
|
-
matchedQuery = queryConfigs[qIdx] || null;
|
|
188
|
+
matchedQuery = input.queryConfigs[qIdx] || null;
|
|
84
189
|
aliasIndex = qIdx;
|
|
85
190
|
} else {
|
|
86
|
-
const fIdx: number = formulaConfigs.findIndex(
|
|
191
|
+
const fIdx: number = input.formulaConfigs.findIndex(
|
|
87
192
|
(f: MetricFormulaConfigData) => {
|
|
88
193
|
return f.metricAliasData?.metricVariable === metricAlias;
|
|
89
194
|
},
|
|
90
195
|
);
|
|
91
196
|
|
|
92
197
|
if (fIdx >= 0) {
|
|
93
|
-
matchedFormula = formulaConfigs[fIdx] || null;
|
|
94
|
-
aliasIndex = queryConfigs.length + fIdx;
|
|
198
|
+
matchedFormula = input.formulaConfigs[fIdx] || null;
|
|
199
|
+
aliasIndex = input.queryConfigs.length + fIdx;
|
|
95
200
|
}
|
|
96
201
|
}
|
|
97
202
|
}
|
|
@@ -105,8 +210,8 @@ export default class MetricMonitorCriteria {
|
|
|
105
210
|
? metricAggregatedResult[aliasIndex]
|
|
106
211
|
: metricAggregatedResult[0];
|
|
107
212
|
|
|
108
|
-
if (!matchedQuery && !matchedFormula && queryConfigs[0]) {
|
|
109
|
-
matchedQuery = queryConfigs[0];
|
|
213
|
+
if (!matchedQuery && !matchedFormula && input.queryConfigs[0]) {
|
|
214
|
+
matchedQuery = input.queryConfigs[0];
|
|
110
215
|
}
|
|
111
216
|
|
|
112
217
|
/*
|
|
@@ -119,14 +224,24 @@ export default class MetricMonitorCriteria {
|
|
|
119
224
|
matchedFormula,
|
|
120
225
|
metricAlias,
|
|
121
226
|
criteriaFilter: input.criteriaFilter,
|
|
122
|
-
queryConfigs,
|
|
123
|
-
formulaConfigs,
|
|
227
|
+
queryConfigs: input.queryConfigs,
|
|
228
|
+
formulaConfigs: input.formulaConfigs,
|
|
124
229
|
});
|
|
125
230
|
|
|
126
|
-
input.
|
|
231
|
+
if (input.seriesFingerprint) {
|
|
232
|
+
metricContext.seriesFingerprint = input.seriesFingerprint;
|
|
233
|
+
}
|
|
234
|
+
if (input.seriesLabels && Object.keys(input.seriesLabels).length > 0) {
|
|
235
|
+
metricContext.seriesLabels = input.seriesLabels;
|
|
236
|
+
}
|
|
127
237
|
|
|
128
238
|
if (rawThreshold === null) {
|
|
129
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
fingerprint: input.seriesFingerprint,
|
|
241
|
+
labels: input.seriesLabels,
|
|
242
|
+
rootCause: null,
|
|
243
|
+
context: metricContext,
|
|
244
|
+
};
|
|
130
245
|
}
|
|
131
246
|
|
|
132
247
|
/*
|
|
@@ -180,11 +295,21 @@ export default class MetricMonitorCriteria {
|
|
|
180
295
|
NoDataPolicy.Ignore;
|
|
181
296
|
|
|
182
297
|
if (policy === NoDataPolicy.Ignore) {
|
|
183
|
-
return
|
|
298
|
+
return {
|
|
299
|
+
fingerprint: input.seriesFingerprint,
|
|
300
|
+
labels: input.seriesLabels,
|
|
301
|
+
rootCause: null,
|
|
302
|
+
context: metricContext,
|
|
303
|
+
};
|
|
184
304
|
}
|
|
185
305
|
|
|
186
306
|
if (policy === NoDataPolicy.Trigger) {
|
|
187
|
-
return
|
|
307
|
+
return {
|
|
308
|
+
fingerprint: input.seriesFingerprint,
|
|
309
|
+
labels: input.seriesLabels,
|
|
310
|
+
rootCause: `No data received for ${metricContext.metricName} in the evaluation window — triggering per no-data policy.`,
|
|
311
|
+
context: metricContext,
|
|
312
|
+
};
|
|
188
313
|
}
|
|
189
314
|
|
|
190
315
|
// TreatAsZero: fall through to the comparator with value 0.
|
|
@@ -206,7 +331,12 @@ export default class MetricMonitorCriteria {
|
|
|
206
331
|
});
|
|
207
332
|
|
|
208
333
|
if (!comparisonMessage) {
|
|
209
|
-
return
|
|
334
|
+
return {
|
|
335
|
+
fingerprint: input.seriesFingerprint,
|
|
336
|
+
labels: input.seriesLabels,
|
|
337
|
+
rootCause: null,
|
|
338
|
+
context: metricContext,
|
|
339
|
+
};
|
|
210
340
|
}
|
|
211
341
|
|
|
212
342
|
/*
|
|
@@ -232,8 +362,8 @@ export default class MetricMonitorCriteria {
|
|
|
232
362
|
matchedFormula
|
|
233
363
|
? MetricMonitorCriteria.buildComponentValueLookup({
|
|
234
364
|
components: metricContext.components || [],
|
|
235
|
-
queryConfigs,
|
|
236
|
-
formulaConfigs,
|
|
365
|
+
queryConfigs: input.queryConfigs,
|
|
366
|
+
formulaConfigs: input.formulaConfigs,
|
|
237
367
|
metricAggregatedResult,
|
|
238
368
|
})
|
|
239
369
|
: null;
|
|
@@ -276,7 +406,12 @@ export default class MetricMonitorCriteria {
|
|
|
276
406
|
metricContext.breachingSamples = breachingSamples;
|
|
277
407
|
}
|
|
278
408
|
|
|
279
|
-
return
|
|
409
|
+
return {
|
|
410
|
+
fingerprint: input.seriesFingerprint,
|
|
411
|
+
labels: input.seriesLabels,
|
|
412
|
+
rootCause: comparisonMessage,
|
|
413
|
+
context: metricContext,
|
|
414
|
+
};
|
|
280
415
|
}
|
|
281
416
|
|
|
282
417
|
private static buildComponentValueLookup(input: {
|
|
@@ -438,6 +573,17 @@ export default class MetricMonitorCriteria {
|
|
|
438
573
|
? Object.keys(q.metricQueryData.groupBy as Record<string, unknown>)
|
|
439
574
|
: [];
|
|
440
575
|
|
|
576
|
+
/*
|
|
577
|
+
* Include user-selected attribute keys as part of the groupBy view
|
|
578
|
+
* so the root-cause block shows "Grouped By: host.name" not just the
|
|
579
|
+
* raw columns ClickHouse was asked to partition on.
|
|
580
|
+
*/
|
|
581
|
+
const groupByAttributeKeys: Array<string> =
|
|
582
|
+
q?.metricQueryData?.groupByAttributeKeys || [];
|
|
583
|
+
const allGroupBy: Array<string> = Array.from(
|
|
584
|
+
new Set([...groupBy, ...groupByAttributeKeys]),
|
|
585
|
+
);
|
|
586
|
+
|
|
441
587
|
const components: Array<MetricComponent> | undefined = f
|
|
442
588
|
? MetricMonitorCriteria.buildFormulaComponents({
|
|
443
589
|
formulaConfig: f,
|
|
@@ -454,7 +600,7 @@ export default class MetricMonitorCriteria {
|
|
|
454
600
|
isFormula: Boolean(f),
|
|
455
601
|
formulaExpression: f?.metricFormulaData?.metricFormula,
|
|
456
602
|
filterAttributes,
|
|
457
|
-
groupBy,
|
|
603
|
+
groupBy: allGroupBy,
|
|
458
604
|
timeWindowMinutes:
|
|
459
605
|
input.criteriaFilter.evaluateOverTimeOptions?.timeValueInMinutes,
|
|
460
606
|
...(components && components.length > 0 ? { components } : {}),
|