Skip to content

Commit 6c862cc

Browse files
refactor(nodejs): remove @opentelemetry/api dependency
Replace the optional @opentelemetry/api peer dependency with a user-provided callback approach: - Add TraceContext interface and TraceContextProvider type - Add onGetTraceContext callback to CopilotClientOptions - Pass traceparent/tracestate directly on ToolInvocation for inbound context - Remove @opentelemetry/api from peerDependencies and devDependencies - Rewrite telemetry.ts to a simple callback-based helper (~27 lines) - Update tests, README, and OpenTelemetry docs with wire-up examples Users who want distributed trace propagation provide a callback: const client = new CopilotClient({ onGetTraceContext: () => { const carrier = {}; propagation.inject(context.active(), carrier); return carrier; }, }); TelemetryConfig (CLI env vars) is unchanged and requires no dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 73baf16 commit 6c862cc

File tree

11 files changed

+327
-137
lines changed

11 files changed

+327
-137
lines changed

docs/observability/opentelemetry.md

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,72 @@ var client = new CopilotClient(new CopilotClientOptions
8080

8181
### Trace Context Propagation
8282

83-
Trace context is propagated automatically — no manual instrumentation is needed:
83+
> **Most users don't need this.** The `TelemetryConfig` above is all you need to collect traces from the CLI. The trace context propagation described in this section is an **advanced feature** for applications that create their own OpenTelemetry spans and want them to appear in the **same distributed trace** as the CLI's spans.
8484
85-
- **SDK → CLI**: `traceparent` and `tracestate` headers from the current span/activity are included in `session.create`, `session.resume`, and `session.send` RPC calls.
86-
- **CLI → SDK**: When the CLI invokes tool handlers, the trace context from the CLI's span is propagated so your tool code runs under the correct parent span.
85+
The SDK can propagate W3C Trace Context (`traceparent`/`tracestate`) on JSON-RPC payloads so that your application's spans and the CLI's spans are linked in one distributed trace. This is useful when, for example, you want to see a "handle tool call" span in your app nested inside the CLI's "execute tool" span, or show the SDK call as a child of your request-handling span.
86+
87+
#### SDK → CLI (outbound)
88+
89+
For **Node.js**, provide an `onGetTraceContext` callback on the client options. This is only needed if your application already uses `@opentelemetry/api` and you want to link your spans with the CLI's spans. The SDK calls this callback before `session.create`, `session.resume`, and `session.send` RPCs:
90+
91+
<!-- docs-validate: skip -->
92+
```typescript
93+
import { CopilotClient } from "@github/copilot-sdk";
94+
import { propagation, context } from "@opentelemetry/api";
95+
96+
const client = new CopilotClient({
97+
telemetry: { otlpEndpoint: "http://localhost:4318" },
98+
onGetTraceContext: () => {
99+
const carrier: Record<string, string> = {};
100+
propagation.inject(context.active(), carrier);
101+
return carrier; // { traceparent: "00-...", tracestate: "..." }
102+
},
103+
});
104+
```
105+
106+
For **Python**, **Go**, and **.NET**, trace context injection is automatic when the respective OpenTelemetry/Activity API is configured — no callback is needed.
107+
108+
#### CLI → SDK (inbound)
109+
110+
When the CLI invokes a tool handler, the `traceparent` and `tracestate` from the CLI's span are available in all languages:
111+
112+
- **Go**: The `ToolInvocation.TraceContext` field is a `context.Context` with the trace already restored — use it directly as the parent for your spans.
113+
- **Python**: Trace context is automatically restored around the handler via `trace_context()` — child spans are parented to the CLI's span automatically.
114+
- **.NET**: Trace context is automatically restored via `RestoreTraceContext()` — child `Activity` instances are parented to the CLI's span automatically.
115+
- **Node.js**: Since the SDK has no OpenTelemetry dependency, `traceparent` and `tracestate` are passed as raw strings on the `ToolInvocation` object. Restore the context manually if needed:
116+
117+
<!-- docs-validate: skip -->
118+
```typescript
119+
import { propagation, context, trace } from "@opentelemetry/api";
120+
121+
session.registerTool(myTool, async (args, invocation) => {
122+
// Restore the CLI's trace context as the active context
123+
const carrier = {
124+
traceparent: invocation.traceparent,
125+
tracestate: invocation.tracestate,
126+
};
127+
const parentCtx = propagation.extract(context.active(), carrier);
128+
129+
// Create a child span under the CLI's span
130+
const tracer = trace.getTracer("my-app");
131+
return context.with(parentCtx, () =>
132+
tracer.startActiveSpan("my-tool", async (span) => {
133+
try {
134+
const result = await doWork(args);
135+
return result;
136+
} finally {
137+
span.end();
138+
}
139+
})
140+
);
141+
});
142+
```
87143

88144
### Per-Language Dependencies
89145

90146
| Language | Dependency | Notes |
91147
|---|---|---|
92-
| Node.js | `@opentelemetry/api` | Optional peer dependency |
148+
| Node.js | | No dependency; provide `onGetTraceContext` callback for outbound propagation |
93149
| Python | `opentelemetry-api` | Install with `pip install copilot-sdk[telemetry]` |
94150
| Go | `go.opentelemetry.io/otel` | Required dependency |
95151
| .NET || Uses built-in `System.Diagnostics.Activity` |

nodejs/README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ new CopilotClient(options?: CopilotClientOptions)
8585
- `githubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods.
8686
- `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `githubToken` is provided). Cannot be used with `cliUrl`.
8787
- `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the CLI process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below.
88+
- `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the CLI's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below.
8889

