Skip to content

Commit 2f9499a

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 2f9499a

File tree

10 files changed

+220
-137
lines changed

10 files changed

+220
-137
lines changed

docs/observability/opentelemetry.md

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

8181
### Trace Context Propagation
8282

83-
Trace context is propagated automatically — no manual instrumentation is needed:
83+
The SDK propagates W3C Trace Context (`traceparent`/`tracestate`) on JSON-RPC payloads so that your application's spans and the CLI's spans appear in the same distributed trace.
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+
#### SDK → CLI (outbound)
86+
87+
For **Node.js**, provide an `onGetTraceContext` callback on the client options. The SDK calls this before `session.create`, `session.resume`, and `session.send` RPCs:
88+
89+
<!-- docs-validate: skip -->
90+
```typescript
91+
import { CopilotClient } from "@github/copilot-sdk";
92+
import { propagation, context } from "@opentelemetry/api";
93+
94+
const client = new CopilotClient({
95+
telemetry: { otlpEndpoint: "http://localhost:4318" },
96+
onGetTraceContext: () => {
97+
const carrier: Record<string, string> = {};
98+
propagation.inject(context.active(), carrier);
99+
return carrier; // { traceparent: "00-...", tracestate: "..." }
100+
},
101+
});
102+
```
103+
104+
For **Python**, **Go**, and **.NET**, trace context injection is automatic when the respective OpenTelemetry/Activity API is configured — no callback is needed.
105+
106+
#### CLI → SDK (inbound)
107+
108+
When the CLI invokes a tool handler, the `traceparent` and `tracestate` from the CLI's span are included on the `ToolInvocation` object in all languages. For **Node.js**, you can restore the OTel context in your handler:
109+
110+
<!-- docs-validate: skip -->
111+
```typescript
112+
import { propagation, context, trace } from "@opentelemetry/api";
113+
114+
session.registerTool(myTool, async (args, invocation) => {
115+
// Restore the CLI's trace context as the active context
116+
const carrier = {
117+
traceparent: invocation.traceparent,
118+
tracestate: invocation.tracestate,
119+
};
120+
const parentCtx = propagation.extract(context.active(), carrier);
121+
122+
// Create a child span under the CLI's span
123+
const tracer = trace.getTracer("my-app");
124+
return context.with(parentCtx, () =>
125+
tracer.startActiveSpan("my-tool", async (span) => {
126+
try {
127+
const result = await doWork(args);
128+
return result;
129+
} finally {
130+
span.end();
131+
}
132+
})
133+
);
134+
});
135+
```
136+
137+
For **Go**, the `ToolInvocation.TraceContext` field is a `context.Context` with the trace already restored — use it directly as the parent for your spans. For **Python** and **.NET**, extract from the raw `traceparent`/`tracestate` strings using the respective APIs.
87138

88139
### Per-Language Dependencies
89140

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

nodejs/README.md

Lines changed: 19 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` - Callback returning W3C Trace Context for distributed trace propagation. 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:
608609

609610
```typescript
610611
const client = new CopilotClient({
@@ -622,9 +623,24 @@ const client = new CopilotClient({
622623
- `sourceName?: string` - Instrumentation scope name
623624
- `captureContent?: boolean` - Whether to capture message content
624625

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.
626+
### Trace Context Propagation
626627

627-
Optional peer dependency: `@opentelemetry/api`
628+
To link your app's spans with the CLI's spans in a single distributed trace, provide an `onGetTraceContext` callback:
629+
630+
```typescript
631+
import { propagation, context } from "@opentelemetry/api";
632+
633+
const client = new CopilotClient({
634+
telemetry: { otlpEndpoint: "http://localhost:4318" },
635+
onGetTraceContext: () => {
636+
const carrier: Record<string, string> = {};
637+
propagation.inject(context.active(), carrier);
638+
return carrier;
639+
},
640+
});
641+
```
642+
643+
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.
628644

629645
## User Input Requests
630646

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)