Fix alert creation bug related to the Slack integration (#1967)

* Fix alert creation bug related to the Slack integration

* Handle token_expired error from slack gracefully
This commit is contained in:
Saadi Myftija
2025-04-23 03:11:17 -07:00
committed by GitHub
parent 26aad58abd
commit 2dbc085bb3
4 changed files with 101 additions and 42 deletions
@@ -1,10 +1,11 @@
import {
AuthenticatableIntegration,
type AuthenticatableIntegration,
OrgIntegrationRepository,
} from "~/models/orgIntegration.server";
import { logger } from "~/services/logger.server";
import { BasePresenter } from "./basePresenter.server";
import { WebClient } from "@slack/web-api";
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) {
@@ -30,42 +31,66 @@ export class NewAlertChannelPresenter extends BasePresenter {
// If there is a slack integration, then we need to get a list of Slack Channels
if (slackIntegration) {
const channels = await getSlackChannelsForToken(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: channels ?? [],
integrationId: slackIntegration.id,
},
};
} else {
if (OrgIntegrationRepository.isSlackSupported) {
return {
slack: {
status: "NOT_CONFIGURED" as const,
},
};
} else {
return {
slack: {
status: "NOT_AVAILABLE" as const,
},
};
}
}
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);
logger.debug("Received a list of slack conversations", {
channels,
});
return (channels ?? [])
.filter((channel) => !channel.is_archived)
.filter((channel) => channel.is_channel)
@@ -100,3 +125,15 @@ async function getAllSlackConversations(client: WebClient) {
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"
);
}
@@ -1,13 +1,11 @@
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { OrgIntegrationRepository } from "~/models/orgIntegration.server";
import { findProjectBySlug } from "~/models/project.server";
import { requireUserId } from "~/services/session.server";
import {
EnvironmentParamSchema,
ProjectParamSchema,
v3NewProjectAlertPath,
v3NewProjectAlertPathConnectToSlackPath,
} from "~/utils/pathBuilder";
@@ -16,6 +14,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
const url = new URL(request.url);
const shouldReinstall = url.searchParams.get("reinstall") === "true";
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
if (!project) {
@@ -30,7 +31,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
},
});
if (integration) {
// If integration exists and we're not reinstalling, redirect back to alerts
if (integration && !shouldReinstall) {
return redirectWithSuccessMessage(
`${v3NewProjectAlertPath({ slug: organizationSlug }, project, {
slug: envParam,
@@ -38,15 +40,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
request,
"Successfully connected your Slack workspace"
);
} else {
// Redirect to Slack
return await OrgIntegrationRepository.redirectToAuthService(
"SLACK",
project.organizationId,
request,
v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project, {
slug: envParam,
})
);
}
// Redirect to Slack for new installation or reinstallation
return await OrgIntegrationRepository.redirectToAuthService(
"SLACK",
project.organizationId,
request,
v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project, {
slug: envParam,
})
);
}
@@ -356,6 +356,31 @@ export default function Page() {
<SlackIcon className="size-5" /> Connect to Slack
</span>
</LinkButton>
) : slack.status === "TOKEN_REVOKED" || slack.status === "TOKEN_EXPIRED" ? (
<div className="flex flex-col gap-4">
<Callout variant="info">
The Slack integration in your workspace has been revoked or has expired.
Please re-connect your Slack workspace.
</Callout>
<LinkButton
variant="tertiary/large"
to={{
pathname: "connect-to-slack",
search: "?reinstall=true",
}}
fullWidth
>
<span className="flex items-center gap-2 text-text-bright">
<SlackIcon className="size-5" /> Connect to Slack
</span>
</LinkButton>
</div>
) : slack.status === "FAILED_FETCHING_CHANNELS" ? (
<div className="flex flex-col gap-4">
<Callout variant="warning">
Failed loading channels from Slack. Please try again later.
</Callout>
</div>
) : (
<Callout variant="warning">
Slack integration is not available. Please contact your organization
@@ -1,10 +1,5 @@
import { OrganizationIntegration } from "@trigger.dev/database";
import { BaseService } from "./baseService.server";
import { WebClient } from "@slack/web-api";
import { env } from "~/env.server";
import { $transaction } from "~/db.server";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { OrgIntegrationRepository } from "~/models/orgIntegration.server";
export class CreateOrgIntegrationService extends BaseService {