Claude
Skills
Sign in
Back

interaction-replay-debugger

Included with Lifetime
$97 forever

Capture and analyze every user interaction with full Event Timing API breakdown (inputDelay, processingTime, presentationDelay). Correlate with Long Animation Frames and console errors to produce a ranked report of the slowest interactions and compute INP.

Backend & APIs

What this skill does


# Interaction Replay Debugger

Install an Event Timing API PerformanceObserver with `durationThreshold: 0` to
capture every interaction event. After performing a series of clicks, key
presses, and text inputs, harvest entries with full per-phase timing
breakdowns. Correlate slow interactions with Long Animation Frames and console
errors to diagnose responsiveness issues.

## When to Use

- A page feels unresponsive on specific interactions but you don't know which
  phase (input delay, processing, or presentation) is the bottleneck.
- You need to compute the page's INP (Interaction to Next Paint) value.
- You want to identify which event handler is slow and whether the slowness is
  in JavaScript processing or in rendering after the handler completes.
- You need to correlate slow interactions with console errors that may indicate
  thrown exceptions in event handlers.

## Prerequisites

- **Playwright MCP server** connected and responding.
- **Chromium-based browser** required for:
  - Event Timing API with `interactionId` (Chrome 96+).
  - `durationThreshold: 0` to capture all interactions, not just slow ones.
  - Long Animation Frame entries with script attribution (Chrome 123+).
- Target page must be reachable from the browser instance.

## Workflow

### Step 1 -- Navigate to the Target Page

```
browser_navigate({ url: "<target_url>" })
```

### Step 2 -- Install Event Timing and Long Animation Frame Observers

Call `browser_evaluate` to set up instrumentation before any interactions.

```javascript
browser_evaluate({
  function: `() => {
    window.__interactions = {
      events: [],
      longFrames: []
    };

    // --- Event Timing Observer (captures ALL interactions) ---
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        window.__interactions.events.push({
          name: entry.name,
          entryType: entry.entryType,
          startTime: entry.startTime,
          duration: entry.duration,
          processingStart: entry.processingStart,
          processingEnd: entry.processingEnd,
          interactionId: entry.interactionId,
          // Phase breakdowns
          inputDelay: entry.processingStart - entry.startTime,
          processingTime: entry.processingEnd - entry.processingStart,
          presentationDelay: entry.startTime + entry.duration - entry.processingEnd,
          // Target identification
          target: entry.target ? (() => {
            const el = entry.target;
            let label = el.tagName;
            if (el.id) label += '#' + el.id;
            else if (el.getAttribute && el.getAttribute('data-testid')) label += '[data-testid="' + el.getAttribute('data-testid') + '"]';
            else if (el.className && typeof el.className === 'string') label += '.' + el.className.trim().split(/\\s+/)[0];
            if (el.textContent && el.textContent.length < 40) label += ' ("' + el.textContent.trim().substring(0, 30) + '")';
            return label;
          })() : null
        });
      }
    }).observe({ type: 'event', durationThreshold: 0, buffered: true });

    // --- Long Animation Frame Observer ---
    try {
      new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          const frame = {
            startTime: entry.startTime,
            duration: entry.duration,
            blockingDuration: entry.blockingDuration,
            renderStart: entry.renderStart,
            styleAndLayoutStart: entry.styleAndLayoutStart,
            scripts: []
          };
          if (entry.scripts) {
            for (const script of entry.scripts) {
              frame.scripts.push({
                invoker: script.invoker || null,
                invokerType: script.invokerType || null,
                sourceURL: script.sourceURL || null,
                sourceFunctionName: script.sourceFunctionName || null,
                sourceCharPosition: script.sourceCharPosition || null,
                executionStart: script.executionStart,
                duration: script.duration,
                forcedStyleAndLayoutDuration: script.forcedStyleAndLayoutDuration || 0
              });
            }
          }
          window.__interactions.longFrames.push(frame);
        }
      }).observe({ type: 'long-animation-frame', buffered: true });
    } catch (e) {
      window.__interactions._loafError = e.message;
    }

    return 'Interaction observers installed (durationThreshold: 0)';
  }`
})
```

### Step 3 -- Perform Interactions

Take a `browser_snapshot` to identify interactive elements and their refs.
Then drive a variety of interactions:

**Click interactions:**
```
browser_click({ ref: "<button_ref>", element: "Submit button" })
browser_wait_for({ time: 1 })
```

**Text input interactions:**
```
browser_type({ ref: "<input_ref>", text: "search query", element: "Search input" })
browser_wait_for({ time: 1 })
```

**Key press interactions:**
```
browser_press_key({ key: "Enter" })
browser_wait_for({ time: 1 })
browser_press_key({ key: "Escape" })
browser_wait_for({ time: 1 })
```

**Scroll interaction (via evaluate):**
```javascript
browser_evaluate({
  function: `() => { window.scrollBy(0, 300); return 'scrolled'; }`
})
```

Allow a brief wait between interactions so the browser has time to process
and the observers can record entries:

```
browser_wait_for({ time: 1 })
```

### Step 4 -- Capture Console Messages

Call `browser_console_messages` to check for errors that occurred during
interactions:

```
browser_console_messages({ level: "error" })
```

Save these for correlation with slow interactions.

### Step 5 -- Harvest and Analyze

Call `browser_evaluate` to process the collected interaction data.

```javascript
browser_evaluate({
  function: `() => {
    const data = window.__interactions;

    // --- Group events by interactionId ---
    // A single user interaction (e.g., click) may produce multiple events
    // (pointerdown, pointerup, click). Group them by interactionId.
    const interactionMap = {};
    for (const evt of data.events) {
      if (evt.interactionId && evt.interactionId > 0) {
        if (!interactionMap[evt.interactionId]) {
          interactionMap[evt.interactionId] = [];
        }
        interactionMap[evt.interactionId].push(evt);
      }
    }

    // For each interaction, take the event with the longest duration
    // (this is how INP is computed -- per interaction, not per event)
    const interactions = Object.entries(interactionMap).map(([id, events]) => {
      const longest = events.reduce((a, b) => a.duration > b.duration ? a : b);
      return {
        interactionId: Number(id),
        dominantEvent: longest.name,
        duration: longest.duration,
        inputDelay: Math.round(longest.inputDelay * 100) / 100,
        processingTime: Math.round(longest.processingTime * 100) / 100,
        presentationDelay: Math.round(longest.presentationDelay * 100) / 100,
        target: longest.target,
        startTime: longest.startTime,
        allEvents: events.map(e => e.name)
      };
    });

    // Sort by duration descending
    interactions.sort((a, b) => b.duration - a.duration);

    // --- Compute INP (p98 of interaction durations) ---
    const durations = interactions.map(i => i.duration).sort((a, b) => a - b);
    let inp = 0;
    if (durations.length > 0) {
      const p98Index = Math.min(Math.ceil(durations.length * 0.98) - 1, durations.length - 1);
      inp = durations[p98Index];
    }

    // --- Correlate with Long Animation Frames ---
    const correlations = [];
    for (const interaction of interactions.slice(0, 10)) {
      const iStart = interaction.startTime;
      const iEnd = interaction.startTime + interaction.duration;
      const overlapping = data.longFrames.filter(f =>
        f.startTime < iEnd && (f.startTime + f.duration) > iStart
      );
      if (overlapping.length > 0) {
        correlations.push({
          interactionId: interaction.interactionId,
          even

Related in Backend & APIs