Skip to content

feat(whatsapp): add analytics v2 event capture - #420

Merged
SAHEED2010 merged 4 commits into
devfrom
feat/whatsapp-analytics-v2-event-capture
Jun 10, 2026
Merged

feat(whatsapp): add analytics v2 event capture#420
SAHEED2010 merged 4 commits into
devfrom
feat/whatsapp-analytics-v2-event-capture

Conversation

@SAHEED2010

@SAHEED2010 SAHEED2010 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds durable WhatsApp analytics v2 event capture for inbound shopper messages. The patch expands the existing WhatsAppAnalytics schema, records best-effort structured rows from consent, linking, image search, product search, product selection, support, fallback, and error paths, and keeps message handling resilient if analytics persistence fails.

Type of change

  • New feature
  • Bug fix
  • Refactor / cleanup
  • Database migration included
  • Chore / maintenance
  • Documentation

Area affected

  • Backend
  • Web
  • WhatsApp
  • Shared package
  • Database / Prisma
  • GitHub / CI / infrastructure

How to test this

  1. cd apps/backend
  2. npx.cmd jest src/channels/whatsapp --runInBand
  3. pnpm.cmd run lint
  4. npx.cmd tsc --noEmit
  5. pnpm.cmd run build
  6. npx.cmd prisma validate --schema=prisma/schema.prisma
  7. npx.cmd prisma generate
  8. git diff --check && git diff --cached --check
    Expected result: WhatsApp tests, lint, typecheck, Prisma validation/generation, and backend build all pass. Analytics writes are best-effort and do not block replies.

Pre-commit checklist

  • Backend lint/type/build pass when backend is affected
  • Web lint/type/build pass when web is affected
  • Shared package build passes when shared is affected
  • No console.log left in production code
  • No secrets or .env files committed
  • No new any types added
  • No non-MVP legacy features reintroduced
  • All money values are BigInt kobo, never float
  • Paystack webhook changes verify HMAC before processing
  • Database migrations are Prisma migrations, not db push
  • Database migrations are backward-compatible or risk is documented

Screenshots

N/A - backend analytics capture only.

Notes for reviewer

Migration scope is limited to whatsapp_analytics: v2 analytics columns, requested indexes, and an optional user_id FK. It does not alter WIZZA reply behavior, search ranking, account linking rules, prompts, checkout, payments, orders, delivery, web, or shared package behavior. Prisma migrate dev could not be used locally because the shared dev database reported pre-existing drift in older migrations, so the migration SQL was created manually and inspected.

Summary by CodeRabbit

  • New Features

    • End-to-end WhatsApp analytics: richer event details for searches, image-based searches, product selection, intent outcomes, escalations, and model/timing metadata.
    • Services now return structured analytics objects and record analytics reliably after message processing.
    • Conversation-turn tracking and exported analytics service integrated into the WhatsApp module.
  • Database

    • Analytics schema expanded to persist the new WhatsApp analytics fields and optimized with new indexes.
  • Tests

    • Added and updated tests covering analytics recording, session/consent flows, image search, and product discovery.

@codesandbox

codesandbox Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
twizrr-web Ready Ready Preview, Comment Jun 10, 2026 2:40pm

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e2f0ad4-04aa-420e-890c-249bc5c53545

📥 Commits

Reviewing files that changed from the base of the PR and between 5d916ec and c8dd354.

📒 Files selected for processing (6)
  • apps/backend/src/channels/whatsapp/image-search.service.spec.ts
  • apps/backend/src/channels/whatsapp/image-search.service.ts
  • apps/backend/src/channels/whatsapp/whatsapp-analytics.service.spec.ts
  • apps/backend/src/channels/whatsapp/whatsapp-analytics.service.ts
  • apps/backend/src/channels/whatsapp/whatsapp-session.service.spec.ts
  • apps/backend/src/channels/whatsapp/whatsapp-session.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/backend/src/channels/whatsapp/whatsapp-analytics.service.spec.ts
  • apps/backend/src/channels/whatsapp/whatsapp-analytics.service.ts

