bitfab 0.38.2 → 0.40.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29901e7611834921e9378870830af40c08e5af02d1ca0708585a02b169b12e6e
4
- data.tar.gz: b42984a3710e9399159fd44af3aaac7b76e3155bd3121eaa5482aa27570fbb2e
3
+ metadata.gz: a178d05e7c40055c3ac8dec21a40eb60eb36b08a005dfa55d031c3c3b9cdf129
4
+ data.tar.gz: e71a921a9bad32b967c723245c1187257366073f13aab6f8dca2882d6b758fb8
5
5
  SHA512:
6
- metadata.gz: 576a44cbe7e2fdf7270f14af3d8a98541bb2e15cd2b02289c18d58f033688d6797b2b3a0cfc8153f1ba8adbf0d72f9b9b9241e1bfd5fbd3ffcc687a4cb9adb44
7
- data.tar.gz: ce3586fbdc541ffad387d87ca4299ee34df5e0c61a5c01a6b5112ee11084cb7900d6616f9e3db45ec0386fc019bc1712f267ea3c5799809880ae3611d3d30cb9
6
+ metadata.gz: 559fed18dd194a527ff477e057f465a172d7266ba05c4e6b3305a693091b0fa1de3847078d8f0df4c6822a4b28b2fd6a0f802eaf2db7d75e38233051f05fc887
7
+ data.tar.gz: f4a718c47568294809eaed27905b2d8b67a3058fc954eff2dac6b10a05ae2219463d1742b40bb0a7725d4979a5c86c2beb37e9e10621a588e007a9a7757d6376
data/lib/bitfab/client.rb CHANGED
@@ -8,11 +8,12 @@ require_relative "http_client"
8
8
  require_relative "replay"
9
9
  require_relative "span_context"
10
10
  require_relative "serialize"
11
+ require_relative "timestamp"
11
12
  require_relative "warn_once"
12
13
 
13
14
  module Bitfab
14
15
  class Client
15
- SPAN_TYPES = %w[llm agent function guardrail handoff custom].freeze
16
+ SPAN_TYPES = ["llm", "agent", "function", "guardrail", "handoff", "custom"].freeze
16
17
  UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
17
18
 
18
19
  # Sentinel returned by check_mock_replay when this span should run real
@@ -29,6 +30,9 @@ module Bitfab
29
30
 
30
31
  attr_reader :service_url
31
32
 
33
+ # Dataset operations for the authenticated organization.
34
+ attr_reader :datasets
35
+
32
36
  def initialize(api_key: nil, service_url: nil, enabled: true, strict: false)
33
37
  @api_key_config = api_key
34
38
  @service_url = service_url || DEFAULT_SERVICE_URL
@@ -42,6 +46,7 @@ module Bitfab
42
46
  # The key is NOT read here. HttpClient gets a proc so the key is resolved
43
47
  # at send time, after any env loading has run.
44
48
  @http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url)
49
+ @datasets = Datasets.new(@http_client)
45
50
  # Mock overrides registered via register_mock_override, applied to every
46
51
  # replay on this client (after any per-call mock_override). Instance
47
52
  # state, no global; clear_mock_overrides resets it.
@@ -156,10 +161,25 @@ module Bitfab
156
161
  name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, grader_ids: nil, mock: "marked",
157
162
  adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
158
163
  Replay.run(
159
- self, receiver, method_name,
160
- trace_function_key:, limit:, trace_ids:, name:, max_concurrency:,
161
- code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, grader_ids:, mock:, adapt_inputs:,
162
- mock_override:, db_branch:, on_item_start:, on_item_finish:,
164
+ self,
165
+ receiver,
166
+ method_name,
167
+ trace_function_key:,
168
+ limit:,
169
+ trace_ids:,
170
+ name:,
171
+ max_concurrency:,
172
+ code_change_description:,
173
+ code_change_files:,
174
+ experiment_group_id:,
175
+ dataset_id:,
176
+ grader_ids:,
177
+ mock:,
178
+ adapt_inputs:,
179
+ mock_override:,
180
+ db_branch:,
181
+ on_item_start:,
182
+ on_item_finish:,
163
183
  on_progress:
164
184
  )
165
185
  end
@@ -202,18 +222,18 @@ module Bitfab
202
222
  match: ->(node) { node[:trace_function_key] == trace_function_key && keyed_match.call(node) },
203
223
  value: keyed_override[:value]
204
224
  }
205
- return nil
225
+ return
206
226
  end
207
227
  if keyed_override.respond_to?(:call)
208
228
  @mock_overrides << {
209
229
  match: ->(node) { node[:trace_function_key] == trace_function_key },
210
230
  value: keyed_override
211
231
  }
212
- return nil
232
+ return
213
233
  end
214
234
  raise ArgumentError,
215
235
  "register_mock_override(trace_function_key, override) requires " \
216
- "a { match:, value: } hash or callable resolver."
236
+ "a { match:, value: } hash or callable resolver."
217
237
  elsif positional.length == 1 && positional[0].is_a?(Hash)
218
238
  override = positional[0]
219
239
  match = override[:match]
@@ -222,7 +242,7 @@ module Bitfab
222
242
  value = override.key?(:value) ? override[:value] : VALUE_UNSET
223
243
  elsif positional.length == 1 && positional[0].respond_to?(:call)
224
244
  @mock_overrides << {match: ->(_) { true }, value: positional[0]}
225
- return nil
245
+ return
226
246
  elsif positional.length == 2
227
247
  match, value = positional
228
248
  end
@@ -231,16 +251,16 @@ module Bitfab
231
251
  unless match.respond_to?(:call)
232
252
  raise ArgumentError,
233
253
  "register_mock_override requires a callable match. Pass (match, value) " \
234
- "positionally, as keywords (match:, value:), or as a { match:, value: } hash. " \
235
- "value may be a flat value or a callable."
254
+ "positionally, as keywords (match:, value:), or as a { match:, value: } hash. " \
255
+ "value may be a flat value or a callable."
236
256
  end
237
257
  # A forgotten value must not silently inject nil. An explicit nil is a
238
258
  # legitimate injected value and passes this guard.
239
259
  if value.equal?(VALUE_UNSET)
240
260
  raise ArgumentError,
241
261
  "register_mock_override requires a value (the second argument, or " \
242
- "value:). It may be a flat value or a callable; pass value: nil " \
243
- "explicitly to inject nil."
262
+ "value:). It may be a flat value or a callable; pass value: nil " \
263
+ "explicitly to inject nil."
244
264
  end
245
265
 
246
266
  @mock_overrides << {match:, value:}
@@ -280,12 +300,13 @@ module Bitfab
280
300
  def get_trace_span(trace_id, id: nil, name: nil, occurrence: "last")
281
301
  validate_trace_id(trace_id)
282
302
  raise ArgumentError, "Provide exactly one of id or name" if id.nil? == name.nil?
303
+
283
304
  validate_span_id(id) unless id.nil?
284
305
  if !name.nil? && (!name.is_a?(String) || name.empty?)
285
306
  raise ArgumentError, "name must be a non-empty string"
286
307
  end
287
308
 
288
- valid_occurrence = %w[first last].include?(occurrence) || (occurrence.is_a?(Integer) && occurrence >= 0)
309
+ valid_occurrence = ["first", "last"].include?(occurrence) || (occurrence.is_a?(Integer) && occurrence >= 0)
289
310
  unless valid_occurrence
290
311
  raise ArgumentError, 'occurrence must be "first", "last", or a non-negative integer'
291
312
  end
@@ -303,11 +324,11 @@ module Bitfab
303
324
  return yield unless tracing_enabled?
304
325
 
305
326
  resolved_capture_when = capture_when.to_s
306
- unless %w[always nested].include?(resolved_capture_when)
327
+ unless ["always", "nested"].include?(resolved_capture_when)
307
328
  Bitfab.warn_once(
308
329
  "invalid-capture-when:#{trace_function_key}",
309
330
  "unknown capture_when value #{capture_when.inspect}; defaulting to \"always\". " \
310
- "Valid values: \"always\", \"nested\"."
331
+ "Valid values: \"always\", \"nested\"."
311
332
  )
312
333
  resolved_capture_when = "always"
313
334
  end
@@ -333,7 +354,7 @@ module Bitfab
333
354
  span_id = SecureRandom.uuid
334
355
  parent_span_id = parent&.dig(:span_id)
335
356
  is_root_span = parent_span_id.nil?
336
- started_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
357
+ started_at = Bitfab.now_iso_timestamp
337
358
  resolved_test_run_id = replay_ctx&.dig(:test_run_id)
338
359
  resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
339
360
  resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
@@ -356,8 +377,14 @@ module Bitfab
356
377
  call_index = advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
357
378
  if call_index
