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:
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
+15
-13
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
+25
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user