Skip to content

Commit 02f2604

Browse files
MackinnonBuckCopilotCopilotstephentoub
authored
Add experimental GitHub telemetry redirection across all SDKs (#1835)
* codegen: add notification flag to RpcMethod metadata Adds an optional otification marker to the shared RpcMethod interface so the per-language generators can emit notification-style dispatch (no JSON-RPC reply) for void clientGlobal methods such as gitHubTelemetry.event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * nodejs: add GitHub telemetry redirection support Regenerates the TypeScript RPC types for the experimental gitHubTelemetry.event clientGlobal notification and wires an onGitHubTelemetry callback on the client. Registering a handler opts created/resumed sessions into telemetry redirection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * dotnet: add GitHub telemetry redirection support Regenerates the C# RPC types for the experimental gitHubTelemetry.event clientGlobal notification and adds an OnGitHubTelemetry callback. Registering a handler opts created/resumed sessions into telemetry redirection. The option is marked [Experimental] and [EditorBrowsable(Never)] to keep it unadvertised. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * python: add GitHub telemetry redirection support Regenerates the Python RPC types for the experimental gitHubTelemetry.event clientGlobal notification and adds an on_github_telemetry callback. Registering a handler opts created/resumed sessions into telemetry redirection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * go: add GitHub telemetry redirection support Regenerates the Go RPC types for the experimental gitHubTelemetry.event clientGlobal notification and adds an OnGitHubTelemetry callback. Registering a handler opts created/resumed sessions into telemetry redirection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * rust: add GitHub telemetry redirection support Regenerates the Rust RPC types for the experimental gitHubTelemetry.event clientGlobal notification and adds an on_github_telemetry callback. Registering a handler opts created/resumed sessions into telemetry redirection. The telemetry module is #[doc(hidden)] to keep it unadvertised. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * java: add GitHub telemetry redirection support Adds the experimental gitHubTelemetry.event notification adapter and an onGitHubTelemetry client option. Registering a handler opts created/resumed sessions into telemetry redirection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix telemetry codegen after schema bump Keep experimental GitHub telemetry schema additions available to codegen until they ship in the packaged Copilot schema, and apply formatter output required by CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename telemetry redirection to forwarding across SDKs The runtime renamed the per-session opt-in capability from "redirection" to "forwarding" (wire field enableGitHubTelemetryForwarding) and updated the GitHubTelemetry schema description strings. This mirrors that rename across every SDK plus the codegen schema overlay so the hand-written opt-in flag matches the runtime contract. Also folds in two PR review fixes: - dotnet: document the expected-teardown catch in GitHubTelemetryTests DisposeAsync (was an empty catch). - python: re-export GitHubTelemetryNotification/Event/ClientInfo from copilot/__init__.py for parity with the nodejs public surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * nodejs: align GitHub telemetry forwarding with cross-SDK contract Address PR review feedback on the Node telemetry surface: - Omit enableGitHubTelemetryForwarding from session.create/resume unless a handler is registered, matching Go/Python/Rust/dotnet/Java (which all omit the field when off) instead of always sending false. - Isolate consumer callback failures: await the handler inside try/catch so async callbacks or thrown errors cannot leak into the JSON-RPC dispatch path (mirrors the panic isolation in the other SDKs). - Widen onGitHubTelemetry to return void | Promise<void> so consumers can supply async callbacks. - Update the no-handler test to assert the field is absent rather than false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * python: address telemetry review nits - Use asyncio.create_task instead of the legacy asyncio.ensure_future when scheduling awaitable notification handlers on the event loop thread. - Drop the redundant local `import asyncio` in the telemetry transport test (asyncio is already imported at module scope), clearing CodeQL py/repeated-import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * dotnet: observe expected exception in telemetry test teardown The DisposeAsync teardown catch swallows expected listener/socket shutdown exceptions. Observe the caught exception so the block is no longer empty, clearing CodeQL cs/empty-catch-block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * go: format telemetry forwarding fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * codegen: remove temporary GitHub telemetry schema overlay The telemetry types ship natively in @github/copilot 1.0.67, so the hand-maintained schema overlay that injected GitHubTelemetryNotification, GitHubTelemetryEvent, GitHubTelemetryClientInfo, and the gitHubTelemetry.event clientGlobal method is no longer needed. Remove the api-additions overlay and its application plumbing in utils.ts, and revert rust.ts to read the schema directly. The generic notification-flag codegen handling is retained, since the published schema marks gitHubTelemetry.event as a notification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reconcile Node/Python/Go telemetry glue with main's generated dispatch Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Repoint Java telemetry glue at generated types; finish Rust/.NET reconcile Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * codegen: restore notification dispatch for gitHubTelemetry.event The clientGlobal method gitHubTelemetry.event is a JSON-RPC notification (schema notification: true, void result). Restore the codegen notification-flag machinery so TypeScript/Python/Go emit notification registration (onNotification / notification-handler table / nil-result SetRequestHandler) instead of request-style onRequest dispatch, and regenerate the affected bindings. Also restore the Python _jsonrpc notification registry and fix the Go adapter to return only an error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: guard gitHubTelemetry.event notification-style dispatch Send an id-less JSON-RPC notification (not a request) through the real wire in the Node and Python unit tests so a regression back to onRequest dispatch is caught. Add a Node E2E that creates a forwarding-enabled session against a live CLI and asserts a gitHubTelemetry.event notification is delivered to the onGitHubTelemetry handler. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: clarify telemetry forwarding comment in e2e Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: use shared waitForCondition helper in telemetry e2e Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: add live gitHubTelemetry forwarding E2E for all languages Adds a live-CLI E2E in Go, Python, .NET, Rust, and Java that registers an onGitHubTelemetry callback, creates a session, and asserts at least one gitHubTelemetry.event notification is forwarded with a well-typed payload. This brings the other SDKs to parity with the existing nodejs E2E. Session creation emits an early session.start hydro event, so no model round-trip (and no recorded CAPI exchange) is needed; the tests are snapshot-free. The Rust change also refactors the E2E harness to run a native COPILOT_CLI_PATH binary directly (non-.js), leaving the node_modules index.js path unchanged for CI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: guard gitHubTelemetry callbacks in Python and .NET adapters Wrap the user-provided on_github_telemetry / OnGitHubTelemetry callback invocation in exception handling so a throwing handler cannot propagate into the JSON-RPC dispatch machinery. Mirrors the existing guards in the Node, Go, Java, and Rust adapters. Errors are logged (Python module logger; .NET ILogger, falling back to NullLogger.Instance) rather than silently swallowed, matching the Java (SEVERE) and Rust (warn) behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: normalize telemetry E2E line endings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(java): log gitHubTelemetry callback errors at WARNING not SEVERE A user-supplied onGitHubTelemetry callback throwing is a routine, recoverable condition, not a serious system failure. Level.WARNING matches the Java SDK''s own convention (CopilotSession "Error in event handler") and the .NET (LogWarning) and Rust (warn) adapters. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(python): log gitHubTelemetry callback errors at WARNING not ERROR A user-supplied on_github_telemetry callback throwing is a routine, recoverable condition, not an error. Use logger.warning(..., exc_info=True) instead of logger.exception (which logs at ERROR), matching the .NET, Java, and Rust adapters and client.py''s own convention (exc_info=True elsewhere, never logger.exception). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Make GitHub telemetry callback async in nodejs, .NET, Java, and Python The runtime-forwarded gitHubTelemetry.event callback now returns an awaitable so consumers can perform async I/O in their telemetry sink, addressing PR feedback. Signatures: - nodejs: (n) => void | Promise<void> - .NET: Func<GitHubTelemetryNotification, Task> - Java: Function<GitHubTelemetryNotification, CompletableFuture<Void>> - Python: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] Each adapter awaits the result and logs handler failures at WARNING. Go and Rust are intentionally left synchronous; neither SDK has an async-callback idiom. Unit and E2E call sites updated accordingly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 32fb841 commit 02f2604

42 files changed

Lines changed: 2581 additions & 69 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dotnet/src/Client.cs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Extensions.Logging.Abstractions;
99
using System.Collections.Concurrent;
1010
using System.Diagnostics;
11+
using System.Diagnostics.CodeAnalysis;
1112
using System.Globalization;
1213
using System.Net.Sockets;
1314
using System.Runtime.ExceptionServices;
@@ -1037,7 +1038,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
10371038
Providers: config.Providers,
10381039
Models: config.Models,
10391040
ToolFilterPrecedence: toolFilter.ToolFilterPrecedence,
1040-
ExpAssignments: config.ExpAssignments);
1041+
ExpAssignments: config.ExpAssignments,
1042+
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null);
10411043

