Visualizing Session Flow With Honeycomb

Visualizing Session Flow With Honeycomb

7 Min. Read

I want to know what users are doing in my application. A distributed trace is the best way to show the data flow of one user interaction through my application, but it isn’t sufficient to show the overall user experience. 

Traces don’t tell the whole story on their own

As helpful as traces are, they detail an isolated series of moments related to a specific activity. But they don’t link to related activities, unless you instrument with span links, and navigating between them can become complex. 

Some interactions are hard to visualize this way; they are just spread out across too many traces. Consider frontend applications: it’s usually impossible to find a trace that includes everything from a click, all the way back to a backend response, because user events only live for the period of time of the click itself.

A single-span click event.  Because of React's synthetic browser events, many user interactions don't get attached to the rest of the user's interaction, because by the time the click is complete, the tracecontext is cleared as the event is over.

By the time the click is over, that top-level span ends. The trace context is lost in the haze of “Reactium” before the browser issues the fetch, which starts a new trace.

Here is a full-stack trace, still originating in our frontend Honeycomb application (user-events-otel) and propagating the trace on to the query engine, poodle.

Ideally the click would be the first span of this trace, but because the context cleared before the POST began, we can't see it here. This requires a different approach to querying user events.

You can consider frontend interactions as a timeline of traces. Each trace has a specific set of spans attached to the initiating event. Some can be quick, such as a click to show a preloaded view from the React Router. Even shorter traces are single events, such as readings generated by Honeycomb’s Core Web Vitals instrumentation. But longer ones, such as a checkout POST, may trigger a cascade of calls from service to service, propagating the tracecontext.

Here’s a crude example:

Trace diagram / session

Where:

  • In the first trace, the click event is attached to a navigational link. If we instrument our routing API, such as in this React Router span processor example, we’ll see routes identified in the trace as well.
  • The second trace is even shorter. Honeycomb’s Core Web Vitals instrumentation relies on the Google Chrome Web Vitals library and emits single-span traces for each browser session.
  • The third trace is a full-stack example. A click on a form (in this case, part of the same trace) generates a fetch call to POST the checkout form data, and because of trace propagation, the receiving server gets the traceparent header with the relevant trace.trace_id, caller’s trace.span_id, and flags.

Each individual event results in a trace, but it’s the big picture of many traces over time that tells the tale. We need a way to tie a collection of these individual users’ traces together to visualize the journey.

How do we relate traces together like this in OpenTelemetry?

The concept of a “session” is essential when looking at longer-running interactions, like user interface interactions from a given user, which is defined in the OpenTelemetry Semantic Conventions specification. It states:

[…]a Session is represented as a collection of Logs, Events, and Spans emitted by the Client Application throughout the Session’s duration. Each Session is assigned a unique identifier, which is included as an attribute in the Logs, Events, and Spans generated during the Session’s lifecycle.

OpenTelemetry Semantic Conventions on Session.ID

Last year, we released our web observability OpenTelemetry Wrapper SDK, which makes it easy to configure your web applications to send telemetry and participate in full-stack traces. We’ve been including an automatically generated session.id attribute, which is defined by a UUID we generated when starting the SDK. We’ve also recently added support for generating your own session.id attribute.

You can find out about your user sessions by filtering or grouping by them in our query tool. For example, how many mobile browser sessions do we have, and with what browser type?

Here is recent traffic, on mobile safari, for my phone:

I can take that session.id field and use it in another query:

This will give me a breakdown of my spans for the session:

But that’s a lot of graphing, and though I can see the number of spans (or filter down to specific types, visualize durations, etc.), I don’t see the user interactions in terms of traces for each event.

 In fact, if I pick the Traces tab, I only see the slowest traces:

So, what can I do?

Explore your data by time to see an event timeline

To visualize the actual stream of events in order, you can use the Explore Data tab, ordering it by Timestamp from oldest to newest. Be aware that you are limited to displaying a maximum of 250 rows at a time, and can export a maximum of 1,000 rows at a time. However, with this table—and using tweaks to the WHERE clause—you can get down to everything you need:

An image of the Explore Data tab in a Honeycomb Query, filtering in only HTTP events and clicks for a given session.id.

I’ve included the trace.id field in my explore data fields, as well as “∆ Time,” which is the elapsed time between each displayed event. To get this to work, I’ve sorted the table by the Timestamp field (you can’t sort by ∆ Time) in ascending order. This will give us a rough idea of the time sequence for visualizing the data.

Useful things to skip in your query

As always, you can just add conditions to your WHERE clause to filter out data you don’t want to include:

  • Skip Core Web Vitals information with library.name != @honeycombio/core-web-vitals if it isn’t important to your investigation.
  • Skip page and resource fetch operations with name does-not-contain document if you’re focusing on post-pageload interactions.

You can also filter out items you don’t want interactively, clicking on the field of interest and choosing to focus on or exclude it. Here’s a demo:

Using session flow in investigations

Using our demonstration “telescope shop” application, here’s an example investigation:

We pay close attention to our purchasing flow, looking for issues so we can correct them before our users start reporting errors. I checked the frontend dataset’s Web Launchpad and found that it reports some errors.

The Web Launchpad shows some counts at the top of the page: total events, page views and total errors. There are 10 total errors in this example.

I explored the error events and grabbed the session ID of one of those errors. Switching to Explore Data, I observed the sequence of events: a click on the /cart page led to an HTTP POST to api/checkout, which caused the exception span. 

By querying where the session.id matches a session.id in one of the traces, we can review all of the interactions for that web browser session. This is an excerpt of the full result set, using the Explore Data tab of Honeycomb's Query Results view. Three spans are shown: a click event which calls the checkout API, the HTTP Fetch call namex HTTP POST that actually invokes the API call, and the exception that is recorded when the POST fails.

Adding target_element and target_xpath to the event fields shows that we’ve clicked on a button, located in the checkout form (our Place Order button):

To go deeper, I added the trace.trace_id field, then clicked the trace.trace_id of the HTTP POST to view the request. This request results in eight spans marked as errors. Here is the top of that trace:

This image details the top of a trace for the HTTP POST operation. The main highlight is that there are 8 spans with errors. The next diagram gets to the root cause of the problem, lower down in the trace.

Further down, we see the error: a problem in a Ruby Sinatra script in emailservice. Here’s how we query it:

Now we have the backend service and the line of code that is the root cause of these errors in the client.

The value of a session

Sessions help us understand a user’s flow through a system. We’ve learned how to define a session.id value in our application code, query for sessions, and explore the events in order.

Honeycomb enables session support not only for web frontends but also for our Mobile SDKs, because you should always be able to know what your users are doing with your applications.

Don’t forget to share!
Ken Rimple

Ken Rimple

Senior Developer Advocate

Ken Rimple, (Senior Developer Relations Advocate, he/him), is a software engineer with more than 35 years of experience. He’s developed on databases, application servers, front-end JS and TypeScript frameworks, and is currently focused on front-end applications and stacks with React, Angular, and other APIs and frameworks. He lives in the Philadelphia, Pennsylvania area of the US with his wife, four mostly adult children and three dogs.

Related posts