mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-23 00:16:25 +01:00
chore: register integration events in Slack App integration (#7631)
https://linear.app/unleash/issue/2-2459/register-integration-events-slack-app Registers integration events in the **Slack App** integration. Similar to: - #7626 - #7621
This commit is contained in:
parent
44192934f8
commit
1033276e97
@ -4,9 +4,11 @@ import { type ChatPostMessageArguments, ErrorCode } from '@slack/web-api';
|
||||
import {
|
||||
type IAddonConfig,
|
||||
type IFlagResolver,
|
||||
serializeDates,
|
||||
SYSTEM_USER_ID,
|
||||
} from '../types';
|
||||
import type { IntegrationEventsService } from '../services';
|
||||
import type { Logger } from '../logger';
|
||||
|
||||
const slackApiCalls: ChatPostMessageArguments[] = [];
|
||||
|
||||
@ -19,6 +21,8 @@ const loggerMock = {
|
||||
};
|
||||
const getLogger = jest.fn(() => loggerMock);
|
||||
|
||||
const registerEventMock = jest.fn();
|
||||
|
||||
const INTEGRATION_ID = 1337;
|
||||
const ARGS: IAddonConfig = {
|
||||
getLogger,
|
||||
@ -47,6 +51,22 @@ jest.mock('@slack/web-api', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'./addon',
|
||||
() =>
|
||||
class Addon {
|
||||
logger: Logger;
|
||||
|
||||
constructor(_, { getLogger }) {
|
||||
this.logger = getLogger('addon/test');
|
||||
}
|
||||
|
||||
async registerEvent(event) {
|
||||
return registerEventMock(event);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe('SlackAppAddon', () => {
|
||||
let addon: SlackAppAddon;
|
||||
const accessToken = 'test-access-token';
|
||||
@ -78,6 +98,7 @@ describe('SlackAppAddon', () => {
|
||||
jest.useFakeTimers();
|
||||
slackApiCalls.length = 0;
|
||||
postMessage.mockClear();
|
||||
registerEventMock.mockClear();
|
||||
addon = new SlackAppAddon(ARGS);
|
||||
});
|
||||
|
||||
@ -187,8 +208,7 @@ describe('SlackAppAddon', () => {
|
||||
);
|
||||
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||
`Error handling event ${event.type}. A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
||||
expect.any(Object),
|
||||
`All (1) Slack client calls failed with the following errors: A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
||||
);
|
||||
});
|
||||
|
||||
@ -219,11 +239,39 @@ describe('SlackAppAddon', () => {
|
||||
|
||||
expect(postMessage).toHaveBeenCalledTimes(3);
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||
`Error handling event ${FEATURE_ENVIRONMENT_ENABLED}. A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(loggerMock.info).toHaveBeenCalledWith(
|
||||
`Handled event ${FEATURE_ENVIRONMENT_ENABLED} dispatching 2 out of 3 messages successfully.`,
|
||||
`Some (1 of 3) Slack client calls failed. Errors: A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('Should call registerEvent', async () => {
|
||||
const eventWith2Tags: IEvent = {
|
||||
...event,
|
||||
tags: [
|
||||
{ type: 'slack', value: 'general' },
|
||||
{ type: 'slack', value: 'another-channel-1' },
|
||||
],
|
||||
};
|
||||
|
||||
await addon.handleEvent(
|
||||
eventWith2Tags,
|
||||
{
|
||||
accessToken,
|
||||
defaultChannels: 'another-channel-1, another-channel-2',
|
||||
},
|
||||
INTEGRATION_ID,
|
||||
);
|
||||
|
||||
expect(registerEventMock).toHaveBeenCalledTimes(1);
|
||||
expect(registerEventMock).toHaveBeenCalledWith({
|
||||
integrationId: INTEGRATION_ID,
|
||||
state: 'success',
|
||||
stateDetails: 'All (3) Slack client calls were successful.',
|
||||
event: serializeDates(eventWith2Tags),
|
||||
details: {
|
||||
channels: ['general', 'another-channel-1', 'another-channel-2'],
|
||||
message:
|
||||
'*some@user.com* enabled *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|default>*',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -13,13 +13,14 @@ import {
|
||||
import Addon from './addon';
|
||||
|
||||
import slackAppDefinition from './slack-app-definition';
|
||||
import type { IAddonConfig } from '../types/model';
|
||||
import { type IAddonConfig, 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;
|
||||
@ -46,30 +47,42 @@ export default class SlackAppAddon extends Addon {
|
||||
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) {
|
||||
this.logger.warn('No access token provided.');
|
||||
const noAccessTokenMessage = 'No access token provided.';
|
||||
this.logger.warn(noAccessTokenMessage);
|
||||
this.registerEarlyFailureEvent(
|
||||
integrationId,
|
||||
event,
|
||||
noAccessTokenMessage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const taggedChannels = this.findTaggedChannels(event);
|
||||
const eventChannels = [
|
||||
...new Set(
|
||||
taggedChannels.concat(
|
||||
this.getDefaultChannels(defaultChannels),
|
||||
),
|
||||
),
|
||||
];
|
||||
channels = this.getUniqueArray(
|
||||
taggedChannels.concat(this.getDefaultChannels(defaultChannels)),
|
||||
);
|
||||
|
||||
if (!eventChannels.length) {
|
||||
this.logger.debug(
|
||||
`No Slack channels found for event ${event.type}.`,
|
||||
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(eventChannels)}.`,
|
||||
`Found candidate channels: ${JSON.stringify(channels)}.`,
|
||||
);
|
||||
|
||||
if (!this.slackClient || this.accessToken !== accessToken) {
|
||||
@ -84,6 +97,7 @@ export default class SlackAppAddon extends Addon {
|
||||
}
|
||||
|
||||
const { text, url } = this.msgFormatter.format(event);
|
||||
message = text;
|
||||
|
||||
const blocks: (Block | KnownBlock)[] = [
|
||||
{
|
||||
@ -113,7 +127,7 @@ export default class SlackAppAddon extends Addon {
|
||||
});
|
||||
}
|
||||
|
||||
const requests = eventChannels.map((name) => {
|
||||
const requests = channels.map((name) => {
|
||||
return this.slackClient!.chat.postMessage({
|
||||
channel: name,
|
||||
text,
|
||||
@ -123,23 +137,73 @@ export default class SlackAppAddon extends Addon {
|
||||
|
||||
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.`,
|
||||
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) {
|
||||
this.logError(event, 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
|
||||
@ -156,39 +220,27 @@ export default class SlackAppAddon extends Addon {
|
||||
return [];
|
||||
}
|
||||
|
||||
logError(event: IEvent, error: Error | CodedError): void {
|
||||
if (!('code' in error)) {
|
||||
this.logger.warn(`Error handling event ${event.type}.`, error);
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === ErrorCode.PlatformError) {
|
||||
const { data } = error as WebAPIPlatformError;
|
||||
this.logger.warn(
|
||||
`Error handling event ${event.type}. A platform error occurred: ${JSON.stringify(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: ${JSON.stringify(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);
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,7 +336,8 @@ describe('Slack integration', () => {
|
||||
url: parameters.url,
|
||||
channels: ['general'],
|
||||
username: 'Unleash',
|
||||
text: '*some@user.com* disabled *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|default>*',
|
||||
message:
|
||||
'*some@user.com* disabled *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|default>*',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -57,11 +57,11 @@ export default class SlackAddon extends Addon {
|
||||
try {
|
||||
extraHeaders = JSON.parse(customHeaders);
|
||||
} catch (e) {
|
||||
const detailMessage =
|
||||
'Could not parse the JSON in the customHeaders parameter.';
|
||||
state = 'successWithErrors';
|
||||
stateDetails.push(detailMessage);
|
||||
this.logger.warn(detailMessage);
|
||||
const badHeadersMessage =
|
||||
'Could not parse the JSON in the customHeaders parameter.';
|
||||
stateDetails.push(badHeadersMessage);
|
||||
this.logger.warn(badHeadersMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,19 +109,19 @@ export default class SlackAddon extends Addon {
|
||||
this.logger.info(`Handled event ${event.type}.`);
|
||||
|
||||
if (failedRequests.length === 0) {
|
||||
const detailMessage = `All (${results.length}) Slack webhook requests were successful with status codes: ${codes}.`;
|
||||
stateDetails.push(detailMessage);
|
||||
this.logger.info(detailMessage);
|
||||
const successMessage = `All (${results.length}) Slack webhook requests were successful with status codes: ${codes}.`;
|
||||
stateDetails.push(successMessage);
|
||||
this.logger.info(successMessage);
|
||||
} else if (failedRequests.length === results.length) {
|
||||
const detailMessage = `All (${results.length}) Slack webhook requests failed with status codes: ${codes}.`;
|
||||
state = 'failed';
|
||||
stateDetails.push(detailMessage);
|
||||
this.logger.warn(detailMessage);
|
||||
const failedMessage = `All (${results.length}) Slack webhook requests failed with status codes: ${codes}.`;
|
||||
stateDetails.push(failedMessage);
|
||||
this.logger.warn(failedMessage);
|
||||
} else {
|
||||
const detailMessage = `Some (${failedRequests.length} of ${results.length}) Slack webhook requests failed. Status codes: ${codes}.`;
|
||||
state = 'successWithErrors';
|
||||
stateDetails.push(detailMessage);
|
||||
this.logger.warn(detailMessage);
|
||||
const successWithErrorsMessage = `Some (${failedRequests.length} of ${results.length}) Slack webhook requests failed. Status codes: ${codes}.`;
|
||||
stateDetails.push(successWithErrorsMessage);
|
||||
this.logger.warn(successWithErrorsMessage);
|
||||
}
|
||||
|
||||
this.registerEvent({
|
||||
@ -133,7 +133,7 @@ export default class SlackAddon extends Addon {
|
||||
url,
|
||||
channels: slackChannels,
|
||||
username,
|
||||
text,
|
||||
message: text,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user