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:
alexsong-oai
2026-05-08 12:16:24 -07:00
committed by GitHub
Unverified
parent 9183503b97
commit bbb6bf0a37
10 changed files with 882 additions and 27 deletions
+145
View File
@@ -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);
}
}
}
+1
View File
@@ -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;