scrapeunblocker 0.1.2 → 0.1.7
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/CHANGELOG.md +17 -0
- data/README.md +44 -11
- data/lib/scrapeunblocker/errors.rb +122 -10
- data/lib/scrapeunblocker/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab6bd524121d205b90ef9cabe0315d1185772ee313a6e40d1bf9057dad3b535b
|
|
4
|
+
data.tar.gz: 82bc7674e38f9bfa125320956e440a66ad535d3aac8ac923e086d0b0ec3fcba2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d8ecc218ec1814f6cdca5681e156488b43302a935a7e8c8fe3d2d73cd98f45558c979c555c33c03db0a43e5a8cf440d2edc8d3b259854844ee7cf9083399fa3
|
|
7
|
+
data.tar.gz: ad8a15c080c559e9d6d5927351e8336788266a65f07971c0c49c41e69ffbee98b8a3d2a661c62d325365d9abdd716b08241a3afbe1a8a2f47234da48d162894e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7 (2026-07-27)
|
|
4
|
+
|
|
5
|
+
- Registry and README links to scrapeunblocker.com now carry UTM parameters so traffic from package registries is attributable. No functional changes.
|
|
6
|
+
|
|
7
|
+
## 0.1.6 (2026-07-23)
|
|
8
|
+
|
|
9
|
+
Version jumps from 0.1.2 to 0.1.6 so all four official SDKs (Python, Node.js, Ruby, PHP) share one version number from here on. Nothing was skipped - 0.1.3 to 0.1.5 were never released for Ruby.
|
|
10
|
+
|
|
11
|
+
- Added `PaymentRequiredError` for HTTP 402, which previously surfaced as a bare `APIError` with no explanation. The three billing blocks now each get their own subclass, picked from the response body: `QuotaExceededError` (`Quota exceeded`), `CreditLimitExceededError` (`Credit limit exceeded`) and `PaymentFailedError` (`Payment failed - update payment method`). Rescue `PaymentRequiredError` to handle all three.
|
|
12
|
+
- Added `NoSubscriptionError`, a subclass of `AuthenticationError`, for the 401 that means "the key is fine, the account has no active plan" (`No valid subscription`) as opposed to an unrecognised key.
|
|
13
|
+
- Added typed errors for the remaining documented status codes: `NotFoundError` (404), `BrowserTimeoutError` (408), `UnsupportedContentError` (415) and `ValidationError` (422). All previously raised a bare `APIError`.
|
|
14
|
+
- Error messages now describe every documented status code accurately - notably 400, which also covers a missing `x-scrapeunblocker-key` header, not just a bad URL.
|
|
15
|
+
- Documented the full error hierarchy in the README, including which errors are retried, which are billed, and how each 402 clears.
|
|
16
|
+
- Fixed the README claim that Oopbuy brand keywords return HTTP 422. They return a successful `200` with `keywordRejected: true` and an empty `results` array.
|
|
17
|
+
|
|
18
|
+
No breaking changes: every new class inherits from `APIError`, so existing `rescue ScrapeUnblocker::APIError` / `rescue ScrapeUnblocker::Error` handlers keep working unchanged.
|
|
19
|
+
|
|
3
20
|
## 0.1.2 (2026-07-22)
|
|
4
21
|
|
|
5
22
|
- Added `oopbuy_search(keyword, ...)` for the new Oopbuy goods search plugin (`POST /goods/oopbuy-search`) - searches 1688, Taobao or the official channel and returns matched products (spu, title, price, monthSold, image, url) as a Hash.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ScrapeUnblocker Ruby client
|
|
2
2
|
|
|
3
|
-
Official Ruby client for the [ScrapeUnblocker](https://scrapeunblocker.com) web scraping API.
|
|
3
|
+
Official Ruby client for the [ScrapeUnblocker](https://scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk) web scraping API.
|
|
4
4
|
|
|
5
5
|
Every request is fully JavaScript-rendered in a real browser and routed through premium proxies, so it bypasses Cloudflare, DataDome, PerimeterX, Akamai, Kasada and similar anti-bot systems - from one simple call. You are only billed for successful requests.
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@ puts product.page_type # "product"
|
|
|
38
38
|
p product.data
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Get your API key at [app.scrapeunblocker.com](https://app.scrapeunblocker.com). The free trial does not require a credit card.
|
|
41
|
+
Get your API key at [app.scrapeunblocker.com](https://app.scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk). The free trial does not require a credit card.
|
|
42
42
|
|
|
43
43
|
## Authentication
|
|
44
44
|
|
|
@@ -94,7 +94,7 @@ goods = su.oopbuy_search("running shoes", channel: "1688", page: 1, page_size: 2
|
|
|
94
94
|
goods["results"].each { |item| puts "#{item['title']} #{item['price']} #{item['url']}" }
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
`channel` is one of `"1688"` (default), `"taobao"` or `"official"`. `sort` is one of `"default"`, `"price_asc"`, `"price_desc"` or `"best_selling"`. `page_size` max is 60.
|
|
97
|
+
`channel` is one of `"1688"` (default), `"taobao"` or `"official"`. `sort` is one of `"default"`, `"price_asc"`, `"price_desc"` or `"best_selling"`. `page_size` max is 60. Oopbuy trademark-blocks brand keywords at its own backend: those come back as a successful `200` with `keywordRejected: true` and an empty `results` array, not an error.
|
|
98
98
|
|
|
99
99
|
## Cookies and the serving proxy
|
|
100
100
|
|
|
@@ -130,13 +130,15 @@ cars = su.skyscanner.carhire(pickup: "Madrid", pickup_datetime: "2026-09-01T10:0
|
|
|
130
130
|
|
|
131
131
|
## Error handling
|
|
132
132
|
|
|
133
|
-
Non-2xx responses raise typed errors, all subclasses of `ScrapeUnblocker::Error`.
|
|
133
|
+
Non-2xx responses raise typed errors, all subclasses of `ScrapeUnblocker::Error`.
|
|
134
134
|
|
|
135
135
|
```ruby
|
|
136
136
|
begin
|
|
137
137
|
html = su.get_page_source("https://example.com")
|
|
138
138
|
rescue ScrapeUnblocker::BlockedError
|
|
139
139
|
# 403: the target blocked every bypass path (not billed)
|
|
140
|
+
rescue ScrapeUnblocker::PaymentRequiredError
|
|
141
|
+
# 402: quota, credit limit, or a failed payment - fix billing
|
|
140
142
|
rescue ScrapeUnblocker::RateLimitError
|
|
141
143
|
# 429: slow down
|
|
142
144
|
rescue ScrapeUnblocker::UpstreamOutageError
|
|
@@ -146,15 +148,46 @@ end
|
|
|
146
148
|
|
|
147
149
|
| Error | Status | Meaning |
|
|
148
150
|
|---|---|---|
|
|
149
|
-
| `InvalidRequestError` | 400 | Bad URL
|
|
150
|
-
| `AuthenticationError` | 401 |
|
|
151
|
+
| `InvalidRequestError` | 400 | Bad URL, unsupported scheme, or the API key header was not sent |
|
|
152
|
+
| `AuthenticationError` | 401 | Key not recognised - typo, stray whitespace, or a rotated key |
|
|
153
|
+
| `NoSubscriptionError` | 401 | Key is fine, but the account has no active plan |
|
|
154
|
+
| `PaymentRequiredError` | 402 | Billing block - base class for the three below |
|
|
155
|
+
| `QuotaExceededError` | 402 | The plan's requests for this period are used up |
|
|
156
|
+
| `CreditLimitExceededError` | 402 | Unpaid balance is past the account's credit limit |
|
|
157
|
+
| `PaymentFailedError` | 402 | A card payment was declined three times |
|
|
151
158
|
| `BlockedError` | 403 | Blocked by bot protection on every path |
|
|
159
|
+
| `NotFoundError` | 404 | Page loaded but held no image (`get_image` only) |
|
|
160
|
+
| `BrowserTimeoutError` | 408 | Our browser run timed out before the page was ready |
|
|
161
|
+
| `UnsupportedContentError` | 415 | The URL serves something other than HTML |
|
|
162
|
+
| `ValidationError` | 422 | Missing or wrong-typed parameter; `body` holds the `detail` array |
|
|
152
163
|
| `RateLimitError` | 429 | Too many requests |
|
|
153
164
|
| `UpstreamOutageError` | 503 | The target origin is down |
|
|
154
|
-
| `ServerError` | 5xx | Unexpected server error |
|
|
155
|
-
| `TimeoutError` | - |
|
|
165
|
+
| `ServerError` | 5xx | Unexpected server error, including a 504 upstream timeout |
|
|
166
|
+
| `TimeoutError` | - | This client gave up locally before the API answered |
|
|
156
167
|
| `ConnectionError` | - | Could not reach the API |
|
|
157
168
|
|
|
169
|
+
Transient failures (429, 502, 503, 504 and network errors) are retried automatically with exponential backoff. A 401 or 402 is never retried - it clears when the key or the billing state changes, not on another attempt. Neither is billed or counted against your quota, because the request is refused before anything is scraped.
|
|
170
|
+
|
|
171
|
+
### Billing errors (402)
|
|
172
|
+
|
|
173
|
+
The three billing blocks share a status code and differ only in their message, so the client raises a dedicated error for each:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
begin
|
|
177
|
+
html = su.get_page_source("https://example.com")
|
|
178
|
+
rescue ScrapeUnblocker::QuotaExceededError
|
|
179
|
+
# plan quota (plus any overage allowance) is used up for this period
|
|
180
|
+
rescue ScrapeUnblocker::CreditLimitExceededError
|
|
181
|
+
# unpaid balance passed the account credit limit
|
|
182
|
+
rescue ScrapeUnblocker::PaymentFailedError
|
|
183
|
+
# card declined three times - update the payment method
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
When more than one applies, the most serious wins: failed payment outranks credit limit, which outranks quota. All three lift by themselves once the billing state changes - access returns within about a minute, and the API key stays the same. One catch worth knowing: subscribing to a new plan does **not** clear `PaymentFailedError`, because the old unpaid invoice stays open until it is paid.
|
|
188
|
+
|
|
189
|
+
Full details for every status code: [developers.scrapeunblocker.com/errors](https://developers.scrapeunblocker.com/errors).
|
|
190
|
+
|
|
158
191
|
## Configuration
|
|
159
192
|
|
|
160
193
|
```ruby
|
|
@@ -168,9 +201,9 @@ ScrapeUnblocker::Client.new(
|
|
|
168
201
|
|
|
169
202
|
## Links
|
|
170
203
|
|
|
171
|
-
- Documentation: https://developers.scrapeunblocker.com
|
|
172
|
-
- Website: https://scrapeunblocker.com
|
|
173
|
-
- Dashboard: https://app.scrapeunblocker.com
|
|
204
|
+
- Documentation: [developers.scrapeunblocker.com](https://developers.scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk)
|
|
205
|
+
- Website: [scrapeunblocker.com](https://scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk)
|
|
206
|
+
- Dashboard: [app.scrapeunblocker.com](https://app.scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk)
|
|
174
207
|
|
|
175
208
|
## License
|
|
176
209
|
|
|
@@ -15,12 +15,89 @@ module ScrapeUnblocker
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# The API key
|
|
18
|
+
# The API key was rejected (HTTP 401).
|
|
19
|
+
#
|
|
20
|
+
# Two cases produce a 401: an unrecognised key ("Unauthorized" - a typo,
|
|
21
|
+
# trailing whitespace, an empty value, or a key rotated in the dashboard),
|
|
22
|
+
# and a valid key on an account with no plan, which raises the
|
|
23
|
+
# NoSubscriptionError subclass. Omitting the key header entirely is a 400,
|
|
24
|
+
# not a 401. Nothing is scraped for a 401, so it is not billed.
|
|
19
25
|
class AuthenticationError < APIError; end
|
|
20
26
|
|
|
21
|
-
# The
|
|
27
|
+
# The key is valid but the account has no active plan (HTTP 401).
|
|
28
|
+
#
|
|
29
|
+
# Raised when the API answers a 401 with "No valid subscription". Pick a plan
|
|
30
|
+
# at https://app.scrapeunblocker.com - access resumes within about a minute,
|
|
31
|
+
# and the key does not change.
|
|
32
|
+
class NoSubscriptionError < AuthenticationError; end
|
|
33
|
+
|
|
34
|
+
# The account has a billing problem (HTTP 402).
|
|
35
|
+
#
|
|
36
|
+
# Credentials are fine - the request was stopped for a billing reason. There
|
|
37
|
+
# are three, each raised as a dedicated subclass: QuotaExceededError,
|
|
38
|
+
# CreditLimitExceededError and PaymentFailedError. Rescue this base class to
|
|
39
|
+
# handle all three.
|
|
40
|
+
#
|
|
41
|
+
# When more than one applies, the most serious wins: failed payment outranks
|
|
42
|
+
# credit limit, which outranks quota. All three lift by themselves once the
|
|
43
|
+
# billing state changes - access returns within roughly a minute, with no key
|
|
44
|
+
# change needed. Like a 401, a 402 is refused before anything is scraped, so
|
|
45
|
+
# it is never billed. Retrying is pointless; fix the billing state first.
|
|
46
|
+
class PaymentRequiredError < APIError; end
|
|
47
|
+
|
|
48
|
+
# Every request the plan allows this period has been used (HTTP 402).
|
|
49
|
+
#
|
|
50
|
+
# On plans that permit overages this only fires past the quota *plus* the
|
|
51
|
+
# overage allowance; inside that band requests still succeed and the extra
|
|
52
|
+
# usage is invoiced. Active coupon credit is spent before plan quota. The
|
|
53
|
+
# counter resets on the subscription's anniversary day, not the first of the
|
|
54
|
+
# month.
|
|
55
|
+
class QuotaExceededError < PaymentRequiredError; end
|
|
56
|
+
|
|
57
|
+
# The unpaid balance has passed the account's credit limit (HTTP 402).
|
|
58
|
+
#
|
|
59
|
+
# The balance counted here is the amount remaining on open invoices plus
|
|
60
|
+
# metered usage already consumed but not yet invoiced. Outstanding invoices
|
|
61
|
+
# are charged automatically when this triggers, so with a working card it
|
|
62
|
+
# usually clears itself within about a minute.
|
|
63
|
+
class CreditLimitExceededError < PaymentRequiredError; end
|
|
64
|
+
|
|
65
|
+
# A card payment has been declined three times (HTTP 402).
|
|
66
|
+
#
|
|
67
|
+
# Those attempts are the payment provider's automatic retries spread over
|
|
68
|
+
# several days, so a card has been failing for a while. Subscribing to a new
|
|
69
|
+
# plan does NOT clear this: the old unpaid invoice stays open, and the block
|
|
70
|
+
# stays until that specific invoice is paid.
|
|
71
|
+
class PaymentFailedError < PaymentRequiredError; end
|
|
72
|
+
|
|
73
|
+
# The request was rejected as invalid (HTTP 400).
|
|
74
|
+
#
|
|
75
|
+
# Raised for a malformed URL or unsupported scheme, for a missing
|
|
76
|
+
# x-scrapeunblocker-key header ("Missing x-scrapeunblocker-key"), and for a
|
|
77
|
+
# URL that belongs to a dedicated plugin - the response names the endpoint to
|
|
78
|
+
# use instead.
|
|
22
79
|
class InvalidRequestError < APIError; end
|
|
23
80
|
|
|
81
|
+
# The page loaded but the requested element was absent (HTTP 404).
|
|
82
|
+
# Only #get_image raises this: the page rendered and held no <img> tag.
|
|
83
|
+
class NotFoundError < APIError; end
|
|
84
|
+
|
|
85
|
+
# The browser run did not finish in time on our side (HTTP 408).
|
|
86
|
+
#
|
|
87
|
+
# Distinct from TimeoutError, which is this client giving up locally. Here
|
|
88
|
+
# the API answered - it just could not render the page in time.
|
|
89
|
+
class BrowserTimeoutError < APIError; end
|
|
90
|
+
|
|
91
|
+
# The URL serves something other than HTML (HTTP 415).
|
|
92
|
+
# The message names the content type found. For images, use #get_image.
|
|
93
|
+
class UnsupportedContentError < APIError; end
|
|
94
|
+
|
|
95
|
+
# A request parameter is missing or has the wrong type (HTTP 422).
|
|
96
|
+
#
|
|
97
|
+
# Unlike the other errors the body is JSON, with a "detail" array pinpointing
|
|
98
|
+
# each problem field. Read it from #body.
|
|
99
|
+
class ValidationError < APIError; end
|
|
100
|
+
|
|
24
101
|
# The target site blocked every available bypass path (HTTP 403).
|
|
25
102
|
# Blocked calls are not billed.
|
|
26
103
|
class BlockedError < APIError; end
|
|
@@ -32,6 +109,7 @@ module ScrapeUnblocker
|
|
|
32
109
|
class UpstreamOutageError < APIError; end
|
|
33
110
|
|
|
34
111
|
# ScrapeUnblocker returned an unexpected 5xx error.
|
|
112
|
+
# Also covers the 504 returned when a SERP fetch times out upstream.
|
|
35
113
|
class ServerError < APIError; end
|
|
36
114
|
|
|
37
115
|
# The request did not complete within the configured timeout.
|
|
@@ -40,24 +118,58 @@ module ScrapeUnblocker
|
|
|
40
118
|
# The client could not reach the ScrapeUnblocker API.
|
|
41
119
|
class ConnectionError < Error; end
|
|
42
120
|
|
|
121
|
+
BASE_MESSAGES = {
|
|
122
|
+
400 => "Invalid request (bad URL, unsupported scheme, or missing API key header)",
|
|
123
|
+
401 => "Authentication failed - key not recognised, or account has no active plan",
|
|
124
|
+
402 => "Billing block - quota exceeded, credit limit exceeded, or a failed payment",
|
|
125
|
+
403 => "Target blocked by bot protection on every bypass path",
|
|
126
|
+
404 => "Requested element not found on the page",
|
|
127
|
+
408 => "Browser run timed out before the page was ready",
|
|
128
|
+
415 => "URL does not serve HTML",
|
|
129
|
+
422 => "Validation error - see the detail array in the response body",
|
|
130
|
+
429 => "Rate limited - too many requests",
|
|
131
|
+
503 => "Upstream origin returned a server-side outage page",
|
|
132
|
+
504 => "Fetch timed out upstream"
|
|
133
|
+
}.freeze
|
|
134
|
+
private_constant :BASE_MESSAGES
|
|
135
|
+
|
|
136
|
+
# A 401 is either an unknown key or a recognised key on an account without a
|
|
137
|
+
# plan, and only the body tells them apart. Anything unrecognised stays on
|
|
138
|
+
# the general AuthenticationError rather than guessing.
|
|
139
|
+
def self.auth_error_class(body)
|
|
140
|
+
(body || "").downcase.include?("no valid subscription") ? NoSubscriptionError : AuthenticationError
|
|
141
|
+
end
|
|
142
|
+
private_class_method :auth_error_class
|
|
143
|
+
|
|
144
|
+
# The three billing blocks share a status code and differ only in their
|
|
145
|
+
# plain-text body. An unrecognised body falls back to PaymentRequiredError.
|
|
146
|
+
def self.billing_error_class(body)
|
|
147
|
+
text = (body || "").downcase
|
|
148
|
+
return QuotaExceededError if text.include?("quota exceeded")
|
|
149
|
+
return CreditLimitExceededError if text.include?("credit limit exceeded")
|
|
150
|
+
return PaymentFailedError if text.include?("payment failed")
|
|
151
|
+
|
|
152
|
+
PaymentRequiredError
|
|
153
|
+
end
|
|
154
|
+
private_class_method :billing_error_class
|
|
155
|
+
|
|
43
156
|
# Build a typed error from an HTTP status code and response body.
|
|
44
157
|
def self.error_for_status(status, body)
|
|
45
158
|
snippet = (body || "").strip.gsub(/\s+/, " ")
|
|
46
159
|
snippet = "#{snippet[0, 200]}..." if snippet.length > 200
|
|
47
|
-
base = {
|
|
48
|
-
400 => "Invalid request (bad URL or unsupported scheme)",
|
|
49
|
-
401 => "Authentication failed - check your API key",
|
|
50
|
-
403 => "Target blocked by bot protection on every bypass path",
|
|
51
|
-
429 => "Rate limited - too many requests",
|
|
52
|
-
503 => "Upstream origin returned a server-side outage page"
|
|
53
|
-
}.fetch(status, "API returned HTTP #{status}")
|
|
160
|
+
base = BASE_MESSAGES.fetch(status, "API returned HTTP #{status}")
|
|
54
161
|
message = snippet.empty? ? base : "#{base}: #{snippet}"
|
|
55
162
|
|
|
56
163
|
klass =
|
|
57
164
|
case status
|
|
58
165
|
when 400 then InvalidRequestError
|
|
59
|
-
when 401 then
|
|
166
|
+
when 401 then auth_error_class(body)
|
|
167
|
+
when 402 then billing_error_class(body)
|
|
60
168
|
when 403 then BlockedError
|
|
169
|
+
when 404 then NotFoundError
|
|
170
|
+
when 408 then BrowserTimeoutError
|
|
171
|
+
when 415 then UnsupportedContentError
|
|
172
|
+
when 422 then ValidationError
|
|
61
173
|
when 429 then RateLimitError
|
|
62
174
|
when 503 then UpstreamOutageError
|
|
63
175
|
else status >= 500 ? ServerError : APIError
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scrapeunblocker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ScrapeUnblocker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: JS-rendered pages that bypass Cloudflare, DataDome, PerimeterX and Akamai,
|
|
14
14
|
plus Google SERP and Skyscanner flights/hotels/car-hire scraping as JSON.
|
|
@@ -27,13 +27,13 @@ files:
|
|
|
27
27
|
- lib/scrapeunblocker/parsed_page.rb
|
|
28
28
|
- lib/scrapeunblocker/skyscanner.rb
|
|
29
29
|
- lib/scrapeunblocker/version.rb
|
|
30
|
-
homepage: https://scrapeunblocker.com
|
|
30
|
+
homepage: https://scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk
|
|
31
31
|
licenses:
|
|
32
32
|
- MIT
|
|
33
33
|
metadata:
|
|
34
|
-
homepage_uri: https://scrapeunblocker.com
|
|
34
|
+
homepage_uri: https://scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk
|
|
35
35
|
source_code_uri: https://github.com/ScrapeUnblocker/scrapeunblocker-ruby
|
|
36
|
-
documentation_uri: https://developers.scrapeunblocker.com
|
|
36
|
+
documentation_uri: https://developers.scrapeunblocker.com?utm_source=rubygems&utm_medium=integration&utm_campaign=ruby-sdk
|
|
37
37
|
changelog_uri: https://github.com/ScrapeUnblocker/scrapeunblocker-ruby/blob/main/CHANGELOG.md
|
|
38
38
|
bug_tracker_uri: https://github.com/ScrapeUnblocker/scrapeunblocker-ruby/issues
|
|
39
39
|
rubygems_mfa_required: 'true'
|