Skip to content

Admin analytics dashboard#454

Merged
JoeProgrammer88 merged 15 commits intomainfrom
AdminAnalyticsDashboard
Jan 12, 2026
Merged

Admin analytics dashboard#454
JoeProgrammer88 merged 15 commits intomainfrom
AdminAnalyticsDashboard

Conversation

@JoeProgrammer88
Copy link
Copy Markdown
Member

Closes #438

Summary

An admin dashboard has been created for the monthly stats they ask for via Email each month. A Copy stats button is added to the top to copy the data as plain text to make it easy to paste into an email. Analytic data is hardcoded for the development environment. KQL queries for Application Insights data were verified in Azure use the Log analytics in Application Insights. Page Views were being tracked twice, once on the server and once on the client, the server side page tracking has been removed in favor of the JS tracking. We are not tracking bot traffic at the moment so this was a simple solution to implement

Copilot Summary

This pull request introduces a new Application Insights-powered analytics dashboard for the admin area, enabling both local development with sample data and production use with real Azure telemetry. It adds new models, controller logic, configuration documentation, and service registration, while removing the old page view tracking filter. The changes provide a robust, configurable analytics solution that supports filtering by date, month, and year, and gracefully handles both local and Azure environments.

Analytics Dashboard Implementation

  • Added AdminController with an Analytics action to display analytics data (page views, PDF downloads, search terms) for a selected date range or month/year, including error handling and configuration guidance.
  • Created AnalyticsViewModel and supporting data models (PageViewData, DownloadData, SearchTermData) to structure analytics data for the dashboard view.

Application Insights Configuration

  • Added ApplicationInsights-Configuration.md documentation, detailing local development setup, production configuration, and troubleshooting for Application Insights and the analytics dashboard.
  • Registered AnalyticsService for dependency injection in Program.cs and updated Application Insights telemetry configuration to support developer mode and adaptive sampling for local debugging.
  • Added Azure SDK packages (Azure.Identity, Azure.Monitor.Query) to support querying telemetry data from Azure Application Insights and Log Analytics.

Codebase Cleanup

  • Removed the obsolete ApplicationInsightsPageViewTracker filter, as analytics tracking is now handled by the new dashboard and service.

Project Structure

  • Added a Filters folder to the project structure in anticipation of future filter implementations.

JoeProgrammer88 and others added 11 commits January 9, 2026 08:36
Introduces an admin-only analytics dashboard displaying page views, PDF downloads, and search terms from the past 30 days, powered by Azure Application Insights. Adds AnalyticsService for querying telemetry, new view models, and an AdminController. Updates navigation, DI setup, and configuration to support the new feature.
The analytics dashboard now supports custom date range and month/year filters, replacing the fixed "past 30 days" view. Controller, service, and view logic were updated to accept and display the selected date range. The UI includes new filter controls and quick-select buttons, and all queries now use the specified date range. Documentation and messages were updated for clarity.
- Always register Application Insights; use real connection string if provided
- Enable developer mode and debug logging in development
- AnalyticsService now auto-detects Azure config and falls back to sample data locally
- _Layout.cshtml conditionally loads real or mock AI JS for client-side telemetry
- Update appsettings.Development.json for local dev and blob storage emulation
- Add ApplicationInsights-Configuration.md with setup and troubleshooting instructions
- Ensures analytics dashboard and telemetry work seamlessly in both Azure and local development
Added a button to copy a formatted analytics report to the clipboard from the Analytics page. Implemented a JavaScript function to generate the report using server-side data and provide user feedback on success. Introduced a structured analyticsData object for easier data handling and refactored related logic. Also added utility functions for date/month filter management. No existing features were removed.
Updated analytics queries to exclude bot and synthetic traffic by filtering on `operation_SyntheticSource` and `client_Type`. Added a note to the analytics UI to clarify that metrics reflect only real human users.
Enhanced the page view metrics query in AnalyticsService to filter out dynamic routes such as '/Details', '/Edit', and '/Delete'. This change ensures analytics reports focus on static, meaningful pages and omit less relevant, detail-oriented routes.
Eliminated the ApplicationInsightsPageViewTracker from MVC controller registration and removed the unused PC2.Filters using directive. Controllers are now registered without custom filters.
Reduced the "Year" dropdown options in Analytics.cshtml from six years (current and previous five) to just the current and previous year. This streamlines the selection and focuses analytics on more recent data.
Deleted ApplicationInsights-Configuration.md, which contained setup instructions, environment details, and troubleshooting guidance for Application Insights in both local development and production. All related documentation has been removed.
Copy link
Copy Markdown
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 introduces a comprehensive admin analytics dashboard powered by Azure Application Insights, enabling administrators to view page views, PDF downloads, and search terms through a web interface. The implementation supports both local development with mock data and production environments with real Azure telemetry data.

