1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02:00
unleash.unleash/src/lib/addons/slack-app.ts
2024-10-14 09:20:27 +02:00

259 lines
8.6 KiB
TypeScript

import {
WebClient,
ErrorCode,
WebClientEvent,
type CodedError,
type WebAPIPlatformError,
type WebAPIRequestError,
type WebAPIRateLimitedError,
type WebAPIHTTPError,
type KnownBlock,
type Block,
} from '@slack/web-api';
import Addon from './addon';
import slackAppDefinition from './slack-app-definition';
import {
type IAddonConfig,
type IFlagResolver,
serializeDates,
} from '../types';
import {
type FeatureEventFormatter,
FeatureEventFormatterMd,
LinkStyle,
} from './feature-event-formatter-md';
import type { IEvent } from '../types/events';
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
interface ISlackAppAddonParameters {
accessToken: string;
defaultChannels: string;
}
export default class SlackAppAddon extends Addon {
private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver;
private accessToken?: string;
private slackClient?: WebClient;
constructor(args: IAddonConfig) {
super(slackAppDefinition, args);
this.msgFormatter = new FeatureEventFormatterMd({
unleashUrl: args.unleashUrl,
linkStyle: LinkStyle.SLACK,
});
this.flagResolver = args.flagResolver;
}
async handleEvent(
event: IEvent,
parameters: ISlackAppAddonParameters,
integrationId: number,
): Promise<void> {
let state: IntegrationEventState = 'success';
const stateDetails: string[] = [];
let channels: string[] = [];
let message = '';
try {
const { accessToken, defaultChannels } = parameters;
if (!accessToken) {
const noAccessTokenMessage = 'No access token provided.';
this.logger.warn(noAccessTokenMessage);
this.registerEarlyFailureEvent(
integrationId,
event,
noAccessTokenMessage,
);
return;
}
const taggedChannels = this.findTaggedChannels(event);
channels = this.getUniqueArray(
taggedChannels.concat(this.getDefaultChannels(defaultChannels)),
);
if (!channels.length) {
const noSlackChannelsMessage = `No Slack channels found for event ${event.type}.`;
this.logger.debug(noSlackChannelsMessage);
this.registerEarlyFailureEvent(
integrationId,
event,
noSlackChannelsMessage,
);
return;
}
this.logger.debug(
`Found candidate channels: ${JSON.stringify(channels)}.`,
);
if (!this.slackClient || this.accessToken !== accessToken) {
const client = new WebClient(accessToken);
client.on(WebClientEvent.RATE_LIMITED, (numSeconds) => {
this.logger.debug(
`Rate limit reached for event ${event.type}. Retry scheduled after ${numSeconds} seconds`,
);
});
this.slackClient = client;
this.accessToken = accessToken;
}
const { text: formattedMessage, url } =
this.msgFormatter.format(event);
const maxLength = 3000;
const text = formattedMessage.substring(0, maxLength);
message = `${formattedMessage}${text.length < formattedMessage.length ? ` (trimmed to ${maxLength} characters)` : ''}`;
const blocks: (Block | KnownBlock)[] = [
{
type: 'section',
text: {
type: 'mrkdwn',
text,
},
},
];
if (url) {
blocks.push({
type: 'actions',
elements: [
{
type: 'button',
url,
text: {
type: 'plain_text',
text: 'Open in Unleash',
},
value: 'featureToggle',
style: 'primary',
},
],
});
}
const requests = channels.map((name) => {
return this.slackClient!.chat.postMessage({
channel: name,
text,
blocks,
unfurl_links: false,
});
});
const results = await Promise.allSettled(requests);
const failedRequests = results.filter(
({ status }) => status === 'rejected',
);
const errors = this.getUniqueArray(
failedRequests.map(({ reason }: PromiseRejectedResult) =>
this.parseError(reason),
),
).join(' ');
if (failedRequests.length === 0) {
const successMessage = `All (${results.length}) Slack client calls were successful.`;
stateDetails.push(successMessage);
this.logger.info(successMessage);
} else if (failedRequests.length === results.length) {
state = 'failed';
const failedMessage = `All (${results.length}) Slack client calls failed with the following errors: ${errors}`;
stateDetails.push(failedMessage);
this.logger.warn(failedMessage);
} else {
state = 'successWithErrors';
const successWithErrorsMessage = `Some (${failedRequests.length} of ${results.length}) Slack client calls failed. Errors: ${errors}`;
stateDetails.push(successWithErrorsMessage);
this.logger.warn(successWithErrorsMessage);
}
} catch (error) {
state = 'failed';
const eventErrorMessage = `Error handling event ${event.type}.`;
stateDetails.push(eventErrorMessage);
this.logger.warn(eventErrorMessage);
const errorMessage = this.parseError(error);
stateDetails.push(errorMessage);
this.logger.warn(errorMessage, error);
} finally {
this.registerEvent({
integrationId,
state,
stateDetails: stateDetails.join('\n'),
event: serializeDates(event),
details: {
channels,
message,
},
});
}
}
getUniqueArray<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
registerEarlyFailureEvent(
integrationId: number,
event: IEvent,
earlyFailureMessage: string,
): void {
this.registerEvent({
integrationId,
state: 'failed',
stateDetails: earlyFailureMessage,
event: serializeDates(event),
details: {
channels: [],
message: '',
},
});
}
findTaggedChannels({ tags }: Pick<IEvent, 'tags'>): string[] {
if (tags) {
return tags
.filter((tag) => tag.type === 'slack')
.map((t) => t.value);
}
return [];
}
getDefaultChannels(defaultChannels?: string): string[] {
if (defaultChannels) {
return defaultChannels.split(',').map((c) => c.trim());
}
return [];
}
parseError(error: Error | CodedError): string {
if ('code' in error) {
if (error.code === ErrorCode.PlatformError) {
const { data } = error as WebAPIPlatformError;
return `A platform error occurred: ${JSON.stringify(data)}`;
}
if (error.code === ErrorCode.RequestError) {
const { original } = error as WebAPIRequestError;
return `A request error occurred: ${JSON.stringify(original)}`;
}
if (error.code === ErrorCode.RateLimitedError) {
const { retryAfter } = error as WebAPIRateLimitedError;
return `A rate limit error occurred: retry after ${retryAfter} seconds`;
}
if (error.code === ErrorCode.HTTPError) {
const { statusCode } = error as WebAPIHTTPError;
return `An HTTP error occurred: status code ${statusCode}`;
}
}
return error.message;
}
}
module.exports = SlackAppAddon;