1
0
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:
Nuno Góis 2024-07-22 11:54:19 +01:00 committed by GitHub
parent 44192934f8
commit 1033276e97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 180 additions and 79 deletions

View File

@ -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>*',
},
});
});
});

View File

@ -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;
}
}

View File

@ -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>*',
},
});
});

View File

@ -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,
},
});
}