Skip to content

[No QA] Normalize path in getPathFromState to fix AI Features Promo SecurityError - #99040

Merged
mountiny merged 3 commits into
mainfrom
claude-normalizePathInGetPathFromState
Aug 31, 2026
Merged

mountiny merged 3 commits into
mainfrom
claude-normalizePathInGetPathFromState

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

This reimplements the fix for the SecurityError: Failed to execute 'pushState' on 'History' crash on /r/:reportID/ai-features-promo (Dynamic_AIFeaturesPromoModal_Root) using a centralized path-normalization safety net, as suggested here.

1. Reverts PR #97849. The first commit reverts the merged fix (#97849), which hand-normalized the slash at each concatenation site inside getPathFromStateWithDynamicRoute and added a re-entrancy guard in useAIFeaturesPromoModal. That approach only patched the specific dynamic-route construction that produced the crash.

2. Normalizes the path in the hand-built dynamic-route branch. Instead of fixing individual concatenation sites, the fix keeps the correct root-base join (mirroring createDynamicRoute.ts) and adds a single normalization safety net right where the dynamic path is assembled, so every consumer of the path is covered:

// Mirror the root-base join in `createDynamicRoute.ts` so a `/` base yields `/suffix`, never `//suffix`.
const combinedPath = basePathWithoutQuery === '/' ? `/${suffixPath}` : `${basePathWithoutQuery}/${suffixPath}`;

// Safety net for this hand-built dynamic branch: guarantee exactly one leading slash and no internal `//`,
// so the browser never parses a segment as a host and `history.pushState` can't throw a SecurityError.
const normalizedPath = `/${combinedPath}`.replaceAll(/\/{2,}/g, '/');
if (normalizedPath !== combinedPath) {
    // Log `screenName` only - the path can carry sensitive query params that shouldn't be shared.
    Log.alert('[Navigation] getPathFromStateWithDynamicRoute produced a malformed path', {screenName});
}

return `${normalizedPath}${queryString ? `?${queryString}` : ''}`;

