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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab180fed2f9ef6a48d37dd386354f989bc28754efed1ab80d266376663508758
4
- data.tar.gz: e6da57a476597d37e570d8fabd2853fbbec7b06d26be4fcf1c2fb6dee3de6627
3
+ metadata.gz: c6cfd0972501711b114e27b2a286612efcfe85bd51aaf91947bc44b8fdfd9aff
4
+ data.tar.gz: 5bd6e630844f13a3115bbb4c54deda04f7f3e1585c79ac4d9037dd7c216d02a1
5
5
  SHA512:
6
- metadata.gz: 44f6b7a4bb0a9234cc55cf106454bca07bafa45f6137cbb34a159ca4641f7b2e2733313480730f65168444332b8b52d617051f98582c408e53373a12dd81323b
7
- data.tar.gz: bf58ea4760ab212945846dabcddbb75e65e3c3a482c86d6ec34b2ecf4480b89a3aaa16b0810c5b84b61d31624ed1c6990aaa44503fc4fbac19551d0d5e52908c
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
- ### 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.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.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'] = '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,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.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?
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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.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.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'] = '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,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.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?
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}📊 CORE WEB VITALS (Lab Metrics):#{Color::RESET}"
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}📊 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
+ 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}"
@@ -170,13 +170,61 @@ 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
+ 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}"
@@ -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,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.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?
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
@@ -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.3'
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.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ApollosWave LLC