gsc-cli 2.2.0 → 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 +48 -18
- data/README.md +186 -141
- data/bin/gsc +129 -19
- data/dist/gsc +129 -19
- data/lib/gsc/cli/audit.rb +49 -1
- data/lib/gsc/cli/keywords.rb +7 -7
- 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
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
|
|
@@ -21612,7 +21626,7 @@ module GSC
|
|
|
21612
21626
|
key = input unless input.nil? || input.empty?
|
|
21613
21627
|
key = existing if key.empty?
|
|
21614
21628
|
else
|
|
21615
|
-
puts "Get your Keywords Everywhere API key at: #{Color.c('https://keywordseverywhere.com
|
|
21629
|
+
puts "Get your Keywords Everywhere API key at: #{Color.c('https://keywordseverywhere.com/?fpr=us25sg', Color::UNDERLINE)}"
|
|
21616
21630
|
print "Paste your Keywords Everywhere API Key: "
|
|
21617
21631
|
key = $stdin.gets&.strip
|
|
21618
21632
|
end
|
|
@@ -21643,7 +21657,7 @@ module GSC
|
|
|
21643
21657
|
puts " 2. #{Color.c('100% Free Live Google Trends (Breakout & Related terms):', Color::BLUE, Color::BOLD)}"
|
|
21644
21658
|
puts " • Run: #{Color.c('gsc trends "seo audit"', Color::BLUE, Color::BOLD)}"
|
|
21645
21659
|
puts " 3. #{Color.c('Top Up Credits for Automated Background CLI Lookups:', Color::GRAY, Color::BOLD)}"
|
|
21646
|
-
puts " • Purchase credits at: https://keywordseverywhere.com/credit-packages.html"
|
|
21660
|
+
puts " • Purchase credits at: https://keywordseverywhere.com/credit-packages.html?fpr=us25sg"
|
|
21647
21661
|
puts " • Your key is already saved; it will work immediately upon top-up.\n"
|
|
21648
21662
|
else
|
|
21649
21663
|
puts "\nTry searching keywords:"
|
|
@@ -21668,7 +21682,7 @@ module GSC
|
|
|
21668
21682
|
puts "\n" + Color.c("ℹ️ You have 0 paid REST API credits remaining.", Color::YELLOW)
|
|
21669
21683
|
puts "• Free Web Alternative: Check volume on keywordseverywhere.com, click 'Copy', then run: #{Color.c('gsc import clip', Color::CYAN)}"
|
|
21670
21684
|
puts "• Free Live Trends: #{Color.c('gsc trends "seo audit"', Color::BLUE)}"
|
|
21671
|
-
puts "• Top up credits at: https://keywordseverywhere.com/credit-packages.html"
|
|
21685
|
+
puts "• Top up credits at: https://keywordseverywhere.com/credit-packages.html?fpr=us25sg"
|
|
21672
21686
|
end
|
|
21673
21687
|
else
|
|
21674
21688
|
if acct[:error].to_s =~ /401|Unauthorized/i
|
|
@@ -21705,7 +21719,7 @@ module GSC
|
|
|
21705
21719
|
else
|
|
21706
21720
|
puts Color.c("\n❌ Keywords Everywhere API key not configured!\n", Color::RED, Color::BOLD)
|
|
21707
21721
|
puts "#{Color::BOLD}RECOMMENDATIONS:#{Color::RESET}"
|
|
21708
|
-
puts " 1. Connect a key: #{Color.c('gsc connect ke', Color::CYAN, Color::BOLD)} (Get key at https://keywordseverywhere.com
|
|
21722
|
+
puts " 1. Connect a key: #{Color.c('gsc connect ke', Color::CYAN, Color::BOLD)} (Get key at https://keywordseverywhere.com/?fpr=us25sg)"
|
|
21709
21723
|
puts " 2. Use 100% free Google Trends (No key needed!): #{Color.c("gsc trends \"#{target}\"", Color::BLUE, Color::BOLD)}"
|
|
21710
21724
|
puts " 3. Use 500 free daily web lookups: Copy web table -> #{Color.c('gsc import clip', Color::GREEN, Color::BOLD)}\n"
|
|
21711
21725
|
end
|
|
@@ -21752,7 +21766,7 @@ module GSC
|
|
|
21752
21766
|
{ action: 'import_clipboard', cmd: 'gsc import clip', desc: 'Use 500 free daily web lookups on keywordseverywhere.com and import via clipboard' },
|
|
21753
21767
|
{ action: 'google_trends', cmd: "gsc trends \"#{target}\"", desc: '100% Free real-time search interest & rising breakout queries' },
|
|
21754
21768
|
{ action: 'gsc_analytics', cmd: 'gsc top-queries', desc: 'Real impressions & clicks from your active Google Search Console domain' },
|
|
21755
|
-
{ action: 'topup', url: 'https://keywordseverywhere.com/credit-packages.html', desc: 'Purchase API credits to enable automated background CLI lookups' }
|
|
21769
|
+
{ action: 'topup', url: 'https://keywordseverywhere.com/credit-packages.html?fpr=us25sg', desc: 'Purchase API credits to enable automated background CLI lookups' }
|
|
21756
21770
|
]
|
|
21757
21771
|
})
|
|
21758
21772
|
return
|
|
@@ -21775,13 +21789,13 @@ module GSC
|
|
|
21775
21789
|
puts " • High-impression queries and Page 2 striking-distance keywords:"
|
|
21776
21790
|
puts " #{Color.c('gsc top-queries', Color::MAGENTA, Color::BOLD)} or #{Color.c('gsc opportunities', Color::MAGENTA, Color::BOLD)}\n"
|
|
21777
21791
|
puts " 4. #{Color.c('Top Up API Credits for Automated Background CLI Lookups', Color::GRAY, Color::BOLD)}"
|
|
21778
|
-
puts " • Purchase credits at: #{Color.c('https://keywordseverywhere.com/credit-packages.html', Color::UNDERLINE)}"
|
|
21792
|
+
puts " • Purchase credits at: #{Color.c('https://keywordseverywhere.com/credit-packages.html?fpr=us25sg', Color::UNDERLINE)}"
|
|
21779
21793
|
puts " • Your key is already saved; it will work immediately once credited.\n"
|
|
21780
21794
|
elsif err_str =~ /401|Unauthorized/i
|
|
21781
21795
|
puts Color.c("\n🔑 Invalid or Expired Keywords Everywhere API Key", Color::RED, Color::BOLD)
|
|
21782
21796
|
puts "Your API key was rejected by api.keywordseverywhere.com (HTTP 401).\n\n"
|
|
21783
21797
|
puts "#{Color::BOLD}RECOMMENDATIONS:#{Color::RESET}"
|
|
21784
|
-
puts " • Check or generate your API key at: #{Color.c('https://keywordseverywhere.com/first-install-addon.html', Color::UNDERLINE)}"
|
|
21798
|
+
puts " • Check or generate your API key at: #{Color.c('https://keywordseverywhere.com/first-install-addon.html?fpr=us25sg', Color::UNDERLINE)}"
|
|
21785
21799
|
puts " • Reconnect key: #{Color.c('gsc connect ke <KEY>', Color::CYAN, Color::BOLD)}"
|
|
21786
21800
|
puts " • Or use free Google Trends without any key: #{Color.c("gsc trends \"#{target}\"", Color::BLUE)}"
|
|
21787
21801
|
elsif err_str =~ /429|Rate Limit/i
|
|
@@ -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
|
|
@@ -21612,7 +21626,7 @@ module GSC
|
|
|
21612
21626
|
key = input unless input.nil? || input.empty?
|
|
21613
21627
|
key = existing if key.empty?
|
|
21614
21628
|
else
|
|
21615
|
-
puts "Get your Keywords Everywhere API key at: #{Color.c('https://keywordseverywhere.com
|
|
21629
|
+
puts "Get your Keywords Everywhere API key at: #{Color.c('https://keywordseverywhere.com/?fpr=us25sg', Color::UNDERLINE)}"
|
|
21616
21630
|
print "Paste your Keywords Everywhere API Key: "
|
|
21617
21631
|
key = $stdin.gets&.strip
|
|
21618
21632
|
end
|
|
@@ -21643,7 +21657,7 @@ module GSC
|
|
|
21643
21657
|
puts " 2. #{Color.c('100% Free Live Google Trends (Breakout & Related terms):', Color::BLUE, Color::BOLD)}"
|
|
21644
21658
|
puts " • Run: #{Color.c('gsc trends "seo audit"', Color::BLUE, Color::BOLD)}"
|
|
21645
21659
|
puts " 3. #{Color.c('Top Up Credits for Automated Background CLI Lookups:', Color::GRAY, Color::BOLD)}"
|
|
21646
|
-
puts " • Purchase credits at: https://keywordseverywhere.com/credit-packages.html"
|
|
21660
|
+
puts " • Purchase credits at: https://keywordseverywhere.com/credit-packages.html?fpr=us25sg"
|
|
21647
21661
|
puts " • Your key is already saved; it will work immediately upon top-up.\n"
|
|
21648
21662
|
else
|
|
21649
21663
|
puts "\nTry searching keywords:"
|
|
@@ -21668,7 +21682,7 @@ module GSC
|
|
|
21668
21682
|
puts "\n" + Color.c("ℹ️ You have 0 paid REST API credits remaining.", Color::YELLOW)
|
|
21669
21683
|
puts "• Free Web Alternative: Check volume on keywordseverywhere.com, click 'Copy', then run: #{Color.c('gsc import clip', Color::CYAN)}"
|
|
21670
21684
|
puts "• Free Live Trends: #{Color.c('gsc trends "seo audit"', Color::BLUE)}"
|
|
21671
|
-
puts "• Top up credits at: https://keywordseverywhere.com/credit-packages.html"
|
|
21685
|
+
puts "• Top up credits at: https://keywordseverywhere.com/credit-packages.html?fpr=us25sg"
|
|
21672
21686
|
end
|
|
21673
21687
|
else
|
|
21674
21688
|
if acct[:error].to_s =~ /401|Unauthorized/i
|
|
@@ -21705,7 +21719,7 @@ module GSC
|
|
|
21705
21719
|
else
|
|
21706
21720
|
puts Color.c("\n❌ Keywords Everywhere API key not configured!\n", Color::RED, Color::BOLD)
|
|
21707
21721
|
puts "#{Color::BOLD}RECOMMENDATIONS:#{Color::RESET}"
|
|
21708
|
-
puts " 1. Connect a key: #{Color.c('gsc connect ke', Color::CYAN, Color::BOLD)} (Get key at https://keywordseverywhere.com
|
|
21722
|
+
puts " 1. Connect a key: #{Color.c('gsc connect ke', Color::CYAN, Color::BOLD)} (Get key at https://keywordseverywhere.com/?fpr=us25sg)"
|
|
21709
21723
|
puts " 2. Use 100% free Google Trends (No key needed!): #{Color.c("gsc trends \"#{target}\"", Color::BLUE, Color::BOLD)}"
|
|
21710
21724
|
puts " 3. Use 500 free daily web lookups: Copy web table -> #{Color.c('gsc import clip', Color::GREEN, Color::BOLD)}\n"
|
|
21711
21725
|
end
|
|
@@ -21752,7 +21766,7 @@ module GSC
|
|
|
21752
21766
|
{ action: 'import_clipboard', cmd: 'gsc import clip', desc: 'Use 500 free daily web lookups on keywordseverywhere.com and import via clipboard' },
|
|
21753
21767
|
{ action: 'google_trends', cmd: "gsc trends \"#{target}\"", desc: '100% Free real-time search interest & rising breakout queries' },
|
|
21754
21768
|
{ action: 'gsc_analytics', cmd: 'gsc top-queries', desc: 'Real impressions & clicks from your active Google Search Console domain' },
|
|
21755
|
-
{ action: 'topup', url: 'https://keywordseverywhere.com/credit-packages.html', desc: 'Purchase API credits to enable automated background CLI lookups' }
|
|
21769
|
+
{ action: 'topup', url: 'https://keywordseverywhere.com/credit-packages.html?fpr=us25sg', desc: 'Purchase API credits to enable automated background CLI lookups' }
|
|
21756
21770
|
]
|
|
21757
21771
|
})
|
|
21758
21772
|
return
|
|
@@ -21775,13 +21789,13 @@ module GSC
|
|
|
21775
21789
|
puts " • High-impression queries and Page 2 striking-distance keywords:"
|
|
21776
21790
|
puts " #{Color.c('gsc top-queries', Color::MAGENTA, Color::BOLD)} or #{Color.c('gsc opportunities', Color::MAGENTA, Color::BOLD)}\n"
|
|
21777
21791
|
puts " 4. #{Color.c('Top Up API Credits for Automated Background CLI Lookups', Color::GRAY, Color::BOLD)}"
|
|
21778
|
-
puts " • Purchase credits at: #{Color.c('https://keywordseverywhere.com/credit-packages.html', Color::UNDERLINE)}"
|
|
21792
|
+
puts " • Purchase credits at: #{Color.c('https://keywordseverywhere.com/credit-packages.html?fpr=us25sg', Color::UNDERLINE)}"
|
|
21779
21793
|
puts " • Your key is already saved; it will work immediately once credited.\n"
|
|
21780
21794
|
elsif err_str =~ /401|Unauthorized/i
|
|
21781
21795
|
puts Color.c("\n🔑 Invalid or Expired Keywords Everywhere API Key", Color::RED, Color::BOLD)
|
|
21782
21796
|
puts "Your API key was rejected by api.keywordseverywhere.com (HTTP 401).\n\n"
|
|
21783
21797
|
puts "#{Color::BOLD}RECOMMENDATIONS:#{Color::RESET}"
|
|
21784
|
-
puts " • Check or generate your API key at: #{Color.c('https://keywordseverywhere.com/first-install-addon.html', Color::UNDERLINE)}"
|
|
21798
|
+
puts " • Check or generate your API key at: #{Color.c('https://keywordseverywhere.com/first-install-addon.html?fpr=us25sg', Color::UNDERLINE)}"
|
|
21785
21799
|
puts " • Reconnect key: #{Color.c('gsc connect ke <KEY>', Color::CYAN, Color::BOLD)}"
|
|
21786
21800
|
puts " • Or use free Google Trends without any key: #{Color.c("gsc trends \"#{target}\"", Color::BLUE)}"
|
|
21787
21801
|
elsif err_str =~ /429|Rate Limit/i
|
|
@@ -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}"
|