Skip to content

Enhance services with metrics and structured results#7

Merged
jongalloway merged 4 commits intomainfrom
feat/copilot-sdk-dashboard-upgrades
Feb 27, 2026
Merged

Enhance services with metrics and structured results#7
jongalloway merged 4 commits intomainfrom
feat/copilot-sdk-dashboard-upgrades

Conversation

@jongalloway
Copy link
Owner

Introduce metrics tracking in AtomFeedService and CacheService, refactor feed fetching and caching logic, and improve NewsletterService with session hooks and streaming. Update VSCodeReleaseNotesService to return structured results, enhancing overall service performance and diagnostics.

- Added FetchFeedWithMetricsAsync method to AtomFeedService for improved feed fetching with metrics.
- Introduced FeedFetchResult record to encapsulate feed fetching results and metrics.
- Updated existing feed fetching logic to utilize the new method and return structured results.

feat: Implement caching metrics in CacheService

- Introduced CacheSectionMetric record to track read and save outcomes in CacheService.
- Enhanced caching logic to record metrics for cache hits, misses, skips, and errors.
- Added GetSectionMetrics method to retrieve and order cache metrics.

feat: Improve NewsletterService with session hooks and streaming

- Added session hooks to handle session events (start, end, error) in NewsletterService.
- Enabled streaming and adjusted reasoning effort for session creation.
- Enhanced logging to track streamed characters and session events.

feat: Refactor VSCodeReleaseNotesService to return structured results

- Added GetReleaseNotesFetchResultForDateRangeAsync method to return detailed fetch results.
- Introduced VSCodeReleaseNotesFetchResult record to encapsulate release notes and metrics.
- Updated release notes fetching logic to return structured results with metrics on URLs and features.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request enhances the newsletter generator application with comprehensive metrics tracking and structured result types across all service layers. The changes improve observability, enable better performance diagnostics, and refactor the dashboard to display detailed pipeline metrics.

Changes:

  • Introduced structured result types (FeedFetchResult, VSCodeReleaseNotesFetchResult, CacheSectionMetric) that bundle data with diagnostic metrics
  • Enhanced NewsletterService with session hooks for error retry and lifecycle logging, plus streaming delta event tracking
  • Added per-section cache metrics tracking in CacheService using ConcurrentDictionary
  • Refactored Program.cs to use RunTrackedTaskAsync helper for consistent timing measurements and updated the dashboard to show detailed breakdowns of source pipeline stages, cache operations by section, timing charts, and warnings

Reviewed changes

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

Show a summary per file
File Description
src/NewsletterGenerator/Services/VSCodeReleaseNotesService.cs Added VSCodeReleaseNotesFetchResult record and GetReleaseNotesFetchResultForDateRangeAsync method with metrics tracking (successful URLs, matched sections, unique features)
src/NewsletterGenerator/Services/NewsletterService.cs Added CreateSessionHooks method for SDK lifecycle events and error retry; enabled streaming with delta event logging; enhanced SendPromptAsync with streaming char tracking
src/NewsletterGenerator/Services/CacheService.cs Added CacheSectionMetric record and per-section metrics tracking using ConcurrentDictionary; added GetSectionMetrics method for retrieving ordered metrics
src/NewsletterGenerator/Services/AtomFeedService.cs Added FeedFetchResult record and FetchFeedWithMetricsAsync method that returns feed entries along with pipeline metrics (total, in-range, matched, skipped items)
src/NewsletterGenerator/Program.cs Added RunTrackedTaskAsync helper for timing; enhanced RunMetrics with warnings, cache sections, output stats, and SDK config; refactored dashboard with separate panels for summary, warnings, cache, sources, and timing; parallelized startup checks; added ping to doctor command
README.md Added comprehensive documentation of GitHub Copilot SDK features used in the project with explanations of where and why each feature is used

Comment on lines 14 to 16
public int CacheHits { get; private set; }
public int CacheMisses { get; private set; }
public int CacheSkips { get; private set; }
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The counter properties CacheHits, CacheMisses, and CacheSkips are not thread-safe, but the CacheService instance is shared across potentially concurrent operations. In GenerateCopilotNewsletterAsync (Program.cs lines 1360-1390), newsSectionTask and releaseSectionTask are started concurrently before being awaited sequentially, meaning they could both access TryGetCachedAsync or SaveCacheAsync simultaneously, causing race conditions on these increment operations. Consider using Interlocked.Increment for these counters or a lock to ensure thread-safe updates.

