Skip to content

fix: populate OTel repository attributes for non-GitHub git remotes - #330284

Open
samir-nimbly wants to merge 2 commits into
microsoft:mainfrom
samir-nimbly:fix/otel-git-repository-non-github-remotes
Open

fix: populate OTel repository attributes for non-GitHub git remotes#330284
samir-nimbly wants to merge 2 commits into
microsoft:mainfrom
samir-nimbly:fix/otel-git-repository-non-github-remotes

Conversation

@samir-nimbly

@samir-nimbly samir-nimbly commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #330239

Problem

On Copilot Chat invoke_agent spans, three OTel attributes were silently absent whenever the workspace's remote pointed at a self-hosted GitHub Enterprise Server instance on a custom domain (e.g. https://git.mycompany.com/owner/repo.git):

  • github.copilot.git.repository
  • copilot_chat.repo.remote_url
  • github.copilot.github.org

Meanwhile github.copilot.git.branch and github.copilot.git.commit_sha were present and correct on the same span. No error, no log line — the attributes just never appeared, so anyone pointing an OTLP collector at their agent turns lost repository correlation entirely.

The published monitoring docs state the repository attribute is emitted "when in a Git repo", so this was a documented-contract violation. GitLab, Bitbucket Server, and plain git servers were affected identically.

Root cause

All five attributes are produced by extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts.

buildWorkspaceMetadata read headBranchName and headCommitHash straight off RepoContext with no host check, but derived remoteUrl exclusively from:

const repoInfo = Array.from(getOrderedRepoInfosFromContext(repoContext))[0];

getOrderedRepoInfosFromContext (platform/git/common/gitService.ts) yields a remote only when getGithubRepoIdFromFetchUrl(url) ?? getAdoRepoIdFromFetchUrl(url) resolves. getGithubRepoIdFromFetchUrl hard-gates on const topLevelUrls = ['github.com', 'ghe.com'], and the Azure DevOps resolver only handles dev.azure.com, ssh.dev.azure.com, and *.visualstudio.com.

A custom GHES domain matches neither allowlist, so the generator yielded nothing, remoteUrl stayed undefined, and the if (metadata.remoteUrl) branch in workspaceMetadataToOTelAttributes never ran — dropping all three attributes at once. That is exactly the asymmetry described in the issue.

Secondary defect: extractGitHubOrg matched only /github\.com[/:]([^/]+)\/[^/]+\/?$/i, so it dropped the org even for *.ghe.com tenants and ssh host aliases (abc-github.com) that getGithubRepoIdFromFetchUrl already resolves correctly.

Changes

1. Host-agnostic remote URL, without changing any existing value

Two non-exported helpers. pickRecognizedRemoteUrl returns the highest-priority remote that resolves to a repo id; pickFallbackRemoteUrl returns the highest-priority remote on any host. The resolvable one is preferred:

const recognizedRemoteUrl = pickRecognizedRemoteUrl(repoContext);
const remoteUrl = recognizedRemoteUrl ?? pickFallbackRemoteUrl(repoContext);

This ordering matters. A naive host-agnostic rewrite regresses mixed-remote repositories: with origin pointing at a GitLab mirror and a second remote at GitHub, the old code reported the GitHub URL (plus github.org), and a first-parseable-wins implementation would silently switch that span to the GitLab URL and drop the org. Preferring the resolvable remote makes the change strictly additive — no repository that already reported a URL sees a different value. Three tests lock this behaviour.

Both passes reuse getOrderedRemoteUrlsFromContext, so remote prioritization (a lone remote → the upstream remote → origin → the rest) is unchanged.

2. Local remotes excluded from the fallback

Removing the host gate means arbitrary remote strings become eligible, including file:///Users/<name>/dev/repo and relative paths, which normalizeFetchUrl returns verbatim from its catch branch. The old allowlist suppressed those incidentally.

The fallback requires parseRemoteUrl(url) to resolve, which admits only ssh, https, and http. That alone is not sufficient, because scp-style syntax can smuggle a local path through an accepted scheme — git@localhost:/Users/alice/dev/repo.git normalizes to ssh://git@localhost//Users/alice/dev/repo.git and passes the scheme check. Loopback hosts (localhost, *.localhost, 127.0.0.0/8, ::1, 0.0.0.0) are therefore rejected on top of the scheme check.