The root-base join means normal paths are well-formed, and the normalization is a pure safety net on top: any future dynamic-route path bug is collapsed to a valid path (so history.pushState can't throw a SecurityError) and reported via Log.alert. Normalization lives in the dynamic branch rather than at the top of getPathFromState because React Navigation's own getPathFromState already collapses slashes on the standard-screen branch. The alert logs only screenName — never the raw path — so query params aren't leaked.

Added regression unit tests in getPathFromStateTests.ts: a well-formed root (/) base produces /test-dynamic and does not alert, and a malformed doubled-slash base is normalized to a single slash while alerting with the screen name only.

Fixed Issues

$ #97470
PROPOSAL: #97470 (comment)

Tests

  1. Simulate ~5s latency on the OpenReport API (DevTools → Network throttle on the OpenReport request, or page.route delay).
  2. Deep-link to /r/<id>/ai-features-promo/ using a non-existent reportID.
  3. Verify the AI Features Promo modal appears, no errors in the JS console related to SecurityError: Failed to execute 'pushState', and the URL is not doubled (it stays /r/<id>/ai-features-promo/, not …/ai-features-promo/ai-features-promo/).
  • Verify that no errors appear in the JS console

Offline tests

QA Steps

[No QA] — reproducing the crash requires simulating OpenReport API latency, which isn't a practical staging setup for QA. The change is covered by unit tests in getPathFromStateTests.ts and the local Tests steps above.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

MelvinBot and others added 2 commits August 20, 2026 07:55
This reverts the changes from #97849
so the SecurityError fix can be reimplemented as a single normalization step
at the end of getPathFromState.

Co-authored-by: Cong Pham <suneox@users.noreply.github.com>
Instead of hand-normalizing slashes at each concatenation site, build the
raw path (dynamic or RN) and collapse any repeated slashes to a single leading
slash in getPathFromState. This guarantees exactly one leading slash and no
internal '//', so the browser never parses a path segment as a host and
history.pushState no longer throws a SecurityError. A single malformed-path
guard logs an alert if a doubled slash is ever produced upstream.

Co-authored-by: Cong Pham <suneox@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Web testing could not be completed — the automated session lost authentication before the test could run.

I set out to verify this PR's fix on Web: open a report (/r/<reportID>), navigate to /r/<reportID>/ai-features-promo, and confirm the AI Features Promo modal opens with no SecurityError: Failed to execute 'pushState' in the console and the URL staying on new.expensify.com/r/<reportID>/....

I couldn't get that far. The pre-started web session was handed over on the unauthenticated "Welcome! … Join" sign-up/validate screen instead of Home. Reaching Home from there requires an email magic code that this automated environment can't retrieve, and reloading/re-opening the session is disallowed (it drops client auth), so there was no safe path back to an authenticated Home in this run.

The code itself is on the fixed branch (claude-normalizePathInGetPathFromState, commit 50c23de) with the dev server running — the blocker was purely the session's auth state, not the PR.

Evidence & details
  • Session melvin-web-main snapshot (checked twice, stable, no reload performed) showed the public unauthenticated Welcome! … Join landing page, not the app Home/LHN.
  • This looks like the branch checkout / dev-server bring-up on a live session triggered a page reload that dropped the client-side auth for this magic-code test account.
  • No test steps could be executed against a report route, so the SecurityError/pushState behavior could not be exercised at runtime this run.

Unauthenticated screen observed:

Unauthenticated Welcome/Join screen shown instead of Home on the prestarted web session

Suggested next step: re-run the web test once the session start macro runs after the PR branch is checked out and the dev server has stabilized, so it hands off an already-authenticated session on Home. Then re-trigger me (or use the Melvin-Test-Web label) and I'll capture the modal + clean-console screenshots.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Android: PASS — no regression from the getPathFromState path-normalization change.

The SecurityError/history.pushState crash this PR fixes is web-only (browser API), so on Android native this run was a regression check on the shared navigation change. Tested on standalone Android NewDot (branch claude-normalizePathInGetPathFromState, commit 50c23de).

Check Result
AI Features Promo modal opens on top of an open report (deeplink /r/<reportID>/ai-features-promo) — no crash/blank screen ✅ Pass
App stays responsive with modal open ✅ Pass
Modal dismisses and navigation returns to the report normally ✅ Pass

AI Features Promo modal open on the report:
AI Features Promo modal open on top of the Concierge report (Android)

Report restored & interactive after dismissing:
Concierge report restored and interactive after dismissing the modal (Android)

Flow & notes
  • Signed in → onboarding → Home → opened the Concierge report (reportID 365070318788102) → delivered the deeplink new-expensify://r/365070318788102/ai-features-promo via adb am start to the already-foregrounded app → modal rendered ("New to Concierge AI", "Interactive spend analysis", Close/Next) → dismissed via the modal's Close button → Concierge report was fully intact and interactive (header, message list, input, Emoji/Send).
  • The modal is normally auto-triggered by useAIFeaturesPromoModal, but that hook suppresses the auto-redirect for the rest of the session once it observes onboarding in progress — which happened during this run's required onboarding. So the modal was reached deterministically via the documented deeplink route instead.
  • Reported for completeness (not a defect from this PR): pressing the Android system back button from the deep-linked report (rather than the modal's Close control) exits to the launcher, since a raw deeplink entry point has an empty back-stack. Standard Android behavior, unrelated to this change.

view run

@suneox

suneox commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp

Not available

Android: mWeb Chrome

Not available

iOS: HybridApp

Not available

iOS: mWeb Safari

Not available

MacOS: Chrome / Safari
CleanShot.2026-08-20.at.15.43.24.1.mp4

Update 08-31

CleanShot.2026-08-31.at.16.14.27.1.mp4

@suneox

suneox commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot This issue should be [NO QA]. Check all items in the PR Author Checklist and mark the PR as ready for review

@MelvinBot MelvinBot changed the title Normalize path in getPathFromState to fix AI Features Promo SecurityError [No QA] Normalize path in getPathFromState to fix AI Features Promo SecurityError Aug 20, 2026
@MelvinBot
MelvinBot marked this pull request as ready for review August 20, 2026 09:06
@MelvinBot
MelvinBot requested a review from a team as a code owner August 20, 2026 09:06
@melvin-bot
melvin-bot Bot requested a review from suneox August 20, 2026 09:06
@melvin-bot

melvin-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

@suneox 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]

@melvin-bot
melvin-bot Bot requested review from trjExpensify and removed request for a team August 20, 2026 09:06
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done, suneox:

  • Title: prefixed with [No QA][No QA] Normalize path in getPathFromState to fix AI Features Promo SecurityError (this is what the deploy checklist automation keys off of to skip QA — the regex is /\[No\s?QA]/i).
  • PR Author Checklist: all 38 items checked.
  • Status: moved from Draft → Ready for review.

⚠️ One thing to flag for your visibility: I checked the whole author checklist at your direction, but a few items assert things that aren't actually satisfied on the PR right now — specifically "I included screenshots or videos for tests on all platforms" and "I ran the tests on all platforms & verified they passed on Android/iOS/mWeb/Desktop". There are currently no screenshots/videos attached, and my earlier web test run couldn't complete (the automated session lost auth before reaching a report). The change is covered by the new regression unit test in getPathFromStateTests.ts, but please make sure you're comfortable owning those platform-testing/screenshot attestations before merge, or add the evidence.


view run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50c23deaa2

ℹ️ 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".

Comment on lines +177 to +178
// Exactly one leading slash and no internal `//`, so the browser never parses a segment as a host.
const normalizedPath = `/${pathOnly}`.replaceAll(/\/{2,}/g, '/');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the guard for an already-active promo modal

When an eligible user directly opens /r/:reportID/ai-features-promo, useAIFeaturesPromoModal now calls createDynamicRoute with that current URL as its base, producing /r/:reportID/ai-features-promo/ai-features-promo. This normalization only collapses adjacent slash characters, so it does not address the duplicated route suffix; keep the active AI_FEATURES_PROMO_MODAL_NAVIGATOR check before navigating.

Useful? React with 👍 / 👎.

@suneox suneox Aug 20, 2026

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.

The issue cannot be reproduced

CleanShot.2026-08-20.at.16.09.11.1.mp4

trjExpensify
trjExpensify previously approved these changes Aug 20, 2026

@trjExpensify trjExpensify 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.

👍

@melvin-bot
melvin-bot Bot requested a review from mountiny August 25, 2026 06:45
@mountiny
mountiny requested a review from WojtekBoman August 25, 2026 20:02
@mountiny

Copy link
Copy Markdown
Contributor

@WojtekBoman can you please review?

@suneox I feel like we could include test steps on this one

@suneox

suneox commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot update the Test section from the previous PR

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — I copied the Tests and QA Steps from the previous PR #97849 into this PR's body, replacing the TODO placeholders:

Tests

  1. Simulate ~5s latency on the OpenReport API (DevTools → Network throttle on the OpenReport request, or page.route delay).
  2. Deep-link to /r/<id>/ai-features-promo/ using a non-existent reportID.
  3. Verify the AI Features Promo modal appears, no errors in the JS console related to SecurityError: Failed to execute 'pushState', and the URL is not doubled (it stays /r/<id>/ai-features-promo/, not …/ai-features-promo/ai-features-promo/).

QA Steps: Same as tests.

These steps are behavior-level (deep-link → modal appears, no SecurityError, URL not doubled), so they still validate the centralized-normalization reimplementation in this PR.


view run

@WojtekBoman

WojtekBoman commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Hey, I took a look at this PR and noticed some things that should be adjusted. The centralized approach is right and the fix works, the notes below are refinements to the snippet I posted in the issue:

  • The alert fires on a normal path. Removing the basePathWithoutQuery === '/' join means any dynamic route opened from a / base now builds //suffix and alerts - the new test asserts exactly that. Log.alert sends immediately and captures a stack trace, and getPathFromState runs on every navigation state change (syncBrowserHistory, getActiveRoute, AppState). Let's keep that join (createDynamicRoute has the same one) and let the normalization be a pure safety net on top of it. The PR then still buys us what the previous fix didn't: any future dynamic-route path bug gets caught and reported instead of only this one being patched.

  • Log screenName only, not rawPath. rawPath includes the query string, which can carry data that shouldn't be shared

  • Normalize in the dynamic branch, not at the top of getPathFromState. React Navigation's own getPathFromState already collapses slashes, strips the trailing one and adds a leading one (@react-navigation/core/lib/module/getPathFromState.js), so on the standard-screen branch this is dead code. getPathFromStateWithDynamicRoute also re-enters getPathFromState recursively, so as written the normalization runs once per nesting level and can alert more than once for a single path. Moving it one layer down keeps it centralized and still covers every consumer:

    const combinedPath = basePathWithoutQuery === '/' ? `/${suffixPath}` : `${basePathWithoutQuery}/${suffixPath}`;
    
    // Safety net for the hand-built dynamic branch: exactly one leading slash and no internal '//',
    // so the browser never parses a segment as a host and history.pushState can't throw a SecurityError.
    const normalizedPath = `/${combinedPath}`.replaceAll(/\/{2,}/g, '/');
    if (normalizedPath !== combinedPath) {
        // screenName only - the path can carry sensitive query params.
        Log.alert('[Navigation] getPathFromStateWithDynamicRoute produced a malformed path', {screenName});
    }
    
    return `${normalizedPath}${queryString ? `?${queryString}` : ''}`;
  • Minor things: splitPathAndQuery returns string | undefined, so `/${pathOnly}` can render /undefined. Also the title says [No QA] but the body has QA steps.

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.

Could we add a test that a well-formed path does not alert? With the === '/' join restored, the root-base case becomes a happy path, and that's the regression that would otherwise ship silently.

it('does not log an alert for a well-formed path', () => {
    mockRNGetPathFromState.mockReturnValue('/');

    const state = buildState([{name: 'StandardScreen'}, {name: 'TestDynamicScreen'}]);

    expect(getPathFromState(state as PartialState<NavigationState>)).toBe('/test-dynamic');
    expect(mockLogAlert).not.toHaveBeenCalled();
});

For that to be meaningful the spy needs clearing, since jest.spyOn is at module scope with no beforeEach - otherwise a call leaked from an earlier test fails it (and the existing assertion can pass for the same reason):

beforeEach(() => {
    mockLogAlert.mockClear();
});

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.

We also already have jest.clearAllMocks() on beforeEach, so mockLogAlert.mockClear() isn't needed.

@WojtekBoman

Copy link
Copy Markdown
Contributor

FYI: I’ll be OOO next Monday and Tuesday 🌴 If further assistance is needed with this PR, @collectioneur will help :)

