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 =

Url inspection endpoint.

'https://searchconsole.googleapis.com/v1/urlInspection/index:inspect'

Instance Method Summary collapse

Constructor Details

#initializeGscApiClient

Returns a new instance of GscApiClient.



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

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: }



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

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



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
# File 'app/services/seo/gsc_api_client.rb', line 163

def inspect_url(inspection_url, site_url: DOMAIN_PROPERTY)
  auth = @service.authorization
  auth.fetch_access_token! if auth.access_token.nil? || auth.expired?

  conn = Faraday.new(
    url: URL_INSPECTION_ENDPOINT,
    headers: {
      'Authorization' => "Bearer #{auth.access_token}",
      'Content-Type'  => 'application/json'
    },
    request: { open_timeout: 10, timeout: 15 }
  ) { |f| f.adapter Faraday.default_adapter }

  response = conn.post('') do |req|
    req.body = { inspectionUrl: inspection_url, siteUrl: site_url }.to_json
  end

  unless response.success?
    Rails.logger.warn "[GscApiClient] URL Inspection failed (#{response.status}): #{response.body.to_s.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: }



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

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: }



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

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)


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

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



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

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