Absolute paths on routable hosts are still reported: git@git.mycompany.com:/srv/git/repo.git is a normal self-hosted layout where the path describes the server's filesystem, not the user's.

3. The host-agnostic remote is scoped to OTel emission

WorkspaceOTelMetadata carries two remote values:

  • remoteUrl — any host. Used for the OTel attributes; this is what fixes the issue.
  • recognizedRemoteUrl — only remotes resolving to a repo id (github.com, *.ghe.com, Azure DevOps), i.e. exactly the set reportable before this PR.

Two consumers forward a remote to GitHub telemetry rather than to the user's own OTel exporter, and both now read recognizedRemoteUrl so their payloads are unchanged from main:

  • extension/conversation/vscode-node/userActions.tssendGHTelemetryEvent('inline.trackEditSurvival', ...)
  • platform/multiFileEdit/common/multiFileEditQualityTelemetry.tssendEnhancedGHTelemetryEvent('fastApply/editOutcome', ...)

A field split is used rather than an opt-in flag on resolveWorkspaceOTelMetadata because EditSurvivalReporter resolves a single metadata object that feeds both emitEditSurvivalEvent (OTel) and the GitHub event; a per-call flag would have forced the OTel path to lose the fix there too.

4. extractGitHubOrg delegates to the canonical resolver

function extractGitHubOrg(remoteUrl: string): string | undefined {
    return getGithubRepoIdFromFetchUrl(remoteUrl)?.org;
}

Reuses the allowlist read-only. This keeps the org attribute scoped to recognized GitHub hosts (preserving the existing omits github.org for non-github remotes test) while recovering the org for *.ghe.com tenants and ssh host aliases that the previous regex dropped.

5. Pre-existing crash, surfaced by the new tests

Adding a remoteFetchUrls: [undefined] case produced:

TypeError: Cannot read properties of undefined (reading 'trim')
    at parseRemoteUrl (gitService.ts:166)
    at getGithubRepoIdFromFetchUrl (gitService.ts:245)
    at getOrderedRepoInfosFromContext (gitService.ts:117)

Strategy 1 of getOrderedRemoteUrlsFromContext asserted out.add(repoContext.remoteFetchUrls[0]!), but a push-only remote carries no fetch URL, so undefined escaped the declared Iterable<string> and threw. This predates this PR and also reaches extension/prompt/node/repoInfoTelemetry.ts. Fixed at source with a truthy guard; strategies 2, 3, and the trailing loop already guarded correctly.

6. Documentation

  • docs/monitoring/agent_monitoring.md — the github.copilot.github.org row now names the full recognized set (github.com, *.ghe.com, and ssh host aliases normalizing to either) and notes it is absent for custom-domain GHES.
  • genAiAttributes.ts — the GITHUB_ORG comment said "gated like the URL itself"; the URL is no longer gated, so it now describes the actual condition, including ssh host aliases.
  • The extractGitHubOrg JSDoc carried the same omission and is corrected to match.

Explicitly out of scope

The repo id allowlist is untouched. getGithubRepoIdFromFetchUrl, getAdoRepoIdFromFetchUrl, and getOrderedRepoInfosFromContext keep their exact current semantics. They feed GitHub API calls, PR creation, the coding agent, and repoId/repoType telemetry across roughly fifteen call sites; returning a GithubRepoId for a GitLab host would be wrong and would change outbound API behaviour. The fix is confined to the OTel metadata path.

github.copilot.github.org remains absent for custom-domain GHES. Distinguishing a self-hosted GHES domain from an arbitrary git host requires reading the github-enterprise.uri setting. platform/otel/common/ currently has no dependency injection, and threading IConfigurationService through would mean constructor churn at call sites that do not inject it (editSurvivalReporter, multiFileEditQualityTelemetry, userActions) for a single telemetry string. Since github.copilot.git.repository is now always populated, a backend can derive the org from the URL. A test documents this limitation explicitly. Happy to follow up if maintainers would prefer the attribute plumbed through.

The following are the same class of bug in core, filed separately rather than folded in here:

  • src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts classifies *.ghe.com as remoteVendor: 'other' and omits ssh.dev.azure.com, disagreeing with src/vs/platform/agentHost/node/agentHostRepoInfoTelemetry.ts.
  • src/vs/workbench/contrib/git/common/utils.ts hasGitHubRemotes uses an un-dot-anchored endsWith('github.com'), so evilgithub.com passes. The function currently has no callers.
  • src/extension/inlineChat2/node/inlineChatIntent.ts creates an invoke_agent Inline Chat span that sets AGENT_TYPE but never spreads the workspace git attributes at all.

Tests

20 new cases. src/platform/otel/common/test/workspaceOTelMetadata.spec.ts:

Case Asserts
GHES custom domain, https the reported regression — URL now emitted
GHES custom domain, scp/ssh git@host:owner/repo.git normalizes to https://host/owner/repo.git
GitLab URL emitted
Bitbucket Server /scm/ path /scm/ segment stripped by normalizeFetchUrl
Azure DevOps unchanged
github.com unchanged (tightened from a loose toContain to an exact match)
GitHub remote beats higher-priority unknown host no regression for mixed-remote repos
ADO remote beats higher-priority unknown host no regression for mixed-remote repos
fallback only when nothing resolves fallback ordering
upstream priority preserved for unknown hosts prioritization intact
file://, absolute path, relative path rejected — local remotes
loopback: scp, ssh://, 127.0.0.1, *.localhost, https with port, [::1] rejected
loopback skipped, later routable remote reported guard does not swallow the whole repo
absolute path on a routable host still reported
recognizedRemoteUrl for github.com / Azure DevOps / custom GHES set, set, undefined
recognizedRemoteUrl in a mixed-remote repo pinned to the resolvable remote
lone remote with no fetch URL undefined, no throw
*.ghe.com github.org now derived (previously dropped)
ssh host alias github.org derived
custom-domain GHES attributes URL and legacy key set, github.org absent — documents the limitation
Bitbucket Server attributes URL set, github.org absent

src/platform/git/test/node/gitService.spec.ts adds direct coverage for getOrderedRemoteUrlsFromContext (lone remote with and without a fetch URL, upstream ordering) and getOrderedRepoInfosFromContext (no throw on a missing fetch URL, unknown hosts skipped).

Verification

Gate Result
npx vitest --run --pool=forks src/platform/otel src/platform/git src/platform/multiFileEdit src/platform/editSurvivalTracking PASS 333, FAIL 0
npm run typecheck (all four tsconfig projects) exit 0, 0 errors
npx eslint on all changed files exit 0, 0 errors, 0 warnings
Full npm run test:unit vs. stashed baseline 39 failed files / 425 failed tests identical both ways; the passing count differs only by the tests this PR adds

