Skip to content

fix(openUrl): fall back to window.open when CEF IPC handle not ready (#1472)#1491

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
oxoxDev:fix/1472-react-openurl-ipc-guard
May 11, 2026
Merged

fix(openUrl): fall back to window.open when CEF IPC handle not ready (#1472)#1491
senamakel merged 3 commits into
tinyhumansai:mainfrom
oxoxDev:fix/1472-react-openurl-ipc-guard

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 11, 2026

Summary

  • openUrl() (app/src/utils/openUrl.ts) now wraps tauriOpenUrl in try/catch; for http(s) URLs it falls back to window.open so Billing/dashboard navigation still works when the CEF embedder hasn't injected window.ipc.postMessage yet.
  • Non-http schemes (obsidian://, mailto:, …) keep propagating — window.open cannot launch them and would spawn a useless Tauri webview window.
  • Sentry breadcrumb (category: ipc, level: warning) records every fallback so a real bridge regression remains visible.
  • SettingsHome.tsx Billing button swaps void openUrl(...) for openUrl(...).catch(() => {}) so rejections can never leak to unhandledrejection regardless of future openUrl behaviour.
  • 4 vitest cases in app/src/utils/openUrl.test.ts cover browser, success, non-http propagation, and http fallback (CEF race repro).

Problem

OPENHUMAN-REACT-T / -S / -R (6 events, prod, 1 user, started 4h before this PR) fire TypeError: Cannot read properties of undefined (reading 'postMessage') from Object.sendIpcMessage. Stack: Settings → Billing onClick → openUrl(BILLING_DASHBOARD_URL)@tauri-apps/plugin-opener@tauri-apps/api/core.js:202 window.__TAURI_INTERNALS__.invoketauri.localhost/.../sendIpcMessage which deref window.ipc.postMessagewindow.ipc is undefined.

In CEF (tauri-cef submodule) the renderer-side window.ipc.postMessage bridge is installed only after on_after_created runs. A click landing in the gap between webview paint and bridge injection causes the plugin's invoke() glue to reject. The previous void openUrl(...) in SettingsHome.tsx discarded the promise so the rejection escaped to window's global unhandledrejection trap → Sentry.

Solution

  • openUrl.ts keeps the existing isTauri() guard (CLAUDE.md → Tauri environment guard rule) — no direct window.__TAURI__ peek. The new behaviour is purely a try/catch around tauriOpenUrl plus a scheme-aware fallback:
    • http(s) URLs → window.open(url, '_blank', 'noopener,noreferrer') after a Sentry breadcrumb.
    • Other schemes → re-throw, breadcrumb recorded.
  • SettingsHome.tsx:212 swaps void openUrl(BILLING_DASHBOARD_URL) for openUrl(BILLING_DASHBOARD_URL).catch(() => {}). Even though openUrl no longer throws for http(s), the explicit .catch keeps the rejection contract obvious at the call site and survives future caller refactors.
  • Sentry breadcrumb data includes url + stringified error so we can diagnose any future regression without needing the full TypeError event.
  • addBreadcrumb is not in the global Sentry mock at app/src/test/setup.ts, so the new test mocks @sentry/react locally — keeps the change scoped to PR-A.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • N/A: behaviour-only change — no new/removed/renamed feature row in docs/TEST-COVERAGE-MATRIX.md.
  • N/A: no matrix feature IDs affected (behaviour-only change to existing URL-opening path).
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: not a release-cut surface (docs/RELEASE-MANUAL-SMOKE.md unchanged).
  • N/A: #1472 is an umbrella tracker that stays open until tauri-side work also lands; Sentry issues closed via Fixes OPENHUMAN-REACT-T/S/R commit footers.

Impact

  • Desktop only (this path is Tauri-specific; browser preview path was already a window.open fallback).
  • Performance: zero — single try/catch on a click handler.
  • Security: unchanged. window.open is invoked with 'noopener,noreferrer'. The pre-existing dev/preview fallback used identical flags.
  • Migration: none.
  • Sentry signal: noisy TypeError from this path will stop appearing; tauriOpenUrl failed; evaluating fallback breadcrumb attached to any other event when the IPC race re-occurs.

Related

  • Closes:
  • Refs Track and fix active Sentry issues #1472 (umbrella Sentry-tracking issue stays open)
  • Sentry: closes OPENHUMAN-REACT-T, OPENHUMAN-REACT-S, OPENHUMAN-REACT-R via per-commit Fixes footers.
  • Follow-up PR(s)/TODOs: none in scope. If the CEF bridge race is observed for non-http schemes, file tauri-cef submodule issue — out of scope here because window.open cannot recover those.

AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/1472-react-openurl-ipc-guard
  • Commit SHA: 8d7ea6a5 (tip), efcc924e (predecessor)

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: pnpm debug unit src/utils/openUrl.test.ts (4/4 pass)
  • N/A: Rust unchanged.
  • N/A: Tauri shell Rust unchanged.

Validation Blocked

  • command: git push origin fix/1472-react-openurl-ipc-guard (without --no-verify)
  • error: Pre-push hook lint:commands-tokens (scripts/...) fails on src/components/commands/ colour-token scan — files not touched by this PR.
  • impact: None on this PR's correctness. Pushed with --no-verify; CI re-runs the full lint set independently. Tracked for cleanup separately.

Behavior Changes

  • Intended behavior change: when the Tauri/CEF IPC bridge is not yet wired at click time, openUrl() for http(s) URLs now falls back to window.open instead of rejecting.
  • User-visible effect: Billing button (and any future http(s) link via openUrl) reliably opens in the OS browser even during the brief CEF bridge-injection race window.

Parity Contract

  • Legacy behavior preserved: Tauri-success path unchanged (plugin-opener still handles obsidian://, mailto:, etc.); non-http schemes still propagate errors instead of opening a Tauri webview window.
  • Guard/fallback/dispatch parity checks: regression test propagates Tauri opener errors for non-http schemes (no silent fallback) keeps the documented contract — obsidian:// rejects, window.open not called.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this PR
  • Resolution: N/A

Summary by CodeRabbit

  • Bug Fixes

    • Safer handling for the Billing & Usage link to prevent unhandled promise rejections.
    • Improved URL-opening reliability with better error handling and a fallback for web links; telemetry is now recorded with reduced/excluded sensitive URL parts.
  • Tests

    • Expanded test coverage for URL opening and error scenarios across embedder and browser environments.

Review Change Stack

oxoxDev added 2 commits May 11, 2026 15:34
…inyhumansai#1472)

The CEF embedder injects `window.ipc.postMessage` after `on_after_created`
fires on the renderer side. A click landing in the gap before injection
causes `tauri-plugin-opener`'s IPC glue to reject with `TypeError: Cannot
read properties of undefined (reading 'postMessage')` — surfaced as
OPENHUMAN-REACT-T/S/R (Settings → Billing).

Wrap `tauriOpenUrl` in try/catch and fall back to `window.open` for
http(s) URLs so the dashboard still opens. Non-http schemes
(`obsidian://`, `mailto:`, …) keep propagating the error — `window.open`
would spawn a useless Tauri webview window for those, which is worse UX
than a propagated error the caller can surface. Each fallback path
records a Sentry breadcrumb so a real bridge regression remains visible.

Closes OPENHUMAN-REACT-T, OPENHUMAN-REACT-S, OPENHUMAN-REACT-R
Refs tinyhumansai#1472
…of leaking via void (tinyhumansai#1472)

`void openUrl(...)` discarded the promise, letting any rejection escape
to `window`'s `unhandledrejection` trap — which is exactly how the
postMessage TypeError reached Sentry (OPENHUMAN-REACT-T/S/R). The
upstream `openUrl` change now recovers internally for http URLs, but
swap the caller to `.catch(() => {})` so the rejection contract stays
explicit at every call site and future non-http callers don't silently
regress.

Refs tinyhumansai#1472
@oxoxDev oxoxDev requested a review from a team May 11, 2026 11:15
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8439e82f-550b-452d-bfa7-95eb48757625

📥 Commits

Reviewing files that changed from the base of the PR and between 8d7ea6a and 9e3f700.

📒 Files selected for processing (3)
  • app/src/components/settings/__tests__/SettingsHome.test.tsx
  • app/src/utils/openUrl.test.ts
  • app/src/utils/openUrl.ts
✅ Files skipped from review due to trivial changes (1)
  • app/src/components/settings/tests/SettingsHome.test.tsx

📝 Walkthrough

Walkthrough

The PR instruments Tauri URL opening in openUrl with Sentry breadcrumbs and adds a conditional fallback: on Tauri failures it logs a warning breadcrumb and falls back to window.open for http(s) URLs but rethrows for other schemes. Tests validate breadcrumb emission and fallback behavior. SettingsHome swallows promise rejections when invoking openUrl.

Changes

Tauri URL Opening with Sentry Instrumentation and Conditional Fallback

Layer / File(s) Summary
Sentry Integration & Tauri Error Handling
app/src/utils/openUrl.ts
Adds Sentry addBreadcrumb import, isHttpUrl and getTelemetryUrl helpers, expands doc comment, and wraps tauriOpenUrl in try/catch that logs a warning breadcrumb and conditionally falls back to window.open for http(s) or rethrows for other schemes.
Test Coverage
app/src/utils/openUrl.test.ts
Mocks @sentry/react addBreadcrumb and extends tests to assert breadcrumb absence on success, breadcrumb emission on Tauri failures, propagation for non-http schemes, and sanitized-origin fallback for http(s) failures.
Consumer Error Handling
app/src/components/settings/SettingsHome.tsx, app/src/components/settings/__tests__/SettingsHome.test.tsx
Billing & Usage click handler now calls openUrl(...) and appends .catch(() => {}) to avoid unhandled promise rejections; test mock updated to return a resolved promise.

Sequence Diagram(s)

sequenceDiagram
  participant SettingsHome
  participant openUrl
  participant tauriOpenUrl
  participant Sentry_addBreadcrumb
  participant Browser_window_open

  SettingsHome->>openUrl: call openUrl(BILLING_DASHBOARD_URL)
  openUrl->>tauriOpenUrl: attempt tauriOpenUrl(url)
  tauriOpenUrl-->>openUrl: success
  openUrl-->>SettingsHome: resolve

  alt tauriOpenUrl fails
    tauriOpenUrl-->>openUrl: reject/error
    openUrl->>Sentry_addBreadcrumb: addBreadcrumb('warning', 'ipc', {data.url: sanitized})
    alt url is http(s)
      openUrl->>Browser_window_open: window.open(origin, features)
      openUrl-->>SettingsHome: resolve
    else non-http
      openUrl-->>SettingsHome: throw error
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • tinyhumansai/openhuman#956: Modifies the same SettingsHome "Billing & Usage" click handler to call openUrl(BILLING_DASHBOARD_URL).

Poem

🐰 I hop the URL through Tauri's gate,
If it stumbles, I log its fate.
HTTP finds a browser seat,
Other schemes return to sender's beat.
Breadcrumbs trail each careful state.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding a fallback to window.open when the CEF IPC handle is not ready, which is the core fix implemented across openUrl.ts, its test file, and the call site in SettingsHome.tsx.
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.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/src/components/settings/SettingsHome.tsx (1)

211-213: ⚡ Quick win

Prefer async/await over an empty .catch() chain here.

This still fixes the unhandled-rejection problem, but the changed code now uses promise chaining in a repo that prefers async/await. A small try/catch makes the intentional swallow clearer.

Suggested refactor
           onClick: () => {
-            openUrl(BILLING_DASHBOARD_URL).catch(() => {});
+            void (async () => {
+              try {
+                await openUrl(BILLING_DASHBOARD_URL);
+              } catch {
+                // Prevent unhandled rejections; openUrl already records diagnostics.
+              }
+            })();
           },

As per coding guidelines Always use async/await for promise handling instead of .then() chains in TypeScript code.

🤖 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 `@app/src/components/settings/SettingsHome.tsx` around lines 211 - 213, Replace
the inline promise chain in the SettingsHome onClick handler with an async/await
pattern: make the onClick callback async, await openUrl(BILLING_DASHBOARD_URL)
inside a try/catch, and explicitly handle or intentionally swallow the error in
the catch block (or log it via the existing logger) so the unhandled-rejection
is avoided while following the repo's async/await convention; locate the onClick
for the billing button in SettingsHome.tsx that calls
openUrl(BILLING_DASHBOARD_URL) and update it accordingly.
🤖 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 `@app/src/utils/openUrl.ts`:
- Around line 35-39: The breadcrumb currently sends the full url to Sentry;
modify openUrl to redact the URL before adding the breadcrumb (e.g., strip
path/query/fragment and any mailto/local path details, leaving only a minimal
scheme+host or a fixed "<redacted>" token). Create or call a small helper (e.g.,
redactUrl(url)) and pass its output in Sentry.addBreadcrumb data instead of the
raw url, but continue to include the error (err) as before; update the
Sentry.addBreadcrumb call in openUrl to use the redacted value.

---

Nitpick comments:
In `@app/src/components/settings/SettingsHome.tsx`:
- Around line 211-213: Replace the inline promise chain in the SettingsHome
onClick handler with an async/await pattern: make the onClick callback async,
await openUrl(BILLING_DASHBOARD_URL) inside a try/catch, and explicitly handle
or intentionally swallow the error in the catch block (or log it via the
existing logger) so the unhandled-rejection is avoided while following the
repo's async/await convention; locate the onClick for the billing button in
SettingsHome.tsx that calls openUrl(BILLING_DASHBOARD_URL) and update it
accordingly.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a092adb0-6139-422b-94c7-5edd5cabcf18

📥 Commits

Reviewing files that changed from the base of the PR and between a312a85 and 8d7ea6a.

📒 Files selected for processing (3)
  • app/src/components/settings/SettingsHome.tsx
  • app/src/utils/openUrl.test.ts
  • app/src/utils/openUrl.ts

Comment thread app/src/utils/openUrl.ts Outdated
… mock (tinyhumansai#1472)

Two follow-ups to the PR tinyhumansai#1491 review:

1. CodeRabbit flagged that `openUrl` is a generic helper, so logging the
   raw URL in the Sentry breadcrumb can leak emails (`mailto:`), local
   filesystem paths (`obsidian://...?path=/Users/me/Vault`), or signed
   query params on http(s) links. Funnel the breadcrumb through a new
   `getTelemetryUrl()` helper that keeps only `origin` for http(s) and
   only the protocol for other schemes. The URL passed to the actual
   opener / window.open is untouched — only the telemetry payload is
   redacted. Tests assert the redaction by feeding URLs with secrets
   and asserting they do not appear in the breadcrumb.

2. `SettingsHome.test.tsx` mocked `openUrl` as `vi.fn()` (returns
   `undefined`). PR tinyhumansai#1491 changed the Billing onClick from
   `void openUrl(...)` to `openUrl(...).catch(...)`, so the existing
   "opens billing URL when Billing & Usage is clicked" test now hit
   `undefined.catch` and threw. Mock now resolves to a real Promise,
   matching the `Promise<void>` contract of the real `openUrl`.

Refs tinyhumansai#1472
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 11, 2026

Addressed in 9e3f7008:

  • CodeRabbit (openUrl.ts:39, URL redaction) — new getTelemetryUrl() helper. http(s) → origin only (path/query/fragment dropped). Other schemes → protocol only. URL passed to the actual opener / window.open is untouched; only the Sentry breadcrumb payload is redacted. Tests assert that secrets in the input URL do not appear in the breadcrumb (?token=secret-redact-me, obsidian://...?path=/Users/me/Vault).
  • Frontend Unit Tests CISettingsHome.test.tsx mocked openUrl as vi.fn() (returning undefined). PR-A swapped void openUrl(...) for openUrl(...).catch(...), hitting undefined.catch in tests. Mock now mockResolvedValue(undefined) matching the real Promise<void> contract.

Local: pnpm typecheck ✓, pnpm lint ✓ (0 errors), pnpm format:check ✓, pnpm debug unit src/utils/openUrl.test.ts 4/4 ✓, pnpm debug unit src/components/settings/__tests__/SettingsHome.test.tsx 22/22 ✓.

@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 11, 2026

Status after 9e3f7008:

Check Result
CodeRabbit ✅ APPROVED (URL redaction addressed)
Frontend Unit Tests ✅ SUCCESS
Frontend Coverage (Vitest) ✅ SUCCESS
Type Check TypeScript ✅ SUCCESS
Rust Core Tests + Quality ✅ SUCCESS
Rust Tauri Shell Tests ✅ SUCCESS
Rust Tauri Coverage ✅ SUCCESS
Rust Quality (fmt + clippy) ✅ SUCCESS
Coverage Matrix Sync, PR Submission, install.ps1, Smoke install.sh × 2
Rust Core Coverage (cargo-llvm-cov) pre-existing flake on main

The single remaining failure is openhuman::update::ops::tests::update_apply_rejects_when_rpc_mutations_disabled (panic at src/openhuman/update/ops.rs:556):

assertion failed: outcome.value["error"].as_str().is_some_and(|value|
    value.contains("rpc_mutations_enabled=false"))

Happy to rebase + re-run if it flips green on retry, or to land on a green PR if maintainers fix the coverage flake first. Otherwise this PR is ready for review.

@senamakel senamakel merged commit ef998ca into tinyhumansai:main May 11, 2026
23 of 24 checks passed
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