Tracking User Behavior in Applications with io.Insights Click Stream

One use case for io.Insights is determining whether an application started, crashed, or responded slowly. It’s important for improving performance and stability and is what developers, platform admins and CTOs are after.

The other important group of questions observability answers is what users actually do inside apps/the platform.

Which tabs do they open? How does usage differ by user or session? Which actions are used regularly? Do users start a workflow but leave before completing it? Is a newly introduced feature being adopted, or is it sitting unused?

These are questions all customers struggle with. The answers help business, product and technical teams make better decisions about application design, user training, feature investment, and the removal of unnecessary functionality. They can also reveal workflows that may need further investigation, e.g. a form that is opened frequently but rarely completed.

io.Insights leverages OpenTelemetry to collect data (metrics, traces, and logs) that can be collected, stored, and visualized through an organization’s existing observability infrastructure (like Prometheus and Grafana). Below we’re showing how exactly to use Traces and Grafana (but you can use your preferred tool) to answer the questions above.

What is io.Insights Click Stream?

Click Stream is one of the trace types io.Insights offers. It records user interactions with DOM elements in io.Connect web applications. Click events are captured by default, and the resulting spans contain information that can identify the element the user interacted with, including its HTML ID, CSS classes, and tag name. Click Stream spans can also be filtered and exposed, making it possible to build dashboards around feature and action usage.

This guide demonstrates one practical implementation of that process:

  1. Capture relevant interactions in an io.Connect application.
  2. Use stable HTML IDs to make those interactions easy to identify.
  3. Convert the Click Stream spans into Prometheus metrics.
  4. Query the metrics and visualize the results in Grafana.

The focus is not simply on counting every click. The aim is to identify interactions that represent meaningful product usage: opening a feature, starting a task, completing it, cancelling it, or leaving it.

The Demo application

The Clickstream Demo application (see the video below) was created specifically for this guide to show how to measure feature usage with io.Insights Click Stream and view it in Grafana. It is a small io.Connect Desktop sample with three areas: Invoices, Bills, and Clients.

The functionality is deliberately simple, but patterns are similar to those found in enterprise apps:

  • Navigating between tabs.
  • Searching records.
  • Opening forms or composers.
  • Completing, cancelling, or closing a workflow.

A user can start a new invoice, upload a bill, schedule a payment, or add a client. Each flow has separate controls for opening the form and for its possible outcomes, such as Save, Cancel, and Close. This makes it possible to compare how often users start a flow with how often they complete or abandon it.

Control Example id Metric role
Completes the layer save-btn Successful completion
Cancels cancel-btn Explicit abandon
Closes close-btn Soft abandon

Tracking additional UI layers (forms and composers)

Primary-layer controls (tabs, toolbars, list actions) are often only the start of a workflow. An additional layer appears after a click, e.g. a form, composer, or dialog. A click on #create-order-btn shows that users started a flow → clicks on #save-btn / #cancel-btn / #close-btn show whether they completed or abandoned that layer.

Use of stable DOM IDs

Telemetry relies on automatic clickstream and stable DOM IDs, as there are no custom markers for ordinary button clicks. For important controls use stable HTML ids so each click is recorded with a clear elementIdSelector (for example #invoices-new-btn) and appears in metrics such as insights_trace_count_total. io.Insights will capture DOM clicks without requiring custom markers for each button. Stable IDs also produce predictable selectors that can be filtered and queried later.

Good (stable id):

<button type="button" id="invoices-new-btn">New invoice</button>
<button type="button" id="invoice-composer-save-btn">Save draft</button>

Bad (unspecified):

<button type="button">New invoice</button>

Aggregation tip: Reuse the same id across similar controls when you want one KPI e.g. all Email buttons share id=“email-btn”.

Prerequisites

1. Goal

Platform admins, product managers and analysts will be able to:

  1. Capture in-app interactions automatically.
  2. Convert those interactions into Prometheus metrics.
  3. Visualize usage in Grafana by feature, user, and session.

2. Solution Overview (io.Insights → Prometheus → Grafana)

Flow:

  1. User clicks in the app.
  2. io.Insights Clickstream creates a DOM interaction span.
  3. The span includes labels such as app, element ID, user, and windowId.
  4. countMetric creates insights_trace_count_total.
  5. Grafana queries Prometheus and shows feature usage panels.

Traces as Metrics

A key part of the setup is the countMetric: true option. It enables Tracing information to be published as a Metric.

Traces provide detailed context about individual interactions, but metrics are better suited to aggregation, comparison, filtering, and dashboarding over longer periods. The original trace captures the interaction and its context, while the generated metric makes that interaction easier to aggregate and visualize at scale.

By converting matching traces into the insights_trace_count_total counter metric, the same Click Stream data can be used to build a wide range of Grafana visualizations and KPIs like usage by application, user, or session; comparisons between related controls; workflow completions; and adoption trends.

3. Step-by-step Guide

Here’s how to configure:

3.1 Enable Clickstream in system.json

Add / verify this in config/system.json:

"otel": {
  "enabled": true,
  "traces": {
    "enabled": true,
    "clickstream": true,
    "url": "https://alloy.interop.io/v1/traces",
    "filters": [
      {
        "source": "interopio.api.clickstream",
        "countMetric": true
      }
    ]
  },
  "metrics": {
    "enabled": true,
    "url": "https://alloy.interop.io/v1/metrics"
  }
}

What this does:

  • otel.enabled / traces.enabled - turns io.Insights telemetry on.
  • clickstream: true - automatically captures DOM clicks.
  • filters.source = interopio.api.clickstream - targets clickstream spans.
  • countMetric: true - creates the Prometheus counter metric insights_trace_count_total.
  • metrics / traces URLs - export telemetry to your collector, for example Alloy.

To export metrics only for business-relevant clicks, configure otel.traces.filters (see 3.2).

3.2 Filter relevant clicks in system.json

Clickstream can capture many DOM interactions. Use otel.traces.filters to export metrics only for the clicks that matter for your KPIs.

In configuration, filters match spans by:

  • source - clickstream span source for your app
  • context.elementId - the clicked element id
  • countMetric: true - include matching clicks in insights_trace_count_total

In Grafana, the same interactions appear on the metric label elementIdSelector (for example #save-btn), together with tracingAppName, user, and windowId.

Important: interactive controls must have stable DOM id values, otherwise clicks may not match your filters.

Example 1 - track multiple relevant actions (regex):

"filters": [
  {
    "source": "interopio.api.clickstream.your-app-name",
    "countMetric": true,
    "context": {
      "elementId": "#(create-order-btn|save-btn|cancel-btn|orders-tab|email-btn)"
    }
  }
]

Example 2 - track one specific action:

"filters": [
  {
    "source": "interopio.api.clickstream.your-app-name",
    "countMetric": true,
    "context": {
      "elementId": "#save-btn"
    }
  }
]

Replace your-app-name with your app name, and the ids with the controls you want to measure.

Notes

  • Config filters control what is exported from io.Connect.
  • Grafana variables ($user, $session, time range) control what you visualize.
  • Restart io.Connect Desktop after changing system.json.

Optional - include additional-layer ids in system.json filters

"filters": [
  {
    "source": "interopio.api.clickstream.your-app-name",
    "countMetric": true,
    "context": {
      "elementId": "#(create-order-btn|save-btn|cancel-btn|close-btn)"
    }
  }
]

Config matches context.elementId. In Grafana, the same clicks appear as elementIdSelector (for example #save-btn).

Example funnel KPI (completion of a create flow):

count(
  max_over_time(
    insights_trace_count_total{
      tracingAppName="your-app-name",
      elementIdSelector="#save-btn",
      user=~"$user",
      windowId=~"$session"
    }[$__range]
  )
)

Add one query per step (open / save / cancel / close) in a bar or funnel panel to compare starts vs completions vs abandonment.

Questions these additional-layer metrics answer:

  • Do users open a create/edit form but not click Save / Confirm?
  • Is Cancel / Close more common than Save for a given flow?
  • After completing one layer, do users continue into the next layer?

3.3 Generate Traffic in the App

Recommended validation path (any app):

  1. Open the app and click the main navigation / tabs you want to measure.
  2. Click the primary action button for a key workflow (for example Create, Submit, Save).
  3. Complete the flow and click the confirmation action (for example Save / Confirm).
  4. Click a secondary or cancel action to capture abandonment paths.
  5. Repeat with a second user/session if you want to validate user and windowId filters.

Each click creates clickstream telemetry that can be filtered later by elementIdSelector.

3.4 Understand What Arrives in Grafana

After Clickstream is enabled and users interact with instrumented controls, telemetry is stored as the Prometheus counter metric insights_trace_count_total. Each matching click (subject to otel.traces.filters and countMetric) is associated with labels that describe the application, the UI element, the user, and the window or session.

The following example shows how a series may appear when inspecting the metric (demo values are illustrative; replace them with your application and element identifiers):

insights_trace_count_total{
  tracingAppName="clickstreamdemo",
  elementIdSelector="#invoices-new-btn",
  user="john.doe",
  windowId="abc-123"
}

Key labels:

  • tracingAppName — app name
  • elementIdSelector — clicked element (#id)
  • user — platform user
  • windowId — session/window
  • Has id → #your-id
  • No id → <unspecified elementIdSelector>

Example selectors (adapt to your app):

  • #orders-tab / #email-btn / #settings-tab
  • #create-order-btn
  • #save-btn
  • #cancel-btn
  • #email-btn

3.5 Filter and Extract the Insights You Care About

Dashboard variables:

  • $user — filter by user
  • $session (windowId) — filter by session
  • Time range ($__range) — selected period

A) Single action KPI (example: main tab clicks):

count(
  max_over_time(
    insights_trace_count_total{
      tracingAppName="clickstreamdemo",
      elementIdSelector="#invoices-tab",
      user=~"$user",
      windowId=~"$session"
    }[$__range]
  )
)

B) Compare related actions:

count by (elementIdSelector) (
  max_over_time(
    insights_trace_count_total{
      tracingAppName="clickstreamdemo",
      elementIdSelector=~"#invoices-new-btn|#invoice-composer-save-btn|#invoice-composer-cancel-btn",
      user=~"$user",
      windowId=~"$session"
    }[$__range]
  )
)

C) Aggregated action usage (example: shared Email button id):

count(
  max_over_time(
    insights_trace_count_total{
      tracingAppName="clickstreamdemo",
      elementIdSelector="#clients-card-email-btn",
      user=~"$user",
      windowId=~"$session"
    }[$__range]
  )
)

Questions these filters answer:

  • Which tabs/actions are used most?
  • Do users start a create flow but not click Save/Confirm?
  • Which users/sessions use a shared action most?
  • Are some controls unused and candidates for UX cleanup?

4. Reference

The result of the steps above is a Grafana dashboard like this:

The generated Clickstream Demo Dashboard.pdf by the sample Clickstream Demo application shows one way the captured interactions can be presented in Grafana, including:

  • Click counts for tabs and common application actions.
  • Comparisons between related buttons and features.
  • Filters for users, sessions, and time ranges.
  • Funnel-style panels comparing how often workflows are opened, completed, cancelled, or closed.

Use the PDF as a visual reference when deciding which panels and KPIs would be useful for your own applications.

The sample insights-demos Dashboards JSON file shows how the Grafana panels, variables, Prometheus queries, and HTML element IDs are connected. It is not intended to be imported as a ready-to-use dashboard, because it refers to the sample application, its specific element IDs, and its configured Prometheus data source.

When building your own dashboard, use the example to understand the mapping between:

  • A control’s stable HTML ID, such as invoices-new-btn.
  • The resulting elementIdSelector, such as #invoices-new-btn.
  • The Prometheus query that selects that interaction.
  • The Grafana panel that presents the result.

Replace the sample application name, element selectors, data source, variables, and queries with values from your own environment.

If you’d like to see the live dashboard in Grafana with some sample data or have any questions just let us know, and we’ll be happy to help.

5. Best Practices

  • Give every important control a stable id.
  • Reuse shared IDs when you want action aggregation.
  • Keep naming consistent, for example #feature-action-btn.
  • Avoid unspecified selectors in KPI panels.
  • Use $user, $session, and the time range for analysis slices.
  • Prefer count(max_over_time(...[$__range])) for click KPIs with this metric model.
  • Instrument additional layers (forms, composers, dialogs) with ids on open and outcome actions so funnels are measurable.

6. Related Docs and Articles