mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-06 00:07:44 +01:00
521cc24a22
https://linear.app/unleash/issue/2-1253/add-support-for-more-events-in-the-slack-app-integration Adds support for a lot more events in our integrations. Here is how the full list looks like: - ADDON_CONFIG_CREATED - ADDON_CONFIG_DELETED - ADDON_CONFIG_UPDATED - API_TOKEN_CREATED - API_TOKEN_DELETED - CHANGE_ADDED - CHANGE_DISCARDED - CHANGE_EDITED - CHANGE_REQUEST_APPLIED - CHANGE_REQUEST_APPROVAL_ADDED - CHANGE_REQUEST_APPROVED - CHANGE_REQUEST_CANCELLED - CHANGE_REQUEST_CREATED - CHANGE_REQUEST_DISCARDED - CHANGE_REQUEST_REJECTED - CHANGE_REQUEST_SENT_TO_REVIEW - CONTEXT_FIELD_CREATED - CONTEXT_FIELD_DELETED - CONTEXT_FIELD_UPDATED - FEATURE_ARCHIVED - FEATURE_CREATED - FEATURE_DELETED - FEATURE_ENVIRONMENT_DISABLED - FEATURE_ENVIRONMENT_ENABLED - FEATURE_ENVIRONMENT_VARIANTS_UPDATED - FEATURE_METADATA_UPDATED - FEATURE_POTENTIALLY_STALE_ON - FEATURE_PROJECT_CHANGE - FEATURE_REVIVED - FEATURE_STALE_OFF - FEATURE_STALE_ON - FEATURE_STRATEGY_ADD - FEATURE_STRATEGY_REMOVE - FEATURE_STRATEGY_UPDATE - FEATURE_TAGGED - FEATURE_UNTAGGED - GROUP_CREATED - GROUP_DELETED - GROUP_UPDATED - PROJECT_CREATED - PROJECT_DELETED - SEGMENT_CREATED - SEGMENT_DELETED - SEGMENT_UPDATED - SERVICE_ACCOUNT_CREATED - SERVICE_ACCOUNT_DELETED - SERVICE_ACCOUNT_UPDATED - USER_CREATED - USER_DELETED - USER_UPDATED I added the events that I thought were relevant based on my own discretion. Know of any event we should add? Let me know and I'll add it 🙂 For now I only added these events to the new Slack App integration, but we can add them to the other integrations as well since they are now supported. The event formatter was refactored and changed quite a bit in order to make it easier to maintain and add new events in the future. As a result, events are now posted with different text. Do we consider this a breaking change? If so, I can keep the old event formatter around, create a new one and only use it for the new Slack App integration. I noticed we don't have good 404 behaviors in the UI for things that are deleted in the meantime, that's why I avoided some links to specific resources (like feature strategies, integration configurations, etc), but we could add them later if we improve this. This PR also tries to add some consistency to the the way we log events.
193 lines
6.0 KiB
TypeScript
193 lines
6.0 KiB
TypeScript
import {
|
|
WebClient,
|
|
ErrorCode,
|
|
WebClientEvent,
|
|
CodedError,
|
|
WebAPIPlatformError,
|
|
WebAPIRequestError,
|
|
WebAPIRateLimitedError,
|
|
WebAPIHTTPError,
|
|
KnownBlock,
|
|
Block,
|
|
} from '@slack/web-api';
|
|
import Addon from './addon';
|
|
|
|
import slackAppDefinition from './slack-app-definition';
|
|
import { IAddonConfig } from '../types/model';
|
|
import {
|
|
FeatureEventFormatter,
|
|
FeatureEventFormatterMd,
|
|
LinkStyle,
|
|
} from './feature-event-formatter-md';
|
|
import { IEvent } from '../types/events';
|
|
|
|
interface ISlackAppAddonParameters {
|
|
accessToken: string;
|
|
defaultChannels: string;
|
|
}
|
|
|
|
export default class SlackAppAddon extends Addon {
|
|
private msgFormatter: FeatureEventFormatter;
|
|
|
|
private accessToken?: string;
|
|
|
|
private slackClient?: WebClient;
|
|
|
|
constructor(args: IAddonConfig) {
|
|
super(slackAppDefinition, args);
|
|
this.msgFormatter = new FeatureEventFormatterMd(
|
|
args.unleashUrl,
|
|
LinkStyle.SLACK,
|
|
);
|
|
}
|
|
|
|
async handleEvent(
|
|
event: IEvent,
|
|
parameters: ISlackAppAddonParameters,
|
|
): Promise<void> {
|
|
try {
|
|
const { accessToken, defaultChannels } = parameters;
|
|
if (!accessToken) {
|
|
this.logger.warn('No access token provided.');
|
|
return;
|
|
}
|
|
|
|
const taggedChannels = this.findTaggedChannels(event);
|
|
const eventChannels = [
|
|
...new Set(
|
|
taggedChannels.concat(
|
|
this.getDefaultChannels(defaultChannels),
|
|
),
|
|
),
|
|
];
|
|
|
|
if (!eventChannels.length) {
|
|
this.logger.debug(
|
|
`No Slack channels found for event ${event.type}.`,
|
|
);
|
|
return;
|
|
}
|
|
this.logger.debug(`Found candidate channels: ${eventChannels}.`);
|
|
|
|
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, url } = this.msgFormatter.format(event);
|
|
|
|
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 = eventChannels.map((name) => {
|
|
return this.slackClient!.chat.postMessage({
|
|
channel: name,
|
|
text,
|
|
blocks,
|
|
});
|
|
});
|
|
|
|
const results = await Promise.allSettled(requests);
|
|
|
|
results
|
|
.filter(({ status }) => status === 'rejected')
|
|
.map(({ reason }: PromiseRejectedResult) =>
|
|
this.logError(event, reason),
|
|
);
|
|
|
|
this.logger.info(
|
|
`Handled event ${event.type} dispatching ${
|
|
results.filter(({ status }) => status === 'fulfilled')
|
|
.length
|
|
} out of ${requests.length} messages successfully.`,
|
|
);
|
|
} catch (error) {
|
|
this.logError(event, error);
|
|
}
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
|
|
logError(event: IEvent, error: Error | CodedError): void {
|
|
if (!('code' in error)) {
|
|
this.logger.warn(`Error handling event ${event.type}.`, error);
|
|
return;
|
|
}
|
|
|
|
if (error.code === ErrorCode.PlatformError) {
|
|
const { data } = error as WebAPIPlatformError;
|
|
this.logger.warn(
|
|
`Error handling event ${event.type}. A platform error occurred: ${data}`,
|
|
error,
|
|
);
|
|
} else if (error.code === ErrorCode.RequestError) {
|
|
const { original } = error as WebAPIRequestError;
|
|
this.logger.warn(
|
|
`Error handling event ${event.type}. A request error occurred: ${original}`,
|
|
error,
|
|
);
|
|
} else if (error.code === ErrorCode.RateLimitedError) {
|
|
const { retryAfter } = error as WebAPIRateLimitedError;
|
|
this.logger.warn(
|
|
`Error handling event ${event.type}. A rate limit error occurred: retry after ${retryAfter} seconds`,
|
|
error,
|
|
);
|
|
} else if (error.code === ErrorCode.HTTPError) {
|
|
const { statusCode } = error as WebAPIHTTPError;
|
|
this.logger.warn(
|
|
`Error handling event ${event.type}. An HTTP error occurred: status code ${statusCode}`,
|
|
error,
|
|
);
|
|
} else {
|
|
this.logger.warn(`Error handling event ${event.type}.`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = SlackAppAddon;
|