browserbeam 0.1.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 +116 -0
- data/lib/browserbeam/client.rb +62 -0
- data/lib/browserbeam/errors.rb +86 -0
- data/lib/browserbeam/http.rb +55 -0
- data/lib/browserbeam/session.rb +158 -0
- data/lib/browserbeam/types.rb +119 -0
- data/lib/browserbeam/version.rb +3 -0
- data/lib/browserbeam.rb +11 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 15e5f1fad0f6f29ae8ce9616b429a9078771a1d6911a220e32b5f230c12ca02a
|
|
4
|
+
data.tar.gz: 18116e46d6cabbc54716f800964ae354a993cead2f1c44d2edf0e97bbc1f2656
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b8b82446fe2731ac36af5d52ee59706a43cc0e7214d404b066d472bb520cac5565a9eb04d1287858077b085e26bb0771f55cdfd3de3e63275e562859caeadd0b
|
|
7
|
+
data.tar.gz: d5f19a469e10f1eb4ff9225c24e5bb44155504a26a6d936be209f48efa4bbc0f4a1c46516b25f7c540bacdfc83f04b1ba3a72ddce54318c11e0926a522204ae2
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicolae Rotaru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Browserbeam Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [Browserbeam API](https://browserbeam.com) — browser automation built for AI agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install browserbeam
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "browserbeam"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "browserbeam"
|
|
21
|
+
|
|
22
|
+
client = Browserbeam::Client.new(api_key: "sk_live_...")
|
|
23
|
+
|
|
24
|
+
session = client.sessions.create(url: "https://example.com")
|
|
25
|
+
|
|
26
|
+
# Page state is available immediately
|
|
27
|
+
puts session.page.title
|
|
28
|
+
puts session.page.interactive_elements
|
|
29
|
+
|
|
30
|
+
# Interact with the page
|
|
31
|
+
session.click(ref: "e1")
|
|
32
|
+
|
|
33
|
+
# Extract structured data
|
|
34
|
+
result = session.extract(
|
|
35
|
+
title: "h1 >> text",
|
|
36
|
+
links: ["a >> href"]
|
|
37
|
+
)
|
|
38
|
+
puts result.extraction
|
|
39
|
+
|
|
40
|
+
# Close when done
|
|
41
|
+
session.close
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
client = Browserbeam::Client.new(
|
|
48
|
+
api_key: "sk_live_...", # or set BROWSERBEAM_API_KEY env var
|
|
49
|
+
base_url: "https://api.browserbeam.com", # default
|
|
50
|
+
timeout: 120, # request timeout in seconds
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Session Options
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
session = client.sessions.create(
|
|
58
|
+
url: "https://example.com",
|
|
59
|
+
viewport: { width: 1280, height: 720 },
|
|
60
|
+
locale: "en-US",
|
|
61
|
+
timezone: "America/New_York",
|
|
62
|
+
proxy: "http://user:pass@proxy:8080",
|
|
63
|
+
block_resources: ["image", "font"],
|
|
64
|
+
auto_dismiss_blockers: true,
|
|
65
|
+
timeout: 300,
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Available Methods
|
|
70
|
+
|
|
71
|
+
| Method | Description |
|
|
72
|
+
|--------|-------------|
|
|
73
|
+
| `session.goto(url)` | Navigate to a URL |
|
|
74
|
+
| `session.observe` | Get page state as markdown |
|
|
75
|
+
| `session.click(ref:)` | Click an element by ref, text, or label |
|
|
76
|
+
| `session.fill(value, ref:)` | Fill an input field |
|
|
77
|
+
| `session.type(value, label:)` | Type text character by character |
|
|
78
|
+
| `session.select(value, label:)` | Select a dropdown option |
|
|
79
|
+
| `session.check(label:)` | Toggle a checkbox |
|
|
80
|
+
| `session.scroll(to: "bottom")` | Scroll the page |
|
|
81
|
+
| `session.scroll_collect` | Scroll and collect all content |
|
|
82
|
+
| `session.screenshot` | Take a screenshot |
|
|
83
|
+
| `session.extract(**schema)` | Extract structured data |
|
|
84
|
+
| `session.fill_form(fields, submit:)` | Fill and submit a form |
|
|
85
|
+
| `session.wait(ms:)` | Wait for time, selector, or text |
|
|
86
|
+
| `session.pdf` | Generate a PDF |
|
|
87
|
+
| `session.execute_js(expr)` | Run JavaScript |
|
|
88
|
+
| `session.close` | Close the session |
|
|
89
|
+
|
|
90
|
+
## Session Management
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
sessions = client.sessions.list(status: "active")
|
|
94
|
+
info = client.sessions.get("ses_abc123")
|
|
95
|
+
client.sessions.destroy("ses_abc123")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
begin
|
|
102
|
+
session = client.sessions.create(url: "https://example.com")
|
|
103
|
+
rescue Browserbeam::RateLimitError => e
|
|
104
|
+
puts "Rate limited. Retry after #{e.retry_after}s"
|
|
105
|
+
rescue Browserbeam::SessionNotFoundError => e
|
|
106
|
+
puts "Session not found: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Documentation
|
|
111
|
+
|
|
112
|
+
Full API documentation at [browserbeam.com/docs](https://browserbeam.com/docs/).
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Browserbeam
|
|
2
|
+
class Client
|
|
3
|
+
attr_reader :sessions
|
|
4
|
+
|
|
5
|
+
def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
6
|
+
resolved_key = api_key || ENV["BROWSERBEAM_API_KEY"] || ""
|
|
7
|
+
raise ArgumentError, "No API key provided. Pass api_key: or set the BROWSERBEAM_API_KEY environment variable." if resolved_key.empty?
|
|
8
|
+
|
|
9
|
+
@http = Http.new(api_key: resolved_key, base_url: base_url, timeout: timeout)
|
|
10
|
+
@sessions = Sessions.new(@http)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Sessions
|
|
15
|
+
def initialize(http)
|
|
16
|
+
@http = http
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create(url: nil, steps: nil, timeout: nil, viewport: nil, user_agent: nil,
|
|
20
|
+
locale: nil, timezone: nil, proxy: nil, block_resources: nil,
|
|
21
|
+
auto_dismiss_blockers: nil, cookies: nil, idempotency_key: nil)
|
|
22
|
+
body = {}
|
|
23
|
+
body[:url] = url if url
|
|
24
|
+
body[:steps] = steps if steps
|
|
25
|
+
body[:timeout] = timeout if timeout
|
|
26
|
+
body[:viewport] = viewport if viewport
|
|
27
|
+
body[:user_agent] = user_agent if user_agent
|
|
28
|
+
body[:locale] = locale if locale
|
|
29
|
+
body[:timezone] = timezone if timezone
|
|
30
|
+
body[:proxy] = proxy if proxy
|
|
31
|
+
body[:block_resources] = block_resources if block_resources
|
|
32
|
+
body[:auto_dismiss_blockers] = auto_dismiss_blockers unless auto_dismiss_blockers.nil?
|
|
33
|
+
body[:cookies] = cookies if cookies
|
|
34
|
+
|
|
35
|
+
extra_headers = {}
|
|
36
|
+
extra_headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
37
|
+
|
|
38
|
+
data = @http.post("/v1/sessions", body, extra_headers)
|
|
39
|
+
envelope = SessionEnvelope.from_hash(data)
|
|
40
|
+
Session.new(envelope, @http)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get(session_id)
|
|
44
|
+
data = @http.get("/v1/sessions/#{session_id}")
|
|
45
|
+
SessionInfo.from_hash(data)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def list(status: nil, limit: nil, after: nil)
|
|
49
|
+
params = {}
|
|
50
|
+
params[:status] = status if status
|
|
51
|
+
params[:limit] = limit if limit
|
|
52
|
+
params[:after] = after if after
|
|
53
|
+
data = @http.get("/v1/sessions", params)
|
|
54
|
+
SessionList.from_hash(data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def destroy(session_id)
|
|
58
|
+
@http.delete("/v1/sessions/#{session_id}")
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Browserbeam
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
attr_reader :code, :status_code, :context, :request_id
|
|
4
|
+
|
|
5
|
+
def initialize(message, code: "", status_code: 0, context: {}, request_id: nil)
|
|
6
|
+
super(message)
|
|
7
|
+
@code = code
|
|
8
|
+
@status_code = status_code
|
|
9
|
+
@context = context || {}
|
|
10
|
+
@request_id = request_id
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AuthenticationError < Error; end
|
|
15
|
+
class InvalidRequestError < Error; end
|
|
16
|
+
class SessionNotFoundError < Error; end
|
|
17
|
+
|
|
18
|
+
class RateLimitError < Error
|
|
19
|
+
attr_reader :retry_after
|
|
20
|
+
|
|
21
|
+
def initialize(message, retry_after: nil, **kwargs)
|
|
22
|
+
super(message, **kwargs)
|
|
23
|
+
@retry_after = retry_after
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class QuotaExceededError < Error
|
|
28
|
+
attr_reader :retry_after
|
|
29
|
+
|
|
30
|
+
def initialize(message, retry_after: nil, **kwargs)
|
|
31
|
+
super(message, **kwargs)
|
|
32
|
+
@retry_after = retry_after
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class EngineUnavailableError < Error
|
|
37
|
+
attr_reader :retry_after
|
|
38
|
+
|
|
39
|
+
def initialize(message, retry_after: nil, **kwargs)
|
|
40
|
+
super(message, **kwargs)
|
|
41
|
+
@retry_after = retry_after
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class StepExecutionError < Error
|
|
46
|
+
attr_reader :step_index, :action
|
|
47
|
+
|
|
48
|
+
def initialize(message, step_index: 0, action: "", **kwargs)
|
|
49
|
+
super(message, **kwargs)
|
|
50
|
+
@step_index = step_index
|
|
51
|
+
@action = action
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module ErrorHandler
|
|
56
|
+
def self.raise_for_status(status_code, body, headers)
|
|
57
|
+
error_data = body.is_a?(Hash) ? (body["error"] || {}) : {}
|
|
58
|
+
code = error_data["code"] || "unknown"
|
|
59
|
+
message = error_data["message"] || "Unknown error"
|
|
60
|
+
context = error_data["context"]
|
|
61
|
+
request_id = headers["x-request-id"]
|
|
62
|
+
retry_after_raw = headers["retry-after"]
|
|
63
|
+
retry_after = retry_after_raw ? retry_after_raw.to_i : nil
|
|
64
|
+
|
|
65
|
+
kwargs = { code: code, status_code: status_code, context: context, request_id: request_id }
|
|
66
|
+
|
|
67
|
+
case status_code
|
|
68
|
+
when 401
|
|
69
|
+
raise AuthenticationError.new(message, **kwargs)
|
|
70
|
+
when 429
|
|
71
|
+
if code == "quota_exceeded"
|
|
72
|
+
raise QuotaExceededError.new(message, retry_after: retry_after, **kwargs)
|
|
73
|
+
end
|
|
74
|
+
raise RateLimitError.new(message, retry_after: retry_after, **kwargs)
|
|
75
|
+
when 404
|
|
76
|
+
raise SessionNotFoundError.new(message, **kwargs)
|
|
77
|
+
when 503
|
|
78
|
+
raise EngineUnavailableError.new(message, retry_after: retry_after, **kwargs)
|
|
79
|
+
when 400
|
|
80
|
+
raise InvalidRequestError.new(message, **kwargs)
|
|
81
|
+
else
|
|
82
|
+
raise Error.new(message, **kwargs)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Browserbeam
|
|
5
|
+
class Http
|
|
6
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
7
|
+
@conn = Faraday.new(url: base_url) do |f|
|
|
8
|
+
f.request :json
|
|
9
|
+
f.response :json, content_type: /\bjson$/
|
|
10
|
+
f.adapter Faraday.default_adapter
|
|
11
|
+
f.headers["Authorization"] = "Bearer #{api_key}"
|
|
12
|
+
f.headers["User-Agent"] = "browserbeam-ruby/#{VERSION}"
|
|
13
|
+
f.options.timeout = timeout
|
|
14
|
+
f.options.open_timeout = 10
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def post(path, body = {}, extra_headers = {})
|
|
19
|
+
response = @conn.post(path) do |req|
|
|
20
|
+
req.body = body
|
|
21
|
+
extra_headers.each { |k, v| req.headers[k] = v }
|
|
22
|
+
end
|
|
23
|
+
handle_response(response)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get(path, params = {})
|
|
27
|
+
response = @conn.get(path) do |req|
|
|
28
|
+
params.each { |k, v| req.params[k.to_s] = v unless v.nil? }
|
|
29
|
+
end
|
|
30
|
+
handle_response(response)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete(path)
|
|
34
|
+
response = @conn.delete(path)
|
|
35
|
+
return {} if response.status == 204
|
|
36
|
+
handle_response(response)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def handle_response(response)
|
|
42
|
+
return {} if response.status == 204
|
|
43
|
+
|
|
44
|
+
body = response.body.is_a?(String) ? JSON.parse(response.body) : response.body
|
|
45
|
+
|
|
46
|
+
if response.status >= 400
|
|
47
|
+
headers = {}
|
|
48
|
+
response.headers.each { |k, v| headers[k.downcase] = v }
|
|
49
|
+
ErrorHandler.raise_for_status(response.status, body, headers)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
body
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module Browserbeam
|
|
2
|
+
class Session
|
|
3
|
+
attr_reader :session_id, :expires_at, :request_id, :completed,
|
|
4
|
+
:page, :media, :extraction, :blockers_dismissed, :error
|
|
5
|
+
|
|
6
|
+
def initialize(envelope, http)
|
|
7
|
+
@http = http
|
|
8
|
+
update(envelope)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def last_response
|
|
12
|
+
@last
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def act(steps)
|
|
16
|
+
data = @http.post("/v1/sessions/#{session_id}/act", { steps: steps })
|
|
17
|
+
envelope = SessionEnvelope.from_hash(data)
|
|
18
|
+
update(envelope)
|
|
19
|
+
envelope
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def goto(url, wait_for: nil, wait_timeout: nil)
|
|
23
|
+
params = { url: url }
|
|
24
|
+
params[:wait_for] = wait_for if wait_for
|
|
25
|
+
params[:wait_timeout] = wait_timeout if wait_timeout
|
|
26
|
+
act([{ goto: params }])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def observe(scope: nil, format: nil, include_links: nil, max_text_length: nil)
|
|
30
|
+
params = {}
|
|
31
|
+
params[:scope] = scope if scope
|
|
32
|
+
params[:format] = format if format
|
|
33
|
+
params[:include_links] = include_links unless include_links.nil?
|
|
34
|
+
params[:max_text_length] = max_text_length if max_text_length
|
|
35
|
+
act([{ observe: params }])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def click(ref: nil, text: nil, label: nil)
|
|
39
|
+
params = {}
|
|
40
|
+
params[:ref] = ref if ref
|
|
41
|
+
params[:text] = text if text
|
|
42
|
+
params[:label] = label if label
|
|
43
|
+
act([{ click: params }])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fill(value, ref: nil, label: nil)
|
|
47
|
+
params = { value: value }
|
|
48
|
+
params[:ref] = ref if ref
|
|
49
|
+
params[:label] = label if label
|
|
50
|
+
act([{ fill: params }])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def type(value, label: nil, ref: nil, delay: nil)
|
|
54
|
+
params = { value: value }
|
|
55
|
+
params[:label] = label if label
|
|
56
|
+
params[:ref] = ref if ref
|
|
57
|
+
params[:delay] = delay if delay
|
|
58
|
+
act([{ type: params }])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def select(value, label: nil, ref: nil)
|
|
62
|
+
params = { value: value }
|
|
63
|
+
params[:label] = label if label
|
|
64
|
+
params[:ref] = ref if ref
|
|
65
|
+
act([{ select: params }])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check(label: nil, ref: nil, checked: nil)
|
|
69
|
+
params = {}
|
|
70
|
+
params[:label] = label if label
|
|
71
|
+
params[:ref] = ref if ref
|
|
72
|
+
params[:checked] = checked unless checked.nil?
|
|
73
|
+
act([{ check: params }])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def scroll(to: nil, direction: nil, amount: nil, times: nil, ref: nil)
|
|
77
|
+
params = {}
|
|
78
|
+
params[:to] = to if to
|
|
79
|
+
params[:direction] = direction if direction
|
|
80
|
+
params[:amount] = amount if amount
|
|
81
|
+
params[:times] = times if times
|
|
82
|
+
params[:ref] = ref if ref
|
|
83
|
+
act([{ scroll: params }])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def scroll_collect(max_scrolls: nil, wait_ms: nil, timeout_ms: nil, max_text_length: nil)
|
|
87
|
+
params = {}
|
|
88
|
+
params[:max_scrolls] = max_scrolls if max_scrolls
|
|
89
|
+
params[:wait_ms] = wait_ms if wait_ms
|
|
90
|
+
params[:timeout_ms] = timeout_ms if timeout_ms
|
|
91
|
+
params[:max_text_length] = max_text_length if max_text_length
|
|
92
|
+
act([{ scroll_collect: params }])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def screenshot(full_page: nil, format: nil, quality: nil, selector: nil)
|
|
96
|
+
params = {}
|
|
97
|
+
params[:full_page] = full_page unless full_page.nil?
|
|
98
|
+
params[:format] = format if format
|
|
99
|
+
params[:quality] = quality if quality
|
|
100
|
+
params[:selector] = selector if selector
|
|
101
|
+
act([{ screenshot: params }])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def wait(ms: nil, selector: nil, text: nil, timeout: nil)
|
|
105
|
+
params = {}
|
|
106
|
+
params[:ms] = ms if ms
|
|
107
|
+
params[:selector] = selector if selector
|
|
108
|
+
params[:text] = text if text
|
|
109
|
+
params[:timeout] = timeout if timeout
|
|
110
|
+
act([{ wait: params }])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def extract(**schema)
|
|
114
|
+
act([{ extract: schema }])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def fill_form(fields, submit: false)
|
|
118
|
+
params = { fields: fields }
|
|
119
|
+
params[:submit] = true if submit
|
|
120
|
+
act([{ fill_form: params }])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def upload(ref, files)
|
|
124
|
+
act([{ upload: { ref: ref, files: files } }])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def pdf(format: nil, landscape: nil, print_background: nil)
|
|
128
|
+
params = {}
|
|
129
|
+
params[:format] = format if format
|
|
130
|
+
params[:landscape] = landscape unless landscape.nil?
|
|
131
|
+
params[:print_background] = print_background unless print_background.nil?
|
|
132
|
+
act([{ pdf: params }])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def execute_js(expression)
|
|
136
|
+
act([{ execute_js: { expression: expression } }])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def close
|
|
140
|
+
act([{ close: {} }])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def update(envelope)
|
|
146
|
+
@last = envelope
|
|
147
|
+
@session_id = envelope.session_id
|
|
148
|
+
@expires_at = envelope.expires_at
|
|
149
|
+
@request_id = envelope.request_id
|
|
150
|
+
@completed = envelope.completed
|
|
151
|
+
@page = envelope.page
|
|
152
|
+
@media = envelope.media
|
|
153
|
+
@extraction = envelope.extraction
|
|
154
|
+
@blockers_dismissed = envelope.blockers_dismissed
|
|
155
|
+
@error = envelope.error
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module Browserbeam
|
|
2
|
+
MarkdownContent = Struct.new(:content, :length, keyword_init: true) do
|
|
3
|
+
def self.from_hash(data)
|
|
4
|
+
return nil unless data.is_a?(Hash)
|
|
5
|
+
new(content: data["content"] || "", length: data["length"])
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
InteractiveElement = Struct.new(:ref, :tag, :role, :label, :value, keyword_init: true) do
|
|
10
|
+
def self.from_hash(data)
|
|
11
|
+
new(
|
|
12
|
+
ref: data["ref"] || "",
|
|
13
|
+
tag: data["tag"] || "",
|
|
14
|
+
role: data["role"] || "",
|
|
15
|
+
label: data["label"] || "",
|
|
16
|
+
value: data["value"],
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Changes = Struct.new(:content_changed, :content_delta, :elements_added, :elements_removed, keyword_init: true) do
|
|
22
|
+
def self.from_hash(data)
|
|
23
|
+
return nil unless data.is_a?(Hash)
|
|
24
|
+
new(
|
|
25
|
+
content_changed: data["content_changed"] || false,
|
|
26
|
+
content_delta: data["content_delta"],
|
|
27
|
+
elements_added: data["elements_added"] || [],
|
|
28
|
+
elements_removed: data["elements_removed"] || [],
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
ScrollState = Struct.new(:y, :height, :viewport, :percent, keyword_init: true) do
|
|
34
|
+
def self.from_hash(data)
|
|
35
|
+
return nil unless data.is_a?(Hash)
|
|
36
|
+
new(y: data["y"] || 0, height: data["height"] || 0, viewport: data["viewport"] || 0, percent: data["percent"] || 0)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
PageState = Struct.new(:url, :title, :stable, :markdown, :interactive_elements, :forms, :changes, :scroll, keyword_init: true) do
|
|
41
|
+
def self.from_hash(data)
|
|
42
|
+
return nil unless data.is_a?(Hash)
|
|
43
|
+
new(
|
|
44
|
+
url: data["url"] || "",
|
|
45
|
+
title: data["title"] || "",
|
|
46
|
+
stable: data["stable"] || false,
|
|
47
|
+
markdown: MarkdownContent.from_hash(data["markdown"]),
|
|
48
|
+
interactive_elements: (data["interactive_elements"] || []).map { |el| InteractiveElement.from_hash(el) },
|
|
49
|
+
forms: data["forms"] || [],
|
|
50
|
+
changes: Changes.from_hash(data["changes"]),
|
|
51
|
+
scroll: ScrollState.from_hash(data["scroll"]),
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
MediaItem = Struct.new(:type, :format, :data, keyword_init: true) do
|
|
57
|
+
def self.from_hash(data)
|
|
58
|
+
new(type: data["type"] || "", format: data["format"] || "", data: data["data"] || "")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
StepError = Struct.new(:step, :action, :code, :message, :context, keyword_init: true) do
|
|
63
|
+
def self.from_hash(data)
|
|
64
|
+
return nil unless data.is_a?(Hash)
|
|
65
|
+
new(
|
|
66
|
+
step: data["step"] || 0,
|
|
67
|
+
action: data["action"] || "",
|
|
68
|
+
code: data["code"] || "",
|
|
69
|
+
message: data["message"] || "",
|
|
70
|
+
context: data["context"] || {},
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
SessionEnvelope = Struct.new(:session_id, :expires_at, :request_id, :completed, :page, :media, :extraction, :blockers_dismissed, :error, keyword_init: true) do
|
|
76
|
+
def self.from_hash(data)
|
|
77
|
+
new(
|
|
78
|
+
session_id: data["session_id"] || "",
|
|
79
|
+
expires_at: data["expires_at"] || "",
|
|
80
|
+
request_id: data["request_id"] || "",
|
|
81
|
+
completed: data["completed"] || 0,
|
|
82
|
+
page: PageState.from_hash(data["page"]),
|
|
83
|
+
media: (data["media"] || []).map { |m| MediaItem.from_hash(m) },
|
|
84
|
+
extraction: data["extraction"],
|
|
85
|
+
blockers_dismissed: data["blockers_dismissed"] || [],
|
|
86
|
+
error: StepError.from_hash(data["error"]),
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
SessionInfo = Struct.new(:session_id, :status, :started_at, :ended_at, :duration_seconds, :expires_at, keyword_init: true) do
|
|
92
|
+
def self.from_hash(data)
|
|
93
|
+
new(
|
|
94
|
+
session_id: data["session_id"] || "",
|
|
95
|
+
status: data["status"] || "",
|
|
96
|
+
started_at: data["started_at"] || "",
|
|
97
|
+
ended_at: data["ended_at"],
|
|
98
|
+
duration_seconds: data["duration_seconds"],
|
|
99
|
+
expires_at: data["expires_at"] || "",
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
SessionListItem = Struct.new(:session_id, :status, :started_at, keyword_init: true) do
|
|
105
|
+
def self.from_hash(data)
|
|
106
|
+
new(session_id: data["session_id"] || "", status: data["status"] || "", started_at: data["started_at"] || "")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
SessionList = Struct.new(:sessions, :has_more, :next_cursor, keyword_init: true) do
|
|
111
|
+
def self.from_hash(data)
|
|
112
|
+
new(
|
|
113
|
+
sessions: (data["sessions"] || []).map { |s| SessionListItem.from_hash(s) },
|
|
114
|
+
has_more: data["has_more"] || false,
|
|
115
|
+
next_cursor: data["next_cursor"],
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/browserbeam.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require_relative "browserbeam/version"
|
|
2
|
+
require_relative "browserbeam/errors"
|
|
3
|
+
require_relative "browserbeam/types"
|
|
4
|
+
require_relative "browserbeam/http"
|
|
5
|
+
require_relative "browserbeam/session"
|
|
6
|
+
require_relative "browserbeam/client"
|
|
7
|
+
|
|
8
|
+
module Browserbeam
|
|
9
|
+
DEFAULT_BASE_URL = "https://api.browserbeam.com"
|
|
10
|
+
DEFAULT_TIMEOUT = 120
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: browserbeam
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Browserbeam
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-24 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: faraday-net_http
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
description: Browser automation built for AI agents. Control real browsers through
|
|
48
|
+
a simple Ruby interface.
|
|
49
|
+
email:
|
|
50
|
+
- hello@browserbeam.com
|
|
51
|
+
executables: []
|
|
52
|
+
extensions: []
|
|
53
|
+
extra_rdoc_files: []
|
|
54
|
+
files:
|
|
55
|
+
- LICENSE
|
|
56
|
+
- README.md
|
|
57
|
+
- lib/browserbeam.rb
|
|
58
|
+
- lib/browserbeam/client.rb
|
|
59
|
+
- lib/browserbeam/errors.rb
|
|
60
|
+
- lib/browserbeam/http.rb
|
|
61
|
+
- lib/browserbeam/session.rb
|
|
62
|
+
- lib/browserbeam/types.rb
|
|
63
|
+
- lib/browserbeam/version.rb
|
|
64
|
+
homepage: https://browserbeam.com
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
homepage_uri: https://browserbeam.com
|
|
69
|
+
source_code_uri: https://github.com/nyku/browserbeam-ruby
|
|
70
|
+
documentation_uri: https://browserbeam.com/docs/
|
|
71
|
+
changelog_uri: https://github.com/nyku/browserbeam-ruby/blob/main/CHANGELOG.md
|
|
72
|
+
post_install_message:
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.0'
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 3.5.22
|
|
88
|
+
signing_key:
|
|
89
|
+
specification_version: 4
|
|
90
|
+
summary: Official Ruby SDK for the Browserbeam API
|
|
91
|
+
test_files: []
|