# Technical Documentation: microsoft/vscode

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/microsoft/vscode)

### Overview (README.md)

# Visual Studio Code - Open Source ("Code - OSS")
[](https://github.com/microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc)
[](https://github.com/microsoft/vscode/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3Abug)

## The Repository

This repository ("`Code - OSS`") is where we (Microsoft) develop the [Visual Studio Code](https://code.visualstudio.com) product together with the community. Not only do we work on code and issues here, but we also publish our [roadmap](https://github.com/microsoft/vscode/wiki/Roadmap), [monthly iteration plans](https://github.com/microsoft/vscode/wiki/Iteration-Plans), and our [endgame plans](https://github.com/microsoft/vscode/wiki/Running-the-Endgame). This source code is available to everyone under the standard [MIT license](https://github.com/microsoft/vscode/blob/main/LICENSE.txt).

## Visual Studio Code

<p align="center">
  <img alt="VS Code in action" src="https://github.com/user-attachments/assets/56af271c-949d-454c-a3ea-16188c063414">
</p>

[Visual Studio Code](https://code.visualstudio.com) is a distribution of the `Code - OSS` repository with Microsoft-specific customizations released under a traditional [Microsoft product license](https://code.visualstudio.com/License/).

[Visual Studio Code](https://code.visualstudio.com) combines the simplicity of a code editor with what developers need for their core edit-build-debug cycle. It provides comprehensive code editing, navigation, and understanding support along with lightweight debugging, a rich extensibility model, and lightweight integration with existing tools.

Visual Studio Code is updated monthly with new features and bug fixes. You can download it for Windows, macOS, and Linux on the [Visual Studio Code website](https://code.visualstudio.com/Download). To get the latest releases every day, install the [Insiders build](https://code.visualstudio.com/insiders).

## Contributing

There are many ways in which you can participate in this project, for example:

* [Submit bugs and feature requests](https://github.com/microsoft/vscode/issues), and help us verify them as they are checked in
* Review [source code changes](https://github.com/microsoft/vscode/pulls)
* Review the [documentation](https://github.com/microsoft/vscode-docs) and make pull requests for anything from typos to new content.

If you are interested in fixing issues and contributing directly to the codebase, please see the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute), which covers the following:

* [How to build and run from source](https://github.com/microsoft/vscode/wiki/How-to-Contribute)
* [The development workflow, including debugging and running tests](https://github.com/microsoft/vscode/wiki/How-to-Contribute#debugging)
* [Coding guidelines](https://github.com/microsoft/vscode/wiki/Coding-Guidelines)
* [Submitting pull requests](https://github.com/microsoft/vscode/wiki/How-to-Contribute#pull-requests)
* [Finding an issue to work on](https://github.com/microsoft/vscode/wiki/How-to-Contribute#where-to-contribute)
* [Contributing to translations](https://aka.ms/vscodeloc)

## Feedback

* Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/vscode)
* [Request a new feature](CONTRIBUTING.md)
* Upvote [popular feature requests](https://github.com/microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc)
* [File an issue](https://github.com/microsoft/vscode/issues)
* Connect with the extension author community on [GitHub Discussions](https://github.com/microsoft/vscode-discussions/discussions) or [Slack](https://aka.ms/vscode-dev-community)
* Follow [@code](https://x.com/code) and let us know what you think!

See our [wiki](https://github.com/microsoft/vscode/wiki/Feedback-Channels) for a description of each of these channels and information on some other available community-driven channels.

## Related Projects

Many of the core components and extensions to VS Code live in their own repositories on GitHub. For example, the [node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter](https://github.com/microsoft/vscode-mono-debug) repositories are separate from each other. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki).

## Bundled Extensions

VS Code includes a set of built-in extensions located in the [extensions](extensions) folder, including grammars and snippets for many languages. Extensions that provide rich language support (inline suggestions, Go to Definition) for a language have the suffix `language-features`. For example, the `json` extension provides coloring for `JSON` and the `json-language-features` extension provides rich language support for `JSON`.

## Development Container

This repository includes a Visual Studio Code Dev Containers / GitHub Codespaces development container.

* For [Dev Containers](https://aka.ms/vscode-remote/download/containers), use the **Dev Containers: Clone Repository in Container Volume...** command, which creates a Docker volume for better disk I/O on macOS and Windows.
  * If you already have VS Code and Docker installed, you can also click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/vscode) to get started. This will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.

* For Codespaces, install the [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) extension in VS Code, and use the **Codespaces: Create New Codespace** command.

Docker / the Codespace should have at least **4 cores and 6 GB of RAM (8 GB recommended)** to run a full build. See the [development container README](.devcontainer/README.md) for more information.

## Code of Conduct

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

## License

Copyright (c) Microsoft Corporation. All rights reserved.

Licensed under the [MIT](LICENSE.txt) license.

---

### Extensions Copilot Docs NES EXPECTED EDIT CAPTURE (`extensions/copilot/docs/NES_EXPECTED_EDIT_CAPTURE.md`)

# NES Expected Edit Capture Feature

## Overview

A feature that allows users to record/capture their "expected suggestion" when a Next Edit Suggestion (NES) was rejected or failed to appear. The captured data is saved in `.recording.w.json` format (compatible with stest infrastructure) for analysis and model improvement.

## Getting Started

### 1. Enable the Feature
Add this setting to your VS Code `settings.json`:

```json
{
  // Enable the capture feature
  "github.copilot.chat.advanced.inlineEdits.recordExpectedEdit.enabled": true
}
```

That's it! Auto-capture on rejection is enabled by default. To disable it (you can still capture manually via the Command Palette):
```json
{
  "github.copilot.chat.advanced.inlineEdits.recordExpectedEdit.onReject": false
}
```

### 2. Capture an Expected Edit

**When NES shows a wrong suggestion:**
1. Reject the suggestion (press `Esc` or continue typing)
2. If `onReject` is enabled, capture mode starts automatically
3. Type the code you *expected* NES to suggest
4. Press **Cmd+Enter** (Mac) / **Ctrl+Enter** (Windows/Linux) to save, or **Esc** to cancel

**When NES didn't appear but should have:**
1. Open the Command Palette (**Cmd+Shift+P** / **Ctrl+Shift+P**) and run **"Copilot: Record Expected Edit (NES)"**
2. Type the code you expected NES to suggest
3. Press **Cmd+Enter** / **Ctrl+Enter** to save

> **Tip:** To indicate that *no* edit was expected (i.e. the rejection was correct), press **Cmd+Enter** / **Ctrl+Enter** without making any edits.

### 3. Submit Your Feedback
Once you've captured some edits:
1. Open Command Palette (**Cmd+Shift+P** / **Ctrl+Shift+P**)
2. Run **"Copilot: Submit NES Captures"**
3. Review the files to be included (you can exclude sensitive files)
4. Click **Submit Feedback** to create a PR

### Quick Reference

| Action | Keybinding |
|--------|------------|
| Start capture manually | Command Palette → **Copilot: Record Expected Edit (NES)** |
| Save capture | **Cmd+Enter** / **Ctrl+Enter** |
| Save as "no edit expected" | **Cmd+Enter** / **Ctrl+Enter** with no edits made |
| Cancel capture | **Esc** |

| Command | Description |
|---------|-------------|
| Copilot: Record Expected Edit (NES) | Start a capture session |
| Copilot: Submit NES Captures | Upload feedback to internal repo |

## How It Works

### Trigger Points
- **Automatic**: Capture starts when you reject an NES suggestion (if `onReject` setting is enabled)
- **Manual**: Run **"Copilot: Record Expected Edit (NES)"** from the Command Palette when NES didn't appear but should have

### Capture Session
When capture mode is active:
1. An animated status bar indicator shows: **"NES CAPTURE MODE ACTIVE"** (with an error background for visibility). Hovering it reveals the available keybindings.
2. Type your expected edit naturally in the editor (Enter inserts a newline as usual)
3. Press **Cmd+Enter** / **Ctrl+Enter** to save (saving with no edits records "no edit expected"), or **Esc** to cancel

### Where Captures Are Saved
Recordings are stored in your workspace under `.copilot/nes-feedback/`:
- `capture-<timestamp>.recording.w.json` — The edit recording
- `capture-<timestamp>.metadata.json` — Context about the capture

---

## Technical Reference

### Commands

| Command ID | Description |
|------------|-------------|
| `github.copilot.nes.captureExpected.start` | Start capture manually |
| `github.copilot.nes.captureExpected.confirm` | Confirm and save |
| `github.copilot.nes.captureExpected.abort` | Cancel capture |
| `github.copilot.nes.captureExpected.submit` | Submit to `microsoft/copilot-nes-feedback` |

### Architecture

#### State Management
The capture controller maintains minimal state:
```typescript
{
  active: boolean;
  startBookmark: DebugRecorderBookmark;
  endBookmark?: DebugRecorderBookmark;
  startDocumentId: DocumentId;
  startTime: number;
  trigger: 'rejection' | 'manual';
  originalNesMetadata?: {
    requestUuid: string;
    providerInfo?: string;
    modelName?: string;
    endpointUrl?: string;
    suggestionText?: string;
    // [startLine, startCharacter, endLine, endCharacter]
    suggestionRange?: [number, number, number, number];
    documentPath?: string;
  };
}
```

### Implementation Flow

The capture flow leverages **DebugRecorder**, which already tracks all document edits automatically—no custom event listeners or manual diff computation needed.

1. **Start Capture**: Create a bookmark in DebugRecorder, store the current document ID, set context key `copilotNesCaptureMode` to enable keybindings, and show status bar indicator.

2. **User Edits**: User types their expected edit naturally in the editor. DebugRecorder automatically tracks all changes in the background.

3. **Confirm Capture**: Create an end bookmark, extract the log slice between start/end bookmarks, filter for edits on the target document, compose them into a single `nextUserEdit`, and save to disk.

4. **Abort/Cleanup**: Clear state, reset context key, and dispose status bar item.

See `ExpectedEditCaptureController` in [vscode-node/components/expectedEditCaptureController.ts](vscode-node/components/expectedEditCaptureController.ts) for the full implementation.

### File Output

#### Location
Recordings are stored in the **first workspace folder** under the `.copilot/nes-feedback/` directory:

- **Full path**: `<workspaceFolder>/.copilot/nes-feedback/capture-<timestamp>.recording.w.json`
- **Timestamp format**: ISO 8601 with colons/periods replaced by hyphens (e.g., `2025-12-04T14-30-45`)
- **Example**: `.copilot/nes-feedback/capture-2025-12-04T14-30-45.recording.w.json`
- The folder is automatically created if it doesn't exist

Each recording generates two files:
1. **Recording file**: `capture-<timestamp>.recording.w.json` - Contains the log and edit data
2. **Metadata file**: `capture-<timestamp>.metadata.json` - Contains capture context and timing

#### Format
Matches existing `.recording.w.json` structure used by stest infrastructure:

```json
{
  "log": [
    {
      "kind": "header",
      "repoRootUri": "file:///workspace",
      "time": 1234567890,
      "uuid": "..."
    },
    {
      "kind": "documentEncountered",
      "id": 0,
      "relativePath": "src/foo.ts",
      "time": 1234567890
    },
    {
      "kind": "setContent",
      "id": 0,
      "v": 1,
      "content": "...",
      "time": 1234567890
    },
    ...
  ],
  "nextUserEdit": {
    "relativePath": "src/foo.ts",
    "edit": [
      [876, 996, "replaced text"],
      [1522, 1530, "more text"]
    ]
  }
}
```

#### Metadata File
A metadata file is saved alongside each recording with capture context:
```jsonc
{
  "captureTimestamp": "2025-11-19T...",    // ISO timestamp when capture started
  "trigger": "rejection",                   // How capture was initiated: 'rejection' or 'manual'
  "durationMs": 5432,                       // Time between start and confirm in milliseconds
  "noEditExpected": false,                  // True if user confirmed without making edits
  "originalNesContext": {                   // Metadata from the rejected NES suggestion (if any)
    "requestUuid": "...",                   // Unique ID of the NES request
    "providerInfo": "...",                  // Source of the suggestion (e.g., 'provider', 'diagnostics')
    "modelName": "...",                     // AI model that generated the suggestion
    "endpointUrl": "...",                   // API endpoint used for the request
    "suggestionText": "...",                // The actual suggested text that was rejected
    "suggestionRange": [10, 0, 15, 20]      // [startLine, startChar, endLine, endChar] of suggestion
  }
}
```

## Benefits

- **Zero-friction**: Type naturally, press Enter — no forms or dialogs
- **Works for both**: Rejected suggestions and missed opportunities
- **Privacy-aware**: Sensitive files are automatically filtered before submission

## Edge Cases

| Scenario | Behavior |
|----------|----------|
| **Multiple rapid rejections** | Only one capture active at a time; subsequent rejections ignored |
| **Document closed** | Capture automatically aborted |
| **No edits made** | Valid feedback! Saved with `noEditExpected: true` (indicates the rejection was correct) |
| **Large edits** | DebugRecorder handles size limits automatically |

## Feedback Submission

When you run **"Copilot: Submit NES Captures"**:

1. All captures from `.copilot/nes-feedback/` are collected
2. A preview dialog shows which files will be included
3. You can exclude specific files if needed
4. A pull request is created in `microsoft/copilot-nes-feedback`

### Privacy & Filtering
Sensitive files are **automatically excluded** from submissions:
- VS Code settings (`settings.json`, `launch.json`)
- Credentials (`.npmrc`, `.env`, `.gitconfig`, etc.)
- Private keys (`.pem`, `.key`, `id_rsa`, etc.)
- Sensitive directories (`.aws/`, `.ssh/`, `.gnupg/`)

**Requirements:** GitHub authentication with repo access to `microsoft/copilot-nes-feedback`

---

## Future Enhancements

- **Diff Preview**: Show visual comparison before saving
- **Category Tagging**: Quick-pick to categorize expectation type (import, refactor, etc.)
- **Auto-Generate stest**: Create `.stest.ts` wrapper file automatically

## Related Files

- [node/debugRecorder.ts](node/debugRecorder.ts) - Core recording infrastructure
- [vscode-node/components/inlineEditDebugComponent.ts](vscode-node/components/inlineEditDebugComponent.ts) - Existing feedback/debug tooling and sensitive file filtering
- [vscode-node/components/expectedEditCaptureController.ts](vscode-node/components/expectedEditCaptureController.ts) - Capture session management
- [vscode-node/components/nesFeedbackSubmitter.ts](vscode-node/components/nesFeedbackSubmitter.ts) - Feedback submission to GitHub
- [common/observableWorkspaceRecordingReplayer.ts](common/observableWorkspaceRecordingReplayer.ts) - Recording replay logic
- [../../../test/simulation/inlineEdit/inlineEditTester.ts](../../../test/simulation/inlineEdit/inlineEditTester.ts) - stest infrastructure

---

### Extensions Copilot Docs Tools (`extensions/copilot/docs/tools.md`)

## So you want to write a tool

New to LLM tools? Here are some starting resources
- https://code.visualstudio.com/api/extension-guides/tools
- https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview
- https://platform.openai.com/docs/guides/function-calling?api-mode=chat
- https://www.anthropic.com/engineering/building-effective-agents

This is aimed at adding tools to vscode-copilot-chat, but much of it would apply to tools in other extensions or MCP servers as well.

### Do we need a new tool?

First, consider whether a new built-in tool is needed. Tools should be built-in if they are related to core VS Code functionality or the core search/edit/terminal agent loop and are needed for common OOB scenarios. Consider whether the tool can be contributed from another extension instead. If the task can be done through normal terminal commands, then it may not need its own tool.

### Static part

First, add an entry in vscode-copilot's package.json under `contributes.languageModelTools`:
- ~~Give it a name that starts with `copilot_`- this pattern is protected for our use only~~
  - This is obsolete- new tools can use any name, I think matching `toolReferenceName` might be a good idea.
  - The existing `copilot_` tools will be renamed later.
- Give it a reasonable `toolReferenceName` and a localized `userDescription`.
  - `toolReferenceName` is the name used in the tool picker, and to reference the tool with `#`, and to add the tool to a mode or toolset.
  - Add it to a toolset in `contributes.languageModelToolSets`- new tools should be part of a toolset.
- Now write your `modelDescription`. This is what the LLM uses to decide whether to use your tool. This should _not_ be localized. Be very detailed:
  - What exactly does the tool do?
  - What kind of information does it return?
  - In what cases should the tool be used?
  - Read more [best practices](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview#best-practices-for-tool-definitions)
- If the tool takes input, add an `inputSchema`. This is a JSON schema which must describe an object with the properties that the tool takes. Describe the properties in detail. File paths should be absolute paths. Think carefully about which properties are `required`.
- In `toolNames.ts`, add entries to `ToolName`, `ContributedToolName`, `contributedToolNameToToolNames`. Follow the naming patterns of other tools. `ToolName` is the real name of your tool that the LLM will see. It should also be clear. A good pattern is to start with a verb, e.g. `read_file`.
- And remember to look for other tools that do similar things, and try to ensure your tool is aligned with them in the input it takes and the terminology it uses, and doesn't overlap in behavior. That will ensure that an LLM can understand how to use them together.

### Tool implementation part

Then, implement your tool in `src/extension/tools/node`:
- If your tool takes input, write an interface and be sure that it matches the schema in package.json exactly, including which properties are required.
- A typical tool can implement `vscode.LanguageModelTool`. More sophisticated tools can implement `ICopilotTool`, which gives you some extra functionality.
- Call `ToolRegistry.registerTool(YourTool);` and import your tool file in `allTools.ts`.
- Is your tool relevant in simulator/swebench scenarios? If so, check that it works.
- I recommend using prompt-tsx for your tool result if it's not a simple string. This lets you compose the result from multiple parts or reuse other prompt-tsx components.

### Input validation

- The input will be validated against the schema in package.json, so you don't need to repeat that validation in your tool.
- When taking paths from the LLM as input, use `IPromptPathRepresentationService`.

### Error handling

If something goes wrong, throw an error with a message that will make sense to the LLM. It will be caught by the agent and shown to the LLM. Should the model call your tool again with different arguments, or do something different? Make sure the model can understand what to do next.

### Tool confirmations

If the tool has a potentially dangerous side-effect (e.g. the terminal tool), it MUST ask for the user's confirmation before running. Do this by returning `PreparedToolInvocation.confirmationMessages`. Give enough context in the confirmation message for the user to understand what the tool will do, and what the risk is. The `message` can be a markdown string containing a codeblock.

### Make it look good

- Fill out `PreparedToolInvocation.invocationMessage` and `pastTenseMessage` with a helpful message to show in the UI.
- Don't add your own `...` to the end of the tool message
- If you want the tool message to react to the result of the tool, you can use `ExtendedLanguageModelToolResult.toolResultMessage`.
- Use markdown where appropriate.
- Setting `toolResultDetails` will make the tool message an expandable list of URIs to show the tool's result. (e.g. file search, text search)



- If you want a clickable file widget in the tool message (e.g. read file), set `ExtendedLanguageModelToolResult.toolResultMessage` to a MarkdownString, using `formatUriForFileWidget`. This currently can't be combined with the `toolResultDetails` option.



### Testing

Consider writing a unit test for your tool. One example to copy is [`readFile.spec.tsx`](https://github.com/microsoft/vscode-copilot/blob/a2b8af8b8e7286d4da77ff4108b6bcdeb1441d79/src/extension/tools/node/test/readFile.spec.tsx#L40-L59). This test invokes the tool with some hardcoded arguments and checks the result against a snapshot.

## Model-Specific Tools

Model-specific tools allow you to provide tool implementations that are only available for certain language models (e.g., Gemini, Claude, GPT-5). This is useful when:
- A model has unique capabilities that require a specialized tool
- You want to adjust tool descriptions/schemas to work better with a specific model
- You need to override an existing tool's behavior for certain models

### When to use model-specific tools

Use model-specific tools when:
- The tool leverages model-specific capabilities (e.g., native model features)
- You need different tool descriptions or schemas that work better with certain models
- You want to override behavior of an existing tool for specific models

### Registering a model-specific tool

Register model-specific tools using `ToolRegistry.registerModelSpecificTool`:

```typescript
class MyGeminiTool implements ICopilotModelSpecificTool<IMyToolInput> {
	async invoke(
		options: vscode.LanguageModelToolInvocationOptions<IMyToolInput>,
		token: vscode.CancellationToken
	): Promise<vscode.LanguageModelToolResult> {
		// Gemini-specific implementation
		return { content: [{ type: 'text', value: 'Result' }] };
	}
}

// Register with a model selector
const disposable = ToolRegistry.registerModelSpecificTool(
	{
		name: 'my_gemini_tool',
		displayName: 'My Gemini Tool',
		description: 'A tool optimized for Gemini models',
		inputSchema: {
			type: 'object',
			properties: {
				query: { type: 'string', description: 'The query to process' }
			},
			required: ['query']
		},
		// Only available for Gemini 3:
		models: [{ family: 'gemini-3-pro' }]
	},
	MyGeminiTool
);
```

### Overriding existing tools

If your model-specific tool should **replace** an existing tool for certain models, use the `overridesTool` property:

```typescript
class MyGeminiSearchTool extends GenericGrepSearchTool {
	public readonly overridesTool = ToolName.GrepSearch;
		// Gemini-optimized search implementation
	}
}

ToolRegistry.registerModelSpecificTool(
	{
		name: 'gemini_grep_search',
		displayName: 'Search (Gemini)',
		description: 'Optimized grep search for Gemini',
		models: [{ family: 'gemini' }],
		inputSchema: { /* ... */ }
	},
	MyGeminiSearchTool
);
```

When `overridesTool` is set:
- The model-specific tool is **not** individually selectable in the UI
- It automatically replaces the base tool when enabled and the model matches
- The base tool must be registered and enabled for the override to work

### Read the prompt

Read the prompt. There is no replacement for just using your tool a lot, and reading the prompt. Read the whole thing top to bottom. What story does it tell? Get familiar with the prompt as a whole, don't get tunnel vision for one message. Does your new tool result make sense to you as a human? Is it formatted in a way that's consistent with other tool results and context in the user message?

---

### Extensions Copilot Docs Monitoring Agent Monitoring (`extensions/copilot/docs/monitoring/agent_monitoring.md`)

# Monitoring Agent Usage with OpenTelemetry

Local Copilot Chat can export **traces**, **metrics**, and **events** via [OpenTelemetry](https://opentelemetry.io/) (OTel) — giving you real-time visibility into extension-host agent interactions, LLM calls, tool executions, and token usage.

This document covers only the local Copilot Chat pipeline configured by `github.copilot.chat.otel.*`. Agent Host sessions for Copilot, Claude, and Codex run in a separate process, use `chat.agentHost.otel.*`, and are documented in [`src/vs/platform/agentHost/OTEL.md`](../../../../src/vs/platform/agentHost/OTEL.md).

Standard `gen_ai.*` signals follow the [OTel GenAI Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/). Extension-specific signals use the `github.copilot.*` and legacy `copilot_chat.*` namespaces. All signals can be exported to OTel-compatible backends such as Jaeger, Grafana, Azure Monitor, Datadog, Honeycomb, and more.

## User-perceived first progress

The shared VS Code UI timer also emits the content-free metadata span
`vscode.chat.user_perceived_time_to_first_progress` through this extension's
OTel pipeline. Its `vscode.chat.user_interaction.timeToFirstProgress` attribute
is UI submission to meaningful visible text, reasoning, or tool progress plus
two animation frames, in milliseconds. This is a rendering approximation,
not model TTFT or exact screen paint. Read the attribute, not the export span's
duration.

OTel must be enabled, but content capture and VS Code product telemetry need not
be enabled. The internal `github.copilot.chat.otel.recordUserInteraction` command
validates and allowlists the shared renderer record. Normal OTel processors
export it to the configured destination and SQLite store. The command waits for
OTel initialization and exporter flush before acknowledging, so the shared
`_chat.flushUserInteractionTelemetry` command also drains local timing exports.
The UI never activates the extension solely to export a timing.

Unsuccessful observations retain their outcome and time to termination, without
inventing first-progress latency. Renderer identity and submission ordinal
distinguish first submissions from followups without relying on export order.
See the [shared wire contract](../../../../src/vs/platform/agentHost/OTEL.md#user-perceived-first-progress)
for fields, visibility semantics, and export-readiness behavior. Agent Host uses
its own exporter rather than this command.

## Quick Start

The fastest way to see Copilot Chat traces locally — no cloud account required. This guide uses the [Aspire Dashboard](https://aspire.dev/dashboard/standalone/), a lightweight container image from Microsoft that provides a trace viewer with a built-in OTLP endpoint. It can be used standalone, without the rest of .NET Aspire.

### Prerequisites

- **Docker** installed
- **VS Code** with the GitHub Copilot Chat extension

### 1. Start the Aspire Dashboard

```bash
docker run --rm -d \
  -p 18888:18888 \
  -p 4318:18890 \
  --name aspire-dashboard \
  mcr.microsoft.com/dotnet/aspire-dashboard:latest
```

This exposes the dashboard UI on port `18888` and an OTLP (HTTP) endpoint on port `4318`.

### 2. Configure VS Code

Open **Settings** (`Ctrl+,`) and add:

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.captureContent": true
}
```

> **Note:** You can also use environment variables instead of VS Code settings (see [Configuration](#configuration)). Environment variables can still override some legacy policy-backed settings; identity capture and managed resource attributes enforce policy precedence. See [governed identity capture](#governed-identity-capture) and the [activation limitations](#activation).

### 3. Generate Telemetry

Open Copilot Chat and send any message — for example, ask a question in Agent mode.

### 4. View Traces

Open http://localhost:18888 → **Traces**. You'll see `invoke_agent` spans with nested `chat` and `execute_tool` children.



### Teardown

```bash
docker stop aspire-dashboard
```

> **Tip:** See the [Aspire Dashboard standalone docs](https://aspire.dev/dashboard/standalone/) for more configuration options.

---

## Configuration

### VS Code Settings

Open **Settings** (`Ctrl+,`) and search for `copilot otel`:

| Setting | Type | Default | Description |
|---|---|---|---|
| `github.copilot.chat.otel.enabled` | boolean | `false` | Enable OTel emission |
| `github.copilot.chat.otel.exporterType` | string | `"otlp-http"` | `otlp-http`, `otlp-grpc`, `console`, or `file` |
| `github.copilot.chat.otel.otlpEndpoint` | string | `"http://localhost:4318"` | OTLP collector endpoint |
| `github.copilot.chat.otel.captureContent` | boolean | `false` | Capture full prompt/response content |
| `github.copilot.chat.otel.captureIdentity` | boolean or null | `null` (off) | Capture authenticated account, OS username, and hostname independently of content. Environment overrides personal preferences; explicit managed policy always wins. |
| `github.copilot.chat.otel.protocol` | string | `""` | OTLP wire protocol: `http/json` (default), `http/protobuf`, or `grpc` |
| `github.copilot.chat.otel.serviceName` | string | `""` | Override the `service.name` resource attribute |
| `github.copilot.chat.otel.resourceAttributes` | object | `{}` | Extra resource attributes (`{ "key": "value" }`) |
| `github.copilot.chat.otel.headers` | object | `{}` | Extra OTLP exporter headers, applied directly to the exporter (`{ "key": "value" }`) |
| `github.copilot.chat.otel.maxAttributeSizeChars` | integer | `0` | Max characters per OTel content attribute (prompts, tool args/results, hook input/output). `0` (the default) disables truncation so backends with no per-attribute limit get full payloads. Set to a positive value to match your backend's per-attribute size limit — consult your backend's documentation. The value counts JavaScript string characters (UTF-16 code units); for non-ASCII content one character can be multiple UTF-8 bytes on the wire. |
| `github.copilot.chat.otel.outfile` | string | `""` | File path for JSON-lines output |
| `github.copilot.chat.otel.dbSpanExporter.enabled` | boolean | `false` | Persist OTel spans to a local SQLite database for the **Chat: Export Agent Traces DB** command. Implicitly enables OTel. |

### Environment Variables

Except for governed identity and managed resource attributes, environment variables retain
their existing precedence. When enterprise OTel configuration is
recognized through the application-scoped policy defaults, the entire Copilot OTel settings
block comes from those policy values and schema defaults. Personal `settings.json` values are
not used to fill omitted fields: headers and resource attributes default to empty maps, not
the user's maps. Other VS Code settings are unaffected.

An identity-only managed block (including `telemetry.capture.identity: false`) is
recognizable enterprise OTel configuration. It therefore also replaces personally
enabled export, endpoints, headers, and DB export with policy/schema defaults.
To keep export enabled under that block, administrators should configure `enabled`
and the intended exporter destination as well. Identity omission remains distinct
from denial; it does not by itself prevent personal/environment identity opt-in.

| Variable | Default | Description |
|---|---|---|
| `COPILOT_OTEL_ENABLED` | `false` | Enable OTel. Also enabled when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. |
| `COPILOT_OTEL_ENDPOINT` | — | OTLP endpoint URL (takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`) |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | — | Standard OTel OTLP endpoint URL |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/json` | OTLP wire protocol. `grpc` selects gRPC, `http/protobuf` selects HTTP protobuf, and other values use HTTP JSON. |
| `COPILOT_OTEL_PROTOCOL` | — | Fallback protocol when `OTEL_EXPORTER_OTLP_PROTOCOL` is unset (`grpc`, `http/protobuf`, or HTTP JSON otherwise). |
| `OTEL_SERVICE_NAME` | `copilot-chat` | Service name in resource attributes |
| `OTEL_RESOURCE_ATTRIBUTES` | — | Extra resource attributes (`key1=val1,key2=val2`) |
| `COPILOT_OTEL_CAPTURE_CONTENT` | `false` | Capture full prompt/response content |
| `COPILOT_OTEL_CAPTURE_IDENTITY` | `false` | Opt into identity capture unless explicitly denied by managed policy. |
| `COPILOT_OTEL_MAX_ATTRIBUTE_SIZE_CHARS` | `0` | Override the max character size for OTel content attributes. `0` (default) disables truncation; set to a positive value when your backend has a per-attribute limit. Takes precedence over the `maxAttributeSizeChars` setting. |
| `COPILOT_OTEL_LOG_LEVEL` | `info` | Min log level: `trace`, `debug`, `info`, `warn`, `error` |
| `COPILOT_OTEL_FILE_EXPORTER_PATH` | — | Write all signals to this file (JSON-lines) |
| `COPILOT_OTEL_HTTP_INSTRUMENTATION` | `false` | Enable HTTP-level OTel instrumentation |
| `OTEL_EXPORTER_OTLP_HEADERS` | — | Auth headers (e.g., `Authorization=Bearer token`) |

### Governed identity capture

Managed `telemetry.capture.identity` is independent of `captureContent` and
`lockCaptureContent`: neither content control enables or denies identity. An explicit
managed `false` overrides personal settings and environment opt-in. Omission is not
a denial and leaves the identity preference available, including when other managed
OTel settings replace the personal configuration block.

Across managed sources, the highest-priority telemetry block wins as a whole:
native MDM, then server-delivered settings, then the managed-settings file.
Omitted identity, resource attributes, and other telemetry leaves are not filled
from weaker managed sources. For server and file object sources, an empty block,
or one containing only unsupported fields, still replaces the weaker block.
Native MDM exposes declared flat keys, not a telemetry object: only an observed
declared telemetry key establishes its block. Empty or unknown-only native blocks
cannot be observed. Removing all observable telemetry keys (or the server/file
block entirely) allows the next managed source to apply. This does not change
precedence for unrelated managed settings or the native runtime's own transport.
Native delivery also observes the other recognized `telemetry.capture.*` controls
for block selection only; this does not implement those content-capture classes
in Local or expose additional Local settings.

When enabled, `user.name` is the current authenticated provider account name on
top-level, subagent, and inline `invoke_agent` spans. Each invocation reads the
authentication service's current session rather than caching an account name in
the telemetry service. Account changes and sign-out take effect once the
authentication service has refreshed its session.
No anonymous identity is invented, and `enduser.pseudo.id` is unchanged.
`process.user.name` and `host.name` are resource attributes. Explicit
`OTEL_RESOURCE_ATTRIBUTES` overrides detected OS/host values; managed
`resourceAttributes` overrides environment attributes. Identity attributes are
removed when capture is off, even if explicitly supplied as resource attributes.
If the OS username cannot be detected (for example, a container user has no passwd
entry), a warning is logged and telemetry continues without a detected
`process.user.name`. Hostname capture and explicitly configured resource values
remain available.

> **Compatibility note:** Explicit `host.name`, `process.user.name`, and `user.name`
> resource attributes from environment variables, personal settings, or managed
> `resourceAttributes` do not bypass the identity gate. They are removed by default
> and under an explicit managed denial, not just omitted from automatic detection.
> Other custom resource attributes are unaffected.

Enabling capture still requires the normal reload/recovery flow. Disabling is
checked before subsequent exports and local completion events, including queued
spans, logs, metrics, and SQLite export. These checks read cached state refreshed
on OTel configuration changes, without resolving settings per span. A denial is
latched when the configuration change is observed, even between exports, and stays in effect
for that service instance, even if policy later allows capture or is withdrawn;
re-enabling capture requires a reload. Already exported or persisted data cannot
be recalled, and an export already handed to its transport cannot be cancelled.
The nullable default preserves omission versus explicit managed `false` through
the editor's configuration API; it does not enable capture.

This applies to the legacy Local extension-host harness. The native Copilot runtime
owns its own managed identity enforcement. The message-content environment bridge
does not enable identity or forward account/OS/host values.
The shared policy also governs the separate
[Agent Host-owned exporter](../../../../src/vs/platform/agentHost/OTEL.md#settings--env-var-translation),
including its metadata resources and DB-mode trace fan-out. Its policy changes
take effect through host replacement, not this extension's live denial latch.

### Activation

When late enterprise OTel settings turn on external export after Copilot's telemetry service
started without it, Copilot can restart the extension hosts for that window to recover. It shows
a progress notification before requesting the restart; that notice clears automatically when
the host restarts or the attempt ends. Successful recovery is logged without another toast.
Restarting also interrupts other extensions in the window. If the restart is unavailable,
vetoed, or fails to apply the settings,
a warning offers **Reload Window** instead. User changes and policy withdrawal remain
opt-in reloads, except that identity denial is enforced before subsequent exports.
Existing exporter selection and legacy environment-variable precedence are unchanged.
It uses changes to the application-scoped, policy-backed configuration defaults as a recovery
signal. Normal personal settings changes do not change those defaults.
If a recognizable enterprise OTel block was already present at initialization, later changes
only offer a reload, including enabling a previously disabled managed configuration, except
when initialization observed the account gate's disabled, restricted configuration.
Automatic recovery additionally requires policy-enabled OTLP export targeting the collector in
those defaults. Disabled and DB-only pipelines, unrelated partial policies, and configurations
still redirected by environment variables to a different collector or file do not qualify.
Policy edits indistinguishable from schema defaults cannot be identified as new policy and retain
the opt-in reload behavior. Conflicting environment variables can still prevent recovery, and this
does not enforce precedence over environment variables or guarantee telemetry produced before
the restart. The default-value signal cannot distinguish a policy consisting entirely of
schema-default values from no policy.

There is no periodic polling or restart loop. A startup check and configuration events trigger
checks, coalesced by a 500 ms debounce. Each check waits for account-policy settlement through
the private `_workbench.whenAccountPolicySettled` command before reading the current configuration.
The command waits for the gate's own post-account initialization update, including native
managed-settings processing, even when that update does not change the gate state.
Pending refreshes defer the check; settled blocked refreshes are authoritative restrictions,
not placeholders. While the gate blocks AI, Local OTel does not offer a reload that would only
repeat the required refresh. The wait resumes on gate changes even if the effective settings are
unchanged. If policy recovery changes the running exporter, the normal reload flow then applies;
if it restores the same exporter, no stale reload action remains.
Automatic recovery is limited to one attempt per editor
session. A reload guard is saved only when the user chooses **Reload Window**, not when a prompt
is shown or dismissed. Its configuration fingerprint excludes `vscode.env.sessionId` and carries
into the immediately following editor session. If that reload still fails to apply the same
policy, a warning remains available without offering another reload. The guard is consumed by
that successor and does not prevent recovery on a later launch. Successful recovery retains
only the current session's automatic-restart budget. Later policy updates offer a reload,
deduplicated while stale.
If policy first arrives after a later sign-in, that single recovery can happen then rather than
immediately at launch. A one-off 15-second grace period allows a requested restart to finish
before a still-running host shows the reload fallback.

OTel is **off by default** with zero overhead. It activates when:

- an applied enterprise policy makes `github.copilot.chat.otel.enabled` true, or
- `COPILOT_OTEL_ENABLED=true`, or
- `OTEL_EXPORTER_OTLP_ENDPOINT` is set, or
- `github.copilot.chat.otel.enabled` is `true`, or
- `github.copilot.chat.otel.dbSpanExporter.enabled` is `true` (the SDK pipeline must be active to feed the SQLite store).

### Commands

| Command | Description |
|---|---|
| **Chat: Export Agent Traces DB** (`github.copilot.chat.otel.exportAgentTracesDB`) | Export the local SQLite span database to a `.db` file. Only available when `github.copilot.chat.otel.dbSpanExporter.enabled` is `true`. |


---

## What Gets Exported

> ### Attribute namespaces & dual-emit policy
>
> Copilot Chat emits OTel attributes under three namespaces:
>
> - **`gen_ai.*`** — [OTel GenAI Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/). Use these whenever a standard key exists.
> - **`github.copilot.*`** — Canonical Copilot-specific namespace. Prefer this for new dashboards and alerts.
> - **`copilot_chat.*`** — Original VS Code extension namespace. Several keys (notably `copilot_chat.repo.*` and `gen_ai.usage.reasoning_tokens`) are now **dual-emitted alongside the `github.copilot.*` equivalents**. Tables below mark these rows as **Legacy** with a pointer to the preferred key.
>
> Legacy keys continue to emit indefinitely so existing collectors, dashboards, and downstream consumers (Agent Debug Log, Chronicle, SQLite span store) keep working without changes. There is no sunset date.

### Traces

Copilot Chat emits a hierarchical span tree for each agent interaction:

```
invoke_agent copilot                           [~15s]
  ├── chat gpt-4o                              [~3s]  (LLM requests tool calls)
  ├── execute_tool readFile                    [~50ms]
  ├── execute_tool runCommand                  [~2s]
  ├── chat gpt-4o                              [~4s]  (LLM generates final response)
  └── (span ends)
```

Inline chat uses the same invocation shape, with `invoke_agent Inline Chat` as the root span and nested `chat` / `execute_tool` children.

**`invoke_agent`** — wraps the entire agent orchestration (all LLM calls + tool executions).

| Attribute | Requirement | Example |
|---|---|---|
| `gen_ai.operation.name` | Required | `invoke_agent` |
| `gen_ai.provider.name` | Required | `github` |
| `gen_ai.agent.name` | Required | `copilot` |
| `gen_ai.conversation.id` | Required | `a1b2c3d4-...` |
| `gen_ai.request.model` | Recommended | `gpt-4o` |
| `gen_ai.response.model` | Recommended | `gpt-4o-2024-08-06` |
| `gen_ai.usage.input_tokens` | Recommended | `12500` |
| `gen_ai.usage.output_tokens` | Recommended | `3200` |
| `gen_ai.usage.cache_read.input_tokens` | When available | `8000` |
| `gen_ai.usage.cache_creation.input_tokens` | When available | `4200` |
| `github.copilot.agent.type` | Always | `builtin` \| `custom` \| `plugin` |
| `github.copilot.git.repository` | When in a repo | `https://github.com/microsoft/vscode.git` |
| `github.copilot.git.branch` | When in a repo | `main` |
| `github.copilot.git.commit_sha` | When in a repo | `deadbeef...` |
| `github.copilot.github.org` | GitHub remotes only | `microsoft` |
| `copilot_chat.repo.remote_url` | **Legacy** — prefer `github.copilot.git.repository` | `https://github.com/...` |
| `copilot_chat.repo.head_branch_name` | **Legacy** — prefer `github.copilot.git.branch` | `main` |
| `copilot_chat.repo.head_commit_hash` | **Legacy** — prefer `github.copilot.git.commit_sha` | `deadbeef...` |
| `copilot_chat.turn_count` | Always | `4` |
| `error.type` | On error | `Error` |
| `gen_ai.input.messages` | Export opt-in (captureContent); retained locally for foreground debugging | `[{"role":"user",...}]` |
| `gen_ai.output.messages` | Export opt-in (captureContent); retained locally for foreground debugging | `[{"role":"assistant",...}]` |
| `gen_ai.tool.definitions` | Export opt-in (captureContent); retained locally for foreground debugging | `[{"type":"function",...}]` |

**`chat`** — one span per LLM API call (span kind: `CLIENT`).

| Attribute | Requirement | Example |
|---|---|---|
| `gen_ai.operation.name` | Required | `chat` |
| `gen_ai.provider.name` | Required | `github` |
| `gen_ai.request.model` | Required | `gpt-4o` |
| `gen_ai.conversation.id` | Session correlation (when a session is available) | `a1b2c3d4-...` |
| `copilot_chat.session_id` | Session correlation | `a1b2c3d4-...` |
| `copilot_chat.chat_session_id` | Session correlation | VS Code chat session ID |
| `gen_ai.request.max_tokens` | Always | `2048` |
| `gen_ai.request.temperature` | When set | `0.1` |
| `gen_ai.request.top_p` | When set | `0.95` |
| `copilot_chat.request.max_prompt_tokens` | Always | `128000` |
| `gen_ai.response.id` | On response | `chatcmpl-abc123` |
| `gen_ai.response.model` | On response | `gpt-4o-2024-08-06` |
| `gen_ai.response.finish_reasons` | On response | `["stop"]` |
| `gen_ai.usage.input_tokens` | On response | `1500` |
| `gen_ai.usage.output_tokens` | On response | `250` |
| `gen_ai.usage.cache_read.input_tokens` | When available | `1200` |
| `gen_ai.usage.cache_creation.input_tokens` | When available | `300` |
| `gen_ai.usage.reasoning.output_tokens` | When available | `512` |
| `gen_ai.usage.reasoning_tokens` | **Legacy** — prefer `gen_ai.usage.reasoning.output_tokens` | `512` |
| `copilot_chat.time_to_first_token` | On response | `450` |
| `gen_ai.request.stream` | Streaming responses | `true` |
| `gen_ai.response.time_to_first_chunk` | Streaming responses (seconds) | `0.45` |
| `server.address` | When available | `api.github.com` |
| `copilot_chat.debug_name` | When available | `agentMode` |
| `error.type` | On error | `TimeoutError` |
| `gen_ai.input.messages` | Opt-in (captureContent) | `[{"role":"system",...}]` |
| `gen_ai.system_instructions` | Opt-in (captureContent) | `[{"type":"text",...}]` |

**`execute_tool`** — one span per tool invocation (span kind: `INTERNAL`).

| Attribute | Requirement | Example |
|---|---|---|
| `gen_ai.operation.name` | Required | `execute_tool` |
| `gen_ai.tool.name` | Required | `readFile` |
| `gen_ai.conversation.id` | Session correlation | `a1b2c3d4-...` |
| `copilot_chat.session_id` | Session correlation | `a1b2c3d4-...` |
| `copilot_chat.chat_session_id` | Session correlation | VS Code chat session ID |
| `gen_ai.tool.type` | Required | `function` or `extension` (MCP tools) |
| `gen_ai.tool.call.id` | Recommended | `call_abc123` |
| `gen_ai.tool.description` | When available | `Read the contents of a file` |
| `github.copilot.tool.parameters.edit_type` | Edit tools | `create` \| `update` \| `str_replace` \| `insert` |
| `github.copilot.tool.parameters.skill_name` | When invoking a skill | `auto-perf-optimize` |
| `github.copilot.tool.parameters.mcp_server_name_hash` | MCP tools | SHA-256 hex of server name |
| `github.copilot.tool.parameters.mcp_tool_name` | MCP tools | `search_issues` |
| `github.copilot.tool.parameters.command` | Shell tools, opt-in (captureContent) | `npm test` (truncated to 256 chars) |
| `github.copilot.tool.parameters.file_path` | File tools, opt-in (captureContent) | `/src/app.ts` |
| `github.copilot.tool.parameters.mcp_server_name` | MCP tools, opt-in (captureContent) | `github` |
| `error.type` | On error | `FileNotFoundError` |
| `gen_ai.tool.call.arguments` | Export opt-in (captureContent); retained locally (bounded) | `{"filePath":"/src/index.ts"}` |
| `gen_ai.tool.call.result` | Export opt-in (captureContent); retained locally (bounded) | `(file contents or summary)` |

**`execute_hook`** — one span per local hook command (span kind: `INTERNAL`). Hook input and successful output are retained for the Agent Debug Log regardless of `captureContent`, but require `captureContent` for primary span export; `maxAttributeSizeChars` still applies.

| Attribute | Requirement | Example |
|---|---|---|
| `gen_ai.operation.name` | Required | `execute_hook` |
| `gen_ai.conversation.id` | Session correlation | `a1b2c3d4-...` |
| `copilot_chat.hook_type` | Required | `PreToolUse` |
| `copilot_chat.hook_command` | Always | `./scripts/check.sh` |
| `copilot_chat.hook_input` | Export opt-in (captureContent); retained locally (bounded) | `{"tool_name":"run_in_terminal",...}` |
| `copilot_chat.hook_output` | Export opt-in (captureContent); retained locally on success (bounded) | `ok` |
| `copilot_chat.hook_result_kind` | On completion | `success` \| `error` \| `non_blocking_error` |
| `github.copilot.hook.tool_names` | When available | `["run_in_terminal"]` |
| `github.copilot.hook.duration` | On completion (seconds) | `0.25` |
| `github.copilot.hook.decision` | On completion | `pass` \| `block` \| `non_blocking_error` |

### Metrics

#### GenAI Convention Metrics

| Metric | Type | Unit | Description |
|---|---|---|---|
| `gen_ai.client.operation.duration` | Histogram | s | LLM API call duration |
| `gen_ai.client.token.usage` | Histogram | tokens | Token counts (input/output) |
| `gen_ai.client.operation.time_to_first_chunk` | Histogram | s | Time to first streaming chunk |
| `gen_ai.client.operation.time_per_output_chunk` | Histogram | s | Inter-chunk latency after the first chunk |

**`gen_ai.client.operation.duration` attributes:**

| Attribute | Description |
|---|---|
| `gen_ai.operation.name` | Operation type (e.g., `chat`) |
| `gen_ai.provider.name` | Provider (e.g., `github`, `anthropic`) |
| `gen_ai.request.model` | Requested model |
| `gen_ai.response.model` | Resolved model (if different) |
| `server.address` | Server hostname |
| `server.port` | Server port |
| `error.type` | Error class (if failed) |

**`gen_ai.client.token.usage` attributes:**

| Attribute | Description |
|---|---|
| `gen_ai.operation.name` | Operation type |
| `gen_ai.provider.name` | Provider name |
| `gen_ai.token.type` | `input` or `output` |
| `gen_ai.request.model` | Requested model |
| `gen_ai.response.model` | Resolved model |
| `server.address` | Server hostname |

**`gen_ai.client.operation.time_to_first_chunk` / `gen_ai.client.operation.time_per_output_chunk` attributes:**

| Attribute | Description |
|---|---|
| `gen_ai.operation.name` | Operation type (e.g., `chat`) |
| `gen_ai.provider.name` | Provider (e.g., `github`, `anthropic`, `gemini`) |
| `gen_ai.request.model` | Requested model |
| `gen_ai.response.model` | Resolved model (when known) |

> Both metrics are tagged with `gen_ai.response.model` for per-model slicing. `time_per_output_chunk` is emitted only on the primary GitHub streaming path; BYOK providers (Anthropic, Gemini) emit `time_to_first_chunk` only, as they do not expose per-chunk arrival timing.

#### Extension-Specific Metrics

| Metric | Type | Unit | Description |
|---|---|---|---|
| `copilot_chat.tool.call.count` | Counter | calls | Tool invocations by name and success |
| `copilot_chat.tool.call.duration` | Histogram | ms | Tool execution latency |
| `copilot_chat.agent.invocation.duration` | Histogram | s | Agent mode end-to-end duration |
| `copilot_chat.agent.turn.count` | Histogram | turns | LLM round-trips per agent invocation |
| `copilot_chat.session.count` | Counter | sessions | Chat sessions started |
| `copilot_chat.time_to_first_token` | Histogram | s | Time to first SSE token |

**`copilot_chat.tool.call.count` attributes:** `gen_ai.tool.name`, `success` (boolean)

**`copilot_chat.tool.call.duration` attributes:** `gen_ai.tool.name`

**`copilot_chat.agent.invocation.duration` attributes:** `gen_ai.agent.name`

**`copilot_chat.agent.turn.count` attributes:** `gen_ai.agent.name`

**`copilot_chat.time_to_first_token` attributes:** `gen_ai.request.model`

#### Agent Activity & Outcome Metrics

These metrics track activity and outcomes reported through the Copilot Chat extension's OTel service.

| Metric | Type | Unit | Description |
|---|---|---|---|
| `copilot_chat.edit.acceptance.count` | Counter | edits | Edit accept/reject decisions (inline chat, chat editing, hunk-level) |
| `copilot_chat.chat_edit.outcome.count` | Counter | edits | File-level chat editing session outcomes (accepted/rejected/saved) |
| `copilot_chat.lines_of_code.count` | Counter | lines | Lines of code added/removed by accepted agent edits |
| `copilot_chat.edit.survival.four_gram` | Histogram | ratio (0-1) | 4-gram text similarity survival score |
| `copilot_chat.edit.survival.no_revert` | Histogram | ratio (0-1) | No-revert survival score |
| `copilot_chat.user.action.count` | Counter | actions | User engagement: copy, insert, apply, followup |
| `copilot_chat.user.feedback.count` | Counter | votes | Thumbs up/down on chat responses |
| `copilot_chat.agent.edit_response.count` | Counter | responses | Agent edit responses by success/error |
| `copilot_chat.agent.summarization.count` | Counter | events | Context summarization outcomes (applied/failed) |
| `copilot_chat.pull_request.count` | Counter | PRs | Pull requests created via CLI agent |
| `copilot_chat.cloud.session.count` | Counter | sessions | Cloud/remote agent sessions by partner |
| `copilot_chat.cloud.operation.count` | Counter | operations | Cloud task operation outcomes (create, fetch, follow-up, PR) |
| `copilot_chat.cloud.operation.duration` | Histogram | ms | Cloud task operation latency |
| `copilot_chat.cloud.error.count` | Counter | errors | Cloud task operation failures by operation and error type |

**`copilot_chat.edit.acceptance.count` attributes:** `copilot_chat.edit.source` (`inline_chat`/`chat_editing`/`chat_editing_hunk`/`apply_patch`/`replace_string`/`code_mapper`), `copilot_chat.edit.outcome` (`accepted`/`rejected`), `copilot_chat.language_id` (optional)

**`copilot_chat.chat_edit.outcome.count` attributes:** `copilot_chat.edit.source`, `copilot_chat.edit.outcome` (`accepted`/`rejected`/`saved`), `copilot_chat.language_id` (optional), `copilot_chat.has_remaining_edits` (optional)

**`copilot_chat.lines_of_code.count` attributes:** `type` (`added`/`removed`), `copilot_chat.language_id` (optional)

**`copilot_chat.edit.survival.four_gram` attributes:** `copilot_chat.edit.source`, `copilot_chat.time_delay_ms`

**`copilot_chat.edit.survival.no_revert` attributes:** `copilot_chat.edit.source`, `copilot_chat.time_delay_ms`

**`copilot_chat.user.action.count` attributes:** `action` (`copy`/`insert`/`apply`/`followup`)

**`copilot_chat.user.feedback.count` attributes:** `rating` (`positive`/`negative`)

**`copilot_chat.agent.edit_response.count` attributes:** `outcome` (`success`/`error`)

**`copilot_chat.agent.summarization.count` attributes:** `outcome` (`applied`/`failed`)

**`copilot_chat.cloud.session.count` attributes:** `partner_agent` (`copilot`/`claude`/`codex`)

**`copilot_chat.cloud.operation.count` attributes:** `operation` (`createSession`/`fetchSessionList`/`fetchContent`/`fetchEvents`/`pollUpdate`/`followUp`/`createPullRequest`/`sessionActivated`), `success`

**`copilot_chat.cloud.operation.duration` attributes:** `operation`

**`copilot_chat.cloud.error.count` attributes:** `operation`, `error.type` (low-cardinality classifier, e.g. `http_500`)

### Events

#### `gen_ai.client.inference.operation.details`

Emitted after each LLM API call with full inference metadata.

| Attribute | Description |
|---|---|
| `gen_ai.operation.name` | Always `chat` |
| `gen_ai.request.model` | Requested model |
| `gen_ai.response.model` | Resolved model |
| `gen_ai.response.id` | Response ID |
| `gen_ai.response.finish_reasons` | Stop reasons (e.g., `["stop"]`) |
| `gen_ai.usage.input_tokens` | Input token count |
| `gen_ai.usage.output_tokens` | Output token count |
| `gen_ai.request.temperature` | Temperature (if set) |
| `gen_ai.request.max_tokens` | Max tokens (if set) |
| `error.type` | Error class (if failed) |
| `gen_ai.input.messages` | Full prompt messages (captureContent only) |
| `gen_ai.system_instructions` | System prompt (captureContent only) |
| `gen_ai.tool.definitions` | Tool schemas (captureContent only) |

#### `copilot_chat.session.start`

Emitted when a new chat session begins (top-level agent invocations only, not subagents).

| Attribute | Description |
|---|---|
| `session.id` | Session identifier |
| `gen_ai.request.model` | Initial model |
| `gen_ai.agent.name` | Chat participant name |

#### `copilot_chat.tool.call`

Emitted when a tool invocation completes.

| Attribute | Description |
|---|---|
| `gen_ai.tool.name` | Tool name |
| `duration_ms` | Execution time in milliseconds |
| `success` | `true` or `false` |
| `error.type` | Error class (if failed) |

#### `copilot_chat.agent.turn`

Emitted for each LLM round-trip within an agent invocation.

| Attribute | Description |
|---|---|
| `turn.index` | Turn number (0-indexed) |
| `gen_ai.usage.input_tokens` | Input tokens this turn |
| `gen_ai.usage.output_tokens` | Output tokens this turn |
| `tool_call_count` | Number of tool calls this turn |

#### Agent Activity & Outcome Events

These events provide drill-down detail for the agent activity metrics above. They are emitted as OTel log records.

##### `copilot_chat.edit.feedback`

Emitted when a user accepts or rejects a file-level edit from the agent.

| Attribute | Description |
|---|---|
| `outcome` | `accepted` or `rejected` |
| `language_id` | Language of the edited file |
| `participant` | Chat participant that proposed the edit |
| `request_id` | Chat request identifier |
| `edit_surface` | `agent` or `inline_chat` |
| `has_remaining_edits` | Whether unreviewed edits remain |
| `is_notebook` | Whether the file is a notebook |

##### `copilot_chat.edit.hunk.action`

Emitted when a user accepts or rejects an individual hunk.

| Attribute | Description |
|---|---|
| `outcome` | `accepted` or `rejected` |
| `language_id` | Language of the edited file |
| `request_id` | Chat request identifier |
| `line_count` | Total lines in the hunk |
| `lines_added` | Lines added |
| `lines_removed` | Lines removed |

##### `copilot_chat.inline.done`

Emitted when an inline chat edit is accepted or rejected.

| Attribute | Description |
|---|---|
| `accepted` | `true` or `false` |
| `language_id` | Language of the edited file |
| `edit_count` | Number of edits suggested |
| `edit_line_count` | Total lines across all edits |
| `reply_type` | How the response was shown |
| `is_notebook` | Whether the document is a notebook |

##### `copilot_chat.edit.survival`

Emitted at intervals (5s, 30s, 2min, 5min, 10min, 15min) after an edit is accepted, measuring how much of the AI-generated code survives.

| Attribute | Description |
|---|---|
| `edit_source` | `apply_patch`, `replace_string`, `code_mapper`, or `inline_chat` |
| `survival_rate_four_gram` | 0-1 ratio of AI edit still present (4-gram similarity) |
| `survival_rate_no_revert` | 0-1 ratio of edit ranges not reverted |
| `time_delay_ms` | Milliseconds since edit acceptance |
| `did_branch_change` | Whether git branch changed (ignore if `true`) |
| `request_id` | Chat request identifier |

##### `copilot_chat.user.feedback`

Emitted when a user votes on a chat response (thumbs up/down).

| Attribute | Description |
|---|---|
| `rating` | `positive` or `negative` |
| `participant` | Chat participant name |
| `conversation_id` | Conversation session ID |
| `request_id` | Chat request identifier |

##### `copilot_chat.cloud.session.invoke`

Emitted when a cloud/remote agent session is started.

| Attribute | Description |
|---|---|
| `partner_agent` | `copilot`, `claude`, or `codex` |
| `model` | Model identifier |
| `request_id` | Chat request identifier |

### Resource Attributes

All signals carry:

| Attribute | Value |
|---|---|
| `service.name` | `copilot-chat` (override via the `github.copilot.chat.otel.serviceName` setting, `OTEL_SERVICE_NAME`, or enterprise policy) |
| `service.version` | Extension version |
| `session.id` | Unique per VS Code window |

Add custom resource attributes with `OTEL_RESOURCE_ATTRIBUTES`:

```bash
export OTEL_RESOURCE_ATTRIBUTES="team.id=platform,department=engineering"
```

These custom attributes are included in all traces, metrics, and events, allowing you to:

- Filter metrics by team or department
- Create team-specific dashboards and alerts
- Track usage across organizational boundaries

> **Note:** `OTEL_RESOURCE_ATTRIBUTES` uses comma-separated `key=value` pairs. The extension splits on commas, trims surrounding whitespace, and does not percent-decode values. Use the `github.copilot.chat.otel.resourceAttributes` object setting when a value needs to contain a comma.

The governed identity keys (`user.name`, `process.user.name`, `host.name`) are
excluded even when explicitly configured unless identity capture is allowed.
See [Governed identity capture](#governed-identity-capture).

---

## Content Capture

The `captureContent` setting controls content on individual LLM `chat` spans and inference events. It also gates known content attributes on spans and span events sent through the Local harness's primary exporters: OTLP, file, and console. When off, the export boundary removes input/output messages, system instructions, tool definitions, tool arguments/results, user requests, reasoning, prompt context/instructions, markdown content, hook input/output, and generic `content`/`toolDefinitions` attributes. Span events can remain without their content attributes.

Foreground `invoke_agent`, `execute_tool`, and `execute_hook` spans still retain content needed by the Agent Debug Log and the separate local SQLite exporter, regardless of this setting. Primary file export is not the SQLite debug exporter. Identity filtering applies to both paths independently.

To include these content attributes in primary exports and capture full LLM request and response content, add to your VS Code settings:

```json
{
  "github.copilot.chat.otel.captureContent": true
}
```

This additionally populates content attributes on `chat` spans and inference events, including:

| Attribute | Content |
|---|---|
| `gen_ai.input.messages` | Full LLM input messages (JSON) |
| `gen_ai.output.messages` | Full LLM response messages (JSON) |
| `gen_ai.system_instructions` | System prompt |
| `gen_ai.tool.definitions` | Tool schemas |

Content attributes are not truncated by default. Set `github.copilot.chat.otel.maxAttributeSizeChars` to a positive value when the backend imposes a per-attribute limit.

> **Warning:** This is filtering of known content attributes, not universal payload sanitization. Other metadata, span names, status messages, or unrecognized attributes may still contain sensitive information. Local debug content retention is separate. Enabling `captureContent` includes prompt, response, and tool content in primary exports. Configure export only for trusted environments.

---

## Example Configurations

**OTLP/gRPC:**

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.exporterType": "otlp-grpc",
  "github.copilot.chat.otel.otlpEndpoint": "http://localhost:4317"
}
```

**Remote collector with authentication:**

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.otlpEndpoint": "https://collector.example.com:4318"
}
```

> **Note:** Authentication headers can be set via the `github.copilot.chat.otel.headers` setting (a `{ "key": "value" }` map applied directly to the exporter) or the `OTEL_EXPORTER_OTLP_HEADERS` environment variable, and can be mandated by enterprise policy. See [Environment Variables](#environment-variables).

**File-based output (offline / CI):**

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.exporterType": "file",
  "github.copilot.chat.otel.outfile": "/tmp/copilot-otel.jsonl"
}
```

The file contains newline-delimited JSON records for spans, logs, and metrics; it is not an OTLP JSON payload. Span records contain public span data, including trace and span IDs, parent context, attributes, events, links, resource attributes, instrumentation scope, status, and dropped-data counts. Trace state is serialized as a string. Span `startTime`, `endTime`, and `duration` use `[seconds, nanoseconds]` pairs, preserving nanosecond precision.

The exporter preserves attributes already captured by instrumentation; see [Content Capture](#content-capture) for what those attributes can contain. Logs and metrics retain their existing SDK JSON representation. Serialization failures are reported as failed exports rather than written as `{}` placeholders, and a batch that cannot be serialized is not partially written.

**Console output (quick debugging):**

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.exporterType": "console"
}
```

---

## Subagent Trace Propagation

When an agent invokes a subagent (e.g., via the `runSubagent` tool), Copilot Chat automatically propagates the trace context so the subagent's `invoke_agent` span is parented to the calling agent's `execute_tool` span. This produces a connected trace tree:

```
invoke_agent copilot                           [~30s]
  ├── chat gpt-4o                              [~3s]
  ├── execute_tool runSubagent                 [~20s]
  │   └── invoke_agent Explore                 [~18s]   ← child via trace context
  │       ├── chat gpt-4o                      [~2s]
  │       ├── execute_tool searchFiles         [~200ms]
  │       ├── execute_tool readFile            [~50ms]
  │       └── chat gpt-4o                      [~3s]
  ├── chat gpt-4o                              [~4s]
  └── (span ends)
```

This propagation works across async boundaries — the parent's trace context is stored when `runSubagent` starts and retrieved when the subagent begins its `invoke_agent` span.

---

## Interpreting the Data

**Traces** — Visualize the full agent execution in Jaeger or Grafana Tempo. Each `invoke_agent` span contains child `chat` and `execute_tool` spans, making it easy to identify bottlenecks and debug failures. Foreground subagent invocations appear as nested `invoke_agent` spans under `execute_tool runSubagent`.

**Metrics** — Track token usage trends by model and provider, monitor tool success rates via `copilot_chat.tool.call.count`, and watch perceived latency with `copilot_chat.time_to_first_token`. Agent activity metrics (`copilot_chat.edit.acceptance.count`, `copilot_chat.edit.survival.four_gram`, `copilot_chat.lines_of_code.count`) power accept rate and edit survival dashboards. All metrics carry the same resource attributes (`service.name`, `service.version`, `session.id`) for consistent filtering.

**Events** — `copilot_chat.session.start` tracks session creation. `copilot_chat.tool.call` events provide per-invocation timing and error details. `copilot_chat.edit.feedback` and `copilot_chat.edit.survival` events enable drill-down into which edits were accepted/rejected and how code survival varies by edit source. `copilot_chat.user.feedback` links thumbs-up/down votes to specific conversations for quality investigation. `gen_ai.client.inference.operation.details` gives the full LLM call record including token usage and, when content capture is enabled, the complete prompt/response messages. Use `gen_ai.conversation.id` to correlate all signals belonging to the same session.

---

## Initialization & Buffering

The OTel SDK is loaded asynchronously via dynamic imports to avoid blocking extension startup. Events emitted before initialization completes are buffered (up to 1,000 items) and replayed once the SDK is ready. If initialization fails, buffered events are discarded and all subsequent calls become no-ops — the extension continues to function normally.

First successful span export is logged to the console (`[OTel] First span batch exported successfully via ...`) to confirm end-to-end connectivity.

---

## Backend Setup Guides

Copilot Chat's OTel data works with any OTLP-compatible backend. This section provides step-by-step setup guides for recommended backends.

### Aspire Dashboard

See [Quick Start](#quick-start) above for setup. The [Aspire Dashboard](https://aspire.dev/dashboard/standalone/) is the simplest option — a single Docker container with a built-in OTLP endpoint and trace viewer. No cloud account or collector needed.

### OTel Collector + Azure Application Insights

[Azure Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) ingests OTel traces, metrics, and logs through an [OTel Collector](https://opentelemetry.io/docs/collector/) with the `azuremonitor` exporter. This repo includes a ready-to-use collector setup in `docs/monitoring/`.

**1. Create an Application Insights resource:**

1. Go to the [Azure Portal](https://portal.azure.com/).
2. Click **Create a resource** → search **Application Insights** → **Create**.
3. Choose your subscription, resource group, name, and region → **Review + Create** → **Create**.
4. Once deployed, go to the resource → **Overview** → copy the **Connection String**.

**2. Start the OTel Collector:**

```bash
export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=...;IngestionEndpoint=..."

cd docs/monitoring
docker compose up -d
```

Verify the collector is healthy:

```bash
# Should return 200
curl -s -o /dev/null -w "%{http_code}" http://localhost:4328/v1/traces \
  -X POST -H "Content-Type: application/json" -d '{"resourceSpans":[]}'
```

**3. Configure VS Code:**

Open **Settings** (`Ctrl+,`) and add:

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.exporterType": "otlp-http",
  "github.copilot.chat.otel.otlpEndpoint": "http://localhost:4328"
}
```

Optionally, to capture full prompt/response content:

```json
{
  "github.copilot.chat.otel.captureContent": true
}
```

> **Warning:** Content capture includes prompts, code, and file contents. Only enable in trusted environments.

**4. Generate telemetry** — Open Copilot Chat and send any message (e.g., use Agent mode).

**5. Verify data:**

- **Jaeger (local):** Open http://localhost:16687, select service `copilot-chat`, click **Find Traces**.
- **App Insights (Azure):** Go to your Application Insights resource → **Transaction search** → filter by "Trace" or "Request".

Run this query in **Application Insights → Logs** to confirm:

```kql
traces
| where timestamp > ago(1h)
| where message contains "GenAI" or message contains "copilot_chat"
| project timestamp, message, customDimensions
| order by timestamp desc
```

For metrics (may take 5–10 minutes to appear):

```kql
customMetrics
| where timestamp > ago(1h)
| where name startswith "gen_ai" or name startswith "copilot_chat"
| summarize avg(value), count() by name
```

**Collector config** (`docs/monitoring/otel-collector-config.yaml`):

```yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

exporters:
  azuremonitor:
    connection_string: "${APPLICATIONINSIGHTS_CONNECTION_STRING}"
  debug:
    verbosity: basic

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [azuremonitor, debug]
    metrics:
      receivers: [otlp]
      exporters: [azuremonitor, debug]
```

> **Note:** The docker-compose maps ports to `4328`/`4327` on the host to avoid conflicts. Adjust in `docker-compose.yaml` if needed. Add additional exporters (e.g., `otlphttp/jaeger`) to fan out to multiple backends. See `docs/monitoring/otel-collector-config.yaml` for the full config including `batch` processor and `logs` pipeline.

### Jaeger

[Jaeger](https://www.jaegertracing.io/) is an open-source distributed tracing platform. It accepts OTLP directly — no collector needed.

**1. Start Jaeger:**

```bash
docker run -d --name jaeger -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest
```

**2. Configure VS Code:**

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.otlpEndpoint": "http://localhost:4318"
}
```

**3. Verify:** Open http://localhost:16686, select service `copilot-chat`, and click **Find Traces**.

### Langfuse

[Langfuse](https://langfuse.com/) is an open-source LLM observability platform with native OTLP ingestion and support for [OTel GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/). See the [Langfuse docs](https://langfuse.com/docs/opentelemetry/introduction) for full details on capabilities and limitations.

**Setup:**

```json
{
  "github.copilot.chat.otel.enabled": true,
  "github.copilot.chat.otel.otlpEndpoint": "http://localhost:3000/api/public/otel",
  "github.copilot.chat.otel.captureContent": true
}
```

Set the auth header with the `github.copilot.chat.otel.headers` setting or the `OTEL_EXPORTER_OTLP_HEADERS` environment variable. For example:

```json
{
  "github.copilot.chat.otel.headers": {
    "Authorization": "Basic <base64-public-key-and-secret-key>"
  }
}
```

Replace `<public-key>` and `<secret-key>` with your Langfuse API keys from **Settings → API Keys**.

**Verify:** Open Langfuse → **Traces**. You should see `invoke_agent` traces with nested `chat` and `execute_tool` spans.

### Other Backends

Any OTLP-compatible backend works with Copilot Chat's OTel output. Some options:

| Backend | Description |
|---|---|
| **[Jaeger](https://www.jaegertracing.io/)** | Open-source distributed tracing platform |
| **[Grafana Tempo](https://grafana.com/oss/tempo/) + [Prometheus](https://prometheus.io/)** | Open-source traces + metrics stack |

Refer to each backend's documentation for OTLP ingestion setup.

---

## Security & Privacy

- **Export is off by default.** No OTel data is sent to an external exporter unless explicitly enabled. When disabled, the OTel SDK is not loaded; a lightweight in-memory service still records data needed by the Agent Debug Log.
- **Content export and local debug retention are separate.** Known span and span-event content attributes require `captureContent` in primary OTLP, file, and console exports. Foreground agent, tool, and hook spans retain content for local debugging even when that setting is off. This is not universal sanitization; review [Content Capture](#content-capture) before enabling export.
- **Treat exported content as sensitive.** User messages, file contents, commands, tool payloads, and hook data can contain personal or confidential information.
- **User-configured endpoints.** Data goes only where you point it — no phone-home behavior.
- **Dynamic imports only.** OTel SDK packages are loaded on-demand, ensuring zero bundle impact when disabled.

---

--- METRICS ---
- Files Extracted: 4
- Estimated Token Budget: ~19208 tokens
- Canonical Reference: https://codewiki.google/github.com/microsoft/vscode