8990
#### Methods
9091

@@ -604,7 +605,7 @@ const session = await client.createSession({
604605
605606
## Telemetry
606607

607-
The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export and automatic W3C Trace Context propagation.
608+
The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export from the CLI process — this is all most users need:
608609

609610
```typescript
610611
const client = new CopilotClient({
@@ -614,6 +615,8 @@ const client = new CopilotClient({
614615
});
615616
```
616617

618+
With just this configuration, the CLI emits spans for every session, message, and tool call to your collector. No additional dependencies or setup required.
619+
617620
**TelemetryConfig options:**
618621

619622
- `otlpEndpoint?: string` - OTLP HTTP endpoint URL
@@ -622,9 +625,28 @@ const client = new CopilotClient({
622625
- `sourceName?: string` - Instrumentation scope name
623626
- `captureContent?: boolean` - Whether to capture message content
624627

625-
Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `session.create`, `session.resume`, and `session.send` calls, and inbound when the CLI invokes tool handlers.
628+
### Advanced: Trace Context Propagation
629+
630+
> **You don't need this for normal telemetry collection.** The `telemetry` config above is sufficient to get full traces from the CLI.
631+
632+
`onGetTraceContext` is only needed if your application creates its own OpenTelemetry spans and you want them to appear in the **same distributed trace** as the CLI's spans — for example, to nest a "handle tool call" span inside the CLI's "execute tool" span, or to show the SDK call as a child of your application's request-handling span.
633+
634+
If you're already using `@opentelemetry/api` in your app and want this linkage, provide a callback:
635+
636+
```typescript
637+
import { propagation, context } from "@opentelemetry/api";
638+
639+
const client = new CopilotClient({
640+
telemetry: { otlpEndpoint: "http://localhost:4318" },
641+
onGetTraceContext: () => {
642+
const carrier: Record<string, string> = {};
643+
propagation.inject(context.active(), carrier);
644+
return carrier;
645+
},
646+
});
647+
```
626648

627-
Optional peer dependency: `@opentelemetry/api`
649+
Inbound trace context from the CLI is available on the `ToolInvocation` object passed to tool handlers as `traceparent` and `tracestate` fields. See the [OpenTelemetry guide](../docs/observability/opentelemetry.md) for a full wire-up example.
628650

629651
## User Input Requests
630652

nodejs/package-lock.json

Lines changed: 0 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodejs/package.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"zod": "^4.3.6"
5050
},
5151
"devDependencies": {
52-
"@opentelemetry/api": "^1.9.0",
5352
"@types/node": "^25.2.0",
5453
"@typescript-eslint/eslint-plugin": "^8.54.0",
5554
"@typescript-eslint/parser": "^8.54.0",
@@ -69,14 +68,6 @@
6968
"engines": {
7069
"node": ">=20.0.0"
7170
},
72-
"peerDependencies": {
73-
"@opentelemetry/api": "^1.0.0"
74-
},
75-
"peerDependenciesMeta": {
76-
"@opentelemetry/api": {
77-
"optional": true
78-
}
79-
},
8071
"files": [
8172
"dist/**/*",
8273
"docs/**/*",

nodejs/src/client.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
import { createServerRpc } from "./generated/rpc.js";
2727
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
2828
import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
29-
import { getTraceContext, withTraceContext } from "./telemetry.js";
29+
import { getTraceContext } from "./telemetry.js";
3030
import type {
3131
ConnectionState,
3232
CopilotClientOptions,
@@ -48,6 +48,7 @@ import type {
4848
ToolCallRequestPayload,
4949
ToolCallResponsePayload,
5050
ToolResultObject,
51+
TraceContextProvider,
5152
TypedSessionLifecycleHandler,
5253
} from "./types.js";
5354

@@ -145,7 +146,13 @@ export class CopilotClient {
145146
private options: Required<
146147
Omit<
147148
CopilotClientOptions,
148-
"cliPath" | "cliUrl" | "githubToken" | "useLoggedInUser" | "onListModels" | "telemetry"
149+
| "cliPath"
150+
| "cliUrl"
151+
| "githubToken"
152+
| "useLoggedInUser"
153+
| "onListModels"
154+
| "telemetry"
155+
| "onGetTraceContext"
149156
>
150157
> & {
151158
cliPath?: string;
@@ -157,6 +164,7 @@ export class CopilotClient {
157164
private isExternalServer: boolean = false;
158165
private forceStopping: boolean = false;
159166
private onListModels?: () => Promise<ModelInfo[]> | ModelInfo[];
167+
private onGetTraceContext?: TraceContextProvider;
160168
private modelsCache: ModelInfo[] | null = null;
161169
private modelsCacheLock: Promise<void> = Promise.resolve();
162170
private sessionLifecycleHandlers: Set<SessionLifecycleHandler> = new Set();
@@ -235,6 +243,7 @@ export class CopilotClient {
235243
}
236244

237245
this.onListModels = options.onListModels;
246+
this.onGetTraceContext = options.onGetTraceContext;
238247

239248
this.options = {
240249
cliPath: options.cliUrl ? undefined : options.cliPath || getBundledCliPath(),
@@ -560,7 +569,12 @@ export class CopilotClient {
560569

561570
// Create and register the session before issuing the RPC so that
562571
// events emitted by the CLI (e.g. session.start) are not dropped.
563-
const session = new CopilotSession(sessionId, this.connection!);
572+
const session = new CopilotSession(
573+
sessionId,
574+
this.connection!,
575+
undefined,
576+
this.onGetTraceContext
577+
);
564578
session.registerTools(config.tools);
565579
session.registerPermissionHandler(config.onPermissionRequest);
566580
if (config.onUserInputRequest) {
@@ -576,7 +590,7 @@ export class CopilotClient {
576590

577591
try {
578592
const response = await this.connection!.sendRequest("session.create", {
579-
...(await getTraceContext()),
593+
...(await getTraceContext(this.onGetTraceContext)),
580594
model: config.model,
581595
sessionId,
582596
clientName: config.clientName,
@@ -661,7 +675,12 @@ export class CopilotClient {
661675

662676
// Create and register the session before issuing the RPC so that
663677
// events emitted by the CLI (e.g. session.start) are not dropped.
664-
const session = new CopilotSession(sessionId, this.connection!);
678+
const session = new CopilotSession(
679+
sessionId,
680+
this.connection!,
681+
undefined,
682+
this.onGetTraceContext
683+
);
665684
session.registerTools(config.tools);
666685
session.registerPermissionHandler(config.onPermissionRequest);
667686
if (config.onUserInputRequest) {
@@ -677,7 +696,7 @@ export class CopilotClient {
677696

678697
try {
679698
const response = await this.connection!.sendRequest("session.resume", {
680-
...(await getTraceContext()),
699+
...(await getTraceContext(this.onGetTraceContext)),
681700
sessionId,
682701
clientName: config.clientName,
683702
model: config.model,
@@ -1586,17 +1605,17 @@ export class CopilotClient {
15861605
}
15871606

15881607
try {
1608+
const traceparent = (params as { traceparent?: string }).traceparent;
1609+
const tracestate = (params as { tracestate?: string }).tracestate;
15891610
const invocation = {
15901611
sessionId: params.sessionId,
15911612
toolCallId: params.toolCallId,
15921613
toolName: params.toolName,
15931614
arguments: params.arguments,
1615+
traceparent,
1616+
tracestate,
15941617
};
1595-
const traceparent = (params as { traceparent?: string }).traceparent;
1596-
const tracestate = (params as { tracestate?: string }).tracestate;
1597-
const result = await withTraceContext(traceparent, tracestate, () =>
1598-
handler(params.arguments, invocation)
1599-
);
1618+
const result = await handler(params.arguments, invocation);
16001619
return { result: this.normalizeToolResultV2(result) };
16011620
} catch (error) {
16021621
const message = error instanceof Error ? error.message : String(error);

nodejs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export type {
4646
SystemMessageConfig,
4747
SystemMessageReplaceConfig,
4848
TelemetryConfig,
49+
TraceContext,
50+
TraceContextProvider,
4951
Tool,
5052
ToolHandler,
5153
ToolInvocation,

nodejs/src/session.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import type { MessageConnection } from "vscode-jsonrpc/node.js";
1111
import { ConnectionError, ResponseError } from "vscode-jsonrpc/node.js";
1212
import { createSessionRpc } from "./generated/rpc.js";
13-
import { getTraceContext, withTraceContext } from "./telemetry.js";
13+
import { getTraceContext } from "./telemetry.js";
1414
import type {
1515
MessageOptions,
1616
PermissionHandler,
@@ -23,6 +23,7 @@ import type {
2323
SessionHooks,
2424
Tool,
2525
ToolHandler,
26+
TraceContextProvider,
2627
TypedSessionEventHandler,
2728
UserInputHandler,
2829
UserInputRequest,
@@ -69,20 +70,25 @@ export class CopilotSession {
6970
private userInputHandler?: UserInputHandler;
7071
private hooks?: SessionHooks;
7172
private _rpc: ReturnType<typeof createSessionRpc> | null = null;
73+
private traceContextProvider?: TraceContextProvider;
7274

7375
/**
7476
* Creates a new CopilotSession instance.
7577
*
7678
* @param sessionId - The unique identifier for this session
7779
* @param connection - The JSON-RPC message connection to the Copilot CLI
7880
* @param workspacePath - Path to the session workspace directory (when infinite sessions enabled)
81+
* @param traceContextProvider - Optional callback to get W3C Trace Context for outbound RPCs
7982
* @internal This constructor is internal. Use {@link CopilotClient.createSession} to create sessions.
8083
*/
8184
constructor(
8285
public readonly sessionId: string,
8386
private connection: MessageConnection,
84-
private _workspacePath?: string
85-
) {}
87+
private _workspacePath?: string,
88+
traceContextProvider?: TraceContextProvider
89+
) {
90+
this.traceContextProvider = traceContextProvider;
91+
}
8692

8793
/**
8894
* Typed session-scoped RPC methods.
@@ -123,7 +129,7 @@ export class CopilotSession {
123129
*/
124130
async send(options: MessageOptions): Promise<string> {
125131
const response = await this.connection.sendRequest("session.send", {
126-
...(await getTraceContext()),
132+
...(await getTraceContext(this.traceContextProvider)),
127133
sessionId: this.sessionId,
128134
prompt: options.prompt,
129135
attachments: options.attachments,
@@ -377,14 +383,14 @@ export class CopilotSession {
377383
tracestate?: string
378384
): Promise<void> {
379385
try {
380-
const rawResult = await withTraceContext(traceparent, tracestate, () =>
381-
handler(args, {
382-
sessionId: this.sessionId,
383-
toolCallId,
384-
toolName,
385-
arguments: args,
386-
})
387-
);
386+
const rawResult = await handler(args, {
387+
sessionId: this.sessionId,
388+
toolCallId,
389+
toolName,
390+
arguments: args,
391+
traceparent,
392+
tracestate,
393+
});
388394
let result: string;
389395
if (rawResult == null) {
390396
result = "";

0 commit comments

Comments
 (0)