diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 54401c94af..8e93c9b1a7 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -438,17 +438,30 @@ (volatile! 0)) (defn on-update-loop - "Handler for the rumext update scheduler trips: logs the throttled - update loop with the events being processed, at most once per + "Handler for the rumext update scheduler trips: logs and reports the + throttled update loop with the events being processed, at most once per `report-window-ms`. Runs inside store watchers and effect bodies, so it never throws." - [_] + [{:keys [rounds]}] (try (let [now (inst-ms (ct/now))] (when (>= (- now @update-loop-warned-at) report-window-ms) (vreset! update-loop-warned-at now) - (.warn js/console "[update-scheduler] update loop throttled; in-flight events:" - (into-array (map ptk/repr-event (ptk/in-flight-events st/state)))))) + (let [events (ptk/in-flight-events st/state) + cause (ex-info "update loop throttled" + {:type :internal + :code :update-loop-throttled + :rounds rounds})] + (.warn js/console "[update-scheduler] update loop throttled; in-flight events:" + (into-array (map ptk/repr-event events))) + (when (seq events) + (.set in-flight-by-cause cause (in-flight-lines events))) + ;; Report outside the loop that tripped the scheduler + (ts/schedule + #(submit-report :event-name "handled-exception" + :hint (ex-message cause) + :report (fn [] (generate-report cause)) + :cause cause))))) (catch :default _ nil))) diff --git a/frontend/test/frontend_tests/main_errors_test.cljs b/frontend/test/frontend_tests/main_errors_test.cljs index 5954eab457..6bf9276a62 100644 --- a/frontend/test/frontend_tests/main_errors_test.cljs +++ b/frontend/test/frontend_tests/main_errors_test.cljs @@ -14,7 +14,8 @@ - events in flight – the full report lists the events the store was processing when it caught the exception; a failing stream pipeline lists the emitters of the value, not its own event - - on-update-loop – never throws into the scheduler's caller + - on-update-loop – never throws into the scheduler's caller; + reports the throttled loop with the events in flight - flash schedules async report and toast – neither the report nor ntf/show is emitted synchronously - organization SSO recovery – expired SSO sessions go back to the provider @@ -554,6 +555,28 @@ {ptk/in-flight-events (fn [_] (throw (js/Error. "in-flight read failure")))} (t/is (nil? (errors/on-update-loop {:rounds 26})))))) +(t/deftest ^:async update-loop-is-reported-with-the-events-in-flight + (errors/reset-report-governor!) + (vreset! @#'errors/update-loop-warned-at 0) + (let [events (atom []) + loop-ev (ptk/reify ::test-update-loop-source)] + (await + (mock/with-mocks* + {st/emit! (mock/stub (fn [& emitted] (swap! events into emitted))) + ptk/in-flight-events (fn [_] [loop-ev]) + st/format-last-events (mock/stub (fn [& _] "(stub last events)")) + rt/get-current-href (constantly "https://penpot.example.com/#/workspace") + tm/schedule (mock/stub (fn [f] (f)))} + (errors/on-update-loop {:rounds 26}) + ;; A second trip within the window is not reported again + (errors/on-update-loop {:rounds 26}) + (t/is (= 1 (count @events))) + (let [props (deref (first @events))] + (t/is (= "handled-exception" (::ev/name props))) + (t/is (= "update loop throttled" (:hint props))) + (t/is (str/includes? (in-flight-section (:report props)) + "test-update-loop-source"))))))) + ;; --------------------------------------------------------------------------- ;; Expired organization SSO session ;;