Class: Seo::GscApiClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/seo/gsc_api_client.rb

Overview

Client for Google Search Console API using Service Account authentication.
Fetches search analytics data (clicks, impressions, CTR, position) for pages.

Examples:

Basic usage

client = Seo::GscApiClient.new
data = client.search_analytics(
  start_date: 28.days.ago.to_date,
  end_date: Date.yesterday
)

Get data by page and query

data = client.search_analytics(
  start_date: 28.days.ago.to_date,
  end_date: Date.yesterday,
  dimensions: %w[page query]
)

Constant Summary collapse

SITE_URL =

URL-prefix property for Search Analytics queries (scoped to en-US content)

'https://www.warmlyyours.com/en-US/'
DOMAIN_PROPERTY =

Domain property covers all locales — required for URL Inspection API
which needs the inspected URL to belong to the specified property.

'sc-domain:warmlyyours.com'
URL_INSPECTION_ENDPOINT =
'https://searchconsole.googleapis.com/v1/urlInspection/index:inspect'

Instance Method Summary collapse

Constructor Details

#initializeGscApiClient

Returns a new instance of GscApiClient.



31
32
33
34
35
36
# File 'app/services/seo/gsc_api_client.rb', line 31

def initialize
  require 'google/apis/searchconsole_v1'
  require 'googleauth'
  @service = Google::Apis::SearchconsoleV1::SearchConsoleService.new
  @service.authorization = 
end

Instance Method Details

#all_pages(start_date:, end_date:, row_limit: 5000) ⇒ Array<Hash>

Get all pages with their metrics

Parameters:

  • start_date (Date)

    Start of date range

  • end_date (Date)

    End of date range

  • row_limit (Integer) (defaults to: 5000)

    Max pages to return

Returns:

  • (Array<Hash>)

    Array of { page:, clicks:, impressions:, ctr:, position: }



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/services/seo/gsc_api_client.rb', line 131

def all_pages(start_date:, end_date:, row_limit: 5000)
  response = search_analytics(
    start_date: start_date,
    end_date: end_date,
    dimensions: ['page'],
    row_limit: row_limit
  )

  return [] unless response.rows

  response.rows.map do |row|
    {
      page: row.keys.first,
      clicks: row.clicks.to_i,
      impressions: row.impressions.to_i,
      ctr: (row.ctr * 100).round(2),
      position: row.position.round(2)
    }
  end
end

#inspect_url(inspection_url, site_url: DOMAIN_PROPERTY) ⇒ Hash?

Inspect a URL's indexing status via the URL Inspection API.
The Ruby gem doesn't include generated methods for this endpoint,
so we call the REST API directly using the same service account auth.

Rate limits: 600/min, 2000/day per property.

Parameters:

  • inspection_url (String)

    Full URL to inspect

  • site_url (String) (defaults to: DOMAIN_PROPERTY)

    GSC property URL (defaults to SITE_URL)

Returns:

  • (Hash, nil)

    Inspection result with :last_crawl_time, :coverage_state,
    :indexing_state, :page_fetch_state, :crawled_as, :google_canonical, :robots_txt_state



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'app/services/seo/gsc_api_client.rb', line 162

def inspect_url(inspection_url, site_url: DOMAIN_PROPERTY)
  require 'net/http'

  auth = @service.authorization
  auth.fetch_access_token! if auth.access_token.nil? || auth.expired?

  uri = URI(URL_INSPECTION_ENDPOINT)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.open_timeout = 10
  http.read_timeout = 15

  request = Net::HTTP::Post.new(uri)
  request['Authorization'] = "Bearer #{auth.access_token}"
  request['Content-Type'] = 'application/json'
  request.body = { inspectionUrl: inspection_url, siteUrl: site_url }.to_json

  response = http.request(request)

  unless response.is_a?(Net::HTTPSuccess)
    Rails.logger.warn "[GscApiClient] URL Inspection failed (#{response.code}): #{response.body.truncate(200)}"
    return nil
  end

  body = JSON.parse(response.body)
  index_result = body.dig('inspectionResult', 'indexStatusResult')
  return nil unless index_result

  {
    last_crawl_time: index_result['lastCrawlTime'],
    coverage_state: index_result['coverageState'],
    indexing_state: index_result['indexingState'],
    page_fetch_state: index_result['pageFetchState'],
    crawled_as: index_result['crawledAs'],
    google_canonical: index_result['googleCanonical'],
    user_canonical: index_result['userCanonical'],
    robots_txt_state: index_result['robotsTxtState'],
    verdict: index_result['verdict'],
    referring_urls: index_result['referringUrls']
  }
rescue StandardError => e
  Rails.logger.error "[GscApiClient] URL Inspection error: #{e.message}"
  nil
end

#page_metrics(page_url:, start_date:, end_date:) ⇒ Hash

Get aggregated metrics for a specific page

Parameters:

  • page_url (String)

    Full URL of the page

  • start_date (Date)

    Start of date range

  • end_date (Date)

    End of date range

Returns:

  • (Hash)

    Aggregated metrics { clicks:, impressions:, ctr:, position: }



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'app/services/seo/gsc_api_client.rb', line 77

def page_metrics(page_url:, start_date:, end_date:)
  response = search_analytics(
    start_date: start_date,
    end_date: end_date,
    dimensions: ['page'],
    filters: [{ dimension: 'page', operator: 'equals', expression: page_url }]
  )

  row = response.rows&.first
  return nil unless row

  {
    clicks: row.clicks.to_i,
    impressions: row.impressions.to_i,
    ctr: (row.ctr * 100).round(2),
    position: row.position.round(2)
  }
end

#page_queries(page_url:, start_date:, end_date:, limit: 50) ⇒ Array<Hash>

Get top queries for a specific page

Parameters:

  • page_url (String)

    Full URL of the page

  • start_date (Date)

    Start of date range

  • end_date (Date)

    End of date range

  • limit (Integer) (defaults to: 50)

    Max queries to return

Returns:

  • (Array<Hash>)

    Array of { query:, clicks:, impressions:, ctr:, position: }



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'app/services/seo/gsc_api_client.rb', line 103

def page_queries(page_url:, start_date:, end_date:, limit: 50)
  response = search_analytics(
    start_date: start_date,
    end_date: end_date,
    dimensions: %w[page query],
    row_limit: limit,
    filters: [{ dimension: 'page', operator: 'equals', expression: page_url }]
  )

  return [] unless response.rows

  response.rows.map do |row|
    {
      query: row.keys[1], # Second dimension is query
      clicks: row.clicks.to_i,
      impressions: row.impressions.to_i,
      ctr: (row.ctr * 100).round(2),
      position: row.position.round(2)
    }
  end
end

#search_analytics(start_date:, end_date:, dimensions: ['page'], row_limit: 1000, filters: nil) ⇒ Google::Apis::SearchconsoleV1::SearchAnalyticsQueryResponse

Query search analytics data from Google Search Console

Parameters:

  • start_date (Date)

    Start of date range

  • end_date (Date)

    End of date range

  • dimensions (Array<String>) (defaults to: ['page'])

    Dimensions to group by: 'page', 'query', 'country', 'device', 'date'

  • row_limit (Integer) (defaults to: 1000)

    Max rows to return (default 1000, max 25000)

  • filters (Array<Hash>) (defaults to: nil)

    Optional filters, e.g., [{ dimension: 'page', operator: 'contains', expression: '/posts/' }]

Returns:

  • (Google::Apis::SearchconsoleV1::SearchAnalyticsQueryResponse)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/services/seo/gsc_api_client.rb', line 46

def search_analytics(start_date:, end_date:, dimensions: ['page'], row_limit: 1000, filters: nil)
  request = Google::Apis::SearchconsoleV1::SearchAnalyticsQueryRequest.new(
    start_date: start_date.to_s,
    end_date: end_date.to_s,
    dimensions: dimensions,
    row_limit: [row_limit, 25_000].min
  )

  if filters.present?
    request.dimension_filter_groups = [
      Google::Apis::SearchconsoleV1::ApiDimensionFilterGroup.new(
        filters: filters.map do |f|
          Google::Apis::SearchconsoleV1::ApiDimensionFilter.new(
            dimension: f[:dimension],
            operator: f[:operator] || 'contains',
            expression: f[:expression]
          )
        end
      )
    ]
  end

  @service.query_searchanalytic(SITE_URL, request)
end

#test_connectionBoolean

Test the connection

Returns:

  • (Boolean)

    true if connection is successful



209
210
211
212
213
214
215
216
217
218
219
# File 'app/services/seo/gsc_api_client.rb', line 209

def test_connection
  # Try to list sites to verify authentication works
  @service.list_sites
  true
rescue Google::Apis::AuthorizationError => e
  Rails.logger.error "[GscApiClient] Authorization failed: #{e.message}"
  false
rescue StandardError => e
  Rails.logger.error "[GscApiClient] Connection test failed: #{e.message}"
  false
end