gsc-cli 2.2.2 â 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AUTH.md +44 -17
- data/README.md +2 -1
- data/bin/gsc +164 -12
- data/dist/gsc +164 -12
- data/lib/gsc/cli/audit.rb +58 -1
- data/lib/gsc/cli_advanced.rb +58 -1
- data/lib/gsc/page_speed.rb +46 -8
- data/lib/gsc/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0154a09e436255ed97202beba5751fe2b9ed58218607b4eb1505874720e3590
|
|
4
|
+
data.tar.gz: e80b323d84dc95de1605efa3f87760c8ad3bb2f85f033d4f4a6625e51267fa21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1f6f6b813125ae759e18bc74309fed17bb9f5acc29a2a6f78515b5127332a7dc27b7cad9c2649a821ce5bef7bcc8ba0c3376d6a19243093918732700294a4dc
|
|
7
|
+
data.tar.gz: 741bd3fd7fd509ad82836c070a7c5f982f81bf114a99d35935cff4b02d022a214beadd05988043036cae536cc43cf0bd289e40d2cf845387a2c3d38c350e2f79
|
data/AUTH.md
CHANGED
|
@@ -96,23 +96,50 @@ export GA4_PROPERTY_ID=123456789
|
|
|
96
96
|
|
|
97
97
|
Audit official mobile and desktop Lighthouse scores and 75th percentile Chrome User Experience Report (CrUX) metrics (LCP, INP, CLS, FCP, TTFB).
|
|
98
98
|
|
|
99
|
-
###
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
gsc speed
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
1
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
99
|
+
### Understanding Lab vs. Field (CrUX) Data:
|
|
100
|
+
* **đŦ Lighthouse Lab Data (Synthetic Simulation):** Evaluated instantaneously in a headless Chrome environment under fixed 4G mobile or desktop throttling. Ideal for immediate debugging and identifying optimization opportunities (LCP, FCP, CLS, TBT, Speed Index).
|
|
101
|
+
* **đ CrUX Field Data (28-Day Real User Monitoring):** Aggregated 75th percentile measurements collected from real Google Chrome users over a rolling 28-day window. This is the **actual telemetry Google Search uses for Core Web Vitals ranking signals** (LCP, INP, CLS, FCP, TTFB).
|
|
102
|
+
* **Traffic Thresholds:** For new or low-traffic pages, Google does not have enough real-user Chrome sessions to publish CrUX data. In those cases, `gsc speed` automatically displays the instantaneous Lighthouse Lab audit and flags that CrUX field data is pending sufficient 28-day visitor volume.
|
|
103
|
+
|
|
104
|
+
> đĄ **Why an API Key is Recommended:**
|
|
105
|
+
> While Google technically permits unauthenticated queries, Google's public unauthenticated IP pool frequently hits `429 Quota Exceeded (RESOURCE_EXHAUSTED)`. Generating a personal API key in Google Cloud gives you **25,000 queries per day at $0 cost**.
|
|
106
|
+
|
|
107
|
+
### 1-Minute API Key Setup
|
|
108
|
+
|
|
109
|
+
1. **Enable the API:**
|
|
110
|
+
* Open [Google Cloud Console: PageSpeed Insights API](https://console.cloud.google.com/apis/library/pagespeedonline.googleapis.com).
|
|
111
|
+
* Ensure your active project is selected in the top bar.
|
|
112
|
+
* Click **Enable** (if not already enabled, Google will return a `403 SERVICE_DISABLED` error).
|
|
113
|
+
|
|
114
|
+
2. **Create the API Key:**
|
|
115
|
+
* In the left sidebar, navigate to **APIs & Services > Credentials**.
|
|
116
|
+
* Click **+ Create Credentials** at the top â select **API Key**.
|
|
117
|
+
* In the side panel:
|
|
118
|
+
* **Name:** `gsc-pagespeed-key` (or default `API key 1`).
|
|
119
|
+
* **Application restrictions:** Keep **None** (since `gsc` is a local command-line tool).
|
|
120
|
+
* **API restrictions:** (Optional) Select **Restrict key** â check **PageSpeed Insights API**.
|
|
121
|
+
* Click the blue **Create** button.
|
|
122
|
+
* Copy the generated key (`AIzaSy...`).
|
|
123
|
+
|
|
124
|
+
3. **Save Your Key:**
|
|
125
|
+
```bash
|
|
126
|
+
gsc config set pagespeed_api_key AIzaSyYourKeyHere
|
|
127
|
+
|
|
128
|
+
# Or pass as environment variable
|
|
129
|
+
export PAGESPEED_API_KEY=AIzaSyYourKeyHere
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
4. **Run Speed Audits:**
|
|
133
|
+
```bash
|
|
134
|
+
# Mobile Core Web Vitals (Default)
|
|
135
|
+
gsc speed https://example.com
|
|
136
|
+
|
|
137
|
+
# Desktop Core Web Vitals
|
|
138
|
+
gsc speed https://example.com --strategy desktop
|
|
139
|
+
|
|
140
|
+
# Correlate CWV with Google Search Console traffic
|
|
141
|
+
gsc speed-correlate https://example.com
|
|
142
|
+
```
|
|
116
143
|
|
|
117
144
|
---
|
|
118
145
|
|
data/README.md
CHANGED
|
@@ -100,6 +100,7 @@ By implementing Google's JWT service account authentication in pure `OpenSSL` an
|
|
|
100
100
|
- đĄī¸ **Cannibalization & Decay Detection (`gsc cannibalization`, `gsc decay`)**: Spot internal URLs fighting for the same queries, and compare 28-day period-over-period traffic trends.
|
|
101
101
|
- đ **Google Analytics 4 (GA4) Behavioral Link (`gsc realtime`, `gsc ga4`)**: Stream live active visitors and correlate SERP rankings with landing page bounce rates.
|
|
102
102
|
- đˇī¸ **Autonomous Site Audit & Broken Link Repair (`gsc site-audit`)**: Crawls all sitemap URLs, checks HTTP response codes for dead internal links (404/500/timeouts), audits missing image alts and heading defects, and exports an AI-actionable Markdown fix sprint.
|
|
103
|
+
- ⥠**Dual Lab & Field Core Web Vitals (`gsc speed` & `gsc speed-correlate`)**: Audits simulated Lighthouse Lab metrics (LCP, FCP, CLS, TBT, Speed Index) alongside official 28-day Chrome User Experience Report (CrUX) Real User Monitoring (RUM) field data (LCP, INP, CLS, FCP, TTFB) and correlates page speed directly with organic GSC search rankings.
|
|
103
104
|
- đ¤ **AI Agent Native**: Every single command supports `--compact`, `--ndjson`, and `--json` for instantaneous programmatic consumption by AI agents.
|
|
104
105
|
|
|
105
106
|
---
|
|
@@ -465,7 +466,7 @@ gsc use 2
|
|
|
465
466
|
| `gsc report` | â | Executive 360° Health Scorecard with letter grade and sparklines | `--days`, `--sparkline`, `--json` |
|
|
466
467
|
| `gsc page <url>` | â | Detailed On-Page DOM audit (meta, headings, alts, schema) + GSC performance | `--check-links`, `--json` |
|
|
467
468
|
| `gsc site-audit <sitemap>`| â | Crawls sitemaps, tests 404 dead links, audits DOM flaws, and outputs fix sprint | `--report <file>`, `--json` |
|
|
468
|
-
| `gsc speed <url>` | `vitals` |
|
|
469
|
+
| `gsc speed <url>` | `vitals` | Dual Lab (Lighthouse) & Field (CrUX 28-day RUM) Core Web Vitals (LCP, INP, CLS, TTFB) | `--strategy mobile|desktop`, `--json` |
|
|
469
470
|
| `gsc speed-correlate` | `sc-perf` | Correlates Core Web Vitals page speed with GSC organic rankings | `--days`, `--strategy`, `--json` |
|
|
470
471
|
| `gsc canonical-chains` | `chains` | Canonical redirect loops and multi-hop chain detector | `--limit`, `--json` |
|
|
471
472
|
| `gsc low-ctr` | â | High-impression low-CTR title & meta description rewriter | `--limit`, `--min-imp`, `--json` |
|
data/bin/gsc
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# ==============================================================================
|
|
5
5
|
# Google Search Console & Indexing API CLI Tool (Pure Ruby - Zero Gem Dependencies)
|
|
6
|
-
# Standalone Single-File Distribution (Built from lib/gsc v2.2.
|
|
6
|
+
# Standalone Single-File Distribution (Built from lib/gsc v2.2.4)
|
|
7
7
|
# ==============================================================================
|
|
8
8
|
|
|
9
9
|
require 'net/http'
|
|
@@ -20,7 +20,7 @@ require 'stringio'
|
|
|
20
20
|
|
|
21
21
|
# --- version.rb ---
|
|
22
22
|
module GSC
|
|
23
|
-
VERSION = '2.2.
|
|
23
|
+
VERSION = '2.2.4'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# --- color.rb ---
|
|
@@ -3116,9 +3116,9 @@ module GSC
|
|
|
3116
3116
|
|
|
3117
3117
|
uri = URI("#{API_URL}?#{query_params.join('&')}")
|
|
3118
3118
|
req = Net::HTTP::Get.new(uri)
|
|
3119
|
-
req['User-Agent'] =
|
|
3119
|
+
req['User-Agent'] = "gsc-cli/#{GSC::VERSION}"
|
|
3120
3120
|
|
|
3121
|
-
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout:
|
|
3121
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 15, read_timeout: 60) do |http|
|
|
3122
3122
|
http.request(req)
|
|
3123
3123
|
end
|
|
3124
3124
|
|
|
@@ -3152,14 +3152,49 @@ module GSC
|
|
|
3152
3152
|
tbt = audits.dig('total-blocking-time', 'displayValue')
|
|
3153
3153
|
si = audits.dig('speed-index', 'displayValue')
|
|
3154
3154
|
|
|
3155
|
-
# CrUX Field Data (if available)
|
|
3155
|
+
# CrUX Field Data (URL or Origin fallback if available)
|
|
3156
3156
|
crux_metrics = {}
|
|
3157
|
+
crux_source = nil
|
|
3157
3158
|
crux = data.dig('loadingExperience', 'metrics') || {}
|
|
3158
|
-
crux
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3159
|
+
if crux && !crux.empty?
|
|
3160
|
+
crux_source = :url
|
|
3161
|
+
else
|
|
3162
|
+
crux = data.dig('originLoadingExperience', 'metrics') || {}
|
|
3163
|
+
crux_source = :origin unless crux.empty?
|
|
3164
|
+
end
|
|
3165
|
+
|
|
3166
|
+
if crux && !crux.empty?
|
|
3167
|
+
crux.each do |k, v|
|
|
3168
|
+
crux_metrics[k] = {
|
|
3169
|
+
percentile: v['percentile'],
|
|
3170
|
+
category: v['category']
|
|
3171
|
+
}
|
|
3172
|
+
end
|
|
3173
|
+
end
|
|
3174
|
+
|
|
3175
|
+
overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
|
|
3176
|
+
|
|
3177
|
+
# Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
|
|
3178
|
+
# LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
|
|
3179
|
+
# - PASSED: All present CWV metrics are 'FAST' (Good).
|
|
3180
|
+
# - POOR: Any CWV metric is 'SLOW' (Poor).
|
|
3181
|
+
# - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
|
|
3182
|
+
cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
3183
|
+
cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
|
|
3184
|
+
|
|
3185
|
+
active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
|
|
3186
|
+
cwv_assessment = if active_cwv.any?
|
|
3187
|
+
if active_cwv.any? { |m| m[:category] == 'SLOW' }
|
|
3188
|
+
'POOR'
|
|
3189
|
+
elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
|
|
3190
|
+
'NEEDS IMPROVEMENT'
|
|
3191
|
+
elsif active_cwv.all? { |m| m[:category] == 'FAST' }
|
|
3192
|
+
'PASSED'
|
|
3193
|
+
else
|
|
3194
|
+
overall_category
|
|
3195
|
+
end
|
|
3196
|
+
else
|
|
3197
|
+
overall_category
|
|
3163
3198
|
end
|
|
3164
3199
|
|
|
3165
3200
|
# Opportunities
|
|
@@ -3191,6 +3226,9 @@ module GSC
|
|
|
3191
3226
|
speed_index: si
|
|
3192
3227
|
},
|
|
3193
3228
|
field_data: crux_metrics,
|
|
3229
|
+
field_source: crux_source,
|
|
3230
|
+
overall_category: overall_category,
|
|
3231
|
+
cwv_assessment: cwv_assessment,
|
|
3194
3232
|
opportunities: opportunities.first(5)
|
|
3195
3233
|
}
|
|
3196
3234
|
end
|
|
@@ -22833,13 +22871,70 @@ module GSC
|
|
|
22833
22871
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
22834
22872
|
|
|
22835
22873
|
m = data[:metrics] || {}
|
|
22836
|
-
puts "\n#{Color::BOLD}
|
|
22874
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
22837
22875
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
22838
22876
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
22839
22877
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
22840
22878
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
22841
22879
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
22842
22880
|
|
|
22881
|
+
fd = data[:field_data] || {}
|
|
22882
|
+
if fd.any?
|
|
22883
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
22884
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
22885
|
+
assessment_badge = case assessment
|
|
22886
|
+
when 'PASSED', 'FAST'
|
|
22887
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
22888
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
22889
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
22890
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
22891
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
22892
|
+
else
|
|
22893
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
22894
|
+
end
|
|
22895
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
22896
|
+
puts " âĸ Core Web Vitals Assessment : #{assessment_badge}"
|
|
22897
|
+
|
|
22898
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
22899
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22900
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
22901
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22902
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22903
|
+
end
|
|
22904
|
+
|
|
22905
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
22906
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
22907
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
22908
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22909
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22910
|
+
end
|
|
22911
|
+
|
|
22912
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
22913
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
22914
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
22915
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22916
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22917
|
+
end
|
|
22918
|
+
|
|
22919
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
22920
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22921
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
22922
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22923
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22924
|
+
end
|
|
22925
|
+
|
|
22926
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
22927
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
22928
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
22929
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22930
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22931
|
+
end
|
|
22932
|
+
else
|
|
22933
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
22934
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
22935
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
22936
|
+
end
|
|
22937
|
+
|
|
22843
22938
|
opps = data[:opportunities] || []
|
|
22844
22939
|
unless opps.empty?
|
|
22845
22940
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
|
@@ -29944,13 +30039,70 @@ module GSC
|
|
|
29944
30039
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
29945
30040
|
|
|
29946
30041
|
m = data[:metrics] || {}
|
|
29947
|
-
puts "\n#{Color::BOLD}
|
|
30042
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
29948
30043
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
29949
30044
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
29950
30045
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
29951
30046
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
29952
30047
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
29953
30048
|
|
|
30049
|
+
fd = data[:field_data] || {}
|
|
30050
|
+
if fd.any?
|
|
30051
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
30052
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
30053
|
+
assessment_badge = case assessment
|
|
30054
|
+
when 'PASSED', 'FAST'
|
|
30055
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
30056
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
30057
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
30058
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
30059
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
30060
|
+
else
|
|
30061
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
30062
|
+
end
|
|
30063
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
30064
|
+
puts " âĸ Core Web Vitals Assessment : #{assessment_badge}"
|
|
30065
|
+
|
|
30066
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
30067
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30068
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
30069
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30070
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30071
|
+
end
|
|
30072
|
+
|
|
30073
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
30074
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
30075
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
30076
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30077
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30078
|
+
end
|
|
30079
|
+
|
|
30080
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
30081
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
30082
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
30083
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30084
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30085
|
+
end
|
|
30086
|
+
|
|
30087
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
30088
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30089
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
30090
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30091
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30092
|
+
end
|
|
30093
|
+
|
|
30094
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
30095
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
30096
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
30097
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30098
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30099
|
+
end
|
|
30100
|
+
else
|
|
30101
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
30102
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
30103
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
30104
|
+
end
|
|
30105
|
+
|
|
29954
30106
|
opps = data[:opportunities] || []
|
|
29955
30107
|
unless opps.empty?
|
|
29956
30108
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
data/dist/gsc
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# ==============================================================================
|
|
5
5
|
# Google Search Console & Indexing API CLI Tool (Pure Ruby - Zero Gem Dependencies)
|
|
6
|
-
# Standalone Single-File Distribution (Built from lib/gsc v2.2.
|
|
6
|
+
# Standalone Single-File Distribution (Built from lib/gsc v2.2.4)
|
|
7
7
|
# ==============================================================================
|
|
8
8
|
|
|
9
9
|
require 'net/http'
|
|
@@ -20,7 +20,7 @@ require 'stringio'
|
|
|
20
20
|
|
|
21
21
|
# --- version.rb ---
|
|
22
22
|
module GSC
|
|
23
|
-
VERSION = '2.2.
|
|
23
|
+
VERSION = '2.2.4'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# --- color.rb ---
|
|
@@ -3116,9 +3116,9 @@ module GSC
|
|
|
3116
3116
|
|
|
3117
3117
|
uri = URI("#{API_URL}?#{query_params.join('&')}")
|
|
3118
3118
|
req = Net::HTTP::Get.new(uri)
|
|
3119
|
-
req['User-Agent'] =
|
|
3119
|
+
req['User-Agent'] = "gsc-cli/#{GSC::VERSION}"
|
|
3120
3120
|
|
|
3121
|
-
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout:
|
|
3121
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 15, read_timeout: 60) do |http|
|
|
3122
3122
|
http.request(req)
|
|
3123
3123
|
end
|
|
3124
3124
|
|
|
@@ -3152,14 +3152,49 @@ module GSC
|
|
|
3152
3152
|
tbt = audits.dig('total-blocking-time', 'displayValue')
|
|
3153
3153
|
si = audits.dig('speed-index', 'displayValue')
|
|
3154
3154
|
|
|
3155
|
-
# CrUX Field Data (if available)
|
|
3155
|
+
# CrUX Field Data (URL or Origin fallback if available)
|
|
3156
3156
|
crux_metrics = {}
|
|
3157
|
+
crux_source = nil
|
|
3157
3158
|
crux = data.dig('loadingExperience', 'metrics') || {}
|
|
3158
|
-
crux
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3159
|
+
if crux && !crux.empty?
|
|
3160
|
+
crux_source = :url
|
|
3161
|
+
else
|
|
3162
|
+
crux = data.dig('originLoadingExperience', 'metrics') || {}
|
|
3163
|
+
crux_source = :origin unless crux.empty?
|
|
3164
|
+
end
|
|
3165
|
+
|
|
3166
|
+
if crux && !crux.empty?
|
|
3167
|
+
crux.each do |k, v|
|
|
3168
|
+
crux_metrics[k] = {
|
|
3169
|
+
percentile: v['percentile'],
|
|
3170
|
+
category: v['category']
|
|
3171
|
+
}
|
|
3172
|
+
end
|
|
3173
|
+
end
|
|
3174
|
+
|
|
3175
|
+
overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
|
|
3176
|
+
|
|
3177
|
+
# Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
|
|
3178
|
+
# LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
|
|
3179
|
+
# - PASSED: All present CWV metrics are 'FAST' (Good).
|
|
3180
|
+
# - POOR: Any CWV metric is 'SLOW' (Poor).
|
|
3181
|
+
# - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
|
|
3182
|
+
cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
3183
|
+
cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
|
|
3184
|
+
|
|
3185
|
+
active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
|
|
3186
|
+
cwv_assessment = if active_cwv.any?
|
|
3187
|
+
if active_cwv.any? { |m| m[:category] == 'SLOW' }
|
|
3188
|
+
'POOR'
|
|
3189
|
+
elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
|
|
3190
|
+
'NEEDS IMPROVEMENT'
|
|
3191
|
+
elsif active_cwv.all? { |m| m[:category] == 'FAST' }
|
|
3192
|
+
'PASSED'
|
|
3193
|
+
else
|
|
3194
|
+
overall_category
|
|
3195
|
+
end
|
|
3196
|
+
else
|
|
3197
|
+
overall_category
|
|
3163
3198
|
end
|
|
3164
3199
|
|
|
3165
3200
|
# Opportunities
|
|
@@ -3191,6 +3226,9 @@ module GSC
|
|
|
3191
3226
|
speed_index: si
|
|
3192
3227
|
},
|
|
3193
3228
|
field_data: crux_metrics,
|
|
3229
|
+
field_source: crux_source,
|
|
3230
|
+
overall_category: overall_category,
|
|
3231
|
+
cwv_assessment: cwv_assessment,
|
|
3194
3232
|
opportunities: opportunities.first(5)
|
|
3195
3233
|
}
|
|
3196
3234
|
end
|
|
@@ -22833,13 +22871,70 @@ module GSC
|
|
|
22833
22871
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
22834
22872
|
|
|
22835
22873
|
m = data[:metrics] || {}
|
|
22836
|
-
puts "\n#{Color::BOLD}
|
|
22874
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
22837
22875
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
22838
22876
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
22839
22877
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
22840
22878
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
22841
22879
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
22842
22880
|
|
|
22881
|
+
fd = data[:field_data] || {}
|
|
22882
|
+
if fd.any?
|
|
22883
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
22884
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
22885
|
+
assessment_badge = case assessment
|
|
22886
|
+
when 'PASSED', 'FAST'
|
|
22887
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
22888
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
22889
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
22890
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
22891
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
22892
|
+
else
|
|
22893
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
22894
|
+
end
|
|
22895
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
22896
|
+
puts " âĸ Core Web Vitals Assessment : #{assessment_badge}"
|
|
22897
|
+
|
|
22898
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
22899
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22900
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
22901
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22902
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22903
|
+
end
|
|
22904
|
+
|
|
22905
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
22906
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
22907
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
22908
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22909
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22910
|
+
end
|
|
22911
|
+
|
|
22912
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
22913
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
22914
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
22915
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22916
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22917
|
+
end
|
|
22918
|
+
|
|
22919
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
22920
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22921
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
22922
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22923
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22924
|
+
end
|
|
22925
|
+
|
|
22926
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
22927
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
22928
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
22929
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22930
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22931
|
+
end
|
|
22932
|
+
else
|
|
22933
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
22934
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
22935
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
22936
|
+
end
|
|
22937
|
+
|
|
22843
22938
|
opps = data[:opportunities] || []
|
|
22844
22939
|
unless opps.empty?
|
|
22845
22940
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
|
@@ -29944,13 +30039,70 @@ module GSC
|
|
|
29944
30039
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
29945
30040
|
|
|
29946
30041
|
m = data[:metrics] || {}
|
|
29947
|
-
puts "\n#{Color::BOLD}
|
|
30042
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
29948
30043
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
29949
30044
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
29950
30045
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
29951
30046
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
29952
30047
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
29953
30048
|
|
|
30049
|
+
fd = data[:field_data] || {}
|
|
30050
|
+
if fd.any?
|
|
30051
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
30052
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
30053
|
+
assessment_badge = case assessment
|
|
30054
|
+
when 'PASSED', 'FAST'
|
|
30055
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
30056
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
30057
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
30058
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
30059
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
30060
|
+
else
|
|
30061
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
30062
|
+
end
|
|
30063
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
30064
|
+
puts " âĸ Core Web Vitals Assessment : #{assessment_badge}"
|
|
30065
|
+
|
|
30066
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
30067
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30068
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
30069
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30070
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30071
|
+
end
|
|
30072
|
+
|
|
30073
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
30074
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
30075
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
30076
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30077
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30078
|
+
end
|
|
30079
|
+
|
|
30080
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
30081
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
30082
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
30083
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30084
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30085
|
+
end
|
|
30086
|
+
|
|
30087
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
30088
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30089
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
30090
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30091
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30092
|
+
end
|
|
30093
|
+
|
|
30094
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
30095
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
30096
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
30097
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30098
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30099
|
+
end
|
|
30100
|
+
else
|
|
30101
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
30102
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
30103
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
30104
|
+
end
|
|
30105
|
+
|
|
29954
30106
|
opps = data[:opportunities] || []
|
|
29955
30107
|
unless opps.empty?
|
|
29956
30108
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
data/lib/gsc/cli/audit.rb
CHANGED
|
@@ -160,13 +160,70 @@ module GSC
|
|
|
160
160
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
161
161
|
|
|
162
162
|
m = data[:metrics] || {}
|
|
163
|
-
puts "\n#{Color::BOLD}
|
|
163
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
164
164
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
165
165
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
166
166
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
167
167
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
168
168
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
169
169
|
|
|
170
|
+
fd = data[:field_data] || {}
|
|
171
|
+
if fd.any?
|
|
172
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
173
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
174
|
+
assessment_badge = case assessment
|
|
175
|
+
when 'PASSED', 'FAST'
|
|
176
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
177
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
178
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
179
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
180
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
181
|
+
else
|
|
182
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
183
|
+
end
|
|
184
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
185
|
+
puts " âĸ Core Web Vitals Assessment : #{assessment_badge}"
|
|
186
|
+
|
|
187
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
188
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
189
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
190
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
191
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
195
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
196
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
197
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
198
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
202
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
203
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
204
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
205
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
209
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
210
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
211
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
212
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
216
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
217
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
218
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
219
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
220
|
+
end
|
|
221
|
+
else
|
|
222
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
223
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
224
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
225
|
+
end
|
|
226
|
+
|
|
170
227
|
opps = data[:opportunities] || []
|
|
171
228
|
unless opps.empty?
|
|
172
229
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
data/lib/gsc/cli_advanced.rb
CHANGED
|
@@ -170,13 +170,70 @@ module GSC
|
|
|
170
170
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
171
171
|
|
|
172
172
|
m = data[:metrics] || {}
|
|
173
|
-
puts "\n#{Color::BOLD}
|
|
173
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
174
174
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
175
175
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
176
176
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
177
177
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
178
178
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
179
179
|
|
|
180
|
+
fd = data[:field_data] || {}
|
|
181
|
+
if fd.any?
|
|
182
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
183
|
+
assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
|
|
184
|
+
assessment_badge = case assessment
|
|
185
|
+
when 'PASSED', 'FAST'
|
|
186
|
+
Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
|
|
187
|
+
when 'NEEDS IMPROVEMENT', 'AVERAGE'
|
|
188
|
+
Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
|
|
189
|
+
when 'POOR', 'SLOW', 'FAILED'
|
|
190
|
+
Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
|
|
191
|
+
else
|
|
192
|
+
Color.c(assessment, Color::YELLOW, Color::BOLD)
|
|
193
|
+
end
|
|
194
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
195
|
+
puts " âĸ Core Web Vitals Assessment : #{assessment_badge}"
|
|
196
|
+
|
|
197
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
198
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
199
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
200
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
201
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
205
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
206
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
207
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
208
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
212
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
213
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
214
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
215
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
219
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
220
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
221
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
222
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
226
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
227
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
228
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
229
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
230
|
+
end
|
|
231
|
+
else
|
|
232
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
233
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
234
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
235
|
+
end
|
|
236
|
+
|
|
180
237
|
opps = data[:opportunities] || []
|
|
181
238
|
unless opps.empty?
|
|
182
239
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
data/lib/gsc/page_speed.rb
CHANGED
|
@@ -28,9 +28,9 @@ module GSC
|
|
|
28
28
|
|
|
29
29
|
uri = URI("#{API_URL}?#{query_params.join('&')}")
|
|
30
30
|
req = Net::HTTP::Get.new(uri)
|
|
31
|
-
req['User-Agent'] =
|
|
31
|
+
req['User-Agent'] = "gsc-cli/#{GSC::VERSION}"
|
|
32
32
|
|
|
33
|
-
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout:
|
|
33
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 15, read_timeout: 60) do |http|
|
|
34
34
|
http.request(req)
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -64,14 +64,49 @@ module GSC
|
|
|
64
64
|
tbt = audits.dig('total-blocking-time', 'displayValue')
|
|
65
65
|
si = audits.dig('speed-index', 'displayValue')
|
|
66
66
|
|
|
67
|
-
# CrUX Field Data (if available)
|
|
67
|
+
# CrUX Field Data (URL or Origin fallback if available)
|
|
68
68
|
crux_metrics = {}
|
|
69
|
+
crux_source = nil
|
|
69
70
|
crux = data.dig('loadingExperience', 'metrics') || {}
|
|
70
|
-
crux
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
if crux && !crux.empty?
|
|
72
|
+
crux_source = :url
|
|
73
|
+
else
|
|
74
|
+
crux = data.dig('originLoadingExperience', 'metrics') || {}
|
|
75
|
+
crux_source = :origin unless crux.empty?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if crux && !crux.empty?
|
|
79
|
+
crux.each do |k, v|
|
|
80
|
+
crux_metrics[k] = {
|
|
81
|
+
percentile: v['percentile'],
|
|
82
|
+
category: v['category']
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
|
|
88
|
+
|
|
89
|
+
# Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
|
|
90
|
+
# LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
|
|
91
|
+
# - PASSED: All present CWV metrics are 'FAST' (Good).
|
|
92
|
+
# - POOR: Any CWV metric is 'SLOW' (Poor).
|
|
93
|
+
# - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
|
|
94
|
+
cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
95
|
+
cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
|
|
96
|
+
|
|
97
|
+
active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
|
|
98
|
+
cwv_assessment = if active_cwv.any?
|
|
99
|
+
if active_cwv.any? { |m| m[:category] == 'SLOW' }
|
|
100
|
+
'POOR'
|
|
101
|
+
elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
|
|
102
|
+
'NEEDS IMPROVEMENT'
|
|
103
|
+
elsif active_cwv.all? { |m| m[:category] == 'FAST' }
|
|
104
|
+
'PASSED'
|
|
105
|
+
else
|
|
106
|
+
overall_category
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
overall_category
|
|
75
110
|
end
|
|
76
111
|
|
|
77
112
|
# Opportunities
|
|
@@ -103,6 +138,9 @@ module GSC
|
|
|
103
138
|
speed_index: si
|
|
104
139
|
},
|
|
105
140
|
field_data: crux_metrics,
|
|
141
|
+
field_source: crux_source,
|
|
142
|
+
overall_category: overall_category,
|
|
143
|
+
cwv_assessment: cwv_assessment,
|
|
106
144
|
opportunities: opportunities.first(5)
|
|
107
145
|
}
|
|
108
146
|
end
|
data/lib/gsc/version.rb
CHANGED