diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 6307c3b912..032690f8b4 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -239,22 +239,63 @@ (recur cause))))))] (with-out-str - (print-all cause))))) + (print-all cause)))) + + :cljs + (defn format-throwable + [cause & {:as opts}] + (with-out-str + (when-let [exdata (ex-data cause)] + (when-let [hint (get exdata :hint)] + (when (str/index-of hint "\n") + (println "Hint:") + (println "--------------------") + (println hint) + (println))) + + (when-let [explain (get exdata ::sm/explain)] + (println "Explain:") + (println "--------------------") + + (println (sm/humanize-explain explain)) + (println)) + + (when-let [explain (get exdata :explain)] + (println "Server Explain:") + (println "--------------------") + (println explain)) + + (println "Data:") + (println "--------------------") + (pp/pprint (dissoc exdata ::sm/explain :explain)) + (println)) + + (when-let [trace (.-stack cause)] + (println "Trace:") + (println "--------------------") + (println (.-stack cause)))))) + +(defn first-line + [s] + (let [break-index (str/index-of s "\n")] + (if (pos? break-index) + (subs s 0 break-index) + s))) (defn print-throwable [cause & {:as opts}] #?(:clj (println (format-throwable cause opts)) :cljs - (let [prefix (get opts :prefix "exception") - title (str prefix ": " (ex-message cause)) - exdata (ex-data cause)] + (let [prefix (get opts :prefix) + data (ex-data cause) + title (cond->> (or (some-> (:hint data) first-line) + (ex-message cause)) + (string? prefix) + (str prefix ": "))] + (js/console.group title) - (when-let [explain (get exdata ::sm/explain)] - (println (sm/humanize-explain explain))) - - (js/console.log "\nData:") - (pp/pprint (dissoc exdata ::sm/explain)) - - (js/console.log "\nTrace:") - (js/console.error (.-stack cause))))) + (try + (js/console.log (format-throwable cause)) + (finally + (js/console.groupEnd)))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 74cd9444f3..df6911fdba 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -7,17 +7,20 @@ (ns app.main.errors "Generic error handling" (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.pprint :as pp] [app.common.schema :as sm] + [app.config :as cf] [app.main.data.auth :as da] + [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.data.workspace :as-alias dw] [app.main.router :as rt] [app.main.store :as st] [app.main.worker] - [app.util.globals :as glob] + [app.util.globals :as g] [app.util.i18n :refer [tr]] [app.util.timers :as ts] [cuerdas.core :as str] @@ -32,45 +35,7 @@ ;; Will contain last uncaught exception (def last-exception nil) -(defn- print-data! - [data] - (-> data - (dissoc ::sm/explain) - (dissoc :explain) - (dissoc ::trace) - (dissoc ::instance) - (pp/pprint {:width 70}))) - -(defn- print-explain! - [data] - (when-let [{:keys [errors] :as explain} (::sm/explain data)] - (let [errors (mapv #(update % :schema sm/form) errors)] - (pp/pprint errors {:width 100 :level 15 :length 20}))) - - (when-let [explain (:explain data)] - (js/console.log explain))) - -(defn- print-trace! - [data] - (some-> data ::trace js/console.log)) - -(defn- print-group! - [message f] - (try - (js/console.group message) - (f) - (catch :default _ nil) - (finally - (js/console.groupEnd message)))) - -(defn print-cause! - [message cause] - (print-group! message (fn [] - (print-data! cause) - (print-explain! cause) - (print-trace! cause)))) - -(defn exception->error-data +(defn- exception->error-data [cause] (let [data (ex-data cause)] (-> data @@ -78,18 +43,6 @@ (assoc ::instance cause) (assoc ::trace (.-stack cause))))) -(defn print-error! - [cause] - (cond - (map? cause) - (print-cause! (:hint cause "Unexpected Error") cause) - - (ex/error? cause) - (print-cause! (ex-message cause) (ex-data cause)) - - :else - (print-cause! (ex-message cause) (exception->error-data cause)))) - (defn on-error "A general purpose error handler." [error] @@ -104,13 +57,65 @@ ;; Set the main potok error handler (reset! st/on-error on-error) +(defn generate-report + [cause] + (try + (let [team-id (:current-team-id @st/state) + file-id (:current-file-id @st/state) + profile-id (:profile-id @st/state) + data (ex-data cause)] + + (with-out-str + (println "Context:") + (println "--------------------") + (println "Hint: " (or (:hint data) (ex-message cause) "--")) + (println "Prof ID: " (str (or profile-id "--"))) + (println "Team ID: " (str (or team-id "--"))) + (when-let [file-id (or (:file-id data) file-id)] + (println "File ID: " (str file-id))) + (println "Version: " (:full cf/version)) + (println "URI: " cf/public-uri) + (println "HREF: " (.-href g/location)) + (println) + + (println + (ex/format-throwable cause)) + (println) + + (println "Last events:") + (println "--------------------") + (pp/pprint @st/last-events {:length 200}) + (println))) + (catch :default cause + (.error js/console "error on generating report" cause) + nil))) + +(defn- show-not-blocking-error + "Show a non user blocking error notification" + [cause] + (let [data (ex-data cause) + hint (or (some-> (:hint data) ex/first-line) + (ex-message cause))] + + (st/emit! + (ev/event {::ev/name "unhandled-exception" + :hint hint + :type (get data :type :unknown) + :report (generate-report cause)}) + + (ntf/show {:content (tr "errors.unexpected-exception" hint) + :type :toast + :level :error + :timeout 3000})))) + (defmethod ptk/handle-error :default [error] - (st/async-emit! (rt/assign-exception error)) - (print-group! "Unhandled Error" - (fn [] - (print-trace! error) - (print-data! error)))) + (if (and (string? (:hint error)) + (str/starts-with? (:hint error) "Assert failed:")) + (ptk/handle-error (assoc error :type :assertion)) + (when-let [cause (::instance error)] + (ex/print-throwable cause :prefix "Unexpected Error") + (show-not-blocking-error cause)))) ;; We receive a explicit authentication error; If the uri is for ;; workspace, dashboard, viewer or settings, then assign the exception @@ -141,11 +146,9 @@ ;; and frontend. (defmethod ptk/handle-error :validation - [{:keys [code] :as error}] - (print-group! "Validation Error" - (fn [] - (print-data! error) - (print-explain! error))) + [{:keys [code ::instance] :as error}] + (ex/print-throwable instance :prefix "Validation Error") + (cond (= code :invalid-paste-data) (let [message (tr "errors.paste-data-validation")] @@ -193,23 +196,14 @@ :else (st/async-emit! (rt/assign-exception error)))) - ;; This is a pure frontend error that can be caused by an active ;; assertion (assertion that is preserved on production builds). From ;; the user perspective this should be treated as internal error. (defmethod ptk/handle-error :assertion [error] - (ts/schedule - #(st/emit! (ntf/show {:content (tr "errors.internal-assertion-error") - :type :toast - :level :error - :timeout 3000}))) - - (print-group! "Internal Assertion Error" - (fn [] - (print-trace! error) - (print-data! error) - (print-explain! error)))) + (when-let [cause (::instance error)] + (show-not-blocking-error cause) + (ex/print-throwable cause :prefix "Assertion Error"))) ;; ;; All the errors that happens on worker are handled here. (defmethod ptk/handle-error :worker-error @@ -221,9 +215,8 @@ :level :error :timeout 3000}))) - (print-group! "Internal Worker Error" - (fn [] - (print-data! error)))) + (some-> (::instance error) + (ex/print-throwable :prefix "Web Worker Error"))) ;; Error on parsing an SVG (defmethod ptk/handle-error :svg-parser @@ -251,12 +244,9 @@ (derive :service-unavailable ::exceptional-state) (defmethod ptk/handle-error ::exceptional-state - [error] - (when-let [cause (::instance error)] - (js/console.log (.-stack cause))) - - (ts/schedule - #(st/emit! (rt/assign-exception error)))) + [{:keys [::instance] :as error}] + (ex/print-throwable instance :prefix "Exceptional State") + (ts/schedule #(st/emit! (rt/assign-exception error)))) (defn- redirect-to-dashboard [] @@ -264,7 +254,7 @@ project-id (:current-project-id @st/state)] (if (and project-id team-id) (st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id})) - (set! (.-href glob/location) "")))) + (set! (.-href g/location) "")))) (defmethod ptk/handle-error :restriction [{:keys [code] :as error}] @@ -312,34 +302,19 @@ :text (tr "errors.deprecated.contact.text") :after (tr "errors.deprecated.contact.after") :on-click #(st/emit! (rt/nav :settings-feedback))}})) - :else - (print-cause! "Restriction Error" error))) + (when-let [cause (::instance error)] + (ex/print-throwable cause :prefix "Restriction Error") + (show-not-blocking-error cause)))) ;; This happens when the backed server fails to process the ;; request. This can be caused by an internal assertion or any other ;; uncontrolled error. (defmethod ptk/handle-error :server-error - [error] - (st/async-emit! (rt/assign-exception error)) - (print-group! "Server Error" - (fn [] - (print-data! (dissoc error :data)) - - (when-let [werror (:data error)] - (cond - (= :assertion (:type werror)) - (print-group! "Assertion Error" - (fn [] - (print-data! werror) - (print-explain! werror))) - - :else - (print-group! "Unexpected" - (fn [] - (print-data! werror) - (print-explain! werror)))))))) + [{:keys [::instance] :as error}] + (ex/print-throwable instance :prefix "Server Error") + (st/async-emit! (rt/assign-exception error))) (defonce uncaught-error-handler (letfn [(is-ignorable-exception? [cause] @@ -354,28 +329,19 @@ (when-let [cause (unchecked-get event "error")] (set! last-exception cause) (when-not (is-ignorable-exception? cause) - (ex/print-throwable cause :prefix "uncaught exception") - (st/async-emit! - (ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause)) - :type :toast - :level :error - :timeout 3000}))))) - + (ex/print-throwable cause :prefix "Uncaught Exception") + (ts/schedule #(show-not-blocking-error cause))))) (on-unhandled-rejection [event] (.preventDefault ^js event) (when-let [cause (unchecked-get event "reason")] (set! last-exception cause) - (ex/print-throwable cause :prefix "uncaught rejection") - (st/async-emit! - (ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause)) - :type :toast - :level :error - :timeout 3000}))))] + (ex/print-throwable cause :prefix "Uncaught Rejection") + (ts/schedule #(show-not-blocking-error cause))))] - (.addEventListener glob/window "error" on-unhandled-error) - (.addEventListener glob/window "unhandledrejection" on-unhandled-rejection) + (.addEventListener g/window "error" on-unhandled-error) + (.addEventListener g/window "unhandledrejection" on-unhandled-rejection) (fn [] - (.removeEventListener glob/window "error" on-unhandled-error) - (.removeEventListener glob/window "unhandledrejection" on-unhandled-rejection)))) + (.removeEventListener g/window "error" on-unhandled-error) + (.removeEventListener g/window "unhandledrejection" on-unhandled-rejection)))) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 8950aaf93d..f5f5d62b9f 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.exceptions :as ex] [app.common.logging :as log] [app.main.data.dashboard :as dd] [app.main.data.event :as ev] @@ -360,7 +361,7 @@ on-error (fn [cause] (reset! status* :error) - (errors/print-error! cause) + (ex/print-throwable cause) (rx/of (modal/hide) (ntf/error (tr "dashboard.libraries-and-templates.import-error")))) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 1140ee319b..d6df101087 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -442,7 +442,7 @@ [{:keys [data route] :as props}] (let [type (get data :type) report (mf/with-memo [data] - (generate-report data)) + (some-> data ::errors/instance errors/generate-report)) props (mf/spread-props props {:report report})] (mf/with-effect [data route report] diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index 6da1b7e27f..fa2641c7b3 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -391,7 +391,7 @@ (group-by :code) (clj->js)) (catch :default cause - (errors/print-error! cause)))))) + (ex/print-throwable cause)))))) (defn ^:export validate-schema [] @@ -399,7 +399,7 @@ (let [file (dsh/lookup-file @st/state)] (cfv/validate-file-schema! file)) (catch :default cause - (errors/print-error! cause)))) + (ex/print-throwable cause)))) (defn ^:export repair [reload?] @@ -431,7 +431,7 @@ (when reload? (dom/reload-current-window))) (fn [cause] - (errors/print-error! cause))))))))) + (ex/print-throwable cause))))))))) (defn ^:export fix-orphan-shapes [] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index fba9f1dec9..10ea4dd8b9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1344,7 +1344,7 @@ msgid "errors.generic" msgstr "Something wrong has happened." msgid "errors.unexpected-exception" -msgstr "Unexpected exception: %s" +msgstr "Unexpected error: %s" #: src/app/main/errors.cljs:200 msgid "errors.internal-assertion-error"