Repository: apify/crawlee-python
File: src/crawlee/statistics/_error_tracker.py (Python)

Code before:
        for stack_group in self._errors.values():
            for name_group in stack_group.values():
                error_count += sum(name_group.values())
        return error_count

    def get_most_common_errors(self, n: int = 3) -> list[tuple[str | None, int]]:
        """Return n most common errors."""
        all_errors: ErrorMessageGroups = Counter()
        for stack_group_name, stack_group in self._errors.items():
            for name_group_name, name_group in stack_group.items():
                for message_group_name, count in name_group.items():
                    all_errors[self._get_error_repr(stack_group_name, name_group_name, message_group_name)] = count
        return all_errors.most_common(n)

    def _get_error_repr(self, stack: str | None, name: str | None, message: str | None) -> str:
        """Get the most specific error representation."""
        stack_part = f'{stack}:' if stack else ''
        name_part = f'{name}:' if name else ''
        message_part = f'{message}' if message else ''
        return f'{stack_part}{name_part}{message_part}'

    @staticmethod
    def _create_generic_message(message_1: str | None, message_2: str | None) -> str | None:
        """Create a generic error message from two messages, if they are similar enough.

        Different parts of similar messages are replaced by `_`.
        """
        if message_1 is None or message_2 is None:
            return None

        replacement_symbol = '_'
        replacement_count = 0

        generic_message_parts = []
        message_1_parts = message_1.split(' ')
        message_2_parts = message_2.split(' ')
        parts_count = min(len(message_1_parts), len(message_2_parts))

        for message_1_part, message_2_part in zip_longest(message_1_parts, message_2_parts, fillvalue=''):
            if message_1_part != message_2_part:
                generic_message_parts.append(replacement_symbol)
                replacement_count += 1
                if replacement_count >= parts_count / 2:
                    # Messages are too different.
                    return ''
            else:
                generic_message_parts.append(message_1_part)
        return ' '.join(generic_message_parts)


Review comment (suggestion):
I'm kinda surprised by the choice of `_` here - I think it might get mixed up with legit underscores in python error messages.

Code after:
                error_count += sum(name_group.values())
        return error_count

    def get_most_common_errors(self, n: int = 3) -> list[tuple[str | None, int]]:
        """Return n most common errors."""
        all_errors: Counter[GroupName] = Counter()
        for file_and_line_group_name, file_and_line_group in self._errors.items():
            for name_group_name, name_group in file_and_line_group.items():
                for message_group_name, count in name_group.items():
                    all_errors[self._get_error_repr(file_and_line_group_name, name_group_name, message_group_name)] = (
                        count
                    )
        return all_errors.most_common(n)

    def _get_error_repr(self, file_and_line: str | None, name: str | None, message: str | None) -> str:
        """Get the most specific error representation."""
        file_and_line_part = f'{file_and_line}:' if file_and_line else ''
        name_part = f'{name}:' if name else ''
        message_part = f'{message}' if message else ''
        return f'{file_and_line_part}{name_part}{message_part}'

    @staticmethod
    def _create_generic_message(message_1: str | None, message_2: str | None) -> str | None:
        """Create a generic error message from two messages, if they are similar enough.

        Different parts of similar messages are replaced by `_`.
        """
        if message_1 is None or message_2 is None:
            return None

        replacement_string = '***'
        replacement_count = 0

        generic_message_parts = []
        message_1_parts = message_1.split(' ')
        message_2_parts = message_2.split(' ')
        parts_count = min(len(message_1_parts), len(message_2_parts))

        for message_1_part, message_2_part in zip_longest(message_1_parts, message_2_parts, fillvalue=''):
            if message_1_part != message_2_part:
                generic_message_parts.append(replacement_string)
                replacement_count += 1
                if replacement_count >= parts_count / 2:
                    # Messages are too different.
                    return ''
            else:
                generic_message_parts.append(message_1_part)
        return ' '.join(generic_message_parts)