10421044
var rpcTimestamp = Stopwatch.GetTimestamp();
10431045

@@ -1246,7 +1248,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
12461248
Providers: config.Providers,
12471249
Models: config.Models,
12481250
ToolFilterPrecedence: toolFilter.ToolFilterPrecedence,
1249-
ExpAssignments: config.ExpAssignments);
1251+
ExpAssignments: config.ExpAssignments,
1252+
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null);
12501253

12511254
var rpcTimestamp = Stopwatch.GetTimestamp();
12521255
var response = await InvokeRpcAsync<ResumeSessionResponse>(
@@ -1724,21 +1727,24 @@ await Rpc.SessionFs.SetProviderAsync(
17241727
}
17251728

17261729
/// <summary>
1727-
/// Builds the client-global RPC handler bag at construction time. Currently
1728-
/// only the LLM inference provider adapter is registered; returns null when no
1730+
/// Builds the client-global RPC handler bag at construction time. Registers
1731+
/// the LLM inference provider adapter and/or the GitHub telemetry adapter
1732+
/// depending on which options are configured; returns null when no
17291733
/// client-global API is configured so the registration is skipped entirely.
17301734
/// </summary>
17311735
private ClientGlobalApiHandlers? BuildClientGlobalApis()
17321736
{
17331737
var handler = _options.RequestHandler;
1734-
if (handler is null)
1738+
var onGitHubTelemetry = _options.OnGitHubTelemetry;
1739+
if (handler is null && onGitHubTelemetry is null)
17351740
{
17361741
return null;
17371742
}
17381743

17391744
return new ClientGlobalApiHandlers
17401745
{
1741-
LlmInference = new LlmInferenceAdapter(handler, () => _serverRpc),
1746+
LlmInference = handler is null ? null : new LlmInferenceAdapter(handler, () => _serverRpc),
1747+
GitHubTelemetry = onGitHubTelemetry is null ? null : new GitHubTelemetryAdapter(onGitHubTelemetry, _logger),
17421748
};
17431749
}
17441750

@@ -2495,7 +2501,8 @@ internal record CreateSessionRequest(
24952501
IList<NamedProviderConfig>? Providers = null,
24962502
IList<ProviderModelConfig>? Models = null,
24972503
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
2498-
[property: JsonPropertyName("expAssignments")] JsonElement? ExpAssignments = null);
2504+
[property: JsonPropertyName("expAssignments")] JsonElement? ExpAssignments = null,
2505+
bool? EnableGitHubTelemetryForwarding = null);
24992506
#pragma warning restore GHCP001
25002507

