Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions LearningHub.Nhs.WebUI/Controllers/Api/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,23 @@ public async Task<IActionResult> RecordSearchExecutedTelemetry(SearchExecutedTel

return this.Ok();
}

/// <summary>
/// Records search facet applied telemetry for facet usage analysis.
/// </summary>
/// <param name="model">The facet applied telemetry payload.</param>
/// <returns>An <see cref="IActionResult"/>.</returns>
[HttpPost("RecordFacetAppliedTelemetry")]
public async Task<IActionResult> RecordFacetAppliedTelemetry(SearchFacetAppliedTelemetryModel model)
{
if (model == null || string.IsNullOrWhiteSpace(model.FacetField))
{
return this.BadRequest();
}

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);

return this.Ok();
}
}
}
199 changes: 196 additions & 3 deletions LearningHub.Nhs.WebUI/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ public async Task<IActionResult> 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)
Expand Down Expand Up @@ -189,6 +189,9 @@ public async Task<IActionResult> 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;
Expand Down Expand Up @@ -447,5 +450,195 @@ public IActionResult RecordAutoSuggestionClick(string term, string url, string c
this.searchService.SendAutoSuggestionClickActionAsync(clickPayloadModel);
return this.Redirect(url);
}

/// <summary>
/// Records facet changes when filters are applied via the Apply button.
/// </summary>
/// <param name="search">The current search request.</param>
/// <param name="filterUpdated">Whether resource type filters were updated.</param>
/// <param name="newFilters">The new resource type filters.</param>
/// <param name="existingFilters">The existing resource type filters.</param>
/// <param name="resourceAccessLevelFilterUpdated">Whether resource access level filter was updated.</param>
/// <param name="newAccessLevelId">The new resource access level filter id.</param>
/// <param name="existingAccessLevelId">The existing resource access level filter id.</param>
/// <param name="filterProviderUpdated">Whether provider filters were updated.</param>
/// <param name="newProviderFilters">The new provider filters.</param>
/// <param name="existingProviderFilters">The existing provider filters.</param>
/// <param name="filterResourceCollectionUpdated">Whether resource collection filters were updated.</param>
/// <param name="newResourceCollectionFilter">The new resource collection filters.</param>
/// <param name="existingResourceCollectionFilter">The existing resource collection filters.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
private async Task RecordFacetChangesAsync(
SearchRequestViewModel search,
bool filterUpdated,
IOrderedEnumerable<string> newFilters,
IOrderedEnumerable<string> existingFilters,
bool resourceAccessLevelFilterUpdated,
int? newAccessLevelId,
int? existingAccessLevelId,
bool filterProviderUpdated,
IOrderedEnumerable<string> newProviderFilters,
IOrderedEnumerable<string> existingProviderFilters,
bool filterResourceCollectionUpdated,
IOrderedEnumerable<string> newResourceCollectionFilter,
IOrderedEnumerable<string> 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);
}
}
}
}
}
7 changes: 7 additions & 0 deletions LearningHub.Nhs.WebUI/Interfaces/ISearchTelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,12 @@ public interface ISearchTelemetryService
/// <param name="model">The search executed telemetry model.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task RecordSearchExecutedFromApiAsync(SearchExecutedTelemetryModel model);

/// <summary>
/// Records search facet applied telemetry for facet usage analysis.
/// </summary>
/// <param name="model">The search facet applied telemetry model.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task RecordFacetAppliedTelemetryAsync(SearchFacetAppliedTelemetryModel model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace LearningHub.Nhs.WebUI.Models.Search
{
/// <summary>
/// Defines telemetry data for a search facet applied event.
/// </summary>
public class SearchFacetAppliedTelemetryModel
{
/// <summary>
/// Gets or sets the correlation id for the originating search request.
/// </summary>
public string CorrelationId { get; set; }

/// <summary>
/// Gets or sets the session id for the search session.
/// </summary>
public string SessionId { get; set; }

/// <summary>
/// Gets or sets the query text.
/// </summary>
public string QueryText { get; set; }

/// <summary>
/// Gets or sets the query mode.
/// </summary>
public string QueryMode { get; set; }

/// <summary>
/// Gets or sets the facet field name (e.g. "ResultType", "Category").
/// </summary>
public string FacetField { get; set; }

/// <summary>
/// Gets or sets the facet value (e.g. "Guidance", "Policy").
/// </summary>
public string FacetValue { get; set; }

/// <summary>
/// Gets or sets the facet action ("applied", "removed", or "cleared").
/// </summary>
public string FacetAction { get; set; }
}
}
36 changes: 36 additions & 0 deletions LearningHub.Nhs.WebUI/Services/SearchTelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,41 @@ public Task RecordSearchExecutedFromApiAsync(SearchExecutedTelemetryModel model)

return Task.CompletedTask;
}

/// <summary>
/// Records search facet applied telemetry for facet usage analysis.
/// </summary>
/// <param name="model">The search facet applied telemetry model.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public Task RecordFacetAppliedTelemetryAsync(SearchFacetAppliedTelemetryModel model)
{
if (model == null || string.IsNullOrWhiteSpace(model.FacetField))
{
return Task.CompletedTask;
}

try
{
var properties = new Dictionary<string, string>
{
{ "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;
}
}
}
Loading