Skip to content

Conversation

@awanmh
Copy link

@awanmh awanmh commented Jan 30, 2026

Description

This PR resolves an issue where the API fails to parse the context of Facebook/Instagram Click-to-WhatsApp ads (externalAdReply). Previously, if a user clicked an ad but sent only the default pre-filled message, the API would treat it as an empty or unknown message type because the ad payload resides in contextInfo, not the main text body.

Changes

  • Modified src/utils/getConversationMessage.ts.
  • Added logic to detect externalAdReply in msg.message.extendedTextMessage.contextInfo.
  • Extracted body (or title as fallback) from the ad payload.
  • Appended the ad context to the user message string using a helper function to ensure downstream controllers receive a standard string format.

Testing

  • Verified with mocked payloads containing externalAdReply.
  • Validated that base-chatbot and typebot services receive the concatenated string correctly.

Summary by Sourcery

Handle Facebook/Instagram Click-to-WhatsApp ad messages by correctly extracting and appending external ad reply context to the conversation message.

Bug Fixes:

  • Avoid runtime errors when location messages lack latitude by safely stringifying the latitude value.
  • Treat externalAdReply context from extended text messages as part of the user message, falling back to the ad title when the body is missing, and exclude this metadata from primary message type detection.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 30, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adjusts WhatsApp message parsing to correctly extract and append Facebook/Instagram Click-to-WhatsApp ad context from externalAdReply in extendedTextMessage, while making location parsing safer and excluding helper-only fields from type detection.

Sequence diagram for parsing externalAdReply Facebook ad context

sequenceDiagram
  participant WhatsAppAPI
  participant WebhookHandler
  participant getConversationMessage
  participant getTypeMessage
  participant getMessageContent
  participant BaseChatbotService
  participant TypebotService

  WhatsAppAPI->>WebhookHandler: incoming msg (extendedTextMessage.contextInfo.externalAdReply)
  WebhookHandler->>getConversationMessage: msg
  getConversationMessage->>getTypeMessage: msg
  getTypeMessage->>getTypeMessage: extract conversation, extendedTextMessage, etc.
  getTypeMessage->>getTypeMessage: read locationMessage using degreesLatitude?.toString()
  getTypeMessage->>getTypeMessage: read externalAdReply from msg.message.extendedTextMessage.contextInfo
  alt externalAdReply.body defined
    getTypeMessage-->>getConversationMessage: types.externalAdReplyBody = externalAdReplyBody|body
  else externalAdReply.title defined
    getTypeMessage-->>getConversationMessage: types.externalAdReplyBody = externalAdReplyBody|title
  else no externalAdReply
    getTypeMessage-->>getConversationMessage: types.externalAdReplyBody = undefined
  end

  getConversationMessage->>getMessageContent: types
  getMessageContent->>getMessageContent: select typeKey excluding externalAdReplyBody and messageType
  getMessageContent-->>getConversationMessage: messageContent (possibly with externalAdReplyBody appended)

  getConversationMessage-->>WebhookHandler: final message string
  WebhookHandler-->>BaseChatbotService: user message with ad context
  WebhookHandler-->>TypebotService: user message with ad context
Loading

File-Level Changes

Change Details Files
Handle Facebook/Instagram Click-to-WhatsApp ad context (externalAdReply) embedded in extendedTextMessage.contextInfo and expose it as a normalized string.
  • Switch externalAdReply lookup from root contextInfo to message.extendedTextMessage.contextInfo.externalAdReply to match actual payload structure.
  • Extract body from externalAdReply when present and prefix with a discriminator string before returning it.
  • Fallback to using title from externalAdReply when body is absent, keeping the same normalized prefix format.
  • Exclude externalAdReplyBody from the primary message type detection while still allowing it to be appended to the final message string.
src/utils/getConversationMessage.ts
Refine message type detection and content extraction helper to be robust and ignore metadata-only fields.
  • Update getMessageContent to ignore both externalAdReplyBody and messageType keys when selecting the main message payload field.
  • Preserve and slightly refactor getConversationMessage to simply compose the types map and resulting message content without extra whitespace changes.
src/utils/getConversationMessage.ts
Make location message parsing resilient to missing latitude data.
  • Guard degreesLatitude.toString() with optional chaining to avoid runtime errors when latitude is undefined in locationMessage payloads.
src/utils/getConversationMessage.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The new externalAdReplyBody logic now only checks msg.message.extendedTextMessage.contextInfo, which may drop support for payloads that previously used msg.contextInfo.externalAdReply; consider preserving the old path as a fallback.
  • The repeated access chain msg.message.extendedTextMessage.contextInfo.externalAdReply could be simplified via optional chaining with a local variable (e.g. const adReply = msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply) to improve readability and reduce duplication.
  • The string key 'externalAdReplyBody' is now hard-coded both when building types and when filtering keys in getMessageContent; consider extracting this into a shared constant to avoid drift if the key name changes later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `externalAdReplyBody` logic now only checks `msg.message.extendedTextMessage.contextInfo`, which may drop support for payloads that previously used `msg.contextInfo.externalAdReply`; consider preserving the old path as a fallback.
- The repeated access chain `msg.message.extendedTextMessage.contextInfo.externalAdReply` could be simplified via optional chaining with a local variable (e.g. `const adReply = msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply`) to improve readability and reduce duplication.
- The string key `'externalAdReplyBody'` is now hard-coded both when building `types` and when filtering keys in `getMessageContent`; consider extracting this into a shared constant to avoid drift if the key name changes later.

## Individual Comments

### Comment 1
<location> `src/utils/getConversationMessage.ts:54-56` </location>
<code_context>
-      ? `externalAdReplyBody|${msg.contextInfo.externalAdReply.body}`
+    
+    // --- FIX FACEBOOK ADS START ---
+    externalAdReplyBody: msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.body
+      ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.body}`
+      : msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.title
+      ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.title}`
       : undefined,
</code_context>

<issue_to_address>
**issue (bug_risk):** Narrowing `externalAdReplyBody` to only `extendedTextMessage.contextInfo` may regress other message types that used the top-level `contextInfo`.

The previous code read from `msg?.contextInfo?.externalAdReply`, which covers cases where `contextInfo` is attached at the top level or under non–extended text messages. The new version only checks `msg.message.extendedTextMessage.contextInfo`, so those other shapes may lose ad text. Consider using the new path first and falling back to the old one, for example:

```ts
externalAdReplyBody:
  msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.body
    ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.body}`
    : msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.title
    ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.title}`
    : msg?.contextInfo?.externalAdReply?.body
    ? `externalAdReplyBody|${msg.contextInfo.externalAdReply.body}`
    : msg?.contextInfo?.externalAdReply?.title
    ? `externalAdReplyBody|${msg.contextInfo.externalAdReply.title}`
    : undefined,
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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.

1 participant