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 +4 -4
- data/README.md +2 -0
- data/lib/bitfab/client.rb +3 -0
- data/lib/bitfab/commit_ref.rb +184 -0
- data/lib/bitfab/git_command.rb +65 -0
- data/lib/bitfab/replay.rb +2 -14
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea799ee55cf7c1ac7167300d800d9593162ddc6b906557c12c3079607fcfc4f4
|
|
4
|
+
data.tar.gz: f7985925a6a8b3cf8c5e2ea4389bd8122646f5bf2eb5bf59e6769d9f533636b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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)
|
data/lib/bitfab/version.rb
CHANGED
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.
|
|
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
|