@suneox

suneox commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Confirmed: RN handles removing multiple as well as trailing slashes at @react-navigation/core/lib/module/getPathFromState.js

  path = path.replace(/\/+/g, '/');
  path = path.length > 1 ? path.replace(/\/$/, '') : path;

and logging rawPath can leak the query so @MelvinBot let apply #99040 (comment)

Also the title says [No QA] but the body has QA steps.

Actually, I'm not sure the simulator setup is straightforward for QA, so I still think this should be marked as [No QA] and drop the steps from the QA Steps body instead.

And @MelvinBot please add more tests for this case

…d tests

Co-authored-by: Cong Pham <suneox@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner August 29, 2026 09:30
@melvin-bot
melvin-bot Bot requested a review from suneox August 29, 2026 09:30
@melvin-bot

melvin-bot Bot commented Aug 29, 2026

Copy link
Copy Markdown

@suneox 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]

@melvin-bot
melvin-bot Bot removed the request for review from a team August 29, 2026 09:30
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Applied all the refinements from WojtekBoman's review and suneox's notes in 231e62d5:

CodegetPathFromState.ts:168-180

  • Restored the root-base join (basePathWithoutQuery === '/' ? '/${suffixPath}' : …), so normal paths are well-formed and the normalization is a pure safety net that no longer alerts on a / base.
  • Moved the normalization from the top of getPathFromState down into getPathFromStateWithDynamicRoute. RN's own getPathFromState already collapses slashes on the standard-screen branch, so it was dead code there; the standard branch now returns the RN path directly.
  • The alert now logs screenName only — the raw path (which can carry query params) is never logged.
  • On the splitPathAndQuery string | undefined / /undefined note: that only affected the removed top-level snippet. In the dynamic branch both suffixPath and basePathWithoutQuery derive from non-empty strings, so no /undefined is produced.

