bitfab 0.22.0 → 0.23.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: d3286f80b5a55bff760f454480b1a3662fdf984174ee65d6f195dcbca5d456ed
4
- data.tar.gz: d1d8bc8e244075f56cc6c06d42dae614da95b4531ea8bafc21de2b84b1d2526a
3
+ metadata.gz: 26ad3b53b9f3da35c01496ecc236ce0f5d27e7921761c7baca7654d1fb8de31e
4
+ data.tar.gz: e653a93c4bf8e5263b22a6114ebcc2a52db6cc3db6eecc7774cb7b6ed07c51c3
5
5
  SHA512:
6
- metadata.gz: 2ffbf4c36e417d4bf391eb9509a0136bc29dda1e54e191c84b51a59c400821fe7c21dbb796d272eaaca30f72502160d3aa8cb93efe1159304778422ac70ca9c1
7
- data.tar.gz: 2dac975acb96571c6e7303ac7632cf3b099f3884ce3bf7f499dbfb35c71c357674868210906ebc6864dabb3a6645b7f82e41b4b8559553ab2a93304e2d106871
6
+ metadata.gz: 7c31a1ffd2e8be0cd31fd94941479351176823eabeadf453972bd493aeadbbc7de97002f484787a6e009024a42a6ed1e3a8fc99ded4b94eb3fd842d6e03772e9
7
+ data.tar.gz: 01ee591f9c3a51254abc101229ba261bc054acd02bfe7c67471367fd8cb0cc2fa015a1c1affc149358caf7dc5af98c7ea4f5351973153b60c6983befbc4d7ac7
data/lib/bitfab/client.rb CHANGED
@@ -20,21 +20,40 @@ module Bitfab
20
20
  # outputs (which may themselves be nil or false).
21
21
  MOCK_REPLAY_MISS = Object.new.freeze
22
22
 
23
- attr_reader :api_key, :service_url, :enabled
23
+ attr_reader :service_url
24
24
 
25
- def initialize(api_key:, service_url: nil, enabled: true)
26
- @api_key = api_key
25
+ def initialize(api_key: nil, service_url: nil, enabled: true, strict: false)
26
+ @api_key_config = api_key
27
27
  @service_url = service_url || DEFAULT_SERVICE_URL
28
- @enabled = enabled
29
- if @enabled && (@api_key.nil? || @api_key.to_s.strip.empty?)
30
- warn "Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing."
31
- @enabled = false
32
- end
33
- @http_client = HttpClient.new(api_key:, service_url: @service_url)
28
+ # The user's on/off intent; effective enabled also requires a resolved key.
29
+ @explicitly_enabled = enabled
30
+ @strict = strict
31
+ # Cached only once a non-empty key is found, so an early resolve (before
32
+ # env loaded) can't poison a later one.
33
+ @resolved_api_key = nil
34
+ @api_key_warned = false
35
+ # The key is NOT read here. HttpClient gets a proc so the key is resolved
36
+ # at send time, after any env loading has run.
37
+ @http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url)
34
38
  @pending_span_threads = {}
35
39
  @pending_span_mutex = Mutex.new
36
40
  end
37
41
 
42
+ # The configured API key (a proc is resolved on read). Reflects what was
43
+ # passed to the client; the ENV["BITFAB_API_KEY"] fallback applied during
44
+ # actual tracing is not surfaced here, and reading this never warns.
45
+ def api_key
46
+ @api_key_config.respond_to?(:call) ? @api_key_config.call : @api_key_config
47
+ end
48
+
49
+ # Effective tracing state, evaluated lazily: enabled only when not
50
+ # explicitly disabled AND a key resolves. Reading this resolves the key
51
+ # (and may emit the one-time empty-key warning), exactly as the first traced
52
+ # call would.
53
+ def enabled
54
+ tracing_enabled?
55
+ end
56
+
38
57
  # Replay historical traces through a method and create a test run.
39
58
  #
40
59
  # @param receiver [Object, Class] an instance for instance methods, or a Class for class methods
@@ -68,11 +87,11 @@ module Bitfab
68
87
  # A raising callback never crashes the run.
69
88
  # @return [Hash] with :items, :test_run_id, :test_run_url
70
89
  def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
71
- code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "none",
90
+ name: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "none",
72
91
  adapt_inputs: nil, environment: nil, on_progress: nil)
73
92
  Replay.run(
74
93
  self, receiver, method_name,
75
- trace_function_key:, limit:, trace_ids:, max_concurrency:,
94
+ trace_function_key:, limit:, trace_ids:, name:, max_concurrency:,
76
95
  code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, mock:, adapt_inputs:, environment:,
77
96
  on_progress:
78
97
  )
@@ -100,7 +119,10 @@ module Bitfab
100
119
  # Called by Traceable, not intended for direct use.
101
120
  def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
102
121
  mock_on_replay: false)
103
- return yield unless @enabled
122
+ # Decide at CALL time, not construction. The key may be set after the
123
+ # client is built (env loaded later), so re-checking per call lets a
124
+ # late-resolved key take effect.
125
+ return yield unless tracing_enabled?
104
126
 
105
127
  # Span setup runs before the user's block. Tracing is a side-channel, so
106
128
  # if anything here raises (id generation, trace-state bookkeeping, a
@@ -321,6 +343,48 @@ module Bitfab
321
343
 
322
344
  private
323
345
 
346
+ # Resolve the API key lazily, the first time a span actually needs it.
347
+ #
348
+ # The key is intentionally NOT read at construction: a client built when a
349
+ # file is required can run before the app loads its env, so the key would be
350
+ # empty at construction even though it is set moments later. Resolution
351
+ # order: the configured value (a proc is called while still unresolved),
352
+ # then a fallback read of ENV["BITFAB_API_KEY"]. Once a non-empty key is
353
+ # found it is cached.
354
+ def resolve_api_key
355
+ return @resolved_api_key unless @resolved_api_key.nil?
356
+
357
+ from_config = @api_key_config.respond_to?(:call) ? @api_key_config.call : @api_key_config
358
+ candidate = if from_config && !from_config.to_s.strip.empty?
359
+ from_config
360
+ else
361
+ ENV["BITFAB_API_KEY"]
362
+ end
363
+ key = (candidate && !candidate.to_s.strip.empty?) ? candidate : nil
364
+ if key
365
+ @resolved_api_key = key
366
+ return key
367
+ end
368
+ if @strict
369
+ raise "Bitfab: no API key resolved. Set BITFAB_API_KEY or pass api_key: to " \
370
+ "Bitfab.configure. If a script loads env later, load it before the first " \
371
+ "traced call, or pass api_key: -> { ENV[\"BITFAB_API_KEY\"] }."
372
+ end
373
+ if @explicitly_enabled && !@api_key_warned
374
+ @api_key_warned = true
375
+ warn "Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing."
376
+ end
377
+ nil
378
+ end
379
+
380
+ # Whether tracing should run, decided lazily at call time. An explicit
381
+ # `enabled: false` short-circuits without ever touching the key.
382
+ def tracing_enabled?
383
+ return false unless @explicitly_enabled
384
+
385
+ !resolve_api_key.nil?
386
+ end
387
+
324
388
  # Build an Enumerator that drives `source`, restoring `[trace_id, span_id]`
325
389
  # on the iterating fiber so nested `bitfab_span` calls inside lazy / `each`
326
390
  # callbacks nest under the parent span. Yielded values are collected as the
@@ -107,10 +107,12 @@ module Bitfab
107
107
  # each as { path:, before:, after: } (use "" for new/deleted files)
108
108
  # @param experiment_group_id [String, nil] optional UUID grouping multiple
109
109
  # replay runs into a single experiment batch
110
+ # @param name [String, nil] optional display name for the resulting
111
+ # experiment/test run
110
112
  # @param dataset_id [String, nil] optional UUID of the dataset this replay
111
113
  # runs against, stored on the resulting experiment for durable attribution
112
114
  def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
113
- code_change_files: nil, experiment_group_id: nil, include_db_branch_lease: false, dataset_id: nil)
115
+ code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_id: nil)
114
116
  payload = {
115
117
  "traceFunctionKey" => trace_function_key
116
118
  }
@@ -118,6 +120,7 @@ module Bitfab
118
120
  # already determines the count), so it's omitted when nil.
119
121
  payload["limit"] = limit unless limit.nil?
120
122
  payload["traceIds"] = trace_ids if trace_ids
123
+ payload["name"] = name unless name.nil?
121
124
  payload["codeChangeDescription"] = code_change_description unless code_change_description.nil?
122
125
  payload["codeChangeFiles"] = normalize_code_change_files(code_change_files) unless code_change_files.nil?
123
126
  payload["experimentGroupId"] = experiment_group_id unless experiment_group_id.nil?
@@ -223,9 +226,12 @@ module Bitfab
223
226
  end
224
227
 
225
228
  def headers
229
+ # Resolve the key at request time, invoking the proc form if one was
230
+ # supplied (never read at construction).
231
+ key = @api_key.respond_to?(:call) ? @api_key.call : @api_key
226
232
  {
227
233
  "Content-Type" => "application/json",
228
- "Authorization" => "Bearer #{@api_key}"
234
+ "Authorization" => "Bearer #{key}"
229
235
  }
230
236
  end
231
237
  end
data/lib/bitfab/replay.rb CHANGED
@@ -82,6 +82,7 @@ module Bitfab
82
82
  # Ignored when trace_ids is passed (with a warning): an explicit ID list
83
83
  # already determines how many traces replay.
84
84
  # @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
85
+ # @param name [String, nil] optional display name for the resulting experiment/test run
85
86
  # @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
86
87
  # @param code_change_description [String, nil] optional rationale for the
87
88
  # code change being tested in this replay (stored on the experiment)
@@ -109,8 +110,9 @@ module Bitfab
109
110
  # render replay progress (e.g. a per-trace log). A raising callback never
110
111
  # crashes the run.
111
112
  # @return [Hash] with :items, :test_run_id, :test_run_url
112
- def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
113
- code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "none",
113
+ def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
114
+ max_concurrency: 10, code_change_description: nil, code_change_files: nil, experiment_group_id: nil,
115
+ dataset_id: nil, mock: "none",
114
116
  adapt_inputs: nil, environment: nil, on_progress: nil)
115
117
  unless MOCK_STRATEGIES.include?(mock.to_s)
116
118
  raise ArgumentError, "Invalid mock strategy '#{mock}'. Must be one of: #{MOCK_STRATEGIES.join(", ")}"
@@ -153,6 +155,7 @@ module Bitfab
153
155
  trace_function_key,
154
156
  effective_limit,
155
157
  trace_ids:,
158
+ name:,
156
159
  code_change_description:,
157
160
  code_change_files:,
158
161
  experiment_group_id:,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.22.0"
4
+ VERSION = "0.23.0"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -59,8 +59,8 @@ module Bitfab
59
59
  # @example
60
60
  # Bitfab.configure(api_key: ENV["BITFAB_API_KEY"])
61
61
  #
62
- def configure(api_key:, service_url: nil, enabled: true)
63
- @client = Client.new(api_key:, service_url:, enabled:)
62
+ def configure(api_key: nil, service_url: nil, enabled: true, strict: false)
63
+ @client = Client.new(api_key:, service_url:, enabled:, strict:)
64
64
  end
65
65
 
66
66
  # Returns the global client, raising if not configured.
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.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team