Key changes:

  • New analytics dashboard with date/month filtering and clipboard export functionality
  • Azure Application Insights integration using KQL queries for production telemetry
  • Removal of duplicate server-side page tracking in favor of client-side JavaScript tracking
  • Configuration for both local development and Azure production environments

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
PC2/Controllers/AdminController.cs New controller with Analytics action to handle date filtering and display analytics data
PC2/Services/AnalyticsService.cs Core service implementing Azure Application Insights queries and local development mock data
PC2/Models/AnalyticsViewModel.cs View models for analytics data (page views, downloads, search terms)
PC2/Views/Admin/Analytics.cshtml Dashboard UI with filtering, statistics cards, and data tables
PC2/wwwroot/js/analytics.js JavaScript for clipboard export and date filtering (duplicated code)
PC2/Views/Shared/_Layout.cshtml Application Insights SDK integration with conditional loading and mock implementation for local dev
PC2/Program.cs Service registration and Application Insights configuration with developer mode support
PC2/appsettings.json Added WorkspaceId configuration for Application Insights
PC2/appsettings.Development.json Local development configuration with mock data enabled
PC2/PC2.csproj Added Azure.Identity and Azure.Monitor.Query packages
PC2/Filters/ApplicationInsightsPageViewTracker.cs Removed obsolete server-side page tracking filter

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread PC2/wwwroot/js/analytics.js Outdated
Comment on lines +1 to +45
// Hidden data for clipboard export
var analyticsData = {
startDate: '@Model.StartDate?.ToString("MMM dd, yyyy")',
endDate: '@Model.EndDate?.ToString("MMM dd, yyyy")',
days: @((Model.EndDate.HasValue && Model.StartDate.HasValue) ? (Model.EndDate.Value - Model.StartDate.Value).Days + 1 : 0),
totalPageViews: @Model.TotalPageViews,
totalPdfDownloads: @Model.TotalPdfDownloads,
totalSearches: @Model.TotalSearches,
pageViews: [
@if (Model.PageViews.Any())
{
for (int i = 0; i < Model.PageViews.Take(20).Count(); i++)
{
var pv = Model.PageViews.ElementAt(i);
var comma = i < Model.PageViews.Take(20).Count() - 1 ? "," : "";
<text> { url: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(pv.PageUrl))', count: @pv.ViewCount }@comma
</text>
}
}
],
pdfDownloads: [
@if (Model.PdfDownloads.Any())
{
for (int i = 0; i < Model.PdfDownloads.Count(); i++)
{
var dl = Model.PdfDownloads.ElementAt(i);
var comma = i < Model.PdfDownloads.Count() - 1 ? "," : "";
<text> { name: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(dl.FileName))', count: @dl.DownloadCount }@comma
</text>
}
}
],
searchTerms: [
@if (Model.SearchTerms.Any())
{
for (int i = 0; i < Model.SearchTerms.Count(); i++)
{
var st = Model.SearchTerms.ElementAt(i);
var comma = i < Model.SearchTerms.Count() - 1 ? "," : "";
<text> { type: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(st.SearchType))', term: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(st.SearchTerm))', count: @st.SearchCount }@comma
</text>
}
}
]
};
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The data in the analyticsData JavaScript object is duplicated in two places - once in analytics.js using Razor syntax to generate JavaScript, and again in the inline script block in Analytics.cshtml. This creates unnecessary code duplication and maintenance overhead. The inline script in Analytics.cshtml should reference the external analytics.js file instead of redefining the same data and functions.

