From 8538f59c49b6df81115be0fb5e4aa4579121dd9e Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 1 Jun 2026 16:45:54 +0100 Subject: [PATCH] capturing facet filtering --- .../Controllers/Api/SearchController.cs | 18 ++ .../Controllers/SearchController.cs | 199 +++++++++++++++++- .../Interfaces/ISearchTelemetryService.cs | 7 + .../SearchFacetAppliedTelemetryModel.cs | 43 ++++ .../Services/SearchTelemetryService.cs | 36 ++++ 5 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Models/Search/SearchFacetAppliedTelemetryModel.cs diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/SearchController.cs index c15273af..c4d3ea95 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/SearchController.cs @@ -185,5 +185,23 @@ public async Task RecordSearchExecutedTelemetry(SearchExecutedTel return this.Ok(); } + + /// + /// Records search facet applied telemetry for facet usage analysis. + /// + /// The facet applied telemetry payload. + /// An . + [HttpPost("RecordFacetAppliedTelemetry")] + public async Task RecordFacetAppliedTelemetry(SearchFacetAppliedTelemetryModel model) + { + if (model == null || string.IsNullOrWhiteSpace(model.FacetField)) + { + return this.BadRequest(); + } + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + + return this.Ok(); + } } } diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index fc6f10fd..ade31f25 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -107,10 +107,10 @@ public async Task Index(SearchRequestViewModel search, bool noSor if (searchResult.CatalogueSearchResult != null) { searchResult.CatalogueSearchResult.SearchId = searchId; - } - // Record SearchExecutedTelemetry for zero-result rate analysis - await this.searchTelemetryService.RecordSearchExecutedAsync(search, searchResult, stopwatch.ElapsedMilliseconds); + // Record SearchExecutedTelemetry for zero-result rate analysis + await this.searchTelemetryService.RecordSearchExecutedAsync(search, searchResult, stopwatch.ElapsedMilliseconds); + } } if (filterApplied) @@ -189,6 +189,9 @@ public async Task IndexPost([FromQuery] SearchRequestViewModel se return await this.Index(search, noSortFilterError: true); } + // Record facet telemetry when filters are changed + await this.RecordFacetChangesAsync(search, filterUpdated, newFilters, existingFilters, resourceAccessLevelFilterUpdated, resourceAccessLevelId, search.ResourceAccessLevelId, filterProviderUpdated, newProviderFilters, existingProviderFilters, filterResourceCollectionUpdated, newResourceCollectionFilter, existingResourceCollectionFilter); + if (search.ResourcePageIndex > 0 && (filterUpdated || resourceAccessLevelFilterUpdated || filterProviderUpdated || filterResourceCollectionUpdated)) { search.ResourcePageIndex = null; @@ -447,5 +450,195 @@ public IActionResult RecordAutoSuggestionClick(string term, string url, string c this.searchService.SendAutoSuggestionClickActionAsync(clickPayloadModel); return this.Redirect(url); } + + /// + /// Records facet changes when filters are applied via the Apply button. + /// + /// The current search request. + /// Whether resource type filters were updated. + /// The new resource type filters. + /// The existing resource type filters. + /// Whether resource access level filter was updated. + /// The new resource access level filter id. + /// The existing resource access level filter id. + /// Whether provider filters were updated. + /// The new provider filters. + /// The existing provider filters. + /// Whether resource collection filters were updated. + /// The new resource collection filters. + /// The existing resource collection filters. + /// A task that represents the asynchronous operation. + private async Task RecordFacetChangesAsync( + SearchRequestViewModel search, + bool filterUpdated, + IOrderedEnumerable newFilters, + IOrderedEnumerable existingFilters, + bool resourceAccessLevelFilterUpdated, + int? newAccessLevelId, + int? existingAccessLevelId, + bool filterProviderUpdated, + IOrderedEnumerable newProviderFilters, + IOrderedEnumerable existingProviderFilters, + bool filterResourceCollectionUpdated, + IOrderedEnumerable newResourceCollectionFilter, + IOrderedEnumerable existingResourceCollectionFilter) + { + var correlationId = search.SearchId.ToString(); + var sessionId = search.GroupId ?? string.Empty; + var queryText = search.Term ?? string.Empty; + + // Record resource type filter changes + if (filterUpdated) + { + var addedFilters = newFilters.Except(existingFilters); + var removedFilters = existingFilters.Except(newFilters); + + foreach (var filter in addedFilters) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "ResourceType", + FacetValue = filter, + FacetAction = "applied", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + + foreach (var filter in removedFilters) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "ResourceType", + FacetValue = filter, + FacetAction = "removed", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + } + + // Record resource access level filter changes + if (resourceAccessLevelFilterUpdated) + { + if (existingAccessLevelId.HasValue && existingAccessLevelId > 0) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "AudienceAccessLevel", + FacetValue = existingAccessLevelId.ToString(), + FacetAction = "removed", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + + if (newAccessLevelId.HasValue && newAccessLevelId > 0) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "AudienceAccessLevel", + FacetValue = newAccessLevelId.ToString(), + FacetAction = "applied", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + } + + // Record provider filter changes + if (filterProviderUpdated) + { + var addedProviders = newProviderFilters.Except(existingProviderFilters); + var removedProviders = existingProviderFilters.Except(newProviderFilters); + + foreach (var provider in addedProviders) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "Provider", + FacetValue = provider, + FacetAction = "applied", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + + foreach (var provider in removedProviders) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "Provider", + FacetValue = provider, + FacetAction = "removed", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + } + + // Record resource collection filter changes + if (filterResourceCollectionUpdated) + { + var addedCollections = newResourceCollectionFilter.Except(existingResourceCollectionFilter); + var removedCollections = existingResourceCollectionFilter.Except(newResourceCollectionFilter); + + foreach (var collection in addedCollections) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "ResourceCollection", + FacetValue = collection, + FacetAction = "applied", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + + foreach (var collection in removedCollections) + { + var model = new SearchFacetAppliedTelemetryModel + { + CorrelationId = correlationId, + SessionId = sessionId, + QueryText = queryText, + QueryMode = "standard", + FacetField = "ResourceCollection", + FacetValue = collection, + FacetAction = "removed", + }; + + await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model); + } + } + } } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Interfaces/ISearchTelemetryService.cs b/LearningHub.Nhs.WebUI/Interfaces/ISearchTelemetryService.cs index 1d6a2165..9e34c7b4 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/ISearchTelemetryService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/ISearchTelemetryService.cs @@ -30,5 +30,12 @@ public interface ISearchTelemetryService /// The search executed telemetry model. /// A representing the asynchronous operation. Task RecordSearchExecutedFromApiAsync(SearchExecutedTelemetryModel model); + + /// + /// Records search facet applied telemetry for facet usage analysis. + /// + /// The search facet applied telemetry model. + /// A representing the asynchronous operation. + Task RecordFacetAppliedTelemetryAsync(SearchFacetAppliedTelemetryModel model); } } diff --git a/LearningHub.Nhs.WebUI/Models/Search/SearchFacetAppliedTelemetryModel.cs b/LearningHub.Nhs.WebUI/Models/Search/SearchFacetAppliedTelemetryModel.cs new file mode 100644 index 00000000..a35e4d46 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Search/SearchFacetAppliedTelemetryModel.cs @@ -0,0 +1,43 @@ +namespace LearningHub.Nhs.WebUI.Models.Search +{ + /// + /// Defines telemetry data for a search facet applied event. + /// + public class SearchFacetAppliedTelemetryModel + { + /// + /// Gets or sets the correlation id for the originating search request. + /// + public string CorrelationId { get; set; } + + /// + /// Gets or sets the session id for the search session. + /// + public string SessionId { get; set; } + + /// + /// Gets or sets the query text. + /// + public string QueryText { get; set; } + + /// + /// Gets or sets the query mode. + /// + public string QueryMode { get; set; } + + /// + /// Gets or sets the facet field name (e.g. "ResultType", "Category"). + /// + public string FacetField { get; set; } + + /// + /// Gets or sets the facet value (e.g. "Guidance", "Policy"). + /// + public string FacetValue { get; set; } + + /// + /// Gets or sets the facet action ("applied", "removed", or "cleared"). + /// + public string FacetAction { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Services/SearchTelemetryService.cs b/LearningHub.Nhs.WebUI/Services/SearchTelemetryService.cs index 69407b8a..4de35041 100644 --- a/LearningHub.Nhs.WebUI/Services/SearchTelemetryService.cs +++ b/LearningHub.Nhs.WebUI/Services/SearchTelemetryService.cs @@ -166,5 +166,41 @@ public Task RecordSearchExecutedFromApiAsync(SearchExecutedTelemetryModel model) return Task.CompletedTask; } + + /// + /// Records search facet applied telemetry for facet usage analysis. + /// + /// The search facet applied telemetry model. + /// A representing the asynchronous operation. + public Task RecordFacetAppliedTelemetryAsync(SearchFacetAppliedTelemetryModel model) + { + if (model == null || string.IsNullOrWhiteSpace(model.FacetField)) + { + return Task.CompletedTask; + } + + try + { + var properties = new Dictionary + { + { "CorrelationId", model.CorrelationId ?? string.Empty }, + { "SessionId", model.SessionId ?? string.Empty }, + { "QueryText", model.QueryText ?? string.Empty }, + { "QueryMode", model.QueryMode ?? string.Empty }, + { "FacetField", model.FacetField ?? string.Empty }, + { "FacetValue", model.FacetValue ?? string.Empty }, + { "FacetAction", model.FacetAction ?? string.Empty }, + }; + + this.telemetryClient.TrackEvent("SearchFacetAppliedTelemetry", properties); + } + catch (Exception ex) + { + // Log the exception but don't let telemetry errors impact search functionality + this.logger.LogError(ex, "Failed to record SearchFacetAppliedTelemetry for facet: {FacetField}", model?.FacetField); + } + + return Task.CompletedTask; + } } }