Guard pending chat members against missing account IDs - #96747
Conversation
…g-chat-members-undefined-toString
…g-chat-members-undefined-toString
|
@eVoloshchak Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 01e11450b2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| */ | ||
| function getPendingChatMembers(accountIDs: number[], previousPendingChatMembers: PendingChatMember[], pendingAction: PendingAction): PendingChatMember[] { | ||
| const pendingChatMembers = accountIDs.map((accountID) => ({accountID: accountID.toString(), pendingAction})); | ||
| const pendingChatMembers = accountIDs.filter((accountID) => accountID != null).map((accountID) => ({accountID: accountID.toString(), pendingAction})); |
There was a problem hiding this comment.
Filter missing IDs before building room updates
When a room/group-chat invite contains the runtime undefined value this change is intended to tolerate, filtering only inside getPendingChatMembers is too late: buildInviteToRoomOnyxData has already written that value as the "undefined" participant key, and the missing invitee receives no pending-member entry that can later carry an API error. The action now proceeds instead of throwing, leaving malformed optimistic participant data and no visible failure feedback for the affected invitee; sanitize the invite mapping/account-ID list before any optimistic data is constructed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in e7ee2f2.
You're right that the helper-level filter ran too late: participantsAfterInvitation is built before getPendingChatMembers, and a missing account ID lands in the participants map as the literal "undefined" key. Worth noting the sharper version of this — before this PR the throw aborted the whole action so nothing was persisted, whereas afterwards the write proceeds, so the fix would have newly persisted that malformed key.
The invitee account IDs are now filtered where they're derived, before any optimistic data is constructed. This covers group chats too, since inviteToGroupChat delegates to inviteToRoom. I deliberately left inviteeEmails untouched so the invite still reaches the server and the real account ID gets reconciled — filtering the emails as well would have silently dropped the invitee instead, which trades malformed data for a missing invite.
The guard inside the helper stays as defence in depth for the other callers.
|
LGTM! |
|
@eVoloshchak slipped that one. added. |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppscreen-20260802-234210-1785706876834.mp4Android: mWeb ChromeScreen.Recording.2026-08-02.at.23.33.06.moviOS: HybridAppScreen.Recording.2026-08-02.at.23.30.53.moviOS: mWeb SafariScreen.Recording.2026-08-02.at.23.28.48.movScreen.Recording.2026-08-02.at.23.28.04.movMacOS: Chrome / SafariScreen.Recording.2026-08-02.at.23.26.51.mov |
|
🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.50-0 🚀
|
|
🤖 No help site changes required. I reviewed the changes in this PR against the help articles under Why: This is a purely defensive bug fix with no user-visible behavior change. It adds an
The PR description confirms it: "there is no user-visible change to look for" and "Everything above should behave exactly as it does on production." The invite-member and remove-member flows for both workspaces and rooms behave identically to today. The help site documents customer-facing features and workflows, none of which change here, so there is nothing to add or amend. If you believe a specific article's described behavior is affected, let me know which one and I'll take another look. |
Explanation of Change
A crash reaches Sentry from the report route as an unhandled promise rejection:
TypeError: undefined is not an object (evaluating 'p.toString'). The helper that assembles a report's pending chat members converts each incoming account ID to a string without first checking that the ID is there. Its parameter is declared as a list of numbers, but nothing enforces that at runtime — the member add and remove flows derive those IDs from personal-detail lookups, and an entry that has not finished loading contributes a missing value. Converting that missing value throws, and because the member actions are dispatched from optimistic-update code the throw surfaces as an unhandled rejection instead of a render error, which is why the production stack has no useful frame.Dropping the missing IDs before the conversion removes the only expression that can throw, and it also keeps a member entry with an empty account ID out of the report's metadata — the alternative of falling back to an empty string would have stored one. Putting the guard in the shared helper rather than at each call site covers the room invite and removal flows and the workspace member flows uniformly, and it leaves behavior for well-formed input unchanged.
Fixed Issues
$ #95348
PROPOSAL: #95348 (comment)
Tests
The Sentry report has no reproduction steps — the failure depends on a personal-detail entry that has not finished loading, so it cannot be triggered on demand. The unit tests added in this PR cover the missing-ID case directly (they throw the same
TypeErrorwithout this change). The steps below confirm the member flows that call the helper are unaffected:Direct check of the guard (console script)
Because the crash depends on runtime data that cannot be produced on demand through the UI, this script calls the real bundled
getPendingChatMembersout of the running dev build and hands it a missing account ID. It is the same script on both branches — only the result differs. It must be run against a local dev build (npm run web): minified staging/production bundles omit the module cache and mangle export names, so the script cannot reach the function there and will say so rather than fail silently.Open any chat (so
ReportUtilsis loaded), then paste this into the DevTools console:On
mainit returnsCRASHES — unpatched (this is main)and logs:On this branch it returns
NO CRASH — guard present (this is the PR branch)and logs:The control case passing on both builds is the point — the only behavioural difference is the missing-ID case. The
TypeErrortext above is Chrome's wording; Safari words the same errorundefined is not an object (evaluating 'p.toString'), which is the form recorded in Sentry.Scope note: this exercises the guard directly by passing the value in. It does not demonstrate the app producing that state on its own, and the reporting Sentry stack is minified, so it is not proof that this helper is the frame that crashed for the reporter.
Platform scope — the console script is web-only. It reaches the function through the dev build's bundler module registry, which only a local web dev build exposes. It will not run on Android or iOS native (those bundle through Metro, with no such registry), and it will not run against staging or production (minified builds omit the module cache and mangle export names — the script detects this and says so rather than failing silently).
Nothing about the change is platform-specific: it is a single null check in shared TypeScript that executes identically on every platform, with no UI, styling, or native code involved. So on the other platforms there is nothing extra to reproduce — the numbered steps above are the whole check. Invite a member and then remove one, and confirm the member list and the workspace chat's system message still behave exactly as they do today. The unit tests added in this PR cover the missing-ID case itself on every platform, since they run in the shared Jest suite.
Offline tests
The change touches only the optimistic pending-member list and adds no network calls. Inviting or removing a member while offline queues the action and shows the member in the pending state exactly as it did before.
QA Steps
Do not run the console script from the Tests section — that is a developer-only check that needs a local dev build. For QA this is purely a regression check on the member flows, since the change is a defensive guard with no visible behaviour of its own:
Everything above should behave exactly as it does on production — there is no user-visible change to look for.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Kapture.2026-07-26.at.16.26.28.mp4
Android: mWeb Chrome
Kapture.2026-07-26.at.16.26.28.mp4
iOS: Native
Kapture.2026-07-26.at.00.15.59.mp4
iOS: mWeb Safari
Kapture.2026-07-26.at.00.17.22.mp4
MacOS: Chrome / Safari
Kapture.2026-07-25.at.23.18.52-compressed.mp4