📝 Walkthrough

Walkthrough

Adds persistent WhatsApp analytics fields and indexes, a NestJS WhatsAppAnalyticsService (Redis turn counting + Prisma fallback), returns structured analytics from product/image flows, upserts session state with IDs, and instruments WhatsAppService to record analytics best-effort after processing.

Changes

WhatsApp Analytics End-to-End

Layer / File(s) Summary
Database Schema & Persistence Foundation
apps/backend/prisma/migrations/..., apps/backend/prisma/schema.prisma
whatsapp_analytics table extended with phone, user linkage, conversationTurn, intent/flow/search classification, product/media counts, escalation/error/Gemini metadata; backfills products_shown_count; adds indexes and FK to users. User gains whatsappAnalytics relation.
WhatsApp Analytics Service
apps/backend/src/channels/whatsapp/whatsapp-analytics.service.ts, whatsapp-analytics.service.spec.ts
New WhatsAppAnalyticsService injectable with recordMessage and nextConversationTurn; persists analytics to Prisma, normalizes Prisma-null shapes, computes productsShownCount, and uses Redis INCR+EXPIRE with Prisma count fallback and masked-phone warning logs.
Session State with Persistence
apps/backend/src/channels/whatsapp/whatsapp-session.service.ts, whatsapp-session.service.spec.ts
WhatsAppConsentState adds id. recordInbound and consent flows use a shared upsertSessionState performing whatsAppSession.upsert keyed by phone and returning normalized session state including persisted id.
Product Discovery & Selection Analytics
apps/backend/src/channels/whatsapp/whatsapp-product-discovery.service.ts, whatsapp-product-discovery.service.spec.ts
Adds WhatsAppProductSearchAnalytics and WhatsAppProductSelectionAnalytics. sendGenericProductSearch and sendProductSelectionFromRecentResults return structured analytics (counts, shown IDs, zeroResults, intentSuccessful, errorType, dropOffStep). sendProductDetails returns boolean success.
Image Search Analytics
apps/backend/src/channels/whatsapp/image-search.service.ts, image-search.service.spec.ts
handleImageSearch returns WhatsAppImageSearchAnalytics; all early-exit/error/success branches return structured analytics. Adds emptyImageAnalytics helper for standardized failure/drop-off payloads.
End-to-End Message Processing & Analytics Recording
apps/backend/src/channels/whatsapp/whatsapp.service.ts, whatsapp.service.spec.ts, whatsapp-shared.module.ts
WhatsAppService.processMessage injects WhatsAppAnalyticsService, builds an analytics context after session recording, updates it across consent/account-linking/image/product/intent branches, and records analytics best-effort in a finally block. Module wiring registers and exports the analytics service.

Sequence Diagram

sequenceDiagram
  participant User
  participant Webhook
  participant WhatsAppService
  participant SessionService
  participant ProductDiscovery
  participant ImageSearch
  participant AnalyticsService
  participant Redis
  participant Prisma
  User->>Webhook: inbound message
  Webhook->>WhatsAppService: processMessage(phone, input)
  WhatsAppService->>SessionService: recordInbound(phone)
  SessionService->>Prisma: whatsAppSession.upsert()
  Prisma-->>SessionService: session record (id)
  SessionService-->>WhatsAppService: consent state + id
  alt product search
    WhatsAppService->>ProductDiscovery: sendGenericProductSearch(query)
    ProductDiscovery->>Prisma: search/filter products
    Prisma-->>ProductDiscovery: results
    ProductDiscovery-->>WhatsAppService: WhatsAppProductSearchAnalytics
  else image search
    WhatsAppService->>ImageSearch: handleImageSearch(imageId)
    ImageSearch->>Prisma: match products
    Prisma-->>ImageSearch: results
    ImageSearch-->>WhatsAppService: WhatsAppImageSearchAnalytics
  end
  WhatsAppService->>AnalyticsService: recordMessage(analytics) [finally]
  AnalyticsService->>Redis: INCR(phone_turn_key)
  alt Redis ok
    Redis-->>AnalyticsService: turn
    AnalyticsService->>Prisma: whatsAppAnalytics.create()
  else Redis fails
    AnalyticsService->>Prisma: whatsAppAnalytics.count(phone)
    Prisma-->>AnalyticsService: count
    AnalyticsService->>Prisma: whatsAppAnalytics.create()
  end
  Prisma-->>AnalyticsService: created
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • coded-devs/twizrr#372: Related changes to image-search flow and product formatting that overlap image-search analytics paths.
  • coded-devs/twizrr#414: Changes product selection/recent-results handling that intersect the product-selection analytics updates.
  • coded-devs/twizrr#408: Modifies intent routing in whatsapp.service.ts where analytics instrumentation was added.

