🐛 Ignore errors from unknown sources (#11816)

This commit is contained in:
Alonso Torres 2026-09-25 09:40:06 +02:00 committed by GitHub
parent 773fc2295e
commit c497bbeb0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 104 additions and 4 deletions

View File

@ -588,13 +588,58 @@
(and (string? stack)
(str/includes? stack "posthog"))))
;; Captures the script location of a stack frame: the part preceding
;; ":line:column", in both the V8 ("at fn (location:1:2)") and the
;; SpiderMonkey/JSC ("fn@location:1:2") formats.
(def ^:private frame-location-rx
#"(?:at |[@(])([^\s()]+):\d+:\d+")
(defn- stack-frames
"The frame lines of a stack trace, without the leading message line
that V8 puts before them."
[stack]
(into []
(comp (map str/trim)
(filter (fn [line]
(or (str/starts-with? line "at ")
(str/includes? line "@")))))
(str/lines stack)))
(defn- located-frame?
"True when a stack frame names the script it belongs to. Code injected
into the page carries no script to name and reports `<anonymous>`."
[frame]
(when-let [location (second (re-find frame-location-rx frame))]
(not (str/starts-with? location "<"))))
(defn- from-injected-code?
"True when the stack is blank, or has frames but none of them names a
script. Either way the error was raised by third-party code injected
into the page (an extension content script, a desktop wrapper, a
bookmarklet) rather than by application code. Firefox, for instance,
raises \"can't access dead object\" with a blank stack when an unloaded
extension's code runs.
Anything our own code calls keeps our frames further down the stack, so
a stack without a single named script never belongs to us and there is
nothing actionable on our side."
[cause]
(let [stack (.-stack cause)]
(and (string? stack)
(or (str/blank? stack)
(let [frames (stack-frames stack)]
(and (some? (seq frames))
(not (some located-frame? frames))))))))
(defn is-ignorable-exception?
"True when the error is known to be harmless (browser extensions, analytics,
React/extension DOM conflicts, etc.) and should NOT be surfaced to the user."
"True when the error is known to be harmless (browser extensions, injected
third-party code, analytics, React/extension DOM conflicts, etc.) and
should NOT be surfaced to the user."
[cause]
(let [message (ex-message cause)]
(or (from-extension? cause)
(from-posthog? cause)
(from-injected-code? cause)
(= message "Possible side-effect in debug-evaluate")
(= message "Unexpected end of input")
(str/starts-with? message "invalid props on component")

View File

@ -10,8 +10,9 @@
[cljs.test :as t :include-macros true]))
(defn- make-error
"Create a JS Error-like object with the given name, message, and optional stack."
[error-name message & {:keys [stack] :or {stack ""}}]
"Create a JS Error-like object with the given name, message, and optional
stack. Without a stack, the error keeps the one the runtime gave it."
[error-name message & {:keys [stack]}]
(let [err (js/Error. message)]
(set! (.-name err) error-name)
(when (some? stack)
@ -93,3 +94,57 @@
(t/testing "Regular TypeError is NOT ignorable"
(let [cause (make-error "TypeError" "undefined is not a function")]
(t/is (false? (errors/is-ignorable-exception? cause))))))
(t/deftest test-ignorable-injected-script
(t/testing "Errors raised by code injected into the page are ignorable"
(let [cause (make-error "TypeError" "Cannot read properties of undefined (reading 'get')"
:stack (str "TypeError: Cannot read properties of undefined (reading 'get')\n"
" at nativeVis (<anonymous>:5:35)\n"
" at HTMLDocument.<anonymous> (<anonymous>:21:64)\n"
" at <anonymous>:17:33"))]
(t/is (true? (errors/is-ignorable-exception? cause))))))
(t/deftest test-not-ignorable-injected-script-calling-into-app
(t/testing "Errors whose stack reaches application code are NOT ignorable"
(let [cause (make-error "TypeError" "Cannot read properties of undefined (reading 'get')"
:stack (str "TypeError: Cannot read properties of undefined (reading 'get')\n"
" at nativeVis (<anonymous>:5:35)\n"
" at zLe (https://design.penpot.app/js/main.js:42:13)"))]
(t/is (false? (errors/is-ignorable-exception? cause))))))
(t/deftest test-not-ignorable-release-build-v8-stack
(t/testing "Application errors from a release build are NOT ignorable (V8 stack)"
;; Frames as they appear in production: advanced-compiled names, an
;; asset URL carrying the version query string, and a trailing frame
;; without a function name.
(let [cause (make-error "TypeError" "Cannot read properties of null (reading 'toString')"
:stack (str "TypeError: Cannot read properties of null (reading 'toString')\n"
" at $app$main$ui$workspace$shapes$path$editor$path_editor_STAR_$$ "
"(https://design.penpot.app/js/main-workspace.js?version=2.18.0-RC5-1789051786:8580:99)\n"
" at $re (https://design.penpot.app/js/libs.js?version=2.18.0-RC5-1789051786:150:48345)\n"
" at https://design.penpot.app/js/libs.js?version=2.18.0-RC5-1789051786:150:125675"))]
(t/is (false? (errors/is-ignorable-exception? cause))))))
(t/deftest test-not-ignorable-release-build-spidermonkey-stack
(t/testing "Application errors from a release build are NOT ignorable (SpiderMonkey/JSC stack)"
;; SpiderMonkey stacks carry no message line and name the script after
;; an @ instead of wrapping it in parentheses.
(let [cause (make-error "InternalError" "too much recursion"
:stack (str "$APP.$JSCompiler_prototypeAlias$$.$inode_lookup$"
"@https://design.penpot.app/js/shared.js?version=2.18.0-RC5-1789051786:23076:58\n"
"$app$common$types$container$get_component_shape$cljs$0core$0IFn$0_invoke$0arity$03$$"
"@https://design.penpot.app/js/shared.js?version=2.18.0-RC5-1789051786:7596:1"))]
(t/is (false? (errors/is-ignorable-exception? cause))))))
(t/deftest test-ignorable-blank-stack
(t/testing "Errors with a blank stack are ignorable"
;; Firefox raises "can't access dead object" with an empty stack when
;; code from an unloaded extension runs in the page.
(let [cause (make-error "TypeError" "can't access dead object" :stack "")]
(t/is (true? (errors/is-ignorable-exception? cause))))))
(t/deftest test-not-ignorable-message-only-stack
(t/testing "Errors whose stack holds only the message line are NOT ignorable"
;; V8 gives native rejections such as a failed fetch no frames at all.
(let [cause (make-error "TypeError" "Failed to fetch" :stack "TypeError: Failed to fetch")]
(t/is (false? (errors/is-ignorable-exception? cause))))))