Skip to content

Commit 9a26898

Browse files
Add analytics with pre-computed tables and stored procedures for environment, user, time patterns, components, and deployment duration (#350)
* Initial plan * Add enhanced analytics to about page with success rate trends and project comparisons Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Revert styling/formatting changes to files not directly related to analytics feature Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Add backend APIs for comprehensive analytics (environment, user, time, component, duration) Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Add TypeScript API clients for new analytics endpoints Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Replace success rate analytics with environment, user, time, component, and duration analytics Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Remove unused import to fix build Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Fix blank charts by adding null checks and conditional rendering Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Add detailed error logging and always render charts even with empty data Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Optimize analytics queries and consolidate controllers per code review feedback Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Refactor analytics to use pre-computed tables with stored procedures (follows existing pattern) Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Adds analytics database objects Adds tables and stored procedures to support analytics, including component usage, duration, environment usage, time patterns, and user activity. These objects will be used to populate the about page with analytics data. * Fix SQL syntax and add ordering to analytics queries Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Fix column name references in stored procedures (UserName and Environment) Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> * Remove redundant individual analytics controllers (consolidated into AnalyticsController) Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benhegartysefe <134416196+benhegartysefe@users.noreply.github.com> Co-authored-by: ben Hegarty <ben.hegarty@sefe.eu>
1 parent ba30917 commit 9a26898

39 files changed

+1372
-64
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using Dorc.ApiModel;
2+
using Dorc.PersistentData.Sources.Interfaces;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace Dorc.Api.Controllers
7+
{
8+
[Route("[controller]")]
9+
[ApiController]
10+
public class AnalyticsController : ControllerBase
11+
{
12+
private readonly IAnalyticsPersistentSource _analyticsPersistentSource;
13+
private readonly ILogger<AnalyticsController> _logger;
14+
15+
public AnalyticsController(
16+
IAnalyticsPersistentSource analyticsPersistentSource,
17+
ILogger<AnalyticsController> logger)
18+
{
19+
_analyticsPersistentSource = analyticsPersistentSource;
20+
_logger = logger;
21+
}
22+
23+
[HttpGet("EnvironmentUsage")]
24+
public ActionResult<IEnumerable<AnalyticsEnvironmentUsageApiModel>> GetEnvironmentUsage()
25+
{
26+
try
27+
{
28+
var data = _analyticsPersistentSource.GetEnvironmentUsage();
29+
return Ok(data);
30+
}
31+
catch (Exception ex)
32+
{
33+
_logger.LogError(ex, "Error retrieving environment usage analytics");
34+
return StatusCode(500, "An error occurred while retrieving environment usage data");
35+
}
36+
}
37+
38+
[HttpGet("UserActivity")]
39+
public ActionResult<IEnumerable<AnalyticsUserActivityApiModel>> GetUserActivity()
40+
{
41+
try
42+
{
43+
var data = _analyticsPersistentSource.GetUserActivity();
44+
return Ok(data);
45+
}
46+
catch (Exception ex)
47+
{
48+
_logger.LogError(ex, "Error retrieving user activity analytics");
49+
return StatusCode(500, "An error occurred while retrieving user activity data");
50+
}
51+
}
52+
53+
[HttpGet("TimePattern")]
54+
public ActionResult<IEnumerable<AnalyticsTimePatternApiModel>> GetTimePattern()
55+
{
56+
try
57+
{
58+
var data = _analyticsPersistentSource.GetTimePatterns();
59+
return Ok(data);
60+
}
61+
catch (Exception ex)
62+
{
63+
_logger.LogError(ex, "Error retrieving time pattern analytics");
64+
return StatusCode(500, "An error occurred while retrieving time pattern data");
65+
}
66+
}
67+
68+
[HttpGet("ComponentUsage")]
69+
public ActionResult<IEnumerable<AnalyticsComponentUsageApiModel>> GetComponentUsage()
70+
{
71+
try
72+
{
73+
var data = _analyticsPersistentSource.GetComponentUsage();
74+
return Ok(data);
75+
}
76+
catch (Exception ex)
77+
{
78+
_logger.LogError(ex, "Error retrieving component usage analytics");
79+
return StatusCode(500, "An error occurred while retrieving component usage data");
80+
}
81+
}
82+
83+
[HttpGet("Duration")]
84+
public ActionResult<AnalyticsDurationApiModel> GetDuration()
85+
{
86+
try
87+
{
88+
var data = _analyticsPersistentSource.GetDeploymentDuration();
89+
return Ok(data);
90+
}
91+
catch (Exception ex)
92+
{
93+
_logger.LogError(ex, "Error retrieving deployment duration analytics");
94+
return StatusCode(500, "An error occurred while retrieving deployment duration data");
95+
}
96+
}
97+
}
98+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Dorc.ApiModel
2+
{
3+
public class AnalyticsComponentUsageApiModel
4+
{
5+
public string ComponentName { get; set; }
6+
public int CountOfDeployments { get; set; }
7+
}
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Dorc.ApiModel
2+
{
3+
public class AnalyticsDurationApiModel
4+
{
5+
public double AverageDurationMinutes { get; set; }
6+
public double MaxDurationMinutes { get; set; }
7+
public double MinDurationMinutes { get; set; }
8+
public int TotalDeployments { get; set; }
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Dorc.ApiModel
2+
{
3+
public class AnalyticsEnvironmentUsageApiModel
4+
{
5+
public string EnvironmentName { get; set; }
6+
public int CountOfDeployments { get; set; }
7+
public int Failed { get; set; }
8+
}
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Dorc.ApiModel
2+
{
3+
public class AnalyticsTimePatternApiModel
4+
{
5+
public int HourOfDay { get; set; }
6+
public int DayOfWeek { get; set; }
7+
public string DayOfWeekName { get; set; }
8+
public int CountOfDeployments { get; set; }
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Dorc.ApiModel
2+
{
3+
public class AnalyticsUserActivityApiModel
4+
{
5+
public string UserName { get; set; }
6+
public int CountOfDeployments { get; set; }
7+
public int Failed { get; set; }
8+
}
9+
}

src/Dorc.Database/Dorc.Database.sqlproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,17 @@
358358
<Build Include="deploy\Stored Procedures\ArchiveDeploymentRequests.sql" />
359359
<Build Include="Schema Objects\Schemas\archive\Tables\EnvironmentComponentStatus.table.sql" />
360360
<None Include="Scripts\Post-Deployment\Environment_AccessControl_Populate.sql" />
361+
<Build Include="deploy\Stored Procedures\get_property_values_for_user.sql" />
362+
<Build Include="deploy\Stored Procedures\sp_PopulateAnalyticsComponentUsage.sql" />
363+
<Build Include="deploy\Stored Procedures\sp_PopulateAnalyticsDuration.sql" />
364+
<Build Include="deploy\Stored Procedures\sp_PopulateAnalyticsEnvironmentUsage.sql" />
365+
<Build Include="deploy\Stored Procedures\sp_PopulateAnalyticsTimePattern.sql" />
366+
<Build Include="deploy\Stored Procedures\sp_PopulateAnalyticsUserActivity.sql" />
367+
<Build Include="deploy\Tables\AnalyticsComponentUsage.sql" />
368+
<Build Include="deploy\Tables\AnalyticsDuration.sql" />
369+
<Build Include="deploy\Tables\AnalyticsEnvironmentUsage.sql" />
370+
<Build Include="deploy\Tables\AnalyticsTimePattern.sql" />
371+
<Build Include="deploy\Tables\AnalyticsUserActivity.sql" />
361372
</ItemGroup>
362373
<ItemGroup>
363374
<None Include="Schema Comparisons\LocalToProject.scmp">
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
CREATE PROCEDURE [deploy].[sp_PopulateAnalyticsComponentUsage]
2+
AS
3+
BEGIN
4+
SET NOCOUNT ON;
5+
6+
-- Clear existing data
7+
TRUNCATE TABLE [deploy].[AnalyticsComponentUsage];
8+
9+
-- Populate with fresh data from both main and archive tables
10+
-- Parse Components field (comma-separated) and count occurrences
11+
INSERT INTO [deploy].[AnalyticsComponentUsage] ([ComponentName], [DeploymentCount])
12+
SELECT TOP 50
13+
LTRIM(RTRIM([Component])) AS [ComponentName],
14+
COUNT(*) AS [DeploymentCount]
15+
FROM (
16+
SELECT [value] AS [Component]
17+
FROM [deploy].[DeploymentRequest]
18+
CROSS APPLY STRING_SPLIT([Components], ',')
19+
WHERE [Components] IS NOT NULL AND [Components] != ''
20+
21+
UNION ALL
22+
23+
SELECT [value] AS [Component]
24+
FROM [archive].[DeploymentRequest]
25+
CROSS APPLY STRING_SPLIT([Components], ',')
26+
WHERE [Components] IS NOT NULL AND [Components] != ''
27+
) AS ComponentData
28+
WHERE [Component] IS NOT NULL AND [Component] != ''
29+
GROUP BY LTRIM(RTRIM([Component]))
30+
ORDER BY COUNT(*) DESC;
31+
END
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
CREATE PROCEDURE [deploy].[sp_PopulateAnalyticsDuration]
2+
AS
3+
BEGIN
4+
SET NOCOUNT ON;
5+
6+
-- Clear existing data
7+
TRUNCATE TABLE [deploy].[AnalyticsDuration];
8+
9+
-- Calculate duration statistics from both main and archive tables
10+
INSERT INTO [deploy].[AnalyticsDuration] ([AverageDurationMinutes], [LongestDurationMinutes], [ShortestDurationMinutes])
11+
SELECT
12+
AVG([DurationMinutes]) AS [AverageDurationMinutes],
13+
MAX([DurationMinutes]) AS [LongestDurationMinutes],
14+
MIN([DurationMinutes]) AS [ShortestDurationMinutes]
15+
FROM (
16+
SELECT
17+
DATEDIFF(MINUTE, [StartedTime], [CompletedTime]) AS [DurationMinutes]
18+
FROM [deploy].[DeploymentRequest]
19+
WHERE [StartedTime] IS NOT NULL
20+
AND [CompletedTime] IS NOT NULL
21+
AND [CompletedTime] > [StartedTime]
22+
AND DATEDIFF(MINUTE, [StartedTime], [CompletedTime]) > 0
23+
AND DATEDIFF(MINUTE, [StartedTime], [CompletedTime]) < 1440
24+
25+
UNION ALL
26+
27+
SELECT
28+
DATEDIFF(MINUTE, [StartedTime], [CompletedTime]) AS [DurationMinutes]
29+
FROM [archive].[DeploymentRequest]
30+
WHERE [StartedTime] IS NOT NULL
31+
AND [CompletedTime] IS NOT NULL
32+
AND [CompletedTime] > [StartedTime]
33+
AND DATEDIFF(MINUTE, [StartedTime], [CompletedTime]) > 0
34+
AND DATEDIFF(MINUTE, [StartedTime], [CompletedTime]) < 1440
35+
) AS DurationData;
36+
END
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
CREATE PROCEDURE [deploy].[sp_PopulateAnalyticsEnvironmentUsage]
2+
AS
3+
BEGIN
4+
SET NOCOUNT ON;
5+
6+
-- Clear existing data
7+
TRUNCATE TABLE [deploy].[AnalyticsEnvironmentUsage];
8+
9+
-- Populate with fresh data from both main and archive tables
10+
INSERT INTO [deploy].[AnalyticsEnvironmentUsage] ([EnvironmentName], [TotalDeployments], [SuccessCount], [FailCount])
11+
SELECT
12+
ISNULL([Environment], 'Unknown') AS [EnvironmentName],
13+
COUNT(*) AS [TotalDeployments],
14+
SUM(CASE WHEN [Status] = 'Completed' OR [Status] = 'Success' THEN 1 ELSE 0 END) AS [SuccessCount],
15+
SUM(CASE WHEN [Status] = 'Failed' OR [Status] = 'Error' THEN 1 ELSE 0 END) AS [FailCount]
16+
FROM (
17+
SELECT [Environment], [Status]
18+
FROM [deploy].[DeploymentRequest]
19+
WHERE [Environment] IS NOT NULL
20+
21+
UNION ALL
22+
23+
SELECT [Environment], [Status]
24+
FROM [archive].[DeploymentRequest]
25+
WHERE [Environment] IS NOT NULL
26+
) AS CombinedData
27+
GROUP BY [Environment];
28+
END

0 commit comments

Comments
 (0)