Skip to content

Commit

Permalink
User-friendly async stack traces
Browse files Browse the repository at this point in the history
  • Loading branch information
ggeoffrey committed Feb 9, 2024
1 parent ae1341b commit 689f5c4
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 154 deletions.
73 changes: 73 additions & 0 deletions src/contrib/str.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,76 @@ This function is a wrapper for goog.i18n.MessageFormat, supporting a subset of t
(defn date
([pattern] (partial date (new DateTimeFormat (or (DATE-FORMATS pattern) pattern))))
([formatter date] (.format formatter date))))

(defn match-position "Return the position of the first `regex` match in `string`."
[regex string]
#?(:clj (let [matcher (re-matcher regex string)]
(if (re-find matcher)
(.start matcher)
0))
:cljs (if-let [match (.exec ^js regex string)]
(.-index match)
0)))

(tests
(match-position #" at " "") := 0
(match-position #" at " "foo at bar") := 3
(match-position #" at " "foo.bar at baz") := 7
)

(defn pad-string "left-pad the first `regex` match in `string` to shift it to the given `position`.
e.g.: (pad-string #\"@\" 5 \"left@right\") => \"left @right\" -- because 'left' is 4 chars "
[regex position string]
(let [match-position (match-position regex string)]
(if (zero? match-position)
string
(str (subs string 0 match-position)
(apply str (repeat (- position match-position) " "))
(subs string match-position)))))

(tests
(pad-string #"@" 5 "left@right") := "left @right"
(pad-string #" = " 0 "var x = 1;") := "var x = 1;"
(pad-string #" = " 5 "var x = 1;") := "var x = 1;"
(pad-string #" = " 6 "var x = 1;") := "var x = 1;"
(pad-string #" = " 10 "var x = 1;") := "var x = 1;"
(pad-string #" = " 6 "var x = 1; var y = 2;") := "var x = 1; var y = 2;"
)

(defn align-regexp* "Will align all `lines` to the first match of `regex`" [regex lines]
(let [max-match-position (apply max (map (partial match-position regex) lines))]
(map (partial pad-string regex max-match-position) lines)))

(tests
(align-regexp* #" = " ["var foo = 1;"
"var bar = 11;"
"var asdf = 111;"])
:= ["var foo = 1;"
"var bar = 11;"
"var asdf = 111;"]
)

(defn align-regexp "
e.g. (align-regexp #\" = \"
\"
var x = 1;
var y = 2;
var asdf = 3;
\")
=>
\"
var x = 1;
var y = 2;
var asdf = 3;
\"
" [regex string]
(clojure.string/join "\n" (align-regexp* regex (clojure.string/split-lines string))))

(tests
(align-regexp #" = "
"var x = 1;
var y = 11;
var asdf = 111;")
:= "var x = 1;\nvar y = 11;\nvar asdf = 111;"
)

123 changes: 76 additions & 47 deletions src/hyperfiddle/electric.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
#?(:cljs [hyperfiddle.electric-client])
[hyperfiddle.electric.impl.io :as io]
[hyperfiddle.electric.debug :as dbg]
[clojure.string :as str])
[clojure.string :as str]
[contrib.str])
#?(:cljs (:require-macros
[hyperfiddle.electric :refer [offload-task offload def check-electric
client server fn fn* defn for-by for watch discard with-cycle
Expand Down Expand Up @@ -247,38 +248,6 @@ executors are allowed (i.e. to control max concurrency, timeouts etc). Currently
`(::lang/closure (let [~@(interleave args lang/arg-sym)] ~@body) ~debug-info)
`(::lang/closure (do ~@body) ~debug-info))))

(cc/defn- -splicev [args] (if (empty? args) args (into [] cat [(pop args) (peek args)])))

(hyperfiddle.electric/def Apply*
(hyperfiddle.electric/fn* [F args]
(let [spliced (-splicev args)]
(case (count spliced)
0 (new F)
1 (new F (nth spliced 0))
2 (new F (nth spliced 0) (nth spliced 1))
3 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2))
4 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3))
5 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4))
6 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5))
7 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6))
8 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7))
9 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8))
10 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9))
11 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10))
12 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11))
13 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12))
14 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13))
15 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14))
16 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15))
17 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16))
18 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17))
19 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18))
20 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18) (nth spliced 19))))))

(defmacro apply [F & args]
(assert (not (empty? args)) (str `apply " takes and Electric function and at least one argument. Given 0.")) ; matches clojure behavior
`(new Apply* ~F [~@args]))

(cc/defn -check-recur-arity [provided actual fname]
(when (not= provided actual)
(throw (ex-info (str "You `recur`d in " (or fname "<unnamed-efn>") " with " provided
Expand Down Expand Up @@ -357,7 +326,8 @@ executors are allowed (i.e. to control max concurrency, timeouts etc). Currently
(?bind-self ?name))
{::dbg/name ?name, ::dbg/type (or (::dbg/type (meta ?name)) :reactive-fn)
::dbg/meta (merge (select-keys (meta &form) [:file :line])
(select-keys (meta ?name) [:file :line]))}))))
(select-keys (meta ?name) [:file :line])
{::dbg/ns (name (.getName *ns*))})}))))