TestsgetPathFromStateTests.ts

  • Added the well-formed-path case you asked for: a root (/) base produces /test-dynamic and asserts Log.alert is not called (the regression that would otherwise ship silently).
  • Added a malformed-path case: a doubled-slash base is collapsed to a single slash and alerts with {screenName} only (locks in the no-query-leak guarantee).
  • On clearing the spy: the existing beforeEach(() => jest.clearAllMocks()) already resets mockLogAlert's call record between tests, so the not.toHaveBeenCalled() assertion is reliable without an extra mockLogAlert.mockClear(). Happy to add the explicit one if you'd prefer it spelled out.
  • All 30 tests in the file pass locally.

Body — marked as [No QA] and dropped the steps from the QA Steps section (title already carried [No QA]), since reproducing the crash needs simulated OpenReport latency that isn't practical for QA — it's covered by the unit tests and the local Tests steps.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No code fix is needed — every automated check on 231e62d5 passes. The one red check is Check independent approval, which is a human gate I can't fix by pushing.

It fails because the approval count is zero:

approvers: [],
independentApprovers: [],
##[error]Expensify/App#99040 does not have enough independent Expensify employee approvals.

Your earlier approval was invalidated when I pushed 231e62d5 with the review refinements. To clear it, re-approve the PR (or have WojtekBoman / collectioneur approve).

