bitfab 0.17.0 → 0.17.1

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: 60fa4998513b5fa9f67950bf4abbbc253b324955732e3726531ca47856544ddb
4
- data.tar.gz: ad45f27ff4c50a4a75329f48e6ba06f252eb74e425b1d19686b55f6c780ae6a0
3
+ metadata.gz: 9dd280a2cd6afdf47b2bbbf62508dd4918dccf42c83ed276845f592497168601
4
+ data.tar.gz: 224fc7cfe39e847f3bfd56e9876151ea356f7a8ad7bff66b529f9704ab360141
5
5
  SHA512:
6
- metadata.gz: 1dd29ae11022f8cc0ad80b3d59e37d809843b85ab9cb48e0623afd01ee1a2d6b4e99e8ba91c996b7cad1acb589757ccf4a0291673a28d7a54eccffa098d5239e
7
- data.tar.gz: efaf095342fee4995aa8d45c69773c7d268f5eb23e358606292c7dd3ba11a3958da7416606025f18c8c11b43c295b62ef0bcd80baf753c0070055ae0d3dcc0f3
6
+ metadata.gz: 4cba57762a9ae927ba34ac70fdd1d750d118a9f8f3cc47402f30d8a09c336337f27cef601f8b36e63140e01acaa410d1a58450f160cb1c3d7e7c2c5b1a198cb3
7
+ data.tar.gz: eaa0a0b2eed0c0f7c0e24a73e2128540c53a504e9a3dd4d6858355e8076aa2fa8a4913ea170b278ac1579c5e407d4333759e1bae345dbe575b9fa91b666275a1
data/lib/bitfab/client.rb CHANGED
@@ -26,7 +26,7 @@ module Bitfab
26
26
  @service_url = service_url || DEFAULT_SERVICE_URL
27
27
  @enabled = enabled
28
28
  if @enabled && (@api_key.nil? || @api_key.to_s.strip.empty?)
29
- warn "Bitfab: api_key is empty tracing is disabled. Provide a valid API key to enable tracing."
29
+ warn "Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing."
30
30
  @enabled = false
31
31
  end
32
32
  @http_client = HttpClient.new(api_key:, service_url: @service_url)
@@ -87,7 +87,7 @@ module Bitfab
87
87
  end
88
88
 
89
89
  # Execute a block inside a span context, sending trace data on completion.
90
- # Called by Traceable not intended for direct use.
90
+ # Called by Traceable, not intended for direct use.
91
91
  def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
92
92
  mock_on_replay: false)
93
93
  return yield unless @enabled
@@ -121,7 +121,7 @@ module Bitfab
121
121
  # Unmarked spans must consume an index so subsequent marked siblings
122
122
  # line up with `build_mock_tree`'s sequential numbering for the same
123
123
  # (key, name) pair. Different (key, name) pairs have independent
124
- # counters they cannot shift each other.
124
+ # counters: they cannot shift each other.
125
125
  call_index = advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
126
126
  if call_index
