feat: add Slack webhook notifications for user creation, project management, and subscription updates

This commit is contained in:
Simon Larsen
2025-02-03 17:50:34 +00:00
parent 2ec6902537
commit d1dd57deec
7 changed files with 185 additions and 50 deletions

View File

@@ -279,8 +279,23 @@ export const AllowedSubscribersCountInFreePlan: number = process.env[
? parseInt(process.env["ALLOWED_SUBSCRIBERS_COUNT_IN_FREE_PLAN"].toString())
: 100;
export const NotificationWebhookOnCreateUser: string =
process.env["NOTIFICATION_WEBHOOK_ON_CREATED_USER"] || "";
export const NotificationSlackWebhookOnCreateUser: string =
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER"] || "";
export const NotificationSlackWebhookOnCreateProject: string = process.env[
"NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT"
] || "";
// notification delete project
export const NotificationSlackWebhookOnDeleteProject: string = process.env[
"NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT"
] || "";
// notification subscripton update.
export const NotificationSlackWebhookOnSubscriptionUpdate: string = process.env[
"NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE"
] || "";
export const AdminDashboardClientURL: URL = new URL(
HttpProtocol,

View File

@@ -1,5 +1,5 @@
import ResellerPlan from "Common/Models/DatabaseModels/ResellerPlan";
import { IsBillingEnabled, getAllEnvVars } from "../EnvironmentConfig";
import { IsBillingEnabled, NotificationSlackWebhookOnCreateProject, NotificationSlackWebhookOnDeleteProject, getAllEnvVars } from "../EnvironmentConfig";
import AllMeteredPlans from "../Types/Billing/MeteredPlan/AllMeteredPlans";
import CreateBy from "../Types/Database/CreateBy";
import DeleteBy from "../Types/Database/DeleteBy";
@@ -61,6 +61,8 @@ import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
import AlertSeverityService from "./AlertSeverityService";
import AlertState from "../../Models/DatabaseModels/AlertState";
import AlertStateService from "./AlertStateService";
import SlackUtil from "../Utils/Slack";
import URL from "../../Types/API/URL";
export interface CurrentPlan {
plan: PlanType | null;
@@ -178,8 +180,8 @@ export class ProjectService extends DatabaseService<Model> {
if (promoCode.planType !== data.data.planName) {
throw new BadDataException(
"Promocode is not valid for this plan. Please select the " +
promoCode.planType +
" plan.",
promoCode.planType +
" plan.",
);
}
@@ -310,9 +312,9 @@ export class ProjectService extends DatabaseService<Model> {
logger.debug(
"Changing plan for project " +
project.id?.toString() +
" to " +
plan.getName(),
project.id?.toString() +
" to " +
plan.getName(),
);
if (!project.paymentProviderSubscriptionSeats) {
@@ -324,11 +326,11 @@ export class ProjectService extends DatabaseService<Model> {
logger.debug(
"Changing plan for project " +
project.id?.toString() +
" to " +
plan.getName() +
" with seats " +
project.paymentProviderSubscriptionSeats,
project.id?.toString() +
" to " +
plan.getName() +
" with seats " +
project.paymentProviderSubscriptionSeats,
);
const subscription: {
@@ -350,12 +352,12 @@ export class ProjectService extends DatabaseService<Model> {
logger.debug(
"Changing plan for project " +
project.id?.toString() +
" to " +
plan.getName() +
" with seats " +
project.paymentProviderSubscriptionSeats +
" completed.",
project.id?.toString() +
" to " +
plan.getName() +
" with seats " +
project.paymentProviderSubscriptionSeats +
" completed.",
);
// refresh subscription status.
@@ -391,12 +393,12 @@ export class ProjectService extends DatabaseService<Model> {
logger.debug(
"Changing plan for project " +
project.id?.toString() +
" to " +
plan.getName() +
" with seats " +
project.paymentProviderSubscriptionSeats +
" completed and project updated.",
project.id?.toString() +
" to " +
plan.getName() +
" with seats " +
project.paymentProviderSubscriptionSeats +
" completed and project updated.",
);
}
}
@@ -561,6 +563,57 @@ export class ProjectService extends DatabaseService<Model> {
createdItem = await this.addDefaultScheduledMaintenanceState(createdItem);
createdItem = await this.addDefaultAlertState(createdItem);
if (NotificationSlackWebhookOnCreateProject) {
// fetch project again.
const project: Model | null = await this.findOneById({
id: createdItem.id!,
select: {
name: true,
id: true,
createdOwnerName: true,
createdOwnerEmail: true,
planName: true,
createdByUserId: true,
paymentProviderSubscriptionStatus: true,
},
props: {
isRoot: true,
},
});
if (!project) {
throw new BadDataException("Project not found");
}
let slackMessage: string = `*Project Created:*
*Project Name:* ${project.name?.toString() || "N/A"}
*Project ID:* ${project.id?.toString() || "N/A"}
`;
if (project.createdOwnerName && project.createdOwnerEmail) {
slackMessage += `*Created By:* ${project?.createdOwnerName?.toString() + "(" + project.createdOwnerEmail.toString() + ")" || "N/A"}
`;
if (IsBillingEnabled) {
// which plan?
slackMessage += `*Plan:* ${project.planName?.toString() || "N/A"}
*Subscription Status:* ${project.paymentProviderSubscriptionStatus?.toString() || "N/A"}
`;
}
SlackUtil.sendMessageToChannel({
url: URL.fromString(NotificationSlackWebhookOnCreateProject),
text: slackMessage,
}).catch((error) => {
logger.error("Error sending slack message: " + error);
});
}
}
return createdItem;
}
@@ -1007,29 +1060,75 @@ export class ProjectService extends DatabaseService<Model> {
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
if (IsBillingEnabled) {
const projects: Array<Model> = await this.findBy({
query: deleteBy.query,
props: deleteBy.props,
limit: LIMIT_MAX,
skip: 0,
select: {
_id: true,
paymentProviderSubscriptionId: true,
paymentProviderMeteredSubscriptionId: true,
},
});
return { deleteBy, carryForward: projects };
}
const projects: Array<Model> = await this.findBy({
query: deleteBy.query,
props: {
isRoot: true,
},
limit: LIMIT_MAX,
skip: 0,
select: {
_id: true,
paymentProviderSubscriptionId: true,
paymentProviderMeteredSubscriptionId: true,
name: true,
createdByUser: {
name: true,
email: true,
}
},
});
return { deleteBy, carryForward: projects };
return { deleteBy, carryForward: [] };
}
protected override async onDeleteSuccess(
onDelete: OnDelete<Model>,
_itemIdsBeforeDelete: ObjectID[],
): Promise<OnDelete<Model>> {
if (NotificationSlackWebhookOnDeleteProject) {
for (const project of onDelete.carryForward) {
let subscriptionStatus: SubscriptionStatus | null = null;
if (IsBillingEnabled) {
subscriptionStatus = await BillingService.getSubscriptionStatus(
project.paymentProviderSubscriptionId!,
);
}
let slackMessage: string = `*Project Deleted:*
*Project Name:* ${project.name?.toString() || "N/A"}
*Project ID:* ${project._id?.toString() || "N/A"}
`;
if (subscriptionStatus) {
slackMessage += `*Project Subscription Status:* ${subscriptionStatus?.toString() || "N/A"}
`;
}
if (project.createdByUser && project.createdByUser.name && project.createdByUser.email) {
slackMessage += `*Created By:* ${project?.createdByUser.name?.toString() + "(" + project.createdByUser.email.toString() + ")" || "N/A"}
`;
}
SlackUtil.sendMessageToChannel({
url: URL.fromString(NotificationSlackWebhookOnDeleteProject),
text: slackMessage,
}).catch((err: Error) => {
// log this error but do not throw it. Not important enough to stop the process.
logger.error(err);
});
}
}
// get project id
if (IsBillingEnabled) {
for (const project of onDelete.carryForward) {
@@ -1047,6 +1146,8 @@ export class ProjectService extends DatabaseService<Model> {
}
}
return onDelete;
}

View File

@@ -1,7 +1,7 @@
import DatabaseConfig from "../DatabaseConfig";
import {
IsBillingEnabled,
NotificationWebhookOnCreateUser,
NotificationSlackWebhookOnCreateUser,
} from "../EnvironmentConfig";
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
import UpdateBy from "../Types/Database/UpdateBy";
@@ -42,9 +42,9 @@ export class Service extends DatabaseService<Model> {
_onCreate: OnCreate<Model>,
createdItem: Model,
): Promise<Model> {
if (NotificationWebhookOnCreateUser) {
if (NotificationSlackWebhookOnCreateUser) {
SlackUtil.sendMessageToChannel({
url: URL.fromString(NotificationWebhookOnCreateUser),
url: URL.fromString(NotificationSlackWebhookOnCreateUser),
text: `*New OneUptime User:*
*Email:* ${createdItem.email?.toString() || "N/A"}
*Name:* ${createdItem.name?.toString() || "N/A"}

View File

@@ -152,10 +152,17 @@ Usage:
- name: IS_SERVER
value: {{ printf "true" | squote }}
- name: NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER
value: {{ $.Values.notifications.webhooks.slack.onCreateUser }}
- name: NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT
value: {{ $.Values.notifications.webhooks.slack.onCreateProject }}
- name: NOTIFICATION_WEBHOOK_ON_CREATED_USER
value: {{ $.Values.notifications.webhooks.onCreateUser }}
- name: NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT
value: {{ $.Values.notifications.webhooks.slack.onDeleteProject }}
- name: NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE
value: {{ $.Values.notifications.webhooks.slack.onSubscriptionUpdate }}
- name: LETS_ENCRYPT_NOTIFICATION_EMAIL
value: {{ $.Values.letsEncrypt.email }}

View File

@@ -392,8 +392,12 @@ externalClickhouse:
# Notification webhooks when certain events happen in the system. (usually they are slack webhooks)
notifications:
webhooks:
# This is the webhook that will be called when a user is created or signs up.
onCreateUser:
slack:
# This is the webhook that will be called when a user is created or signs up.
onCreateUser:
onDeleteProject:
onCreateProject:
onSubscriptionUpdate:

View File

@@ -250,7 +250,13 @@ ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN=10
# Notifications Webhook (Slack)
# This webhook notifies slack when the new user signs up or is created.
NOTIFICATION_WEBHOOK_ON_CREATED_USER=
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER=
# This webhook notifies slack when the new project is created.
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT=
# This webhook notifies slack when the project is deleted.
NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT=
# This webhook notifies slack when the subscription is updated.
NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE=
# Copilot Environment Variables
COPILOT_ONEUPTIME_URL=http://localhost

View File

@@ -115,8 +115,10 @@ x-common-server-variables: &common-server-variables
DISABLE_AUTOMATIC_ALERT_CREATION: ${DISABLE_AUTOMATIC_ALERT_CREATION}
# Notification Webhooks
NOTIFICATION_WEBHOOK_ON_CREATED_USER: ${NOTIFICATION_WEBHOOK_ON_CREATED_USER}
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER}
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT}
NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT: ${NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT}
NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE: ${NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE}
services: