Class: SerpCheap::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/serpcheap/client.rb

Constant Summary collapse

DEFAULT_BASE_URL =
"https://api.serp.cheap"
DEFAULT_TIMEOUT_MS =
15_000
DEFAULT_MAX_RETRIES =
2

Instance Method Summary collapse

Constructor Details

#initialize(api_key, base_url: nil, timeout_ms: DEFAULT_TIMEOUT_MS, max_retries: DEFAULT_MAX_RETRIES) ⇒ Client

Returns a new instance of Client.

Raises:



13
14
15
16
17
18
19
20
# File 'lib/serpcheap/client.rb', line 13

def initialize(api_key, base_url: nil, timeout_ms: DEFAULT_TIMEOUT_MS, max_retries: DEFAULT_MAX_RETRIES)
  raise Error.new("missing_api_key", "An API key is required. Get one at https://app.serp.cheap.") if api_key.nil? || api_key.empty?

  @api_key = api_key
  @base_url = (base_url || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
  @timeout_ms = timeout_ms
  @max_retries = max_retries
end

Instance Method Details

#rank(url, q, gl: "us", hl: nil, tbs: nil, pages: 1, match_type: "domain") ⇒ Object

Find where a url/domain ranks for a keyword. Retries transient errors with backoff.



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/serpcheap/client.rb', line 65

def rank(url, q, gl: "us", hl: nil, tbs: nil, pages: 1, match_type: "domain")
  payload = { "url" => url, "q" => q, "gl" => gl, "pages" => pages, "match_type" => match_type }
  payload["hl"] = hl unless hl.nil? || hl.empty?
  payload["tbs"] = tbs unless tbs.nil? || tbs.empty?

  body = request("/v1/rank", payload)
  unless body["organic"].is_a?(Array) && body["matches"].is_a?(Array)
    raise Error.new("invalid_response", "The API response did not match the expected shape.")
  end

  RankResponse.from_hash(body)
end

#scrape(url, render_js: nil, screenshot: nil, wait_for: nil, wait_ms: nil, screenshot_width: nil, screenshot_height: nil) ⇒ Object

Fetch and extract a single page. Retries transient errors with backoff.

Raises:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/serpcheap/client.rb', line 47

def scrape(url, render_js: nil, screenshot: nil, wait_for: nil, wait_ms: nil, screenshot_width: nil, screenshot_height: nil)
  payload = compact(
    "url" => url,
    "render_js" => render_js,
    "screenshot" => screenshot,
    "wait_for" => wait_for,
    "wait_ms" => wait_ms,
    "screenshot_width" => screenshot_width,
    "screenshot_height" => screenshot_height
  )

  body = request("/v1/scrape", payload)
  raise Error.new("invalid_response", "The API response did not match the expected shape.") unless body["url"].is_a?(String)

  ScrapeResponse.from_hash(body)
end

#search(q, gl: "us", hl: nil, tbs: nil, page: 1, scrape: nil) ⇒ Object

Run a Google search. Retries transient errors (429/503/timeout) with backoff.

Raises:



23
24
25
26
27
28
29
30
31
32
33
# File 'lib/serpcheap/client.rb', line 23

def search(q, gl: "us", hl: nil, tbs: nil, page: 1, scrape: nil)
  payload = { "q" => q, "gl" => gl, "page" => page }
  payload["hl"] = hl unless hl.nil? || hl.empty?
  payload["tbs"] = tbs unless tbs.nil? || tbs.empty?
  payload["scrape"] = compact(scrape) if scrape.is_a?(Hash) && !scrape.empty?

  body = request("/v1/search", payload)
  raise Error.new("invalid_response", "The API response did not match the expected shape.") unless body["organic"].is_a?(Array)

  SearchResponse.from_hash(body)
end

#search_pages(q, from: 1, to: 10, **opts) ⇒ Object

Eagerly fetch pages [from..to] (inclusive). Stops on the first empty page.



36
37
38
39
40
41
42
43
44
# File 'lib/serpcheap/client.rb', line 36

def search_pages(q, from: 1, to: 10, **opts)
  pages = []
  (from..to).each do |page|
    res = search(q, page: page, **opts)
    pages << res
    break if res.organic.empty?
  end
  pages
end