From cfcda6c6178ff5922b31450ebd495e0b53375048 Mon Sep 17 00:00:00 2001 From: Emre KARA Date: Wed, 21 May 2025 13:36:18 +0300 Subject: [PATCH] Refactor telemetry solution info handling. --- .../Admin/Localization/Resources/en.json | 3 +- ...pApplicationWithExternalServiceProvider.cs | 9 +- .../Activity/ActivityDataProvider.cs | 218 ++-------------- .../Activity/ActivityPropertyName.cs | 32 +++ .../Abp/Telemetry/Activity/ActivityStorage.cs | 28 +- .../Telemetry/Activity/IActivityStorage.cs | 15 +- .../Activity/ISolutionInfoProvider.cs | 12 + .../Activity/SolutionInfoProvider.cs | 244 ++++++++++++++++++ .../Volo/Abp/Telemetry/ITelemetryService.cs | 1 + .../Volo/Abp/Telemetry/TelemetryService.cs | 12 + 10 files changed, 364 insertions(+), 210 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ISolutionInfoProvider.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/SolutionInfoProvider.cs diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index d056e70b09..053cf12c42 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -717,6 +717,7 @@ "IncludePrereleaseInfo": "Gets or sets whether to include prerelease versions in the cache", "CacheVersionCount": "Cache Version Count", "CacheVersionCountInfo": "Gets or sets the number of versions to cache", - "CacheLatestVersionsInfo": "This caches the latest versions of the NuGet packages" + "CacheLatestVersionsInfo": "This caches the latest versions of the NuGet packages", + "Menu:TelemetryReports" : "Telemetry Reports" } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs index 7b44c2fd39..4480dd1ced 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs @@ -88,11 +88,10 @@ internal class AbpApplicationWithExternalServiceProvider : AbpApplicationBase, I await using var _ = telemetryService.TrackActivity(ActivityNameConsts.ApplicationRun, activity => { - activity.Add(ActivityPropertyName.Assembly, assembly.Location); - activity.Add(ActivityPropertyName.ProjectId, packageMetadata.ProjectId!); - activity.Add(ActivityPropertyName.ProjectType, packageMetadata.Role!); - activity.Add(ActivityPropertyName.SolutionPath, - packageMetadata.AbpSlnPath ?? string.Empty); + activity[ActivityPropertyName.Assembly] = assembly.Location; + activity[ActivityPropertyName.ProjectId] = packageMetadata.ProjectId!; + activity[ActivityPropertyName.ProjectType] = packageMetadata.Role!; + activity[ActivityPropertyName.SolutionPath] = packageMetadata.AbpSlnPath!; }); } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityDataProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityDataProvider.cs index bea53f1c8c..cf1f946dd2 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityDataProvider.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityDataProvider.cs @@ -1,18 +1,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.Json; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; -using Volo.Abp.Telemetry.EnvironmentInspection; using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; -using Volo.Abp.Telemetry.Shared; using Volo.Abp.Telemetry.Shared.Enums; -using DatabaseProvider = Volo.Abp.Telemetry.Shared.Enums.DatabaseProvider; -using MobileApp = Volo.Abp.Telemetry.Shared.Enums.MobileApp; namespace Volo.Abp.Telemetry.Activity; @@ -22,20 +14,22 @@ public class ActivityDataProvider : IActivityDataProvider, IScopedDependency private readonly ISoftwareInfoProvider _softwareInfoProvider; private readonly IEnumerable _applicationInfoContributors; private readonly IActivityStorage _activityStorage; + private readonly ISolutionInfoProvider _solutionInfoProvider; public ActivityDataProvider( IDeviceInfoProvider deviceInfoProvider, ISoftwareInfoProvider softwareInfoProvider, IEnumerable applicationInfoContributors, - IActivityStorage activityStorage) + IActivityStorage activityStorage, ISolutionInfoProvider solutionInfoProvider) { _deviceInfoProvider = deviceInfoProvider; _softwareInfoProvider = softwareInfoProvider; _applicationInfoContributors = applicationInfoContributors; _activityStorage = activityStorage; + _solutionInfoProvider = solutionInfoProvider; } - public virtual SessionType GetSessionType() + protected virtual SessionType GetSessionType() { return SessionType.ApplicationRuntime; } @@ -51,7 +45,7 @@ public class ActivityDataProvider : IActivityDataProvider, IScopedDependency activity[ActivityPropertyName.IsFirstSession] = isFirstSession; activity[ActivityPropertyName.DeviceId] = _deviceInfoProvider.GetDeviceId(); - if (await ShouldAddDeviceInfoAsync()) + if (await _activityStorage.ShouldAddDeviceInfoAsync()) { await AddDeviceInformationAsync(activity); } @@ -63,28 +57,26 @@ public class ActivityDataProvider : IActivityDataProvider, IScopedDependency if (await ShouldAddSolutionInformation(activity)) { - await AddSolutionInformationAsync(activity); + var solutionInfo = await _solutionInfoProvider.GetSolutionInfoAsync(activity[ActivityPropertyName.SolutionPath].ToString()!); + + foreach (var entry in solutionInfo) + { + activity[entry.Key] = entry.Value; + } } } - protected virtual async Task ShouldAddDeviceInfoAsync() - { - var lastSend = await _activityStorage.GetLastDeviceInfoSendTimeAsync(); - return lastSend is null || DateTimeOffset.UtcNow - lastSend > TimeSpan.FromDays(7); - } - protected virtual async Task ShouldAddSolutionInformation(ActivityData activity) { if ( !activity.TryGetValue(ActivityPropertyName.SolutionId, out var id) || - !Guid.TryParse(id.ToString(), out var solutionId)) + !Guid.TryParse(id.ToString(), out var solutionId) && activity.ContainsKey(ActivityPropertyName.SolutionPath)) { return false; } - var lastSend = await _activityStorage.GetLastSolutionInfoSendTimeAsync(solutionId); - return lastSend is null || DateTimeOffset.UtcNow - lastSend > TimeSpan.FromDays(7); + return await _activityStorage.ShouldAddSolutionInformation(solutionId); } protected virtual async Task AddDeviceInformationAsync(ActivityData activityData) @@ -96,6 +88,8 @@ public class ActivityDataProvider : IActivityDataProvider, IScopedDependency var softwareList = await _softwareInfoProvider.GetSoftwareInfoAsync(); activityData[ActivityPropertyName.InstalledSoftwares] = softwareList; + + await _activityStorage.MarkDeviceInfoAsSentAsync(); } protected virtual async Task AddApplicationInformationAsync(ActivityData activityData) @@ -106,26 +100,6 @@ public class ActivityDataProvider : IActivityDataProvider, IScopedDependency } } - protected virtual async Task AddSolutionInformationAsync(ActivityData activityData) - { - var solutionJson = File.ReadAllText(activityData[ActivityPropertyName.SolutionPath].ToString()!); - using var solutionDoc = JsonDocument.Parse(solutionJson); - var root = solutionDoc.RootElement; - - - if (root.TryGetProperty("creatingStudioConfiguration", out var config)) - { - AddCreatingStudioConfiguration(activityData, config); - } - - activityData[ActivityPropertyName.LicenseType] = await GetLicenseTypeAsync(); - - if (root.TryGetProperty("modules", out var modulesElement) && modulesElement.ValueKind == JsonValueKind.Object) - { - var modules = ExtractModules(modulesElement); - activityData[ActivityPropertyName.InstalledModules] = modules; - } - } protected virtual void AddSolutionId(ActivityData activityData) { @@ -139,165 +113,11 @@ public class ActivityDataProvider : IActivityDataProvider, IScopedDependency return; } - var solutionJson = File.ReadAllText((string)path); - using var solutionDoc = JsonDocument.Parse(solutionJson); - var root = solutionDoc.RootElement; - - var solutionId = ReadSolutionIdFromJson(root); - activityData[ActivityPropertyName.SolutionId] = solutionId; - } - - protected virtual Guid ReadSolutionIdFromJson(JsonElement root) - { - return root.TryGetProperty("id", out var idElement) && Guid.TryParse(idElement.GetString(), out var id) - ? id - : throw new Exception("Solution ID is not valid."); - } - - protected virtual void AddCreatingStudioConfiguration(ActivityData activityData, JsonElement config) - { - activityData[ActivityPropertyName.Template] = MapSolutionTemplate(config.GetProperty("template").GetString()); - activityData[ActivityPropertyName.CreatedAbpStudioVersion] = config.GetProperty("createdAbpStudioVersion").GetString()!; - activityData[ActivityPropertyName.IsTiered] = config.GetProperty("Tiered").GetBoolean(); - activityData[ActivityPropertyName.UiFramework] = MapUiFramework(config.GetProperty("uiFramework").GetString()); - activityData[ActivityPropertyName.DatabaseProvider] = MapDatabaseProvider(config.GetProperty("databaseProvider").GetString()); - activityData[ActivityPropertyName.DatabaseManagementSystem] = MapDbms(config.GetProperty("databaseManagementSystem").GetString()); - activityData[ActivityPropertyName.IsSeparateTenantSchema] = config.GetProperty("separateTenantSchema").GetBoolean(); - activityData[ActivityPropertyName.Theme] = MapUiTheme(config.GetProperty("theme").GetString()); - activityData[ActivityPropertyName.ThemeStyle] = MapUiThemeStyle(config.GetProperty("themeStyle").GetString()); - activityData[ActivityPropertyName.MobileFramework] = MapMobileApp(config.GetProperty("mobileFramework").GetString()); - activityData[ActivityPropertyName.HasPublicWebsite] = config.GetProperty("publicWebsite").GetBoolean(); - activityData[ActivityPropertyName.IncludeTests] = config.GetProperty("includeTests").GetBoolean(); - activityData[ActivityPropertyName.MultiTenancy] = config.GetProperty("multiTenancy").GetBoolean(); - activityData[ActivityPropertyName.DynamicLocalization] = config.GetProperty("dynamicLocalization").GetBoolean(); - activityData[ActivityPropertyName.KubernetesConfiguration] = config.GetProperty("kubernetesConfiguration").GetBoolean(); - activityData[ActivityPropertyName.GrafanaDashboard] = config.GetProperty("grafanaDashboard").GetBoolean(); - activityData[ActivityPropertyName.SocialLogins] = config.GetProperty("socialLogin").GetBoolean(); - - } - - protected virtual List ExtractModules(JsonElement modulesElement) - { - var modules = new List(); - - foreach (var module in modulesElement.EnumerateObject()) + var solutionId = _solutionInfoProvider.GetSolutionId((string)path); + + if (solutionId.HasValue) { - var name = module.Name; - if (!module.Value.TryGetProperty("path", out var pathElement)) - { - continue; - } - - var path = pathElement.GetString(); - if (path.IsNullOrEmpty() || !File.Exists(path)) - { - continue; - } - - var moduleJson = File.ReadAllText(path); - using var moduleDoc = JsonDocument.Parse(moduleJson); - - if (!moduleDoc.RootElement.TryGetProperty("imports", out var imports) || imports.ValueKind != JsonValueKind.Object) - { - continue; - } - - foreach (var import in imports.EnumerateObject()) - { - var version = import.Value.GetProperty("version").GetString(); - var creationTime = import.Value.TryGetProperty("creationTime", out var ct) ? DateTimeOffset.Parse(ct.GetString()!) : (DateTimeOffset?)null; - - if (modules.Any(x => x.ModuleName == name && x.Version == version)) - { - continue; - } - - modules.Add(new SolutionModuleInstallationInfo { ModuleName = name, Version = version, InstallationTime = creationTime }); - } - } - - return modules; - } - - protected virtual async Task GetLicenseTypeAsync() - { - if (!File.Exists(AbpTelemetryPaths.AccessToken)) - { - return 0; + activityData[ActivityPropertyName.SolutionId] = solutionId.Value; } - - using var httpClient = new HttpClient(); - var accessToken = File.ReadAllText(AbpTelemetryPaths.AccessToken); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - - var json = await httpClient.GetStringAsync($"{AbpPlatformUrls.AbpIoUrl}api/license/api-key"); - using var doc = JsonDocument.Parse(json); - - return doc.RootElement.GetProperty("licenseType").GetInt32(); } - - protected virtual UiTheme MapUiTheme(string? theme) => theme switch - { - "none" => UiTheme.None, - "basic" => UiTheme.Basic, - "leptonx" => UiTheme.LeptonX, - "leptonx-lite" => UiTheme.LeptonXLite, - _ => UiTheme.Unknown - }; - - protected virtual UiThemeStyle MapUiThemeStyle(string? style) => style switch - { - "dim" => UiThemeStyle.Dim, - "style" => UiThemeStyle.System, - "dark" => UiThemeStyle.Dark, - "light" => UiThemeStyle.Light, - _ => UiThemeStyle.Unknown - }; - - protected virtual SolutionTemplate MapSolutionTemplate(string? template) => template switch - { - "microservice" => SolutionTemplate.Microservice, - "app-nolayers" => SolutionTemplate.AppNoLayers, - "app" => SolutionTemplate.AppLayered, - _ => SolutionTemplate.Unknown - }; - - protected virtual UiFramework MapUiFramework(string? framework) => framework switch - { - "mvc" => UiFramework.MvcRazorPages, - "blazor" => UiFramework.BlazorWasm, - "angular" => UiFramework.Angular, - "blazor-server" => UiFramework.BlazorServer, - "blazor-webapp" => UiFramework.BlazorWebApp, - "maui-blazor" => UiFramework.BlazorMaUI, - "none" => UiFramework.None, - _ => UiFramework.Unknown - }; - - protected virtual DatabaseProvider MapDatabaseProvider(string? provider) => provider switch - { - "ef" => DatabaseProvider.EfCore, - "mongodb" => DatabaseProvider.MongoDb, - "none" => DatabaseProvider.None, - _ => DatabaseProvider.Unknown - }; - - protected virtual Dbms MapDbms(string? dbms) => dbms switch - { - "mysql" => Dbms.MySql, - "oracle" => Dbms.Oracle, - "oracle-devart" => Dbms.OracleDevart, - "postgresql" => Dbms.PostgreSql, - "sqlserver" => Dbms.SqlServer, - "sqlite" => Dbms.Sqlite, - _ => Dbms.Unknown - }; - - protected virtual MobileApp MapMobileApp(string? mobile) => mobile switch - { - "maui" => MobileApp.Maui, - "react-native" => MobileApp.ReactNative, - "none" => MobileApp.None, - _ => MobileApp.Unknown - }; } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityPropertyName.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityPropertyName.cs index 6507844ccb..acba00a8fc 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityPropertyName.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityPropertyName.cs @@ -3,6 +3,10 @@ public static class ActivityPropertyName { public const string SessionId = nameof(SessionId); + public const string Id = nameof(Id); + public const string UserId = nameof(UserId); + public const string OrganizationId = nameof(OrganizationId); + public const string IpAddress = nameof(IpAddress); public const string IsFirstSession = nameof(IsFirstSession); public const string DeviceId = nameof(DeviceId); public const string DeviceType = nameof(DeviceType); @@ -41,4 +45,32 @@ public static class ActivityPropertyName public const string Assembly = nameof(Assembly); public const string LicenseType = nameof(LicenseType); public const string SessionType = nameof(SessionType); + public const string HasError = nameof(HasError); + public const string ActivityDuration = nameof(ActivityDuration); + public const string ActivityDetails = nameof(ActivityDetails); + public const string ApplicationId = nameof(ApplicationId); + public const string Time = nameof(Time); + public const string SoftwareName = nameof(SoftwareName); + public const string SoftwareVersion = nameof(SoftwareVersion); + public const string SoftwareUiTheme = nameof(SoftwareUiTheme); + public const string SoftwareType = nameof(SoftwareType); + public const string UserDeviceId = nameof(UserDeviceId); + public const string SolutionTemplate = nameof(SolutionTemplate); + public const string WebFramework = nameof(WebFramework); + public const string Dbms = nameof(Dbms); + public const string UiTheme = nameof(UiTheme); + public const string UiThemeStyle = nameof(UiThemeStyle); + public const string MobileApp = nameof(MobileApp); + public const string IsMultiTenant = nameof(IsMultiTenant); + public const string SampleCrudPage = nameof(SampleCrudPage); + public const string FirstAbpVersion = nameof(FirstAbpVersion); + public const string FirstDotnetVersion = nameof(FirstDotnetVersion); + public const string CreationTool = nameof(CreationTool); + public const string Applications = nameof(Applications); + public const string ModuleName = nameof(ModuleName); + public const string ModuleVersion = nameof(ModuleVersion); + public const string ModuleInstallationTime = nameof(ModuleInstallationTime); + public const string ApplicationType = nameof(ApplicationType); + public const string AbpModuleCount = nameof(AbpModuleCount); + } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityStorage.cs index d007d8b2fd..f10f6c3716 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityStorage.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ActivityStorage.cs @@ -128,7 +128,10 @@ public class ActivityStorage : IActivityStorage, ISingletonDependency { using var reader = new StreamReader(stream, Encoding.UTF8); var json = await reader.ReadToEndAsync(); - return JsonSerializer.Deserialize(json) ?? new ActivityStorageState(); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }) ?? new ActivityStorageState(); }); return _cachedState; } @@ -178,7 +181,7 @@ public class ActivityStorage : IActivityStorage, ISingletonDependency } - private async Task EnsureFileExistsAsync() + private Task EnsureFileExistsAsync() { try { @@ -192,7 +195,11 @@ public class ActivityStorage : IActivityStorage, ISingletonDependency if (!File.Exists(AbpTelemetryPaths.ActivityStorage)) { var json = JsonSerializer.Serialize(_cachedState ?? new ActivityStorageState(), - new JsonSerializerOptions { WriteIndented = true }); + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }); File.WriteAllText(AbpTelemetryPaths.ActivityStorage, json, Encoding.UTF8); } } @@ -200,5 +207,20 @@ public class ActivityStorage : IActivityStorage, ISingletonDependency { //ignored } + + return Task.CompletedTask; + } + + + public virtual async Task ShouldAddDeviceInfoAsync() + { + var lastSend = await GetLastDeviceInfoSendTimeAsync(); + return lastSend is null || DateTimeOffset.UtcNow - lastSend > TimeSpan.FromDays(7); + } + + public virtual async Task ShouldAddSolutionInformation(Guid solutionId) + { + var lastSend = await GetLastSolutionInfoSendTimeAsync(solutionId); + return lastSend is null || DateTimeOffset.UtcNow - lastSend > TimeSpan.FromDays(7); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/IActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/IActivityStorage.cs index 80e00cf908..652e6a68ae 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/IActivityStorage.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/IActivityStorage.cs @@ -1,7 +1,16 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Telemetry.Shared; +using Volo.Abp.Telemetry.Shared.Enums; namespace Volo.Abp.Telemetry.Activity; @@ -10,10 +19,12 @@ public interface IActivityStorage Task GetLastActivitySendTimeAsync(); Task<(bool isFirstSession, Guid sessionId)> GetOrCreateSessionInfoAsync(); Task MarkActivitiesAsSentAsync(); + Task MarkDeviceInfoAsSentAsync(); Task GetLastSolutionInfoSendTimeAsync(Guid solutionId); Task GetLastDeviceInfoSendTimeAsync(); Task BufferActivityAsync(ActivityData activityData); Task> GetBufferedActivitiesAsync(); Task EndSessionAsync(); -} - + Task ShouldAddDeviceInfoAsync(); + Task ShouldAddSolutionInformation(Guid solutionId); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ISolutionInfoProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ISolutionInfoProvider.cs new file mode 100644 index 0000000000..abaf487d0a --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/ISolutionInfoProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.Telemetry.Activity; + +public interface ISolutionInfoProvider +{ + Task> GetSolutionInfoAsync(string solutionPath); + + Guid? GetSolutionId(string solutionPath); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/SolutionInfoProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/SolutionInfoProvider.cs new file mode 100644 index 0000000000..5818845bd7 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/SolutionInfoProvider.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Telemetry.Shared; +using Volo.Abp.Telemetry.Shared.Enums; + +namespace Volo.Abp.Telemetry.Activity; + +public class SolutionInfoProvider : ISolutionInfoProvider , ISingletonDependency +{ + public async Task> GetSolutionInfoAsync(string solutionPath) + { + var dictionary = new Dictionary(); + + var solutionJson = File.ReadAllText(solutionPath); + using var solutionDoc = JsonDocument.Parse(solutionJson); + var root = solutionDoc.RootElement; + + + if (root.TryGetProperty("creatingStudioConfiguration", out var config)) + { + AddCreatingStudioConfiguration(dictionary, config); + } + + dictionary[ActivityPropertyName.LicenseType] = await GetLicenseTypeAsync(); + + if (root.TryGetProperty("modules", out var modulesElement) && modulesElement.ValueKind == JsonValueKind.Object) + { + var modules = ExtractModules(modulesElement); + dictionary[ActivityPropertyName.InstalledModules] = modules; + } + + return dictionary; + } + + public Guid? GetSolutionId(string solutionPath) + { + var solutionJson = File.ReadAllText((string)solutionPath); + using var solutionDoc = JsonDocument.Parse(solutionJson); + var root = solutionDoc.RootElement; + + return ReadSolutionIdFromJson(root); + } + + private Guid? ReadSolutionIdFromJson(JsonElement root) + { + return root.TryGetProperty("id", out var idElement) && Guid.TryParse(idElement.GetString(), out var id) + ? id + : null; + } + + protected virtual void AddCreatingStudioConfiguration(Dictionary dict, JsonElement config) + { + try + { + var mappings = new Dictionary> + { + { "template", value => dict[ActivityPropertyName.Template] = MapSolutionTemplate(value.GetString()) }, + { "createdAbpStudioVersion", value => dict[ActivityPropertyName.CreatedAbpStudioVersion] = value.GetString()! }, + { "tiered", value => dict[ActivityPropertyName.IsTiered] = ParseBool(value) }, + { "uiFramework", value => dict[ActivityPropertyName.UiFramework] = MapUiFramework(value.GetString()) }, + { "databaseProvider", value => dict[ActivityPropertyName.DatabaseProvider] = MapDatabaseProvider(value.GetString()) }, + { "databaseManagementSystem", value => dict[ActivityPropertyName.DatabaseManagementSystem] = MapDbms(value.GetString()) }, + { "separateTenantSchema", value => dict[ActivityPropertyName.IsSeparateTenantSchema] = ParseBool(value) }, + { "theme", value => dict[ActivityPropertyName.Theme] = MapUiTheme(value.GetString()) }, + { "themeStyle", value => dict[ActivityPropertyName.ThemeStyle] = MapUiThemeStyle(value.GetString()) }, + { "mobileFramework", value => dict[ActivityPropertyName.MobileFramework] = MapMobileApp(value.GetString()) }, + { "publicWebsite", value => dict[ActivityPropertyName.HasPublicWebsite] = ParseBool(value) }, + { "includeTests", value => dict[ActivityPropertyName.IncludeTests] = ParseBool(value) }, + { "multiTenancy", value => dict[ActivityPropertyName.MultiTenancy] = ParseBool(value) }, + { "dynamicLocalization", value => dict[ActivityPropertyName.DynamicLocalization] = ParseBool(value) }, + { "kubernetesConfiguration", value => dict[ActivityPropertyName.KubernetesConfiguration] = ParseBool(value) }, + { "grafanaDashboard", value => dict[ActivityPropertyName.GrafanaDashboard] = ParseBool(value) }, + { "socialLogin", value => dict[ActivityPropertyName.SocialLogins] = ParseBool(value) } + }; + foreach (var mapping in mappings) + { + try + { + if (config.TryGetProperty(mapping.Key, out var propertyValue)) + { + mapping.Value(propertyValue); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static bool ParseBool(JsonElement element) => + element.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.String when bool.TryParse(element.GetString(), out var b) => b, + _ => false + }; + + + protected virtual List ExtractModules(JsonElement modulesElement) + { + var modules = new List(); + + foreach (var module in modulesElement.EnumerateObject()) + { + var name = module.Name; + if (!module.Value.TryGetProperty("path", out var pathElement)) + { + continue; + } + + var path = pathElement.GetString(); + if (path.IsNullOrEmpty() || !File.Exists(path)) + { + continue; + } + + var moduleJson = File.ReadAllText(path); + using var moduleDoc = JsonDocument.Parse(moduleJson); + + if (!moduleDoc.RootElement.TryGetProperty("imports", out var imports) || + imports.ValueKind != JsonValueKind.Object) + { + continue; + } + + foreach (var import in imports.EnumerateObject()) + { + var version = import.Value.GetProperty("version").GetString(); + var creationTime = import.Value.TryGetProperty("creationTime", out var ct) + ? DateTimeOffset.Parse(ct.GetString()!) + : (DateTimeOffset?)null; + + if (modules.Any(x => x.ModuleName == name && x.Version == version)) + { + continue; + } + + modules.Add(new SolutionModuleInstallationInfo + { + ModuleName = name, Version = version, InstallationTime = creationTime + }); + } + } + + return modules; + } + + protected virtual async Task GetLicenseTypeAsync() + { + if (!File.Exists(AbpTelemetryPaths.AccessToken)) + { + return 0; + } + + using var httpClient = new HttpClient(); + var accessToken = File.ReadAllText(AbpTelemetryPaths.AccessToken); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + var json = await httpClient.GetStringAsync($"{AbpPlatformUrls.AbpIoUrl}api/license/api-key"); + using var doc = JsonDocument.Parse(json); + + return doc.RootElement.GetProperty("licenseType").GetInt32(); + } + + protected virtual UiTheme MapUiTheme(string? theme) => theme switch + { + "none" => UiTheme.None, + "basic" => UiTheme.Basic, + "leptonx" => UiTheme.LeptonX, + "leptonx-lite" => UiTheme.LeptonXLite, + _ => UiTheme.Unknown + }; + + protected virtual UiThemeStyle MapUiThemeStyle(string? style) => style switch + { + "dim" => UiThemeStyle.Dim, + "style" => UiThemeStyle.System, + "dark" => UiThemeStyle.Dark, + "light" => UiThemeStyle.Light, + _ => UiThemeStyle.Unknown + }; + + protected virtual SolutionTemplate MapSolutionTemplate(string? template) => template switch + { + "microservice" => SolutionTemplate.Microservice, + "app-nolayers" => SolutionTemplate.AppNoLayers, + "app" => SolutionTemplate.AppLayered, + _ => SolutionTemplate.Unknown + }; + + protected virtual UiFramework MapUiFramework(string? framework) => framework switch + { + "mvc" => UiFramework.MvcRazorPages, + "blazor" => UiFramework.BlazorWasm, + "angular" => UiFramework.Angular, + "blazor-server" => UiFramework.BlazorServer, + "blazor-webapp" => UiFramework.BlazorWebApp, + "maui-blazor" => UiFramework.BlazorMaUI, + "none" => UiFramework.None, + _ => UiFramework.Unknown + }; + + protected virtual DatabaseProvider MapDatabaseProvider(string? provider) => provider switch + { + "ef" => DatabaseProvider.EfCore, + "mongodb" => DatabaseProvider.MongoDb, + "none" => DatabaseProvider.None, + _ => DatabaseProvider.Unknown + }; + + protected virtual Dbms MapDbms(string? dbms) => dbms switch + { + "mysql" => Dbms.MySql, + "oracle" => Dbms.Oracle, + "oracle-devart" => Dbms.OracleDevart, + "postgresql" => Dbms.PostgreSql, + "sqlserver" => Dbms.SqlServer, + "sqlite" => Dbms.Sqlite, + _ => Dbms.Unknown + }; + + protected virtual MobileApp MapMobileApp(string? mobile) => mobile switch + { + "maui" => MobileApp.Maui, + "react-native" => MobileApp.ReactNative, + "none" => MobileApp.None, + _ => MobileApp.Unknown + }; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/ITelemetryService.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/ITelemetryService.cs index beb189cf5b..25a2567bc5 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/ITelemetryService.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/ITelemetryService.cs @@ -11,4 +11,5 @@ public interface ITelemetryService IAsyncDisposable TrackActivity(ActivityData activityData); Task AddActivityAsync(ActivityData data); Task AddActivityAsync(string activityName, string? details = null); + Task AddActivityAsync(string activityName, Action configure); } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryService.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryService.cs index d9226c1d76..ef60105b83 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryService.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryService.cs @@ -95,4 +95,16 @@ public class TelemetryService : ITelemetryService , IScopedDependency { await AddActivityAsync(new ActivityData(activityName, details)); } + + public async Task AddActivityAsync(string activityName, Action configure) + { + var activityData = new ActivityData(activityName) + { + Time = DateTimeOffset.UtcNow + }; + + configure?.Invoke(activityData); + + await AddActivityAsync(activityData); + } } \ No newline at end of file