Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Whatsapp failed msg logging #1607

Merged

Conversation

maheshsattala
Copy link
Contributor

@maheshsattala maheshsattala commented Nov 26, 2024

Added code for logging whatsapp failed messages and added test cases accordingly.

Summary by CodeRabbit

  • New Features

    • Introduced error handling for failed WhatsApp message deliveries, logging relevant details for debugging.
    • Added a method to log failed WhatsApp messages in the chat processor.
    • Expanded initialization capabilities of the WhatsApp Cloud class with a new optional parameter.
  • Tests

    • Enhanced test coverage with new tests for logging failed WhatsApp messages and error handling scenarios.

Copy link

coderabbitai bot commented Nov 26, 2024

Walkthrough

The changes in this pull request introduce modifications to the WhatsappBot class in whatsapp.py and add a new static method in ChatDataProcessor to log failed WhatsApp message responses. The send_custom_json method now captures the response from the WhatsApp client and handles errors by logging failed messages. Additionally, new tests are added to validate the functionality of the logging mechanism and error handling for WhatsApp messages.

Changes

File Path Change Summary
kairon/chat/handlers/channels/whatsapp.py Modified WhatsappBot.send_custom_json to capture response and handle errors by logging failed messages.
kairon/shared/chat/processor.py Added static method save_whatsapp_failed_messages in ChatDataProcessor to log details of failed WhatsApp messages.
tests/unit_test/chat/chat_test.py Added tests for save_whatsapp_failed_messages and error handling in TestChat class, ensuring proper logging.
kairon/chat/handlers/channels/clients/whatsapp/cloud.py Added optional parameter metadata to WhatsappCloud.__init__ method for enhanced initialization.

Possibly related PRs

Suggested reviewers

  • hiteshghuge
  • sfahad1414

Poem

In the garden where messages bloom,
A bot sends whispers, dispelling the gloom.
With logs to catch errors, swift as a hare,
Each message a tale, handled with care.
So hop along, dear friends, take delight,
For WhatsApp's now brighter, a joyful sight! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (7)
kairon/shared/chat/processor.py (2)

170-183: Minor documentation improvements needed

The method signature and documentation are well-structured, but consider these improvements:

  • Add type hints in the docstring for better clarity
  • Add examples of usage in the docstring
  • Document possible exceptions that might be raised
 @staticmethod
 def save_whatsapp_failed_messages(resp: Dict, bot: Text, recipient: Text, channel_type: Text, **kwargs):
     """
     Logs failed WhatsApp message responses for debugging and tracking purposes.

     Args:
-        resp (Dict): The response dictionary containing error details.
-        bot (Text): The bot identifier.
-        recipient (Text): The recipient identifier.
-        channel_type (Text): The type of channel (e.g., WhatsApp).
+        resp (Dict): The response dictionary containing error details. Example: {"error": {"error_data": {"details": "Invalid number"}}}
+        bot (Text): The bot identifier. Example: "bot_123"
+        recipient (Text): The recipient identifier. Example: "+1234567890"
+        channel_type (Text): The type of channel (e.g., WhatsApp).
+        **kwargs: Additional keyword arguments including:
+            - json_message (Dict): The original message that failed to send

     Returns:
-        None
+        None: The method saves the log entry to the database.
+
+    Raises:
+        mongoengine.errors.ValidationError: If required fields are missing or invalid
     """

170-201: Consider architectural improvements for logging consistency

The new method shares similar functionality with save_whatsapp_audit_log. Consider these architectural improvements:

  1. Create a common base method for WhatsApp logging
  2. Standardize error handling and logging patterns
  3. Consider using an enum for status types

Example approach:

from enum import Enum

class WhatsAppMessageStatus(Enum):
    DELIVERED = "delivered"
    READ = "read"
    FAILED = "failed"
    CAPTURED = "captured"

@staticmethod
def _save_whatsapp_log(
    status: WhatsAppMessageStatus,
    data: Dict,
    bot: Text,
    recipient: Text,
    channel_type: Text,
    **kwargs
) -> None:
    """Common method for WhatsApp logging."""
    # Common logging logic here

This would help maintain consistency across different types of WhatsApp message logging and make the code more maintainable.

kairon/chat/handlers/channels/whatsapp.py (2)

241-245: Enhance security measures for message handling

Consider implementing additional security measures:

  1. Validate and sanitize the recipient_id format before processing
  2. Implement rate limiting for failed message logging
  3. Sanitize error messages before logging to prevent log injection
  4. Add audit logging for security-sensitive operations

Would you like me to provide a detailed implementation for these security enhancements?


241-245: Add comprehensive test coverage

The new error handling logic requires additional test coverage:

  1. Unit tests for various error scenarios:
    • Missing assistant_id
    • Various WhatsApp error responses
    • Database logging failures
  2. Integration tests for WhatsApp client interaction
  3. Mocking strategy for external dependencies

Would you like me to help create the test cases?

tests/unit_test/chat/chat_test.py (3)

244-250: Remove duplicate assertion for message type.

The test contains a duplicate assertion for the WhatsApp message type:

assert log['type'] == 'whatsapp'  # Line 244
assert log['type'] == 'whatsapp'  # Line 250

Remove one of these duplicate assertions to maintain cleaner test code.


305-305: Remove unused variable.

The variable channel_type is assigned but never used in the test method.

Remove this unused variable assignment:

-channel_type = "whatsapp"
🧰 Tools
🪛 Ruff (0.8.0)

305-305: Local variable channel_type is assigned to but never used

Remove assignment to unused variable channel_type

(F841)


317-323: Remove duplicate assertion for message type.

The test contains a duplicate assertion for the WhatsApp message type:

assert log['type'] == 'whatsapp'  # Line 317
assert log['type'] == 'whatsapp'  # Line 323

Remove one of these duplicate assertions to maintain cleaner test code.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 9a425e0 and cc58de0.

📒 Files selected for processing (3)
  • kairon/chat/handlers/channels/whatsapp.py (1 hunks)
  • kairon/shared/chat/processor.py (1 hunks)
  • tests/unit_test/chat/chat_test.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.0)
tests/unit_test/chat/chat_test.py

305-305: Local variable channel_type is assigned to but never used

Remove assignment to unused variable channel_type

(F841)

🔇 Additional comments (3)
kairon/shared/chat/processor.py (1)

170-201: Verify logging patterns and model usage

Let's verify the consistency of logging patterns and ChannelLogs model usage across the codebase.

✅ Verification successful

Based on the gathered information, I can now provide the final response:

Implementation is consistent with logging patterns

The implementation of save_whatsapp_failed_messages follows the correct pattern for logging WhatsApp message failures:

  • Uses the standard ChannelLogs model with all required fields (type, status, data, message_id, bot, user)
  • Properly handles error details through the failure_reason field
  • Consistent with test cases showing various message statuses (sent, delivered, read, failed)
  • Follows the same pattern used in other WhatsApp message logging throughout the codebase
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for similar logging patterns and ChannelLogs usage

# Find similar logging patterns
echo "Checking for similar logging patterns..."
rg -A 5 "ChannelLogs\(" --type py

# Check for other WhatsApp-related logging
echo "Checking for WhatsApp-related logging..."
rg -B 2 -A 2 "whatsapp.*log" --type py -i

# Find all status types used with ChannelLogs
echo "Checking ChannelLogs status types..."
rg "status=['\"].*['\"].*ChannelLogs" --type py

Length of output: 101334


Script:

#!/bin/bash
# Description: Check for ChannelLogs model definition and usage

# Find ChannelLogs model definition
echo "Checking ChannelLogs model definition..."
ast-grep --pattern 'class ChannelLogs($$$)' --lang python

# Find all ChannelLogs instantiations with status field
echo "Checking ChannelLogs instantiations with status..."
ast-grep --pattern 'ChannelLogs(
  $$$
  status=$_,
  $$$
)' --lang python

# Find all WhatsApp message failure handling
echo "Checking WhatsApp message failure handling..."
rg -B 2 -A 2 "save_whatsapp.*failed" --type py

Length of output: 73250


Script:

#!/bin/bash
# Description: Check for ChannelLogs model definition and related code

# Find ChannelLogs model definition
echo "Checking ChannelLogs model definition..."
rg -A 10 "class ChannelLogs" --type py

