bitfab 0.51.0 → 0.51.2

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: 5ece2db3c44be909ec097e539479143d0c4138663809c26a05a968076e681066
4
- data.tar.gz: 6d68e352c7e484586147d1700998de7936fe904c5d04cabfe07df80e302104b1
3
+ metadata.gz: ea799ee55cf7c1ac7167300d800d9593162ddc6b906557c12c3079607fcfc4f4
4
+ data.tar.gz: f7985925a6a8b3cf8c5e2ea4389bd8122646f5bf2eb5bf59e6769d9f533636b1
5
5
  SHA512:
6
- metadata.gz: bbb4750e80b63864d612592e1bebaac664463059e2d139fe7d556f68ce73b46868001daa54391d1a4562767a2e7e16355fdb8eefc89c64ace8f864dffffce86b
7
- data.tar.gz: 02d731c682b787b8233a1c9a43135fc8b6d17d5959806ff65bec3be9979dbda95d1bfb6d01373b427405a5fb9fccd976c87706d8d488ea7733fe70ab0f8fa15b
6
+ metadata.gz: 10c67dc35cf10ed1c445108ce1801993b8f4115afda9411fe1dc3b9e12213ab090441e91c6a59a0864925822a09d344360e4d10ab15cac0a1a183c56d007972e
7
+ data.tar.gz: 2f968a4c58b66fd51d49c74199b20207a44aecd56285656b2ece8df04a76e9070b9f87c7dcbb53f2eeedfe61ba0a271d68660b934b4a56eb1f8fca278221f452
data/README.md CHANGED
@@ -399,6 +399,8 @@ batch is never re-encoded to measure its size. If Bitfab accepts only part of a
399
399
  the standard OTLP `partialSuccess` response and the SDK warns with the rejected-span count and
400
400
  reason.
401
401
 
402
+ 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.
403
+
402
404
  Before finalizing a replay, the SDK flushes OTel and polls Bitfab's replay-status API until every
403
405
  expected trace completion and span count is persisted.
404
406
 
data/lib/bitfab/client.rb CHANGED
@@ -51,6 +51,7 @@ module Bitfab
51
51
  # replay on this client (after any per-call mock_override). Instance
52
52
  # state, no global; clear_mock_overrides resets it.
53
53
  @mock_overrides = []
54
+ CommitRef.start_resolution
54
55
  end
55
56
 
56
57
  # The configured API key (a proc is resolved on read). Reflects what was
@@ -95,9 +96,12 @@ module Bitfab
95
96
  # @param limit [Integer, nil] maximum number of traces to replay (default: 5;
96
97
  # maximum: 5,000). Ignored when trace_ids or a dataset is passed because
97
98
  # either source already determines how many traces replay. Supplying
98
- # trace_ids also emits a warning.
99
- # @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100),
100
- # mutually exclusive with dataset_id and dataset_ids
99
+ # trace_ids also emits a warning. To replay part of a dataset selection,
100
+ # name the members to run in trace_ids rather than bounding it here.
101
+ # @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100).
102
+ # Passed alongside dataset_id or dataset_ids it pins which members of that
103
+ # selection replay, and the server rejects any ID none of those datasets
104
+ # contains
101
105
  # @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
102
106
  # @param code_change_description [String, nil] optional rationale for the
103
107
  # code change being tested in this replay (stored on the experiment).
@@ -112,10 +116,12 @@ module Bitfab
112
116
  # replay runs into a single experiment batch
113
117
  # @param dataset_ids [Array<String>, nil] optional UUIDs of the datasets this
114
118
  # replay runs against, for benchmarking one method against several corpora
115
- # in a single run; mutually exclusive with dataset_id
119
+ # in a single run; mutually exclusive with dataset_id. Passing trace_ids
120
+ # alongside it narrows the run to those members
116
121
  # @param dataset_id [String, nil] optional UUID of the dataset this replay
117
- # runs against, mutually exclusive with trace_ids and stored on the
118
- # resulting experiment for durable attribution
122
+ # runs against, mutually exclusive with dataset_ids and stored on the
123
+ # resulting experiment for durable attribution. Alone it replays the
124
+ # dataset's full membership, and with trace_ids only those members
119
125
  # @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
120
126
  # directly to this experiment, graded as the union with the dataset's
121
127
  # runnable graders at completion; each must be an active/live grader in the
@@ -668,6 +674,8 @@ module Bitfab
668
674
  if trace_state&.dig(:db_snapshot_ref)
669
675
  raw_trace["db_snapshot_ref"] = trace_state[:db_snapshot_ref]
670
676
  end
677
+ commit_ref = CommitRef.current
678
+ raw_trace["commit_ref"] = commit_ref if commit_ref
671
679
  if db_snapshot_usage
672
680
  usage = {"neon_branch_id" => db_snapshot_usage[:neon_branch_id]}
673
681
  if db_snapshot_usage[:snapshot_timestamp]
@@ -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"
@@ -206,12 +207,14 @@ module Bitfab
206
207
  # replay runs into a single experiment batch
207
208
  # @param dataset_id [String, nil] optional UUID of the dataset this replay
208
209
  # runs against, stored on the resulting experiment for durable attribution;
209
- # mutually exclusive with dataset_ids
210
+ # mutually exclusive with dataset_ids. Alone it replays the dataset's full
211
+ # membership, and with trace_ids it replays only those members
210
212
  # @param dataset_ids [Array<String>, nil] optional UUIDs of the datasets this
211
213
  # replay runs against, for benchmarking one method against several corpora
212
214
  # in a single run; mutually exclusive with dataset_id. The run replays the
213
215
  # union of their traces, graded by the union of their graders, and is
214
- # attributed to every one of them
216
+ # attributed to every one of them. Passing trace_ids alongside it narrows
217
+ # the run to those members
215
218
  # @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
216
219
  # directly to this experiment, graded as the union with the dataset's
217
220
  # runnable graders at completion; each must be an active/live grader in the
@@ -267,10 +270,6 @@ module Bitfab
267
270
  end
268
271
  end
269
272
  resolved_dataset_ids = resolve_dataset_ids(dataset_id, dataset_ids)
270
- if trace_ids && resolved_dataset_ids
271
- raise ArgumentError,
272
- "trace_ids and dataset_ids select different replay sources and cannot be used together."
273
- end
274
273
  if limit && trace_ids
275
274
  warn "Bitfab: limit is ignored when trace_ids is passed: the explicit trace ID list already " \
276
275
  "determines how many traces replay."
@@ -562,20 +561,7 @@ module Bitfab
562
561
  end
563
562
 
564
563
  def git(cwd, args)
565
- # capture3 so git's stderr (e.g. "not a git repository") is swallowed, not
566
- # leaked to the host process's console.
567
- out, _err, status = Timeout.timeout(30) do
568
- Open3.capture3("git", *args, chdir: cwd)
569
- end
570
- return nil unless status.success?
571
-
572
- # Tag as UTF-8 and scrub invalid bytes so blob contents compare cleanly
573
- # against the UTF-8 working-file reads and never break JSON.generate later
574
- # (a non-UTF-8 file that slips past the null-byte check would otherwise
575
- # abort the whole start-replay request).
576
- out.force_encoding("UTF-8").scrub
577
- rescue
578
- nil
564
+ GitCommand.run(cwd, args, timeout: 30)
579
565
  end
580
566
 
581
567
  def cc_ref_exists?(root, ref)
@@ -81,8 +81,26 @@ module Bitfab
81
81
 
82
82
  # Argument parsing and output for the SDK-owned replay command.
83
83
  module ReplayCli
84
+ MAX_PINNED_TRACE_IDS = 100
85
+
84
86
  module_function
85
87
 