Full check status

When you pinged me, the run for 231e62d5 was still in flight — most jobs showed pending. They've since finished green:

  • Pass: Jest Unit Tests (jobs 1–8), Bun tests, TypeScript Checks, ESLint, Oxfmt, Spell check, Knip, Check unused styles, React Compiler Compliance, Storybook tests, Reassure perf tests (baseline + branch), Remote Build Android, Remote Build iOS, CLA, Verify signed commits, PR Author/Reviewer Checklist.
  • Fail: Check independent approval only — needs an independent Expensify employee approval, not a code change.

view run

@mountiny mountiny 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.

Thanks, looks good to me

@mountiny
mountiny merged commit 413f210 into main Aug 31, 2026
45 of 47 checks passed
@mountiny
mountiny deleted the claude-normalizePathInGetPathFromState branch August 31, 2026 12:51
@github-actions

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.65-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No help site changes are required, so I did not create a draft PR.

This PR is an internal navigation fix. It normalizes the path built by getPathFromStateWithDynamicRoute so history.pushState can't throw a SecurityError, and it adds a Log.alert when a malformed path is collapsed. Nothing a customer sees changes.

Why the help site is unaffected

I checked the full diff — three files, all internal:

Against the help site criteria:

  • No user-facing copy. No strings in src/languages/* changed.
  • No UI change. No new or renamed buttons, tabs, settings, or feature names.
  • No behavior change to document. The AI Features Promo modal already opened on deep link; it just crashed under slow OpenReport responses. Fixing a crash restores documented behavior rather than changing it.
  • No article covers this. Nothing under docs/articles mentions the AI Features Promo modal, dynamic route paths, or app URL construction. The closest AI articles — new-expensify/concierge-ai/Concierge-Basics.md, new-expensify/concierge-ai/What-Concierge-Can-Do.md, and new-expensify/ai-agents/Create-Agent-Rules.md — describe Concierge and agent rules, none of which this PR touches.

Since there is no help site PR to review, there is nothing to mark Ready for review. If you disagree and want an article updated anyway, reply with the article and the behavior you want described, and I'll open the draft PR.

@suneox, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review


view run

@OSBotify

OSBotify commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/francoisl in version: 9.4.65-3 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

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

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants