bitfab 0.51.1 → 0.51.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 +4 -4
- data/README.md +51 -0
- data/lib/bitfab/client.rb +120 -21
- data/lib/bitfab/commit_ref.rb +184 -0
- data/lib/bitfab/git_command.rb +65 -0
- data/lib/bitfab/replay.rb +2 -14
- data/lib/bitfab/span_context.rb +6 -2
- data/lib/bitfab/subtree.rb +774 -0
- data/lib/bitfab/traceable.rb +253 -6
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 571e505294f7f5abe3b1d4c8521c4427cf30c105e03eb87509e70dd929559f43
|
|
4
|
+
data.tar.gz: 2a39fa504e22027f19a845efb156fb6a7c38d4a1187b7df64dad1623b5cccb4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4146889a3a1f02b3bca7a104810067d59ec1e138cd099dc4822d241bcf34a6cb1514535d0bb902628e18a990bb31b5b1c79074cb946bdae7416bad3121a877c
|
|
7
|
+
data.tar.gz: ab9ab7427fb6065d4a6c1748b780a60d12b70302971b474927741653da400f60deb8a739addb5eb60ef79e7e9c791922538c86cbe53f163a1eb69c4dd175b87b
|
data/README.md
CHANGED
|
@@ -215,6 +215,51 @@ end
|
|
|
215
215
|
# └─ check_fraud (grandchild)
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
### Experimental Subtree Tracing
|
|
219
|
+
|
|
220
|
+
Use `bitfab_trace` to record a root method and every first-party Ruby method it calls without wrapping each descendant:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
class OrderPipeline
|
|
224
|
+
include Bitfab::Traceable
|
|
225
|
+
bitfab_function "order-pipeline"
|
|
226
|
+
|
|
227
|
+
bitfab_trace :process, type: "agent"
|
|
228
|
+
def process(order_id)
|
|
229
|
+
validate(order_id)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def validate(order_id)
|
|
233
|
+
{ order_id:, valid: true }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting `max_spans` stops new capture but allows already-open spans in every Fiber to finish. `max_depth:`, `max_spans:`, `exclude:`, and `include_wrappers:` control the subtree. Automatically captured calls are not replay mock targets. Declare a call with `bitfab_span` when replay must substitute it: its explicit span records once, and automatic descendants continue beneath it.
|
|
239
|
+
|
|
240
|
+
When capture ends, already-open automatic calls in suspended Fibers are emitted once with a nil output and `Subtree capture ended before the call returned` as the error, unless a real error was already observed. Their inputs and parenting are retained. The SDK does not resume Fibers, and later resumption cannot add spans to the ended capture. If hot reload moves the root to an unresolved source, the last valid first-party boundary and its exclusions remain in use.
|
|
241
|
+
|
|
242
|
+
Returning an `Enumerator` keeps capture pending until iteration finishes or raises, including calls already paused in child Fibers. Capture is inactive between the initial return and consumption. Automatic spans record lazy return values as placeholders without iterating them; the traced root records the yielded values.
|
|
243
|
+
|
|
244
|
+
A nested `bitfab_trace` with a different key starts an independent trace while the outer trace continues recording the overlapping subtree. Both traces have distinct trace and span IDs, including during replay, and the complete structure each root would record alone, so two active roots double span volume in their shared region. A recursive call or nested annotation using the same key stays in the active trace instead of creating another root.
|
|
245
|
+
|
|
246
|
+
Configure a discovered method with `bitfab_node`. A node never creates a span or trace by itself; the active `bitfab_trace` owns its key, limits, parent, and lifecycle:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
bitfab_node :summarize,
|
|
250
|
+
name: "Model call",
|
|
251
|
+
type: "llm",
|
|
252
|
+
mock_on_replay: true,
|
|
253
|
+
finalize: ->(response) { {text: response.text} }
|
|
254
|
+
def summarize(order)
|
|
255
|
+
model.generate(order)
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Nodes support `name:`, `type:`, `capture:`, `test_run_id:`, `mock_on_replay:`, and `finalize:`. `capture: false` omits the method while attaching captured descendants to its nearest visible parent. `finalize:` changes only the recorded output, never the return value. A finalizer failure is recorded on the node without crashing the caller. The before-`def`, inline, and after-`def` forms all preserve method visibility. For external code, use `Bitfab.client.node(MyClass, :method)`.
|
|
260
|
+
|
|
261
|
+
`bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After `max_spans` is exhausted, it stops collecting metadata and values immediately, then stops processing events for that session once its captured calls finish. Each active root still captures and serializes its own copy of every overlapping call, so this remains a discovery tool rather than the production default. Prefer explicit `bitfab_span` declarations on hot paths. Run `mise exec -- ruby -Ilib benchmark/subtree.rb` from this package to measure the no-network caller path on your Ruby build.
|
|
262
|
+
|
|
218
263
|
### Input/Output Capture
|
|
219
264
|
|
|
220
265
|
Positional and keyword arguments are automatically captured:
|
|
@@ -399,6 +444,8 @@ batch is never re-encoded to measure its size. If Bitfab accepts only part of a
|
|
|
399
444
|
the standard OTLP `partialSuccess` response and the SDK warns with the rejected-span count and
|
|
400
445
|
reason.
|
|
401
446
|
|
|
447
|
+
Every root trace also records the commit its code ran at as `commit_ref`. Deploy platforms such as Vercel, GitHub Actions, Railway, Render, Heroku, Cloudflare Pages, GitLab CI, Azure Pipelines, and CircleCI supply it through their build variables, a plain checkout resolves it from `git` once per process in the background, and a container built without either should set `BITFAB_COMMIT_SHA` at build time. Set `BITFAB_DISABLE_COMMIT_REF` to send no `commit_ref` at all.
|
|
448
|
+
|
|
402
449
|
Before finalizing a replay, the SDK flushes OTel and polls Bitfab's replay-status API until every
|
|
403
450
|
expected trace completion and span count is persisted.
|
|
404
451
|
|
|
@@ -425,6 +472,10 @@ See [bitfab-ruby-example/](../bitfab-ruby-example/) for complete working example
|
|
|
425
472
|
|
|
426
473
|
- [test_span.rb](../bitfab-ruby-example/scripts/test_span.rb) - Basic span creation
|
|
427
474
|
- [test_nested_spans.rb](../bitfab-ruby-example/scripts/test_nested_spans.rb) - Nested span hierarchies
|
|
475
|
+
- [test_subtree.rb](../bitfab-ruby-example/scripts/test_subtree.rb) - Whole-subtree capture with independent overlapping roots
|
|
476
|
+
- [test_subtree_comprehensive.rb](../bitfab-ruby-example/scripts/test_subtree_comprehensive.rb) - Self-checking subtree and node behavior examples, including child-Fiber context and installed application gems
|
|
477
|
+
- [test_subtree_replay.rb](../bitfab-ruby-example/scripts/test_subtree_replay.rb) - Live subtree record and replay
|
|
478
|
+
- [subtree coverage map](../bitfab-ruby-example/subtree_examples/README.md) - Every subtree spec behavior mapped to its runnable example
|
|
428
479
|
- [test_wrap.rb](../bitfab-ruby-example/scripts/test_wrap.rb) - Wrapping external code
|
|
429
480
|
- [test_enabled.rb](../bitfab-ruby-example/scripts/test_enabled.rb) - Using the enabled flag
|
|
430
481
|
- [test_metadata.rb](../bitfab-ruby-example/scripts/test_metadata.rb) - Custom metadata
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "constants"
|
|
|
7
7
|
require_relative "http_client"
|
|
8
8
|
require_relative "replay"
|
|
9
9
|
require_relative "span_context"
|
|
10
|
+
require_relative "subtree"
|
|
10
11
|
require_relative "serialize"
|
|
11
12
|
require_relative "timestamp"
|
|
12
13
|
require_relative "warn_once"
|
|
@@ -51,6 +52,7 @@ module Bitfab
|
|
|
51
52
|
# replay on this client (after any per-call mock_override). Instance
|
|
52
53
|
# state, no global; clear_mock_overrides resets it.
|
|
53
54
|
@mock_overrides = []
|
|
55
|
+
CommitRef.start_resolution
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
# The configured API key (a proc is resolved on read). Reflects what was
|
|
@@ -302,6 +304,23 @@ module Bitfab
|
|
|
302
304
|
BitfabFunction.new(self, trace_function_key)
|
|
303
305
|
end
|
|
304
306
|
|
|
307
|
+
# Configure an existing method only when it is discovered beneath a
|
|
308
|
+
# bitfab_trace root owned by this client.
|
|
309
|
+
def node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
310
|
+
test_run_id: nil, mock_on_replay: false, finalize: nil)
|
|
311
|
+
Bitfab::Traceable.node(
|
|
312
|
+
klass,
|
|
313
|
+
method_name,
|
|
314
|
+
name:,
|
|
315
|
+
type:,
|
|
316
|
+
capture:,
|
|
317
|
+
test_run_id:,
|
|
318
|
+
mock_on_replay:,
|
|
319
|
+
finalize:,
|
|
320
|
+
client: self
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
305
324
|
# Fetch one persisted span without loading the full trace.
|
|
306
325
|
# Exactly one of id or name is required. Name lookups return the last
|
|
307
326
|
# matching span by default; occurrence also accepts "first" or a zero-based
|
|
@@ -326,7 +345,8 @@ module Bitfab
|
|
|
326
345
|
# Execute a block inside a span context, sending trace data on completion.
|
|
327
346
|
# Called by Traceable, not intended for direct use.
|
|
328
347
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
|
|
329
|
-
capture_when: "always", mock_on_replay: false
|
|
348
|
+
capture_when: "always", mock_on_replay: false, independent_root: false,
|
|
349
|
+
explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil)
|
|
330
350
|
# Decide at CALL time, not construction. The key may be set after the
|
|
331
351
|
# client is built (env loaded later), so re-checking per call lets a
|
|
332
352
|
# late-resolved key take effect.
|
|
@@ -358,13 +378,24 @@ module Bitfab
|
|
|
358
378
|
resolved_input_source_span_id = nil
|
|
359
379
|
begin
|
|
360
380
|
replay_ctx = ReplayContext.current
|
|
361
|
-
|
|
362
|
-
|
|
381
|
+
# A subtree annotation declares a trace-function root, even when it is
|
|
382
|
+
# invoked inside another span. Outer subtree sessions keep observing it
|
|
383
|
+
# through TracePoint instead of owning this root span.
|
|
384
|
+
ambient_parent = SpanContext.current || ReplayContext.current_span
|
|
385
|
+
parent = independent_root ? nil : ambient_parent
|
|
386
|
+
if parent && (subtree_parent_span_id = Subtree.current_parent_span_id(parent[:trace_id]))
|
|
387
|
+
parent = parent.merge(span_id: subtree_parent_span_id)
|
|
388
|
+
end
|
|
389
|
+
trace_id = if independent_root && ambient_parent
|
|
390
|
+
SecureRandom.uuid
|
|
391
|
+
else
|
|
392
|
+
parent&.dig(:trace_id) || replay_ctx&.dig(:trace_id) || SecureRandom.uuid
|
|
393
|
+
end
|
|
363
394
|
span_id = SecureRandom.uuid
|
|
364
395
|
parent_span_id = parent&.dig(:span_id)
|
|
365
396
|
is_root_span = parent_span_id.nil?
|
|
366
397
|
started_at = Bitfab.now_iso_timestamp
|
|
367
|
-
resolved_test_run_id = replay_ctx
|
|
398
|
+
resolved_test_run_id = replay_ctx ? replay_ctx[:test_run_id] : test_run_id
|
|
368
399
|
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
369
400
|
resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
|
|
370
401
|
|
|
@@ -438,18 +469,28 @@ module Bitfab
|
|
|
438
469
|
error = nil
|
|
439
470
|
span_contexts = nil
|
|
440
471
|
span_prompt = nil
|
|
441
|
-
|
|
472
|
+
completed = false
|
|
442
473
|
|
|
443
|
-
|
|
474
|
+
complete = lambda do |final_result, final_error|
|
|
444
475
|
# Never crash the host app due to span building/sending. Idempotent:
|
|
445
476
|
# only the first call sends the span. Subsequent calls (e.g. from the
|
|
446
477
|
# enumerator wrapper after iteration completes) are no-ops.
|
|
447
|
-
next if
|
|
478
|
+
next if completed
|
|
448
479
|
|
|
449
|
-
|
|
480
|
+
completed = true
|
|
450
481
|
|
|
451
482
|
begin
|
|
452
483
|
ended_at = Bitfab.now_iso_timestamp
|
|
484
|
+
recorded_result = final_result
|
|
485
|
+
recorded_error = final_error
|
|
486
|
+
if finalize && final_error.nil?
|
|
487
|
+
begin
|
|
488
|
+
recorded_result = finalize.call(final_result)
|
|
489
|
+
rescue => e
|
|
490
|
+
recorded_result = nil
|
|
491
|
+
recorded_error = "finalize failed: #{e.message}"
|
|
492
|
+
end
|
|
493
|
+
end
|
|
453
494
|
|
|
454
495
|
send_span(
|
|
455
496
|
trace_function_key:,
|
|
@@ -463,8 +504,8 @@ module Bitfab
|
|
|
463
504
|
prompt: span_prompt,
|
|
464
505
|
args:,
|
|
465
506
|
kwargs:,
|
|
466
|
-
result:
|
|
467
|
-
error:
|
|
507
|
+
result: recorded_result,
|
|
508
|
+
error: recorded_error,
|
|
468
509
|
started_at:,
|
|
469
510
|
ended_at:,
|
|
470
511
|
test_run_id: resolved_test_run_id,
|
|
@@ -513,8 +554,22 @@ module Bitfab
|
|
|
513
554
|
|
|
514
555
|
begin
|
|
515
556
|
ReplayContext.with_span(trace_id:, span_id:) do
|
|
516
|
-
SpanContext.with_span(
|
|
517
|
-
|
|
557
|
+
SpanContext.with_span(
|
|
558
|
+
trace_id:,
|
|
559
|
+
span_id:,
|
|
560
|
+
explicit_span_receiver:,
|
|
561
|
+
explicit_span_method_name: explicit_span_method_name&.to_sym
|
|
562
|
+
) do
|
|
563
|
+
result = if explicit_span_receiver
|
|
564
|
+
Subtree.with_borrowed_span(
|
|
565
|
+
trace_id:,
|
|
566
|
+
span_id:,
|
|
567
|
+
receiver: explicit_span_receiver,
|
|
568
|
+
method_name: explicit_span_method_name
|
|
569
|
+
) { yield }
|
|
570
|
+
else
|
|
571
|
+
yield
|
|
572
|
+
end
|
|
518
573
|
ensure
|
|
519
574
|
# Capture contexts before the span context is popped
|
|
520
575
|
span_contexts = SpanContext.current&.dig(:contexts)
|
|
@@ -523,7 +578,7 @@ module Bitfab
|
|
|
523
578
|
end
|
|
524
579
|
rescue => e
|
|
525
580
|
error = e.message
|
|
526
|
-
|
|
581
|
+
complete.call(result, error)
|
|
527
582
|
raise
|
|
528
583
|
end
|
|
529
584
|
|
|
@@ -541,10 +596,17 @@ module Bitfab
|
|
|
541
596
|
# once iteration completes (or errors).
|
|
542
597
|
#
|
|
543
598
|
if result.is_a?(Enumerator)
|
|
544
|
-
return wrap_enumerator(
|
|
599
|
+
return wrap_enumerator(
|
|
600
|
+
result,
|
|
601
|
+
trace_id:,
|
|
602
|
+
span_id:,
|
|
603
|
+
complete:,
|
|
604
|
+
explicit_span_receiver:,
|
|
605
|
+
explicit_span_method_name:
|
|
606
|
+
)
|
|
545
607
|
end
|
|
546
608
|
|
|
547
|
-
|
|
609
|
+
complete.call(result, error)
|
|
548
610
|
result
|
|
549
611
|
end
|
|
550
612
|
|
|
@@ -605,8 +667,13 @@ module Bitfab
|
|
|
605
667
|
# callbacks nest under the parent span. Yielded values are collected as the
|
|
606
668
|
# span output. The span is sent exactly once: when iteration finishes,
|
|
607
669
|
# raises, or the wrapper is `.close`d.
|
|
608
|
-
def wrap_enumerator(source, trace_id:, span_id:,
|
|
670
|
+
def wrap_enumerator(source, trace_id:, span_id:, complete:, explicit_span_receiver: nil,
|
|
671
|
+
explicit_span_method_name: nil)
|
|
609
672
|
span_entry = {trace_id:, span_id:}
|
|
673
|
+
if explicit_span_receiver
|
|
674
|
+
span_entry[:explicit_span_receiver] = explicit_span_receiver
|
|
675
|
+
span_entry[:explicit_span_method_name] = explicit_span_method_name&.to_sym
|
|
676
|
+
end
|
|
610
677
|
yielded = []
|
|
611
678
|
|
|
612
679
|
Enumerator.new do |yielder|
|
|
@@ -618,13 +685,26 @@ module Bitfab
|
|
|
618
685
|
SpanContext.with_fiber_bridge(trace_id:, span_id:) do
|
|
619
686
|
SpanContext.stack.push(span_entry)
|
|
620
687
|
begin
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
688
|
+
iterate = lambda do
|
|
689
|
+
source.each do |value|
|
|
690
|
+
yielded << value
|
|
691
|
+
yielder << value
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
if explicit_span_receiver
|
|
695
|
+
Subtree.with_borrowed_span(
|
|
696
|
+
trace_id:,
|
|
697
|
+
span_id:,
|
|
698
|
+
receiver: explicit_span_receiver,
|
|
699
|
+
method_name: explicit_span_method_name,
|
|
700
|
+
&iterate
|
|
701
|
+
)
|
|
702
|
+
else
|
|
703
|
+
iterate.call
|
|
624
704
|
end
|
|
625
|
-
|
|
705
|
+
complete.call(yielded, nil)
|
|
626
706
|
rescue => e
|
|
627
|
-
|
|
707
|
+
complete.call(yielded, e.message)
|
|
628
708
|
raise
|
|
629
709
|
ensure
|
|
630
710
|
SpanContext.stack.pop
|
|
@@ -673,6 +753,8 @@ module Bitfab
|
|
|
673
753
|
if trace_state&.dig(:db_snapshot_ref)
|
|
674
754
|
raw_trace["db_snapshot_ref"] = trace_state[:db_snapshot_ref]
|
|
675
755
|
end
|
|
756
|
+
commit_ref = CommitRef.current
|
|
757
|
+
raw_trace["commit_ref"] = commit_ref if commit_ref
|
|
676
758
|
if db_snapshot_usage
|
|
677
759
|
usage = {"neon_branch_id" => db_snapshot_usage[:neon_branch_id]}
|
|
678
760
|
if db_snapshot_usage[:snapshot_timestamp]
|
|
@@ -1006,5 +1088,22 @@ module Bitfab
|
|
|
1006
1088
|
client: @client
|
|
1007
1089
|
)
|
|
1008
1090
|
end
|
|
1091
|
+
|
|
1092
|
+
# Experimentally trace an existing method and its first-party call subtree.
|
|
1093
|
+
def trace(klass, method_name, name: nil, type: "custom",
|
|
1094
|
+
max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
|
|
1095
|
+
exclude: [], include_wrappers: false)
|
|
1096
|
+
Bitfab::Traceable.trace(
|
|
1097
|
+
klass, method_name,
|
|
1098
|
+
trace_function_key: @trace_function_key,
|
|
1099
|
+
name:,
|
|
1100
|
+
type:,
|
|
1101
|
+
max_depth:,
|
|
1102
|
+
max_spans:,
|
|
1103
|
+
exclude:,
|
|
1104
|
+
include_wrappers:,
|
|
1105
|
+
client: @client
|
|
1106
|
+
)
|
|
1107
|
+
end
|
|
1009
1108
|
end
|
|
1010
1109
|
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require_relative "git_command"
|
|
5
|
+
|
|
6
|
+
module Bitfab
|
|
7
|
+
module CommitRef
|
|
8
|
+
EXPLICIT_SHA_ENV = "BITFAB_COMMIT_SHA"
|
|
9
|
+
DISABLE_ENV = "BITFAB_DISABLE_COMMIT_REF"
|
|
10
|
+
GIT_TIMEOUT_SECONDS = 2
|
|
11
|
+
|
|
12
|
+
VERCEL_PROVIDER_HOSTS = {
|
|
13
|
+
"github" => "github.com",
|
|
14
|
+
"gitlab" => "gitlab.com",
|
|
15
|
+
"bitbucket" => "bitbucket.org"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
PLATFORM_ENVS = [
|
|
19
|
+
["VERCEL_GIT_COMMIT_SHA", "VERCEL_GIT_COMMIT_REF", :vercel],
|
|
20
|
+
["GITHUB_SHA", "GITHUB_REF_NAME", ["GITHUB_SERVER_URL", "GITHUB_REPOSITORY"]],
|
|
21
|
+
["RAILWAY_GIT_COMMIT_SHA", "RAILWAY_GIT_BRANCH", nil],
|
|
22
|
+
["RENDER_GIT_COMMIT", "RENDER_GIT_BRANCH", nil],
|
|
23
|
+
["SOURCE_VERSION", nil, nil],
|
|
24
|
+
["CF_PAGES_COMMIT_SHA", "CF_PAGES_BRANCH", nil],
|
|
25
|
+
["CI_COMMIT_SHA", "CI_COMMIT_REF_NAME", ["CI_REPOSITORY_URL"]],
|
|
26
|
+
["BUILD_SOURCEVERSION", "BUILD_SOURCEBRANCHNAME", ["BUILD_REPOSITORY_URI"]],
|
|
27
|
+
["CIRCLE_SHA1", "CIRCLE_BRANCH", ["CIRCLE_REPOSITORY_URL"]]
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
REMOTE_ENVS = %w[
|
|
31
|
+
GITHUB_SERVER_URL GITHUB_REPOSITORY CI_REPOSITORY_URL BUILD_REPOSITORY_URI
|
|
32
|
+
CIRCLE_REPOSITORY_URL VERCEL_GIT_PROVIDER VERCEL_GIT_REPO_OWNER VERCEL_GIT_REPO_SLUG
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
ENV_NAMES = ([EXPLICIT_SHA_ENV, DISABLE_ENV] + PLATFORM_ENVS.flat_map { |sha, branch, _| [sha, branch].compact } + REMOTE_ENVS).freeze
|
|
36
|
+
|
|
37
|
+
@lock = Mutex.new
|
|
38
|
+
@resolved = false
|
|
39
|
+
@ref = nil
|
|
40
|
+
@git_thread = nil
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def read(env, name)
|
|
45
|
+
return nil if name.nil?
|
|
46
|
+
|
|
47
|
+
value = env[name].to_s.strip
|
|
48
|
+
value.empty? ? nil : value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def remote_from_env(env, spec)
|
|
52
|
+
case spec
|
|
53
|
+
when :vercel
|
|
54
|
+
host = VERCEL_PROVIDER_HOSTS[read(env, "VERCEL_GIT_PROVIDER").to_s.downcase]
|
|
55
|
+
owner = read(env, "VERCEL_GIT_REPO_OWNER")
|
|
56
|
+
slug = read(env, "VERCEL_GIT_REPO_SLUG")
|
|
57
|
+
(host && owner && slug) ? "#{host}/#{owner}/#{slug}" : nil
|
|
58
|
+
when Array
|
|
59
|
+
parts = spec.map { |name| read(env, name) }
|
|
60
|
+
parts.all? ? normalize_remote(parts.join("/")) : nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def normalize_remote(raw)
|
|
65
|
+
value = raw.to_s.strip
|
|
66
|
+
return nil if value.empty?
|
|
67
|
+
|
|
68
|
+
if value.include?("://")
|
|
69
|
+
parsed = begin
|
|
70
|
+
URI.parse(value)
|
|
71
|
+
rescue URI::InvalidURIError
|
|
72
|
+
return nil
|
|
73
|
+
end
|
|
74
|
+
host = parsed.host.to_s.downcase
|
|
75
|
+
path = parsed.path.to_s
|
|
76
|
+
else
|
|
77
|
+
head, separator, tail = value.partition(":")
|
|
78
|
+
host = separator.empty? ? "" : head.rpartition("@").last
|
|
79
|
+
path = separator.empty? ? value : tail
|
|
80
|
+
end
|
|
81
|
+
path = path.gsub(%r{\A/+|/+\z}, "")
|
|
82
|
+
path = path.delete_suffix(".git").sub(%r{/+\z}, "") if path.end_with?(".git")
|
|
83
|
+
return (path.empty? ? nil : path) if host.empty?
|
|
84
|
+
|
|
85
|
+
path.empty? ? host : "#{host}/#{path}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve_from_env(env)
|
|
89
|
+
explicit = read(env, EXPLICIT_SHA_ENV)
|
|
90
|
+
platform = PLATFORM_ENVS.find { |sha_env, _, _| read(env, sha_env) }
|
|
91
|
+
if platform.nil?
|
|
92
|
+
return nil if explicit.nil?
|
|
93
|
+
|
|
94
|
+
return {"sha" => explicit, "branch" => nil, "dirty" => nil, "remote" => nil, "root_sha" => nil}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
sha_env, branch_env, remote_spec = platform
|
|
98
|
+
{
|
|
99
|
+
"sha" => explicit || read(env, sha_env),
|
|
100
|
+
"branch" => read(env, branch_env),
|
|
101
|
+
"dirty" => nil,
|
|
102
|
+
"remote" => remote_from_env(env, remote_spec),
|
|
103
|
+
"root_sha" => nil
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def git(cwd, *args)
|
|
108
|
+
GitCommand.run(cwd, args, timeout: GIT_TIMEOUT_SECONDS)&.strip
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def resolve_from_git(cwd)
|
|
112
|
+
sha = git(cwd, "rev-parse", "HEAD")
|
|
113
|
+
return nil if sha.nil? || sha.empty?
|
|
114
|
+
|
|
115
|
+
branch = git(cwd, "symbolic-ref", "--short", "-q", "HEAD")
|
|
116
|
+
status = git(cwd, "status", "--porcelain")
|
|
117
|
+
roots = git(cwd, "rev-list", "--max-parents=0", "HEAD")
|
|
118
|
+
{
|
|
119
|
+
"sha" => sha,
|
|
120
|
+
"branch" => (branch.nil? || branch.empty?) ? nil : branch,
|
|
121
|
+
"dirty" => status.nil? ? nil : !status.empty?,
|
|
122
|
+
"remote" => normalize_remote(git(cwd, "remote", "get-url", "origin")),
|
|
123
|
+
"root_sha" => (roots.nil? || roots.empty?) ? nil : roots.split.min
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def start_resolution
|
|
128
|
+
@lock.synchronize do
|
|
129
|
+
return if @resolved || @git_thread
|
|
130
|
+
|
|
131
|
+
if read(ENV, DISABLE_ENV)
|
|
132
|
+
@resolved = true
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
from_env = resolve_from_env(ENV)
|
|
137
|
+
if from_env
|
|
138
|
+
@ref = from_env
|
|
139
|
+
@resolved = true
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
cwd = begin
|
|
144
|
+
Dir.pwd
|
|
145
|
+
rescue SystemCallError
|
|
146
|
+
@resolved = true
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
@git_thread = Thread.new { resolve_git_in_background(cwd) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def resolve_git_in_background(cwd)
|
|
154
|
+
ref = resolve_from_git(cwd)
|
|
155
|
+
@lock.synchronize do
|
|
156
|
+
unless @resolved
|
|
157
|
+
@ref = ref
|
|
158
|
+
@resolved = true
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
rescue
|
|
162
|
+
@lock.synchronize { @resolved = true }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def current
|
|
166
|
+
start_resolution
|
|
167
|
+
@lock.synchronize { @ref }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def settle(timeout = 5)
|
|
171
|
+
thread = @lock.synchronize { @git_thread }
|
|
172
|
+
thread&.join(timeout)
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def reset!
|
|
177
|
+
@lock.synchronize do
|
|
178
|
+
@ref = nil
|
|
179
|
+
@resolved = false
|
|
180
|
+
@git_thread = nil
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
module GitCommand
|
|
7
|
+
KILL_GRACE_SECONDS = 1
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def run(cwd, args, timeout:)
|
|
12
|
+
Open3.popen3("git", *args, chdir: cwd, **spawn_options) do |stdin, stdout, stderr, wait_thread|
|
|
13
|
+
stdin.close
|
|
14
|
+
out = +""
|
|
15
|
+
reader = Thread.new { out = read_all(stdout) }
|
|
16
|
+
drain = Thread.new { read_all(stderr) }
|
|
17
|
+
finished = wait_thread.join(timeout)
|
|
18
|
+
stop(wait_thread) unless finished
|
|
19
|
+
settle(reader, stdout)
|
|
20
|
+
settle(drain, stderr)
|
|
21
|
+
(finished && wait_thread.value.success?) ? out.force_encoding("UTF-8").scrub : nil
|
|
22
|
+
end
|
|
23
|
+
rescue
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def spawn_options
|
|
28
|
+
Gem.win_platform? ? {} : {pgroup: true}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def kill_target(wait_thread)
|
|
32
|
+
Gem.win_platform? ? wait_thread.pid : -wait_thread.pid
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read_all(io)
|
|
36
|
+
io.read.to_s
|
|
37
|
+
rescue
|
|
38
|
+
""
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def settle(thread, io)
|
|
42
|
+
return if thread.join(KILL_GRACE_SECONDS)
|
|
43
|
+
|
|
44
|
+
io.close
|
|
45
|
+
thread.join
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def stop(wait_thread)
|
|
49
|
+
target = kill_target(wait_thread)
|
|
50
|
+
if Gem.win_platform?
|
|
51
|
+
signal("KILL", target)
|
|
52
|
+
else
|
|
53
|
+
signal("TERM", target)
|
|
54
|
+
signal("KILL", target) unless wait_thread.join(KILL_GRACE_SECONDS)
|
|
55
|
+
end
|
|
56
|
+
wait_thread.join
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def signal(name, target)
|
|
60
|
+
Process.kill(name, target)
|
|
61
|
+
rescue
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "open3"
|
|
|
6
6
|
require "timeout"
|
|
7
7
|
|
|
8
8
|
require_relative "constants"
|
|
9
|
+
require_relative "git_command"
|
|
9
10
|
require_relative "mock_override"
|
|
10
11
|
require_relative "serialize"
|
|
11
12
|
require_relative "traceable"
|
|
@@ -560,20 +561,7 @@ module Bitfab
|
|
|
560
561
|
end
|
|
561
562
|
|
|
562
563
|
def git(cwd, args)
|
|
563
|
-
|
|
564
|
-
# leaked to the host process's console.
|
|
565
|
-
out, _err, status = Timeout.timeout(30) do
|
|
566
|
-
Open3.capture3("git", *args, chdir: cwd)
|
|
567
|
-
end
|
|
568
|
-
return nil unless status.success?
|
|
569
|
-
|
|
570
|
-
# Tag as UTF-8 and scrub invalid bytes so blob contents compare cleanly
|
|
571
|
-
# against the UTF-8 working-file reads and never break JSON.generate later
|
|
572
|
-
# (a non-UTF-8 file that slips past the null-byte check would otherwise
|
|
573
|
-
# abort the whole start-replay request).
|
|
574
|
-
out.force_encoding("UTF-8").scrub
|
|
575
|
-
rescue
|
|
576
|
-
nil
|
|
564
|
+
GitCommand.run(cwd, args, timeout: 30)
|
|
577
565
|
end
|
|
578
566
|
|
|
579
567
|
def cc_ref_exists?(root, ref)
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -133,7 +133,7 @@ module Bitfab
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def current
|
|
136
|
-
stack.last || fiber_bridge_stack.last
|
|
136
|
+
stack.last || Subtree.fiber_context&.inherited_span || fiber_bridge_stack.last
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
# Enumerator.new and enum_for run their source body in another Fiber on
|
|
@@ -149,8 +149,12 @@ module Bitfab
|
|
|
149
149
|
|
|
150
150
|
# Execute a block with a new span pushed onto the stack.
|
|
151
151
|
# The span is automatically popped when the block completes.
|
|
152
|
-
def with_span(trace_id:, span_id:)
|
|
152
|
+
def with_span(trace_id:, span_id:, explicit_span_receiver: nil, explicit_span_method_name: nil)
|
|
153
153
|
entry = {trace_id:, span_id:}
|
|
154
|
+
if explicit_span_receiver
|
|
155
|
+
entry[:explicit_span_receiver] = explicit_span_receiver
|
|
156
|
+
entry[:explicit_span_method_name] = explicit_span_method_name
|
|
157
|
+
end
|
|
154
158
|
stack.push(entry)
|
|
155
159
|
yield
|
|
156
160
|
ensure
|