hooksniff 1.2.0 → 1.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/Gemfile.lock +2 -2
- data/README.md +49 -53
- data/hooksniff.gemspec +1 -1
- data/lib/hooksniff/api/authentication.rb +0 -13
- data/lib/hooksniff/api/integration.rb +8 -8
- data/lib/hooksniff/api/statistics.rb +0 -13
- data/lib/hooksniff/api/stream.rb +28 -9
- data/lib/hooksniff/errors.rb +79 -0
- data/lib/hooksniff/hooksniff_http_client.rb +48 -9
- data/lib/hooksniff/options.rb +31 -0
- data/lib/hooksniff/paginator.rb +45 -0
- data/lib/hooksniff/response_metadata.rb +48 -0
- data/lib/hooksniff/version.rb +1 -1
- data/lib/hooksniff/webhook.rb +62 -0
- data/lib/hooksniff/webhook_event.rb +295 -0
- data/lib/hooksniff.rb +3 -0
- data/test/test_hooksniff.rb +0 -11
- data/test/test_typed_events.rb +260 -0
- metadata +9 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a947ca7c8c9c75d2aac6b59f30cd1e0c7f6096aee40816fb16bba954ff60570b
|
|
4
|
+
data.tar.gz: da3d3b65337ff775633efc6ce7797cdd23033a3f2b1b9cfb5bdc3096b485f4e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f609bcef3b51f1fee6540b705bb9191f54b7cc3c681346a08b1482d1aeef0a52e661f0b26f6e432f30995bbff1b54ae043c775b42938c25975da4fcfe36ac3f8
|
|
7
|
+
data.tar.gz: a3c6d7bf7516cf4e41fdb531af663d8036056210e853a54c46674e2a56f60a41523b7f53ac06bd4c1e226711c0143281d8f276a89e39820447757b1b2275998a
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
|
|
4
|
+
hooksniff (1.2.0)
|
|
5
5
|
base64 (~> 0.3.0)
|
|
6
6
|
logger (~> 1.0)
|
|
7
7
|
|
|
@@ -45,11 +45,11 @@ PLATFORMS
|
|
|
45
45
|
|
|
46
46
|
DEPENDENCIES
|
|
47
47
|
base64
|
|
48
|
+
hooksniff!
|
|
48
49
|
logger
|
|
49
50
|
ostruct
|
|
50
51
|
rake (~> 13.0)
|
|
51
52
|
rspec (~> 3.2)
|
|
52
|
-
svix!
|
|
53
53
|
webmock (~> 3.25)
|
|
54
54
|
|
|
55
55
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# HookSniff Ruby SDK
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="https://
|
|
5
|
-
<a href="https://
|
|
4
|
+
<a href="https://rubygems.org/gems/hooksniff"><img src="https://img.shields.io/gem/v/hooksniff.svg" alt="Gem"></a>
|
|
5
|
+
<a href="https://github.com/servetarslan02/hooksniff-ruby"><img src="https://img.shields.io/github/license/servetarslan02/hooksniff-ruby" alt="License"></a>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
Ruby SDK for the [HookSniff](https://hooksniff.
|
|
8
|
+
Ruby SDK for the [HookSniff](https://hooksniff.vercel.app) webhook delivery platform.
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
|
+
```ruby
|
|
13
|
+
gem 'hooksniff'
|
|
14
|
+
```
|
|
15
|
+
|
|
12
16
|
```bash
|
|
13
17
|
gem install hooksniff
|
|
14
18
|
```
|
|
@@ -16,61 +20,53 @@ gem install hooksniff
|
|
|
16
20
|
## Quick Start
|
|
17
21
|
|
|
18
22
|
```ruby
|
|
19
|
-
require
|
|
23
|
+
require 'hooksniff'
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
hs = HookSniff::Client.new('hooksniff_xxx')
|
|
22
26
|
|
|
23
27
|
# List endpoints
|
|
24
|
-
endpoints =
|
|
28
|
+
endpoints = hs.endpoints.list
|
|
25
29
|
|
|
26
|
-
#
|
|
27
|
-
|
|
30
|
+
# Send a webhook
|
|
31
|
+
delivery = hs.messages.create(
|
|
32
|
+
endpoint_id: 'ep_xxx',
|
|
33
|
+
event: 'order.created',
|
|
34
|
+
data: { order_id: '123', amount: 99.99 }
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Webhook Verification
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
wh = HookSniff::Webhook.new(
|
|
40
|
+
```ruby
|
|
41
|
+
wh = HookSniff::Webhook.new('whsec_xxx')
|
|
31
42
|
payload = wh.verify(body, headers)
|
|
32
43
|
```
|
|
33
44
|
|
|
34
|
-
## Resources
|
|
35
|
-
|
|
36
|
-
| Resource |
|
|
37
|
-
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
| **Sso** | SSO config management |
|
|
63
|
-
| **AuditLog** | Audit entry listing |
|
|
64
|
-
| **CustomDomain** | Domain management, verification |
|
|
65
|
-
| **RateLimit** | Per-endpoint rate limits |
|
|
66
|
-
| **Routing** | Routing rules, endpoint health |
|
|
67
|
-
| **Template** | Template listing, apply |
|
|
68
|
-
| **Schema** | Schema registry, validation |
|
|
69
|
-
| **Playground** | Test webhooks |
|
|
70
|
-
| **ServiceToken** | Service token management |
|
|
71
|
-
|
|
72
|
-
## Links
|
|
73
|
-
|
|
74
|
-
- [Documentation](https://docs.hooksniff.com)
|
|
75
|
-
- [API Reference](https://api.hooksniff.com)
|
|
76
|
-
- [GitHub](https://github.com/servetarslan02/HookSniff)
|
|
45
|
+
## API Resources
|
|
46
|
+
|
|
47
|
+
| Resource | Methods |
|
|
48
|
+
|----------|---------|
|
|
49
|
+
| Endpoints | list, create, get, update, delete, rotate_secret |
|
|
50
|
+
| Messages | create, list, get |
|
|
51
|
+
| MessageAttempts | list, get, resend, list_by_msg |
|
|
52
|
+
| EventTypes | list, create, get, update, delete |
|
|
53
|
+
| Stream | list_channels, get_channel, create_channel, subscribe, publish |
|
|
54
|
+
| Authentication | login, register, logout |
|
|
55
|
+
| BackgroundTasks | list, get |
|
|
56
|
+
| Connectors | list, get |
|
|
57
|
+
| Integrations | list, get, create, update, delete |
|
|
58
|
+
| Inbound | list, create, get, delete |
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- ✅ HMAC-SHA256 webhook verification
|
|
63
|
+
- ✅ Typed webhook events
|
|
64
|
+
- ✅ Automatic retry with exponential backoff
|
|
65
|
+
- ✅ Pagination helpers
|
|
66
|
+
- ✅ Rate limit header parsing
|
|
67
|
+
- ✅ SSE streaming
|
|
68
|
+
- ✅ Idempotency keys
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
data/hooksniff.gemspec
CHANGED
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.name = "hooksniff"
|
|
9
9
|
spec.version = HookSniff::VERSION
|
|
10
10
|
spec.authors = ["HookSniff"]
|
|
11
|
-
spec.email = ["
|
|
11
|
+
spec.email = ["servetarslan02@gmail.com"]
|
|
12
12
|
spec.license = "MIT"
|
|
13
13
|
|
|
14
14
|
spec.summary = "HookSniff webhooks API client and webhook verification library"
|
|
@@ -8,19 +8,6 @@ module HookSniff
|
|
|
8
8
|
@client = client
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def app_portal_access(app_id, app_portal_access_in, options = {})
|
|
12
|
-
options = options.transform_keys(&:to_s)
|
|
13
|
-
res = @client.execute_request(
|
|
14
|
-
"POST",
|
|
15
|
-
"/v1/auth/portal-access/#{app_id}",
|
|
16
|
-
headers: {
|
|
17
|
-
"idempotency-key" => options["idempotency-key"]
|
|
18
|
-
},
|
|
19
|
-
body: app_portal_access_in
|
|
20
|
-
)
|
|
21
|
-
DashboardAccessOut.deserialize(res)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
11
|
def logout(options = {})
|
|
25
12
|
options = options.transform_keys(&:to_s)
|
|
26
13
|
@client.execute_request(
|
|
@@ -7,36 +7,36 @@ module HookSniff
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def list
|
|
10
|
-
@client.request(:get, "/
|
|
10
|
+
@client.request(:get, "/v1/integrations")
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def get(id)
|
|
14
|
-
@client.request(:get, "/
|
|
14
|
+
@client.request(:get, "/v1/integrations/#{id}")
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
create_attrs = %i[name description connector_config_id endpoint_id event_filter transform_id retry_policy metadata enabled]
|
|
18
18
|
def create(attrs)
|
|
19
|
-
@client.request(:post, "/
|
|
19
|
+
@client.request(:post, "/v1/integrations", attrs)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def update(id, attrs)
|
|
23
|
-
@client.request(:put, "/
|
|
23
|
+
@client.request(:put, "/v1/integrations/#{id}", attrs)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def delete(id)
|
|
27
|
-
@client.request(:delete, "/
|
|
27
|
+
@client.request(:delete, "/v1/integrations/#{id}")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def test(id)
|
|
31
|
-
@client.request(:post, "/
|
|
31
|
+
@client.request(:post, "/v1/integrations/#{id}/test")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def list_events(id, params = {})
|
|
35
|
-
@client.request(:get, "/
|
|
35
|
+
@client.request(:get, "/v1/integrations/#{id}/events", params)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def get_stats(id)
|
|
39
|
-
@client.request(:get, "/
|
|
39
|
+
@client.request(:get, "/v1/integrations/#{id}/stats")
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -20,18 +20,5 @@ module HookSniff
|
|
|
20
20
|
)
|
|
21
21
|
AggregateEventTypesOut.deserialize(res)
|
|
22
22
|
end
|
|
23
|
-
|
|
24
|
-
def app_stats(app_id, options = {})
|
|
25
|
-
options = options.transform_keys(&:to_s)
|
|
26
|
-
res = @client.execute_request(
|
|
27
|
-
"GET",
|
|
28
|
-
"/v1/stats/app/#{app_id}",
|
|
29
|
-
query_params: {
|
|
30
|
-
"since" => options["since"],
|
|
31
|
-
"until" => options["until"]
|
|
32
|
-
}
|
|
33
|
-
)
|
|
34
|
-
res
|
|
35
|
-
end
|
|
36
23
|
end
|
|
37
24
|
end
|
data/lib/hooksniff/api/stream.rb
CHANGED
|
@@ -7,39 +7,58 @@ module HookSniff
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def list_channels
|
|
10
|
-
@client.request(:get, "/
|
|
10
|
+
@client.request(:get, "/v1/stream/channels")
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def get_channel(id)
|
|
14
|
-
@client.request(:get, "/
|
|
14
|
+
@client.request(:get, "/v1/stream/channels/#{id}")
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def create_channel(attrs)
|
|
18
|
-
@client.request(:post, "/
|
|
18
|
+
@client.request(:post, "/v1/stream/channels", attrs)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def update_channel(id, attrs)
|
|
22
|
-
@client.request(:put, "/
|
|
22
|
+
@client.request(:put, "/v1/stream/channels/#{id}", attrs)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def delete_channel(id)
|
|
26
|
-
@client.request(:delete, "/
|
|
26
|
+
@client.request(:delete, "/v1/stream/channels/#{id}")
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def list_messages(id, params = {})
|
|
30
|
-
@client.request(:get, "/
|
|
30
|
+
@client.request(:get, "/v1/stream/channels/#{id}/messages", params)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def list_subscriptions
|
|
34
|
-
@client.request(:get, "/
|
|
34
|
+
@client.request(:get, "/v1/stream/subscriptions")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def disconnect_subscription(id)
|
|
38
|
-
@client.request(:delete, "/
|
|
38
|
+
@client.request(:delete, "/v1/stream/subscriptions/#{id}")
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def publish(body)
|
|
42
|
-
@client.request(:post, "/
|
|
42
|
+
@client.request(:post, "/v1/stream/publish", body)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
# Subscribe to real-time events via SSE
|
|
48
|
+
# @param channel_id [String] Channel ID to subscribe to
|
|
49
|
+
# @param block [Block] Callback for each event
|
|
50
|
+
# @return [void]
|
|
51
|
+
def subscribe(channel_id, &block)
|
|
52
|
+
@client.request_stream(:get, "/v1/stream/channels/#{channel_id}/subscribe") do |event|
|
|
53
|
+
block.call(event)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Subscribe to delivery events (legacy SSE endpoint)
|
|
58
|
+
# @param block [Block] Callback for each event
|
|
59
|
+
# @return [void]
|
|
60
|
+
def subscribe_deliveries(&block)
|
|
61
|
+
@client.request_stream(:get, "/v1/stream/deliveries") do |event|
|
|
62
|
+
block.call(event)
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/hooksniff/errors.rb
CHANGED
|
@@ -41,6 +41,13 @@ module HookSniff
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# 408 Request Timeout
|
|
45
|
+
class RequestTimeoutError < ApiError
|
|
46
|
+
def initialize(response_headers: {}, response_body: nil)
|
|
47
|
+
super(code: 408, response_headers: response_headers, response_body: response_body)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
44
51
|
# 409 Conflict
|
|
45
52
|
class ConflictError < ApiError
|
|
46
53
|
def initialize(response_headers: {}, response_body: nil)
|
|
@@ -48,6 +55,20 @@ module HookSniff
|
|
|
48
55
|
end
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
# 410 Gone
|
|
59
|
+
class GoneError < ApiError
|
|
60
|
+
def initialize(response_headers: {}, response_body: nil)
|
|
61
|
+
super(code: 410, response_headers: response_headers, response_body: response_body)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 413 Payload Too Large
|
|
66
|
+
class PayloadTooLargeError < ApiError
|
|
67
|
+
def initialize(response_headers: {}, response_body: nil)
|
|
68
|
+
super(code: 413, response_headers: response_headers, response_body: response_body)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
51
72
|
# 422 Unprocessable Entity
|
|
52
73
|
class UnprocessableEntityError < ApiError
|
|
53
74
|
attr_reader :validation_errors
|
|
@@ -75,6 +96,13 @@ module HookSniff
|
|
|
75
96
|
end
|
|
76
97
|
end
|
|
77
98
|
|
|
99
|
+
# 501 Not Implemented
|
|
100
|
+
class NotImplementedError < ApiError
|
|
101
|
+
def initialize(response_headers: {}, response_body: nil)
|
|
102
|
+
super(code: 501, response_headers: response_headers, response_body: response_body)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
78
106
|
# 502 Bad Gateway
|
|
79
107
|
class BadGatewayError < ApiError
|
|
80
108
|
def initialize(response_headers: {}, response_body: nil)
|
|
@@ -96,6 +124,45 @@ module HookSniff
|
|
|
96
124
|
end
|
|
97
125
|
end
|
|
98
126
|
|
|
127
|
+
# 507 Insufficient Storage
|
|
128
|
+
class InsufficientStorageError < ApiError
|
|
129
|
+
def initialize(response_headers: {}, response_body: nil)
|
|
130
|
+
super(code: 507, response_headers: response_headers, response_body: response_body)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# 508 Loop Detected
|
|
135
|
+
class LoopDetectedError < ApiError
|
|
136
|
+
def initialize(response_headers: {}, response_body: nil)
|
|
137
|
+
super(code: 508, response_headers: response_headers, response_body: response_body)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Timeout — request exceeded the configured timeout
|
|
142
|
+
class TimeoutError < StandardError
|
|
143
|
+
attr_reader :code
|
|
144
|
+
def initialize(message = "Request timeout")
|
|
145
|
+
@code = 0
|
|
146
|
+
super(message)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Network error — connection failed
|
|
151
|
+
class NetworkError < StandardError
|
|
152
|
+
attr_reader :code
|
|
153
|
+
def initialize(message = "Network error")
|
|
154
|
+
@code = 0
|
|
155
|
+
super(message)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Authentication error — token invalid, expired, or missing
|
|
160
|
+
class AuthenticationError < ApiError
|
|
161
|
+
def initialize(response_headers: {}, response_body: nil)
|
|
162
|
+
super(code: 401, response_headers: response_headers, response_body: response_body)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
99
166
|
# Create the appropriate error from a status code
|
|
100
167
|
def self.create_error_from_status(status_code, response_headers: {}, response_body: nil)
|
|
101
168
|
case status_code
|
|
@@ -107,8 +174,14 @@ module HookSniff
|
|
|
107
174
|
ForbiddenError.new(response_headers: response_headers, response_body: response_body)
|
|
108
175
|
when 404
|
|
109
176
|
NotFoundError.new(response_headers: response_headers, response_body: response_body)
|
|
177
|
+
when 408
|
|
178
|
+
RequestTimeoutError.new(response_headers: response_headers, response_body: response_body)
|
|
110
179
|
when 409
|
|
111
180
|
ConflictError.new(response_headers: response_headers, response_body: response_body)
|
|
181
|
+
when 410
|
|
182
|
+
GoneError.new(response_headers: response_headers, response_body: response_body)
|
|
183
|
+
when 413
|
|
184
|
+
PayloadTooLargeError.new(response_headers: response_headers, response_body: response_body)
|
|
112
185
|
when 422
|
|
113
186
|
UnprocessableEntityError.new(response_headers: response_headers, response_body: response_body)
|
|
114
187
|
when 429
|
|
@@ -116,12 +189,18 @@ module HookSniff
|
|
|
116
189
|
RateLimitError.new(retry_after: retry_after, response_headers: response_headers, response_body: response_body)
|
|
117
190
|
when 500
|
|
118
191
|
InternalServerError.new(response_headers: response_headers, response_body: response_body)
|
|
192
|
+
when 501
|
|
193
|
+
NotImplementedError.new(response_headers: response_headers, response_body: response_body)
|
|
119
194
|
when 502
|
|
120
195
|
BadGatewayError.new(response_headers: response_headers, response_body: response_body)
|
|
121
196
|
when 503
|
|
122
197
|
ServiceUnavailableError.new(response_headers: response_headers, response_body: response_body)
|
|
123
198
|
when 504
|
|
124
199
|
GatewayTimeoutError.new(response_headers: response_headers, response_body: response_body)
|
|
200
|
+
when 507
|
|
201
|
+
InsufficientStorageError.new(response_headers: response_headers, response_body: response_body)
|
|
202
|
+
when 508
|
|
203
|
+
LoopDetectedError.new(response_headers: response_headers, response_body: response_body)
|
|
125
204
|
else
|
|
126
205
|
ApiError.new(code: status_code, response_headers: response_headers, response_body: response_body)
|
|
127
206
|
end
|
|
@@ -47,7 +47,9 @@ module HookSniff
|
|
|
47
47
|
# Create request object
|
|
48
48
|
request = request_class.new(uri.request_uri)
|
|
49
49
|
request["Authorization"] = "Bearer #{@token}"
|
|
50
|
-
|
|
50
|
+
sdk_ua = "hooksniff-libs/#{VERSION}/ruby"
|
|
51
|
+
request["User-Agent"] = sdk_ua
|
|
52
|
+
request["X-HookSniff-SDK"] = sdk_ua
|
|
51
53
|
request["hooksniff-req-id"] = rand(0...(2 ** 64))
|
|
52
54
|
|
|
53
55
|
# Add headers
|
|
@@ -83,26 +85,41 @@ module HookSniff
|
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
private def execute_request_with_retries(request, http)
|
|
86
|
-
res =
|
|
88
|
+
res = nil
|
|
89
|
+
retries = [1, 2, 4] # seconds: 1s, 2s, 4s exponential backoff
|
|
90
|
+
max_retries = retries.length
|
|
91
|
+
|
|
92
|
+
retries.each_with_index do |sleep_duration, index|
|
|
93
|
+
begin
|
|
94
|
+
res = http.request(request)
|
|
95
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Timeout::Error, Errno::ETIMEDOUT, IOError => _e
|
|
96
|
+
# Timeout — retry
|
|
97
|
+
if index < max_retries - 1
|
|
98
|
+
sleep(sleep_duration)
|
|
99
|
+
request["hooksniff-retry-count"] = index + 1
|
|
100
|
+
next
|
|
101
|
+
else
|
|
102
|
+
raise
|
|
103
|
+
end
|
|
104
|
+
end
|
|
87
105
|
|
|
88
|
-
[0.05, 0.1, 0.2].each_with_index do |sleep_duration, index|
|
|
89
106
|
# 429 Rate Limit — respect Retry-After header
|
|
90
107
|
if Integer(res.code) == 429
|
|
91
108
|
retry_after = res["Retry-After"]
|
|
92
109
|
delay = retry_after ? retry_after.to_f : sleep_duration
|
|
93
110
|
sleep(delay)
|
|
94
111
|
request["hooksniff-retry-count"] = index + 1
|
|
95
|
-
res = http.request(request)
|
|
96
112
|
next
|
|
97
113
|
end
|
|
98
114
|
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
# 5xx Server Error — exponential backoff
|
|
116
|
+
if Integer(res.code) >= 500 && index < max_retries - 1
|
|
117
|
+
sleep(sleep_duration)
|
|
118
|
+
request["hooksniff-retry-count"] = index + 1
|
|
119
|
+
next
|
|
101
120
|
end
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
request["hooksniff-retry-count"] = index + 1
|
|
105
|
-
res = http.request(request)
|
|
122
|
+
break
|
|
106
123
|
end
|
|
107
124
|
|
|
108
125
|
res
|
|
@@ -126,3 +143,25 @@ module HookSniff
|
|
|
126
143
|
end
|
|
127
144
|
end
|
|
128
145
|
end
|
|
146
|
+
|
|
147
|
+
# Response metadata accessor
|
|
148
|
+
module HookSniff
|
|
149
|
+
class << self
|
|
150
|
+
attr_accessor :last_response
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Debug logging helper
|
|
155
|
+
module HookSniff
|
|
156
|
+
module DebugLogger
|
|
157
|
+
def self.log_request(method, url)
|
|
158
|
+
return unless HookSniff.respond_to?(:debug) && HookSniff.debug
|
|
159
|
+
puts "[HookSniff] → #{method.upcase} #{url}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.log_response(status_code, elapsed_ms)
|
|
163
|
+
return unless HookSniff.respond_to?(:debug) && HookSniff.debug
|
|
164
|
+
puts "[HookSniff] ← #{status_code} (#{elapsed_ms}ms)"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HookSniff
|
|
4
|
+
# Configuration options for the HookSniff client.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# options = HookSniff::Options.new(
|
|
8
|
+
# server_url: "https://custom.hooksniff.com",
|
|
9
|
+
# timeout: 60,
|
|
10
|
+
# debug: true,
|
|
11
|
+
# headers: { "X-Custom" => "value" }
|
|
12
|
+
# )
|
|
13
|
+
# client = HookSniff::HookSniff.new("token", options)
|
|
14
|
+
class Options
|
|
15
|
+
attr_accessor :server_url, :timeout, :debug, :headers, :retry_schedule
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
server_url: nil,
|
|
19
|
+
timeout: 30,
|
|
20
|
+
debug: false,
|
|
21
|
+
headers: {},
|
|
22
|
+
retry_schedule: [1, 2, 4]
|
|
23
|
+
)
|
|
24
|
+
@server_url = server_url
|
|
25
|
+
@timeout = timeout
|
|
26
|
+
@debug = debug
|
|
27
|
+
@headers = headers || {}
|
|
28
|
+
@retry_schedule = retry_schedule
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Pagination Helper for HookSniff Ruby SDK.
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# hs.message.list_all(limit: 100).each do |msg|
|
|
5
|
+
# puts msg.id
|
|
6
|
+
# end
|
|
7
|
+
#
|
|
8
|
+
# # Or collect all
|
|
9
|
+
# all_messages = hs.message.list_all(limit: 100).to_a
|
|
10
|
+
|
|
11
|
+
module HookSniff
|
|
12
|
+
class Paginator
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
def initialize(fetch_page, limit: nil)
|
|
16
|
+
@fetch_page = fetch_page
|
|
17
|
+
@limit = limit
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def each(&block)
|
|
21
|
+
return enum_for(:each) unless block_given?
|
|
22
|
+
|
|
23
|
+
iterator = nil
|
|
24
|
+
|
|
25
|
+
loop do
|
|
26
|
+
page = @fetch_page.call(limit: @limit, iterator: iterator)
|
|
27
|
+
|
|
28
|
+
page.data.each(&block)
|
|
29
|
+
|
|
30
|
+
break if page.done || page.iterator.nil? || page.iterator.empty?
|
|
31
|
+
iterator = page.iterator
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Collect all items into an array
|
|
36
|
+
def to_a
|
|
37
|
+
each.to_a
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Count all items
|
|
41
|
+
def count
|
|
42
|
+
each.count
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HookSniff
|
|
4
|
+
# Response metadata from the last API request.
|
|
5
|
+
#
|
|
6
|
+
# Access via +client.last_response+ after any API call.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# endpoints = client.endpoint.list
|
|
10
|
+
# puts client.last_response.request_id
|
|
11
|
+
# puts client.last_response.rate_limit_remaining
|
|
12
|
+
class ResponseMetadata
|
|
13
|
+
# @return [Integer] HTTP status code
|
|
14
|
+
attr_reader :status_code
|
|
15
|
+
|
|
16
|
+
# @return [String, nil] x-request-id header
|
|
17
|
+
attr_reader :request_id
|
|
18
|
+
|
|
19
|
+
# @return [Integer, nil] x-ratelimit-remaining header
|
|
20
|
+
attr_reader :rate_limit_remaining
|
|
21
|
+
|
|
22
|
+
# @return [Integer, nil] x-ratelimit-reset header (Unix timestamp)
|
|
23
|
+
attr_reader :rate_limit_reset
|
|
24
|
+
|
|
25
|
+
# @return [Hash] All response headers
|
|
26
|
+
attr_reader :headers
|
|
27
|
+
|
|
28
|
+
def initialize(status_code:, request_id: nil, rate_limit_remaining: nil, rate_limit_reset: nil, headers: {})
|
|
29
|
+
@status_code = status_code
|
|
30
|
+
@request_id = request_id
|
|
31
|
+
@rate_limit_remaining = rate_limit_remaining
|
|
32
|
+
@rate_limit_reset = rate_limit_reset
|
|
33
|
+
@headers = headers
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Create from a Net::HTTP response.
|
|
37
|
+
def self.from_http_response(response)
|
|
38
|
+
headers = response.to_hash.transform_values { |v| v.is_a?(Array) ? v.first : v }
|
|
39
|
+
new(
|
|
40
|
+
status_code: response.code.to_i,
|
|
41
|
+
request_id: headers["x-request-id"],
|
|
42
|
+
rate_limit_remaining: headers["x-ratelimit-remaining"]&.to_i,
|
|
43
|
+
rate_limit_reset: headers["x-ratelimit-reset"]&.to_i,
|
|
44
|
+
headers: headers
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/hooksniff/version.rb
CHANGED