<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Karina Babcock</title>
    <description>The latest articles on DEV Community by Karina Babcock (@karina_babcock).</description>
    <link>https://dev.to/karina_babcock</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3827743%2F65313751-2f40-4c29-bec4-0bf592acf9e3.png</url>
      <title>DEV Community: Karina Babcock</title>
      <link>https://dev.to/karina_babcock</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karina_babcock"/>
    <language>en</language>
    <item>
      <title>A sneak peek at new bitdrift Workflows UI</title>
      <dc:creator>Karina Babcock</dc:creator>
      <pubDate>Thu, 26 Mar 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/bitdrift/a-sneak-peek-at-new-bitdrift-workflows-ui-ifk</link>
      <guid>https://dev.to/bitdrift/a-sneak-peek-at-new-bitdrift-workflows-ui-ifk</guid>
      <description>&lt;p&gt;We're about to ship a ground-up redesign of Capture workflows that makes them fast to build, easy to read, and ready for AI agents to create and manage programmatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sneak peek at our new Workflows UI
&lt;/h2&gt;

&lt;p&gt;bitdrift Capture workflows have always been powerful. We're about to ship a ground-up redesign that makes them fast to build, easy to read, and (when we release our public API in the very near future) ready for AI agents to create and manage programmatically. This redesign lays the groundwork for first-class tooling support: a public API, CLI access, and the ability for LLM agents to instrument apps and manage workflows autonomously.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick refresher
&lt;/h2&gt;

&lt;p&gt;Capture is a fundamentally different take on mobile observability. Rather than sending non-useful samples or firehosing every log from every device to a backend that processes and discards most of it, Capture keeps data on-device and only ships it when something interesting happens. The mechanism that defines "interesting" is the workflow: a state machine deployed in real time to your fleet that describes sequences of events to match on and actions to take when they do.&lt;/p&gt;

&lt;p&gt;Workflows are powerful. They can also be humbling. Over time, as customers pushed them to their limits, we heard the same feedback again and again: too many clicks, too many advanced features, too much nuance to learn before being productive. We took that feedback seriously and rebuilt the workflow experience from the ground up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's coming
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A faster, cleaner builder
&lt;/h3&gt;

&lt;p&gt;The new builder is optimized for speed. Graph layout is now calculated automatically, drag and drop is gone, and steps are placed contextually as you build. Keyboard shortcuts cover the most common operations, so power users can move quickly without reaching for the mouse. The result is an experience that feels much more like writing code than assembling a flowchart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqt3b6a21miu9ir2bbmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqt3b6a21miu9ir2bbmj.png" alt="new-bitdrift-workflow-builder" width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The new workflow builder: Actions are attached, not floating
&lt;/h3&gt;

&lt;p&gt;In the previous builder, actions (record session, plot chart, etc.) were standalone floating nodes connected to matchers by edges. This made complex workflows hard to read at a glance. Actions are now directly attached to the step that triggers them, so the relationship between a match condition and its outcome is immediately obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps have names. Everything follows.
&lt;/h3&gt;

&lt;p&gt;You can now name your workflow steps, and all titles, series names, and chart metadata are inferred from those names automatically. Less busywork, and your charts finally describe what they're actually measuring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exit conditions
&lt;/h3&gt;

&lt;p&gt;We're generalizing and replacing the previous "timeout rule" with a more powerful and flexible concept: exit conditions. An exit condition can be a timeout or any matcher, and when it fires, it can trigger any combination of actions (charts, record sessions, etc.) just like a normal match step. This makes it straightforward to express patterns like "if the user abandons the checkout flow at any point, record the session."&lt;/p&gt;

&lt;h3&gt;
  
  
  Reset and entrance behavior, made visible
&lt;/h3&gt;

&lt;p&gt;Reset and entrance behavior — the rules that govern when a workflow restarts or re-enters — are now surfaced clearly in the UI rather than buried in configuration drawers. Workflows can now contain multiple independent flows, each with its own entry point, running in parallel. Resets on one flow are isolated from all others.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smarter conditionals
&lt;/h3&gt;

&lt;p&gt;Match conditions now support both ANDs of ORs and ORs of ANDs. Previously, only one form was supported, which forced awkward workarounds for common cases. Both are now first-class.&lt;/p&gt;

&lt;h3&gt;
  
  
  Charts, simplified and split out
&lt;/h3&gt;

&lt;p&gt;The previous "chart" action covered a lot of ground in a single configuration surface. We've split it into focused types (Counters, Histograms, Rate, Average, Funnel, and Sankey), each with a vastly simpler configuration. The "Advanced charts" section is gone. Chart customization (titles, series names, units) is now done directly on the chart itself rather than in the workflow, which means the same underlying data can be visualized differently depending on where it's shown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure duration gets more powerful
&lt;/h3&gt;

&lt;p&gt;The measure duration step now supports restricting actions to a specific duration range. For example, only record a session when the time between two steps exceeds 10 seconds. You can also attach any existing action to a measure duration step, giving you full flexibility over what happens when a timing threshold is crossed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big reason we rebuilt this
&lt;/h2&gt;

&lt;p&gt;Customer feedback drove a lot of this. We heard clearly that workflows needed to be more intuitive, faster to learn, and less prone to surprising behavior. Every item in the list above traces back to a real conversation with a real user who got stuck.&lt;/p&gt;

&lt;p&gt;But there's a bigger reason.&lt;/p&gt;

&lt;p&gt;We're about to launch a public API and CLI that unlock everything the UI can do (and more). AI agents will be able to instrument apps, create workflows, and debug issues autonomously. For that to work, workflows had to be representable as clean, predictable code rather than a sprawling visual graph with implicit rules and hidden state. The redesign wasn't just about making workflows easier for humans. It was about making them machine-readable.&lt;br&gt;
This is the foundation for what comes next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmt5qcgh0tps93nzobi8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmt5qcgh0tps93nzobi8.png" alt="New bitdrift Workflows UI walk-thru" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://blog.bitdrift.io/v/16514498037657525939/assets/posts/new-workflows-ui/walkthrough.mp4" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;blog.bitdrift.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  New Workflows UI walkthrough
&lt;/h2&gt;

&lt;p&gt;Interested in learning more? &lt;a href="https://bitdrift.io/signup?utm_campaign=eg&amp;amp;utm_medium=social&amp;amp;utm_source=reddit" rel="noopener noreferrer"&gt;Get in touch with us for a demo&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://communityinviter.com/apps/bitdriftpublic/bitdrifters" rel="noopener noreferrer"&gt;Please join us in Slack&lt;/a&gt; as well to ask questions and give feedback!&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>observability</category>
    </item>
    <item>
      <title>User experience signals to log for mobile observability</title>
      <dc:creator>Karina Babcock</dc:creator>
      <pubDate>Tue, 17 Mar 2026 17:02:34 +0000</pubDate>
      <link>https://dev.to/bitdrift/user-experience-signals-to-log-for-mobile-observability-3b2e</link>
      <guid>https://dev.to/bitdrift/user-experience-signals-to-log-for-mobile-observability-3b2e</guid>
      <description>&lt;h2&gt;
  
  
  The green dashboard fallacy
&lt;/h2&gt;

&lt;p&gt;Every mobile engineer has experienced this scenario in some form or another: your backend health is green, your crash free rate is at 99.9%. Yet, App Store reviews are flooding in about the app being "slow" or about a broken experience users encountered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the problem: Mobile apps fail in ways traditional observability tools can't detect and dashboards can't see.&lt;/strong&gt; Teams log for the disaster (the crash) but they're blind to the friction (the jank). The approach is reactive, fragmented, and heavily sampled.&lt;/p&gt;

&lt;p&gt;To build a high performance mobile app in 2026, you need to log the signals that explain how the app behaves on a real device. Your strategy should shine a light on the mobile-specific blind spots that servers simply don't have.&lt;/p&gt;

&lt;p&gt;In this three-part series, we'll unpack the most important signals mobile teams should be capturing, starting with user experience (UX) signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  User experience (UX) signals
&lt;/h2&gt;

&lt;p&gt;The first blindspot in mobile observability is that users experience apps as interactions, not metrics. In other words: Users don't care about your uptime; they care about how the app feels.&lt;br&gt;
Here are some specific metrics to help you understand user experience.&lt;/p&gt;
&lt;h3&gt;
  
  
  Frame drops and UI jank
&lt;/h3&gt;

&lt;p&gt;Mobile users immediately notice frame drops. Even small stutters can make an app feel unreliable. This is one of the biggest blind spots on Android: micro stutters or freezes.&lt;/p&gt;

&lt;p&gt;Aside from being annoying for end users, Google Play penalizes apps' Play Store rankings if they exceed their threshold and your app might even get a warning label on your Google Play store page. Scary, right? To make it worse, you might know your app feels laggy, or you might see a generic slow rendering percentage in Google Play Console, but how do you actually fix that?&lt;/p&gt;

&lt;p&gt;Most tools only show aggregate rendering metrics. They don't tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which screen dropped frames&lt;/li&gt;
&lt;li&gt;what the UI state was&lt;/li&gt;
&lt;li&gt;what the device was doing at the time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The good news is that libraries like Android JankStats do expose this information at the frame level. When you log these events with context (screen name, device state, network state), you can pinpoint exactly why rendering fell below 60fps.&lt;/p&gt;

&lt;p&gt;That's why we've &lt;a href="https://blog.bitdrift.io/post/jank-stats-integration?utm_campaign=eg&amp;amp;utm_medium=social&amp;amp;utm_source=devto" rel="noopener noreferrer"&gt;integrated bitdrift&lt;/a&gt; directly with the Android JankStats library. This allows us to track every single frame drop and, crucially, attach UI State to it. By capturing the specific UI state at the exact millisecond a frame exceeds 16ms, you can identify and fix the issues that actually impact your Google Play Store ranking. It's the difference between guessing where a bottleneck lives and having a frame-by-frame receipt of why your app isn't hitting its 60fps target.&lt;/p&gt;
&lt;h3&gt;
  
  
  "Intent-to-action" workflows
&lt;/h3&gt;

&lt;p&gt;Mobile users don't experience apps as isolated functions. They experience workflows. A user taps "Add to Cart," submits a login form, or starts checkout and expects something to happen immediately. When those workflows feel slow or fail silently, users notice. Yet many observability tools focus on backend latency or crashes, leaving the client-side gap between user intent and visible result largely unmeasured.&lt;/p&gt;

&lt;p&gt;Mobile teams should instrument the start and end of key user actions to track workflow latency, completion rate, and timeouts. Measuring the time between events like "Add to Cart tapped" and "Cart updated" reveals whether real user interactions are getting faster or slower across releases. Capturing outcomes (success, retry, failure) and basic context like device model, network type, and UI state helps engineers understand why a workflow is degrading. These signals often surface issues such as network stalls, UI deadlocks, or backend slowdowns that never show up in crash dashboards but still damage the user experience.&lt;/p&gt;

&lt;p&gt;This is another area we've looked closely at as we built our &lt;a href="https://bitdrift.io/?utm_campaign=eg&amp;amp;utm_medium=social&amp;amp;utm_source=devto" rel="noopener noreferrer"&gt;mobile observability solution&lt;/a&gt;. With bitdrift, measuring the time between any two logs is trivial. You don't need to wrap your code in complex stopwatches or spans. If you have a log when a user taps "Add to Cart" and another when a "Success" message appears, you can create a dynamic span in the bitdrift dashboard using workflows to measure that duration across your entire fleet. The best part is that these dynamic spans don't require a new app store release and you can tweak them to broaden the trigger/end point. You have full control.&lt;/p&gt;