# Find WhatsApp message status handling
echo "Checking WhatsApp message status handling..."
rg -B 2 -A 2 "status.*whatsapp" --type py -i

# Find all imports of ChannelLogs
echo "Checking ChannelLogs imports..."
rg "from.*ChannelLogs.*import" --type py

Length of output: 1196

kairon/chat/handlers/channels/whatsapp.py (1)

241-245: Verify consistent error handling patterns

Let's verify if similar error handling patterns exist in the codebase:

✅ Verification successful

Error handling implementation is consistent and complete

The verification confirms that the WhatsApp error handling implementation is consistent and well-integrated:

  1. The save_whatsapp_failed_messages method in ChatDataProcessor properly logs failed messages to ChannelLogs with all necessary context (bot, recipient, channel type, error details)
  2. The error handling pattern is consistent with other channel implementations and follows the established logging pattern using ChannelLogs
  3. The implementation includes proper error tracking with status, message IDs, and failure reasons
  4. Test coverage demonstrates comprehensive error scenarios and logging validation

The code follows a consistent pattern for error handling across the WhatsApp channel implementation and integrates well with the existing logging infrastructure.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for similar error handling patterns in WhatsApp-related code

# Look for WhatsApp error handling patterns
echo "Searching for WhatsApp error handling patterns..."
rg -A 5 "whatsapp.*error" --type py

# Look for similar message logging patterns
echo "Searching for message logging patterns..."
ast-grep --pattern 'save_whatsapp_$_messages'

# Check for other channel implementations
echo "Checking other channel implementations..."
rg -A 5 "send_custom_json.*error" --type py

Length of output: 16937


Script:

#!/bin/bash
# Let's search for the implementation of save_whatsapp_failed_messages and similar error handling patterns

# Search for the implementation of save_whatsapp_failed_messages
echo "Searching for WhatsApp message saving implementations..."
ast-grep --pattern 'def save_whatsapp_failed_messages'

# Look for error handling in WhatsApp client
echo "Searching for WhatsApp client error handling..."
rg -A 5 "if.*error.*whatsapp" --type py

# Check for similar error handling in message processors
echo "Checking message processor error handling..."
rg -A 5 "ChatDataProcessor.*save" --type py

Length of output: 26178


Script:

#!/bin/bash
# Let's look for the implementation of save_whatsapp_failed_messages and related error handling

# Search for save_whatsapp_failed_messages implementation
echo "Searching for save_whatsapp_failed_messages implementation..."
rg -A 10 "def save_whatsapp_failed_messages" --type py

# Look for similar error handling in other channel handlers
echo "Searching for error handling in channel handlers..."
rg -A 5 "if.*resp.get\(.*error" --type py

# Check for ChannelLogs usage
echo "Checking ChannelLogs usage..."
rg -A 5 "ChannelLogs" --type py

Length of output: 46857

tests/unit_test/chat/chat_test.py (1)

201-340: LGTM! Comprehensive test coverage for WhatsApp message logging.

The test implementation effectively covers:

  • Direct usage of the logging function
  • Integration with WhatsApp bot's message sending
  • Verification of all log fields
  • Error scenarios with invalid message formats
🧰 Tools
🪛 Ruff (0.8.0)

305-305: Local variable channel_type is assigned to but never used

Remove assignment to unused variable channel_type

(F841)

Comment on lines 184 to 200
error = resp.get("error", {})
message_id = f"failed-{channel_type}-{recipient}-{bot}"
user = "unknown_user"
failure_reason = error.get("error_data", {}).get("details")
logger.debug(f"WhatsApp message failed to send: {error}")

ChannelLogs(
type=channel_type,
status="failed",
data=resp,
failure_reason=failure_reason,
message_id=message_id,
user=user,
bot=bot,
recipient=recipient,
json_message=kwargs.get('json_message')
).save()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Several improvements needed in the implementation

The implementation has several areas that could be improved for better reliability and maintainability:

  1. The hardcoded "unknown_user" should be replaced with a proper user identifier from kwargs or system constants
  2. The message ID format might not guarantee uniqueness
  3. Missing validation for required fields
  4. No error handling for database operations
  5. No sanitization of the failure reason

Consider applying these improvements:

