| name | dx-metrics |
| description | Define and track developer experience metrics across Claude Code skills — success rate, invocations, time saved, adoption tier |
DX Metrics Skill
When to activate
- You need to calculate DX metrics from raw usage logs (
.claude/usage-log.jsonl)
- Generating a monthly DX scorecard (
.claude/dx-scorecard.json)
- Analyzing adoption trends for a specific skill or cohort
- Creating summary statistics for team reporting
- Identifying high-error or abandoned skills
- Benchmarking skill effectiveness (invocations vs. time saved)
- Building time-series trend data for dashboards
When NOT to use
- Collecting raw usage data (use PostToolUse hook + usage-tracker instead)
- Analyzing a single session (use session-log.md instead)
- Debugging a specific tool failure (use code-review or audit-logger)
- Real-time monitoring (this is batch/periodic analysis)
- Privacy analysis or PII review (use audit-logger or security-review)
Instructions
Setup
- Ensure
.claude/usage-log.jsonl exists and contains PostToolUse hook output:
{
"timestamp": "2026-06-15T14:32:15.234Z",
"session_id": "sess_7f8a9b2c",
"user_id": "alice@company.com",
"skill_name": "code-review",
"tool_called": "Bash",
"duration_ms": 2847,
"exit_code": 0,
"success": true
}
- Create metric calculation script (or use provided Python module):
python3 << 'PYTHON_EOF'
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from collections import defaultdict
METRICS_OUTPUT = Path.cwd() / ".claude" / "dx-scorecard.json"
USAGE_LOG = Path.cwd() / ".claude" / "usage-log.jsonl"
HISTORY_LOG = Path.cwd() / ".claude" / "dx-scorecard-history.jsonl"
class DXMetricsCalculator:
def __init__(self, usage_log_path):
self.usage_log = usage_log_path
self.metrics_by_skill = defaultdict(lambda: {
"invocations": 0,
"successes": 0,
"durations_ms": [],
"users": set(),
"errors": [],
"last_invoked": None
})
def load_usage_logs(self):
"""Parse JSONL usage log into memory."""
if not self.usage_log.exists():
print(f"❌ Usage log not found: {self.usage_log}", file=sys.stderr)
return False
with open(self.usage_log) as f:
for line in f:
if not line.strip():
continue
try:
entry = json.loads(line)
skill = entry.get("skill_name", "unknown")
self.metrics_by_skill[skill]["invocations"] += 1
if entry.get("success", False):
self.metrics_by_skill[skill]["successes"] += 1
else:
self.metrics_by_skill[skill]["errors"].append({
: entry.get(),
: entry.get()
})
self.metrics_by_skill[skill][].append(
entry.get(, 0)
)
self.metrics_by_skill[skill][].add(entry.get())
ts = entry.get()
ts and (not self.metrics_by_skill[skill][] or
ts > self.metrics_by_skill[skill][]):
self.metrics_by_skill[skill][] = ts
except json.JSONDecodeError as e:
(f, file=sys.stderr)
True
def calculate_metrics(self):
result = {
: self._get_period_start(),
: datetime.utcnow().isoformat() + ,
: datetime.utcnow().isoformat() + ,
: {},
: self._compute_summary()
}
skill_name, data sorted(self.metrics_by_skill.items()):
result[][skill_name] = self._compute_skill_metrics(
skill_name, data
)
result
def _compute_skill_metrics(self, skill_name, data):
invocations = data[]
successes = data[]
durations = data[]
success_rate = (successes / invocations * 100) invocations > 0 0
error_rate = 100 - success_rate
avg_duration_ms = (durations) / len(durations) durations 0
user_count = len(data[])
adoption_tier = self._adoption_tier(invocations)
avg_duration_sec = avg_duration_ms / 1000
time_saved_min = (avg_duration_sec * invocations * 2) / 60
friction_index = error_rate + (100 - success_rate)
{
: invocations,
: round(success_rate, 1),
: round(error_rate, 1),
: round(avg_duration_sec, 1),
: round(avg_duration_ms, 0),
: user_count,
: adoption_tier,
: round(time_saved_min, 0),
: round(friction_index, 1),
: data[],
: len(data[]),
: data[][:3]
}
def _adoption_tier(self, invocations):
invocations < 5:
invocations < 50:
invocations < 500:
:
def _compute_summary(self):
all_users = ()
total_time_saved = 0
total_invocations = 0
scores = []
skill_data self.metrics_by_skill.values():
all_users.update(skill_data[])
total_invocations += skill_data[]
skill_metrics self._compute_all_metrics():
total_time_saved += skill_metrics.get(, 0)
scores.append(self._dx_score(skill_metrics))
avg_dx_score = (scores) / len(scores) scores 0
critical_issues = []
high_issues = []
skill_name, metrics self._compute_all_metrics():
metrics[] > 20:
critical_issues.append(f)
metrics[] > 10:
high_issues.append(f)
{
: len(all_users),
: total_invocations,
: round(total_time_saved / 60, 1),
: round(avg_dx_score, 1),
: round(self._avg_friction_index(), 1),
: len(self.metrics_by_skill),
: self._adoption_breakdown(),
: critical_issues,
: high_issues,
: self._top_skills(3),
: self._lowest_adoption(3)
}
def _compute_all_metrics(self):
skill_name, data sorted(self.metrics_by_skill.items()):
yield skill_name, self._compute_skill_metrics(skill_name, data)
def _dx_score(self, metrics):
(
(metrics[] * 0.4) +
(self._adoption_score(metrics[]) * 0.3) +
(min(100, (metrics[] / 10)) * 0.3)
)
def _adoption_score(self, tier):
{: 0, : 50, : 85, : 100}.get(tier, 50)
def _avg_friction_index(self):
indices = [m[] m self._compute_all_metrics()]
(indices) / len(indices) indices 0
def _adoption_breakdown(self):
breakdown = {: 0, : 0, : 0, : 0}
skill_data self.metrics_by_skill.values():
tier = self._adoption_tier(skill_data[])
breakdown[tier] += 1
breakdown
def _top_skills(self, n):
sorted(
[(name, data[]) name, data self.metrics_by_skill.items()],
key=lambda x: x[1],
reverse=True
)[:n]
def _lowest_adoption(self, n):
sorted(
[(name, data[]) name, data self.metrics_by_skill.items()],
key=lambda x: x[1]
)[:n]
def _get_period_start(self):
start = datetime.utcnow() - timedelta(days=30)
start.isoformat() +
calculator = DXMetricsCalculator(USAGE_LOG)
calculator.load_usage_logs():
metrics = calculator.calculate_metrics()
METRICS_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
with open(METRICS_OUTPUT, ) as f:
json.dump(metrics, f, indent=2)
(f)
(json.dumps(metrics[], indent=2))
HISTORY_LOG.parent.mkdir(parents=True, exist_ok=True)
with open(HISTORY_LOG, ) as f:
f.write(json.dumps(metrics) + )
(f)
:
sys.exit(1)
PYTHON_EOF
Usage
Interactive: Invoke the skill to compute metrics:
/dx-metrics
Scripted: Add to .claude/hooks/ for automated monthly runs:
{
"hooks": {
"SessionStart": [
{
"matcher": "day-of-month == 1 && hour == 9",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/calculate-dx-metrics.sh"
}
]
}
]
}
}
Metric Definitions
| Metric | Formula | Notes |
|---|
| invocations | COUNT(*) | Total skill calls in period |
| success_rate | (successes / invocations * 100) | 0–100% |
| error_rate | 100 - success_rate | % of failed calls |
| avg_duration_sec | SUM(duration_ms) / COUNT(*) / 1000 | Seconds per invocation |
| user_count | COUNT(DISTINCT user_id) | Unique users |
| adoption_tier | CASE invocations < 5 'abandoned' ... 'core' END | Category |
| time_saved_min | avg_duration_sec * invocations * 2 / 60 | Estimated minutes saved (2x multiplier for human speed) |
| friction_index | error_rate + (100 - success_rate) | 0–200: lower is better |
| dx_score | (success_rate * 0.4) + (adoption_score * 0.3) + (time_saved * 0.3) | 0–100: overall health |
Output Format
.claude/dx-scorecard.json:
{
"period_start": "2026-05-15T00:00:00Z",
"period_end": "2026-06-15T00:00:00Z",
"generated_at": "2026-06-15T14:32:00Z",
"metrics": {
"code-review": {
"invocations": 127,
"success_rate": 96.9,
"error_rate": 3.1,
"avg_duration_sec": 14.2,
"avg_duration_ms": 14200,
"user_count": 18,
"adoption_tier": "active",
"time_saved_min": 1589,
"friction_index": 3.1,
"last_invoked":
...
Advanced Usage
Filter by date range:
self.start_date = datetime.fromisoformat("2026-06-01T00:00:00Z")
self.end_date = datetime.fromisoformat("2026-06-30T23:59:59Z")
entry_ts = datetime.fromisoformat(entry["timestamp"])
if not (self.start_date <= entry_ts <= self.end_date):
continue
Compare periods (YoY, MoM):
jq 'select(.period_start | startswith("2025-06"))' .claude/dx-scorecard-history.jsonl > june-2025.json
jq 'select(.period_start | startswith("2026-06"))' .claude/dx-scorecard-history.jsonl > june-2026.json
diff <(jq '.summary' june-2025.json) <(jq '.summary' june-2026.json)
Identify high-friction skills:
jq '.metrics | to_entries[] |
select(.value.friction_index > 15) |
{skill: .key, friction: .value.friction_index, error_rate: .value.error_rate}' \
.claude/dx-scorecard.json
Example
Monthly DX Review Workflow:
- Run
/dx-metrics to populate .claude/dx-scorecard.json
- Review summary:
jq '.summary' .claude/dx-scorecard.json
- Identify critical issues:
jq '.summary.critical_issues[]' .claude/dx-scorecard.json
- Propose fixes: "deep-research needs retry logic (25% error rate → proposal in dx-review workflow)"
- Schedule follow-up for 2 weeks post-fix
Dashboard Integration:
Export to Prometheus/Grafana:
jq '.metrics | to_entries[] | {name: .key, success_rate: .value.success_rate}' \
.claude/dx-scorecard.json | \
while read metric; do
echo "dx_success_rate{skill=\"$(echo $metric | jq -r .name)\"} $(echo $metric | jq .success_rate)"
done