UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

181 lines • 7.53 kB
import { WebClient, ErrorCode, WebClientEvent, } from '@slack/web-api'; import Addon from './addon.js'; import slackAppDefinition from './slack-app-definition.js'; import { serializeDates, } from '../types/index.js'; import { FeatureEventFormatterMd, LinkStyle, } from './feature-event-formatter-md.js'; const defaultClientProvider = (accessToken) => { return new WebClient(accessToken); }; export default class SlackAppAddon extends Addon { constructor(args, clientProvider = defaultClientProvider) { super(slackAppDefinition, args); this.clientProvider = clientProvider; this.msgFormatter = new FeatureEventFormatterMd({ unleashUrl: args.unleashUrl, linkStyle: LinkStyle.SLACK, }); this.flagResolver = args.flagResolver; } async handleEvent(event, parameters, integrationId) { let state = 'success'; const stateDetails = []; let channels = []; 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 = this.clientProvider(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 = [ { 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 }) => 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(arr) { return [...new Set(arr)]; } registerEarlyFailureEvent(integrationId, event, earlyFailureMessage) { this.registerEvent({ integrationId, state: 'failed', stateDetails: earlyFailureMessage, event: serializeDates(event), details: { channels: [], message: '', }, }); } findTaggedChannels({ tags }) { if (tags) { return tags .filter((tag) => tag.type === 'slack') .map((t) => t.value); } return []; } getDefaultChannels(defaultChannels) { if (defaultChannels) { return defaultChannels.split(',').map((c) => c.trim()); } return []; } parseError(error) { if ('code' in error) { if (error.code === ErrorCode.PlatformError) { const { data } = error; return `A platform error occurred: ${JSON.stringify(data)}`; } if (error.code === ErrorCode.RequestError) { const { original } = error; return `A request error occurred: ${JSON.stringify(original)}`; } if (error.code === ErrorCode.RateLimitedError) { const { retryAfter } = error; return `A rate limit error occurred: retry after ${retryAfter} seconds`; } if (error.code === ErrorCode.HTTPError) { const { statusCode } = error; return `An HTTP error occurred: status code ${statusCode}`; } } return error.message; } } //# sourceMappingURL=slack-app.js.map