Suggested reviewers

  • onerandomdevv

"🐰 I hopped through logs and schema rows,
I counted turns where conversation goes,
Images parsed and products shown,
Errors tagged before they're known,
I munch on metrics, sniff the trace — analytics in their proper place!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding WhatsApp analytics v2 event capture. It is concise, directly related to the primary changeset objective, and avoids vague terminology.
Description check ✅ Passed The description covers all required template sections with substantial detail: clear problem statement, appropriate type/area selections, comprehensive testing instructions, pre-commit checklist completion, and relevant notes explaining migration rationale and design decisions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/whatsapp-analytics-v2-event-capture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/backend/src/channels/whatsapp/whatsapp-product-discovery.service.ts (1)

262-273: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Differentiate invalid numeric picks from ambiguous matches.

If the shopper replies with an out-of-range number, this branch sends the ambiguous-match copy and records AMBIGUOUS_PRODUCT_SELECTION, even though there was no matching row at all. That will mislead the user and skew the new selection-failure analytics.

Suggested fix
+const INVALID_SELECTION_MESSAGE =
+  "I couldn't find that number in the recent results. Please choose one of the listed options.";
+
       const selected = results.find((result) => result.rank === indexReference);
       if (!selected) {
         await this.interactiveService.sendTextMessage(
           phone,
-          AMBIGUOUS_SELECTION_MESSAGE,
+          INVALID_SELECTION_MESSAGE,
         );
         return {
           handled: true,
           intentSuccessful: false,
-          errorType: "AMBIGUOUS_PRODUCT_SELECTION",
-          dropOffStep: "product_selection_ambiguous",
+          errorType: "INVALID_PRODUCT_SELECTION",
+          dropOffStep: "product_selection_invalid",
         };
       }
apps/backend/src/channels/whatsapp/whatsapp.service.ts (1)

62-110: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Paused/resumed messages still bypass the new analytics flow.

processMessage can return from the Redis pause branch before recordInbound, analytics initialization, and the finally block run, so those inbound WhatsApp messages never get a whatsapp_analytics row. That leaves a durable-capture gap for an entire production branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/channels/whatsapp/whatsapp.service.ts` around lines 62 -
110, processMessage currently returns early from the Redis "paused" branch
before recordInbound runs and analytics is initialized, so paused inbound
messages never reach the finally block and no whatsapp_analytics row is created;
fix by ensuring recordInbound and analytics initialization always run before any
early return: either move the pause check to after obtaining
normalizedPhone/userId/session or, if you must keep the pause check early, call
this.whatsappSessionService.recordInbound(...) and populate the analytics object
(same shape as the existing block) before returning, so the finally block can
persist analytics; reference processMessage, redisService.get(pauseKey),
recordInbound, analytics, and the finally block when making the change.
🧹 Nitpick comments (2)
apps/backend/src/channels/whatsapp/whatsapp-session.service.spec.ts (1)

95-103: ⚡ Quick win

The upsert mock never exercises the update payload.

This mock always echoes create, so the cached-inbound tests cannot catch regressions in the real update branch. Please assert the upsert(...).update object, or model an existing session separately, so null-clobbering bugs on userId and consentAt fail here.

Also applies to: 129-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/channels/whatsapp/whatsapp-session.service.spec.ts` around
lines 95 - 103, The upsert mock in the test for prisma.whatsAppSession.upsert
currently always returns the create payload so the update branch (and potential
null-clobbering of fields like userId and consentAt) is never exercised; update
the test to either assert the shape of the upsert call's update argument or
change the mockImplementation to detect when an update is intended and return an
object that merges existing session values with the provided update (e.g., read
args.update and args.create from UpsertSessionArgs and return a merged result),
and apply the same change to the other similar mocks referenced around lines
129-170 so cached-inbound tests fail on regressions.
apps/backend/src/channels/whatsapp/whatsapp.service.spec.ts (1)

