pramoddutta/qaskills/ux-friction-logger

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Identify and log user experience friction points including excessive clicks, confusing navigation, slow interactions, and workflow bottlenecks through automated heuristic analysis

Install

npx vskill@latest install pramoddutta/qaskills/ux-friction-logger

SKILL.md

UX Friction Logger Skill

You are an expert UX quality engineer specializing in automated friction detection and usability heuristic analysis. When asked to identify, log, or remediate user experience friction points, follow these comprehensive instructions to systematically uncover workflow bottlenecks, excessive interaction costs, and confusing navigation patterns.

Core Principles

  1. Every Click Has a Cost -- Each additional click, scroll, or interaction a user must perform increases cognitive load and the probability of abandonment. Measure interaction cost quantitatively and set strict budgets for critical user journeys.
  1. Friction is Contextual -- What constitutes friction depends on user intent, device capability, and task urgency. A five-step checkout may be acceptable for a high-value purchase but unacceptable for a quick reorder. Always evaluate friction relative to the task criticality.
  1. Progressive Disclosure Over Information Overload -- Presenting all options at once creates decision paralysis. Test that interfaces reveal complexity gradually and that primary actions are immediately visible without scrolling or hunting.
  1. Error Recovery Should Be Effortless -- The cost of recovering from an error must not exceed the cost of the original task. Measure how many additional steps a user needs after encountering an error to return to their intended workflow.
  1. Consistency Reduces Cognitive Load -- Inconsistent patterns across pages force users to relearn interactions. Audit for consistency in navigation placement, button labeling, form behavior, and feedback mechanisms.
  1. Invisible Friction is Still Friction -- Users may not consciously notice micro-frustrations like slightly too-small click targets, 200ms delays before feedback, or subtle layout shifts, but these accumulate into overall dissatisfaction. Instrument everything.
  1. Measure, Don't Assume -- Gut feelings about UX quality are unreliable. Automate quantitative measurement of click counts, time-on-task, scroll depth, and error rates so that friction is tracked objectively across releases.

Project Structure

project-root/
tests/
friction/
config/
friction.config.ts # Thresholds and scoring rules
workflows.ts # Defined user journey maps
helpers/
click-tracker.ts # Click counting and path recording
scroll-analyzer.ts # Scroll depth measurement
timing-collector.ts # Interaction timing utilities
navigation-mapper.ts # Navigation depth and dead-end detection
heuristic-scorer.ts # Nielsen heuristic evaluation scoring
specs/
click-to-completion.spec.ts
navigation-depth.spec.ts
form-interaction.spec.ts
error-recovery.spec.ts
tooltip-dependency.spec.ts
dead-end-detection.spec.ts
workflow-steps.spec.ts
scroll-depth.spec.ts
heuristic-evaluation.spec.ts
reports/
friction-report.ts # HTML/JSON report generator
playwright.config.ts

Setting Up the Friction Logger

Installation and Configuration
bash
npm install --save-dev @playwright/test
npx playwright install
Friction Configuration
typescript
// tests/friction/config/friction.config.ts
export interface FrictionThresholds {
maxClicksToComplete: number;
maxNavigationDepth: number;
maxFormFieldTime: number; // milliseconds per field
maxErrorRecoverySteps: number;
maxScrollDepthForCritical: number; // percentage
maxWorkflowSteps: number;
maxTooltipDependencies: number;
maxTimeToInteractive: number; // milliseconds
}
export const defaultThresholds: FrictionThresholds = {
maxClicksToComplete: 5,
maxNavigationDepth: 3,
maxFormFieldTime: 8000,
maxErrorRecoverySteps: 3,
maxScrollDepthForCritical: 50,
maxWorkflowSteps: 7,
maxTooltipDependencies: 2,
maxTimeToInteractive: 3000,
};
export interface WorkflowDefinition {
name: string;
description: string;
startUrl: string;
successIndicator: string; // selector or URL pattern
criticalContent?: string[]; // selectors that must be visible without excessive scrolling
maxAcceptableClicks: number;
}
export const criticalWorkflows: WorkflowDefinition[] = [
{
name: 'User Registration',
description: 'New user completes signup flow',
startUrl: '/signup',
successIndicator: '[data-testid="welcome-dashboard"]',
maxAcceptableClicks: 4,
},
{
name: 'Product Purchase',
description: 'User finds and purchases a product',
startUrl: '/',
successIndicator: '[data-testid="order-confirmation"]',
criticalContent: ['[data-testid="add-to-cart"]', '[data-testid="price"]'],
maxAcceptableClicks: 6,
},
{
name: 'Password Reset',
description: 'User recovers account access',
startUrl: '/login',
successIndicator: '[data-testid="password-updated"]',
maxAcceptableClicks: 5,
},
];

Click-to-Completion Counting

Track the total number of clicks required to complete critical user journeys. This is the most fundamental friction metric.

typescript
// tests/friction/helpers/click-tracker.ts
import { Page } from '@playwright/test';
export interface ClickEvent {
timestamp: number;
selector: string;
tagName: string;
innerText: string;
url: string;
coordinates: { x: number; y: number };
}
export interface ClickTrackingResult {
totalClicks: number;
uniqueClicks: number;
redundantClicks: number;
clickPath: ClickEvent[];
timeToComplete: number;
averageTimeBetweenClicks: number;
}
export class ClickTracker {
private clicks: ClickEvent[] = [];
private startTime: number = 0;
async attach(page: Page): Promise<void> {
this.clicks = [];
this.startTime = Date.now();
await page.exposeFunction('__recordClick', (event: ClickEvent) => {
this.clicks.push(event);
});
await page.addInitScript(() => {
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const clickEvent = {
timestamp: Date.now(),
selector: target.getAttribute('data-testid') || target.id || target.className,
tagName: target.tagName,
innerText: target.innerText?.substring(0, 100) || '',
url: window.location.href,
coordinates: { x: e.clientX, y: e.clientY },
};
(window as any).__recordClick(clickEvent);
});
});
}
getResults(): ClickTrackingResult {
const totalClicks = this.clicks.length;
const uniqueSelectors = new Set(this.clicks.map((c) => `${c.selector}-${c.url}`));
const redundantClicks = totalClicks - uniqueSelectors.size;
const intervals = this.clicks
.slice(1)
.map((click, i) => click.timestamp - this.clicks[i].timestamp);
const averageTimeBetweenClicks =
intervals.length > 0 ? intervals.reduce((a, b) => a + b, 0) / intervals.length : 0;
return {
totalClicks,
uniqueClicks: uniqueSelectors.size,
redundantClicks,
clickPath: this.clicks,
timeToComplete: this.clicks.length > 0
? this.clicks[this.clicks.length - 1].timestamp - this.startTime
: 0,
averageTimeBetweenClicks,
};
}
reset(): void {
this.clicks = [];
this.startTime = Date.now();
}
}
typescript
// tests/friction/specs/click-to-completion.spec.ts
import { test, expect } from '@playwright/test';
import { ClickTracker } from '../helpers/click-tracker';
import { criticalWorkflows, defaultThresholds } from '../config/friction.config';
test.describe('Click-to-Completion Analysis', () => {
let tracker: ClickTracker;
test.beforeEach(() => {
tracker = new ClickTracker();
});
for (const workflow of criticalWorkflows) {
test(`${workflow.name}: should complete within ${workflow.maxAcceptableClicks} clicks`, async ({
page,
}) => {
await tracker.attach(page);
await page.goto(workflow.startUrl);
// Execute workflow steps -- these would be customized per workflow
// The test records every click automatically
await page.click('[data-testid="get-started"]');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.click('[data-testid="submit"]');
const results = tracker.getResults();
expect(results.totalClicks).toBeLessThanOrEqual(workflow.maxAcceptableClicks);
expect(results.redundantClicks).toBe(0);
expect(results.averageTimeBetweenClicks).toBeLessThan(
defaultThresholds.maxFormFieldTime
);
// Log detailed path for analysis
console.log(`Workflow: ${workflow.name}`);
console.log(`Total clicks: ${results.totalClicks}`);
console.log(`Redundant clicks: ${results.redundantClicks}`);
console.log(`Time to complete: ${results.timeToComplete}ms`);
results.clickPath.forEach((click, i) => {
console.log(` Step ${i + 1}: ${click.tagName} "${click.innerText}" on ${click.url}`);
});
});
}
});

