Skip to content

Commit 8107f9d

Browse files
committed
feat: add enhanced CrUX metrics with category ratios and thresholds
Add comprehensive Chrome UX Report (CrUX) metrics export including: - Category distribution ratios (fast/average/slow proportions) - Core Web Vitals thresholds extracted from API bucket boundaries - Full backward compatibility maintained Changes: - collector/collector.go: Enhanced collectLoadingExperience() to export category_ratio and threshold metrics for all CrUX metrics (LCP, INP, CLS, FCP, TTFB) - README.md: Added CrUX Metrics section with comprehensive documentation - CLAUDE.md: Updated metrics documentation with new metric types All metrics handle proper unit conversions (ms to seconds, CLS hundredths to decimal) and gracefully handle missing data.
1 parent 173fd76 commit 8107f9d

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,71 @@ Note: The example dashboard assumes you're fetching all pagespeed categories.
2929

3030
Prometheus exporter for google pagespeed metrics
3131

32+
## CrUX Metrics (Real User Monitoring)
33+
34+
The exporter provides Chrome User Experience Report (CrUX) metrics, which represent real-world user experience data collected from Chrome browsers. CrUX data is only available for URLs and origins with sufficient user traffic.
35+
36+
### Metric Prefixes
37+
38+
- `pagespeed_loading_experience_*` - URL-specific RUM data (when available for the specific page)
39+
- `pagespeed_origin_loading_experience_*` - Origin-wide RUM data (aggregated across the entire domain)
40+
41+
### Available CrUX Metrics
42+
43+
The exporter provides three types of metrics for each Core Web Vital:
44+
45+
1. **P75 Percentile** - The 75th percentile value (75% of users experience better performance)
46+
2. **Category Ratios** - Proportion of users experiencing fast/average/slow performance
47+
3. **Thresholds** - Google's Core Web Vitals performance boundaries
48+
49+
#### Core Web Vitals Included
50+
51+
- **Largest Contentful Paint (LCP)** - Measures loading performance
52+
- **Interaction to Next Paint (INP)** - Measures interactivity
53+
- **Cumulative Layout Shift (CLS)** - Measures visual stability
54+
- **First Contentful Paint (FCP)** - Measures perceived load speed
55+
- **Experimental Time to First Byte (TTFB)** - Measures server responsiveness
56+
57+
### Example Metrics Output
58+
59+
For Largest Contentful Paint (LCP):
60+
61+
```prometheus
62+
# P75 percentile (existing metric)
63+
pagespeed_loading_experience_metrics_largest_contentful_paint_duration_seconds 1.278
64+
65+
# Category distribution ratios (proportion of users in each category)
66+
pagespeed_loading_experience_metrics_largest_contentful_paint_category_ratio{category="fast"} 0.9389
67+
pagespeed_loading_experience_metrics_largest_contentful_paint_category_ratio{category="average"} 0.0371
68+
pagespeed_loading_experience_metrics_largest_contentful_paint_category_ratio{category="slow"} 0.0240
69+
70+
# Core Web Vitals thresholds
71+
pagespeed_loading_experience_metrics_largest_contentful_paint_threshold_duration_seconds{threshold="good"} 2.5
72+
pagespeed_loading_experience_metrics_largest_contentful_paint_threshold_duration_seconds{threshold="poor"} 4.0
73+
```
74+
75+
The same pattern applies to INP, FCP, and TTFB. For CLS (which is unitless), the metrics omit the `_duration_seconds` suffix:
76+
77+
```prometheus
78+
pagespeed_loading_experience_metrics_cumulative_layout_shift_score 0.0
79+
pagespeed_loading_experience_metrics_cumulative_layout_shift_score_category_ratio{category="fast"} 0.9994
80+
pagespeed_loading_experience_metrics_cumulative_layout_shift_score_threshold{threshold="good"} 0.1
81+
```
82+
83+
### Performance Categories
84+
85+
- **Fast** - Meets Google's "good" threshold (provides a good user experience)
86+
- **Average** - Between "good" and "poor" thresholds (needs improvement)
87+
- **Slow** - Exceeds "poor" threshold (provides a poor user experience)
88+
89+
### Data Availability
90+
91+
CrUX data is based on real user measurements from Chrome browsers over the last 28 days. Metrics will only be available for:
92+
- URLs with sufficient Chrome user traffic
93+
- Origins (domains) with sufficient Chrome user traffic
94+
95+
If data is unavailable, the corresponding metrics will not be exported.
96+
3297

