screenshotfreeapi 1.0.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +442 -0
- data/lib/screenshotfreeapi/client.rb +67 -0
- data/lib/screenshotfreeapi/errors.rb +89 -0
- data/lib/screenshotfreeapi/http_client.rb +180 -0
- data/lib/screenshotfreeapi/resources/auth.rb +58 -0
- data/lib/screenshotfreeapi/resources/billing.rb +77 -0
- data/lib/screenshotfreeapi/resources/integrations.rb +52 -0
- data/lib/screenshotfreeapi/resources/jobs.rb +59 -0
- data/lib/screenshotfreeapi/resources/monitors.rb +88 -0
- data/lib/screenshotfreeapi/resources/screenshots.rb +166 -0
- data/lib/screenshotfreeapi/resources/workspaces.rb +96 -0
- data/lib/screenshotfreeapi/version.rb +5 -0
- data/lib/screenshotfreeapi/webhooks.rb +78 -0
- data/lib/screenshotfreeapi.rb +47 -0
- metadata +64 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module ScreenshotFreeAPI
|
|
8
|
+
# Low-level HTTP client responsible for:
|
|
9
|
+
# - Building requests with correct headers
|
|
10
|
+
# - Parsing JSON responses
|
|
11
|
+
# - Mapping HTTP error statuses to typed exceptions
|
|
12
|
+
# - Retrying transient failures (429, 5xx) with exponential backoff
|
|
13
|
+
class HttpClient
|
|
14
|
+
DEFAULT_BASE_URL = "https://api.screenshotfreeapi.com"
|
|
15
|
+
DEFAULT_TIMEOUT = 30
|
|
16
|
+
DEFAULT_RETRIES = 3
|
|
17
|
+
DEFAULT_DELAY = 1 # seconds — doubles on each retry: 1s, 2s, 4s
|
|
18
|
+
|
|
19
|
+
# @param api_key [String] Bearer token (API key or JWT)
|
|
20
|
+
# @param base_url [String] Override for testing / staging
|
|
21
|
+
# @param timeout [Integer] Open/read timeout in seconds
|
|
22
|
+
# @param max_retries [Integer] Maximum number of retry attempts (not counting the first try)
|
|
23
|
+
# @param retry_delay [Numeric] Base delay in seconds; doubles each retry
|
|
24
|
+
def initialize(
|
|
25
|
+
api_key:,
|
|
26
|
+
base_url: DEFAULT_BASE_URL,
|
|
27
|
+
timeout: DEFAULT_TIMEOUT,
|
|
28
|
+
max_retries: DEFAULT_RETRIES,
|
|
29
|
+
retry_delay: DEFAULT_DELAY
|
|
30
|
+
)
|
|
31
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.to_s.strip.empty?
|
|
32
|
+
|
|
33
|
+
@api_key = api_key
|
|
34
|
+
@base_url = base_url.to_s.chomp("/")
|
|
35
|
+
@timeout = timeout
|
|
36
|
+
@max_retries = max_retries
|
|
37
|
+
@retry_delay = retry_delay
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Perform an HTTP request with automatic retry on transient errors.
|
|
41
|
+
#
|
|
42
|
+
# @param method [Symbol] :get, :post, :patch, :delete
|
|
43
|
+
# @param path [String] Path starting with "/"
|
|
44
|
+
# @param body [Hash, nil] Request body; serialised to JSON automatically
|
|
45
|
+
# @param query [Hash] Query-string parameters
|
|
46
|
+
# @param auth_header [String, nil] Override the Authorization header value
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash] Parsed JSON response body
|
|
49
|
+
def request(method, path, body: nil, query: {}, auth_header: nil)
|
|
50
|
+
uri = build_uri(path, query)
|
|
51
|
+
|
|
52
|
+
attempts = 0
|
|
53
|
+
delay = @retry_delay
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
attempts += 1
|
|
57
|
+
response = execute(uri, method, body, auth_header)
|
|
58
|
+
handle_response(response)
|
|
59
|
+
rescue RateLimitError => e
|
|
60
|
+
# Respect Retry-After if present; always retry rate-limit errors up to max_retries
|
|
61
|
+
if attempts <= @max_retries
|
|
62
|
+
wait = e.retry_after || delay
|
|
63
|
+
sleep(wait)
|
|
64
|
+
delay *= 2
|
|
65
|
+
retry
|
|
66
|
+
end
|
|
67
|
+
raise
|
|
68
|
+
rescue ScreenshotFreeAPIError => e
|
|
69
|
+
# Retry 5xx errors with exponential backoff
|
|
70
|
+
if e.status_code.to_i >= 500 && attempts <= @max_retries
|
|
71
|
+
sleep(delay)
|
|
72
|
+
delay *= 2
|
|
73
|
+
retry
|
|
74
|
+
end
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Build a URI object from the base URL, path, and optional query params.
|
|
82
|
+
def build_uri(path, query)
|
|
83
|
+
url = "#{@base_url}#{path}"
|
|
84
|
+
uri = URI.parse(url)
|
|
85
|
+
|
|
86
|
+
unless query.nil? || query.empty?
|
|
87
|
+
params = query.map { |k, v| "#{URI.encode_www_form_component(k.to_s)}=#{URI.encode_www_form_component(v.to_s)}" }
|
|
88
|
+
uri.query = params.join("&")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
uri
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Execute a single HTTP call and return the Net::HTTP response object.
|
|
95
|
+
def execute(uri, method, body, auth_header)
|
|
96
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
97
|
+
open_timeout: @timeout,
|
|
98
|
+
read_timeout: @timeout) do |http|
|
|
99
|
+
req = build_request(method, uri, body, auth_header)
|
|
100
|
+
http.request(req)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Instantiate the correct Net::HTTP request class and set headers/body.
|
|
105
|
+
def build_request(method, uri, body, auth_header)
|
|
106
|
+
klass = case method
|
|
107
|
+
when :get then Net::HTTP::Get
|
|
108
|
+
when :post then Net::HTTP::Post
|
|
109
|
+
when :patch then Net::HTTP::Patch
|
|
110
|
+
when :delete then Net::HTTP::Delete
|
|
111
|
+
when :put then Net::HTTP::Put
|
|
112
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
req = klass.new(uri.request_uri)
|
|
116
|
+
# auth_header: nil => use default API key Bearer token
|
|
117
|
+
# auth_header: "" => omit the Authorization header (public endpoints)
|
|
118
|
+
# auth_header: "Bearer <jwt>" => use the supplied value as-is
|
|
119
|
+
if auth_header.nil?
|
|
120
|
+
req["Authorization"] = "Bearer #{@api_key}"
|
|
121
|
+
elsif !auth_header.empty?
|
|
122
|
+
req["Authorization"] = auth_header
|
|
123
|
+
end
|
|
124
|
+
req["Content-Type"] = "application/json"
|
|
125
|
+
req["Accept"] = "application/json"
|
|
126
|
+
req["User-Agent"] = "screenshotfreeapi-ruby/#{ScreenshotFreeAPI::VERSION}"
|
|
127
|
+
|
|
128
|
+
if body
|
|
129
|
+
req.body = JSON.generate(body)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
req
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Parse the HTTP response and raise typed errors for non-2xx statuses.
|
|
136
|
+
def handle_response(response)
|
|
137
|
+
status = response.code.to_i
|
|
138
|
+
body = parse_body(response.body)
|
|
139
|
+
|
|
140
|
+
return body if status >= 200 && status < 300
|
|
141
|
+
|
|
142
|
+
# Extract structured error info from the API's error envelope.
|
|
143
|
+
error_code = body.is_a?(Hash) ? (body["error"] || body["code"] || "UnknownError").to_s : "UnknownError"
|
|
144
|
+
message = body.is_a?(Hash) ? (body["message"] || response.message || "Unknown error") : (response.message || "Unknown error")
|
|
145
|
+
details = body.is_a?(Hash) ? body["details"] : nil
|
|
146
|
+
|
|
147
|
+
case status
|
|
148
|
+
when 400
|
|
149
|
+
raise ValidationError.new(message, details)
|
|
150
|
+
when 401
|
|
151
|
+
raise AuthenticationError.new(message)
|
|
152
|
+
when 402
|
|
153
|
+
raise PaymentRequiredError.new(message)
|
|
154
|
+
when 403
|
|
155
|
+
raise ForbiddenError.new(message)
|
|
156
|
+
when 404
|
|
157
|
+
raise NotFoundError.new(message)
|
|
158
|
+
when 429
|
|
159
|
+
retry_after_header = response["Retry-After"]
|
|
160
|
+
retry_after = retry_after_header ? retry_after_header.to_f : nil
|
|
161
|
+
|
|
162
|
+
if error_code == "QuotaExceeded"
|
|
163
|
+
raise QuotaExceededError.new(message)
|
|
164
|
+
else
|
|
165
|
+
raise RateLimitError.new(retry_after, message)
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
raise ScreenshotFreeAPIError.new(status, error_code, message, details)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Attempt to parse a JSON response body; return raw string on failure.
|
|
173
|
+
def parse_body(raw)
|
|
174
|
+
return nil if raw.nil? || raw.empty?
|
|
175
|
+
JSON.parse(raw)
|
|
176
|
+
rescue JSON::ParserError
|
|
177
|
+
raw
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
module Resources
|
|
5
|
+
# Authentication endpoints — registration, API key management, and JWT tokens.
|
|
6
|
+
#
|
|
7
|
+
# Note: `token` and `refresh` return JWTs that should be passed as the
|
|
8
|
+
# `jwt:` parameter to billing, workspace, and monitor resource methods.
|
|
9
|
+
class Auth
|
|
10
|
+
def initialize(http)
|
|
11
|
+
@http = http
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Register a new account.
|
|
15
|
+
#
|
|
16
|
+
# @param email [String] User email address
|
|
17
|
+
# @param password [String] Password (min 8 chars on the server)
|
|
18
|
+
# @param name [String] Display name
|
|
19
|
+
#
|
|
20
|
+
# @return [Hash] { "userId", "email", "apiKey" }
|
|
21
|
+
# NOTE: apiKey is shown ONCE — store it securely.
|
|
22
|
+
def register(email:, password:, name:)
|
|
23
|
+
@http.request(:post, "/auth/register", body: {
|
|
24
|
+
email: email,
|
|
25
|
+
password: password,
|
|
26
|
+
name: name
|
|
27
|
+
})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Obtain a management JWT by providing account credentials.
|
|
31
|
+
#
|
|
32
|
+
# The returned token is required for billing, workspace, and monitor
|
|
33
|
+
# endpoints. Pass it as `jwt:` to those resource methods.
|
|
34
|
+
#
|
|
35
|
+
# @param email [String]
|
|
36
|
+
# @param password [String]
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash] { "token", "expiresAt" }
|
|
39
|
+
def token(email:, password:)
|
|
40
|
+
@http.request(:post, "/auth/token", body: {
|
|
41
|
+
email: email,
|
|
42
|
+
password: password
|
|
43
|
+
})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Refresh an expired JWT using a refresh token.
|
|
47
|
+
#
|
|
48
|
+
# @param refresh_token [String] The refresh token from a previous `token` call
|
|
49
|
+
#
|
|
50
|
+
# @return [Hash] { "token", "expiresAt" }
|
|
51
|
+
def refresh(refresh_token:)
|
|
52
|
+
@http.request(:post, "/auth/refresh", body: {
|
|
53
|
+
refreshToken: refresh_token
|
|
54
|
+
})
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
module Resources
|
|
5
|
+
# Billing, plan, and subscription management.
|
|
6
|
+
#
|
|
7
|
+
# Most methods require a JWT (obtained via Auth#token) passed in the
|
|
8
|
+
# `jwt:` keyword argument. The client will send it as the Bearer token
|
|
9
|
+
# for that single request instead of the API key.
|
|
10
|
+
#
|
|
11
|
+
# The only exception is `plans`, which is publicly accessible.
|
|
12
|
+
class Billing
|
|
13
|
+
def initialize(http)
|
|
14
|
+
@http = http
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# List all available subscription plans (no auth required).
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<Hash>] Array of plan objects with name, price, limits, etc.
|
|
20
|
+
def plans
|
|
21
|
+
@http.request(:get, "/billing/plans", auth_header: "")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get the current user's active plan and usage summary.
|
|
25
|
+
#
|
|
26
|
+
# @param jwt [String] Management JWT from Auth#token
|
|
27
|
+
#
|
|
28
|
+
# @return [Hash] { "plan", "status", "screenshotsUsed", "screenshotsLimit", ... }
|
|
29
|
+
def plan(jwt:)
|
|
30
|
+
@http.request(:get, "/billing/plan", auth_header: "Bearer #{jwt}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Retrieve the 30-day daily usage history.
|
|
34
|
+
#
|
|
35
|
+
# @param jwt [String] Management JWT
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash] { "usage" => Array of { "date", "count" } }
|
|
38
|
+
def usage(jwt:)
|
|
39
|
+
@http.request(:get, "/billing/usage", auth_header: "Bearer #{jwt}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Initiate a plan upgrade via Flutterwave checkout.
|
|
43
|
+
#
|
|
44
|
+
# @param jwt [String] Management JWT
|
|
45
|
+
# @param plan [String] Target plan tier: "STARTER" | "GROWTH" | "BUSINESS" | "ENTERPRISE"
|
|
46
|
+
# @param options [Hash] Additional upgrade options
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash] { "checkoutUrl", "transactionRef" } — redirect user to checkoutUrl
|
|
49
|
+
def upgrade(jwt:, plan:, **options)
|
|
50
|
+
body = { plan: plan }.merge(options)
|
|
51
|
+
@http.request(:post, "/billing/upgrade", body: body, auth_header: "Bearer #{jwt}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Server-side verification of a completed Flutterwave payment.
|
|
55
|
+
#
|
|
56
|
+
# Call this after the user returns from the Flutterwave checkout redirect.
|
|
57
|
+
#
|
|
58
|
+
# @param jwt [String] Management JWT
|
|
59
|
+
# @param transaction_ref [String] The transactionRef from `upgrade`
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] { "success", "plan", "message" }
|
|
62
|
+
def verify(jwt:, transaction_ref: nil)
|
|
63
|
+
query = transaction_ref ? { transaction_ref: transaction_ref } : {}
|
|
64
|
+
@http.request(:get, "/billing/verify", query: query, auth_header: "Bearer #{jwt}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Cancel the active subscription.
|
|
68
|
+
#
|
|
69
|
+
# @param jwt [String] Management JWT
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] { "message", "effectiveAt" }
|
|
72
|
+
def cancel(jwt:)
|
|
73
|
+
@http.request(:post, "/billing/cancel", body: {}, auth_header: "Bearer #{jwt}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
module Resources
|
|
5
|
+
# Third-party integration endpoints.
|
|
6
|
+
#
|
|
7
|
+
# Currently supports Zapier REST Hooks for no-code workflow automation.
|
|
8
|
+
# All methods use API key authentication (no JWT required).
|
|
9
|
+
class Integrations
|
|
10
|
+
def initialize(http)
|
|
11
|
+
@http = http
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Register a Zapier webhook subscription (REST Hook subscribe).
|
|
15
|
+
#
|
|
16
|
+
# Zapier calls this endpoint when a Zap is turned on. You can also call
|
|
17
|
+
# it directly to register any webhook for screenshot-complete events.
|
|
18
|
+
#
|
|
19
|
+
# @param target_url [String] URL Zapier will POST events to
|
|
20
|
+
# @param event [String] Event type to subscribe to, e.g. "screenshot.completed"
|
|
21
|
+
# @param options [Hash] Additional subscription options
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash] { "id", "targetUrl", "event", "createdAt" }
|
|
24
|
+
def zapier_subscribe(target_url:, event:, **options)
|
|
25
|
+
body = { targetUrl: target_url, event: event }.merge(options)
|
|
26
|
+
@http.request(:post, "/integrations/zapier/subscribe", body: body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Unregister a Zapier webhook subscription (REST Hook unsubscribe).
|
|
30
|
+
#
|
|
31
|
+
# Zapier calls this when the Zap is turned off.
|
|
32
|
+
#
|
|
33
|
+
# @param subscription_id [String] ID returned by `zapier_subscribe`
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] Confirmation object
|
|
36
|
+
def zapier_unsubscribe(subscription_id:)
|
|
37
|
+
@http.request(:delete, "/integrations/zapier/unsubscribe/#{subscription_id}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retrieve a sample payload for a Zapier trigger event.
|
|
41
|
+
#
|
|
42
|
+
# Zapier uses this to show users a preview of the data they can map in their Zap.
|
|
43
|
+
#
|
|
44
|
+
# @param event [String] Event type, e.g. "screenshot.completed"
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Hash>] Array of sample event payloads (Zapier polling format)
|
|
47
|
+
def zapier_triggers(event:)
|
|
48
|
+
@http.request(:get, "/integrations/zapier/triggers/#{event}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
module Resources
|
|
5
|
+
# Job polling endpoints.
|
|
6
|
+
#
|
|
7
|
+
# All screenshot requests are asynchronous — the POST endpoints return a
|
|
8
|
+
# jobId immediately. Use these methods to check progress and retrieve
|
|
9
|
+
# results once the job reaches the "completed" state.
|
|
10
|
+
class Jobs
|
|
11
|
+
def initialize(http)
|
|
12
|
+
@http = http
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Retrieve the current status of a job.
|
|
16
|
+
#
|
|
17
|
+
# @param job_id [String] The jobId returned by a screenshot POST
|
|
18
|
+
#
|
|
19
|
+
# @return [Hash]
|
|
20
|
+
# {
|
|
21
|
+
# "jobId" => String,
|
|
22
|
+
# "status" => "queued" | "processing" | "completed" | "failed",
|
|
23
|
+
# "progress" => Integer (0-100),
|
|
24
|
+
# "error" => String | nil,
|
|
25
|
+
# "estimatedSeconds" => Integer | nil,
|
|
26
|
+
# "createdAt" => String (ISO 8601),
|
|
27
|
+
# "updatedAt" => String (ISO 8601)
|
|
28
|
+
# }
|
|
29
|
+
def status(job_id)
|
|
30
|
+
@http.request(:get, "/jobs/#{job_id}/status")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Retrieve the completed result of a job.
|
|
34
|
+
#
|
|
35
|
+
# Only available once `status` returns "completed". Raises NotFoundError
|
|
36
|
+
# if the job is not yet done or was not found.
|
|
37
|
+
#
|
|
38
|
+
# @param job_id [String]
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash]
|
|
41
|
+
# {
|
|
42
|
+
# "jobId" => String,
|
|
43
|
+
# "type" => "web" | "mobile" | "html",
|
|
44
|
+
# "screenshots" => Array of {
|
|
45
|
+
# "url" => String (presigned S3 URL, 15-min TTL),
|
|
46
|
+
# "format" => "png" | "jpeg" | "webp" | "pdf",
|
|
47
|
+
# "width" => Integer,
|
|
48
|
+
# "height" => Integer,
|
|
49
|
+
# "capturedAt" => String,
|
|
50
|
+
# "selector" => String | nil
|
|
51
|
+
# },
|
|
52
|
+
# "metadata" => Hash (AI info, processingMs, etc.)
|
|
53
|
+
# }
|
|
54
|
+
def result(job_id)
|
|
55
|
+
@http.request(:get, "/jobs/#{job_id}/result")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
module Resources
|
|
5
|
+
# App monitoring — schedule recurring captures and receive diff alerts.
|
|
6
|
+
#
|
|
7
|
+
# Monitors run on a cron schedule and alert you when an app's screenshots
|
|
8
|
+
# change (e.g., new version, UI refresh). Requires a BUSINESS+ plan.
|
|
9
|
+
#
|
|
10
|
+
# All methods require a management JWT passed as `jwt:`.
|
|
11
|
+
class Monitors
|
|
12
|
+
def initialize(http)
|
|
13
|
+
@http = http
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Create a new app monitor.
|
|
17
|
+
#
|
|
18
|
+
# @param jwt [String] Management JWT
|
|
19
|
+
# @param app_name [String] App display name to monitor
|
|
20
|
+
# @param platform [String] "ios" | "android" | "both"
|
|
21
|
+
# @param schedule [String] Cron expression, e.g. "0 9 * * *" (daily at 9 AM UTC)
|
|
22
|
+
# @param webhook_url [String] URL to notify on detected changes
|
|
23
|
+
# @param options [Hash] Additional monitor options
|
|
24
|
+
#
|
|
25
|
+
# @return [Hash] Monitor object with "id", "appName", "platform", "schedule"
|
|
26
|
+
def create(jwt:, app_name:, platform:, schedule:, webhook_url: nil, **options)
|
|
27
|
+
body = {
|
|
28
|
+
appName: app_name,
|
|
29
|
+
platform: platform,
|
|
30
|
+
schedule: schedule,
|
|
31
|
+
webhookUrl: webhook_url
|
|
32
|
+
}.compact.merge(options)
|
|
33
|
+
|
|
34
|
+
@http.request(:post, "/monitors/app", body: body, auth_header: "Bearer #{jwt}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# List all monitors for the authenticated user.
|
|
38
|
+
#
|
|
39
|
+
# @param jwt [String] Management JWT
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<Hash>] Array of monitor objects
|
|
42
|
+
def list(jwt:)
|
|
43
|
+
@http.request(:get, "/monitors/app", auth_header: "Bearer #{jwt}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get a single monitor's details.
|
|
47
|
+
#
|
|
48
|
+
# @param jwt [String] Management JWT
|
|
49
|
+
# @param monitor_id [String] Monitor ID
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] Monitor object
|
|
52
|
+
def get(jwt:, monitor_id:)
|
|
53
|
+
@http.request(:get, "/monitors/app/#{monitor_id}", auth_header: "Bearer #{jwt}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Delete a monitor.
|
|
57
|
+
#
|
|
58
|
+
# @param jwt [String] Management JWT
|
|
59
|
+
# @param monitor_id [String] Monitor ID
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] Confirmation object
|
|
62
|
+
def delete(jwt:, monitor_id:)
|
|
63
|
+
@http.request(:delete, "/monitors/app/#{monitor_id}", auth_header: "Bearer #{jwt}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Retrieve the run history and diff results for a monitor.
|
|
67
|
+
#
|
|
68
|
+
# @param jwt [String] Management JWT
|
|
69
|
+
# @param monitor_id [String] Monitor ID
|
|
70
|
+
# @param limit [Integer] Number of history entries to return (optional)
|
|
71
|
+
# @param offset [Integer] Pagination offset (optional)
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash] { "history" => Array of run result hashes }
|
|
74
|
+
def history(jwt:, monitor_id:, limit: nil, offset: nil)
|
|
75
|
+
query = {}
|
|
76
|
+
query[:limit] = limit if limit
|
|
77
|
+
query[:offset] = offset if offset
|
|
78
|
+
|
|
79
|
+
@http.request(
|
|
80
|
+
:get,
|
|
81
|
+
"/monitors/app/#{monitor_id}/history",
|
|
82
|
+
query: query,
|
|
83
|
+
auth_header: "Bearer #{jwt}"
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
module Resources
|
|
5
|
+
# Screenshot capture endpoints with optional synchronous polling helpers.
|
|
6
|
+
#
|
|
7
|
+
# All `web`, `mobile`, and `html` methods return a 202 job receipt.
|
|
8
|
+
# The `*_and_wait` variants block until the job completes and return the
|
|
9
|
+
# full result hash directly.
|
|
10
|
+
class Screenshots
|
|
11
|
+
# @param http [HttpClient]
|
|
12
|
+
def initialize(http)
|
|
13
|
+
@http = http
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Enqueue a web screenshot job.
|
|
17
|
+
#
|
|
18
|
+
# @param url [String] Required. Public URL to capture.
|
|
19
|
+
# @param description [String] Plain-English description of the element
|
|
20
|
+
# to target (triggers AI vision path).
|
|
21
|
+
# @param element [String] CSS selector to capture instead of full page.
|
|
22
|
+
# @param dimensions [Hash] { width: Integer, height: Integer }
|
|
23
|
+
# @param full_page [Boolean] Capture the entire scrollable page.
|
|
24
|
+
# @param scrolling [Boolean] Enable scroll-based capture.
|
|
25
|
+
# @param format [String] "png" | "jpeg" | "webp" | "pdf" (default: "png")
|
|
26
|
+
# @param paper_size [String] PDF paper size, e.g. "A4", "Letter"
|
|
27
|
+
# @param block_ads [Boolean] Block ad scripts before capture (STARTER+)
|
|
28
|
+
# @param accept_cookies [Boolean] Auto-dismiss cookie banners (STARTER+)
|
|
29
|
+
# @param stealth [Boolean] Enable stealth mode (BUSINESS+)
|
|
30
|
+
# @param proxy_location [String] Proxy region, e.g. "eu-west" (BUSINESS+)
|
|
31
|
+
# @param video [Hash] { duration: Integer, fps: Integer } (GROWTH+)
|
|
32
|
+
# @param bypass_cache [Boolean] Skip the response cache.
|
|
33
|
+
# @param storage [Hash] Custom S3 config (BUSINESS+)
|
|
34
|
+
# @param webhook_url [String] URL to POST completion event to.
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash] { "jobId", "status", "statusUrl", "estimatedSeconds" }
|
|
37
|
+
def web(options = {})
|
|
38
|
+
@http.request(:post, "/screenshots/web", body: camelize_keys(options))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Enqueue a mobile app screenshot job.
|
|
42
|
+
#
|
|
43
|
+
# @param app_name [String] App display name (e.g. "Instagram")
|
|
44
|
+
# @param platform [String] "ios" | "android" | "both"
|
|
45
|
+
# @param bundle_id [String] Bundle/package ID
|
|
46
|
+
# @param include_store_listing [Boolean] Also download store listing images
|
|
47
|
+
# @param device_emulation [String] Playwright device preset (e.g. "iPhone 12")
|
|
48
|
+
# @param webhook_url [String]
|
|
49
|
+
#
|
|
50
|
+
# @return [Hash] { "jobId", "status", "statusUrl", "estimatedSeconds" }
|
|
51
|
+
def mobile(options = {})
|
|
52
|
+
@http.request(:post, "/screenshots/mobile", body: camelize_keys(options))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Enqueue an HTML-string screenshot job.
|
|
56
|
+
#
|
|
57
|
+
# @param html [String] Raw HTML to render and capture.
|
|
58
|
+
# @param dimensions [Hash] { width: Integer, height: Integer }
|
|
59
|
+
# @param full_page [Boolean]
|
|
60
|
+
# @param format [String] "png" | "jpeg" | "webp" | "pdf"
|
|
61
|
+
# @param webhook_url [String]
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash] { "jobId", "status", "statusUrl", "estimatedSeconds" }
|
|
64
|
+
def html(options = {})
|
|
65
|
+
@http.request(:post, "/screenshots/html", body: camelize_keys(options))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Capture a web screenshot and block until it completes.
|
|
69
|
+
#
|
|
70
|
+
# Combines `web` + `wait` in a single call.
|
|
71
|
+
#
|
|
72
|
+
# @param options [Hash] Same keyword arguments as `web`.
|
|
73
|
+
# @param poll_interval [Numeric] Seconds between status polls (default: 2)
|
|
74
|
+
# @param timeout [Numeric] Maximum seconds to wait (default: 120)
|
|
75
|
+
# @yieldparam status [Hash] Called on each poll with the current status hash.
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash] Full result hash (same as Jobs#result)
|
|
78
|
+
def web_and_wait(options = {}, poll_interval: 2, timeout: 120, &on_progress)
|
|
79
|
+
job = web(options)
|
|
80
|
+
wait(job["jobId"], poll_interval: poll_interval, timeout: timeout, &on_progress)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Capture a mobile screenshot and block until it completes.
|
|
84
|
+
#
|
|
85
|
+
# @param options [Hash] Same keyword arguments as `mobile`.
|
|
86
|
+
# @param poll_interval [Numeric]
|
|
87
|
+
# @param timeout [Numeric]
|
|
88
|
+
#
|
|
89
|
+
# @return [Hash] Full result hash
|
|
90
|
+
def mobile_and_wait(options = {}, poll_interval: 2, timeout: 120)
|
|
91
|
+
job = mobile(options)
|
|
92
|
+
wait(job["jobId"], poll_interval: poll_interval, timeout: timeout)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Render an HTML string and block until the screenshot is ready.
|
|
96
|
+
#
|
|
97
|
+
# @param options [Hash] Same keyword arguments as `html`.
|
|
98
|
+
# @param poll_interval [Numeric]
|
|
99
|
+
# @param timeout [Numeric]
|
|
100
|
+
#
|
|
101
|
+
# @return [Hash] Full result hash
|
|
102
|
+
def html_and_wait(options = {}, poll_interval: 2, timeout: 120)
|
|
103
|
+
job = html(options)
|
|
104
|
+
wait(job["jobId"], poll_interval: poll_interval, timeout: timeout)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Poll a job until it reaches a terminal state.
|
|
108
|
+
#
|
|
109
|
+
# @param job_id [String]
|
|
110
|
+
# @param poll_interval [Numeric] Seconds between status polls (default: 2)
|
|
111
|
+
# @param timeout [Numeric] Maximum seconds to wait (default: 120)
|
|
112
|
+
# @yieldparam status [Hash] Called on each poll (optional progress callback)
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash] Full result hash (from /jobs/:id/result)
|
|
115
|
+
# @raise [JobFailedError] If the job transitions to "failed"
|
|
116
|
+
# @raise [JobTimeoutError] If the job does not complete within `timeout` seconds
|
|
117
|
+
def wait(job_id, poll_interval: 2, timeout: 120, &on_progress)
|
|
118
|
+
deadline = Time.now + timeout
|
|
119
|
+
|
|
120
|
+
loop do
|
|
121
|
+
status_data = @http.request(:get, "/jobs/#{job_id}/status")
|
|
122
|
+
|
|
123
|
+
on_progress.call(status_data) if on_progress
|
|
124
|
+
|
|
125
|
+
case status_data["status"]
|
|
126
|
+
when "completed"
|
|
127
|
+
return @http.request(:get, "/jobs/#{job_id}/result")
|
|
128
|
+
when "failed"
|
|
129
|
+
reason = status_data["error"] || "Unknown failure"
|
|
130
|
+
raise JobFailedError.new(job_id, reason)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if Time.now >= deadline
|
|
134
|
+
raise JobTimeoutError.new(job_id, timeout)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
sleep(poll_interval)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Convert Ruby snake_case keys to camelCase for the JSON API.
|
|
144
|
+
# Nested hashes and arrays are recursively converted.
|
|
145
|
+
def camelize_keys(obj)
|
|
146
|
+
case obj
|
|
147
|
+
when Hash
|
|
148
|
+
obj.each_with_object({}) do |(k, v), memo|
|
|
149
|
+
camel_key = snake_to_camel(k.to_s)
|
|
150
|
+
memo[camel_key] = camelize_keys(v)
|
|
151
|
+
end
|
|
152
|
+
when Array
|
|
153
|
+
obj.map { |item| camelize_keys(item) }
|
|
154
|
+
else
|
|
155
|
+
obj
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# "some_key_name" => "someKeyName"
|
|
160
|
+
def snake_to_camel(str)
|
|
161
|
+
parts = str.split("_")
|
|
162
|
+
parts[0] + parts[1..].map(&:capitalize).join
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|