Navigation Depth Analysis

Measure how deep in the navigation hierarchy users must go to reach important content. Deep nesting creates disorientation and abandonment.

typescript
// tests/friction/helpers/navigation-mapper.ts
import { Page } from '@playwright/test';
export interface NavigationNode {
url: string;
title: string;
depth: number;
parent: string | null;
children: string[];
isDeadEnd: boolean;
hasBackNavigation: boolean;
breadcrumbPresent: boolean;
}
export interface NavigationMap {
nodes: Map<string, NavigationNode>;
maxDepth: number;
deadEnds: string[];
orphanPages: string[];
averageDepth: number;
}
export class NavigationMapper {
private visited: Map<string, NavigationNode> = new Map();
private maxCrawlDepth: number;
constructor(maxCrawlDepth: number = 5) {
this.maxCrawlDepth = maxCrawlDepth;
}
async mapNavigation(page: Page, startUrl: string): Promise<NavigationMap> {
await this.crawl(page, startUrl, 0, null);
return this.buildMap();
}
private async crawl(
page: Page,
url: string,
depth: number,
parentUrl: string | null
): Promise<void> {
if (depth > this.maxCrawlDepth || this.visited.has(url)) return;
await page.goto(url, { waitUntil: 'networkidle' });
const title = await page.title();
const links = await page.$$eval('a[href]', (anchors) =>
anchors
.map((a) => (a as HTMLAnchorElement).href)
.filter((href) => href.startsWith(window.location.origin))
);
const hasBreadcrumb = await page.$('[aria-label="breadcrumb"], .breadcrumb, nav.breadcrumbs')
.then((el) => el !== null);
const hasBackButton = await page
.$('a[href*="back"], button[aria-label*="back"], [data-testid="back-button"]')
.then((el) => el !== null);
const internalLinks = links.filter(
(link) => !link.includes('#') && !link.includes('mailto:')
);
const node: NavigationNode = {
url,
title,
depth,
parent: parentUrl,
children: internalLinks,
isDeadEnd: internalLinks.length === 0,
hasBackNavigation: hasBackButton || hasBreadcrumb,
breadcrumbPresent: hasBreadcrumb,
};
this.visited.set(url, node);
for (const link of internalLinks.slice(0, 10)) {
await this.crawl(page, link, depth + 1, url);
}
}
private buildMap(): NavigationMap {
const nodes = this.visited;
const deadEnds = Array.from(nodes.values())
.filter((n) => n.isDeadEnd)
.map((n) => n.url);
const orphanPages = Array.from(nodes.values())
.filter((n) => n.depth > 0 && !n.hasBackNavigation)
.map((n) => n.url);
const depths = Array.from(nodes.values()).map((n) => n.depth);
const maxDepth = Math.max(...depths, 0);
const averageDepth = depths.length > 0 ? depths.reduce((a, b) => a + b, 0) / depths.length : 0;

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
117
Repository forks
5
7-day trend score
24

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 10 May 2026, 5 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/pramoddutta/qaskills/ux-friction-logger/badge)](https://verified-skill.com/skills/pramoddutta/qaskills/ux-friction-logger)

Something wrong with this skill or its page? Tell us.