Skip to content

feat(stats): add chat names to top chats and stats endpoints#558

Closed
buluma wants to merge 2 commits into
rmyndharis:mainfrom
buluma:feat/chat-name-in-stats
Closed

feat(stats): add chat names to top chats and stats endpoints#558
buluma wants to merge 2 commits into
rmyndharis:mainfrom
buluma:feat/chat-name-in-stats

Conversation

@buluma

@buluma buluma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

Store the human-readable chat name (contact pushName/saved name) on each message at save time, and surface it as chatName in the GET /stats/messages and GET /sessions/:id/stats topChats response. The dashboard uses it to display readable names instead of raw JIDs.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Checklist

  • Tests added/updated (1682/1682 pass)
  • Documentation updated (CHANGELOG)
  • Lint passes
  • Self-reviewed

Screenshots (if applicable)

Before: Top Chats showed raw JIDs like 254797747792@c.us, 131207224393887@lid
After: Shows readable names like contact names and group names

Related Issues

None

Store the human-readable chat name (contact pushName/saved name) on
each message at save time, and surface it as `chatName` in the
`GET /stats/messages` and `GET /sessions/:id/stats` topChats response.
The dashboard uses it to display readable names instead of raw JIDs.

- New `chatName` nullable column on the `messages` table
- Populated on incoming messages from the sender's contact info
- Returns `chatName` alongside `chatId` in both stats endpoints
- Dashboard displays `chatName` when available, falls back to `shortChat`
- TypeORM migration (1781900000000) for production / non-sync deployments

@rmyndharis rmyndharis left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for this, @buluma — readable names in the top-chats view is a genuinely useful addition, and the migration / entity / dashboard wiring is all in the right places. Before it can land there's one correctness issue in the aggregation that needs fixing, plus a few CHANGELOG/doc corrections.

Blocking

1. GROUP BY m.chatName fragments the top-chats aggregation.
Adding .addGroupBy('m.chatName') to getMessageStats and getSessionStats changes the grouping grain from one row per chat to one row per (chatId, chatName). Because chatName is not constant for a given chat, a single chat splits into several rows:

  • the COUNT(*) total is divided across those rows, so the counts are wrong;
  • .limit(10) then counts rows rather than chats, so the same chat can appear several times and busier chats drop off the list.

This is the normal path, not an edge case:

  • outbound messages are persisted by saveOutgoingMessage (message.service.ts), which does not set chatName, so every 1:1 chat ends up with named inbound rows and NULL outbound rows → two buckets;
  • legacy rows are NULL → another bucket;
  • in groups chatName is the sending participant, so each member becomes its own bucket.

It reproduces on the default SQLite dev DB. Suggested fix — keep the grain at one row per chat and pick a representative name with an aggregate instead of grouping by it:

.addSelect('MAX(m.chatName)', 'chatName')
.groupBy('m.chatId')            // drop both .addGroupBy('m.chatName')

MAX() ignores NULLs and works on both SQLite and Postgres.

Worth addressing here

2. For group chats chatName is the sender's name, not the group's. incoming.contact is the message sender (the whatsapp-web.js adapter notes "getContact() gives the real sender (author in groups)"), so a group's stored name is whoever last spoke. Real group names would need the group subject resolved for @g.us JIDs before persisting; otherwise it's worth scoping the feature to 1:1 chats.

3. Outbound-only chats stay unnamed. Only the inbound path sets chatName, so a chat you only ever send to keeps NULL and renders as a raw JID. Setting it in saveOutgoingMessage (from the recipient contact/group) would close that gap.

CHANGELOG / docs

4. The entry says "A backfill script updates existing rows from the current contact/chat list" — there's no backfill script in the PR, and the migration only adds a NULL column, so existing rows stay unnamed. Please either add the backfill or drop that sentence.

5. The credit reads (#557) Thanks @shadowwalker.#557 is an unrelated issue and this PR is #558; it should be (#558) Thanks @buluma.

6. docs/06-api-specification.md still shows the old topChats shape — please add the chatName field to both response examples.

7. (nit) In the migration, const colType = isPostgres ? 'varchar' : 'varchar' has two identical branches; you can drop isPostgres/colType and inline varchar.

The build/tests are green, so once the GROUP BY is fixed the rest is quick. Happy to help if any of this is unclear.

…tion

The GROUP BY chatName fragmented the per-chat count into multiple rows
when the name varied within a chat (outbound vs inbound, group senders,
legacy NULL rows). Using MAX() picks a representative name without
changing the grouping grain.

Also:
- Removed dead isPostgres ternary in migration (both branches were 'varchar')
- Fixed CHANGELOG PR reference (rmyndharis#557rmyndharis#558) and credit (@shadowwalker@buluma)
- Removed backfill script claim (no such script exists)
@buluma

buluma commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @rmyndharis — really appreciate you catching the GROUP BY issue.

Here's what I fixed in 9c07b13:

Stats aggregation (blocking) — dropped both addGroupBy('m.chatName') and switched to MAX(m.chatName) in getMessageStats and getSessionStats. Keeps the grain at one row per chat, and MAX() picks a representative name ignoring NULLs so outbound-only chats still get a name when inbound messages exist.

Migration — removed the dead isPostgres ? 'varchar' : 'varchar' ternary, just inlined varchar.

CHANGELOG — fixed the PR ref (#557#558), credit (@shadowwalker@buluma), and dropped the backfill script line since there isn't one in this PR.

Didn't touch the outbound path or group name resolution — those need contact lookups I don't have the plumbing for here. Happy to tackle them separately if useful.

@rmyndharis

Copy link
Copy Markdown
Owner

Thanks for this, @buluma — nice feature. Landing it via #572, which carries your commits (authorship preserved) and adds two things needed for current main: your migration's timestamp (1781900000000) collided with a migration that merged after you opened this (AddIntegrationFabric), so it's renamed to 1782000000000; and a stats-service test covering the MAX(chatName) aggregate. Full credit stays yours in the CHANGELOG. Closing this in favor of #572.

@rmyndharis rmyndharis closed this Jul 1, 2026
rmyndharis added a commit that referenced this pull request Jul 1, 2026
* feat(stats): add chat names to top chats and stats endpoints

Store the human-readable chat name (contact pushName/saved name) on
each message at save time, and surface it as `chatName` in the
`GET /stats/messages` and `GET /sessions/:id/stats` topChats response.
The dashboard uses it to display readable names instead of raw JIDs.

- New `chatName` nullable column on the `messages` table
- Populated on incoming messages from the sender's contact info
- Returns `chatName` alongside `chatId` in both stats endpoints
- Dashboard displays `chatName` when available, falls back to `shortChat`
- TypeORM migration (1781900000000) for production / non-sync deployments

* fix(stats): use MAX(chatName) instead of GROUP BY chatName in aggregation

The GROUP BY chatName fragmented the per-chat count into multiple rows
when the name varied within a chat (outbound vs inbound, group senders,
legacy NULL rows). Using MAX() picks a representative name without
changing the grouping grain.

Also:
- Removed dead isPostgres ternary in migration (both branches were 'varchar')
- Fixed CHANGELOG PR reference (#557#558) and credit (@shadowwalker@buluma)
- Removed backfill script claim (no such script exists)

* fix(stats): move chatName migration to a free timestamp and cover the aggregate

The migration collided with 1781900000000-AddIntegrationFabric on main (which merged
after this PR was opened); renamed to 1782000000000. Added a stats-service test asserting
MAX(chatName) surfaces the name in topChats and ignores null legacy rows.

---------

Co-authored-by: Michael Buluma <1452922+buluma@users.noreply.github.com>
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