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 all 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
1 change: 1 addition & 0 deletions kairon/chat/handlers/channels/clients/whatsapp/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self, access_token, **kwargs):
self.api_version = kwargs.get('api_version', DEFAULT_API_VERSION)
self.app = 'https://graph.facebook.com/v{api_version}'.format(api_version=self.api_version)
self.app_secret = kwargs.get('app_secret')
self.metadata = kwargs.get("metadata")

@property
def client_type(self):
Expand Down
20 changes: 19 additions & 1 deletion kairon/chat/handlers/channels/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ async def _handle_user_message(
) -> None:
"""Pass on the text to the dialogue engine for processing."""
out_channel = WhatsappBot(self.client)
self.client.metadata = metadata
await out_channel.mark_as_read(metadata["id"])
user_msg = UserMessage(
text, out_channel, sender_id, input_channel=self.name(), metadata=metadata
Expand Down Expand Up @@ -238,7 +239,24 @@ 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")
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)}")
Comment on lines +244 to +259
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.

else:
self.send(recipient_id, {"preview_url": True, "body": str(json_message)})

Expand Down
35 changes: 35 additions & 0 deletions kairon/shared/chat/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,41 @@ 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 = kwargs.get("message_id")
user = kwargs.get("user")
json_message = kwargs.get('json_message')
metadata = kwargs.get("metadata")
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=json_message,
metadata=metadata
).save()

@staticmethod
def get_instagram_static_comment(bot: str) -> str:
channel = ChatDataProcessor.get_channel_config(bot=bot, connector_type="instagram", mask_characters=False)
Expand Down
174 changes: 174 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,180 @@ 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"
message_id = "wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA"
user = "91865712345"
ChatDataProcessor.save_whatsapp_failed_messages(resp, bot, recipient, channel_type, json_message=json_message,
message_id=message_id, user=user)
log = (
ChannelLogs.objects(
bot=bot,
message_id="wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA",
)
.get()
.to_mongo()
.to_dict()
)
print(log)
assert log['type'] == 'whatsapp'
assert log['data'] == resp
assert log['status'] == 'failed'
assert log['message_id'] == 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA'
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'] == '91865712345'
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"
user = "91865712345"
channel_type = "whatsapp"
metadata = {
'from': '919876543210',
'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA',
'timestamp': '1732622052',
'text': {'body': '/btn'},
'type': 'text',
'is_integration_user': True,
'bot': bot,
'account': 8,
'channel_type': 'whatsapp',
'bsp_type': '360dialog',
'tabname': 'default',
'display_phone_number': '91865712345',
'phone_number_id': '142427035629239'
}
client = BSP360Dialog("asajjdkjskdkdjfk", metadata=metadata)
await WhatsappBot(client).send_custom_json(recipient, json_message, assistant_id=bot)
log = (
ChannelLogs.objects(
bot=bot,
message_id="wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA",
)
.get()
.to_mongo()
.to_dict()
)
assert log['type'] == 'whatsapp'
assert log['data'] == resp
assert log['status'] == 'failed'
assert log['message_id'] == 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA'
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'] == '91865712345'
assert log['json_message'] == {
'data': [
{
'type': 'button',
'value': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'id': 1,
'children': [
{
'text': '/byeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
}
]
}
],
'type': 'button'
}
assert log['metadata'] == {
'from': recipient,
'id': 'wamid.HBgMOTE5NTE1OTkxNjg1FQIAEhggNjdDMUMxODhENEMyQUM1QzVBREQzN0YxQjQyNzA4MzAA',
'timestamp': '1732622052',
'text': {'body': '/btn'},
'type': 'text',
'is_integration_user': True,
'bot': bot,
'account': 8,
'channel_type': 'whatsapp',
'bsp_type': '360dialog',
'tabname': 'default',
'display_phone_number': user,
'phone_number_id': '142427035629239'
}

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