The 39 pre-existing failures are environmental in this checkout — they need dist/*.wasm tree-sitter builds and a seeded SQLite model-metadata cache. The stashed-baseline run confirms none of them are attributable to this change.

Not performed locally: an end-to-end run against a live GHES remote with "github.copilot.chat.otel.enabled": true and a real Copilot session.

Notes for reviewers

Two behavioural consequences worth a deliberate look:

  • New values, confined to the user's own exporter. Self-hosted corporate git hostnames (git.mycompany.com) now appear where they previously did not, but only on spans exported to the OTLP endpoint the user configured. Following review, the two consumers that forward a remote to GitHub telemetry (inline.trackEditSurvival and fastApply/editOutcome) read recognizedRemoteUrl and so collect exactly what they collected on main.
  • Cardinality. github.copilot.git.repository moves from GitHub/ADO hosts only to every host, so distinct-value cardinality on that attribute will rise. Worth a look from whoever owns OTel backend cost.

Local-path exposure is addressed by the loopback rejection described above. Credential exposure is unchanged: normalizeFetchUrl rebuilds from hostname + pathname, dropping userinfo, port, and query; the scp branch drops the user; and a user:pass@ form does not match the scp regex, so it goes down the credential-stripping new URL() path.

`github.copilot.git.repository`, `copilot_chat.repo.remote_url`, and
`github.copilot.github.org` were silently absent on `invoke_agent` spans for
workspaces whose remote points at a self-hosted GitHub Enterprise Server
instance on a custom domain, while `github.copilot.git.branch` and
`github.copilot.git.commit_sha` were present on the same span. GitLab,
Bitbucket Server, and plain git servers were affected the same way.

`buildWorkspaceMetadata` derived the remote URL exclusively from
`getOrderedRepoInfosFromContext`, which only yields remotes that resolve to a
repo id via the `['github.com', 'ghe.com']` allowlist or the Azure DevOps
hosts. Branch and commit are read straight off `RepoContext` with no host
check, hence the asymmetry.

Add a `pickRemoteUrl` helper that prefers a remote resolving to a repo id, so
repositories that already reported a URL keep reporting the same one, and only
falls back to the highest-priority remote of any host when nothing resolves.
The fallback requires `parseRemoteUrl` to succeed, which admits only ssh, https
and http, keeping local remotes (`file://`, absolute and relative paths) out of
telemetry.

Rewrite `extractGitHubOrg` to delegate to `getGithubRepoIdFromFetchUrl` instead
of a `github.com`-only regex. This keeps the org attribute scoped to recognized
GitHub hosts while recovering it for `*.ghe.com` and ssh host aliases, which
the regex previously dropped. It remains absent for custom-domain GitHub
Enterprise Server, which cannot be distinguished from an arbitrary git host
without reading `github-enterprise.uri`.

Also fix an unrelated latent crash surfaced by the new tests: strategy 1 of
`getOrderedRemoteUrlsFromContext` asserted `remoteFetchUrls[0]!`, but a
push-only remote carries no fetch URL, so `undefined` escaped the declared
`Iterable<string>` and threw inside `parseRemoteUrl`.

The repo id allowlist, `getOrderedRepoInfosFromContext`, and every consumer
that depends on repo identity for GitHub API calls are left untouched.

Fixes microsoft#330239

Co-authored-by: Mohammed Samir <51956976+SamirSaji@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 11, 2026 16:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes repository metadata for non-GitHub remotes in Copilot OTel spans while preserving existing remote priority.

Changes:

  • Adds host-agnostic remote URL fallback and GitHub organization resolution.
  • Handles missing fetch URLs safely.
  • Expands tests and monitoring documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
workspaceOTelMetadata.ts Selects and normalizes arbitrary-host remotes.
workspaceOTelMetadata.spec.ts Tests remote selection and OTel attributes.
genAiAttributes.ts Updates repository attribute documentation.
gitService.ts Guards missing lone-remote fetch URLs.
gitService.spec.ts Tests remote ordering and missing URLs.
agent_monitoring.md Documents GitHub organization availability.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts Outdated
Comment thread extensions/copilot/docs/monitoring/agent_monitoring.md Outdated
Comment thread extensions/copilot/src/platform/otel/common/genAiAttributes.ts
Scope the host-agnostic remote to OTel emission. `WorkspaceOTelMetadata` now
carries both `remoteUrl` (any host, for OTel attributes) and
`recognizedRemoteUrl` (restricted to remotes resolving to a repo id). The two
consumers that forward a remote to GitHub telemetry rather than to the user's
own OTel exporter — `inline.trackEditSurvival` in userActions and
`fastApply/editOutcome` in MultiFileEditInternalTelemetryService — now read
`recognizedRemoteUrl`, so those channels collect exactly what they collected
before and the OTel fix carries no privacy expansion.

Reject loopback hosts in the fallback. `parseRemoteUrl` admits ssh, https, and
http, so scp-style syntax could smuggle a local filesystem path through an
accepted scheme (`git@localhost:/Users/alice/dev/repo.git`). Loopback hosts
(localhost, *.localhost, 127.0.0.0/8, ::1, 0.0.0.0) are now skipped. Absolute
paths on routable hosts are still reported, since those describe the server's
layout rather than the user's.

Document that ssh host aliases normalizing to github.com or *.ghe.com also
produce `github.copilot.github.org`, in both the attribute reference and the
monitoring docs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

github.copilot.git.repository not populated for self-hosted GitHub Enterprise Server remotes with custom domains

4 participants