54-91: ⚡ Quick win

Use a full session fixture now that analytics reads session.id.

Most tests still stub recordInbound with { isConsentGiven: ... }, but processMessage now always reads session.id and sometimes session.userId before recording analytics. A shared makeSession() helper with id, userId, and isConsentGiven would keep these tests representative and avoid false positives when the session contract changes again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/channels/whatsapp/whatsapp.service.spec.ts` around lines 54
- 91, Tests currently stub analytics inputs with only isConsentGiven, but
processMessage (in WhatsAppService) and whatsappAnalyticsService.recordMessage
now read session.id and session.userId; create a shared makeSession() fixture
that returns { id, userId, isConsentGiven } and update all test stubs (where
recordInbound/analytics/session is mocked) to use that full session object so
tests supply id and userId consistently; update any uses in the spec that call
whatsappAnalyticsService.recordMessage or invoke WhatsAppService.processMessage
to pass or return the full session from whatsappSessionService.normalizePhone /
session mocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/channels/whatsapp/image-search.service.ts`:
- Around line 268-278: The helper emptyImageAnalytics currently always sets
zeroResults: true which causes transport/vision/download/term-extraction
failures to be recorded as zero-result searches; change
emptyImageAnalytics(errorType: string, zeroResults = false) to accept an
explicit zeroResults boolean (default false), set zeroResults to that parameter,
and update all callers (e.g., the code paths that handle download failures,
vision/term-extraction failures, and the catch-all error path) to pass false for
actual failures and only pass true when you legitimately want to record a
zero-result image search; keep errorType populated so analytics can distinguish
failure vs. genuine zero-result searches.

In `@apps/backend/src/channels/whatsapp/whatsapp-analytics.service.ts`:
- Around line 99-114: The current try/catch around both
this.redisService.incr(key) and expire() discards a successfully allocated Redis
turn when expire() fails; change the flow so incr is awaited first and its
returned turn is kept, then call expire() in a separate try/catch: call const
turn = await this.redisService.incr(key); then try { await
this.redisService.expire(key, WA_ANALYTICS_TURN_TTL); } catch (error) {
this.logger.warn(`... ${error instanceof Error ? error.message : 'unknown
error'}`); } and finally return turn; only fall back to
this.prisma.whatsAppAnalytics.count({ where: { phone } }) + 1 if incr itself
throws (i.e., catch only the incr failure).

In `@apps/backend/src/channels/whatsapp/whatsapp-session.service.ts`:
- Around line 45-50: The upsertSessionState call sites (e.g., the call at
whatsapp-session.service.ts lines shown) pass null for userId/consent values
which causes upsertSessionState to overwrite existing whatsAppSession.userId and
whatsAppSession.consentAt; change upsertSessionState (or its callers) so that
when userId or consent values are null it preserves the existing whatsAppSession
values instead of writing null: inside upsertSessionState, fetch the existing
whatsAppSession by normalizedPhone (or use an existing loader like
getSession/state lookup), and only overwrite userId and consentAt when the
incoming parameters are non-null/defined—otherwise reuse
existing.whatsAppSession.userId and existing.whatsAppSession.consentAt—then
persist the merged session; apply the same preservation logic for all other call
sites that currently pass null.

