Skip to content

Debugging

Viktor Eriksson edited this page May 26, 2016 · 73 revisions

This page describes a simple and effective technique for debugging re-frame apps. It proposes a particular combination of tools. By the end, you'll be better at dominos.

Know The Beast!

re-frame apps are event driven.

Event driven apps have this core, perpetual loop:

  1. your app is in some quiescent state, patiently waiting for the next event
  2. an event arrives (user presses a button, a websocket gets data, etc)
  3. computation/processing follows as the event is handled, leading to changes in app state, the UI, etc
  4. Goto 1

When debugging an event driven system, our focus will be step 3.

re-frame's Step 3

With re-frame, step 3 happens like this:

3.1. a (dispatch [:event-id ....]) happens (that's how events are initiated)

3.2. an Event Handler is run (along with middleware), changing the value in app-db.

3.3. one or more subscriptions fire (because of 3.2)

3.4. components rerender (because of 3.3)

Every single event is processed in the same way. Every single one.

It is like a four domino sequence: an event arrives and then bang, bang, bang, one domino triggers the next. A delightfully regular environment to understand and debug!

Observe The Beast

Bret Victor has explained to us the importance of observability. In which case, when we are debugging re-frame, what do we want to observe?

re-frame's four domino process involves data values flowing in and out of relatively simple, pure functions. Derived data flowing. So, to debug we want to observe:

  • which functions are called
  • what data flowed in and out of them

Functions and data: What data was in the event? What event handler was then called? What middleware then ran? What state changes did that event handler cause? What subscription handlers were then triggered? What new values did they then return? And which Reagent components then rerendered? What hiccup did they return? It's all just functions processing data.

So, in Clojurescript, how do we observe functions and data? Well, as luck would have it, ClojureScript is a lisp and it is readily traceable.

How To Trace?

I suggest a particular combination of technologies which, working together, will write a trace to the devtools console. Sorry, but there's no fancy SVG dashboard. We said simple, right?

First, use clairvoyant to trace function calls and data flow. We've had a couple of Clairvoyant PRs accepted, and they make it work well for us. We've also written a specific Clairvoyant tracer tuned for our re-frame needs. https://clojars.org/day8/re-frame-tracer.

Second, use cljs-devtools because it allows you to inspect traced data. That means you'll need to be using a very fresh version of Chrome. But it is worth it.

Finally, because we want you to easily scan, parse and drill into trace data, we'll be using Chrome devtool's console.group() and console.endGroup().

Your browser

You'll need to install clj-devtools by following these instructions.

Your Project

Add these to your project.clj :dependencies. First up a private fork of clairvoyant.

Clojars Project

Then the customised tracer for cljs-devtools that includes a colour choice Clojars Project

Next, we're going to assume that you have structured you app in the recommended way You'll need to make changes to handlers.cljs, subs.cljs and views.cljs. These namespaces contain the functions we want to trace.

  1. At the top of each add these requires:

     [clairvoyant.core :refer-macros [trace-forms]]
     [re-frame-tracer.core :refer [tracer]]
  2. Then, immediately after the ns form add (if you want a green colour):

    (trace-forms {:tracer (tracer :color "green")}
  3. Finally, put in a closing ) at the end of the file.

  4. Colour choice

    We have sauntered in the direction of the following colours

    file colour
    handlers.clj green
    subs.cljs brown
    views.clj gold

    But I still think orange, flared pants are a good look. So, yeah. You may end up choosing others.

  5. Subscriptions issues

    Unfortunately since both trace-forms and reaction are macros they don't work well together. So there is some necessary changes to your subscriptions code to get them to work with clairvoyant, you need to replace the macro reaction with the function make-reaction.

    so the following code

    (ns my.ns
      (:require-macros [reagent.ratom :refer [reaction]]))
    
    ;; ...
    
    (subs/register
      :my-sub
      (fn
        [db _]
        (reaction (get-in @db [db-root :my-sub]))))

    needs to become

    (ns my.ns
      (:require [reagent.ratom :refer [make-reaction]]))
    
    ;; ...
    
    (subs/register
      :my-sub
      (fn
        [db _]
        (make-reaction (fn my-subscription 
                         []
                        (get-in @db [db-root :my-sub])))))

Say No To Anonymous

To get good quality tracing, you need to provide names for all your functions. Don't let functions be anonymous.

For example, make sure you name the renderer in a Form2 component:

(defn my-view
   []
   (let [name   (subscribe [:name])]
      (fn my-view-rendere []                ;;   <--  name it!! 
        [:div @name])))

And name those event handlers:

(register-handler
   :blah
   [middlewares]
   (fn blah-handler [db v]        ;;   <-- name it
      (reaction (:blah @db))))

Dev vs Prod - IMPORTANT

By default, our clairvoyant fork does not produce any trace!!

You must throw a compile-time switch for tracing to be included into development builds.

If you are using lein, do this in your project.clj file:

:cljsbuild {:builds [{:id "dev"            ;; for the development build, turn on tracing
                      ....
                      :closure-defines {"clairvoyant.core.devmode" true}
                      }]}

So, just to be clear, if you see no tracing when you are debugging, it is almost certainly because you haven't successfully turned on this switch. Your production builds need to nothing because, by default, all trace is compiled out of the code.

The result

Load your app, and open the dev-tools console. Make an event happen (click a button?). Notice the colour coded tracing showing the functions being called and the derived data flowing.

Do you see the dominos?

Warning

If the functions you are tracing take large data-structures as parameters, or return large values, then you will be asking clairvoyant to push/log a LOT of data into the js/console. This can take a while and might mean devtools takes a lot of RAM.

For example, if your app-db was big and complicated, you might use path middleware to "narrow" that part of app-db passed into your event handler because logging all of app-db to js/console might take a while (and not be that useful).

Macro Help

From @mccraigmccraig we get the following (untested by me, but they look great):

I finally had enough of all the boilerplate required to use clairvoyant with re-frame subs & handlers and wrote some code to tidy it up...

https://www.refheap.com/799f89796b078b6e2459df038

gives you subs like this - https://www.refheap.com/e80f7f982f2bf75bd36bb1062

and handlers like this - https://www.refheap.com/e6a6a3a78eb768de386f54b49

React Native

If you have not enabled Remote JS Debugging in the emulator you will get the following error related to console.groupCollapsed:

[TypeError: console.groupCollapsed is not a function. (In 'console.groupCollapsed("%c%s",[cljs.core.str("color:"),cljs.core.str(self__.color),cljs.core.str(";")].join(''),title)', 'console.groupCollapsed' is undefined)] line: 112, column: 23

Enable Debug JS Remotely to fix this.