Copilot uses AI. Check for mistakes.
}
catch (Exception ex)
{
ViewBag.ErrorMessage = $"Error retrieving analytics data: {ex.Message}";
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The error message displayed to users includes the raw exception message which may expose sensitive technical details or stack traces. For user-facing error messages, consider showing a generic message and logging the detailed exception information separately using proper logging infrastructure.

Copilot uses AI. Check for mistakes.
Comment thread PC2/Views/Shared/_Layout.cshtml Outdated
@@ -18,6 +27,25 @@ cfg: { // Application Insights Configuration
instrumentationKey: "c4b69618-5135-49c7-a75d-215953ddde9b"
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The hardcoded instrumentation key 'c4b69618-5135-49c7-a75d-215953ddde9b' in the Application Insights JavaScript snippet should not be committed to source control. This key could be a production key that grants access to your telemetry data. Consider reading this from configuration and injecting it via Razor, or ensure this is a development-only key.

Suggested change
instrumentationKey: "c4b69618-5135-49c7-a75d-215953ddde9b"
instrumentationKey: "@appInsightsKey"

Copilot uses AI. Check for mistakes.
Comment thread PC2/wwwroot/js/analytics.js Outdated
Comment on lines +16 to +42
<text> { url: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(pv.PageUrl))', count: @pv.ViewCount }@comma
</text>
}
}
],
pdfDownloads: [
@if (Model.PdfDownloads.Any())
{
for (int i = 0; i < Model.PdfDownloads.Count(); i++)
{
var dl = Model.PdfDownloads.ElementAt(i);
var comma = i < Model.PdfDownloads.Count() - 1 ? "," : "";
<text> { name: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(dl.FileName))', count: @dl.DownloadCount }@comma
</text>
}
}
],
searchTerms: [
@if (Model.SearchTerms.Any())
{
for (int i = 0; i < Model.SearchTerms.Count(); i++)
{
var st = Model.SearchTerms.ElementAt(i);
var comma = i < Model.SearchTerms.Count() - 1 ? "," : "";
<text> { type: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(st.SearchType))', term: '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(st.SearchTerm))', count: @st.SearchCount }@comma
</text>
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The data serialization using System.Text.Json.JsonSerializer.Serialize mixed with Html.Raw() could be simplified. While this approach works to safely escape special characters in strings for JavaScript, it's creating nested encoding. Consider using a more straightforward approach like @Json.Serialize() or creating a properly typed JavaScript object from server-side data.

Copilot uses AI. Check for mistakes.
Comment thread PC2/Services/AnalyticsService.cs Outdated
Comment on lines +19 to +24
private readonly TelemetryClient? _telemetryClient;
private readonly string? _workspaceId;
private readonly bool _isAzureConfigured;
private readonly IConfiguration _configuration;

public AnalyticsService(IConfiguration configuration, TelemetryClient? telemetryClient = null)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The AnalyticsService is registered with AddScoped lifetime, but in the constructor it only accepts IConfiguration and an optional TelemetryClient. If TelemetryClient needs to be injected, it should be a required parameter rather than optional with a default value of null. The optional parameter pattern can lead to confusion about whether the dependency is truly optional or if it's just a workaround for testing scenarios.

Suggested change
private readonly TelemetryClient? _telemetryClient;
private readonly string? _workspaceId;
private readonly bool _isAzureConfigured;
private readonly IConfiguration _configuration;
public AnalyticsService(IConfiguration configuration, TelemetryClient? telemetryClient = null)
private readonly TelemetryClient _telemetryClient;
private readonly string? _workspaceId;
private readonly bool _isAzureConfigured;
private readonly IConfiguration _configuration;
public AnalyticsService(IConfiguration configuration, TelemetryClient telemetryClient)

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +141
@for (int y = DateTime.Now.Year; y >= DateTime.Now.Year - 1; y--)
{
<option value="@y" selected="@(Model.SelectedYear == y)">@y</option>
}
</select>
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The year dropdown only shows the current year and one year back (line 137: DateTime.Now.Year - 1). This is very limiting for analytics purposes where you may want to view historical data from previous years. Consider expanding this range to show at least 5-10 years of history, or all years since the application was deployed.

Copilot uses AI. Check for mistakes.
Comment thread PC2/Services/AnalyticsService.cs Outdated
Comment on lines +358 to +359
catch
{
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Empty catch blocks are a poor practice as they silently swallow exceptions and hide potential issues. If URL parsing fails in ExtractFileNameFromUrl, the method returns the original URL, which may not be the desired behavior. Consider at minimum logging the exception, or returning a more descriptive fallback value like "Invalid URL" or "Parse Error".

Suggested change
catch
{
catch (Exception ex)
{
Console.WriteLine($"Error extracting file name from URL '{url}': {ex.Message}");

Copilot uses AI. Check for mistakes.
Comment thread PC2/Services/AnalyticsService.cs Outdated
Comment on lines +200 to +211
var query = $@"
pageViews
| where timestamp >= datetime({startDate:yyyy-MM-ddTHH:mm:ssZ})
| where timestamp <= datetime({endDate:yyyy-MM-ddTHH:mm:ssZ})
| where isempty(operation_SyntheticSource)
| where client_Type == 'Browser' or client_Type == 'PC'
| where name !has '/Details/' and name !endswith '/Details'
| where name !has '/Edit/' and name !endswith '/Edit'
| where name !has '/Delete/' and name !endswith '/Delete'
| summarize ViewCount = count() by name
| order by ViewCount desc
| limit 50";
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The dates are formatted directly in KQL queries using string interpolation (lines 202-203, 253-254, 306-307). This approach is vulnerable to issues if the DateTime format changes or if timezones are not properly handled. While this isn't a SQL injection risk (KQL doesn't have that vulnerability in this context), using parameterized queries or the KQL datetime() function more safely would be more robust. The current format assumes the DateTime is formatted consistently, which could fail with different cultures or timezone settings.

Copilot uses AI. Check for mistakes.
Comment thread PC2/Views/Admin/Analytics.cshtml Outdated
startDate = endDate.Value.AddDays(-30);
}
// Ensure end date includes the full day
else if (endDate.HasValue)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Condition is always true because of access to property HasValue.

Suggested change
else if (endDate.HasValue)
else

Copilot uses AI. Check for mistakes.
JoeProgrammer88 and others added 4 commits January 12, 2026 08:14
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Refactored AnalyticsService for clarity, modularity, and better error handling; separated Azure and local data logic.
- Moved analytics data preparation from Razor view to a JS object (`analyticsData`) and delegated clipboard export logic to a new external JS file (`admin-analytics.js`).
- Cleaned up analytics.js to only handle report formatting and export, expecting data from the global object.
- Improves separation of concerns, maintainability, and testability.
Always include and properly format the Application Insights script in _Layout.cshtml. Remove the local development mock; production instrumentation key is now always used. Improves maintainability and clarity of configuration.
Replaced all Console.WriteLine statements with structured logging using ILogger<AnalyticsService> via dependency injection. Improved logging for initialization, error handling, and local development scenarios to enhance observability and align with .NET best practices.
@JoeProgrammer88 JoeProgrammer88 merged commit a30c4ec into main Jan 12, 2026
1 check passed
@JoeProgrammer88 JoeProgrammer88 deleted the AdminAnalyticsDashboard branch January 12, 2026 16:53
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.

Create dashboard for admin

2 participants