25012508
internal record ToolDefinition(
@@ -2594,7 +2601,8 @@ internal record ResumeSessionRequest(
25942601
IList<NamedProviderConfig>? Providers = null,
25952602
IList<ProviderModelConfig>? Models = null,
25962603
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
2597-
[property: JsonPropertyName("expAssignments")] JsonElement? ExpAssignments = null);
2604+
[property: JsonPropertyName("expAssignments")] JsonElement? ExpAssignments = null,
2605+
bool? EnableGitHubTelemetryForwarding = null);
25982606
#pragma warning restore GHCP001
25992607

26002608
internal record ResumeSessionResponse(
@@ -2713,3 +2721,28 @@ public sealed class ToolResultAIContent(ToolResultObject toolResult) : AIContent
27132721
/// </summary>
27142722
public ToolResultObject Result => toolResult;
27152723
}
2724+
2725+
/// <summary>
2726+
/// Bridges the generated <see cref="Rpc.IGitHubTelemetryHandler"/> client-global handler to
2727+
/// the public <c>OnGitHubTelemetry</c> callback, forwarding the generated
2728+
/// <see cref="Rpc.GitHubTelemetryNotification"/> payload unchanged.
2729+
/// </summary>
2730+
[Experimental(Diagnostics.Experimental)]
2731+
internal sealed class GitHubTelemetryAdapter(Func<Rpc.GitHubTelemetryNotification, Task> callback, ILogger logger) : Rpc.IGitHubTelemetryHandler
2732+
{
2733+
private readonly Func<Rpc.GitHubTelemetryNotification, Task> _callback = callback ?? throw new ArgumentNullException(nameof(callback));
2734+
private readonly ILogger _logger = logger ?? NullLogger.Instance;
2735+
2736+
public async Task EventAsync(Rpc.GitHubTelemetryNotification request, CancellationToken cancellationToken = default)
2737+
{
2738+
ArgumentNullException.ThrowIfNull(request);
2739+
try
2740+
{
2741+
await _callback(request).ConfigureAwait(false);
2742+
}
2743+
catch (Exception ex)
2744+
{
2745+
_logger.LogWarning(ex, "Error handling gitHubTelemetry.event notification");
2746+
}
2747+
}
2748+
}

