Record and replay individual visitor interaction with Elm

Lucamug
6 min readDec 20, 2017

--

I wanted to replay the user interaction with an Elm app, including the scrolling of the pages.

Something similar to what David Gilbertson nicely describe in his article A user encounters a JavaScript error. You’ll never guess what happens next!!

Elm has an official “History Explorer” that already does a good job out of the box, but it doesn’t replay commands, that are needed to change the scrolling position of the page.

Elm History Explorer can be activated adding the “— debug” parameter to elm-make during the compilation

The reason why it doesn’t replay commands is because these commands are causing side effect and it would not be safe to have your app effecting the environment (for example changing a database) while replaying an interaction.

Let’s see more in details what would be the implication or replaying commands.

The signature of the update function in Elm is, in case you use Html.beginnerProgram:

update : Msg -> Model -> Model

for Html.program instead:

update : Msg -> Model -> ( Model, Cmd Msg )

This is the flow for Html.beginnerProgram:

Flow of the Html.beginnerProgram. Source: https://guide.elm-lang.org/architecture/effects/

The Html.beginnerProgram is used for simple app, so let’s focus on Html.program.

Looking at the function signature, ( Model, Cmd Msg ) means that the updated function return a tuplet where

  • the first element is an updated model
  • the second element is a command (Cmd) that, after being executed return a message (Msg)

In the Elm Architecture there are other things that return messages:

- view           signature:        Model ->         Html Msg
— subscriptions signature: Model -> Sub Msg
— update signature: Msg -> Model -> (Model, Cmd Msg)

with the Html.program the flow has two new branches, Sub and Cmd. Sub are coming from subscriptions, Cmd are coming from the new update signature:

Flow of the Html.program. The two blue branches (Cmd and Sub) are the differences compared to the Html.beginnerProgram. Source: https://guide.elm-lang.org/architecture/effects/

Commands are usually (always?) used when side effects are involved, for example

  • Sending Ajax requests
  • Generating random numbers
  • Getting date and time
  • Save something in the local store
  • Using Web Sockets

So the problem running a History Playback is: should we execute also the commands or just follow the messages thread?

As mentioned earlier the “History Explorer” is not replaying commands to avoid undesirable side effects.

Moreover Commands are generating Messages so if we replay both Messages and Commands and then Commands generate messages again, the same messages will arrive two times. Which one should we choose?

Scrolling the page is one of these things that involve side effects (setting the page to a certain scrolling position).

At the moment is only possible to store the scrolling position in the model but is not possible replaying it during the history playback.

To give you and idea of what could be the replay of the interaction, have a look at these examples (click on the History Explorer at the bottom right of the screen and move back and forward along the list) :

Possible solutions

I see couple of possible solutions to this issue, but I am sure there are more

1. History Wrapper

Create a wrapper of the update function that would also execute the commands, following some rule. For example we could have these two messages:

  • ScrollTo Float (this scroll the page to a certain position, using a port or
  • OnScroll Float (this save the scrolling position to the model)

During the replay, we should

  • ignore the ScrollTo
  • convert the OnScroll to ScrollTo

2. Simulate the scrolling with some css attribute

For example using CSS transformation “translate”. In this case the app need to be aware when it is in “replay mode” and apply these styles only in this situation.

More about History wrapper

I experimented with the History Wrapper by Ilias Van Peer, but no luck also there because again the wrapper is not replaying the commands.

Saving the history

During the visitor interaction, Msg are stored in a list.

This is achieved wrapping the view inside the wrapper view with

wrapperView ... = 
div []
[ view model |> Html.map Wrapped
, ...
]

Where

  • view and model are the view and model from the original app
  • Wrapped is a Msg in the wrapper app

So messages are converted, for example, from

Increment

to

Wrapped Increment

In the wrapper update, when a Wrapped message arrive it is added to a list.

Commands instead are just sent to the Elm Runtime, as they usually are, but they are not saved in the history.

This is the flow of data during the execution of the wrapped app:

Information flow during the recording of the history, saved as list of messages

Replaying the history

During the history playback, the Cmd generated by update are not sent to the Elm Runtime but just discarded.

The core of history playback is:

foldl (\msg model -> update msg model |> Tuple.first) init msgs

The signature of foldl is

foldl : (a -> b -> b) -> b -> List a -> b

where

  • a is Msg
  • b (the accumulator) is Model
  • (a -> b -> b) is the updated function in its Html.beginnerProgram format: (Msg -> Model -> Model)

and

  • msgs is a list of Msg
  • init is the model in its initial state
  • update is the update function

Tuple.first take the return value from the model, that is in the Html.program format: Msg -> Model -> ( Model, Cmd Msg ) and extract only the Model part, ignoring Cmd Msg.

So basically this history wrapper downgrade the Html.program to Html.beginnerProgram during the replay.

This give us also another interesting insight:

The Elm Architecture (Model Update View) is as simple as one function (Update) going through a fold function where the Model is the accumulator.

The flow of data is

Replay of the history, using Tuple.first to only use Msg and dropping Cmd

What would happen if during the playback we send Cmd to the Elm Runtime?

This would be the situation:

Information flow in case we would not ignore Cmd

As you can see, if we ask the Elm Runtime to execute commands, there will be a conflict of Msg, one coming from History Wrapper and one coming from Elm Runtime.

More about Debuggers

Jinjor built an alternative debugger called elm-time-travel, that can easily wrap your app, just using TimeTravel.program instead of Html.program. Examples here.

This is the Mario Debugger.

http://debug.elm-lang.org/

Conclusion

Replaying users interaction would be useful for tracking bugs, improving usability and many other purposes. Elm is already doing a great job recording the interaction with its History Explorer. Hopefully in the future there would be some tool that could make the recording and playing of the interaction even better.

--

--

Responses (1)