160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
import {
|
|
type AuthenticatableIntegration,
|
|
OrgIntegrationRepository,
|
|
} from "~/models/orgIntegration.server";
|
|
import { BasePresenter } from "./basePresenter.server";
|
|
import { type WebClient } from "@slack/web-api";
|
|
import { tryCatch } from "@trigger.dev/core";
|
|
import { logger } from "~/services/logger.server";
|
|
|
|
export class NewAlertChannelPresenter extends BasePresenter {
|
|
public async call(projectId: string) {
|
|
const project = await this._prisma.project.findFirstOrThrow({
|
|
where: {
|
|
id: projectId,
|
|
},
|
|
});
|
|
|
|
// Find the latest Slack integration
|
|
const slackIntegration = await this._prisma.organizationIntegration.findFirst({
|
|
where: {
|
|
service: "SLACK",
|
|
organizationId: project.organizationId,
|
|
deletedAt: null,
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
include: {
|
|
tokenReference: true,
|
|
},
|
|
});
|
|
|
|
// If there is a slack integration, then we need to get a list of Slack Channels
|
|
if (slackIntegration) {
|
|
const [error, channels] = await tryCatch(getSlackChannelsForToken(slackIntegration));
|
|
|
|
if (error) {
|
|
if (isSlackError(error) && error.data.error === "token_revoked") {
|
|
return {
|
|
slack: {
|
|
status: "TOKEN_REVOKED" as const,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (isSlackError(error) && error.data.error === "token_expired") {
|
|
return {
|
|
slack: {
|
|
status: "TOKEN_EXPIRED" as const,
|
|
},
|
|
};
|
|
}
|
|
|
|
logger.error("Failed fetching Slack channels for creating alerts", {
|
|
error,
|
|
slackIntegrationId: slackIntegration.id,
|
|
});
|
|
|
|
return {
|
|
slack: {
|
|
status: "FAILED_FETCHING_CHANNELS" as const,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
slack: {
|
|
status: "READY" as const,
|
|
channels: channels ?? [],
|
|
integrationId: slackIntegration.id,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (OrgIntegrationRepository.isSlackSupported) {
|
|
return {
|
|
slack: {
|
|
status: "NOT_CONFIGURED" as const,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
slack: {
|
|
status: "NOT_AVAILABLE" as const,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
async function getSlackChannelsForToken(integration: AuthenticatableIntegration) {
|
|
const client = await OrgIntegrationRepository.getAuthenticatedClientForIntegration(integration);
|
|
const channels = await getAllSlackConversations(client);
|
|
|
|
return (channels ?? [])
|
|
.filter((channel) => !channel.is_archived)
|
|
.filter((channel) => channel.is_channel)
|
|
.filter((channel) => channel.num_members)
|
|
.sort((a, b) => a.name!.localeCompare(b.name!));
|
|
}
|
|
|
|
type Channels = Awaited<ReturnType<WebClient["conversations"]["list"]>>["channels"];
|
|
|
|
async function getAllSlackConversations(client: WebClient) {
|
|
let nextCursor: string | undefined = undefined;
|
|
let channels: Channels = [];
|
|
|
|
try {
|
|
do {
|
|
// The `tryCatch` util runs into a type error due to self referencing.
|
|
// So we fall back to a good old try/catch block here.
|
|
const response = await client.conversations.list({
|
|
types: "public_channel,private_channel",
|
|
exclude_archived: true,
|
|
cursor: nextCursor,
|
|
limit: 999,
|
|
});
|
|
|
|
channels = channels.concat(response.channels ?? []);
|
|
nextCursor = response.response_metadata?.next_cursor;
|
|
} while (nextCursor);
|
|
} catch (error) {
|
|
if (error && isSlackError(error) && error.data.error === "ratelimited") {
|
|
logger.warn("Rate limiting issue occurred while fetching Slack channels", {
|
|
error,
|
|
});
|
|
|
|
// This is a workaround to the current way we handle Slack channels for creating alerts.
|
|
// For workspaces with a lot of channels (>10000) we might hit the rate limit for this slack endpoint,
|
|
// as multiple requests are needed to fetch all channels.
|
|
// We use the largest allowed page size of 999 to reduce the chance of hitting the rate limit.
|
|
|
|
// This is mainly due to workspaces with a large number of archived channels,
|
|
// which this slack endpoint unfortunately filters out only after fetching the page of channels without applying any filters first.
|
|
// https://api.slack.com/methods/conversations.list#markdown
|
|
|
|
// We expect most workspaces not to run into this issue, but if they do, we at least return some channels.
|
|
// In the future, we might revisit the way we handle Slack channels and cache them on our side to support
|
|
// proper searching. Until then, we track occurrences of this issue using a metric.
|
|
return channels;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
return channels;
|
|
}
|
|
|
|
function isSlackError(obj: unknown): obj is { data: { error: string } } {
|
|
return Boolean(
|
|
typeof obj === "object" &&
|
|
obj !== null &&
|
|
"data" in obj &&
|
|
typeof obj.data === "object" &&
|
|
obj.data !== null &&
|
|
"error" in obj.data &&
|
|
typeof obj.data.error === "string"
|
|
);
|
|
}
|