dotnet/src/Types.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ private CopilotClientOptions(CopilotClientOptions? other)
281281
OnListModels = other.OnListModels;
282282
SessionFs = other.SessionFs;
283283
RequestHandler = other.RequestHandler;
284+
OnGitHubTelemetry = other.OnGitHubTelemetry;
284285
SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds;
285286
EnableRemoteSessions = other.EnableRemoteSessions;
286287
Mode = other.Mode;
@@ -378,6 +379,15 @@ private CopilotClientOptions(CopilotClientOptions? other)
378379
[Experimental(Diagnostics.Experimental)]
379380
public CopilotRequestHandler? RequestHandler { get; set; }
380381

382+
/// <summary>
383+
/// Experimental. Receives GitHub telemetry events the runtime forwards to this
384+
/// connection; setting a handler opts created/resumed sessions into forwarding.
385+
/// The SDK awaits the handler task so it may perform asynchronous work.
386+
/// </summary>
387+
[Experimental(Diagnostics.Experimental)]
388+
[EditorBrowsable(EditorBrowsableState.Never)]
389+
public Func<Rpc.GitHubTelemetryNotification, Task>? OnGitHubTelemetry { get; set; }
390+
381391
/// <summary>
382392
/// OpenTelemetry configuration for the runtime.
383393
/// When set to a non-<see langword="null"/> instance, the runtime is started with OpenTelemetry instrumentation enabled.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.Collections.Concurrent;
6+
using GitHub.Copilot.Rpc;
7+
using GitHub.Copilot.Test.Harness;
8+
using Xunit;
9+
using Xunit.Abstractions;
10+
11+
namespace GitHub.Copilot.Test.E2E;
12+
13+
#pragma warning disable GHCP001 // GitHub telemetry forwarding is experimental.
14+
15+
public class GitHubTelemetryForwardingE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
16+
: E2ETestBase(fixture, "github_telemetry", output)
17+
{
18+
[Fact]
19+
public async Task Should_Forward_GitHub_Telemetry_For_A_Live_Session()
20+
{
21+
var notifications = new ConcurrentQueue<GitHubTelemetryNotification>();
22+
23+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
24+
{
25+
OnGitHubTelemetry = notification =>
26+
{
27+
notifications.Enqueue(notification);
28+
return Task.CompletedTask;
29+
},
30+
});
31+
32+
CopilotSession? session = null;
33+
try
34+
{
35+
session = await client.CreateSessionAsync(new SessionConfig
36+
{
37+
OnPermissionRequest = PermissionHandler.ApproveAll,
38+
});
39+
40+
await TestHelper.WaitForConditionAsync(
41+
() => !notifications.IsEmpty,
42+
timeout: TimeSpan.FromSeconds(30),
43+
timeoutMessage: "Timed out waiting for GitHub telemetry notification.");
44+
45+
Assert.True(notifications.TryPeek(out var notification));
46+
Assert.NotEmpty(notification.SessionId);
47+
Assert.NotNull(notification.Event);
48+
Assert.NotEmpty(notification.Event.Kind);
49+
Assert.IsType<bool>(notification.Restricted);
50+
}
51+
finally
52+
{
53+
if (session is not null)
54+
{
55+
await session.DisposeAsync();
56+
}
57+
58+
await client.StopAsync();
59+
}
60+
}
61+
}
62+
63+
#pragma warning restore GHCP001

0 commit comments

Comments
 (0)