Skip to content

Commit 58b8f74

Browse files
authored
Merge pull request #20 from port-labs/break-up-slack-messages
Fix bug where Slack API returns 400 due to large entities
2 parents 22e627b + 20b0909 commit 58b8f74

File tree

1 file changed

+107
-18
lines changed

1 file changed

+107
-18
lines changed

generators/slack.py

+107-18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88

99
class SlackMessageGenerator(generators.base.BaseMessageGenerator):
10+
# Slack has a limit of 3001 characters per message block
11+
# We will use this constant to split the message blocks into smaller ones
12+
SLACK_MAX_MESSAGE_BLOCK_SIZE = 3000
1013

1114
def scorecard_report(self, blueprint: str, scorecard: Dict[str, Any], entities: list):
1215
blueprint_plural = utils.convert_to_plural(blueprint).title()
@@ -233,14 +236,10 @@ def scorecard_reminder(self,
233236
"text": f"*⚠️ {number_of_entities_didnt_pass_all_rules} {blueprint_plural} with unmet rules*"
234237
}
235238
},
236-
{
237-
"type": "section",
238-
"text": {
239-
"type": "mrkdwn",
240-
"text": self._generate_entities_list_with_level_and_link(blueprint,
241-
entities_didnt_pass_all_rules)
242-
}
243-
}
239+
*self._generate_entities_list_with_level_and_link(
240+
blueprint,
241+
entities_didnt_pass_all_rules
242+
)
244243
]
245244
return blocks
246245

@@ -285,7 +284,11 @@ def _resolve_top_highest_lowest_scored_rules(entities: list,
285284
if not matched:
286285
filtered_top_lowest_scored_rules_by_percentage.append((lowest_rule, value))
287286

288-
return top_3_highest_scored_rules_by_percentage, filtered_top_lowest_scored_rules_by_percentage
287+
return (
288+
top_3_highest_scored_rules_by_percentage,
289+
filtered_top_lowest_scored_rules_by_percentage
290+
)
291+
289292

290293
@staticmethod
291294
def _calculate_top_teams_by_percentage(entities: list,
@@ -303,16 +306,102 @@ def _calculate_top_teams_by_percentage(entities: list,
303306
return top_3_teams_by_percentage
304307

305308
@staticmethod
306-
def _generate_entities_list_with_level_and_link(blueprint: str,
307-
entities_by_level: Dict[str, List[Dict[str, str]]]) -> str:
308-
text = ""
309-
base_entity_url = f"{get_port_url(settings.port_region, 'app')}/{blueprint}Entity?identifier="
309+
def _generate_entities_list_with_level_and_link(
310+
blueprint: str,
311+
entities_by_level: Dict[str, List[Dict[str, str]]]
312+
) -> list[dict[str, str]]:
313+
"""
314+
Generates message block for Slack with entities grouped by level,
315+
being aware of the 3001 message limit for Slack blocks.
316+
"""
317+
block = []
310318
for level, entities in entities_by_level.items():
311319
if not entities:
312320
continue
321+
322+
level_title = f"*{level}*\n\n"
323+
324+
# we must account for the length of the level title
325+
# in the max size of a slack message block
326+
max_message_block_size_for_entities = (
327+
SlackMessageGenerator.SLACK_MAX_MESSAGE_BLOCK_SIZE
328+
- len(level_title)
329+
)
330+
# let's us know if this is the first batch of entities
331+
# which will contain the title of the level
332+
batch_count = 0
333+
current_count = 0
334+
# the entities that will be written to slack at this current
335+
# iteration
336+
current_entities = []
313337

314-
text += f"\n*{level}*\n\n"
315-
for entity in entities:
316-
text += f"• <{base_entity_url}{entity.get('identifier')}|{entity.get('name')}>" \
317-
f" - `[{len(entity.get('passed_rules'))}/{entity.get('number_of_rules')}] Passed` \n"
318-
return text
338+
while current_count < len(entities):
339+
# we will keep adding entities to the current_entities list
340+
# until we reach the max size of a slack message block
341+
text_length = 0
342+
entities_text = ""
343+
while text_length < (
344+
max_message_block_size_for_entities
345+
) and current_count < len(entities):
346+
current_entities.append(entities[current_count])
347+
entities_text = " \n".join(
348+
SlackMessageGenerator._generate_text_for_entity(blueprint, entity)
349+
for entity in current_entities
350+
)
351+
text_length = len(entities_text)
352+
current_count += 1
353+
354+
# if we reach the end of the entities list,
355+
# we will generate the block
356+
if current_count == len(entities):
357+
block.append(
358+
SlackMessageGenerator._generate_block_for_entities(
359+
current_entities,
360+
blueprint,
361+
level_title if batch_count == 0 else ""
362+
)
363+
)
364+
break
365+
366+
# the only way we can reach this point is if the current_entities
367+
# list is above the max size of a slack message block
368+
# if we simply make the request, the API call will fail
369+
# therefore, we will remove the last entity from the list
370+
# to stay within the limit.
371+
current_entities.pop()
372+
# update the current count to reflect the removal of the last entity
373+
current_count -= 1
374+
block.append(
375+
SlackMessageGenerator._generate_block_for_entities(
376+
current_entities,
377+
blueprint,
378+
level_title if batch_count == 0 else ""
379+
)
380+
)
381+
current_entities = []
382+
batch_count += 1
383+
return block
384+
385+
@staticmethod
386+
def _generate_block_for_entities(entities: List[Dict[str, str]], blueprint: str, prefix: str = "") -> Dict[str, Any]:
387+
return {
388+
"type": "section",
389+
"text": {
390+
"type": "mrkdwn",
391+
"text": (
392+
prefix +
393+
" \n".join(
394+
SlackMessageGenerator._generate_text_for_entity(blueprint, entity)
395+
for entity in entities
396+
)
397+
)
398+
}
399+
}
400+
401+
@staticmethod
402+
def _generate_text_for_entity(blueprint: str, entity: Dict[str, str]) -> str:
403+
base_entity_url = f"{get_port_url(settings.port_region, 'app')}/{blueprint}Entity?identifier="
404+
return (
405+
f"• <{base_entity_url}{entity.get('identifier')}|{entity.get('name')}>"
406+
f" - `[{len(entity.get('passed_rules'))}/{entity.get('number_of_rules')}] Passed`"
407+
)

0 commit comments

Comments
 (0)