Skip to content

Commit 2dbc085

Browse files
authored
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
1 parent 26aad58 commit 2dbc085

File tree

4 files changed

+101
-42
lines changed

4 files changed

+101
-42
lines changed

apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts

+61-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {
2-
AuthenticatableIntegration,
2+
type AuthenticatableIntegration,
33
OrgIntegrationRepository,
44
} from "~/models/orgIntegration.server";
5-
import { logger } from "~/services/logger.server";
65
import { BasePresenter } from "./basePresenter.server";
7-
import { WebClient } from "@slack/web-api";
6+
import { type WebClient } from "@slack/web-api";
7+
import { tryCatch } from "@trigger.dev/core";
8+
import { logger } from "~/services/logger.server";
89

910
export class NewAlertChannelPresenter extends BasePresenter {
1011
public async call(projectId: string) {
@@ -30,42 +31,66 @@ export class NewAlertChannelPresenter extends BasePresenter {
3031

3132
// If there is a slack integration, then we need to get a list of Slack Channels
3233
if (slackIntegration) {
33-
const channels = await getSlackChannelsForToken(slackIntegration);
34+
const [error, channels] = await tryCatch(getSlackChannelsForToken(slackIntegration));
35+
36+
if (error) {
37+
if (isSlackError(error) && error.data.error === "token_revoked") {
38+
return {
39+
slack: {
40+
status: "TOKEN_REVOKED" as const,
41+
},
42+
};
43+
}
44+
45+
if (isSlackError(error) && error.data.error === "token_expired") {
46+
return {
47+
slack: {
48+
status: "TOKEN_EXPIRED" as const,
49+
},
50+
};
51+
}
52+
53+
logger.error("Failed fetching Slack channels for creating alerts", {
54+
error,
55+
slackIntegrationId: slackIntegration.id,
56+
});
57+
58+
return {
59+
slack: {
60+
status: "FAILED_FETCHING_CHANNELS" as const,
61+
},
62+
};
63+
}
3464

3565
return {
3666
slack: {
3767
status: "READY" as const,
38-
channels,
68+
channels: channels ?? [],
3969
integrationId: slackIntegration.id,
4070
},
4171
};
42-
} else {
43-
if (OrgIntegrationRepository.isSlackSupported) {
44-
return {
45-
slack: {
46-
status: "NOT_CONFIGURED" as const,
47-
},
48-
};
49-
} else {
50-
return {
51-
slack: {
52-
status: "NOT_AVAILABLE" as const,
53-
},
54-
};
55-
}
5672
}
73+
74+
if (OrgIntegrationRepository.isSlackSupported) {
75+
return {
76+
slack: {
77+
status: "NOT_CONFIGURED" as const,
78+
},
79+
};
80+
}
81+
82+
return {
83+
slack: {
84+
status: "NOT_AVAILABLE" as const,
85+
},
86+
};
5787
}
5888
}
5989

6090
async function getSlackChannelsForToken(integration: AuthenticatableIntegration) {
6191
const client = await OrgIntegrationRepository.getAuthenticatedClientForIntegration(integration);
62-
6392
const channels = await getAllSlackConversations(client);
6493

65-
logger.debug("Received a list of slack conversations", {
66-
channels,
67-
});
68-
6994
return (channels ?? [])
7095
.filter((channel) => !channel.is_archived)
7196
.filter((channel) => channel.is_channel)
@@ -100,3 +125,15 @@ async function getAllSlackConversations(client: WebClient) {
100125

101126
return channels;
102127
}
128+
129+
function isSlackError(obj: unknown): obj is { data: { error: string } } {
130+
return Boolean(
131+
typeof obj === "object" &&
132+
obj !== null &&
133+
"data" in obj &&
134+
typeof obj.data === "object" &&
135+
obj.data !== null &&
136+
"error" in obj.data &&
137+
typeof obj.data.error === "string"
138+
);
139+
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { prisma } from "~/db.server";
3-
import { env } from "~/env.server";
43
import { redirectWithSuccessMessage } from "~/models/message.server";
54
import { OrgIntegrationRepository } from "~/models/orgIntegration.server";
65
import { findProjectBySlug } from "~/models/project.server";
76
import { requireUserId } from "~/services/session.server";
87
import {
98
EnvironmentParamSchema,
10-
ProjectParamSchema,
119
v3NewProjectAlertPath,
1210
v3NewProjectAlertPathConnectToSlackPath,
1311
} from "~/utils/pathBuilder";
@@ -16,6 +14,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
1614
const userId = await requireUserId(request);
1715
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
1816

17+
const url = new URL(request.url);
18+
const shouldReinstall = url.searchParams.get("reinstall") === "true";
19+
1920
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
2021

2122
if (!project) {
@@ -30,23 +31,24 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
3031
},
3132
});
3233

33-
if (integration) {
34+
// If integration exists and we're not reinstalling, redirect back to alerts
35+
if (integration && !shouldReinstall) {
3436
return redirectWithSuccessMessage(
3537
`${v3NewProjectAlertPath({ slug: organizationSlug }, project, {
3638
slug: envParam,
3739
})}?option=slack`,
3840
request,
3941
"Successfully connected your Slack workspace"
4042
);
41-
} else {
42-
// Redirect to Slack
43-
return await OrgIntegrationRepository.redirectToAuthService(
44-
"SLACK",
45-
project.organizationId,
46-
request,
47-
v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project, {
48-
slug: envParam,
49-
})
50-
);
5143
}
44+
45+
// Redirect to Slack for new installation or reinstallation
46+
return await OrgIntegrationRepository.redirectToAuthService(
47+
"SLACK",
48+
project.organizationId,
49+
request,
50+
v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project, {
51+
slug: envParam,
52+
})
53+
);
5254
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,31 @@ export default function Page() {
356356
<SlackIcon className="size-5" /> Connect to Slack
357357
</span>
358358
</LinkButton>
359+
) : slack.status === "TOKEN_REVOKED" || slack.status === "TOKEN_EXPIRED" ? (
360+
<div className="flex flex-col gap-4">
361+
<Callout variant="info">
362+
The Slack integration in your workspace has been revoked or has expired.
363+
Please re-connect your Slack workspace.
364+
</Callout>
365+
<LinkButton
366+
variant="tertiary/large"
367+
to={{
368+
pathname: "connect-to-slack",
369+
search: "?reinstall=true",
370+
}}
371+
fullWidth
372+
>
373+
<span className="flex items-center gap-2 text-text-bright">
374+
<SlackIcon className="size-5" /> Connect to Slack
375+
</span>
376+
</LinkButton>
377+
</div>
378+
) : slack.status === "FAILED_FETCHING_CHANNELS" ? (
379+
<div className="flex flex-col gap-4">
380+
<Callout variant="warning">
381+
Failed loading channels from Slack. Please try again later.
382+
</Callout>
383+
</div>
359384
) : (
360385
<Callout variant="warning">
361386
Slack integration is not available. Please contact your organization

apps/webapp/app/v3/services/createOrgIntegration.server.ts

-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { OrganizationIntegration } from "@trigger.dev/database";
22
import { BaseService } from "./baseService.server";
3-
import { WebClient } from "@slack/web-api";
4-
import { env } from "~/env.server";
5-
import { $transaction } from "~/db.server";
6-
import { getSecretStore } from "~/services/secrets/secretStore.server";
7-
import { generateFriendlyId } from "../friendlyIdentifiers";
83
import { OrgIntegrationRepository } from "~/models/orgIntegration.server";
94

105
export class CreateOrgIntegrationService extends BaseService {

0 commit comments

Comments
 (0)