bitfab 0.9.0 → 0.10.5
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/lib/bitfab/client.rb +96 -22
- data/lib/bitfab/http_client.rb +20 -1
- data/lib/bitfab/replay.rb +41 -5
- data/lib/bitfab/span_context.rb +4 -3
- data/lib/bitfab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b789df310c91a7e9130f3bdb6a35e6237a8ddadede4df49a5f7dc4afc0525e91
|
|
4
|
+
data.tar.gz: 0cd79b9c9bc8bf15ef926625c0aa1aba8f3cf1798062c0d077e371d98b9f6aba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbf62eeabc5741c36ef1b867730f548039f20331d6591ccae7a66cc48882cd4638877edc1b80d57c3a326827edfbedff1d3ec226ed3b4e3358f9041e3405ea5f
|
|
7
|
+
data.tar.gz: 579cb384585a42160d5f1c9941cbf0762f53d35ffda7f109cb40c4c52a291a02d446e113ca664a2c84a08916481fe86912eb386a58956fb5651ff1a6dc108771
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -36,9 +36,18 @@ module Bitfab
|
|
|
36
36
|
# @param limit [Integer] maximum number of traces to replay (default: 5)
|
|
37
37
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to filter
|
|
38
38
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
39
|
+
# @param code_change_description [String, nil] optional rationale for the
|
|
40
|
+
# code change being tested in this replay (stored on the experiment)
|
|
41
|
+
# @param code_change_files [Array<Hash>, nil] optional list of edited files,
|
|
42
|
+
# each as { path:, before:, after: } (use "" for new/deleted files)
|
|
39
43
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
40
|
-
def replay(receiver, method_name, trace_function_key:, limit: 5, trace_ids: nil, max_concurrency: 10
|
|
41
|
-
|
|
44
|
+
def replay(receiver, method_name, trace_function_key:, limit: 5, trace_ids: nil, max_concurrency: 10,
|
|
45
|
+
code_change_description: nil, code_change_files: nil)
|
|
46
|
+
Replay.run(
|
|
47
|
+
self, receiver, method_name,
|
|
48
|
+
trace_function_key:, limit:, trace_ids:, max_concurrency:,
|
|
49
|
+
code_change_description:, code_change_files:
|
|
50
|
+
)
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
# Execute a block inside a span context, sending trace data on completion.
|
|
@@ -53,9 +62,13 @@ module Bitfab
|
|
|
53
62
|
is_root_span = parent_span_id.nil?
|
|
54
63
|
started_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
55
64
|
|
|
65
|
+
replay_ctx = ReplayContext.current
|
|
66
|
+
resolved_test_run_id = replay_ctx&.dig(:test_run_id)
|
|
67
|
+
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
68
|
+
|
|
56
69
|
# Register trace state for root spans
|
|
57
70
|
if is_root_span && !TraceState.get(trace_id)
|
|
58
|
-
TraceState.create(trace_id)
|
|
71
|
+
TraceState.create(trace_id, test_run_id: resolved_test_run_id)
|
|
59
72
|
end
|
|
60
73
|
|
|
61
74
|
if is_root_span
|
|
@@ -66,27 +79,18 @@ module Bitfab
|
|
|
66
79
|
error = nil
|
|
67
80
|
span_contexts = nil
|
|
68
81
|
span_prompt = nil
|
|
82
|
+
finalized = false
|
|
83
|
+
|
|
84
|
+
finalize = lambda do |final_result, final_error|
|
|
85
|
+
# Never crash the host app due to span building/sending. Idempotent —
|
|
86
|
+
# only the first call sends the span. Subsequent calls (e.g. from the
|
|
87
|
+
# enumerator wrapper after iteration completes) are no-ops.
|
|
88
|
+
next if finalized
|
|
89
|
+
finalized = true
|
|
69
90
|
|
|
70
|
-
begin
|
|
71
|
-
SpanContext.with_span(trace_id:, span_id:) do
|
|
72
|
-
result = yield
|
|
73
|
-
ensure
|
|
74
|
-
# Capture contexts before the span context is popped
|
|
75
|
-
span_contexts = SpanContext.current&.dig(:contexts)
|
|
76
|
-
span_prompt = SpanContext.current&.dig(:prompt)
|
|
77
|
-
end
|
|
78
|
-
rescue => e
|
|
79
|
-
error = e.message
|
|
80
|
-
raise
|
|
81
|
-
ensure
|
|
82
|
-
# Never crash the host app due to span building/sending
|
|
83
91
|
begin
|
|
84
92
|
ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
85
93
|
|
|
86
|
-
replay_ctx = ReplayContext.current
|
|
87
|
-
resolved_test_run_id = replay_ctx&.dig(:test_run_id)
|
|
88
|
-
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
89
|
-
|
|
90
94
|
span_thread = send_span(
|
|
91
95
|
trace_function_key:,
|
|
92
96
|
trace_id:,
|
|
@@ -99,8 +103,8 @@ module Bitfab
|
|
|
99
103
|
prompt: span_prompt,
|
|
100
104
|
args:,
|
|
101
105
|
kwargs:,
|
|
102
|
-
result
|
|
103
|
-
error
|
|
106
|
+
result: final_result,
|
|
107
|
+
error: final_error,
|
|
104
108
|
started_at:,
|
|
105
109
|
ended_at:,
|
|
106
110
|
test_run_id: resolved_test_run_id,
|
|
@@ -130,11 +134,78 @@ module Bitfab
|
|
|
130
134
|
end
|
|
131
135
|
end
|
|
132
136
|
|
|
137
|
+
begin
|
|
138
|
+
SpanContext.with_span(trace_id:, span_id:) do
|
|
139
|
+
result = yield
|
|
140
|
+
ensure
|
|
141
|
+
# Capture contexts before the span context is popped
|
|
142
|
+
span_contexts = SpanContext.current&.dig(:contexts)
|
|
143
|
+
span_prompt = SpanContext.current&.dig(:prompt)
|
|
144
|
+
end
|
|
145
|
+
rescue => e
|
|
146
|
+
error = e.message
|
|
147
|
+
finalize.call(result, error)
|
|
148
|
+
raise
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# If the wrapped block returned an Enumerator (lazy iteration via
|
|
152
|
+
# `enum_for`, `to_enum`, `Enumerator.new`, `[...].lazy.map(...)`, etc.),
|
|
153
|
+
# the work hasn't actually run yet — the values are produced as the
|
|
154
|
+
# caller iterates. Without special handling we'd close the span here
|
|
155
|
+
# with `result == <the Enumerator object>`, and any nested `bitfab_span`
|
|
156
|
+
# calls inside the enumerator body would see an empty span stack and
|
|
157
|
+
# post their own root traces, fragmenting one logical workflow.
|
|
158
|
+
#
|
|
159
|
+
# Instead, hand the caller a wrapping Enumerator whose body restores
|
|
160
|
+
# the parent span stack on the iterating fiber, drives the source,
|
|
161
|
+
# collects yielded values as the span output, and finalizes the span
|
|
162
|
+
# once iteration completes (or errors).
|
|
163
|
+
#
|
|
164
|
+
# Limitation: when the source enumerator itself runs its body in a
|
|
165
|
+
# separate fiber (e.g. `Enumerator.new { |y| ... }` or `enum_for(...)`
|
|
166
|
+
# without a block), nested `bitfab_span` calls inside that body fiber
|
|
167
|
+
# still see an empty stack because `Thread.current[STACK_KEY]` is
|
|
168
|
+
# fiber-local. Lazy chains over collections (`.lazy.map`) and ordinary
|
|
169
|
+
# `each` callbacks DO run in the iterating fiber and nest correctly.
|
|
170
|
+
if result.is_a?(Enumerator)
|
|
171
|
+
return wrap_enumerator(result, trace_id:, span_id:, finalize:)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
finalize.call(result, error)
|
|
133
175
|
result
|
|
134
176
|
end
|
|
135
177
|
|
|
136
178
|
private
|
|
137
179
|
|
|
180
|
+
# Build an Enumerator that drives `source`, restoring `[trace_id, span_id]`
|
|
181
|
+
# on the iterating fiber so nested `bitfab_span` calls inside lazy / `each`
|
|
182
|
+
# callbacks nest under the parent span. Yielded values are collected as the
|
|
183
|
+
# span output. The span is sent exactly once — when iteration finishes,
|
|
184
|
+
# raises, or the wrapper is `.close`d.
|
|
185
|
+
def wrap_enumerator(source, trace_id:, span_id:, finalize:)
|
|
186
|
+
span_entry = {trace_id:, span_id:}
|
|
187
|
+
yielded = []
|
|
188
|
+
|
|
189
|
+
Enumerator.new do |yielder|
|
|
190
|
+
# Push our span onto this fiber's stack so anything that runs in this
|
|
191
|
+
# fiber (including the user's lazy block and `each` callbacks) sees
|
|
192
|
+
# the right parent.
|
|
193
|
+
SpanContext.stack.push(span_entry)
|
|
194
|
+
begin
|
|
195
|
+
source.each do |value|
|
|
196
|
+
yielded << value
|
|
197
|
+
yielder << value
|
|
198
|
+
end
|
|
199
|
+
finalize.call(yielded, nil)
|
|
200
|
+
rescue => e
|
|
201
|
+
finalize.call(yielded, e.message)
|
|
202
|
+
raise
|
|
203
|
+
ensure
|
|
204
|
+
SpanContext.stack.pop
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
138
209
|
def validate_span_type!(type)
|
|
139
210
|
return if SPAN_TYPES.include?(type.to_s)
|
|
140
211
|
|
|
@@ -169,6 +240,9 @@ module Bitfab
|
|
|
169
240
|
if trace_state&.dig(:session_id)
|
|
170
241
|
payload["sessionId"] = trace_state[:session_id]
|
|
171
242
|
end
|
|
243
|
+
if trace_state&.dig(:test_run_id)
|
|
244
|
+
payload["testRunId"] = trace_state[:test_run_id]
|
|
245
|
+
end
|
|
172
246
|
|
|
173
247
|
@http_client.send_external_trace(payload)
|
|
174
248
|
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -98,12 +98,19 @@ module Bitfab
|
|
|
98
98
|
|
|
99
99
|
# Start a replay session by fetching historical traces.
|
|
100
100
|
# Blocking call. Returns hash with testRunId, testRunUrl, and items array.
|
|
101
|
-
|
|
101
|
+
#
|
|
102
|
+
# @param code_change_description [String, nil] optional rationale for the
|
|
103
|
+
# code change being tested in this replay
|
|
104
|
+
# @param code_change_files [Array<Hash>, nil] optional list of edited files,
|
|
105
|
+
# each as { path:, before:, after: } (use "" for new/deleted files)
|
|
106
|
+
def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil, code_change_files: nil)
|
|
102
107
|
payload = {
|
|
103
108
|
"traceFunctionKey" => trace_function_key,
|
|
104
109
|
"limit" => limit
|
|
105
110
|
}
|
|
106
111
|
payload["traceIds"] = trace_ids if trace_ids
|
|
112
|
+
payload["codeChangeDescription"] = code_change_description unless code_change_description.nil?
|
|
113
|
+
payload["codeChangeFiles"] = normalize_code_change_files(code_change_files) unless code_change_files.nil?
|
|
107
114
|
|
|
108
115
|
request("/api/sdk/replay/start", payload, timeout: 30)
|
|
109
116
|
end
|
|
@@ -129,6 +136,18 @@ module Bitfab
|
|
|
129
136
|
|
|
130
137
|
private
|
|
131
138
|
|
|
139
|
+
# Normalize each entry to a hash with stable string keys, accepting either
|
|
140
|
+
# symbol-keyed or string-keyed hashes from callers.
|
|
141
|
+
def normalize_code_change_files(files)
|
|
142
|
+
files.map do |file|
|
|
143
|
+
{
|
|
144
|
+
"path" => file[:path] || file["path"],
|
|
145
|
+
"before" => file[:before] || file["before"],
|
|
146
|
+
"after" => file[:after] || file["after"]
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
132
151
|
def headers
|
|
133
152
|
{
|
|
134
153
|
"Content-Type" => "application/json",
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -42,11 +42,22 @@ module Bitfab
|
|
|
42
42
|
# @param limit [Integer] maximum number of traces to replay (default: 5)
|
|
43
43
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to filter
|
|
44
44
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
45
|
+
# @param code_change_description [String, nil] optional rationale for the
|
|
46
|
+
# code change being tested in this replay (stored on the experiment)
|
|
47
|
+
# @param code_change_files [Array<Hash>, nil] optional list of edited files,
|
|
48
|
+
# each as { path:, before:, after: } (empty string for new/deleted files)
|
|
45
49
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
46
|
-
def run(client, receiver, method_name, trace_function_key:, limit: 5, trace_ids: nil, max_concurrency: 10
|
|
50
|
+
def run(client, receiver, method_name, trace_function_key:, limit: 5, trace_ids: nil, max_concurrency: 10,
|
|
51
|
+
code_change_description: nil, code_change_files: nil)
|
|
47
52
|
http_client = client.instance_variable_get(:@http_client)
|
|
48
53
|
|
|
49
|
-
replay_data = http_client.start_replay(
|
|
54
|
+
replay_data = http_client.start_replay(
|
|
55
|
+
trace_function_key,
|
|
56
|
+
limit,
|
|
57
|
+
trace_ids:,
|
|
58
|
+
code_change_description:,
|
|
59
|
+
code_change_files:
|
|
60
|
+
)
|
|
50
61
|
test_run_id = replay_data["testRunId"]
|
|
51
62
|
test_run_url = replay_data["testRunUrl"]
|
|
52
63
|
server_items = replay_data["items"] || []
|
|
@@ -105,7 +116,8 @@ module Bitfab
|
|
|
105
116
|
def process_single_item(http_client, server_item, receiver, method_name, test_run_id)
|
|
106
117
|
span = http_client.get_external_span(server_item["externalSpanId"])
|
|
107
118
|
item_data = extract_span_data(span)
|
|
108
|
-
|
|
119
|
+
metrics = extract_server_item_metrics(server_item)
|
|
120
|
+
execute_item(item_data, receiver, method_name, test_run_id, span["id"], metrics)
|
|
109
121
|
end
|
|
110
122
|
|
|
111
123
|
# Extract input/output data from an external span's rawData.
|
|
@@ -121,8 +133,29 @@ module Bitfab
|
|
|
121
133
|
}
|
|
122
134
|
end
|
|
123
135
|
|
|
136
|
+
# Pull durationMs / tokens / model from the start-replay server item.
|
|
137
|
+
# Normalizes to symbol-keyed tokens hash and nil-safe defaults so older
|
|
138
|
+
# servers without these fields still produce a consistent shape.
|
|
139
|
+
def extract_server_item_metrics(server_item)
|
|
140
|
+
raw_tokens = server_item["tokens"]
|
|
141
|
+
tokens = if raw_tokens.is_a?(Hash)
|
|
142
|
+
{
|
|
143
|
+
input: raw_tokens["input"],
|
|
144
|
+
output: raw_tokens["output"],
|
|
145
|
+
cached: raw_tokens["cached"],
|
|
146
|
+
total: raw_tokens["total"]
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
duration_ms: server_item["durationMs"],
|
|
152
|
+
tokens:,
|
|
153
|
+
model: server_item["model"]
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
124
157
|
# Execute a single replay item: deserialize inputs, call method with replay context.
|
|
125
|
-
def execute_item(item, receiver, method_name, test_run_id, input_source_span_id = nil)
|
|
158
|
+
def execute_item(item, receiver, method_name, test_run_id, input_source_span_id = nil, metrics = {})
|
|
126
159
|
args, kwargs = Serialize.deserialize_inputs(item)
|
|
127
160
|
|
|
128
161
|
fn_result = nil
|
|
@@ -142,7 +175,10 @@ module Bitfab
|
|
|
142
175
|
input: args,
|
|
143
176
|
result: fn_result,
|
|
144
177
|
original_output: item["output"],
|
|
145
|
-
error: fn_error
|
|
178
|
+
error: fn_error,
|
|
179
|
+
duration_ms: metrics[:duration_ms],
|
|
180
|
+
tokens: metrics[:tokens],
|
|
181
|
+
model: metrics[:model]
|
|
146
182
|
}
|
|
147
183
|
end
|
|
148
184
|
end
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -131,12 +131,13 @@ module Bitfab
|
|
|
131
131
|
@states_mutex.synchronize { @states[trace_id] }
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
def create(trace_id)
|
|
134
|
+
def create(trace_id, test_run_id: nil)
|
|
135
135
|
@states_mutex.synchronize do
|
|
136
136
|
@states[trace_id] ||= {
|
|
137
137
|
trace_id:,
|
|
138
|
-
started_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
139
|
-
|
|
138
|
+
started_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ"),
|
|
139
|
+
test_run_id:
|
|
140
|
+
}.compact
|
|
140
141
|
end
|
|
141
142
|
end
|
|
142
143
|
|
data/lib/bitfab/version.rb
CHANGED