-        error = resp.get("error", {})
-        message_id = f"failed-{channel_type}-{recipient}-{bot}"
-        user = "unknown_user"
-        failure_reason = error.get("error_data", {}).get("details")
-        logger.debug(f"WhatsApp message failed to send: {error}")
+        error = resp.get("error", {})
+        timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S")
+        message_id = f"failed-{channel_type}-{recipient}-{bot}-{timestamp}"
+        user = kwargs.get('user', 'system')
+        
+        error_data = error.get("error_data", {})
+        failure_reason = error_data.get("details")
+        if failure_reason:
+            failure_reason = str(failure_reason)[:500]  # Limit length for storage
+        
+        logger.debug(f"WhatsApp message failed to send: {error}")
+        
+        try:
+            if not all([bot, recipient, channel_type]):
+                raise ValueError("Missing required fields: bot, recipient, or channel_type")
 
-        ChannelLogs(
+            log_entry = ChannelLogs(
                 type=channel_type,
                 status="failed",
                 data=resp,
@@ -199,3 +208,8 @@
                 recipient=recipient,
                 json_message=kwargs.get('json_message')
-        ).save()
+            )
+            log_entry.save()
+        except Exception as e:
+            logger.error(f"Failed to save WhatsApp error log: {str(e)}")
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
error = resp.get("error", {})
message_id = f"failed-{channel_type}-{recipient}-{bot}"
user = "unknown_user"
failure_reason = error.get("error_data", {}).get("details")
logger.debug(f"WhatsApp message failed to send: {error}")
ChannelLogs(
type=channel_type,
status="failed",
data=resp,
failure_reason=failure_reason,
message_id=message_id,
user=user,
bot=bot,
recipient=recipient,
json_message=kwargs.get('json_message')
).save()
error = resp.get("error", {})
timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S")
message_id = f"failed-{channel_type}-{recipient}-{bot}-{timestamp}"
user = kwargs.get('user', 'system')
error_data = error.get("error_data", {})
failure_reason = error_data.get("details")
if failure_reason:
failure_reason = str(failure_reason)[:500] # Limit length for storage
logger.debug(f"WhatsApp message failed to send: {error}")
try:
if not all([bot, recipient, channel_type]):
raise ValueError("Missing required fields: bot, recipient, or channel_type")
log_entry = ChannelLogs(
type=channel_type,
status="failed",
data=resp,
failure_reason=failure_reason,
message_id=message_id,
user=user,
bot=bot,
recipient=recipient,
json_message=kwargs.get('json_message')
)
log_entry.save()
except Exception as e:
logger.error(f"Failed to save WhatsApp error log: {str(e)}")
raise

Comment on lines 241 to 245
resp = self.whatsapp_client.send(response, recipient_id, messaging_type)
if resp.get("error"):
bot = kwargs.get("assistant_id")
ChatDataProcessor.save_whatsapp_failed_messages(resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
json_message=json_message)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type hints and improve error handling

The error handling implementation could be enhanced in several ways:

  1. Add type hints and improve error handling:
-            resp = self.whatsapp_client.send(response, recipient_id, messaging_type)
-            if resp.get("error"):
-                bot = kwargs.get("assistant_id")
-                ChatDataProcessor.save_whatsapp_failed_messages(resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
-                                                                json_message=json_message)
+            resp: Dict[str, Any] = self.whatsapp_client.send(response, recipient_id, messaging_type)
+            if resp.get("error"):
+                bot = kwargs.get("assistant_id")
+                if not bot:
+                    logger.error("Missing assistant_id in kwargs for failed message logging")
+                    return
+                logger.error(f"WhatsApp message failed: {resp.get('error')}")
+                try:
+                    ChatDataProcessor.save_whatsapp_failed_messages(
+                        resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
+                        json_message=json_message
+                    )
+                except Exception as e:
+                    logger.error(f"Failed to log WhatsApp error: {str(e)}")
  1. Consider adding validation for the response structure:
if not isinstance(resp, dict):
    logger.error(f"Unexpected response type from WhatsApp client: {type(resp)}")
    return

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
kairon/chat/handlers/channels/whatsapp.py (1)

241-254: Consider adding performance monitoring