---

Outside diff comments:
In `@apps/backend/src/channels/whatsapp/whatsapp.service.ts`:
- Around line 62-110: processMessage currently returns early from the Redis
"paused" branch before recordInbound runs and analytics is initialized, so
paused inbound messages never reach the finally block and no whatsapp_analytics
row is created; fix by ensuring recordInbound and analytics initialization
always run before any early return: either move the pause check to after
obtaining normalizedPhone/userId/session or, if you must keep the pause check
early, call this.whatsappSessionService.recordInbound(...) and populate the
analytics object (same shape as the existing block) before returning, so the
finally block can persist analytics; reference processMessage,
redisService.get(pauseKey), recordInbound, analytics, and the finally block when
making the change.

---

Nitpick comments:
In `@apps/backend/src/channels/whatsapp/whatsapp-session.service.spec.ts`:
- Around line 95-103: The upsert mock in the test for
prisma.whatsAppSession.upsert currently always returns the create payload so the
update branch (and potential null-clobbering of fields like userId and
consentAt) is never exercised; update the test to either assert the shape of the
upsert call's update argument or change the mockImplementation to detect when an
update is intended and return an object that merges existing session values with
the provided update (e.g., read args.update and args.create from
UpsertSessionArgs and return a merged result), and apply the same change to the
other similar mocks referenced around lines 129-170 so cached-inbound tests fail
on regressions.

In `@apps/backend/src/channels/whatsapp/whatsapp.service.spec.ts`:
- Around line 54-91: Tests currently stub analytics inputs with only
isConsentGiven, but processMessage (in WhatsAppService) and
whatsappAnalyticsService.recordMessage now read session.id and session.userId;
create a shared makeSession() fixture that returns { id, userId, isConsentGiven
} and update all test stubs (where recordInbound/analytics/session is mocked) to
use that full session object so tests supply id and userId consistently; update
any uses in the spec that call whatsappAnalyticsService.recordMessage or invoke
WhatsAppService.processMessage to pass or return the full session from
whatsappSessionService.normalizePhone / session mocks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 79108276-76db-4096-9b48-ed5aff581b45

📥 Commits

Reviewing files that changed from the base of the PR and between b7c2428 and 5d916ec.

📒 Files selected for processing (12)
  • apps/backend/prisma/migrations/20260611120000_add_whatsapp_analytics_v2_fields/migration.sql
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/channels/whatsapp/image-search.service.ts
  • apps/backend/src/channels/whatsapp/whatsapp-analytics.service.spec.ts
  • apps/backend/src/channels/whatsapp/whatsapp-analytics.service.ts
  • apps/backend/src/channels/whatsapp/whatsapp-product-discovery.service.spec.ts
  • apps/backend/src/channels/whatsapp/whatsapp-product-discovery.service.ts
  • apps/backend/src/channels/whatsapp/whatsapp-session.service.spec.ts
  • apps/backend/src/channels/whatsapp/whatsapp-session.service.ts
  • apps/backend/src/channels/whatsapp/whatsapp-shared.module.ts
  • apps/backend/src/channels/whatsapp/whatsapp.service.spec.ts
  • apps/backend/src/channels/whatsapp/whatsapp.service.ts

Comment thread apps/backend/src/channels/whatsapp/image-search.service.ts
Comment thread apps/backend/src/channels/whatsapp/whatsapp-analytics.service.ts
Comment thread apps/backend/src/channels/whatsapp/whatsapp-session.service.ts Outdated
@SAHEED2010
SAHEED2010 merged commit 783c0d3 into dev Jun 10, 2026
10 checks passed
@onerandomdevv
onerandomdevv deleted the feat/whatsapp-analytics-v2-event-capture branch June 11, 2026 04:29
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