&lt;p&gt;Here's a code sample of how you can add a custom log like "Add to Cart" to your code using bitdrift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;kotlin&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.os.SystemClock&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.bitdrift.capture.Capture&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.UUID&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onAddToCartClicked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;opId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;startedAtMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elapsedRealtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// 1) Start marker: user intent + context&lt;/span&gt;
    &lt;span class="nc"&gt;Capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"event"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"cart_add_initiated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"op_id"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;opId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"product_id"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"price_usd"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s"&gt;"ui_state"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"product_detail_view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"interaction_type"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"tap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Cart.AddInitiated"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;cartRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorMessage&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;durationMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SystemClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elapsedRealtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;startedAtMs&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 2) Success marker: bitdrift now has a clear 'intent-to-action' delta&lt;/span&gt;
            &lt;span class="nc"&gt;Capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"event"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"cart_add_success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"op_id"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;opId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"product_id"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"duration_ms"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Cart.AddSuccess"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 3) Non-fatal failure marker: Captured with error context&lt;/span&gt;
            &lt;span class="nc"&gt;Capture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"event"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"cart_add_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"op_id"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;opId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"duration_ms"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"error_type"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"network_timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"error_code"&lt;/span&gt; &lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errorCode&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s"&gt;"error_message"&lt;/span&gt; &lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errorMessage&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s"&gt;"was_retry"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Cart.AddFailed"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's what the resulting log would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cart.AddFailed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-03T17:41:06Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cart_add_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"op_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f47ac10b-58cc-4372-a567-0e02b2c3d479"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sku_9921_x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"45.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1240"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"network_timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"504"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Gateway Timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"was_retry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"device_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pixel 8 Pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"os_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Android 14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"network_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cellular"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"thermal_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nominal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memory_usage_mb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"412"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sess_882194"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"app_lifecycle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"foreground"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The "silent" timeout
&lt;/h3&gt;

&lt;p&gt;Workflows aren't just for successes. You can and should also be tracking timeouts. What happens if a user starts a checkout but the order_confirmed log never fires?&lt;br&gt;
One of the most damaging mobile experiences is the silent timeout:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User taps a button&lt;/li&gt;
&lt;li&gt;A loading spinner appears&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nothing ever finishes&lt;br&gt;
Because these events don't crash the app, they often go completely undetected.&lt;br&gt;
Mobile teams should define expected completion windows for critical workflows. If a workflow does not complete within that window, it should be treated as a failure event.&lt;br&gt;
This helps identify:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Backend stalls&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lost network requests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;UI deadlocks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dropped callbacks&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; this is another area where bitdrift can help. With bitdrift's Timeout Matcher, you can set a workflow to trigger an action, like "record session", if a specific sequence of logs doesn't complete within a set timeframe (e.g., 15 seconds). This will help you catch the "spinning loading wheel of death" use cases that don't technically crash but definitely cause churn. Maybe even more important: it also means you get the full user session allowing you to see exactly what happened to the user during this eternity…errr 15 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Crash rates don't tell you how your app actually feels to users. Signals like frame drops, workflow latency, and silent timeouts reveal the friction that traditional observability tools often miss. Logging these UX signals is the first step toward understanding what's really happening on user devices.&lt;br&gt;
Next in this series, we'll look at device performance signals: the memory, network, and thermal metrics that explain why perfectly good code can suddenly slow down in the real world.&lt;/p&gt;

&lt;p&gt;Interested in learning more? Check out the &lt;a href="https://bitdrift.io/sandbox?utm_campaign=eg&amp;amp;utm_medium=social&amp;amp;utm_source=devto" rel="noopener noreferrer"&gt;sandbox&lt;/a&gt; or start a &lt;a href="https://bitdrift.io/signup?utm_campaign=eg&amp;amp;utm_medium=social&amp;amp;utm_source=devto" rel="noopener noreferrer"&gt;free trial&lt;/a&gt; to see what working with Capture is like.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ux</category>
      <category>observability</category>
    </item>
  </channel>
</rss>
