From b8f4fba3036115a64ccc44a04cefd6a7373350f2 Mon Sep 17 00:00:00 2001 From: saadi Date: Tue, 22 Apr 2025 16:53:05 -0700 Subject: [PATCH 1/2] Fix alert creation bug related to the Slack integration --- .../v3/NewAlertChannelPresenter.server.ts | 77 +++++++++++++------ ...v.$envParam.alerts.new.connect-to-slack.ts | 28 +++---- .../route.tsx | 25 ++++++ .../services/createOrgIntegration.server.ts | 5 -- 4 files changed, 93 insertions(+), 42 deletions(-) diff --git a/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts b/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts index 5bf83bf493..676078c037 100644 --- a/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts @@ -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,58 @@ 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: "ACCESS_REVOKED" 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 +117,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" + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts index 99dc07f12a..6800ab2ed8 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts @@ -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, + }) + ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 526798cd78..4eda149085 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -356,6 +356,31 @@ export default function Page() { Connect to Slack + ) : slack.status === "ACCESS_REVOKED" ? ( +
+ + The Slack integration in your workspace has been revoked. Please re-connect + your Slack workspace. + + + + Connect to Slack + + +
+ ) : slack.status === "FAILED_FETCHING_CHANNELS" ? ( +
+ + Failed loading channels from Slack. Please try again later. + +
) : ( Slack integration is not available. Please contact your organization diff --git a/apps/webapp/app/v3/services/createOrgIntegration.server.ts b/apps/webapp/app/v3/services/createOrgIntegration.server.ts index 4db6581c9a..4f33a58fe8 100644 --- a/apps/webapp/app/v3/services/createOrgIntegration.server.ts +++ b/apps/webapp/app/v3/services/createOrgIntegration.server.ts @@ -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 { From 24d23c9284d0989d17891c0a31d541162305b5ec Mon Sep 17 00:00:00 2001 From: saadi Date: Tue, 22 Apr 2025 17:10:31 -0700 Subject: [PATCH 2/2] Handle token_expired error from slack gracefully --- .../presenters/v3/NewAlertChannelPresenter.server.ts | 10 +++++++++- .../route.tsx | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts b/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts index 676078c037..b7f665ff10 100644 --- a/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts @@ -37,7 +37,15 @@ export class NewAlertChannelPresenter extends BasePresenter { if (isSlackError(error) && error.data.error === "token_revoked") { return { slack: { - status: "ACCESS_REVOKED" as const, + status: "TOKEN_REVOKED" as const, + }, + }; + } + + if (isSlackError(error) && error.data.error === "token_expired") { + return { + slack: { + status: "TOKEN_EXPIRED" as const, }, }; } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 4eda149085..18ef2e522e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -356,11 +356,11 @@ export default function Page() { Connect to Slack - ) : slack.status === "ACCESS_REVOKED" ? ( + ) : slack.status === "TOKEN_REVOKED" || slack.status === "TOKEN_EXPIRED" ? (
- The Slack integration in your workspace has been revoked. Please re-connect - your Slack workspace. + The Slack integration in your workspace has been revoked or has expired. + Please re-connect your Slack workspace.