To track and optimize the performance of WhatsApp message handling:

  1. Add metrics collection for:

    • Message send latency
    • Error rates
    • Retry attempts
    • Database operation latency
  2. Consider implementing circuit breaker pattern for external calls to prevent cascading failures.

Would you like me to provide an implementation example?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between cc58de0 and dc867ca.

📒 Files selected for processing (1)
  • kairon/chat/handlers/channels/whatsapp.py (1 hunks)
🔇 Additional comments (1)
kairon/chat/handlers/channels/whatsapp.py (1)

241-254: Verify error handling coverage

Let's verify the error handling implementation across the codebase:

✅ Verification successful

Based on the shell script results, I can see that the error handling implementation in the WhatsApp handler is comprehensive and follows good practices:

  1. The code logs errors at appropriate severity levels using logger.error()
  2. Failed message details are saved using ChatDataProcessor.save_whatsapp_failed_messages()
  3. Error responses from the WhatsApp API are properly checked and handled
  4. Exception handling is in place for the save operation itself

Error handling is properly implemented

The code follows a robust error handling pattern:

  • Checks for error in WhatsApp API response
  • Logs error details at appropriate severity level
  • Persists failed message details for debugging/monitoring
  • Has exception handling for the persistence operation itself
  • Includes relevant context like bot ID in error logs
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check error handling patterns in WhatsApp related code

# Check for error handling patterns in WhatsApp related files
echo "Checking error handling patterns in WhatsApp related files..."
rg -A 5 "WhatsappBot|WhatsappHandler|Whatsapp" --type py

# Check for logging patterns
echo "Checking logging patterns..."
rg "logger\.(error|warning|info|debug)" --type py

# Check for retry mechanisms
echo "Checking retry mechanisms..."
rg "attempt|retry|backoff" --type py

# Check for error response handling
echo "Checking error response handling..."
rg "get\(['\"]error['\"]" --type py

Length of output: 102220

Comment on lines +241 to +242
resp = self.whatsapp_client.send(response, recipient_id, messaging_type)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type hints and response validation

The response handling could be improved with proper type hints and validation:

-            resp = self.whatsapp_client.send(response, recipient_id, messaging_type)
+            resp: Dict[str, Any] = self.whatsapp_client.send(response, recipient_id, messaging_type)
+            if not isinstance(resp, dict):
+                logger.error(f"Unexpected response type from WhatsApp client: {type(resp)}")
+                return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resp = self.whatsapp_client.send(response, recipient_id, messaging_type)
resp: Dict[str, Any] = self.whatsapp_client.send(response, recipient_id, messaging_type)
if not isinstance(resp, dict):
logger.error(f"Unexpected response type from WhatsApp client: {type(resp)}")
return

Comment on lines 243 to 254
if resp.get("error"):
bot = kwargs.get("assistant_id")
if not bot:
logger.error("Missing assistant_id in kwargs for failed message logging")
return
logger.error(f"WhatsApp message failed: {resp.get('error')}")
try:
ChatDataProcessor.save_whatsapp_failed_messages(
resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
json_message=json_message
)
except Exception as e:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and logging security

The error handling implementation could be improved in several ways:

  1. Add retry mechanism for transient failures
  2. Sanitize sensitive data in logs
  3. Add structured logging with correlation IDs
