zenrows 0.2.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +40 -0
- data/lib/zenrows/api_client.rb +70 -7
- data/lib/zenrows/backends/base.rb +31 -1
- data/lib/zenrows/backends/http_rb.rb +10 -2
- data/lib/zenrows/backends/net_http.rb +10 -2
- data/lib/zenrows/client.rb +86 -3
- data/lib/zenrows/configuration.rb +111 -0
- data/lib/zenrows/hooks/context.rb +142 -0
- data/lib/zenrows/hooks/log_subscriber.rb +124 -0
- data/lib/zenrows/hooks.rb +213 -0
- data/lib/zenrows/instrumented_client.rb +187 -0
- data/lib/zenrows/version.rb +1 -1
- data/lib/zenrows.rb +4 -0
- data/sig/zenrows/api_client.rbs +4 -1
- data/sig/zenrows/backends/base.rbs +4 -1
- data/sig/zenrows/client.rbs +2 -1
- data/sig/zenrows/configuration.rbs +9 -0
- data/sig/zenrows/hook_configurator.rbs +9 -0
- data/sig/zenrows/hooks/context.rbs +6 -0
- data/sig/zenrows/hooks/log_subscriber.rbs +15 -0
- data/sig/zenrows/hooks.rbs +23 -0
- data/sig/zenrows/instrumented_client.rbs +22 -0
- data/test/test_helper.rb +42 -0
- data/test/zenrows/client_hooks_test.rb +105 -0
- data/test/zenrows/configuration_hooks_test.rb +101 -0
- data/test/zenrows/hooks/context_test.rb +150 -0
- data/test/zenrows/hooks/log_subscriber_test.rb +105 -0
- data/test/zenrows/hooks_test.rb +215 -0
- data/test/zenrows/instrumented_client_test.rb +153 -0
- metadata +18 -3
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
class Zenrows::Backends::Base
|
|
2
2
|
attr_reader proxy: Zenrows::Proxy
|
|
3
3
|
attr_reader config: Zenrows::Configuration
|
|
4
|
+
attr_reader hooks: Zenrows::Hooks
|
|
4
5
|
|
|
5
|
-
def initialize: (proxy: Zenrows::Proxy, config: Zenrows::Configuration) -> void
|
|
6
|
+
def initialize: (proxy: Zenrows::Proxy, config: Zenrows::Configuration, ?hooks: Zenrows::Hooks?) -> void
|
|
6
7
|
def build_client: (?Hash[Symbol, untyped] options) -> untyped
|
|
7
8
|
def ssl_context: () -> OpenSSL::SSL::SSLContext
|
|
8
9
|
def calculate_timeouts: (?Hash[Symbol, untyped] options) -> Hash[Symbol, Integer]
|
|
10
|
+
def wrap_client: (untyped client, Hash[Symbol, untyped] options) -> untyped
|
|
11
|
+
def backend_name: () -> Symbol
|
|
9
12
|
end
|
data/sig/zenrows/client.rbs
CHANGED
|
@@ -2,8 +2,9 @@ class Zenrows::Client
|
|
|
2
2
|
attr_reader config: Zenrows::Configuration
|
|
3
3
|
attr_reader proxy: Zenrows::Proxy
|
|
4
4
|
attr_reader backend: Zenrows::Backends::Base
|
|
5
|
+
attr_reader hooks: Zenrows::Hooks
|
|
5
6
|
|
|
6
|
-
def initialize: (?api_key: String?, ?host: String?, ?port: Integer?, ?backend: Symbol?) -> void
|
|
7
|
+
def initialize: (?api_key: String?, ?host: String?, ?port: Integer?, ?backend: Symbol?) ?{ (Zenrows::HookConfigurator) -> void } -> void
|
|
7
8
|
def http: (?Hash[Symbol, untyped] options) -> untyped
|
|
8
9
|
def ssl_context: () -> OpenSSL::SSL::SSLContext
|
|
9
10
|
def proxy_config: (?Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
|
|
@@ -11,10 +11,19 @@ class Zenrows::Configuration
|
|
|
11
11
|
attr_accessor read_timeout: Integer
|
|
12
12
|
attr_accessor backend: Symbol
|
|
13
13
|
attr_accessor logger: Logger?
|
|
14
|
+
attr_reader hooks: Zenrows::Hooks
|
|
14
15
|
|
|
15
16
|
def initialize: () -> void
|
|
16
17
|
def reset!: () -> void
|
|
17
18
|
def validate!: () -> true
|
|
18
19
|
def valid?: () -> bool
|
|
19
20
|
def to_h: () -> Hash[Symbol, untyped]
|
|
21
|
+
|
|
22
|
+
# Hook registration methods
|
|
23
|
+
def before_request: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
24
|
+
def after_request: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
25
|
+
def on_response: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
26
|
+
def on_error: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
27
|
+
def around_request: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
28
|
+
def add_subscriber: (untyped subscriber) -> self
|
|
20
29
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class Zenrows::HookConfigurator
|
|
2
|
+
def initialize: (Zenrows::Hooks hooks) -> void
|
|
3
|
+
def before_request: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
4
|
+
def after_request: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
5
|
+
def on_response: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
6
|
+
def on_error: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
7
|
+
def around_request: (?untyped? callable) ?{ (*untyped) -> untyped } -> self
|
|
8
|
+
def add_subscriber: (untyped subscriber) -> self
|
|
9
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
class Zenrows::Hooks::Context
|
|
2
|
+
ZENROWS_HEADERS: Hash[String, Symbol]
|
|
3
|
+
|
|
4
|
+
def self.for_request: (method: Symbol, url: String, options: Hash[Symbol, untyped], backend: Symbol) -> Hash[Symbol, untyped]
|
|
5
|
+
def self.enrich_with_response: (Hash[Symbol, untyped] context, untyped response) -> Hash[Symbol, untyped]
|
|
6
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Zenrows::Hooks::LogSubscriber
|
|
2
|
+
attr_reader logger: Logger?
|
|
3
|
+
|
|
4
|
+
def initialize: (?logger: Logger?) -> void
|
|
5
|
+
def before_request: (Hash[Symbol, untyped] context) -> void
|
|
6
|
+
def on_response: (untyped response, Hash[Symbol, untyped] context) -> void
|
|
7
|
+
def on_error: (Exception error, Hash[Symbol, untyped] context) -> void
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def effective_logger: () -> Logger?
|
|
12
|
+
def log: (Symbol level) { () -> String } -> void
|
|
13
|
+
def extract_status: (untyped response) -> (Integer | String)
|
|
14
|
+
def format_duration: (Float? duration) -> String?
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class Zenrows::Hooks
|
|
2
|
+
include MonitorMixin
|
|
3
|
+
|
|
4
|
+
EVENTS: Array[Symbol]
|
|
5
|
+
|
|
6
|
+
def initialize: () -> void
|
|
7
|
+
def register: (Symbol event, ?_Callable? callable) ?{ (*untyped) -> untyped } -> self
|
|
8
|
+
def add_subscriber: (untyped subscriber) -> self
|
|
9
|
+
def run: (Symbol event, *untyped args) -> void
|
|
10
|
+
def run_around: (Hash[Symbol, untyped] context) { () -> untyped } -> untyped
|
|
11
|
+
def empty?: () -> bool
|
|
12
|
+
def merge: (Zenrows::Hooks? other) -> self
|
|
13
|
+
def dup: () -> Zenrows::Hooks
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def validate_event!: (Symbol event) -> void
|
|
18
|
+
def execute_chain: (Array[untyped] chain, Hash[Symbol, untyped] context) { () -> untyped } -> untyped
|
|
19
|
+
|
|
20
|
+
interface _Callable
|
|
21
|
+
def call: (*untyped) -> untyped
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Zenrows::InstrumentedClient
|
|
2
|
+
HTTP_METHODS: Array[Symbol]
|
|
3
|
+
|
|
4
|
+
attr_reader http: untyped
|
|
5
|
+
attr_reader hooks: Zenrows::Hooks
|
|
6
|
+
attr_reader context_base: Hash[Symbol, untyped]
|
|
7
|
+
|
|
8
|
+
def initialize: (untyped http, hooks: Zenrows::Hooks, context_base: Hash[Symbol, untyped]) -> void
|
|
9
|
+
def get: (String url, **untyped options) -> untyped
|
|
10
|
+
def post: (String url, **untyped options) -> untyped
|
|
11
|
+
def put: (String url, **untyped options) -> untyped
|
|
12
|
+
def patch: (String url, **untyped options) -> untyped
|
|
13
|
+
def delete: (String url, **untyped options) -> untyped
|
|
14
|
+
def head: (String url, **untyped options) -> untyped
|
|
15
|
+
def options: (String url, **untyped options) -> untyped
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def instrument: (Symbol method, String url, Hash[Symbol, untyped] options) { () -> untyped } -> untyped
|
|
20
|
+
def build_context: (Symbol method, String url, Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
|
|
21
|
+
def execute_request: (Hash[Symbol, untyped] context) { () -> untyped } -> untyped
|
|
22
|
+
end
|
data/test/test_helper.rb
CHANGED
|
@@ -5,3 +5,45 @@ require "zenrows"
|
|
|
5
5
|
|
|
6
6
|
require "minitest/autorun"
|
|
7
7
|
require "webmock/minitest"
|
|
8
|
+
|
|
9
|
+
# Simple mock helper for tests
|
|
10
|
+
class MockResponse
|
|
11
|
+
attr_reader :status, :headers
|
|
12
|
+
|
|
13
|
+
def initialize(status, headers = {})
|
|
14
|
+
@status = status
|
|
15
|
+
@headers = headers
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class MockHttpClient
|
|
20
|
+
attr_reader :calls
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@calls = []
|
|
24
|
+
@responses = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stub_response(method, url, response)
|
|
28
|
+
@responses[[method, url]] = response
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get(url, **options)
|
|
32
|
+
@calls << [:get, url, options]
|
|
33
|
+
@responses[[:get, url]] || MockResponse.new(200)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def post(url, **options)
|
|
37
|
+
@calls << [:post, url, options]
|
|
38
|
+
@responses[[:post, url]] || MockResponse.new(200)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def respond_to?(method, include_private = false)
|
|
42
|
+
[:get, :post, :custom_method].include?(method) || super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def custom_method(*args)
|
|
46
|
+
@calls << [:custom_method, *args]
|
|
47
|
+
:result
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ClientHooksTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
Zenrows.reset_configuration!
|
|
8
|
+
Zenrows.configure do |c|
|
|
9
|
+
c.api_key = "test_api_key"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
Zenrows.reset_configuration!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_client_inherits_global_hooks
|
|
18
|
+
global_called = false
|
|
19
|
+
Zenrows.configure do |c|
|
|
20
|
+
c.on_response { global_called = true }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
client = Zenrows::Client.new
|
|
24
|
+
|
|
25
|
+
refute_empty client.hooks
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_client_with_per_client_hooks
|
|
29
|
+
client = Zenrows::Client.new do |c|
|
|
30
|
+
c.on_response {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
refute_empty client.hooks
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_per_client_hooks_dont_modify_global
|
|
37
|
+
global_empty_before = Zenrows.configuration.hooks.empty?
|
|
38
|
+
|
|
39
|
+
Zenrows.configure do |c|
|
|
40
|
+
c.on_response {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
global_not_empty = !Zenrows.configuration.hooks.empty?
|
|
44
|
+
|
|
45
|
+
client = Zenrows::Client.new do |c|
|
|
46
|
+
c.on_error {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Global hooks should still have same state
|
|
50
|
+
global_still_not_empty = !Zenrows.configuration.hooks.empty?
|
|
51
|
+
|
|
52
|
+
assert global_empty_before
|
|
53
|
+
assert global_not_empty
|
|
54
|
+
assert global_still_not_empty
|
|
55
|
+
refute_empty client.hooks
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_multiple_clients_have_independent_hooks
|
|
59
|
+
client1 = Zenrows::Client.new do |c|
|
|
60
|
+
c.on_response { :client1 }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
client2 = Zenrows::Client.new do |c|
|
|
64
|
+
c.on_error { :client2 }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Each client should have its own hooks instance
|
|
68
|
+
refute_same client1.hooks, client2.hooks
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_http_returns_instrumented_client_when_hooks_present
|
|
72
|
+
client = Zenrows::Client.new do |c|
|
|
73
|
+
c.on_response {}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
http = client.http(js_render: true)
|
|
77
|
+
|
|
78
|
+
assert_instance_of Zenrows::InstrumentedClient, http
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_http_returns_raw_client_when_no_hooks
|
|
82
|
+
# Reset to ensure no global hooks
|
|
83
|
+
Zenrows.reset_configuration!
|
|
84
|
+
Zenrows.configure { |c| c.api_key = "test" }
|
|
85
|
+
|
|
86
|
+
client = Zenrows::Client.new
|
|
87
|
+
http = client.http(js_render: true)
|
|
88
|
+
|
|
89
|
+
assert_instance_of HTTP::Client, http
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_per_client_hook_configurator_methods
|
|
93
|
+
# Test all DSL methods are available
|
|
94
|
+
client = Zenrows::Client.new do |c|
|
|
95
|
+
c.before_request {}
|
|
96
|
+
c.after_request {}
|
|
97
|
+
c.on_response {}
|
|
98
|
+
c.on_error {}
|
|
99
|
+
c.around_request { |ctx, &block| block.call }
|
|
100
|
+
c.add_subscriber(Object.new)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
refute_empty client.hooks
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ConfigurationHooksTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
Zenrows.reset_configuration!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def teardown
|
|
11
|
+
Zenrows.reset_configuration!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_configuration_has_hooks
|
|
15
|
+
assert_instance_of Zenrows::Hooks, Zenrows.configuration.hooks
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_on_response_registers_hook
|
|
19
|
+
called = false
|
|
20
|
+
Zenrows.configure do |c|
|
|
21
|
+
c.api_key = "test"
|
|
22
|
+
c.on_response { called = true }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
refute_empty Zenrows.configuration.hooks
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_on_error_registers_hook
|
|
29
|
+
called = false
|
|
30
|
+
Zenrows.configure do |c|
|
|
31
|
+
c.api_key = "test"
|
|
32
|
+
c.on_error { called = true }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
refute_empty Zenrows.configuration.hooks
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_before_request_registers_hook
|
|
39
|
+
Zenrows.configure do |c|
|
|
40
|
+
c.api_key = "test"
|
|
41
|
+
c.before_request {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
refute_empty Zenrows.configuration.hooks
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_after_request_registers_hook
|
|
48
|
+
Zenrows.configure do |c|
|
|
49
|
+
c.api_key = "test"
|
|
50
|
+
c.after_request {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
refute_empty Zenrows.configuration.hooks
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_around_request_registers_hook
|
|
57
|
+
Zenrows.configure do |c|
|
|
58
|
+
c.api_key = "test"
|
|
59
|
+
c.around_request { |ctx, &block| block.call }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
refute_empty Zenrows.configuration.hooks
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_add_subscriber_registers_subscriber
|
|
66
|
+
subscriber = Object.new
|
|
67
|
+
def subscriber.on_response(resp, ctx)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Zenrows.configure do |c|
|
|
71
|
+
c.api_key = "test"
|
|
72
|
+
c.add_subscriber(subscriber)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
refute_empty Zenrows.configuration.hooks
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_reset_clears_hooks
|
|
79
|
+
Zenrows.configure do |c|
|
|
80
|
+
c.api_key = "test"
|
|
81
|
+
c.on_response {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
refute_empty Zenrows.configuration.hooks
|
|
85
|
+
|
|
86
|
+
Zenrows.reset_configuration!
|
|
87
|
+
|
|
88
|
+
assert_empty Zenrows.configuration.hooks
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_hook_methods_return_self_for_chaining
|
|
92
|
+
Zenrows.configure do |c|
|
|
93
|
+
result = c.on_response {}
|
|
94
|
+
.on_error {}
|
|
95
|
+
.before_request {}
|
|
96
|
+
.after_request {}
|
|
97
|
+
|
|
98
|
+
assert_equal c, result
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HooksContextTest < Minitest::Test
|
|
6
|
+
def test_for_request_builds_context
|
|
7
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
8
|
+
method: :get,
|
|
9
|
+
url: "https://example.com/path",
|
|
10
|
+
options: {js_render: true},
|
|
11
|
+
backend: :http_rb
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
assert_equal :get, context[:method]
|
|
15
|
+
assert_equal "https://example.com/path", context[:url]
|
|
16
|
+
assert_equal "example.com", context[:host]
|
|
17
|
+
assert_equal({js_render: true}, context[:options])
|
|
18
|
+
assert_equal :http_rb, context[:backend]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_for_request_initializes_timing_and_headers
|
|
22
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
23
|
+
method: :get,
|
|
24
|
+
url: "https://example.com",
|
|
25
|
+
options: {},
|
|
26
|
+
backend: :http_rb
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert_instance_of Float, context[:started_at]
|
|
30
|
+
assert_empty(context[:zenrows_headers])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_for_request_with_invalid_url
|
|
34
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
35
|
+
method: :get,
|
|
36
|
+
url: "not a valid url::::",
|
|
37
|
+
options: {},
|
|
38
|
+
backend: :http_rb
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert_nil context[:host]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_for_request_freezes_options
|
|
45
|
+
options = {js_render: true}
|
|
46
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
47
|
+
method: :get,
|
|
48
|
+
url: "https://example.com",
|
|
49
|
+
options: options,
|
|
50
|
+
backend: :http_rb
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert_predicate context[:options], :frozen?
|
|
54
|
+
# Original should not be frozen
|
|
55
|
+
refute_predicate options, :frozen?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_enrich_with_response_parses_zenrows_headers
|
|
59
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
60
|
+
method: :get,
|
|
61
|
+
url: "https://example.com",
|
|
62
|
+
options: {},
|
|
63
|
+
backend: :http_rb
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
response = MockResponse.new(200, {
|
|
67
|
+
"Concurrency-Limit" => "25",
|
|
68
|
+
"Concurrency-Remaining" => "23",
|
|
69
|
+
"X-Request-Cost" => "5.5",
|
|
70
|
+
"X-Request-Id" => "abc123",
|
|
71
|
+
"Zr-Final-Url" => "https://example.com/final"
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
Zenrows::Hooks::Context.enrich_with_response(context, response)
|
|
75
|
+
|
|
76
|
+
assert_equal 25, context[:zenrows_headers][:concurrency_limit]
|
|
77
|
+
assert_equal 23, context[:zenrows_headers][:concurrency_remaining]
|
|
78
|
+
assert_in_delta(5.5, context[:zenrows_headers][:request_cost])
|
|
79
|
+
assert_equal "abc123", context[:zenrows_headers][:request_id]
|
|
80
|
+
assert_equal "https://example.com/final", context[:zenrows_headers][:final_url]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_enrich_with_response_sets_timing
|
|
84
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
85
|
+
method: :get,
|
|
86
|
+
url: "https://example.com",
|
|
87
|
+
options: {},
|
|
88
|
+
backend: :http_rb
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response = MockResponse.new(200, {})
|
|
92
|
+
|
|
93
|
+
Zenrows::Hooks::Context.enrich_with_response(context, response)
|
|
94
|
+
|
|
95
|
+
assert_instance_of Float, context[:completed_at]
|
|
96
|
+
assert_instance_of Float, context[:duration]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_enrich_with_response_handles_missing_headers
|
|
100
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
101
|
+
method: :get,
|
|
102
|
+
url: "https://example.com",
|
|
103
|
+
options: {},
|
|
104
|
+
backend: :http_rb
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
response = MockResponse.new(200, {})
|
|
108
|
+
|
|
109
|
+
Zenrows::Hooks::Context.enrich_with_response(context, response)
|
|
110
|
+
|
|
111
|
+
assert_empty(context[:zenrows_headers])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_enrich_with_response_handles_lowercase_headers
|
|
115
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
116
|
+
method: :get,
|
|
117
|
+
url: "https://example.com",
|
|
118
|
+
options: {},
|
|
119
|
+
backend: :http_rb
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
response = MockResponse.new(200, {
|
|
123
|
+
"concurrency-limit" => "10",
|
|
124
|
+
"x-request-cost" => "2.0"
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
Zenrows::Hooks::Context.enrich_with_response(context, response)
|
|
128
|
+
|
|
129
|
+
assert_equal 10, context[:zenrows_headers][:concurrency_limit]
|
|
130
|
+
assert_in_delta(2.0, context[:zenrows_headers][:request_cost])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_enrich_calculates_duration
|
|
134
|
+
context = Zenrows::Hooks::Context.for_request(
|
|
135
|
+
method: :get,
|
|
136
|
+
url: "https://example.com",
|
|
137
|
+
options: {},
|
|
138
|
+
backend: :http_rb
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
sleep 0.01 # Small delay
|
|
142
|
+
|
|
143
|
+
response = MockResponse.new(200, {})
|
|
144
|
+
|
|
145
|
+
Zenrows::Hooks::Context.enrich_with_response(context, response)
|
|
146
|
+
|
|
147
|
+
assert_operator context[:duration], :>=, 0.01
|
|
148
|
+
assert_operator context[:completed_at], :>, context[:started_at]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "logger"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
class LogSubscriberTest < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
@output = StringIO.new
|
|
10
|
+
@logger = Logger.new(@output)
|
|
11
|
+
@logger.level = Logger::DEBUG
|
|
12
|
+
@subscriber = Zenrows::Hooks::LogSubscriber.new(logger: @logger)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_before_request_logs_debug
|
|
16
|
+
context = {method: :get, url: "https://example.com"}
|
|
17
|
+
|
|
18
|
+
@subscriber.before_request(context)
|
|
19
|
+
|
|
20
|
+
assert_includes @output.string, "ZenRows request: GET https://example.com"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_on_response_logs_info_with_status
|
|
24
|
+
response = MockResponse.new(200)
|
|
25
|
+
|
|
26
|
+
context = {
|
|
27
|
+
url: "https://example.com",
|
|
28
|
+
duration: 1.5,
|
|
29
|
+
zenrows_headers: {request_cost: 5.0}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@subscriber.on_response(response, context)
|
|
33
|
+
|
|
34
|
+
assert_includes @output.string, "ZenRows https://example.com -> 200"
|
|
35
|
+
assert_includes @output.string, "1.5s"
|
|
36
|
+
assert_includes @output.string, "cost: 5.0"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_on_response_formats_milliseconds
|
|
40
|
+
response = MockResponse.new(200)
|
|
41
|
+
|
|
42
|
+
context = {
|
|
43
|
+
url: "https://example.com",
|
|
44
|
+
duration: 0.150,
|
|
45
|
+
zenrows_headers: {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@subscriber.on_response(response, context)
|
|
49
|
+
|
|
50
|
+
assert_includes @output.string, "150ms"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_on_error_logs_error
|
|
54
|
+
error = StandardError.new("Connection failed")
|
|
55
|
+
context = {
|
|
56
|
+
url: "https://example.com",
|
|
57
|
+
zenrows_headers: {request_id: "req123"}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@subscriber.on_error(error, context)
|
|
61
|
+
|
|
62
|
+
assert_includes @output.string, "ZenRows https://example.com failed"
|
|
63
|
+
assert_includes @output.string, "StandardError"
|
|
64
|
+
assert_includes @output.string, "Connection failed"
|
|
65
|
+
assert_includes @output.string, "request_id: req123"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_uses_global_logger_when_none_provided
|
|
69
|
+
Zenrows.configure do |c|
|
|
70
|
+
c.api_key = "test"
|
|
71
|
+
c.logger = @logger
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
subscriber = Zenrows::Hooks::LogSubscriber.new
|
|
75
|
+
subscriber.before_request({method: :get, url: "https://test.com"})
|
|
76
|
+
|
|
77
|
+
assert_includes @output.string, "https://test.com"
|
|
78
|
+
ensure
|
|
79
|
+
Zenrows.reset_configuration!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_handles_nil_logger
|
|
83
|
+
subscriber = Zenrows::Hooks::LogSubscriber.new(logger: nil)
|
|
84
|
+
|
|
85
|
+
# Should not raise
|
|
86
|
+
subscriber.before_request({method: :get, url: "https://example.com"})
|
|
87
|
+
subscriber.on_response(MockResponse.new(200), {url: "x", zenrows_headers: {}})
|
|
88
|
+
subscriber.on_error(StandardError.new, {url: "x", zenrows_headers: {}})
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_handles_status_object_with_code
|
|
92
|
+
# Create a response with a status object that has a code method
|
|
93
|
+
status = Object.new
|
|
94
|
+
status.define_singleton_method(:code) { 201 }
|
|
95
|
+
|
|
96
|
+
response = Object.new
|
|
97
|
+
response.define_singleton_method(:status) { status }
|
|
98
|
+
|
|
99
|
+
context = {url: "https://example.com", zenrows_headers: {}}
|
|
100
|
+
|
|
101
|
+
@subscriber.on_response(response, context)
|
|
102
|
+
|
|
103
|
+
assert_includes @output.string, "201"
|
|
104
|
+
end
|
|
105
|
+
end
|