3398
## Building And Running
3499

collector/collector.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,75 @@ func collectLoadingExperience(prefix string, lexp *pagespeedonline.PagespeedApiL
159159

160160
for k, v := range lexp.Metrics {
161161
name := strings.TrimSuffix(strings.ToLower(k), "_ms")
162+
163+
// Export P75 percentile (existing metric - unchanged)
162164
ch <- prometheus.MustNewConstMetric(
163165
prometheus.NewDesc(fqname(prefix, "metrics", name, "duration_seconds"), "Percentile metrics for "+strings.Replace(name, "_", " ", -1), nil, constLables),
164166
prometheus.GaugeValue,
165167
float64(v.Percentile)/1000)
168+
169+
// Export category distribution ratios (NEW)
170+
if len(v.Distributions) >= 3 {
171+
categories := []string{"fast", "average", "slow"}
172+
for i, dist := range v.Distributions {
173+
if i >= 3 {
174+
break
175+
}
176+
ch <- prometheus.MustNewConstMetric(
177+
prometheus.NewDesc(fqname(prefix, "metrics", name, "category_ratio"), "Proportion of users experiencing "+categories[i]+" performance", []string{"category"}, constLables),
178+
prometheus.GaugeValue,
179+
dist.Proportion,
180+
categories[i])
181+
}
182+
}
183+
184+
// Export Core Web Vitals thresholds (NEW)
185+
if len(v.Distributions) >= 2 {
186+
// Determine if this is CLS (uses hundredths) or time-based (uses ms)
187+
isCLS := strings.Contains(strings.ToLower(k), "cumulative_layout_shift")
188+
189+
// Extract good threshold (upper bound of FAST bucket)
190+
if v.Distributions[0].Max > 0 {
191+
goodThreshold := float64(v.Distributions[0].Max)
192+
if isCLS {
193+
goodThreshold = goodThreshold / 100.0 // Convert hundredths to decimal
194+
} else {
195+
goodThreshold = goodThreshold / 1000.0 // Convert ms to seconds
196+
}
197+
198+
metricSuffix := "duration_seconds"
199+
if isCLS {
200+
metricSuffix = "" // CLS is unitless
201+
}
202+
203+
ch <- prometheus.MustNewConstMetric(
204+
prometheus.NewDesc(fqname(prefix, "metrics", name, "threshold", metricSuffix), "Core Web Vitals threshold for "+strings.Replace(name, "_", " ", -1), []string{"threshold"}, constLables),
205+
prometheus.GaugeValue,
206+
goodThreshold,
207+
"good")
208+
}
209+
210+
// Extract poor threshold (upper bound of AVERAGE bucket)
211+
if v.Distributions[1].Max > 0 {
212+
poorThreshold := float64(v.Distributions[1].Max)
213+
if isCLS {
214+
poorThreshold = poorThreshold / 100.0
215+
} else {
216+
poorThreshold = poorThreshold / 1000.0
217+
}
218+
219+
metricSuffix := "duration_seconds"
220+
if isCLS {
221+
metricSuffix = ""
222+
}
223+
224+
ch <- prometheus.MustNewConstMetric(
225+
prometheus.NewDesc(fqname(prefix, "metrics", name, "threshold", metricSuffix), "Core Web Vitals threshold for "+strings.Replace(name, "_", " ", -1), []string{"threshold"}, constLables),
226+
prometheus.GaugeValue,
227+
poorThreshold,
228+
"poor")
229+
}
230+
}
166231
}
167232

168233
}

0 commit comments

Comments
 (0)