Frustration Detection

Automatically detect rage clicks, dead clicks, and error clicks to identify UX problems.

What is Frustration Detection?#

Frustration detection is enabled automatically when session replay is active (which is on by default). It analyzes user behavior during session replays to identify moments where users struggled with your interface. Instead of watching hours of replay footage, you can jump directly to the moments that matter. No additional configuration is required.

JustAnalytics detects three types of frustration signals:

  • Rage clicks -- rapid repeated clicks on the same element
  • Dead clicks -- clicks that produce no visible response
  • Error clicks -- clicks that trigger a JavaScript error

Each session receives a frustration score based on the number and severity of detected signals. Higher scores indicate sessions where users had a worse experience.

Rage Clicks#

A rage click is detected when a user clicks the same element (or area within 30px) three or more times within one second. This typically indicates the user expected something to happen and it did not.

Detection Criteria#

| Parameter | Value | |-----------|-------| | Minimum clicks | 3 | | Time window | 1 second | | Proximity radius | 30px | | Element matching | Same element or parent up to 3 levels |

Common Causes#

  • Broken buttons -- click handlers not attached, event listeners lost after re-render
  • Slow responses -- action takes too long, user thinks the click did not register
  • Non-interactive elements -- elements that look clickable but are not (missing cursor, styled like a link)
  • Disabled state confusion -- buttons that appear active but are disabled
  • Z-index issues -- overlay or invisible element blocking clicks

Example in the Dashboard#

When viewing a session replay with rage clicks, you will see:

  1. A red flame icon on the timeline at the point of the rage click
  2. A red highlight ring around the target element in the replay
  3. The click count displayed next to the element (e.g., "7 clicks in 0.8s")

Reducing Rage Clicks#

// Problem: button click handler is slow, no feedback
submitButton.addEventListener('click', async () => {
  await processOrder(); // Takes 3 seconds, no loading state
});

// Solution: add immediate feedback
submitButton.addEventListener('click', async () => {
  submitButton.disabled = true;
  submitButton.textContent = 'Processing...';
  try {
    await processOrder();
    submitButton.textContent = 'Done!';
  } catch (error) {
    submitButton.disabled = false;
    submitButton.textContent = 'Try Again';
  }
});
/* Ensure interactive elements have cursor: pointer */
.clickable-card,
button,
a,
[role="button"] {
  cursor: pointer;
}

/* Make disabled state visually distinct */
button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Dead Clicks#

A dead click is detected when a user clicks an element and no DOM change occurs within 500ms. The user clicked, but nothing happened.

Detection Criteria#

| Parameter | Value | |-----------|-------| | DOM change timeout | 500ms | | Monitored changes | Child list, attributes, character data, network requests | | Excluded elements | <a> with href, <input>, <select>, <textarea> |

What Counts as a Response#

The dead click detector watches for any of the following within 500ms of a click:

  • DOM nodes added or removed
  • CSS class changes on any element
  • Attribute changes (style, aria-, data-)
  • Navigation (URL change)
  • Network request initiated
  • Element content change

If none of these occur, the click is flagged as dead.

Common Causes#

  • Missing event listeners -- button rendered without its click handler
  • JavaScript errors -- the click handler throws before producing a DOM change
  • Conditional rendering -- element renders before the state it depends on is ready
  • Static content -- text or images that look interactive but have no behavior
  • Server-rendered pages -- hydration has not completed yet

Example in the Dashboard#

Dead clicks appear as gray dot markers on the replay timeline. Hovering shows the clicked element's tag, class, and text content.

Reducing Dead Clicks#

// Problem: button renders before data is loaded
function ProductPage() {
  const [product, setProduct] = useState(null);

  return (
    <div>
      {/* This button is rendered but addToCart will fail without product */}
      <button onClick={() => addToCart(product)}>Add to Cart</button>
    </div>
  );
}

// Solution: disable button until ready
function ProductPage() {
  const [product, setProduct] = useState(null);

  return (
    <div>
      <button
        onClick={() => addToCart(product)}
        disabled={!product}
      >
        {product ? 'Add to Cart' : 'Loading...'}
      </button>
    </div>
  );
}

Error Clicks#

An error click is detected when a JavaScript error occurs within 1 second of a user's click. This directly links user actions to application failures.

Detection Criteria#

| Parameter | Value | |-----------|-------| | Time window | 1 second after click | | Error types | Uncaught exceptions, unhandled promise rejections, console.error | | Attribution | Error is attributed to the clicked element |

Example in the Dashboard#

Error clicks appear as red circle markers on the replay timeline. Clicking the marker shows:

  • The error message and stack trace
  • The clicked element (tag, class, text)
  • The timestamp and URL
  • A link to the full error in Error Tracking

Common Causes#

  • API failures -- click triggers a fetch that returns an error
  • Null references -- click handler accesses state that has not loaded
  • Type errors -- incorrect data shape from an API response
  • Third-party script errors -- click triggers analytics or ad script that throws

Reducing Error Clicks#

// Problem: click handler does not handle errors
async function handleCheckout() {
  const response = await fetch('/api/checkout', { method: 'POST' });
  const data = await response.json(); // Throws if response is not JSON
  window.location.href = data.redirectUrl; // Throws if data is null
}

// Solution: defensive error handling with user feedback
async function handleCheckout() {
  try {
    const response = await fetch('/api/checkout', { method: 'POST' });
    if (!response.ok) {
      throw new Error(`Checkout failed: ${response.status}`);
    }
    const data = await response.json();
    if (data.redirectUrl) {
      window.location.href = data.redirectUrl;
    } else {
      showNotification('Checkout completed! Check your email for confirmation.');
    }
  } catch (error) {
    showNotification('Something went wrong. Please try again.', 'error');
    JA.captureException(error, { tags: { action: 'checkout' } });
  }
}

Frustration Score#

Each session receives a frustration score calculated from the weighted sum of detected frustration signals:

| Signal | Weight | |--------|--------| | Rage click | 3 points | | Dead click | 1 point | | Error click | 5 points |

Score Ranges#

| Score | Level | Indicator | |-------|-------|-----------| | 0 | None | Green | | 1-5 | Low | Yellow | | 6-15 | Medium | Orange | | 16+ | High | Red |

Filtering by Frustration#

In the Session Replay list view, you can filter sessions by frustration level:

  1. Navigate to Monitoring > Session Replay
  2. Click the Frustration filter dropdown
  3. Select a minimum frustration level: Low, Medium, or High
  4. Sessions are sorted by frustration score (highest first)

You can also combine frustration filters with other criteria:

  • Frustration + Page URL -- find frustrated users on a specific page
  • Frustration + Browser -- identify browser-specific UX issues
  • Frustration + Error -- find sessions with both frustration and errors
  • Frustration + Country -- spot regional usability problems

Frustration Summary Widget#

The dashboard includes a Frustration Summary widget that provides an at-a-glance view of UX issues across your site.

Widget Metrics#

  • Frustrated sessions (%) -- percentage of sessions with frustration score > 0
  • Top rage click targets -- most rage-clicked elements ranked by frequency
  • Top dead click targets -- elements with the most dead clicks
  • Frustration trend -- frustration rate over time (line chart)
  • Pages with most frustration -- URLs ranked by total frustration score

Setting Up the Widget#

The Frustration Summary widget is automatically available when session replay is enabled. To add it to your dashboard:

  1. Navigate to your Dashboard
  2. Click Customize
  3. Drag Frustration Summary into your preferred position
  4. Configure the time range (7 days recommended for meaningful trends)

Alerting on Frustration#

Create alert rules that trigger when frustration exceeds a threshold:

  1. Navigate to Alerts > Create Rule
  2. Select metric: Frustrated Session Rate
  3. Set condition: Greater than 15% over 1 hour
  4. Configure notification channel (email, Slack, webhook)
  5. Save the rule
{
  "name": "High Frustration Rate",
  "metric": "frustrated_session_rate",
  "condition": "greater_than",
  "threshold": 0.15,
  "window": "1h",
  "channels": ["email", "slack"]
}

Best Practices#

Prioritize by Impact#

Focus on frustration signals that affect the most users:

  1. Start with error clicks -- these represent actual broken functionality
  2. Address rage clicks on high-traffic pages -- these affect the most users
  3. Investigate dead clicks on conversion-critical elements -- buttons in checkout, signup forms

Track Frustration Over Releases#

Compare frustration rates before and after deploys:

  1. Tag your releases with data-release on the monitor script
  2. View frustration metrics filtered by release version
  3. Set up alerts for frustration rate increases after deploys

Use Frustration to Prioritize Bugs#

When triaging bugs, use the frustration score to estimate user impact:

  • Bug causes error clicks on checkout button (50 sessions/day) -- P0
  • Bug causes dead clicks on secondary nav link (5 sessions/day) -- P2
  • Bug causes rage clicks on tooltip icon (2 sessions/day) -- P3

Next Steps#