358
379
  mock_result = check_mock_replay(
359
- replay_ctx, trace_function_key, span_name, call_index,
360
- span_type:, args:, kwargs:, mock_on_replay:
380
+ replay_ctx,
381
+ trace_function_key,
382
+ span_name,
383
+ call_index,
384
+ span_type:,
385
+ args:,
386
+ kwargs:,
387
+ mock_on_replay:
361
388
  )
362
389
  if mock_result != MOCK_REPLAY_MISS
363
390
  mocked_output, mock_source = mock_result
@@ -393,7 +420,7 @@ module Bitfab
393
420
  Bitfab.warn_once(
394
421
  "span-setup:#{trace_function_key}",
395
422
  "span setup failed for '#{trace_function_key}'; this call runs untraced. " \
396
- "Your method still executes and returns normally."
423
+ "Your method still executes and returns normally."
397
424
  )
398
425
  return yield
399
426
  end
@@ -409,10 +436,11 @@ module Bitfab
409
436
  # only the first call sends the span. Subsequent calls (e.g. from the
410
437
  # enumerator wrapper after iteration completes) are no-ops.
411
438
  next if finalized
439
+
412
440
  finalized = true
413
441
 
414
442
  begin
415
- ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
443
+ ended_at = Bitfab.now_iso_timestamp
416
444
 
417
445
  send_span(
418
446
  trace_function_key:,
@@ -550,7 +578,7 @@ module Bitfab
550
578
  end
551
579
  if @explicitly_enabled && !@api_key_warned
552
580
  @api_key_warned = true
553
- warn "Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing."
581
+ warn("Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing.")
554
582
  end
555
583
  nil
556
584
  end
@@ -689,7 +717,7 @@ module Bitfab
689
717
  # still rides out with dropped: true, so the server scrubs any sibling
690
718
  # spans that already raced out before the flag was set.
691
719
  trace_dropped = TraceState.get(trace_id)&.dig(:dropped) == true
692
- return nil if trace_dropped
720
+ return if trace_dropped
693
721
 
694
722
  # Human-readable JSON (input/output fields). The *_with_report variants
695
723
  # also report what could not be faithfully captured so a lossy span is
@@ -771,8 +799,8 @@ module Bitfab
771
799
  # mocked) so unmarked spans don't silently shift subsequent marked
772
800
  # spans' indices. Different (key, name) pairs have independent counters.
773
801
  def advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
774
- return nil if is_root_span
775
- return nil unless replay_ctx&.dig(:mock_tree)
802
+ return if is_root_span
803
+ return unless replay_ctx&.dig(:mock_tree)
776
804
 
777
805
  counters = replay_ctx[:call_counters]
778
806
  counter_key = "#{trace_function_key}:#{span_name}"
@@ -818,6 +846,7 @@ module Bitfab
818
846
  # output, wrap it in a callable value.
819
847
  unless value.respond_to?(:call)
820
848
  return [value, "override"] unless value.equal?(NO_MOCK_OVERRIDE)
849
+
821
850
  next
822
851
  end
823
852
 
@@ -825,6 +854,7 @@ module Bitfab
825
854
  unless mock_entry
826
855
  raise "No recorded span to source output for '#{trace_function_key}'."
827
856
  end
857
+
828
858
  resolve_recorded_output(mock_entry, replay_ctx)
829
859
  end
830
860
  resolved = value.call({node:, inputs: args, kwargs:, get_original_output:})
@@ -846,7 +876,7 @@ module Bitfab
846
876
  unless mock_entry
847
877
  raise StandardError,
848
878
  "Replay selected span '#{trace_function_key}:#{span_name}' for mocking, " \
849
- "but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
879
+ "but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
850
880
  end
851
881
 
852
882
  [resolve_recorded_output(mock_entry, replay_ctx), "recorded"]
@@ -889,7 +919,7 @@ module Bitfab
889
919
  def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
890
920
  span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
891
921
  started_at:, test_run_id:, input_source_span_id:, mock_source:)