-            if resp.get("error"):
-                bot = kwargs.get("assistant_id")
-                if not bot:
-                    logger.error("Missing assistant_id in kwargs for failed message logging")
-                    return
-                logger.error(f"WhatsApp message failed: {resp.get('error')}")
-                try:
-                    ChatDataProcessor.save_whatsapp_failed_messages(
-                        resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
-                        json_message=json_message
-                    )
-                except Exception as e:
-                    logger.error(f"Failed to log WhatsApp error: {str(e)}")
+            if resp.get("error"):
+                bot = kwargs.get("assistant_id")
+                if not bot:
+                    logger.error("Missing assistant_id in kwargs for failed message logging",
+                               extra={"correlation_id": Utility.generate_correlation_id()})
+                    return
+                
+                error_context = {
+                    "correlation_id": Utility.generate_correlation_id(),
+                    "error_code": resp.get("error", {}).get("code"),
+                    "error_type": resp.get("error", {}).get("type"),
+                    "recipient_id": Utility.mask_sensitive_data(recipient_id)
+                }
+                logger.error("WhatsApp message failed", extra=error_context)
+                
+                try:
+                    for attempt in range(3):  # Retry up to 3 times
+                        try:
+                            ChatDataProcessor.save_whatsapp_failed_messages(
+                                resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
+                                json_message=json_message
+                            )
+                            break
+                        except Exception as e:
+                            if attempt == 2:  # Last attempt
+                                logger.error(f"Failed to log WhatsApp error after 3 attempts",
+                                           extra={"correlation_id": error_context["correlation_id"],
+                                                 "error": str(e)})
+                            else:
+                                time.sleep(1 * (attempt + 1))  # Exponential backoff
+                except Exception as e:
+                    logger.error("Failed to log WhatsApp error",
+                               extra={"correlation_id": error_context["correlation_id"],
+                                     "error": str(e)})

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (6)
kairon/chat/handlers/channels/clients/whatsapp/cloud.py (1)

49-49: Add documentation and type hints for the metadata parameter.

Please update the following:

  1. Add metadata to the docstring's @optional section with its purpose
  2. Add type hints for consistency with other parameters

Apply this diff:

     def __init__(self, access_token, **kwargs):
         """
             @required:
                 access_token
             @optional:
                 session
                 api_version
                 app_secret
+                metadata: Additional context for message tracking and logging
         """
-        self.metadata = kwargs.get("metadata")
+        self.metadata: dict = kwargs.get("metadata")
kairon/chat/handlers/channels/whatsapp.py (1)

161-161: Add metadata validation

Consider adding validation before assigning metadata to prevent potential issues if metadata is None.

-        self.client.metadata = metadata
+        if metadata is not None:
+            self.client.metadata = metadata
+        else:
+            logger.warning("No metadata provided for WhatsApp client")
+            self.client.metadata = {}
tests/unit_test/chat/chat_test.py (4)

246-246: Remove debug print statement.

The print statement is not needed for the test and should be removed.

-        print(log)

201-270: Add test data cleanup.

Consider adding test data cleanup in a teardown method to ensure test isolation.

Add this cleanup code in a teardown method:

def teardown_method(self):
    from kairon.shared.chat.data_objects import ChannelLogs
    ChannelLogs.objects(bot="5e564fbcdcf0d5fad89e3abd").delete()

309-309: Remove unused variable.

The channel_type variable is assigned but never used.

-        channel_type = "whatsapp"
🧰 Tools
🪛 Ruff (0.8.0)

309-309: Local variable channel_type is assigned to but never used

Remove assignment to unused variable channel_type

(F841)


271-374: Add test data cleanup.

Consider adding test data cleanup in a teardown method to ensure test isolation.

Add this cleanup code in a teardown method:

def teardown_method(self):
    from kairon.shared.chat.data_objects import ChannelLogs
    ChannelLogs.objects(bot="5e564fbcdcf0d5fad89e3abcd").delete()
🧰 Tools
🪛 Ruff (0.8.0)

309-309: Local variable channel_type is assigned to but never used

Remove assignment to unused variable channel_type

(F841)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between dc867ca and 7974cc5.

📒 Files selected for processing (4)
  • kairon/chat/handlers/channels/clients/whatsapp/cloud.py (1 hunks)
  • kairon/chat/handlers/channels/whatsapp.py (2 hunks)
  • kairon/shared/chat/processor.py (1 hunks)
  • tests/unit_test/chat/chat_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • kairon/shared/chat/processor.py
🧰 Additional context used
🪛 Ruff (0.8.0)
tests/unit_test/chat/chat_test.py

309-309: Local variable channel_type is assigned to but never used

Remove assignment to unused variable channel_type

(F841)

🔇 Additional comments (3)
kairon/chat/handlers/channels/clients/whatsapp/cloud.py (1)

49-49: Verify if metadata parameter is necessary at this stage.

The metadata parameter is added but not utilized within this class. While the AI summary suggests it's for future use in WhatsappBot, consider:

  1. Whether it should be added only when the actual functionality is implemented
  2. If it's needed now, document its intended future use in comments

