Skip to content

Commit

Permalink
v2.4.5: Split Slack message into chunks.
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelmhtr committed Aug 27, 2022
1 parent d02e082 commit ac5c6eb
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 34 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [2.4.5] - 2022-08-27
### Fixed
- [#38](https://github.com/flowwer-dev/pull-request-stats/issues/38#issuecomment-1171087421) Split Slack message to prevent hitting characters limit.

## [2.4.4] - 2022-08-21
### Fixed
- [#46](https://github.com/flowwer-dev/pull-request-stats/issues/46) Filtering reviewers with `undefined` name.
Expand Down
94 changes: 79 additions & 15 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10996,6 +10996,18 @@ function setup(env) {
module.exports = setup;


/***/ }),

/***/ 508:
/***/ (function(module) {

const getSlackCharsLimit = () => 39000;

module.exports = {
getSlackCharsLimit,
};


/***/ }),

/***/ 510:
Expand Down Expand Up @@ -13045,6 +13057,50 @@ module.exports = function settle(resolve, reject, response) {
};


/***/ }),

/***/ 569:
/***/ (function(module, __unusedexports, __webpack_require__) {

const { getSlackCharsLimit } = __webpack_require__(508);
const { median } = __webpack_require__(353);

const CHARS_LIMIT = getSlackCharsLimit();

const getSize = (obj) => JSON.stringify(obj).length;

const getBlockLengths = (blocks) => blocks
.filter(({ type }) => type === 'section') // Ignoring "divider" blocks
.map((block) => getSize(block));

const getSizePerBlock = (blocks) => Math.round(median(getBlockLengths(blocks)));

module.exports = (message) => {
const blockSize = Math.max(1, getSizePerBlock(message.blocks));

const getBlocksToSplit = (blocks) => {
const currentSize = getSize({ blocks });
const diff = currentSize - CHARS_LIMIT;
if (diff < 0 || blocks.length === 1) return 0;

const blocksSpace = Math.ceil(diff / blockSize);
const blocksCount = Math.max(1, Math.min(blocks.length - 1, blocksSpace));
const firsts = blocks.slice(0, blocksCount);
return getBlocksToSplit(firsts) || blocksCount;
};

const getChunks = (prev, msg) => {
const blocksToSplit = getBlocksToSplit(msg.blocks);
if (!blocksToSplit) return [...prev, msg];
const blocks = msg.blocks.slice(0, blocksToSplit);
const others = msg.blocks.slice(blocksToSplit);
return getChunks([...prev, { blocks }], { blocks: others });
};

return getChunks([], message);
};


/***/ }),