892
- ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
922
+ ended_at = Bitfab.now_iso_timestamp
893
923
  send_span(
894
924
  trace_function_key:,
895
925
  trace_id:,
@@ -954,9 +984,13 @@ module Bitfab
954
984
  # mock strategy. A missing selected occurrence fails closed.
955
985
  def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
956
986
  Bitfab::Traceable.wrap(
957
- klass, method_name,
987
+ klass,
988
+ method_name,
958
989
  trace_function_key: @trace_function_key,
959
- name:, type:, capture_when:, mock_on_replay:,
990
+ name:,
991
+ type:,
992
+ capture_when:,
993
+ mock_on_replay:,
960
994
  client: @client
961
995
  )
962
996
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitfab
4
+ # Dataset operations for the authenticated organization, reached as
5
+ # `client.datasets`. A dataset is a named bucket of traces scoped to one
6
+ # trace function. Experiments replay against it and its graders score its
7
+ # members. Every method returns the parsed JSON response as a Hash with
8
+ # string keys matching the HTTP API ("traceCount", "addedTraceIds", ...).
9
+ class Datasets
10
+ DEFAULT_RERUN_TIMEOUT_SECONDS = 90
11
+ DEFAULT_RERUN_POLL_INTERVAL_SECONDS = 1
12
+ TERMINAL_RERUN_STATUSES = ["completed", "errored"].freeze
13
+
14
+ def initialize(http_client)
15
+ @http_client = http_client
16
+ end
17
+
18
+ # Create a dataset, or update the one already named this way under the
19
+ # same trace function. The result's "created" reports which happened. A
20
+ # nil description leaves an existing description untouched.
21
+ def save(trace_function_key:, name:, description: nil)
22
+ payload = {"traceFunctionKey" => trace_function_key, "name" => name}
23
+ payload["description"] = description unless description.nil?
24
+ @http_client.request("/api/sdk/datasets", payload)
25
+ end
26
+
27
+ # List datasets, scoped to one trace function when given and
28
+ # organization-wide otherwise.
29
+ def list(trace_function_key: nil)
30
+ query = trace_function_key.nil? ? "" : "?#{URI.encode_www_form("traceFunctionKey" => trace_function_key)}"
31
+ @http_client.get("/api/sdk/datasets#{query}")["datasets"]
32
+ end
33
+
34
+ # Fetch one dataset by id. A dataset outside this organization raises
35
+ # Net::HTTPError with a 404.
36
+ def get(dataset_id)
37
+ @http_client.get(dataset_path(dataset_id))["dataset"]
38
+ end
39
+
40
+ # The ids of every trace in the dataset, the same membership a replay with
41
+ # dataset_id: selects.
42
+ def list_traces(dataset_id)
43
+ @http_client.get(dataset_path(dataset_id, "/traces"))
44
+ end
45
+
46
+ # Add traces to the dataset (1 to 100 ids per call). Traces outside the
47
+ # organization or under another trace function are reported in
48
+ # "skippedTraceIds" rather than failing the call.
49
+ def add_traces(dataset_id, trace_ids)
50
+ @http_client.request(dataset_path(dataset_id, "/traces"), {"traceIds" => trace_ids})
51
+ end
52
+
53
+ # Remove traces from the dataset. The traces themselves are never deleted.
54
+ def remove_traces(dataset_id, trace_ids)
55
+ @http_client.request(dataset_path(dataset_id, "/removeTraces"), {"traceIds" => trace_ids})
56
+ end
57
+
58
+ # Assign graders to the dataset (1 to 100 ids per call). Graders outside
59
+ # the organization or under another trace function are reported in
60
+ # "skippedGraderIds" rather than failing the call.
61
+ def add_graders(dataset_id, grader_ids)
62
+ @http_client.request(dataset_path(dataset_id, "/graders"), {"graderIds" => grader_ids})
63
+ end
64
+
65
+ # Unassign graders from the dataset.
66
+ def remove_graders(dataset_id, grader_ids)
67
+ @http_client.request(dataset_path(dataset_id, "/removeGraders"), {"graderIds" => grader_ids})
68
+ end
69
+
70
+ # Re-run graders over every trace in the dataset. Defaults to every
71
+ # assigned grader, and an unassigned id is rejected. Waits for the run to
72
+ # finish (up to `timeout` seconds) unless `wait:` is false, and returns the
73
+ # last run state seen either way. A request matching an in-flight run
74
+ # joins it.
75
+ def rerun_graders(dataset_id, grader_ids: nil, wait: true,
76
+ timeout: DEFAULT_RERUN_TIMEOUT_SECONDS, poll_interval: DEFAULT_RERUN_POLL_INTERVAL_SECONDS)
77
+ payload = grader_ids.nil? ? {} : {"graderIds" => grader_ids}
78
+ started = @http_client.request(dataset_path(dataset_id, "/rerunGraders"), payload)
79
+ return started unless wait
80
+
81
+ deadline = monotonic_now + timeout
82
+ run = started["run"]
83
+ while !TERMINAL_RERUN_STATUSES.include?(run["status"]) && monotonic_now < deadline
84
+ sleep(poll_interval)
85
+ run = get_grader_rerun(dataset_id, run_id: run["id"]) || run
86
+ end
87
+ {"run" => run, "joinedExisting" => started["joinedExisting"]}
88
+ end
89
+
90
+ # The dataset's active grader re-run, or the run named by run_id:. Returns
91
+ # nil when nothing is active or the run is not this dataset's.
92
+ def get_grader_rerun(dataset_id, run_id: nil)
93
+ query = run_id.nil? ? "" : "?#{URI.encode_www_form("runId" => run_id)}"
94
+ @http_client.get(dataset_path(dataset_id, "/rerunGraders#{query}"))["run"]
95
+ end
96
+
97
+ private
98
+
99
+ def dataset_path(dataset_id, suffix = "")
100
+ "/api/sdk/datasets/#{URI.encode_www_form_component(dataset_id)}#{suffix}"
101
+ end
102
+
103
+ def monotonic_now
104
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
105
+ end
106
+ end
107
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "timestamp"
4
+
3
5
  module Bitfab
4
6
  # Handle to the current active span, allowing context to be added.
5
7
  class CurrentSpan
@@ -171,7 +173,7 @@ module Bitfab
171
173
  def create(trace_id, test_run_id: nil, input_source_trace_id: nil)
172
174
  @states_mutex.synchronize do
173
175
  @states[trace_id] ||= begin
174
- started_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
176
+ started_at = Bitfab.now_iso_timestamp
175
177
  {
176
178
  trace_id:,
177
179
  started_at:,
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Bitfab
6
+ @timestamp_mutex = Mutex.new
7
+ @last_timestamp_us = 0
8
+
9
+ def self.now_iso_timestamp
10
+ wall_clock_us = (Time.now.to_r * 1_000_000).to_i
11
+ timestamp_us = @timestamp_mutex.synchronize do
12
+ @last_timestamp_us = [wall_clock_us, @last_timestamp_us + 1].max
13
+ end
14
+ Time.at(Rational(timestamp_us, 1_000_000)).utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.38.2"
4
+ VERSION = "0.40.0"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "bitfab/serialize"
10
10
  require_relative "bitfab/db_snapshot"
11
11
  require_relative "bitfab/span_context"
12
12
  require_relative "bitfab/http_client"
13
+ require_relative "bitfab/datasets"
13
14
  require_relative "bitfab/mock_override"
14
15
  require_relative "bitfab/replay"
15
16
  require_relative "bitfab/replay_branch"
@@ -119,16 +120,16 @@ module Bitfab
119
120
  # whose source trace carried no DB snapshot reference
120
121
  def current_replay_branch
121
122
  ctx = ReplayContext.current
122
- return nil unless ctx
123
+ return unless ctx
123
124
 
124
125
  lease = ctx[:db_branch_lease]
125
- return nil unless lease
126
+ return unless lease
126
127
 
127
128
  # Surface the Bitfab trace ID (what the customer sees in the dashboard),
128
129
  # not the external one. Falling back to the external ID keeps replays from
129
130
  # external sources working until that path is fully wired.
130
131
  trace_id = ctx[:source_bitfab_trace_id] || ctx[:input_source_trace_id]
131
- return nil unless trace_id
132
+ return unless trace_id
132
133
 
133
134
  ReplayBranch.new(lease, trace_id, ctx)
134
135
  end
@@ -159,7 +160,7 @@ module Bitfab
159
160
  # succeeded, errored, and item (the object replay already passes to
160
161
  # on_item_finish)
161
162
  def report_replay_progress(progress)
162
- warn "#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}"
163
+ warn("#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}")
163
164
  rescue
164
165
  nil
165
166
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.38.2
4
+ version: 0.40.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -142,6 +142,7 @@ files:
142
142
  - lib/bitfab/client.rb
143
143
  - lib/bitfab/compress.rb
144
144
  - lib/bitfab/constants.rb
145
+ - lib/bitfab/datasets.rb
145
146
  - lib/bitfab/db_snapshot.rb
146
147
  - lib/bitfab/http_client.rb
147
148
  - lib/bitfab/mock_override.rb
@@ -153,6 +154,7 @@ files:
153
154
  - lib/bitfab/replay_registry.rb
154
155
  - lib/bitfab/serialize.rb
155
156
  - lib/bitfab/span_context.rb
157
+ - lib/bitfab/timestamp.rb
156
158
  - lib/bitfab/traceable.rb
157
159
  - lib/bitfab/transport.rb
158
160
  - lib/bitfab/version.rb