Skip to content

[Due for payment 2026-10-21] [$175] LHN sometimes doesn't show unread chats until the chat is opened #102973

Open

Description

@melvin-bot

Problem

Over the past few days or weeks, chats with new messages sometimes don't appear as unread in the LHN. The unread state shows only after the user opens the chat manually.

Details

  • Platform: Unconfirmed (possibly web)
  • Reproduction steps: Not known yet. The issue happens on and off.

Next steps

  • Find reliable steps to reproduce the issue.
  • Check whether the existing LHN logging captures these cases. If it does not, add telemetry to detect when a report has unread actions but the LHN doesn't show it as unread.

Reported in Slack.

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 5, 2026
  2. MelvinBot commented on Oct 5, 2026

    @MelvinBot
    Contributor

    Proposal

    Confidence: medium. The code and the PR that introduced the change support this. I couldn't check production logs in this run, so it isn't confirmed for a specific user yet.

    What is the root cause of that problem?

    The LHN is fine. On web, the new-message update never reaches the client.

    Since [No QA] fix: skip reconnectApp on pusher socket blips (merged 2026-08-29), web skips ReconnectApp when the Pusher user channel resubscribes, unless pusher-js reached the unavailable state (PusherUtils.ts:58-67, Pusher/index.ts:115-118).

    1. A socket dies silently, for example after sleep, a Wi-Fi change, or a throttled tab. pusher-js only notices this after its activity and pong timeouts.
    2. It then reconnects in under 10 seconds, so it never reaches unavailable. The resubscribe skips the sync.
    3. Pusher doesn't replay events. Messages sent while the socket was dead are lost.
    4. Gap detection only runs when the next server payload with previousUpdateID arrives (applyOnyxUpdatesReliably.ts:50-51). Opening the chat sends OpenReport, which triggers it. That's why the unread state appears only after the user opens the chat.

    PR #99084 calls this out in its own test step 10: "Verify the message does not appear yet. This is the accepted tradeoff… an idle tab stays stale until one arrives."

    What changes do you think we should make in order to solve the problem?

    1. In src/libs/Pusher/index.ts, also mark an outage when the socket drops after an activity or pong timeout (a silent death). Optionally, also mark one when the time since the last inbound socket message is longer than a few seconds. Short blips still skip ReconnectApp, and silent deaths sync again.
    2. Add a unit test to tests/unit/PusherSubscribeWebTest.ts. It should cover a pong-timeout drop followed by a fast reconnect, and check that ReconnectApp fires.
    3. For the telemetry the issue asks for, add a Log.info when gap detection runs. It should include the gap size and the time since the last "Skipping reconnect" line. Then we can measure how often skipped syncs leave clients stale.

    What alternative solutions did you explore? (Optional)

    • Revert [No QA] fix: skip reconnectApp on pusher socket blips #99084. This is simple but brings back about 54k extra ReconnectApp calls every 2 hours, per the PR.
    • An LHN rendering bug. I traced the code from isUnread to the LHN row and found no memo or cache that skips a changed report. Opening the chat repairs the data itself, which points to missing data, not a render bug.
    Investigation details

    Possibly related issues:


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102973) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 5, 2026
  4. melvin-bot commented on Oct 5, 2026

    @melvin-bot
    Author

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @dmkt9 (External)

  5. changed the title [-]LHN sometimes doesn't show unread chats until the chat is opened[/-] [+][$175] LHN sometimes doesn't show unread chats until the chat is opened[/+] on Oct 5, 2026
  6. melvin-bot commented on Oct 5, 2026

    @melvin-bot
    Author
  7. melvin-bot commented on Oct 5, 2026

    @melvin-bot
    Author

    📣 @dmkt9 We're missing your Upwork ID to automatically send you an offer for the Reviewer role.
    Once you apply to the Upwork job, your Upwork ID will be stored and you will be automatically hired for future jobs!

  8. camesenin commented on Oct 5, 2026

    @camesenin
    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Web, unread chat messages do not highlight as unread in the Left Hand Navigation (LHN) until the user clicks and opens the chat report.

    What is the root cause of that problem?

    In src/libs/PusherUtils.ts (lines 60–67), when Pusher re-subscribes to the private user channel, it calls Pusher.claimOutageSync():

    unregisterPrivateUserChannelResubscribe = Pusher.onChannelResubscribe(getUserChannelName(accountID), () => {
        if (!Pusher.claimOutageSync()) {
            Log.info('[PusherUtils] Skipping reconnect, socket recovered without going unavailable');
            return;
        }
        Log.info('[PusherUtils] Pusher re-subscribed to private user channel, triggering reconnect');
        reconnect();
    });

    In src/libs/Pusher/index.ts (line 116), hasUnclaimedOutage is only set if the socket reaches the unavailable state:

    hasUnclaimedOutage ||= states.current === 'unavailable';

    When a socket connection drops silently (e.g. laptop sleep, Wi-Fi network switch, browser throttling idle tabs, or activity/pong timeout) and reconnects in under 10 seconds, pusher-js never transitions into unavailable.

    Consequently:

    1. claimOutageSync() returns false, skipping reconnect() (ReconnectApp).
    2. Pusher does not replay missed real-time events that occurred while the socket was disconnected.
    3. The report's Onyx state is never updated with the new message until the user navigates into the chat, which triggers OpenReport and fetches the latest report data.

    What changes do you think we should make in order to solve the problem?

    1. In src/libs/Pusher/index.ts:
      • Track when a socket disconnect occurs due to transport error or connection timeout (such as activity/pong watchdog drop) by setting hasUnclaimedOutage = true.
      • Ensure that any resubscription that occurs after an unnoticed connection break properly triggers ReconnectApp via claimOutageSync(), while keeping immediate blips without missed events optimized.
    2. In src/libs/actions/applyOnyxUpdatesReliably.ts:
      • Add telemetry logging in gap detection to monitor gap size and delta time since the last skipped reconnect to ensure zero dropped syncs.
    3. Unit test:
      • Add a test case in tests/unit/PusherSubscribeWebTest.ts asserting that an activity/pong timeout drop followed by a fast reconnect correctly marks an outage and triggers ReconnectApp.

    What alternative solutions did you explore? (Optional)

    • Reverting PR [No QA] fix: skip reconnectApp on pusher socket blips #99084: This would trigger excessive ReconnectApp calls on transient socket blips (~54k extra calls). Targeting actual timeout/drop disconnects rather than relying solely on unavailable preserves efficiency while eliminating stale state.
    • Client-side polling in LHN: Unnecessary and contradicts Onyx push-driven architecture.

    Contributor details
    Your Expensify account email: camesenin@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0170d6b02e9440e604

  9. 29 remaining items

  10. VickyStash commented on Oct 15, 2026

    @VickyStash
    Contributor

    UPD: I see the log added by us was triggered several times, but I can't verify it over real FS sessions today (data capture will be resumed by FS tomorrow).

  11. VickyStash commented on Oct 16, 2026

    @VickyStash
    Contributor

    I've rechecked logs vs sessions and for spotted two cases

    • FS link 1 time 11:18: report is shown in LHN for a moment and then disappears from LHN (though the user currently has it open).
      It looks like for some reason check for current report didn't work. I wasn't able to reproduce it so I'll keep an eye on similar cases if any.

    • FS link 2 time 8:28: expense report with unread replies from May isn't shown in LHN.

    I've reproduced the same an it looks like it's the thing for the one transaction threads that we intentionally skip it from display in LHN, even if it has some unread (check 1 and check 2):

    lhn_transaction_thread.mp4

    See, at first the thread is bolded in All tab, and not visible in Unreads.
    After Clear cache & restart it's not visible at all. Only when user opens is they see the unread message.
    Just to confirm, is it expected behaviour @mountiny?

  12. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Oct 20, 2026
  13. melvin-bot commented on Oct 20, 2026

    @melvin-bot
    Author

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  14. melvin-bot commented on Oct 20, 2026

    @melvin-bot
    Author

    Payment Summary

    Resolving PRs:

    Upwork Job

    • Contributor: @VickyStash does not require payment (Contractor)
    • Assignee: @dmkt9 requires payment (Needs manual offer from BZ)

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  15. mallenexpensify commented on Oct 21, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @dmkt9 due $175 via NewDot

  16. dmkt9 commented on Oct 22, 2026

    @dmkt9
    Contributor

    Contributor+: @dmkt9 due $175 via NewDot

    @mallenexpensify I'm still receiving payments through Upwork. I've reviewed two separate PRs: #103147 and #103463.

  17. mallenexpensify commented on Oct 22, 2026

    @mallenexpensify
    Contributor

    @dmkt9 can you please accept the job below? Please reply here and tag me once you have.
    https://www.upwork.com/jobs/~022107043087024337291

  18. dmkt9 commented on Oct 23, 2026

    @dmkt9
    Contributor

    46. @dmkt9 can you please accept the job below? Please reply here and tag me once you have.

    @mallenexpensify I've requested $350 for reviewing two PRs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorNeeds ReproductionReproducible steps needed

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions