mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Emit accepted line fingerprint analytics (#21601)
## Why Codex assisted-code attribution needs a client-side accepted-code source that does not upload raw code. This adds a hash-only analytics event derived from the turn diff so downstream attribution can compare accepted Codex lines against commit or PR diffs. ## What Changed - Parse accepted/effective added lines from the final turn diff and emit `codex_accepted_line_fingerprints` analytics. - Hash repo, path, and normalized line content before upload; raw code and raw diffs are not included in the event. - Chunk large fingerprint payloads and send accepted-line fingerprint events in isolated requests while preserving normal batching for other analytics events. - Canonicalize Git remote URLs before repo hashing so SSH/HTTPS GitHub remotes join to the same repo hash. - Add parser coverage for unified diff hunk lines that look like `+++` or `---` file headers. ## Verification - `cargo test -p codex-analytics` - `cargo test -p codex-git-utils canonicalize_git_remote_url` - `just fix -p codex-analytics` - `just bazel-lock-check` - `git diff --check`
This commit is contained in:
committed by
GitHub
Unverified
parent
9183503b97
commit
bbb6bf0a37
@@ -158,6 +158,108 @@ pub async fn get_head_commit_hash(cwd: &Path) -> Option<GitSha> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canonicalize_git_remote_url(url: &str) -> Option<String> {
|
||||
let url = trim_git_suffix(url.trim().trim_end_matches('/'));
|
||||
if url.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((scheme, rest)) = url.split_once("://") {
|
||||
return canonicalize_git_url_like_remote(scheme, rest);
|
||||
}
|
||||
|
||||
if let Some((host_part, path)) = parse_scp_like_remote(url) {
|
||||
return canonicalize_git_remote_host_path(host_part, path, /*default_port*/ None);
|
||||
}
|
||||
|
||||
let (host_part, path) = url.split_once('/')?;
|
||||
canonicalize_git_remote_host_path(host_part, path, /*default_port*/ None)
|
||||
}
|
||||
|
||||
fn canonicalize_git_url_like_remote(scheme: &str, rest: &str) -> Option<String> {
|
||||
let default_port = match scheme {
|
||||
"git" => Some("9418"),
|
||||
"http" => Some("80"),
|
||||
"https" => Some("443"),
|
||||
"ssh" => Some("22"),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let rest = rest
|
||||
.find(['?', '#'])
|
||||
.map_or(rest, |suffix_index| &rest[..suffix_index]);
|
||||
let (host_part, path) = rest.split_once('/')?;
|
||||
canonicalize_git_remote_host_path(host_part, path, default_port)
|
||||
}
|
||||
|
||||
fn parse_scp_like_remote(remote: &str) -> Option<(&str, &str)> {
|
||||
if remote.contains('/')
|
||||
&& remote
|
||||
.find('/')
|
||||
.is_some_and(|slash| remote.find(':').is_none_or(|colon| slash < colon))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let (host_part, path) = remote.split_once(':')?;
|
||||
if host_part.is_empty() || path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((host_part, path))
|
||||
}
|
||||
|
||||
fn canonicalize_git_remote_host_path(
|
||||
host_part: &str,
|
||||
path: &str,
|
||||
default_port: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let host = normalize_remote_host(
|
||||
host_part
|
||||
.rsplit_once('@')
|
||||
.map_or(host_part, |(_, host)| host)
|
||||
.trim()
|
||||
.trim_end_matches('/'),
|
||||
default_port,
|
||||
);
|
||||
if host.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = trim_git_suffix(path.trim().trim_matches('/'));
|
||||
let components = path
|
||||
.split('/')
|
||||
.filter(|component| !component.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
let [owner, repo, ..] = components.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if matches!((*owner, *repo), ("." | "..", _) | (_, "." | "..")) {
|
||||
return None;
|
||||
}
|
||||
let path = components.join("/");
|
||||
|
||||
if host == "github.com" {
|
||||
Some(format!("{host}/{}", path.to_ascii_lowercase()))
|
||||
} else {
|
||||
Some(format!("{host}/{path}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_remote_host(host: &str, default_port: Option<&str>) -> String {
|
||||
let host = host.to_ascii_lowercase();
|
||||
if let Some(default_port) = default_port
|
||||
&& let Some((host_without_port, port)) = host.rsplit_once(':')
|
||||
&& port == default_port
|
||||
{
|
||||
return host_without_port.to_string();
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
fn trim_git_suffix(value: &str) -> &str {
|
||||
value.strip_suffix(".git").unwrap_or(value)
|
||||
}
|
||||
|
||||
pub async fn get_has_changes(cwd: &Path) -> Option<bool> {
|
||||
let output = run_git_command_with_timeout(&["status", "--porcelain"], cwd).await?;
|
||||
if !output.status.success() {
|
||||
@@ -724,3 +826,46 @@ pub async fn current_branch_name(cwd: &Path) -> Option<String> {
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|name| !name.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn canonicalize_git_remote_url_normalizes_github_variants() {
|
||||
for remote in [
|
||||
"git@github.com:OpenAI/Codex.git",
|
||||
"ssh://git@github.com/openai/codex.git",
|
||||
"ssh://git@github.com:22/OpenAI/Codex.git",
|
||||
"https://github.com/openai/codex.git",
|
||||
"https://github.com:443/openai/codex.git",
|
||||
"https://token@github.com/openai/codex/",
|
||||
"github.com/OpenAI/Codex.git",
|
||||
] {
|
||||
assert_eq!(
|
||||
canonicalize_git_remote_url(remote),
|
||||
Some("github.com/openai/codex".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_git_remote_url_handles_ghe_without_lowercasing_path() {
|
||||
assert_eq!(
|
||||
canonicalize_git_remote_url("git@ghe.company.com:Org/Repo.git"),
|
||||
Some("ghe.company.com/Org/Repo".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
canonicalize_git_remote_url("ssh://git@ghe.company.com:2222/Org/Repo.git"),
|
||||
Some("ghe.company.com:2222/Org/Repo".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_git_remote_url_rejects_non_repository_values() {
|
||||
for remote in ["", "file:///tmp/repo", "github.com/openai", "/tmp/repo"] {
|
||||
assert_eq!(canonicalize_git_remote_url(remote), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ pub use errors::GitToolingError;
|
||||
pub use info::CommitLogEntry;
|
||||
pub use info::GitDiffToRemote;
|
||||
pub use info::GitInfo;
|
||||
pub use info::canonicalize_git_remote_url;
|
||||
pub use info::collect_git_info;
|
||||
pub use info::current_branch_name;
|
||||
pub use info::default_branch_name;
|
||||
|
||||
Reference in New Issue
Block a user