88
+ def pinned_dataset_members(client, dataset_ids, limit)
89
+ members = dataset_ids.flat_map { |dataset_id|
90
+ client.datasets.list_traces(dataset_id).fetch("traceIds")
91
+ }.uniq.sort
92
+ return nil if limit >= members.length
93
+
94
+ if limit > MAX_PINNED_TRACE_IDS
95
+ raise ArgumentError,
96
+ "Bounding a dataset replay to #{limit} names one trace ID per replayed item, and " \
97
+ "a replay accepts at most #{MAX_PINNED_TRACE_IDS}. Drop the limit to replay " \
98
+ "every selected dataset in full."
99
+ end
100
+
101
+ members.first(limit)
102
+ end
103
+
86
104
  def run(registry, argv: ARGV, stdout: $stdout, stderr: $stderr)
87
105
  args = parse(registry, argv.dup)
88
106
  registration = registry.fetch(args.fetch(:pipeline))
@@ -108,20 +126,25 @@ module Bitfab
108
126
  end
109
127
 
110
128
  if args[:trace_ids]
111
- options.delete(:limit)
112
129
  options.delete(:dataset_ids)
113
130
  options[:trace_ids] = args[:trace_ids]
114
131
  elsif args[:dataset_ids]
115
132
  options.delete(:trace_ids)
116
- options.delete(:limit)
117
- elsif args[:limit]
118
- options.delete(:trace_ids)
119
- options.delete(:dataset_ids)
120
- options[:limit] = args[:limit]
121
- elsif options[:trace_ids] || selected_dataset_ids
122
- options.delete(:limit)
133
+ end
134
+
135
+ bound = args[:limit] || options[:limit]
136
+ options.delete(:limit)
137
+ bounded_dataset_ids = args[:dataset_ids] || options[:dataset_ids]
138
+
139
+ if options[:trace_ids]
140
+ options[:trace_ids] = options[:trace_ids].first(bound) if bound
141
+ elsif bounded_dataset_ids
142
+ if bound
143
+ pinned = pinned_dataset_members(registration.client, bounded_dataset_ids, bound)
144
+ options[:trace_ids] = pinned if pinned
145
+ end
123
146
  else
124
- options[:limit] = options.fetch(:limit, 10)
147
+ options[:limit] = bound || 10
125
148
  end
126
149
 
127
150
  %i[name max_concurrency experiment_group_id dataset_ids grader_ids mock].each do |key|
@@ -179,7 +202,10 @@ module Bitfab
179
202
  args = {}
180
203
  parser = OptionParser.new do |options|
181
204
  options.banner = "Usage: bitfab-replay <#{registry.names.join("|")}> [options]"
182
- options.on("--limit N", Integer) { |value| args[:limit] = positive_integer("--limit", value) }
205
+ options.on("--limit N", Integer,
206
+ "How many traces to replay. It bounds a trace ID or dataset selection instead of replacing it") do |value|
207
+ args[:limit] = positive_integer("--limit", value)
208
+ end
183
209
  options.on("--trace-ids IDS") { |value| args[:trace_ids] = comma_separated("--trace-ids", value) }
184
210
  options.on("--name NAME") { |value| args[:name] = value }
185
211
  options.on("--concurrency N", Integer) do |value|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.51.0"
4
+ VERSION = "0.51.2"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -8,6 +8,8 @@ require_relative "bitfab/warn_once"
8
8
  require_relative "bitfab/payload_budget"
9
9
  require_relative "bitfab/serialize"
10
10
  require_relative "bitfab/db_snapshot"
11
+ require_relative "bitfab/git_command"
12
+ require_relative "bitfab/commit_ref"
11
13
  require_relative "bitfab/span_context"
12
14
  require_relative "bitfab/http_client"
13
15
  require_relative "bitfab/datasets"
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.51.0
4
+ version: 0.51.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -140,10 +140,12 @@ files:
140
140
  - exe/bitfab-replay
141
141
  - lib/bitfab.rb
142
142
  - lib/bitfab/client.rb
143
+ - lib/bitfab/commit_ref.rb
143
144
  - lib/bitfab/compress.rb
144
145
  - lib/bitfab/constants.rb
145
146
  - lib/bitfab/datasets.rb
146
147
  - lib/bitfab/db_snapshot.rb
148
+ - lib/bitfab/git_command.rb
147
149
  - lib/bitfab/http_client.rb
148
150
  - lib/bitfab/mock_override.rb
149
151
  - lib/bitfab/otel.rb