Let's check if this parameter is actually used in WhatsappBot:

✅ Verification successful

Metadata parameter is necessary and actively used

The metadata parameter is extensively used throughout the WhatsApp integration:

  • It's passed through multiple handler methods and updated with channel-specific information
  • Used for message processing, webhook handling, and error logging
  • Contains critical information like phone numbers, message IDs, and channel types
  • The client's metadata is accessed in WhatsappBot for sending messages and error handling
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for metadata usage in WhatsappBot
rg -l "metadata" kairon/chat/handlers/channels/whatsapp.py

Length of output: 100


Script:

#!/bin/bash
# Search for actual usage of metadata in WhatsappBot
rg -A 5 "metadata" kairon/chat/handlers/channels/whatsapp.py

Length of output: 4971

kairon/chat/handlers/channels/whatsapp.py (1)

242-243: Add type hints and response validation

This was previously suggested in past reviews and is still applicable. Add type hints and validate the response structure.

tests/unit_test/chat/chat_test.py (1)

201-374: Well-structured tests with good coverage!

The tests thoroughly verify the WhatsApp failed message logging functionality through both direct API testing and integration testing. They validate all required fields in the log entries and handle error scenarios appropriately.

🧰 Tools
🪛 Ruff (0.8.0)

309-309: Local variable channel_type is assigned to but never used

Remove assignment to unused variable channel_type

(F841)

Comment on lines +244 to +259
if resp.get("error"):
bot = kwargs.get("assistant_id")
message_id = self.whatsapp_client.metadata.get("id")
user = self.whatsapp_client.metadata.get("display_phone_number")
if not bot:
logger.error("Missing assistant_id in kwargs for failed message logging")
return
logger.error(f"WhatsApp message failed: {resp.get('error')}")
try:
ChatDataProcessor.save_whatsapp_failed_messages(
resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
json_message=json_message, message_id=message_id, user=user,
metadata=self.whatsapp_client.metadata
)
except Exception as e:
logger.error(f"Failed to log WhatsApp error: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling with retry mechanism

While the error handling is comprehensive, consider adding a retry mechanism for transient failures and handling specific error types differently.

-            if resp.get("error"):
+            max_retries = 3
+            retry_count = 0
+            while retry_count < max_retries:
+                if resp.get("error"):
+                    error_type = resp.get("error", {}).get("type")
+                    error_code = resp.get("error", {}).get("code")
+                    
+                    # Don't retry for permanent errors
+                    if error_type in ["permanent_error", "invalid_parameter"]:
+                        break
+                        
+                    if retry_count < max_retries - 1:
+                        retry_count += 1
+                        await asyncio.sleep(2 ** retry_count)  # Exponential backoff
+                        resp = self.whatsapp_client.send(response, recipient_id, messaging_type)
+                        continue
+                        
                 bot = kwargs.get("assistant_id")
                 message_id = self.whatsapp_client.metadata.get("id")
                 user = self.whatsapp_client.metadata.get("display_phone_number")
                 if not bot:
                     logger.error("Missing assistant_id in kwargs for failed message logging")
                     return
-                logger.error(f"WhatsApp message failed: {resp.get('error')}")
+                logger.error(
+                    "WhatsApp message failed",
+                    extra={
+                        "error": resp.get("error"),
+                        "retry_count": retry_count,
+                        "recipient_id": recipient_id,
+                        "message_type": messaging_type
+                    }
+                )
                 try:
                     ChatDataProcessor.save_whatsapp_failed_messages(
                         resp, bot, recipient_id, ChannelTypes.WHATSAPP.value,
                         json_message=json_message, message_id=message_id, user=user,
                         metadata=self.whatsapp_client.metadata
                     )
                 except Exception as e:
-                    logger.error(f"Failed to log WhatsApp error: {str(e)}")
+                    logger.error(
+                        "Failed to log WhatsApp error",
+                        extra={
+                            "error": str(e),
+                            "recipient_id": recipient_id,
+                            "message_type": messaging_type
+                        }
+                    )
+                break

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Collaborator

@hiteshghuge hiteshghuge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviewed

@hiteshghuge hiteshghuge merged commit 48554f8 into digiteinfotech:master Nov 27, 2024
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants