gsc-cli 2.2.2 â 2.2.3
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 +122 -12
- data/dist/gsc +122 -12
- data/lib/gsc/cli/audit.rb +49 -1
- data/lib/gsc/cli_advanced.rb +49 -1
- data/lib/gsc/page_speed.rb +22 -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: c6cfd0972501711b114e27b2a286612efcfe85bd51aaf91947bc44b8fdfd9aff
|
|
4
|
+
data.tar.gz: 5bd6e630844f13a3115bbb4c54deda04f7f3e1585c79ac4d9037dd7c216d02a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab41e83aa4f41ca0d296218f288cb2291275c893cf60347d6cbc7083371430e06feebf00aa084d5c5978fd0bd0bd5a35f29a1beaf5839f322f2efc629bf38429
|
|
7
|
+
data.tar.gz: 50ea5330dee9bae3b09570be862e69c3b4ec5e1aafc7126525bdd2dbe1a894c2392204749b5c7f187d32c0273530cc55f6ba66599502068b8870f1a7bdcd3f6b
|
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.3)
|
|
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.3'
|
|
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,16 +3152,28 @@ 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?
|
|
3163
3164
|
end
|
|
3164
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
|
+
|
|
3165
3177
|
# Opportunities
|
|
3166
3178
|
opportunities = []
|
|
3167
3179
|
audits.each do |k, v|
|
|
@@ -3191,6 +3203,8 @@ module GSC
|
|
|
3191
3203
|
speed_index: si
|
|
3192
3204
|
},
|
|
3193
3205
|
field_data: crux_metrics,
|
|
3206
|
+
field_source: crux_source,
|
|
3207
|
+
overall_category: overall_category,
|
|
3194
3208
|
opportunities: opportunities.first(5)
|
|
3195
3209
|
}
|
|
3196
3210
|
end
|
|
@@ -22833,13 +22847,61 @@ module GSC
|
|
|
22833
22847
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
22834
22848
|
|
|
22835
22849
|
m = data[:metrics] || {}
|
|
22836
|
-
puts "\n#{Color::BOLD}
|
|
22850
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
22837
22851
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
22838
22852
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
22839
22853
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
22840
22854
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
22841
22855
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
22842
22856
|
|
|
22857
|
+
fd = data[:field_data] || {}
|
|
22858
|
+
if fd.any?
|
|
22859
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
22860
|
+
overall = data[:overall_category] || "UNKNOWN"
|
|
22861
|
+
overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22862
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
22863
|
+
puts " âĸ Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
|
|
22864
|
+
|
|
22865
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
22866
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22867
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
22868
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22869
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22870
|
+
end
|
|
22871
|
+
|
|
22872
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
22873
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
22874
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
22875
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22876
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22877
|
+
end
|
|
22878
|
+
|
|
22879
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
22880
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
22881
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
22882
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22883
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22884
|
+
end
|
|
22885
|
+
|
|
22886
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
22887
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22888
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
22889
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22890
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22891
|
+
end
|
|
22892
|
+
|
|
22893
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
22894
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
22895
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
22896
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22897
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22898
|
+
end
|
|
22899
|
+
else
|
|
22900
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
22901
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
22902
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
22903
|
+
end
|
|
22904
|
+
|
|
22843
22905
|
opps = data[:opportunities] || []
|
|
22844
22906
|
unless opps.empty?
|
|
22845
22907
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
|
@@ -29944,13 +30006,61 @@ module GSC
|
|
|
29944
30006
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
29945
30007
|
|
|
29946
30008
|
m = data[:metrics] || {}
|
|
29947
|
-
puts "\n#{Color::BOLD}
|
|
30009
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
29948
30010
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
29949
30011
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
29950
30012
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
29951
30013
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
29952
30014
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
29953
30015
|
|
|
30016
|
+
fd = data[:field_data] || {}
|
|
30017
|
+
if fd.any?
|
|
30018
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
30019
|
+
overall = data[:overall_category] || "UNKNOWN"
|
|
30020
|
+
overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30021
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
30022
|
+
puts " âĸ Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
|
|
30023
|
+
|
|
30024
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
30025
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30026
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
30027
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30028
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30029
|
+
end
|
|
30030
|
+
|
|
30031
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
30032
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
30033
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
30034
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30035
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30036
|
+
end
|
|
30037
|
+
|
|
30038
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
30039
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
30040
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
30041
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30042
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30043
|
+
end
|
|
30044
|
+
|
|
30045
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
30046
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30047
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
30048
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30049
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30050
|
+
end
|
|
30051
|
+
|
|
30052
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
30053
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
30054
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
30055
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30056
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30057
|
+
end
|
|
30058
|
+
else
|
|
30059
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
30060
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
30061
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
30062
|
+
end
|
|
30063
|
+
|
|
29954
30064
|
opps = data[:opportunities] || []
|
|
29955
30065
|
unless opps.empty?
|
|
29956
30066
|
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.3)
|
|
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.3'
|
|
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,16 +3152,28 @@ 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?
|
|
3163
3164
|
end
|
|
3164
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
|
+
|
|
3165
3177
|
# Opportunities
|
|
3166
3178
|
opportunities = []
|
|
3167
3179
|
audits.each do |k, v|
|
|
@@ -3191,6 +3203,8 @@ module GSC
|
|
|
3191
3203
|
speed_index: si
|
|
3192
3204
|
},
|
|
3193
3205
|
field_data: crux_metrics,
|
|
3206
|
+
field_source: crux_source,
|
|
3207
|
+
overall_category: overall_category,
|
|
3194
3208
|
opportunities: opportunities.first(5)
|
|
3195
3209
|
}
|
|
3196
3210
|
end
|
|
@@ -22833,13 +22847,61 @@ module GSC
|
|
|
22833
22847
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
22834
22848
|
|
|
22835
22849
|
m = data[:metrics] || {}
|
|
22836
|
-
puts "\n#{Color::BOLD}
|
|
22850
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
22837
22851
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
22838
22852
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
22839
22853
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
22840
22854
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
22841
22855
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
22842
22856
|
|
|
22857
|
+
fd = data[:field_data] || {}
|
|
22858
|
+
if fd.any?
|
|
22859
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
22860
|
+
overall = data[:overall_category] || "UNKNOWN"
|
|
22861
|
+
overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22862
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
22863
|
+
puts " âĸ Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
|
|
22864
|
+
|
|
22865
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
22866
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22867
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
22868
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22869
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22870
|
+
end
|
|
22871
|
+
|
|
22872
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
22873
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
22874
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
22875
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22876
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22877
|
+
end
|
|
22878
|
+
|
|
22879
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
22880
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
22881
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
22882
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22883
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22884
|
+
end
|
|
22885
|
+
|
|
22886
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
22887
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
22888
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
22889
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22890
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22891
|
+
end
|
|
22892
|
+
|
|
22893
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
22894
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
22895
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
22896
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
22897
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
22898
|
+
end
|
|
22899
|
+
else
|
|
22900
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
22901
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
22902
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
22903
|
+
end
|
|
22904
|
+
|
|
22843
22905
|
opps = data[:opportunities] || []
|
|
22844
22906
|
unless opps.empty?
|
|
22845
22907
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
|
@@ -29944,13 +30006,61 @@ module GSC
|
|
|
29944
30006
|
puts "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
|
|
29945
30007
|
|
|
29946
30008
|
m = data[:metrics] || {}
|
|
29947
|
-
puts "\n#{Color::BOLD}
|
|
30009
|
+
puts "\n#{Color::BOLD}đŦ LIGHTHOUSE LAB METRICS (Simulated #{strategy.upcase}):#{Color::RESET}"
|
|
29948
30010
|
puts " âĸ LCP (Largest Contentful Paint) : #{Color.c(m[:lcp] || 'N/A', Color::BOLD)}"
|
|
29949
30011
|
puts " âĸ FCP (First Contentful Paint) : #{Color.c(m[:fcp] || 'N/A', Color::BOLD)}"
|
|
29950
30012
|
puts " âĸ CLS (Cumulative Layout Shift) : #{Color.c(m[:cls] || 'N/A', Color::BOLD)}"
|
|
29951
30013
|
puts " âĸ TBT (Total Blocking Time) : #{Color.c(m[:tbt] || 'N/A', Color::BOLD)}"
|
|
29952
30014
|
puts " âĸ Speed Index : #{Color.c(m[:speed_index] || 'N/A', Color::BOLD)}"
|
|
29953
30015
|
|
|
30016
|
+
fd = data[:field_data] || {}
|
|
30017
|
+
if fd.any?
|
|
30018
|
+
source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
|
|
30019
|
+
overall = data[:overall_category] || "UNKNOWN"
|
|
30020
|
+
overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30021
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
30022
|
+
puts " âĸ Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
|
|
30023
|
+
|
|
30024
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
30025
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30026
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
30027
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30028
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30029
|
+
end
|
|
30030
|
+
|
|
30031
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
30032
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
30033
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
30034
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30035
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30036
|
+
end
|
|
30037
|
+
|
|
30038
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
30039
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
30040
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
30041
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30042
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30043
|
+
end
|
|
30044
|
+
|
|
30045
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
30046
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
30047
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
30048
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30049
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30050
|
+
end
|
|
30051
|
+
|
|
30052
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
30053
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
30054
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
30055
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
30056
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
30057
|
+
end
|
|
30058
|
+
else
|
|
30059
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
30060
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
30061
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
30062
|
+
end
|
|
30063
|
+
|
|
29954
30064
|
opps = data[:opportunities] || []
|
|
29955
30065
|
unless opps.empty?
|
|
29956
30066
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
data/lib/gsc/cli/audit.rb
CHANGED
|
@@ -160,13 +160,61 @@ 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
|
+
overall = data[:overall_category] || "UNKNOWN"
|
|
174
|
+
overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
175
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
176
|
+
puts " âĸ Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
|
|
177
|
+
|
|
178
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
179
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
180
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
181
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
182
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
186
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
187
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
188
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
189
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
193
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
194
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
195
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
196
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
200
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
201
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
202
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
203
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
207
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
208
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
209
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
210
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
211
|
+
end
|
|
212
|
+
else
|
|
213
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
214
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
215
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
216
|
+
end
|
|
217
|
+
|
|
170
218
|
opps = data[:opportunities] || []
|
|
171
219
|
unless opps.empty?
|
|
172
220
|
puts "\n#{Color::BOLD}đĄ TOP SPEED OPPORTUNITIES:#{Color::RESET}"
|
data/lib/gsc/cli_advanced.rb
CHANGED
|
@@ -170,13 +170,61 @@ 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
|
+
overall = data[:overall_category] || "UNKNOWN"
|
|
184
|
+
overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
185
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
|
|
186
|
+
puts " âĸ Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
|
|
187
|
+
|
|
188
|
+
if fd['LARGEST_CONTENTFUL_PAINT_MS']
|
|
189
|
+
val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
190
|
+
cat = fd['LARGEST_CONTENTFUL_PAINT_MS'][:category]
|
|
191
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
192
|
+
puts " âĸ LCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if fd['INTERACTION_TO_NEXT_PAINT']
|
|
196
|
+
val = "#{fd['INTERACTION_TO_NEXT_PAINT'][:percentile]} ms"
|
|
197
|
+
cat = fd['INTERACTION_TO_NEXT_PAINT'][:category]
|
|
198
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
199
|
+
puts " âĸ INP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if fd['CUMULATIVE_LAYOUT_SHIFT_SCORE']
|
|
203
|
+
val = (fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:percentile].to_f / 100).round(3).to_s
|
|
204
|
+
cat = fd['CUMULATIVE_LAYOUT_SHIFT_SCORE'][:category]
|
|
205
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
206
|
+
puts " âĸ CLS (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if fd['FIRST_CONTENTFUL_PAINT_MS']
|
|
210
|
+
val = "#{(fd['FIRST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
|
|
211
|
+
cat = fd['FIRST_CONTENTFUL_PAINT_MS'][:category]
|
|
212
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
213
|
+
puts " âĸ FCP (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE']
|
|
217
|
+
val = "#{fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:percentile]} ms"
|
|
218
|
+
cat = fd['EXPERIMENTAL_TIME_TO_FIRST_BYTE'][:category]
|
|
219
|
+
col = cat == 'FAST' ? Color::GREEN : (cat == 'AVERAGE' ? Color::YELLOW : Color::RED)
|
|
220
|
+
puts " âĸ TTFB (75th Percentile) : #{Color.c(val, Color::BOLD)} [#{Color.c(cat, col)}]"
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
puts "\n#{Color::BOLD}đ CrUX FIELD DATA (28-Day Real User Monitoring):#{Color::RESET}"
|
|
224
|
+
puts " #{Color.c('âšī¸ Insufficient real-user Chrome visits over the rolling 28-day window for Google CrUX telemetry.', Color::GRAY)}"
|
|
225
|
+
puts " #{Color.c(' (Google requires a minimum traffic threshold before publishing real-user CrUX field data).', Color::GRAY)}"
|
|
226
|
+
end
|
|
227
|
+
|
|
180
228
|
opps = data[:opportunities] || []
|
|
181
229
|
unless opps.empty?
|
|
182
230
|
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,16 +64,28 @@ 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?
|
|
75
76
|
end
|
|
76
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
|
+
|
|
77
89
|
# Opportunities
|
|
78
90
|
opportunities = []
|
|
79
91
|
audits.each do |k, v|
|
|
@@ -103,6 +115,8 @@ module GSC
|
|
|
103
115
|
speed_index: si
|
|
104
116
|
},
|
|
105
117
|
field_data: crux_metrics,
|
|
118
|
+
field_source: crux_source,
|
|
119
|
+
overall_category: overall_category,
|
|
106
120
|
opportunities: opportunities.first(5)
|
|
107
121
|
}
|
|
108
122
|
end
|
data/lib/gsc/version.rb
CHANGED