(defmacro defn [sym & fdecl]
(let [[_defn sym' & _] (macroexpand `(cc/defn ~sym ~@fdecl))] ; GG: docstring support
Expand All @@ -375,6 +345,38 @@ executors are allowed (i.e. to control max concurrency, timeouts etc). Currently
(rest fdecl)
fdecl)))))

(cc/defn- -splicev [args] (if (empty? args) args (into [] cat [(pop args) (peek args)])))

(hyperfiddle.electric/defn* Apply* [F args] ; we use `defn*` instead of e/def e/fn* for better stacktraces
(let [spliced (-splicev args)]
(case (count spliced)
0 (new F)
1 (new F (nth spliced 0))
2 (new F (nth spliced 0) (nth spliced 1))
3 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2))
4 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3))
5 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4))
6 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5))
7 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6))
8 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7))
9 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8))
10 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9))
11 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10))
12 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11))
13 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12))
14 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13))
15 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14))
16 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15))
17 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16))
18 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17))
19 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18))
20 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18) (nth spliced 19)))))

(defmacro apply [F & args]
(assert (not (empty? args)) (str `apply " takes and Electric function and at least one argument. Given 0.")) ; matches clojure behavior
`(new Apply* ~F [~@args]))


(defmacro for-by
{:style/indent 2}
[kf bindings & body]
Expand Down Expand Up @@ -466,19 +468,47 @@ Quoting it directly is idiomatic as well."
Standard electric code runs on mount, therefore there is no `on-mount`."
[f] `(new (on-unmount* ~f))) ; experimental

(cc/defn log-root-error [exception]
#?(:clj (log/error exception)
:cljs (println exception)))
(cc/defn log-root-error [exception async-stack-trace]
#?(:clj (let [ex (dbg/empty-client-exception exception)
ex (dbg/clean-jvm-stack-trace! (dbg/remove-async-stack-trace ex))
ex (dbg/add-async-frames! ex async-stack-trace)]
(if-some [data (not-empty (dissoc (ex-data ex) ::type))]
(log/error ex "Uncaugh exception:" (ex-message ex) "\n" data)
(log/error ex "Uncaugh exception")))
:cljs (js/console.error exception)))

#?(:cljs
(cc/defn- client-log-server-error [message async-trace]
(let [err (js/Error. message)]
(set! (.-stack err) (first (str/split-lines (.-stack err))))
(js/console.error err) ; We'd like to bundle these two messages into one, but chrome refuses to render "\n" after an exception.
; We would need browser-custom formatting. Not worth it today.
(js/console.log (->> (dbg/render-async-stack-trace async-trace)
(contrib.str/align-regexp #" at ")
(dbg/left-pad-stack-trace 4))
"\n" "This is a server-side exception. The full exception was printed on the server."))))

#?(:cljs
(cc/defn- client-log-client-error [ex async-trace]
(set! (.-stack ex) (dbg/cleanup-js-stack-trace (.-stack ex)))
(js/console.error ex) ; We'd like to bundle these two messages into one, but chrome refuses to render "\n" after an exception.
; We would need browser-custom formatting. Not worth it today.
(js/console.log (->> (dbg/render-async-stack-trace async-trace)
(contrib.str/align-regexp #" at ")
(dbg/left-pad-stack-trace 4)))))

(hyperfiddle.electric/defn ?PrintClientException [msg id]
(try (client
(if-some [ex (io/get-original-ex id)]
(do
(log-root-error ex)
(try (server (println "client logged an exception, too"))
(catch Pending _)))
(js/console.warn "exception printed on server: " msg)))
(catch Pending _)))
(server
(let [async-trace (::dbg/trace (ex-data lang/trace))]
(try
(client
(if-some [ex (io/get-original-ex id)]
(do
(client-log-client-error ex async-trace)
(try (server (log/info "This is a client-side exception. The full exception was printed on the client."))
(catch Pending _)))
(client-log-server-error msg async-trace)))
(catch Pending _)))))

(defmacro with-zero-config-entrypoint
{:style/indent 0}
Expand All @@ -488,8 +518,7 @@ Quoting it directly is idiomatic as well."
(catch Pending _#) ; silently ignore
(catch Cancelled e# (throw e#)) ; bypass catchall, app is shutting down
(catch Throwable err#
(log-root-error (or (io/get-original-ex (dbg/ex-id lang/trace)) err#))
(println (dbg/stack-trace lang/trace))
(log-root-error (or (io/get-original-ex (dbg/ex-id lang/trace)) err#) (dbg/get-async-trace lang/trace))
(new ?PrintClientException (ex-message err#) (dbg/ex-id lang/trace)))))

(defmacro boot-server [opts Main & args]
Expand Down
Loading

0 comments on commit 689f5c4

Please sign in to comment.