billdogeng 1.0.2.pre.beta.3
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/README.md +173 -0
- data/lib/billdogeng/analytics.rb +530 -0
- data/lib/billdogeng/client.rb +272 -0
- data/lib/billdogeng/errors.rb +20 -0
- data/lib/billdogeng/flags.rb +518 -0
- data/lib/billdogeng/llm.rb +59 -0
- data/lib/billdogeng/messaging.rb +44 -0
- data/lib/billdogeng/murmur.rb +86 -0
- data/lib/billdogeng/quota_limited.rb +52 -0
- data/lib/billdogeng/surveys.rb +117 -0
- data/lib/billdogeng/targeting.rb +278 -0
- data/lib/billdogeng/transport.rb +182 -0
- data/lib/billdogeng/version.rb +5 -0
- data/lib/billdogeng.rb +37 -0
- metadata +90 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "zlib"
|
|
7
|
+
require "stringio"
|
|
8
|
+
|
|
9
|
+
require_relative "errors"
|
|
10
|
+
|
|
11
|
+
module BilldogEng
|
|
12
|
+
# Thin HTTP client around Net::HTTP with:
|
|
13
|
+
# - per-request timeout
|
|
14
|
+
# - optional gzip request bodies (only when large enough to benefit)
|
|
15
|
+
# - exponential backoff retry (1s, 2s, 4s …) for 5xx / 429 / network errors
|
|
16
|
+
# - unwrapping of the `{ success, data, meta }` envelope (required on 2xx)
|
|
17
|
+
class Transport
|
|
18
|
+
GZIP_THRESHOLD_BYTES = 1024 # only compress bodies large enough to benefit
|
|
19
|
+
|
|
20
|
+
# @param host [String] base URL, e.g. "https://api.billdog.io/v1"
|
|
21
|
+
# @param request_timeout [Integer] per-request timeout in milliseconds
|
|
22
|
+
# @param gzip [Boolean] gzip large request bodies
|
|
23
|
+
# @param max_retries [Integer] retry attempts for transient failures
|
|
24
|
+
# @param enable_logging [Boolean] emit verbose diagnostics
|
|
25
|
+
# @param http_adapter [#call] optional injectable HTTP fn (for tests):
|
|
26
|
+
# `->(method, url, headers, body) { [status_int, body_string] }`
|
|
27
|
+
def initialize(host:, request_timeout:, gzip:, max_retries:, enable_logging:, http_adapter: nil)
|
|
28
|
+
@host = host.to_s.sub(%r{/+\z}, "")
|
|
29
|
+
@request_timeout = request_timeout
|
|
30
|
+
@gzip = gzip
|
|
31
|
+
@max_retries = max_retries
|
|
32
|
+
@enable_logging = enable_logging
|
|
33
|
+
@http_adapter = http_adapter
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Perform a POST (or GET when body is nil) request with retry/backoff.
|
|
37
|
+
# Returns the parsed JSON body, unwrapped from the standard envelope.
|
|
38
|
+
# Raises BilldogEng::Error after the final retry fails.
|
|
39
|
+
#
|
|
40
|
+
# @param path [String] path appended to host, e.g. "/ingest-events"
|
|
41
|
+
# @param body [Object] request body (serialized to JSON), or nil for GET
|
|
42
|
+
# @param headers [Hash] extra headers (auth supplied per-call)
|
|
43
|
+
# @param gzip [Boolean] override: gzip this request (only if config.gzip)
|
|
44
|
+
def request(path:, body: nil, headers: {}, gzip: true)
|
|
45
|
+
url = "#{@host}#{path}"
|
|
46
|
+
max_attempts = @max_retries + 1
|
|
47
|
+
last_error = nil
|
|
48
|
+
|
|
49
|
+
max_attempts.times do |attempt|
|
|
50
|
+
if attempt.positive?
|
|
51
|
+
backoff = 1.0 * (2**(attempt - 1)) # 1s, 2s, 4s …
|
|
52
|
+
log("retry #{attempt}/#{@max_retries} after #{backoff}s -> #{path}")
|
|
53
|
+
sleep(backoff)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
return attempt_request(url, body, headers, gzip)
|
|
58
|
+
rescue BilldogEng::Error => e
|
|
59
|
+
last_error = e
|
|
60
|
+
raise e if !retryable?(e.status) || attempt == max_attempts - 1
|
|
61
|
+
|
|
62
|
+
log("request failed (status #{e.status}), will retry: #{e.message}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
raise(last_error || BilldogEng::Error.new("request failed", status: 0))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def log(*args)
|
|
72
|
+
warn("[BilldogEng] #{args.join(" ")}") if @enable_logging
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Status codes that are safe to retry (transient).
|
|
76
|
+
def retryable?(status)
|
|
77
|
+
status.zero? || status == 408 || status == 429 || (status >= 500 && status <= 599)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def attempt_request(url, body, headers, gzip_opt)
|
|
81
|
+
req_headers = {
|
|
82
|
+
"Content-Type" => "application/json",
|
|
83
|
+
"User-Agent" => "billdogeng-ruby/#{BilldogEng::VERSION}",
|
|
84
|
+
}.merge(headers)
|
|
85
|
+
|
|
86
|
+
payload = nil
|
|
87
|
+
unless body.nil?
|
|
88
|
+
json = JSON.generate(body)
|
|
89
|
+
use_gzip = @gzip && gzip_opt != false && json.bytesize >= GZIP_THRESHOLD_BYTES
|
|
90
|
+
if use_gzip
|
|
91
|
+
req_headers["Content-Encoding"] = "gzip"
|
|
92
|
+
payload = gzip_compress(json)
|
|
93
|
+
else
|
|
94
|
+
payload = json
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
method = body.nil? ? "GET" : "POST"
|
|
99
|
+
status, text = perform(method, url, req_headers, payload)
|
|
100
|
+
|
|
101
|
+
parsed = parse_body(text)
|
|
102
|
+
|
|
103
|
+
unless status >= 200 && status < 300
|
|
104
|
+
# The nested error envelope: `{ success: false, error: { code, message } }`.
|
|
105
|
+
error = parsed.is_a?(Hash) && parsed["error"].is_a?(Hash) ? parsed["error"] : nil
|
|
106
|
+
message = (error && error["message"]) || "HTTP #{status}"
|
|
107
|
+
code = error && error["code"]
|
|
108
|
+
raise BilldogEng::Error.new(message, status: status, code: code, body: parsed)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
unwrap_envelope(parsed, status)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns [status_int, body_string].
|
|
115
|
+
def perform(method, url, headers, payload)
|
|
116
|
+
return @http_adapter.call(method, url, headers, payload) if @http_adapter
|
|
117
|
+
|
|
118
|
+
uri = URI.parse(url)
|
|
119
|
+
timeout_s = @request_timeout / 1000.0
|
|
120
|
+
|
|
121
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
122
|
+
http.use_ssl = uri.scheme == "https"
|
|
123
|
+
http.open_timeout = timeout_s
|
|
124
|
+
http.read_timeout = timeout_s
|
|
125
|
+
http.write_timeout = timeout_s if http.respond_to?(:write_timeout=)
|
|
126
|
+
|
|
127
|
+
request =
|
|
128
|
+
if method == "GET"
|
|
129
|
+
Net::HTTP::Get.new(uri.request_uri)
|
|
130
|
+
else
|
|
131
|
+
Net::HTTP::Post.new(uri.request_uri)
|
|
132
|
+
end
|
|
133
|
+
headers.each { |k, v| request[k] = v }
|
|
134
|
+
request.body = payload if payload
|
|
135
|
+
|
|
136
|
+
begin
|
|
137
|
+
response = http.request(request)
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
# Network failure / timeout → status 0, retryable.
|
|
140
|
+
raise BilldogEng::Error.new("network error: #{e.message}", status: 0, code: "NETWORK_ERROR")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
[response.code.to_i, response.body.to_s]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def parse_body(text)
|
|
147
|
+
return nil if text.nil? || text.empty?
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
JSON.parse(text)
|
|
151
|
+
rescue JSON::ParserError
|
|
152
|
+
text
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Every endpoint answers the standard `{ success, data, meta }` envelope, and
|
|
157
|
+
# `data` is the payload. Anything else is not a BillDog answer and is refused,
|
|
158
|
+
# never read as a payload.
|
|
159
|
+
def unwrap_envelope(parsed, status)
|
|
160
|
+
# A quota refusal rides at the TOP level of the body, deliberately outside the
|
|
161
|
+
# envelope and with no `data`, so never unwrap past one: the caller has to see
|
|
162
|
+
# `quota_limited` to know the 200 it just got discarded its payload.
|
|
163
|
+
return parsed if parsed.is_a?(Hash) && parsed.key?("quota_limited")
|
|
164
|
+
|
|
165
|
+
return parsed["data"] if parsed.is_a?(Hash) && parsed["success"] == true && parsed.key?("data")
|
|
166
|
+
|
|
167
|
+
raise BilldogEng::Error.new(
|
|
168
|
+
"malformed response: expected the { success, data } envelope",
|
|
169
|
+
status: status, code: "MALFORMED_RESPONSE", body: parsed
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def gzip_compress(str)
|
|
174
|
+
io = StringIO.new
|
|
175
|
+
io.set_encoding(Encoding::BINARY)
|
|
176
|
+
gz = Zlib::GzipWriter.new(io)
|
|
177
|
+
gz.write(str)
|
|
178
|
+
gz.close
|
|
179
|
+
io.string
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/billdogeng.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# billdogeng — Official BilldogEng server SDK for Ruby.
|
|
4
|
+
#
|
|
5
|
+
# Engagement suite: Analytics + Feature Flags (remote + local eval) + Surveys +
|
|
6
|
+
# Messaging + LLM observability.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# require "billdogeng"
|
|
10
|
+
# bd = BilldogEng.new("bd_test_xxx", local_evaluation: true)
|
|
11
|
+
# bd.capture("user-123", "order_completed", { revenue: 49.99 })
|
|
12
|
+
# bd.shutdown
|
|
13
|
+
|
|
14
|
+
require_relative "billdogeng/version"
|
|
15
|
+
require_relative "billdogeng/errors"
|
|
16
|
+
require_relative "billdogeng/quota_limited"
|
|
17
|
+
require_relative "billdogeng/murmur"
|
|
18
|
+
require_relative "billdogeng/targeting"
|
|
19
|
+
require_relative "billdogeng/transport"
|
|
20
|
+
require_relative "billdogeng/analytics"
|
|
21
|
+
require_relative "billdogeng/flags"
|
|
22
|
+
require_relative "billdogeng/surveys"
|
|
23
|
+
require_relative "billdogeng/messaging"
|
|
24
|
+
require_relative "billdogeng/llm"
|
|
25
|
+
require_relative "billdogeng/client"
|
|
26
|
+
|
|
27
|
+
module BilldogEng
|
|
28
|
+
# Convenience constructor: `BilldogEng.new(api_key, **opts)` → BilldogEng::Client.
|
|
29
|
+
def self.new(api_key, opts = {})
|
|
30
|
+
Client.new(api_key, opts)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Expose the bucketing primitive at the top level for cross-platform parity use.
|
|
34
|
+
def self.murmurhash3(key, seed = 0)
|
|
35
|
+
Murmur.murmurhash3(key, seed)
|
|
36
|
+
end
|
|
37
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: billdogeng
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.2.pre.beta.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- BillDog
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.13'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: webrick
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.8'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.8'
|
|
41
|
+
description: 'BilldogEng engagement suite for server-side Ruby: Analytics, Feature
|
|
42
|
+
Flags (remote + local evaluation), Surveys (data API), Messaging dispatch, and LLM
|
|
43
|
+
observability.'
|
|
44
|
+
email:
|
|
45
|
+
- sdk@billdog.io
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- README.md
|
|
51
|
+
- lib/billdogeng.rb
|
|
52
|
+
- lib/billdogeng/analytics.rb
|
|
53
|
+
- lib/billdogeng/client.rb
|
|
54
|
+
- lib/billdogeng/errors.rb
|
|
55
|
+
- lib/billdogeng/flags.rb
|
|
56
|
+
- lib/billdogeng/llm.rb
|
|
57
|
+
- lib/billdogeng/messaging.rb
|
|
58
|
+
- lib/billdogeng/murmur.rb
|
|
59
|
+
- lib/billdogeng/quota_limited.rb
|
|
60
|
+
- lib/billdogeng/surveys.rb
|
|
61
|
+
- lib/billdogeng/targeting.rb
|
|
62
|
+
- lib/billdogeng/transport.rb
|
|
63
|
+
- lib/billdogeng/version.rb
|
|
64
|
+
homepage: https://billdog.io
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
homepage_uri: https://billdog.io
|
|
69
|
+
source_code_uri: https://github.com/billdog/billdogeng-ruby
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
post_install_message:
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 2.6.0
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">"
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 1.3.1
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 3.0.3.1
|
|
87
|
+
signing_key:
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: Official BilldogEng server SDK for Ruby.
|
|
90
|
+
test_files: []
|