/***/ 578:
Expand Down Expand Up @@ -14873,7 +14929,7 @@ module.exports = function bind(fn, thisArg) {
/***/ 731:
/***/ (function(module) {

module.exports = {"name":"pull-request-stats","version":"2.4.4","description":"Github action to print relevant stats about Pull Request reviewers","main":"dist/index.js","scripts":{"build":"ncc build src/index.js","test":"yarn run build && jest"},"keywords":[],"author":"Manuel de la Torre","license":"MIT","jest":{"testEnvironment":"node","testMatch":["**/?(*.)+(spec|test).[jt]s?(x)"]},"dependencies":{"@actions/core":"^1.5.0","@actions/github":"^5.0.0","@sentry/react-native":"^3.4.2","axios":"^0.26.1","dotenv":"^16.0.1","graphql":"^16.5.0","graphql-anywhere":"^4.2.7","humanize-duration":"^3.27.0","i18n-js":"^3.9.2","jsurl":"^0.1.5","lodash":"^4.17.21","lodash.get":"^4.4.2","lottie-react-native":"^5.1.3","markdown-table":"^2.0.0","mixpanel":"^0.13.0"},"devDependencies":{"@zeit/ncc":"^0.22.3","eslint":"^7.32.0","eslint-config-airbnb-base":"^14.2.1","eslint-plugin-import":"^2.24.1","eslint-plugin-jest":"^24.4.0","jest":"^27.0.6"},"funding":"https://github.com/sponsors/manuelmhtr"};
module.exports = {"name":"pull-request-stats","version":"2.4.5","description":"Github action to print relevant stats about Pull Request reviewers","main":"dist/index.js","scripts":{"build":"ncc build src/index.js","test":"yarn run build && jest"},"keywords":[],"author":"Manuel de la Torre","license":"MIT","jest":{"testEnvironment":"node","testMatch":["**/?(*.)+(spec|test).[jt]s?(x)"]},"dependencies":{"@actions/core":"^1.5.0","@actions/github":"^5.0.0","@sentry/react-native":"^3.4.2","axios":"^0.26.1","dotenv":"^16.0.1","graphql":"^16.5.0","graphql-anywhere":"^4.2.7","humanize-duration":"^3.27.0","i18n-js":"^3.9.2","jsurl":"^0.1.5","lodash":"^4.17.21","lodash.get":"^4.4.2","lottie-react-native":"^5.1.3","markdown-table":"^2.0.0","mixpanel":"^0.13.0"},"devDependencies":{"@zeit/ncc":"^0.22.3","eslint":"^7.32.0","eslint-config-airbnb-base":"^14.2.1","eslint-plugin-import":"^2.24.1","eslint-plugin-jest":"^24.4.0","jest":"^27.0.6"},"funding":"https://github.com/sponsors/manuelmhtr"};

/***/ }),

Expand Down Expand Up @@ -18246,6 +18302,7 @@ module.exports = require("tty");
const { t } = __webpack_require__(781);
const { postToSlack } = __webpack_require__(162);
const buildSlackMessage = __webpack_require__(337);
const splitInChunks = __webpack_require__(569);

module.exports = async ({
org,
Expand All @@ -18271,7 +18328,19 @@ module.exports = async ({
return;
}

const message = buildSlackMessage({
const send = (message) => {
const params = {
webhook,
channel,
message,
iconUrl: t('table.icon'),
username: t('table.title'),
};
core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`);
return postToSlack(params);
};

const fullMessage = buildSlackMessage({
org,
repos,
reviewers,
Expand All @@ -18281,19 +18350,14 @@ module.exports = async ({
displayCharts,
});

const params = {
webhook,
channel,
message,
iconUrl: t('table.icon'),
username: t('table.title'),
};
core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`);

await postToSlack(params).catch((error) => {
core.error(`Error posting Slack message: ${error}`);
throw error;
});
const chunks = splitInChunks(fullMessage);
await chunks.reduce(async (promise, message) => {
await promise;
return send(message).catch((error) => {
core.error(`Error posting Slack message: ${error}`);
throw error;
});
}, Promise.resolve());

core.debug('Successfully posted to slack');
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pull-request-stats",
"version": "2.4.4",
"version": "2.4.5",
"description": "Github action to print relevant stats about Pull Request reviewers",
"main": "dist/index.js",
"scripts": {
Expand Down
5 changes: 5 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const getSlackCharsLimit = () => 39000;

module.exports = {
getSlackCharsLimit,
};
19 changes: 15 additions & 4 deletions src/interactors/postSlackMessage/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
const Fetchers = require('../../../fetchers');
const { t } = require('../../../i18n');
const buildSlackMessage = require('../buildSlackMessage');
const splitInChunks = require('../splitInChunks');
const postSlackMessage = require('../index');

const MESSAGE = 'MESSAGE';
const MESSAGE = { blocks: ['MESSAGE'] };

jest.mock('../../../fetchers', () => ({ postToSlack: jest.fn(() => Promise.resolve()) }));
jest.mock('../buildSlackMessage', () => jest.fn(() => MESSAGE));
jest.mock('../splitInChunks', () => jest.fn((message) => [message]));

describe('Interactors | .postSlackMessage', () => {
const debug = jest.fn();
Expand Down Expand Up @@ -68,23 +70,32 @@ describe('Interactors | .postSlackMessage', () => {
});

describe('when integration is enabled', () => {
it('logs an error', async () => {
it('posts successfully to Slack', async () => {
await postSlackMessage({ ...defaultOptions });
expect(error).not.toHaveBeenCalled();
expect(buildSlackMessage).toHaveBeenCalledWith({
expect(buildSlackMessage).toBeCalledWith({
reviewers: defaultOptions.reviewers,
pullRequest: defaultOptions.pullRequest,
periodLength: defaultOptions.periodLength,
disableLinks: defaultOptions.disableLinks,
displayCharts: defaultOptions.displayCharts,
});
expect(Fetchers.postToSlack).toHaveBeenCalledWith({
expect(Fetchers.postToSlack).toBeCalledTimes(1);
expect(Fetchers.postToSlack).toBeCalledWith({
webhook: defaultOptions.slack.webhook,
channel: defaultOptions.slack.channel,
message: MESSAGE,
iconUrl: t('table.icon'),
username: t('table.title'),
});
});

it('posts multiple times with divided in chunks', async () => {
splitInChunks.mockImplementationOnce((message) => [message, message, message]);
await postSlackMessage({ ...defaultOptions });
expect(error).not.toHaveBeenCalled();
expect(buildSlackMessage).toBeCalledTimes(1);
expect(Fetchers.postToSlack).toBeCalledTimes(3);
});
});
});
49 changes: 49 additions & 0 deletions src/interactors/postSlackMessage/__tests__/splitInChunks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// const { getSlackCharsLimit } = require('../../../config');
const splitInChunks = require('../splitInChunks');

const buildBlock = (str, length) => `${str}`.padEnd(length, '.');

jest.mock('../../../config', () => ({
getSlackCharsLimit: () => 100,
}));

describe('Interactors | .postSlackMessage | .splitInChunks', () => {
it('wraps block in array when length is ok', () => {
const block1 = buildBlock('BLOCK 1', 10);
const message = {
blocks: [block1],
};
const expectedChunks = [message];
const result = splitInChunks(message);
expect(result).toEqual(expectedChunks);
});

it('divides block in chunks when above length, keeping order', () => {
const block1 = buildBlock('BLOCK 1', 15);
const block2 = buildBlock('BLOCK 2', 15);
const block3 = buildBlock('BLOCK 3', 120);
const block4 = buildBlock('BLOCK 4', 60);
const block5 = buildBlock('BLOCK 5', 50);
const block6 = buildBlock('BLOCK 6', 10);
const block7 = buildBlock('BLOCK 7', 10);
const message = {
blocks: [
block1,
block2,
block3,
block4,
block5,
block6,
block7,
],
};
const expectedChunks = [
{ blocks: [block1, block2] },
{ blocks: [block3] },
{ blocks: [block4] },
{ blocks: [block5, block6, block7] },
];
const result = splitInChunks(message);
expect(result).toEqual(expectedChunks);
});
});
36 changes: 22 additions & 14 deletions src/interactors/postSlackMessage/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { t } = require('../../i18n');
const { postToSlack } = require('../../fetchers');
const buildSlackMessage = require('./buildSlackMessage');
const splitInChunks = require('./splitInChunks');

module.exports = async ({
org,
Expand All @@ -26,7 +27,19 @@ module.exports = async ({
return;
}

const message = buildSlackMessage({
const send = (message) => {
const params = {
webhook,
channel,
message,
iconUrl: t('table.icon'),
username: t('table.title'),
};
core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`);
return postToSlack(params);
};

const fullMessage = buildSlackMessage({
org,
repos,
reviewers,
Expand All @@ -36,19 +49,14 @@ module.exports = async ({
displayCharts,
});

const params = {
webhook,
channel,
message,
iconUrl: t('table.icon'),
username: t('table.title'),
};
core.debug(`Post a Slack message with params: ${JSON.stringify(params, null, 2)}`);

await postToSlack(params).catch((error) => {
core.error(`Error posting Slack message: ${error}`);
throw error;
});
const chunks = splitInChunks(fullMessage);
await chunks.reduce(async (promise, message) => {
await promise;
return send(message).catch((error) => {
core.error(`Error posting Slack message: ${error}`);
throw error;
});
}, Promise.resolve());

core.debug('Successfully posted to slack');
};
37 changes: 37 additions & 0 deletions src/interactors/postSlackMessage/splitInChunks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { getSlackCharsLimit } = require('../../config');
const { median } = require('../../utils');

const CHARS_LIMIT = getSlackCharsLimit();

const getSize = (obj) => JSON.stringify(obj).length;

const getBlockLengths = (blocks) => blocks
.filter(({ type }) => type === 'section') // Ignoring "divider" blocks
.map((block) => getSize(block));

const getSizePerBlock = (blocks) => Math.round(median(getBlockLengths(blocks)));

module.exports = (message) => {
const blockSize = Math.max(1, getSizePerBlock(message.blocks));

const getBlocksToSplit = (blocks) => {
const currentSize = getSize({ blocks });
const diff = currentSize - CHARS_LIMIT;
if (diff < 0 || blocks.length === 1) return 0;

const blocksSpace = Math.ceil(diff / blockSize);
const blocksCount = Math.max(1, Math.min(blocks.length - 1, blocksSpace));
const firsts = blocks.slice(0, blocksCount);
return getBlocksToSplit(firsts) || blocksCount;
};

const getChunks = (prev, msg) => {
const blocksToSplit = getBlocksToSplit(msg.blocks);
if (!blocksToSplit) return [...prev, msg];
const blocks = msg.blocks.slice(0, blocksToSplit);
const others = msg.blocks.slice(blocksToSplit);
return getChunks([...prev, { blocks }], { blocks: others });
};

return getChunks([], message);
};

0 comments on commit ac5c6eb

Please sign in to comment.