127
127
  mocked_output = check_mock_replay(
@@ -154,7 +154,7 @@ module Bitfab
154
154
  finalized = false
155
155
 
156
156
  finalize = lambda do |final_result, final_error|
157
- # Never crash the host app due to span building/sending. Idempotent
157
+ # Never crash the host app due to span building/sending. Idempotent:
158
158
  # only the first call sends the span. Subsequent calls (e.g. from the
159
159
  # enumerator wrapper after iteration completes) are no-ops.
160
160
  next if finalized
@@ -188,11 +188,27 @@ module Bitfab
188
188
  pending << span_thread if span_thread
189
189
  pending.each { |t| t.join(5) }
190
190
 
191
+ # Built AFTER the wrapped method finished (finalize runs at root
192
+ # span end), so :accessed reflects whether customer code obtained
193
+ # the branch URL during this item. nil (key omitted) when no
194
+ # lease was attached, so the server can distinguish "no branch"
195
+ # from "branch ignored".
196
+ lease = replay_ctx&.dig(:db_branch_lease)
197
+ db_snapshot_usage = if lease
198
+ {
199
+ neon_branch_id: lease["neonBranchId"],
200
+ snapshot_timestamp: lease["snapshotTimestamp"],
201
+ source_trace_id: replay_ctx[:source_bitfab_trace_id],
202
+ accessed: replay_ctx[:db_snapshot_accessed] == true
203
+ }
204
+ end
205
+
191
206
  completion_thread = send_trace_completion(
192
207
  trace_function_key:,
193
208
  trace_id:,
194
209
  started_at:,
195
- ended_at:
210
+ ended_at:,
211
+ db_snapshot_usage:
196
212
  )
197
213
 
198
214
  # In replay, persistence is correctness: the replay runner joins
@@ -212,7 +228,7 @@ module Bitfab
212
228
  end
213
229
  end
214
230
  rescue Exception # rubocop:disable Lint/RescueException
215
- # Silently ignore user's result/exception takes priority
231
+ # Silently ignore: user's result/exception takes priority
216
232
  # Catches Exception (not just StandardError) to handle SystemStackError
217
233
  # from deeply nested serialization
218
234
  end
@@ -234,7 +250,7 @@ module Bitfab
234
250
 
235
251
  # If the wrapped block returned an Enumerator (lazy iteration via
236
252
  # `enum_for`, `to_enum`, `Enumerator.new`, `[...].lazy.map(...)`, etc.),
237
- # the work hasn't actually run yet the values are produced as the
253
+ # the work hasn't actually run yet: the values are produced as the
238
254
  # caller iterates. Without special handling we'd close the span here
239
255
  # with `result == <the Enumerator object>`, and any nested `bitfab_span`
240
256
  # calls inside the enumerator body would see an empty span stack and
@@ -264,7 +280,7 @@ module Bitfab
264
280
  # Build an Enumerator that drives `source`, restoring `[trace_id, span_id]`
265
281
  # on the iterating fiber so nested `bitfab_span` calls inside lazy / `each`
266
282
  # callbacks nest under the parent span. Yielded values are collected as the
267
- # span output. The span is sent exactly once when iteration finishes,
283
+ # span output. The span is sent exactly once: when iteration finishes,
268
284
  # raises, or the wrapper is `.close`d.
269
285
  def wrap_enumerator(source, trace_id:, span_id:, finalize:)
270
286
  span_entry = {trace_id:, span_id:}
@@ -296,7 +312,14 @@ module Bitfab
296
312
  raise ArgumentError, "Invalid span type '#{type}'. Must be one of: #{SPAN_TYPES.join(", ")}"
297
313
  end
298
314
 
299
- def send_trace_completion(trace_function_key:, trace_id:, started_at:, ended_at:)
315
+ # db_snapshot_usage: replay DB branch usage record, present only when a
316
+ # lease was attached to the replay item. Serialized as `db_snapshot_usage`
317
+ # on the raw trace so the server can stamp the trace's metadata at ingest:
318
+ # { neon_branch_id:, snapshot_timestamp: (optional), source_trace_id:
319
+ # (optional), accessed: } with :accessed true if customer code obtained
320
+ # the branch URL and false if it ignored it. nil outside replay or when no
321
+ # lease was attached, in which case the key is omitted entirely.
322
+ def send_trace_completion(trace_function_key:, trace_id:, started_at:, ended_at:, db_snapshot_usage: nil)
300
323
  trace_state = TraceState.get(trace_id)
301
324
  trace_started_at = trace_state&.dig(:started_at) || started_at
302
325
 
@@ -318,6 +341,17 @@ module Bitfab
318
341
  if trace_state&.dig(:db_snapshot_ref)
319
342
  raw_trace["db_snapshot_ref"] = trace_state[:db_snapshot_ref]
320
343
  end
344
+ if db_snapshot_usage
345
+ usage = {"neon_branch_id" => db_snapshot_usage[:neon_branch_id]}
346
+ if db_snapshot_usage[:snapshot_timestamp]
347
+ usage["snapshot_timestamp"] = db_snapshot_usage[:snapshot_timestamp]
348
+ end
349
+ if db_snapshot_usage[:source_trace_id]
350
+ usage["source_trace_id"] = db_snapshot_usage[:source_trace_id]
351
+ end
352
+ usage["accessed"] = db_snapshot_usage[:accessed]
353
+ raw_trace["db_snapshot_usage"] = usage
354
+ end
321
355
 
322
356
  payload = {
323
357
  "type" => "sdk-function",
@@ -339,7 +373,7 @@ module Bitfab
339
373
  # Clean up trace state
340
374
  TraceState.delete(trace_id)
341
375
 
342
- # Returned so the replay path can join it trace completions must be
376
+ # Returned so the replay path can join it: trace completions must be
343
377
  # persisted before complete_replay builds the trace-ID mapping.
344
378
  completion_thread
345
379
  end
@@ -398,8 +432,8 @@ module Bitfab
398
432
  # non-root span under an active mock tree. Returns the call index this
399
433
  # invocation owns, or nil when there's nothing to advance (root span, or
400
434
  # no replay mock context). The counter MUST advance for every child span
401
- # sharing the same (key, name) pair including spans that won't be
402
- # mocked so unmarked spans don't silently shift subsequent marked
435
+ # sharing the same (key, name) pair (including spans that won't be
436
+ # mocked) so unmarked spans don't silently shift subsequent marked
403
437
  # spans' indices. Different (key, name) pairs have independent counters.
404
438
  def advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
405
439
  return nil if is_root_span
@@ -433,7 +467,7 @@ module Bitfab
433
467
  output_meta = mock_entry[:output_meta]
434
468
 
435
469
  # Type-preserving deserialization when the server included Ruby-side
436
- # Marshal+Base64 metadata. Falls back to the JSON output silently the
470
+ # Marshal+Base64 metadata. Falls back to the JSON output silently: the
437
471
  # spanTree endpoint currently returns superjson/jsonpickle-shaped meta,
438
472
  # which Ruby cannot reconstruct.
439
473
  if output_meta.is_a?(String) && !output_meta.empty?
@@ -484,13 +518,13 @@ module Bitfab
484
518
  @pending_span_threads[trace_id] << span_thread if span_thread && @pending_span_threads.key?(trace_id)
485
519
  end
486
520
  rescue Exception # rubocop:disable Lint/RescueException
487
- # Never crash the host app mocked span recording is best-effort
521
+ # Never crash the host app: mocked span recording is best-effort
488
522
  end
489
523
  end
490
524
 
491
525
  # Fluent wrapper bound to a single trace_function_key. Mirrors
492
526
  # `BitfabFunction` in the Python SDK and `BitfabFunction` in the TypeScript
493
- # SDK lets callers wrap multiple methods without repeating the key.
527
+ # SDK: lets callers wrap multiple methods without repeating the key.
494
528
  class BitfabFunction
495
529
  attr_reader :trace_function_key
496
530
 
data/lib/bitfab/replay.rb CHANGED
@@ -6,9 +6,9 @@ require_relative "serialize"
6
6
  module Bitfab
7
7
  # Replay mock strategies. Mirrors the Python and TypeScript SDKs.
8
8
  #
9
- # - "none" every child span runs real code (default)
10
- # - "all" every child span returns its historical output
11
- # - "marked" only spans declared with mock_on_replay: true return historical
9
+ # - "none" : every child span runs real code (default)
10
+ # - "all" : every child span returns its historical output
11
+ # - "marked" : only spans declared with mock_on_replay: true return historical
12
12
  # output; everything else runs real code
13
13
  MOCK_STRATEGIES = %w[none all marked].freeze
14
14
 
@@ -45,6 +45,16 @@ module Bitfab
45
45
  # The per-trace DB branch (resolved server-side) and the Bitfab trace ID
46
46
  # it belongs to ride on the context so ReplayEnvironment can read them
47
47
  # inside the replayed method.
48
+ #
49
+ # ReplayEnvironment also sets ctx[:db_snapshot_accessed] = true the
50
+ # first time customer code actually obtains the branch URL for this
51
+ # item (via +database_url+ or +snapshot+). Reported on the trace
52
+ # completion inside the +db_snapshot_usage+ record (its +accessed+
53
+ # field) so the server can distinguish "branch was provisioned and
54
+ # exposed" from "branch URL was actually consumed". Any future
55
+ # consumption path that hands the URL to customer code by other means
56
+ # (e.g. a process-isolated runner writing an env overlay) must also
57
+ # set this.
48
58
  ctx[:db_branch_lease] = db_branch_lease if db_branch_lease
49
59
  ctx[:source_bitfab_trace_id] = source_bitfab_trace_id if source_bitfab_trace_id
50
60
  Thread.current[REPLAY_CONTEXT_KEY] = ctx
@@ -135,7 +145,7 @@ module Bitfab
135
145
 
136
146
  # Every item joined its own trace-persistence threads (span uploads +
137
147
  # completion) in execute_item, so all replay traces are on the server
138
- # by now no flush needed, and complete_replay's trace-ID mapping is
148
+ # by now: no flush needed, and complete_replay's trace-ID mapping is
139
149
  # deterministic. complete_replay failures propagate: a missing mapping
140
150
  # means verdicts can't be persisted, which callers must hear about
141
151
  # loudly.
@@ -151,7 +161,7 @@ module Bitfab
151
161
  else
152
162
  # Map each item's locally-generated trace ID to the server's trace
153
163
  # row ID. A completed item with no mapping means its trace was sent
154
- # but the server has no record a nil trace_id blocks verdict
164
+ # but the server has no record: a nil trace_id blocks verdict
155
165
  # persistence and the Studio experiments view downstream, so this
156
166
  # must never be silent.
157
167
  #
@@ -244,8 +254,8 @@ module Bitfab
244
254
  metrics = extract_server_item_metrics(server_item)
245
255
  # The server resolves a Neon preview branch per item during /replay/start
246
256
  # (only when include_db_branch_lease was sent). Release it in the +ensure+
247
- # below so any raise span fetch, mock-tree build, or the replayed
248
- # method frees the Neon resource. Items whose source trace had no
257
+ # below so any raise (span fetch, mock-tree build, or the replayed
258
+ # method) frees the Neon resource. Items whose source trace had no
249
259
  # snapshot ref, or whose resolve failed server-side, arrive without a
250
260
  # lease (env.active? is false for those).
251
261
  lease = include_db_branch_lease ? server_item["dbBranchLease"] : nil
@@ -295,7 +305,7 @@ module Bitfab
295
305
  end
296
306
 
297
307
  # Delete the per-item Neon preview branch. Best-effort: a failure is warned
298
- # but never raised the server-side TTL janitor reaps orphans.
308
+ # but never raised: the server-side TTL janitor reaps orphans.
299
309
  def release_db_branch_lease(http_client, lease)
300
310
  neon_branch_id = lease["neonBranchId"]
301
311
  return unless neon_branch_id
@@ -308,7 +318,7 @@ module Bitfab
308
318
  # Walk the children of a root span tree node depth-first and build a
309
319
  # lookup keyed by "#{trace_function_key}:#{span_name}:#{call_index}".
310
320
  #
311
- # The root node itself is excluded at replay time the runtime root span
321
+ # The root node itself is excluded: at replay time the runtime root span
312
322
  # never queries the mock tree.
313
323
  #
314
324
  # The compound (key, name) match disambiguates same-key spans that come
@@ -316,7 +326,7 @@ module Bitfab
316
326
  # wrapped method shares trace_function_key but differs in span_name. The
317
327
  # counter is per-(key, name) pair so repeated same-name calls (including
318
328
  # recursion) still order by occurrence. Mirrors the Python and TypeScript
319
- # SDKs after HVT-2078 keying by trace_function_key alone caused the
329
+ # SDKs after HVT-2078: keying by trace_function_key alone caused the
320
330
  # wrong historical output for fluent-API span sets.
321
331
  def build_mock_tree(root)
322
332
  spans = {}
@@ -389,7 +399,7 @@ module Bitfab
389
399
  sdk_trace_id = SecureRandom.uuid
390
400
  # Collects the root span's persistence threads (span uploads + trace
391
401
  # completion). Joined below so this item's trace is on the server
392
- # before run() calls complete_replay otherwise the server's trace-ID
402
+ # before run() calls complete_replay: otherwise the server's trace-ID
393
403
  # mapping races the uploads and the item's trace_id comes back nil.
394
404
  pending_persistence = []
395
405
 
@@ -421,7 +431,7 @@ module Bitfab
421
431
  end
422
432
 
423
433
  # Wait for this item's trace (spans + completion) to be fully persisted
424
- # before the item resolves. Runs on the error path too a raising
434
+ # before the item resolves. Runs on the error path too: a raising
425
435
  # method still emits a root span whose trace must land before
426
436
  # complete_replay. Joins are bounded by the HTTP layer's own timeouts.
427
437
  pending_persistence.each(&:join)
@@ -23,7 +23,9 @@ module Bitfab
23
23
  # The per-trace branch URL for the item currently being replayed.
24
24
  # Raises if read outside a replay item.
25
25
  def database_url
26
- require_snapshot.fetch(:database_url)
26
+ snap = require_snapshot
27
+ mark_accessed
28
+ snap.fetch(:database_url)
27
29
  end
28
30
 
29
31
  # When the per-trace branch URL stops being valid. ISO-8601.
@@ -55,11 +57,25 @@ module Bitfab
55
57
  # Non-raising variant for callers that handle the inactive case. Returns a
56
58
  # symbol-keyed hash or nil.
57
59
  def snapshot
58
- read_snapshot
60
+ snap = read_snapshot
61
+ mark_accessed if snap
62
+ snap
59
63
  end
60
64
 
61
65
  private
62
66
 
67
+ # Record on the replay context that customer code obtained the branch
68
+ # URL. Only +database_url+ and +snapshot+ count: +active?+, +read_only+
69
+ # and friends inspect the lease without exposing the connection string,
70
+ # so they don't prove the replayed code could have connected to the
71
+ # branch.
72
+ def mark_accessed
73
+ ctx = ReplayContext.current
74
+ return unless ctx && ctx[:db_branch_lease]
75
+
76
+ ctx[:db_snapshot_accessed] = true
77
+ end
78
+
63
79
  def read_snapshot
64
80
  ctx = ReplayContext.current
65
81
  return nil unless ctx
@@ -69,7 +85,7 @@ module Bitfab
69
85
 
70
86
  # Surface the Bitfab trace ID (what the customer sees in the dashboard),
71
87
  # falling back to the external trace ID only if the Bitfab ID is somehow
72
- # absent keeps replays from external sources working until the
88
+ # absent: keeps replays from external sources working until the
73
89
  # source-system path is fully wired.
74
90
  trace_id = ctx[:source_bitfab_trace_id] || ctx[:input_source_trace_id]
75
91
  return nil unless trace_id
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.17.0"
4
+ VERSION = "0.17.1"
5
5
  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.17.0
4
+ version: 0.17.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team