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
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion kairon/chat/handlers/channels/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,21 @@ async def send_custom_json(
from kairon.chat.converters.channels.response_factory import ConverterFactory
converter_instance = ConverterFactory.getConcreteInstance(messagetype, ChannelTypes.WHATSAPP.value)
response = await converter_instance.messageConverter(message)
self.whatsapp_client.send(response, recipient_id, messaging_type)
resp = self.whatsapp_client.send(response, recipient_id, messaging_type)

Comment on lines +242 to +243
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

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.

logger.error(f"Failed to log WhatsApp error: {str(e)}")
else:
self.send(recipient_id, {"preview_url": True, "body": str(json_message)})

Expand Down
32 changes: 32 additions & 0 deletions kairon/shared/chat/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,38 @@ def save_whatsapp_audit_log(status_data: Dict, bot: Text, user: Text, recipient:
campaign_id=campaign_id
).save()

@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).

Returns:
None
"""
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


@staticmethod
def get_instagram_static_comment(bot: str) -> str:
channel = ChatDataProcessor.get_channel_config(bot=bot, connector_type="instagram", mask_characters=False)
Expand Down
140 changes: 140 additions & 0 deletions tests/unit_test/chat/chat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,146 @@ def __mock_get_bot(*args, **kwargs):
"client_secret": "a23456789sfdghhtyutryuivcbn", "is_primary": False}}, "test", "test"
)

def test_save_whatsapp_failed_messages(self):
from kairon.shared.chat.data_objects import ChannelLogs
json_message = {
'data': [
{
'type': 'button',
'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'id': 1,
'children': [
{
'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
}
]
}
],
'type': 'button'
}

resp = {
'error': {
'message': '(#131009) Parameter value is not valid', 'type': 'OAuthException',
'code': 131009,
'error_data': {
'messaging_product': 'whatsapp',
'details': 'Button title length invalid. Min length: 1, Max length: 20'
},
'fbtrace_id': 'ALfhCiNWtld8enTUJyg0FFo'
}
}
bot = "5e564fbcdcf0d5fad89e3abd"
recipient = "919876543210"
channel_type = "whatsapp"
ChatDataProcessor.save_whatsapp_failed_messages(resp, bot, recipient, channel_type, json_message=json_message)
log = (
ChannelLogs.objects(
bot=bot,
message_id="failed-whatsapp-919876543210-5e564fbcdcf0d5fad89e3abd",
)
.get()
.to_mongo()
.to_dict()
)
print(log)
assert log['type'] == 'whatsapp'
assert log['data'] == resp
assert log['status'] == 'failed'
assert log['message_id'] == 'failed-whatsapp-919876543210-5e564fbcdcf0d5fad89e3abd'
assert log['failure_reason'] == 'Button title length invalid. Min length: 1, Max length: 20'
assert log['recipient'] == '919876543210'
assert log['type'] == 'whatsapp'
assert log['user'] == 'unknown_user'
assert log['json_message'] == {
'data': [
{
'type': 'button',
'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'id': 1,
'children': [
{
'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
}
]
}
],
'type': 'button'
}

@pytest.mark.asyncio
@mock.patch("kairon.chat.handlers.channels.clients.whatsapp.dialog360.BSP360Dialog.send", autospec=True)
async def test_save_whatsapp_failed_messages_through_send_custom_json(self, mock_bsp):
from kairon.shared.chat.data_objects import ChannelLogs
from kairon.chat.handlers.channels.whatsapp import WhatsappBot
from kairon.chat.handlers.channels.clients.whatsapp.dialog360 import BSP360Dialog

json_message = {
'data': [
{
'type': 'button',
'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'id': 1,
'children': [
{
'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
}
]
}
],
'type': 'button'
}

resp = {
'error': {
'message': '(#131009) Parameter value is not valid', 'type': 'OAuthException',
'code': 131009,
'error_data': {
'messaging_product': 'whatsapp',
'details': 'Button title length invalid. Min length: 1, Max length: 20'
},
'fbtrace_id': 'ALfhCiNWtld8enTUJyg0FFo'
}
}
mock_bsp.return_value = resp
bot = "5e564fbcdcf0d5fad89e3abcd"
recipient = "919876543210"
channel_type = "whatsapp"
client = BSP360Dialog("asajjdkjskdkdjfk")
await WhatsappBot(client).send_custom_json(recipient, json_message, assistant_id=bot)
log = (
ChannelLogs.objects(
bot=bot,
message_id="failed-whatsapp-919876543210-5e564fbcdcf0d5fad89e3abcd",
)
.get()
.to_mongo()
.to_dict()
)
assert log['type'] == 'whatsapp'
assert log['data'] == resp
assert log['status'] == 'failed'
assert log['message_id'] == 'failed-whatsapp-919876543210-5e564fbcdcf0d5fad89e3abcd'
assert log['failure_reason'] == 'Button title length invalid. Min length: 1, Max length: 20'
assert log['recipient'] == '919876543210'
assert log['type'] == 'whatsapp'
assert log['user'] == 'unknown_user'
assert log['json_message'] == {
'data': [
{
'type': 'button',
'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'id': 1,
'children': [
{
'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
}
]
}
],
'type': 'button'
}

def test_list_channels(self):
channels = list(ChatDataProcessor.list_channel_config("test"))
assert channels.__len__() == 3
Expand Down
Loading