mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-27 00:19:39 +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 {
|
import {
|
||||||
type IAddonConfig,
|
type IAddonConfig,
|
||||||
type IFlagResolver,
|
type IFlagResolver,
|
||||||
|
serializeDates,
|
||||||
SYSTEM_USER_ID,
|
SYSTEM_USER_ID,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import type { IntegrationEventsService } from '../services';
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
import type { Logger } from '../logger';
|
||||||
|
|
||||||
const slackApiCalls: ChatPostMessageArguments[] = [];
|
const slackApiCalls: ChatPostMessageArguments[] = [];
|
||||||
|
|
||||||
@ -19,6 +21,8 @@ const loggerMock = {
|
|||||||
};
|
};
|
||||||
const getLogger = jest.fn(() => loggerMock);
|
const getLogger = jest.fn(() => loggerMock);
|
||||||
|
|
||||||
|
const registerEventMock = jest.fn();
|
||||||
|
|
||||||
const INTEGRATION_ID = 1337;
|
const INTEGRATION_ID = 1337;
|
||||||
const ARGS: IAddonConfig = {
|
const ARGS: IAddonConfig = {
|
||||||
getLogger,
|
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', () => {
|
describe('SlackAppAddon', () => {
|
||||||
let addon: SlackAppAddon;
|
let addon: SlackAppAddon;
|
||||||
const accessToken = 'test-access-token';
|
const accessToken = 'test-access-token';
|
||||||
@ -78,6 +98,7 @@ describe('SlackAppAddon', () => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
slackApiCalls.length = 0;
|
slackApiCalls.length = 0;
|
||||||
postMessage.mockClear();
|
postMessage.mockClear();
|
||||||
|
registerEventMock.mockClear();
|
||||||
addon = new SlackAppAddon(ARGS);
|
addon = new SlackAppAddon(ARGS);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -187,8 +208,7 @@ describe('SlackAppAddon', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||||
`Error handling event ${event.type}. A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
`All (1) Slack client calls failed with the following errors: A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
||||||
expect.any(Object),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -219,11 +239,39 @@ describe('SlackAppAddon', () => {
|
|||||||
|
|
||||||
expect(postMessage).toHaveBeenCalledTimes(3);
|
expect(postMessage).toHaveBeenCalledTimes(3);
|
||||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||||
`Error handling event ${FEATURE_ENVIRONMENT_ENABLED}. A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
`Some (1 of 3) Slack client calls failed. Errors: 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.`,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 Addon from './addon';
|
||||||
|
|
||||||
import slackAppDefinition from './slack-app-definition';
|
import slackAppDefinition from './slack-app-definition';
|
||||||
import type { IAddonConfig } from '../types/model';
|
import { type IAddonConfig, serializeDates } from '../types';
|
||||||
import {
|
import {
|
||||||
type FeatureEventFormatter,
|
type FeatureEventFormatter,
|
||||||
FeatureEventFormatterMd,
|
FeatureEventFormatterMd,
|
||||||
LinkStyle,
|
LinkStyle,
|
||||||
} from './feature-event-formatter-md';
|
} from './feature-event-formatter-md';
|
||||||
import type { IEvent } from '../types/events';
|
import type { IEvent } from '../types/events';
|
||||||
|
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
|
||||||
|
|
||||||
interface ISlackAppAddonParameters {
|
interface ISlackAppAddonParameters {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@ -46,30 +47,42 @@ export default class SlackAppAddon extends Addon {
|
|||||||
parameters: ISlackAppAddonParameters,
|
parameters: ISlackAppAddonParameters,
|
||||||
integrationId: number,
|
integrationId: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
let state: IntegrationEventState = 'success';
|
||||||
|
const stateDetails: string[] = [];
|
||||||
|
let channels: string[] = [];
|
||||||
|
let message = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { accessToken, defaultChannels } = parameters;
|
const { accessToken, defaultChannels } = parameters;
|
||||||
if (!accessToken) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const taggedChannels = this.findTaggedChannels(event);
|
const taggedChannels = this.findTaggedChannels(event);
|
||||||
const eventChannels = [
|
channels = this.getUniqueArray(
|
||||||
...new Set(
|
taggedChannels.concat(this.getDefaultChannels(defaultChannels)),
|
||||||
taggedChannels.concat(
|
);
|
||||||
this.getDefaultChannels(defaultChannels),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!eventChannels.length) {
|
if (!channels.length) {
|
||||||
this.logger.debug(
|
const noSlackChannelsMessage = `No Slack channels found for event ${event.type}.`;
|
||||||
`No Slack channels found for event ${event.type}.`,
|
this.logger.debug(noSlackChannelsMessage);
|
||||||
|
this.registerEarlyFailureEvent(
|
||||||
|
integrationId,
|
||||||
|
event,
|
||||||
|
noSlackChannelsMessage,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Found candidate channels: ${JSON.stringify(eventChannels)}.`,
|
`Found candidate channels: ${JSON.stringify(channels)}.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.slackClient || this.accessToken !== accessToken) {
|
if (!this.slackClient || this.accessToken !== accessToken) {
|
||||||
@ -84,6 +97,7 @@ export default class SlackAppAddon extends Addon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { text, url } = this.msgFormatter.format(event);
|
const { text, url } = this.msgFormatter.format(event);
|
||||||
|
message = text;
|
||||||
|
|
||||||
const blocks: (Block | KnownBlock)[] = [
|
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({
|
return this.slackClient!.chat.postMessage({
|
||||||
channel: name,
|
channel: name,
|
||||||
text,
|
text,
|
||||||
@ -123,23 +137,73 @@ export default class SlackAppAddon extends Addon {
|
|||||||
|
|
||||||
const results = await Promise.allSettled(requests);
|
const results = await Promise.allSettled(requests);
|
||||||
|
|
||||||
results
|
const failedRequests = results.filter(
|
||||||
.filter(({ status }) => status === 'rejected')
|
({ 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 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) {
|
} 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[] {
|
findTaggedChannels({ tags }: Pick<IEvent, 'tags'>): string[] {
|
||||||
if (tags) {
|
if (tags) {
|
||||||
return tags
|
return tags
|
||||||
@ -156,39 +220,27 @@ export default class SlackAppAddon extends Addon {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(event: IEvent, error: Error | CodedError): void {
|
parseError(error: Error | CodedError): string {
|
||||||
if (!('code' in error)) {
|
if ('code' in error) {
|
||||||
this.logger.warn(`Error handling event ${event.type}.`, error);
|
if (error.code === ErrorCode.PlatformError) {
|
||||||
return;
|
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) {
|
return error.message;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,7 +336,8 @@ describe('Slack integration', () => {
|
|||||||
url: parameters.url,
|
url: parameters.url,
|
||||||
channels: ['general'],
|
channels: ['general'],
|
||||||
username: 'Unleash',
|
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 {
|
try {
|
||||||
extraHeaders = JSON.parse(customHeaders);
|
extraHeaders = JSON.parse(customHeaders);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const detailMessage =
|
|
||||||
'Could not parse the JSON in the customHeaders parameter.';
|
|
||||||
state = 'successWithErrors';
|
state = 'successWithErrors';
|
||||||
stateDetails.push(detailMessage);
|
const badHeadersMessage =
|
||||||
this.logger.warn(detailMessage);
|
'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}.`);
|
this.logger.info(`Handled event ${event.type}.`);
|
||||||
|
|
||||||
if (failedRequests.length === 0) {
|
if (failedRequests.length === 0) {
|
||||||
const detailMessage = `All (${results.length}) Slack webhook requests were successful with status codes: ${codes}.`;
|
const successMessage = `All (${results.length}) Slack webhook requests were successful with status codes: ${codes}.`;
|
||||||
stateDetails.push(detailMessage);
|
stateDetails.push(successMessage);
|
||||||
this.logger.info(detailMessage);
|
this.logger.info(successMessage);
|
||||||
} else if (failedRequests.length === results.length) {
|
} else if (failedRequests.length === results.length) {
|
||||||
const detailMessage = `All (${results.length}) Slack webhook requests failed with status codes: ${codes}.`;
|
|
||||||
state = 'failed';
|
state = 'failed';
|
||||||
stateDetails.push(detailMessage);
|
const failedMessage = `All (${results.length}) Slack webhook requests failed with status codes: ${codes}.`;
|
||||||
this.logger.warn(detailMessage);
|
stateDetails.push(failedMessage);
|
||||||
|
this.logger.warn(failedMessage);
|
||||||
} else {
|
} else {
|
||||||
const detailMessage = `Some (${failedRequests.length} of ${results.length}) Slack webhook requests failed. Status codes: ${codes}.`;
|
|
||||||
state = 'successWithErrors';
|
state = 'successWithErrors';
|
||||||
stateDetails.push(detailMessage);
|
const successWithErrorsMessage = `Some (${failedRequests.length} of ${results.length}) Slack webhook requests failed. Status codes: ${codes}.`;
|
||||||
this.logger.warn(detailMessage);
|
stateDetails.push(successWithErrorsMessage);
|
||||||
|
this.logger.warn(successWithErrorsMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.registerEvent({
|
this.registerEvent({
|
||||||
@ -133,7 +133,7 @@ export default class SlackAddon extends Addon {
|
|||||||
url,
|
url,
|
||||||
channels: slackChannels,
|
channels: slackChannels,
|
||||||
username,
|
username,
|
||||||
text,
|
message: text,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user