bitfab 0.51.1 → 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: 3dd6fa16ae3ff5f26d94f6b86f9fa702129cfb502e3f94b293e383d107d1c0de
4
- data.tar.gz: 220cdb686e1a5522fdcd9579e897f60b0896360583b49615006f0489723c6d21
3
+ metadata.gz: ea799ee55cf7c1ac7167300d800d9593162ddc6b906557c12c3079607fcfc4f4
4
+ data.tar.gz: f7985925a6a8b3cf8c5e2ea4389bd8122646f5bf2eb5bf59e6769d9f533636b1
5
5
  SHA512:
6
- metadata.gz: cf75d4072ac82e17312f40f86c1a0e54c3a60a019818695507a5d53cfc1bfb15db6e705ad6a971c6e77abd67b4b752e180457d483b5f42becc52167323784dd9
7
- data.tar.gz: 8857e35745377b128479948ec4622ef2223f3876015f03e161ac80e43f61ec1c1556cb4d4839dd174c35afba9b3e1fbfcbd0c4da69766b80cd593cd8caf96c07
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
@@ -673,6 +674,8 @@ module Bitfab
673
674
  if trace_state&.dig(:db_snapshot_ref)
674
675
  raw_trace["db_snapshot_ref"] = trace_state[:db_snapshot_ref]
675
676
  end
677
+ commit_ref = CommitRef.current
678
+ raw_trace["commit_ref"] = commit_ref if commit_ref
676
679
  if db_snapshot_usage
677
680
  usage = {"neon_branch_id" => db_snapshot_usage[:neon_branch_id]}
678
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"
@@ -560,20 +561,7 @@ module Bitfab
560
561
  end
561
562
 
562
563
  def git(cwd, args)
563
- # capture3 so git's stderr (e.g. "not a git repository") is swallowed, not
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.51.1"
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.1
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