Copilot uses AI. Check for mistakes.
Comment on lines 12 to 31
OnErrorOccurred = async (input, invocation) =>
{
logger.LogWarning("Session error in {Context}: {Error}", input.ErrorContext, input.Error);
await Task.CompletedTask;
return new ErrorOccurredHookOutput
{
ErrorHandling = "retry"
};
},
OnSessionStart = async (input, invocation) =>
{
logger.LogDebug("Session started (source={Source})", input.Source);
await Task.CompletedTask;
return new SessionStartHookOutput();
},
OnSessionEnd = async (input, invocation) =>
{
logger.LogDebug("Session ended (reason={Reason})", input.Reason);
await Task.CompletedTask;
return null;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The hook functions use 'await Task.CompletedTask' which is redundant and serves no purpose. Task.CompletedTask is already completed, so awaiting it is unnecessary. You can remove these lines (15, 24, 30) and simply return the result directly without the await. Alternatively, if you want to keep the async signature for consistency, you can use Task.FromResult directly or omit the await.

Suggested change
OnErrorOccurred = async (input, invocation) =>
{
logger.LogWarning("Session error in {Context}: {Error}", input.ErrorContext, input.Error);
await Task.CompletedTask;
return new ErrorOccurredHookOutput
{
ErrorHandling = "retry"
};
},
OnSessionStart = async (input, invocation) =>
{
logger.LogDebug("Session started (source={Source})", input.Source);
await Task.CompletedTask;
return new SessionStartHookOutput();
},
OnSessionEnd = async (input, invocation) =>
{
logger.LogDebug("Session ended (reason={Reason})", input.Reason);
await Task.CompletedTask;
return null;
OnErrorOccurred = (input, invocation) =>
{
logger.LogWarning("Session error in {Context}: {Error}", input.ErrorContext, input.Error);
return Task.FromResult(new ErrorOccurredHookOutput
{
ErrorHandling = "retry"
});
},
OnSessionStart = (input, invocation) =>
{
logger.LogDebug("Session started (source={Source})", input.Source);
return Task.FromResult(new SessionStartHookOutput());
},
OnSessionEnd = (input, invocation) =>
{
logger.LogDebug("Session ended (reason={Reason})", input.Reason);
return Task.FromResult<SessionEndHookOutput?>(null);

Copilot uses AI. Check for mistakes.
Comment on lines +1388 to +1390
newsSection = await newsSectionTask;

releaseSection = await releaseSectionTask;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The newsSectionTask and releaseSectionTask are created (started) at lines 1360-1386 but awaited sequentially at lines 1388 and 1390. Since async methods start executing immediately when called, these two tasks will run concurrently until the first await, but then you wait for them sequentially. This works correctly but is not the most efficient pattern. Consider using Task.WhenAll to await both in parallel explicitly: 'await Task.WhenAll(newsSectionTask, releaseSectionTask);' before extracting results. This makes the concurrent intent clearer and could improve timing accuracy in metrics.

Suggested change
newsSection = await newsSectionTask;
releaseSection = await releaseSectionTask;
await Task.WhenAll(newsSectionTask, releaseSectionTask);
newsSection = newsSectionTask.Result;
releaseSection = releaseSectionTask.Result;

Copilot uses AI. Check for mistakes.
Comment on lines 154 to 167
// Lightweight connectivity ping
try
{
await using var client = new CopilotClient();
await client.StartAsync();
var ping = await client.PingAsync();
AnsiConsole.MarkupLine($"[green]✓[/] Ping: OK");
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[yellow]⚠[/] Ping failed: {Markup.Escape(ex.Message)}");
healthy = false;
}

Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The DoctorCommand creates a second CopilotClient and performs a ping check, but PrintCopilotStartupStatusAsync (called at line 151) already creates a CopilotClient, starts it, and performs a ping (see line 756 in PrintCopilotStartupStatusAsync). This creates two client instances and pings twice, which is redundant. Consider removing lines 154-166 since the ping status is already checked and added to the metrics/warnings in PrintCopilotStartupStatusAsync. If you want to display the ping result separately, you could return it from PrintCopilotStartupStatusAsync or check the warnings in metrics instead.

Suggested change
// Lightweight connectivity ping
try
{
await using var client = new CopilotClient();
await client.StartAsync();
var ping = await client.PingAsync();
AnsiConsole.MarkupLine($"[green]✓[/] Ping: OK");
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[yellow]⚠[/] Ping failed: {Markup.Escape(ex.Message)}");
healthy = false;
}

Copilot uses AI. Check for mistakes.
Comment on lines 1469 to 1472
var headingCount = content.Split('\n')
.Count(line => line.StartsWith("## ", StringComparison.Ordinal) || line.StartsWith("### ", StringComparison.Ordinal));

return headingCount + 1; // Include the welcome section.
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The section counting logic assumes there's always a "welcome section" by adding 1 to the heading count. However, if the content is empty or contains only headings without a welcome paragraph, this could be inaccurate. Consider verifying that the content actually has a welcome/intro section before adding 1, or document this assumption clearly in a comment.

Suggested change
var headingCount = content.Split('\n')
.Count(line => line.StartsWith("## ", StringComparison.Ordinal) || line.StartsWith("### ", StringComparison.Ordinal));
return headingCount + 1; // Include the welcome section.
var lines = content.Split('\n');
var headingCount = lines
.Count(line => line.StartsWith("## ", StringComparison.Ordinal) || line.StartsWith("### ", StringComparison.Ordinal));
// Treat any non-empty content before the first heading as a "welcome" section.
var hasWelcomeSection = false;
foreach (var line in lines)
{
if (line.StartsWith("## ", StringComparison.Ordinal) || line.StartsWith("### ", StringComparison.Ordinal))
break;
if (!string.IsNullOrWhiteSpace(line))
{
hasWelcomeSection = true;
break;
}
}
return headingCount + (hasWelcomeSection ? 1 : 0);

Copilot uses AI. Check for mistakes.
@jongalloway
Copy link
Owner Author

@copilot look at the review comment. if valid, fix.

Copy link
Contributor

Copilot AI commented Feb 26, 2026

@jongalloway I've opened a new pull request, #8, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits February 26, 2026 23:06
…s, Task.WhenAll, remove duplicate ping, fix CountSections

Co-authored-by: jongalloway <68539+jongalloway@users.noreply.github.com>
Fix thread safety, redundant async/await, and section-count logic from review feedback
@jongalloway jongalloway merged commit 8f4990b into main Feb 27, 2026
2 checks passed
@jongalloway jongalloway deleted the feat/copilot-sdk-dashboard-upgrades branch February 27, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants