Skip to content

[OPIK-7428] [FE] Prevent Google Translate removeChild crash on mobile get-started#7556

Merged
awkoy merged 1 commit into
mainfrom
awkoy/OPIK-7428-onboarding-translate-crash
Jul 21, 2026
Merged

[OPIK-7428] [FE] Prevent Google Translate removeChild crash on mobile get-started#7556
awkoy merged 1 commit into
mainfrom
awkoy/OPIK-7428-onboarding-translate-crash

Conversation

@awkoy

@awkoy awkoy commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Details

Production console crash on the Opik get-started onboarding (mobile), reported from a Chrome/Android session in a non-English locale:

NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

MobileOnboarding/WelcomeStep.tsx renders a JS typewriter (TypingText) that rewrites a text node every 30–80 ms. When the browser auto-translates the page, Google Translate rewrites the DOM (wrapping text nodes in <font> elements). React's next commit then calls removeChild on a node whose parent changed and throws — the continuous animation makes the crash near-certain once translation is active (see facebook/react#11538).

Fix: mark the animated demo prompt translate="no" so the browser excludes it from translation (Google's recommended handling for JS-updated dynamic content). Translate no longer mutates that subtree, so React never hits the crash. The typewriter animation is unchanged and the rest of the page still translates; the only trade-off is that this one decorative example prompt is not translated.

Change checklist

  • User facing
  • Documentation update

Issues

OPIK-7428

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 4.8
  • Scope: root-cause investigation, the one-line translate="no" fix, and local reproduction
  • Human verification: crash reproduced and fix verified locally (mobile viewport + Chrome translate) before submission

Testing

  • Reproduced the crash locally with a mobile viewport + Chrome page translation, and confirmed it no longer occurs with the fix.
  • ESLint clean (--max-warnings=0); frontend eslint + typecheck CI checks pass.

Documentation

No documentation changes needed — the fix is a single translate="no" attribute on the animated onboarding prompt.

… get-started

The mobile WelcomeStep typewriter (TypingText) rewrites a text node every
30-80ms. When the browser auto-translates the page, Google Translate rewrites
the DOM (wrapping text nodes in <font> elements), and React's next commit then
throws "NotFoundError: Failed to execute 'removeChild' on 'Node'". The
continuous animation makes the crash near-certain once translation is active.

Mark the animated demo prompt translate="no" so the browser excludes it from
translation (Google's recommended handling for JS-updated dynamic content).
Translate no longer mutates that subtree, so React never hits the crash. The
animation is unchanged and the rest of the page still translates.

OPIK-7428

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
🌐 typecheck — frontend Whole-project tsc type check 42.98s
☕ spotless — java backend Format Java code 3.63s
🌐 eslint — frontend Lint + autofix JS/TS 3.36s
Total (3 ran) 49.97s
⏭️ 38 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️

@awkoy awkoy changed the title fix(onboarding): prevent Google Translate removeChild crash on mobile get-started [OPIK-7428] [OPIK-7428] [FE] Prevent Google Translate removeChild crash on mobile get-started Jul 21, 2026
still translates. */}
<div
className="min-h-5 text-sm leading-5 text-foreground"
translate="no"

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.

💡 suggestion | Testing

Nice, tightly-scoped fix — and I was able to fully verify it. I reproduced the exact NotFoundError: removeChild crash on main in a mobile Chrome viewport by racing the live typewriter against a faithful Google-Translate DOM mutation (wrapping the animated text node in <font>), and confirmed the crash disappears with this translate="no" in place while the animation keeps running. ✅

Since the whole fix hinges on this one attribute staying put, here's an optional regression test that fails without it and passes with it (deterministic, no flakiness). Feel free to add it as WelcomeStep.test.tsx if you like — no pressure:

import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
import WelcomeStep from "./WelcomeStep";

describe("WelcomeStep", () => {
  // Regression guard for OPIK-7428: the typewriter rewrites this text node
  // every few dozen ms. If the browser is allowed to translate it, Google
  // Translate re-parents the node (wrapping it in <font>) and React's next
  // commit crashes with "NotFoundError: removeChild". translate="no" excludes
  // just this animated node, which prevents the crash.
  it("marks the animated demo prompt as non-translatable", () => {
    const { container } = render(<WelcomeStep />);

    const noTranslate = container.querySelector('[translate="no"]');

    expect(noTranslate).not.toBeNull();
    // The caret span lives inside the animated node, so its non-translatable
    // ancestor is the exact subtree that must be excluded.
    expect(noTranslate?.querySelector(".animate-caret-blink")).not.toBeNull();
  });
});

🤖 Review posted via /review-github-pr

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

Approved — verified end-to-end.

I reproduced the reported NotFoundError: removeChild crash on main (mobile Chrome viewport, live typewriter raced against a faithful Google-Translate DOM mutation) and confirmed it's gone with this translate="no" fix while the animation keeps running. Root cause, fix, and scope all check out; the trade-off (one decorative demo prompt not translated) is well-documented.

Left one optional, non-blocking suggestion inline: a small regression test that guards the attribute.

🤖 Review posted via /review-github-pr

@awkoy
awkoy merged commit 6669c89 into main Jul 21, 2026
15 of 16 checks passed
@awkoy
awkoy deleted the awkoy/OPIK-7428-onboarding-translate-crash branch July 21, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants