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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab180fed2f9ef6a48d37dd386354f989bc28754efed1ab80d266376663508758
4
- data.tar.gz: e6da57a476597d37e570d8fabd2853fbbec7b06d26be4fcf1c2fb6dee3de6627
3
+ metadata.gz: a0154a09e436255ed97202beba5751fe2b9ed58218607b4eb1505874720e3590
4
+ data.tar.gz: e80b323d84dc95de1605efa3f87760c8ad3bb2f85f033d4f4a6625e51267fa21
5
5
  SHA512:
6
- metadata.gz: 44f6b7a4bb0a9234cc55cf106454bca07bafa45f6137cbb34a159ca4641f7b2e2733313480730f65168444332b8b52d617051f98582c408e53373a12dd81323b
7
- data.tar.gz: bf58ea4760ab212945846dabcddbb75e65e3c3a482c86d6ec34b2ecf4480b89a3aaa16b0810c5b84b61d31624ed1c6990aaa44503fc4fbac19551d0d5e52908c
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
- ### Unauthenticated Mode (Default)
100
- `gsc speed` works immediately with **zero configuration** for normal development auditing:
101
- ```bash
102
- gsc speed https://example.com
103
- ```
104
-
105
- ### High-Throughput Mode (Optional Free API Key)
106
- If you run high-frequency batch audits in CI/CD pipelines (up to 25,000 queries/day free):
107
- 1. In Google Cloud Console, enable **PageSpeed Insights API**.
108
- 2. Go to **APIs & Services > Credentials** > **Create Credentials** > **API Key**.
109
- 3. Save the key:
110
- ```bash
111
- gsc config set pagespeed_api_key AIzaSyYourKeyHere
112
-
113
- # Or pass as environment variable
114
- export PAGESPEED_API_KEY=AIzaSyYourKeyHere
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` | Official Core Web Vitals via PageSpeed Insights (LCP, INP, CLS, TTFB) | `--strategy mobile|desktop`, `--json` |
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.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.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'] = 'gsc-cli/2.1'
3119
+ req['User-Agent'] = "gsc-cli/#{GSC::VERSION}"
3120
3120
 
3121
- res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
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.each do |k, v|
3159
- crux_metrics[k] = {
3160
- percentile: v['percentile'],
3161
- category: v['category']
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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.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.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'] = 'gsc-cli/2.1'
3119
+ req['User-Agent'] = "gsc-cli/#{GSC::VERSION}"
3120
3120
 
3121
- res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
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.each do |k, v|
3159
- crux_metrics[k] = {
3160
- percentile: v['percentile'],
3161
- category: v['category']
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}"
@@ -170,13 +170,70 @@ module GSC
170
170
  puts "╚══════════════════════════════════════════════════════════════╝"
171
171
 
172
172
  m = data[:metrics] || {}
173
- puts "\n#{Color::BOLD}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}"
@@ -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'] = 'gsc-cli/2.1'
31
+ req['User-Agent'] = "gsc-cli/#{GSC::VERSION}"
32
32
 
33
- res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
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.each do |k, v|
71
- crux_metrics[k] = {
72
- percentile: v['percentile'],
73
- category: v['category']
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GSC
4
- VERSION = '2.2.2'
4
+ VERSION = '2.2.4'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gsc-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.2
4
+ version: 2.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - ApollosWave LLC