From cc0effe7db177153e458cf6d2a3f47dc63522f14 Mon Sep 17 00:00:00 2001 From: Emre KARA Date: Mon, 21 Jul 2025 14:05:49 +0300 Subject: [PATCH 1/5] data collection implementation --- Directory.Packages.props | 1 + .../TelemetryApplicationMetricsEnricher.cs | 47 +++ .../Properties/AssemblyInfo.cs | 20 ++ .../Permissions/PermissionDefinition.cs | 2 + .../PermissionDefinitionContext.cs | 18 +- .../Permissions/PermissionGroupDefinition.cs | 2 + .../StaticPermissionDefinitionStore.cs | 5 + .../TelemetryPermissionInfoEnricher.cs | 45 +++ .../src/Volo.Abp.Core/Volo.Abp.Core.csproj | 1 + .../Volo/Abp/AbpApplicationBase.cs | 54 ++++ ...pApplicationWithExternalServiceProvider.cs | 4 + ...pApplicationWithInternalServiceProvider.cs | 3 + .../Telemetry/Activity/ActivityContext.cs | 50 +++ .../Telemetry/Activity/ActivityEvent.cs | 147 +++++++++ ...HasParentTelemetryActivityEventEnricher.cs | 7 + .../ITelemetryActivityEventBuilder.cs | 8 + .../ITelemetryActivityEventEnricher.cs | 11 + .../Contracts/ITelemetryActivityStorage.cs | 17 ++ .../TelemetryActivityEventBuilder.cs | 49 +++ .../TelemetryActivityEventEnricher.cs | 70 +++++ .../TelemetryApplicationInfoEnricher.cs | 104 +++++++ .../Providers/TelemetryDeviceInfoEnricher.cs | 79 +++++ .../Providers/TelemetryModuleInfoEnricher.cs | 39 +++ .../Providers/TelemetrySessionInfoEnricher.cs | 27 ++ .../TelemetrySolutionInfoEnricher.cs | 130 ++++++++ .../Activity/Storage/FailedActivityInfo.cs | 18 ++ .../Storage/TelemetryActivityStorage.cs | 205 +++++++++++++ .../Storage/TelemetryActivityStorageState.cs | 16 + .../Activity/Storage/TelemetryPeriod.cs | 34 +++ .../Activity/TelemetryJsonExtensions.cs | 34 +++ .../Telemetry/Constants/AbpPlatformUrls.cs | 12 + .../Telemetry/Constants/ActivityNameConsts.cs | 77 +++++ .../Constants/ActivityPropertyNames.cs | 77 +++++ .../Telemetry/Constants/DeviceManager.cs | 286 ++++++++++++++++++ .../Telemetry/Constants/Enums/AbpTool.cs | 9 + .../Constants/Enums/OperationSystem.cs | 9 + .../Telemetry/Constants/Enums/SessionType.cs | 9 + .../Telemetry/Constants/Enums/SoftwareType.cs | 11 + .../Telemetry/Constants/TelemetryConsts.cs | 6 + .../Telemetry/Constants/TelemetryPaths.cs | 13 + .../Contracts/ISoftwareDetector.cs | 9 + .../Contracts/ISoftwareInfoProvider.cs | 9 + .../Contracts/SoftwareInfo.cs | 11 + .../Core/SoftwareDetector.cs | 78 +++++ .../Detectors/AbpStudioDetector.cs | 76 +++++ .../Detectors/ChromeDetector.cs | 48 +++ .../Detectors/DotnetSdkDetector.cs | 17 ++ .../Detectors/FireFoxDetector.cs | 45 +++ .../Detectors/MsEdgeDetector.cs | 47 +++ .../Detectors/NodeJsDetector.cs | 33 ++ .../Detectors/OperatingSystemDetector.cs | 77 +++++ .../Detectors/RiderDetector.cs | 100 ++++++ .../Detectors/VisualStudioCodeDetector.cs | 132 ++++++++ .../Detectors/VisualStudioDetector.cs | 106 +++++++ .../Providers/SoftwareInfoProvider.cs | 40 +++ .../Helpers/AbpPackageMetadataReader.cs | 135 +++++++++ .../Telemetry/Helpers/AbpProjectMetaData.cs | 10 + .../Telemetry/Helpers/Cryptography.cs | 42 +++ .../Telemetry/Helpers/MutexExecutor.cs | 46 +++ .../Telemetry/ITelemetryActivitySender.cs | 8 + .../Internal/Telemetry/ITelemetryService.cs | 13 + .../Telemetry/TelemetryActivitySender.cs | 131 ++++++++ .../Internal/Telemetry/TelemetryService.cs | 101 +++++++ .../Telemetry/TelemetryDomainInfoEnricher.cs | 41 +++ ...izationTestPermissionDefinitionProvider.cs | 6 +- .../Volo/Abp/Telemetry/ActivityEvent_Tests.cs | 89 ++++++ .../Volo/Abp/Telemetry/Cryptography_Tests.cs | 52 ++++ 67 files changed, 3256 insertions(+), 2 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/TelemetryApplicationMetricsEnricher.cs create mode 100644 framework/src/Volo.Abp.Authorization.Abstractions/Properties/AssemblyInfo.cs create mode 100644 framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/TelemetryPermissionInfoEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityContext.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityEvent.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/IHasParentTelemetryActivityEventEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventBuilder.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityStorage.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventBuilder.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryApplicationInfoEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryDeviceInfoEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryModuleInfoEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySessionInfoEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/FailedActivityInfo.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorageState.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryPeriod.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/TelemetryJsonExtensions.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/AbpPlatformUrls.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/AbpTool.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/OperationSystem.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SessionType.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SoftwareType.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryConsts.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryPaths.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Core/SoftwareDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Providers/SoftwareInfoProvider.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpPackageMetadataReader.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpProjectMetaData.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/Cryptography.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/MutexExecutor.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryActivitySender.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryService.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryActivitySender.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs create mode 100644 framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Telemetry/TelemetryDomainInfoEnricher.cs create mode 100644 framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/ActivityEvent_Tests.cs create mode 100644 framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/Cryptography_Tests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5d9885ddbd..9e0440965a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -184,5 +184,6 @@ + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/TelemetryApplicationMetricsEnricher.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/TelemetryApplicationMetricsEnricher.cs new file mode 100644 index 0000000000..fa95f5c298 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/TelemetryApplicationMetricsEnricher.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.DependencyInjection; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Internal.Telemetry.Activity; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Activity.Providers; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Reflection; + +namespace Volo.Abp.AspNetCore.Mvc; + +[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))] +public sealed class TelemetryApplicationMetricsEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher +{ + private readonly ITypeFinder _typeFinder; + public TelemetryApplicationMetricsEnricher(ITypeFinder typeFinder, IServiceProvider serviceProvider) : base(serviceProvider) + { + _typeFinder = typeFinder; + } + + protected override Task CanExecuteAsync(ActivityContext context) + { + return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime); + } + + protected override Task ExecuteAsync(ActivityContext context) + { + var appServiceCount = _typeFinder.Types.Count(t => + typeof(IApplicationService).IsAssignableFrom(t) && + t is { IsAbstract: false, IsInterface: false } && + !t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter)); + + var controllerCount = _typeFinder.Types.Count(t => + typeof(ControllerBase).IsAssignableFrom(t) && + !t.IsAbstract && + !t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter)); + + + context.Current[ActivityPropertyNames.AppServiceCount] = appServiceCount; + context.Current[ActivityPropertyNames.ControllerCount] = controllerCount; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Properties/AssemblyInfo.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f9e35084b9 --- /dev/null +++ b/framework/src/Volo.Abp.Authorization.Abstractions/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Volo.Abp.Authorization.Abstractions")] +[assembly: AssemblyTrademark("")] +[assembly: InternalsVisibleTo("Volo.Abp.Authorization")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("83632bec-2f60-4c2b-b964-30e8b37fa9d8")] diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs index ab49e4aad4..0f5d713e2b 100644 --- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs +++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs @@ -108,6 +108,8 @@ public class PermissionDefinition : Parent = this }; + child[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName] = this[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName]; + _children.Add(child); return child; diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs index 85d771a6ab..394cdb9d82 100644 --- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs +++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs @@ -11,6 +11,13 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext public Dictionary Groups { get; } + internal IPermissionDefinitionProvider? CurrentProvider { get; set; } + + public static class KnownPropertyNames + { + public const string CurrentProviderName = "_CurrentProviderName"; + } + public PermissionDefinitionContext(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; @@ -28,7 +35,16 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext throw new AbpException($"There is already an existing permission group with name: {name}"); } - return Groups[name] = new PermissionGroupDefinition(name, displayName); + var group = new PermissionGroupDefinition(name, displayName); + + if (CurrentProvider != null) + { + group[KnownPropertyNames.CurrentProviderName] = CurrentProvider.GetType().FullName; + } + + Groups[name] = group; + + return group; } [NotNull] diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs index bbc6e96cdf..0ad2dca099 100644 --- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs +++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs @@ -61,6 +61,8 @@ public class PermissionGroupDefinition : ICanAddChildPermission isEnabled ); + permission[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName] = this[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName]; + _permissions.Add(permission); return permission; diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs index 2d0a9d668c..4e6ff0d11c 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs @@ -84,18 +84,23 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore, foreach (var provider in providers) { + context.CurrentProvider = provider; provider.PreDefine(context); } foreach (var provider in providers) { + context.CurrentProvider = provider; provider.Define(context); } foreach (var provider in providers) { + context.CurrentProvider = provider; provider.PostDefine(context); } + + context.CurrentProvider = null; return context.Groups; } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/TelemetryPermissionInfoEnricher.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/TelemetryPermissionInfoEnricher.cs new file mode 100644 index 0000000000..4fab06ba8f --- /dev/null +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/TelemetryPermissionInfoEnricher.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Activity.Providers; +using Volo.Abp.Internal.Telemetry.Constants; + +namespace Volo.Abp.Authorization.Permissions; + +[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))] +public sealed class TelemetryPermissionInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher +{ + private readonly IPermissionDefinitionManager _permissionDefinitionManager; + + public TelemetryPermissionInfoEnricher(IPermissionDefinitionManager permissionDefinitionManager, + IServiceProvider serviceProvider) : base(serviceProvider) + { + _permissionDefinitionManager = permissionDefinitionManager; + } + + protected override Task CanExecuteAsync(ActivityContext context) + { + return Task.FromResult(context.ProjectId.HasValue); + } + + protected async override Task ExecuteAsync(ActivityContext context) + { + var permissions = await _permissionDefinitionManager.GetPermissionsAsync(); + + var userDefinedPermissionsCount = permissions.Count(IsUserDefinedPermission); + + context.Current[ActivityPropertyNames.PermissionCount] = userDefinedPermissionsCount; + } + + private static bool IsUserDefinedPermission(PermissionDefinition permission) + { + return permission.Properties.TryGetValue(PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName, out var providerName) && + providerName is string && + !providerName.ToString()!.StartsWith(TelemetryConsts.VoloNameSpaceFilter); + } + + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj index caa6b6be94..5afcca9fbe 100644 --- a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj +++ b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs index ec20467ac9..6811e2a9a1 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs @@ -4,13 +4,17 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using JetBrains.Annotations; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; using Volo.Abp.Internal; +using Volo.Abp.Internal.Telemetry; +using Volo.Abp.Internal.Telemetry.Constants; using Volo.Abp.Logging; using Volo.Abp.Modularity; +using Volo.Abp.Threading; namespace Volo.Abp; @@ -149,6 +153,56 @@ public abstract class AbpApplicationBase : IAbpApplication options.PlugInSources ); } + protected void SetupTelemetryTracking() + { + if (!ShouldSendTelemetryData()) + { + return; + } + + AsyncHelper.RunSync(InitializeTelemetryTracking); + } + + protected async Task SetupTelemetryTrackingAsync() + { + if (!ShouldSendTelemetryData()) + { + return; + } + + await InitializeTelemetryTracking(); + } + + private async Task InitializeTelemetryTracking() + { + try + { + using var scope = ServiceProvider.CreateScope(); + var telemetryService = scope.ServiceProvider.GetRequiredService(); + await telemetryService.AddActivityAsync(ActivityNameConsts.ApplicationRun); + } + catch (Exception ex) + { + try + { + using var scope = ServiceProvider.CreateScope(); + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogException(ex, LogLevel.Trace); + } + catch + { + /* ignored */ + } + } + } + + private bool ShouldSendTelemetryData() + { + using var scope = ServiceProvider.CreateScope(); + var abpHostEnvironment = scope.ServiceProvider.GetRequiredService(); + var configuration = scope.ServiceProvider.GetRequiredService(); + return abpHostEnvironment.IsDevelopment() && configuration.GetValue("Abp:Telemetry:IsEnabled") == true; + } //TODO: We can extract a new class for this public virtual async Task ConfigureServicesAsync() diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs index d80e46c5d7..faf4f7fa1a 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs @@ -44,6 +44,8 @@ internal class AbpApplicationWithExternalServiceProvider : AbpApplicationBase, I SetServiceProvider(serviceProvider); await InitializeModulesAsync(); + + await SetupTelemetryTrackingAsync(); } public void Initialize([NotNull] IServiceProvider serviceProvider) @@ -53,6 +55,8 @@ internal class AbpApplicationWithExternalServiceProvider : AbpApplicationBase, I SetServiceProvider(serviceProvider); InitializeModules(); + + SetupTelemetryTracking(); } public override void Dispose() diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs index c5e5eccc83..b652999a7a 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs @@ -50,12 +50,15 @@ internal class AbpApplicationWithInternalServiceProvider : AbpApplicationBase, I { CreateServiceProvider(); await InitializeModulesAsync(); + await SetupTelemetryTrackingAsync(); + } public void Initialize() { CreateServiceProvider(); InitializeModules(); + SetupTelemetryTracking(); } public override void Dispose() diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityContext.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityContext.cs new file mode 100644 index 0000000000..8a03a66e65 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityContext.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Constants.Enums; + +namespace Volo.Abp.Internal.Telemetry.Activity; + +public class ActivityContext +{ + public ActivityEvent Current { get; } + public Dictionary ExtraProperties { get; } = new(); + public bool IsTerminated { get; private set; } + + public Guid? ProjectId => Current.Get(ActivityPropertyNames.ProjectId); + + public Guid? SolutionId => Current.Get(ActivityPropertyNames.SolutionId); + + public SessionType? SessionType => Current.Get(ActivityPropertyNames.SessionType); + + public string? DeviceId => Current.Get(ActivityPropertyNames.DeviceId); + + public string? SolutionPath => ExtraProperties.TryGetValue(ActivityPropertyNames.SolutionPath, out var solutionPath) + ? solutionPath?.ToString() + : null; + + private ActivityContext(ActivityEvent current) + { + Current = current; + } + + public static ActivityContext Create(string activityName, string? details = null, + Action>? additionalProperties = null) + { + var activity = new ActivityEvent(activityName, details); + + if (additionalProperties is not null) + { + var additionalPropertiesDict = new Dictionary(); + activity[ActivityPropertyNames.AdditionalProperties] = additionalPropertiesDict; + additionalProperties.Invoke(additionalPropertiesDict); + } + + return new ActivityContext(activity); + } + + public void Terminate() + { + IsTerminated = true; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityEvent.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityEvent.cs new file mode 100644 index 0000000000..22ad4c0da3 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityEvent.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Volo.Abp.Internal.Telemetry.Constants; + +namespace Volo.Abp.Internal.Telemetry.Activity; + +public class ActivityEvent : Dictionary +{ + public ActivityEvent() + { + this[ActivityPropertyNames.Id] = Guid.NewGuid(); + this[ActivityPropertyNames.Time] = DateTimeOffset.UtcNow; + } + + public ActivityEvent(string activityName, string? details = null) : this() + { + Check.NotNullOrWhiteSpace(activityName, nameof(activityName)); + + this[ActivityPropertyNames.ActivityName] = activityName; + this[ActivityPropertyNames.ActivityDetails] = details; + } + + public bool HasSolutionInfo() + { + return this.ContainsKey(ActivityPropertyNames.HasSolutionInfo); + } + + public bool HasDeviceInfo() + { + return this.ContainsKey(ActivityPropertyNames.HasDeviceInfo); + } + + public bool HasProjectInfo() + { + return this.ContainsKey(ActivityPropertyNames.HasProjectInfo); + } + + public T Get(string key) + { + return TryConvert(key, out var value) ? value : default!; + } + + public bool TryGetValue(string key, out T value) + { + return TryConvert(key, out value); + } + + private bool TryConvert(string key, out T result) + { + result = default!; + if (!this.TryGetValue(key, out var value) || value is null) + { + return false; + } + + try + { + if (value is T tValue) + { + result = tValue; + return true; + } + + if (value is JsonElement jsonElement) + { + value = ExtractFromJsonElement(jsonElement); + if (value is null) + { + return false; + } + } + + var targetType = typeof(T); + var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; + if (underlyingType.IsEnum) + { + if (value is string str) + { + result = (T)Enum.Parse(underlyingType, str, ignoreCase: true); + } + else if (value is int intValue) + { + result = (T)Enum.ToObject(underlyingType, intValue); + } + + return true; + } + + + if (underlyingType == typeof(Dictionary[])) + { + result = (T)value; + return true; + } + + if (underlyingType == typeof(Guid)) + { + result = (T)(object)Guid.Parse(value.ToString()!); + return true; + } + + if (underlyingType == typeof(DateTimeOffset)) + { + result = (T)(object)DateTimeOffset.Parse(value.ToString()!); + return true; + } + + // Nullable types + result = (T)Convert.ChangeType(value, underlyingType); + return true; + } + catch + { + return false; + } + } + + private static object? ExtractFromJsonElement(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.GetInt32(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Array => element.EnumerateArray() + .Select(item => + { + if (item.ValueKind == JsonValueKind.Object) + { + return item.EnumerateObject() + .ToDictionary(prop => prop.Name, prop => ExtractFromJsonElement(prop.Value)); + } + + return new Dictionary { { "value", ExtractFromJsonElement(item) } }; + }) + .ToArray(), + + JsonValueKind.Object => element.EnumerateObject() + .ToDictionary(prop => prop.Name, prop => ExtractFromJsonElement(prop.Value)), + _ => element.ToString() + }; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/IHasParentTelemetryActivityEventEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/IHasParentTelemetryActivityEventEnricher.cs new file mode 100644 index 0000000000..5547445d54 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/IHasParentTelemetryActivityEventEnricher.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Internal.Telemetry.Activity.Providers; + +namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; + +public interface IHasParentTelemetryActivityEventEnricher where TParent: TelemetryActivityEventEnricher +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventBuilder.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventBuilder.cs new file mode 100644 index 0000000000..0542b9d676 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventBuilder.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; + +public interface ITelemetryActivityEventBuilder +{ + Task BuildAsync(ActivityContext context); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventEnricher.cs new file mode 100644 index 0000000000..b83741cd88 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventEnricher.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; + +public interface ITelemetryActivityEventEnricher +{ + int ExecutionOrder { get; } + + Task EnrichAsync(ActivityContext context); + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityStorage.cs new file mode 100644 index 0000000000..870876ffbd --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityStorage.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; + +public interface ITelemetryActivityStorage +{ + Guid InitializeOrGetSession(); + void DeleteActivities(ActivityEvent[] activities); + void SaveActivity(ActivityEvent activityEvent); + List GetActivities(); + bool ShouldAddDeviceInfo(); + bool ShouldAddSolutionInformation(Guid solutionId); + bool ShouldAddProjectInfo(Guid projectId); + bool ShouldSendActivities(); + void MarkActivitiesAsFailed(ActivityEvent[] activities); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventBuilder.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventBuilder.cs new file mode 100644 index 0000000000..201ac8c3b6 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventBuilder.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DynamicProxy; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; + +namespace Volo.Abp.Internal.Telemetry.Activity.Providers; + +public class TelemetryActivityEventBuilder : ITelemetryActivityEventBuilder, ISingletonDependency +{ + private readonly List _activityEnrichers; + + public TelemetryActivityEventBuilder(IEnumerable activityDataEnrichers) + { + _activityEnrichers = activityDataEnrichers + .Where(FilterEnricher) + .OrderByDescending(x => x.ExecutionOrder) + .ToList(); + } + public virtual async Task BuildAsync(ActivityContext context) + { + foreach (var enricher in _activityEnrichers) + { + try + { + await enricher.EnrichAsync(context); + } + catch + { + //ignored + } + + if (context.IsTerminated) + { + return null; + } + } + + return context.Current; + } + + private static bool FilterEnricher(ITelemetryActivityEventEnricher enricher) + { + return ProxyHelper.GetUnProxiedType(enricher).Assembly.FullName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter) && + enricher is not IHasParentTelemetryActivityEventEnricher; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventEnricher.cs new file mode 100644 index 0000000000..d25e9c58f8 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventEnricher.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DynamicProxy; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; + +namespace Volo.Abp.Internal.Telemetry.Activity.Providers; + +public abstract class TelemetryActivityEventEnricher : ITelemetryActivityEventEnricher, IScopedDependency +{ + public virtual int ExecutionOrder { get; set; } = 0; + protected bool IgnoreChildren { get; set; } + protected virtual Type? ReplaceParentType { get; set; } + + private readonly IServiceProvider _serviceProvider; + + protected TelemetryActivityEventEnricher(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task EnrichAsync(ActivityContext context) + { + if (!await CanExecuteAsync(context)) + { + return; + } + + await ExecuteAsync(context); + await ExecuteChildrenAsync(context); + } + + protected virtual Task CanExecuteAsync(ActivityContext context) + { + return Task.FromResult(true); + } + + protected abstract Task ExecuteAsync(ActivityContext context); + + protected virtual async Task ExecuteChildrenAsync(ActivityContext context) + { + if (IgnoreChildren) + { + return; + } + + using var scope = _serviceProvider.CreateScope(); + + foreach (var child in GetChildren(scope.ServiceProvider)) + { + await child.EnrichAsync(context); + } + } + + private ITelemetryActivityEventEnricher[] GetChildren(IServiceProvider serviceProvider) + { + var targetType = ReplaceParentType ?? ProxyHelper.GetUnProxiedType(this); + var genericInterfaceType = typeof(IHasParentTelemetryActivityEventEnricher<>).MakeGenericType(targetType); + var enumerableType = typeof(IEnumerable<>).MakeGenericType(genericInterfaceType); + + var childServices = (IEnumerable)serviceProvider.GetRequiredService(enumerableType); + + return childServices + .Cast() + .ToArray(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryApplicationInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryApplicationInfoEnricher.cs new file mode 100644 index 0000000000..6616c62bda --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryApplicationInfoEnricher.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.Helpers; + +namespace Volo.Abp.Internal.Telemetry.Activity.Providers; + +[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))] +public sealed class TelemetryApplicationInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher +{ + private readonly ITelemetryActivityStorage _telemetryActivityStorage; + + public TelemetryApplicationInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, IServiceProvider serviceProvider) : base(serviceProvider) + { + _telemetryActivityStorage = telemetryActivityStorage; + } + + protected override Task CanExecuteAsync(ActivityContext context) + { + return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime); + } + + protected override Task ExecuteAsync(ActivityContext context) + { + try + { + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly is null) + { + context.Terminate(); + return Task.CompletedTask; + } + + var projectMetaData = AbpProjectMetadataReader.ReadProjectMetadata(entryAssembly); + if (projectMetaData?.ProjectId == null || projectMetaData.AbpSlnPath.IsNullOrEmpty()) + { + context.Terminate(); + return Task.CompletedTask; + } + + if (!_telemetryActivityStorage.ShouldAddProjectInfo(projectMetaData.ProjectId.Value)) + { + IgnoreChildren = true; + return Task.CompletedTask; + } + + var solutionId = ReadSolutionIdFromSolutionPath(projectMetaData.AbpSlnPath); + + if (!solutionId.HasValue) + { + IgnoreChildren = true; + context.Terminate(); + return Task.CompletedTask; + } + + context.ExtraProperties[ActivityPropertyNames.SolutionPath] = projectMetaData.AbpSlnPath; + context.Current[ActivityPropertyNames.ProjectType] = projectMetaData.Role ?? string.Empty; + context.Current[ActivityPropertyNames.ProjectId] = projectMetaData.ProjectId.Value; + context.Current[ActivityPropertyNames.SolutionId] = solutionId; + context.Current[ActivityPropertyNames.HasProjectInfo] = true; + } + catch + { + //ignored + } + + return Task.CompletedTask; + } + + + private static Guid? ReadSolutionIdFromSolutionPath(string solutionPath) + { + try + { + if (solutionPath.IsNullOrEmpty()) + { + return null; + } + + using var fs = new FileStream(solutionPath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var doc = JsonDocument.Parse(fs, new JsonDocumentOptions + { + AllowTrailingCommas = true + }); + if (doc.RootElement.TryGetProperty("id", out var property) && property.TryGetGuid(out var solutionId)) + { + return solutionId; + } + } + catch + { + // ignored + } + + return null; + } + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryDeviceInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryDeviceInfoEnricher.cs new file mode 100644 index 0000000000..5081519b40 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryDeviceInfoEnricher.cs @@ -0,0 +1,79 @@ +using System; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Internal.Telemetry.Activity.Providers; + +[ExposeServices(typeof(ITelemetryActivityEventEnricher))] +internal sealed class TelemetryDeviceInfoEnricher : TelemetryActivityEventEnricher +{ + private readonly ITelemetryActivityStorage _telemetryActivityStorage; + private readonly ISoftwareInfoProvider _softwareInfoProvider; + + public TelemetryDeviceInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, + ISoftwareInfoProvider softwareInfoProvider, IServiceProvider serviceProvider) : base(serviceProvider) + { + _telemetryActivityStorage = telemetryActivityStorage; + _softwareInfoProvider = softwareInfoProvider; + } + protected async override Task ExecuteAsync(ActivityContext context) + { + try + { + var deviceId = DeviceManager.GetUniquePhysicalKey(true); + context.Current[ActivityPropertyNames.DeviceId] = deviceId; + + if (!_telemetryActivityStorage.ShouldAddDeviceInfo()) + { + return; + } + + var softwareList = await _softwareInfoProvider.GetSoftwareInfoAsync(); + + context.Current[ActivityPropertyNames.InstalledSoftwares] = softwareList; + context.Current[ActivityPropertyNames.DeviceLanguage] = CultureInfo.CurrentUICulture.Name; + context.Current[ActivityPropertyNames.OperatingSystem] = GetOperatingSystem(); + context.Current[ActivityPropertyNames.CountryIsoCode] = GetCountry(); + context.Current[ActivityPropertyNames.HasDeviceInfo] = true; + context.Current[ActivityPropertyNames.OperatingSystemArchitecture] = RuntimeInformation.OSArchitecture.ToString(); + } + catch + { + //ignored + } + } + + private static OperationSystem GetOperatingSystem() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OperationSystem.Windows; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OperationSystem.Linux; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return OperationSystem.MacOS; + } + + return OperationSystem.Unknown; + } + + + + private static string GetCountry() + { + var region = new RegionInfo(CultureInfo.InstalledUICulture.Name); + return region.TwoLetterISORegionName; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryModuleInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryModuleInfoEnricher.cs new file mode 100644 index 0000000000..d35973b79d --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryModuleInfoEnricher.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Modularity; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Internal.Telemetry.Activity.Providers; + +[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))] +internal sealed class TelemetryModuleInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher +{ + private readonly IModuleContainer _moduleContainer; + private readonly IAssemblyFinder _assemblyFinder; + + public TelemetryModuleInfoEnricher(IModuleContainer moduleContainer, IAssemblyFinder assemblyFinder, + IServiceProvider serviceProvider) : base(serviceProvider) + { + _moduleContainer = moduleContainer; + _assemblyFinder = assemblyFinder; + } + + protected override Task CanExecuteAsync(ActivityContext context) + { + return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime); + } + + protected override Task ExecuteAsync(ActivityContext context) + { + context.Current[ActivityPropertyNames.ModuleCount] = _moduleContainer.Modules.Count; + context.Current[ActivityPropertyNames.ProjectCount] = _assemblyFinder.Assemblies.Count(x => + !x.FullName.IsNullOrEmpty() && + !x.FullName.StartsWith(TelemetryConsts.VoloNameSpaceFilter)); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySessionInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySessionInfoEnricher.cs new file mode 100644 index 0000000000..a015d40733 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySessionInfoEnricher.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Constants.Enums; + +namespace Volo.Abp.Internal.Telemetry.Activity.Providers; +[ExposeServices(typeof(ITelemetryActivityEventEnricher))] +public class TelemetrySessionInfoEnricher : TelemetryActivityEventEnricher +{ + public override int ExecutionOrder { get; set; } = 10; + + public TelemetrySessionInfoEnricher(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override Task ExecuteAsync(ActivityContext context) + { + context.Current[ActivityPropertyNames.SessionType] = SessionType.ApplicationRuntime; + context.Current[ActivityPropertyNames.SessionId] = Guid.NewGuid().ToString(); + context.Current[ActivityPropertyNames.IsFirstSession] = !File.Exists(TelemetryPaths.ActivityStorage); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs new file mode 100644 index 0000000000..785ca0dfa1 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; + +namespace Volo.Abp.Internal.Telemetry.Activity.Providers; + +[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))] +internal sealed class TelemetrySolutionInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher +{ + private readonly ITelemetryActivityStorage _telemetryActivityStorage; + + public TelemetrySolutionInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, IServiceProvider serviceProvider) : base(serviceProvider) + { + _telemetryActivityStorage = telemetryActivityStorage; + } + + protected override Task CanExecuteAsync(ActivityContext context) + { + if (context.SolutionId.HasValue && !context.SolutionPath.IsNullOrEmpty()) + { + return Task.FromResult(_telemetryActivityStorage.ShouldAddSolutionInformation(context.SolutionId.Value)); + } + + return Task.FromResult(false); + } + + protected override Task ExecuteAsync(ActivityContext context) + { + try + { + var jsonContent = File.ReadAllText(context.SolutionPath!); + using var doc = JsonDocument.Parse(jsonContent, new JsonDocumentOptions + { + AllowTrailingCommas = true + }); + + var root = doc.RootElement; + + if (root.TryGetProperty("creatingStudioConfiguration", out var creatingStudioConfiguration)) + { + AddSolutionCreationConfiguration(context, creatingStudioConfiguration); + } + + if (root.TryGetProperty("modules", out var modulesElement)) + { + AddModuleInfo(context, modulesElement); + } + + context.Current[ActivityPropertyNames.HasSolutionInfo] = true; + } + catch + { + //ignored + } + + return Task.CompletedTask; + } + + private static void AddSolutionCreationConfiguration(ActivityContext context, JsonElement config) + { + context.Current[ActivityPropertyNames.Template] = TelemetryJsonExtensions.GetStringOrNull(config, "template"); + context.Current[ActivityPropertyNames.CreatedAbpStudioVersion] = TelemetryJsonExtensions.GetStringOrNull(config, "createdAbpStudioVersion"); + context.Current[ActivityPropertyNames.MultiTenancy] = TelemetryJsonExtensions.GetBooleanOrNull(config, "multiTenancy"); + context.Current[ActivityPropertyNames.UiFramework] = TelemetryJsonExtensions.GetStringOrNull(config, "uiFramework"); + context.Current[ActivityPropertyNames.DatabaseProvider] = TelemetryJsonExtensions.GetStringOrNull(config, "databaseProvider"); + context.Current[ActivityPropertyNames.Theme] = TelemetryJsonExtensions.GetStringOrNull(config, "theme"); + context.Current[ActivityPropertyNames.ThemeStyle] = TelemetryJsonExtensions.GetStringOrNull(config, "themeStyle"); + context.Current[ActivityPropertyNames.HasPublicWebsite] = TelemetryJsonExtensions.GetBooleanOrNull(config, "publicWebsite"); + context.Current[ActivityPropertyNames.IsTiered] = TelemetryJsonExtensions.GetBooleanOrNull(config, "tiered"); + context.Current[ActivityPropertyNames.SocialLogins] = TelemetryJsonExtensions.GetBooleanOrNull(config, "socialLogin"); + context.Current[ActivityPropertyNames.DatabaseManagementSystem] = TelemetryJsonExtensions.GetStringOrNull(config, "databaseManagementSystem"); + context.Current[ActivityPropertyNames.IsSeparateTenantSchema] = TelemetryJsonExtensions.GetBooleanOrNull(config, "separateTenantSchema"); + context.Current[ActivityPropertyNames.MobileFramework] = TelemetryJsonExtensions.GetStringOrNull(config, "mobileFramework"); + context.Current[ActivityPropertyNames.IncludeTests] = TelemetryJsonExtensions.GetBooleanOrNull(config, "includeTests"); + context.Current[ActivityPropertyNames.DynamicLocalization] = TelemetryJsonExtensions.GetBooleanOrNull(config, "dynamicLocalization"); + context.Current[ActivityPropertyNames.KubernetesConfiguration] = TelemetryJsonExtensions.GetBooleanOrNull(config, "kubernetesConfiguration"); + context.Current[ActivityPropertyNames.GrafanaDashboard] = TelemetryJsonExtensions.GetBooleanOrNull(config, "grafanaDashboard"); + } + + private static void AddModuleInfo(ActivityContext context, JsonElement modulesElement) + { + var modules = new List>(); + + foreach (var module in modulesElement.EnumerateObject()) + { + var modulePath = GetModuleFilePath(context.SolutionPath!, module); + if (modulePath.IsNullOrEmpty()) + { + continue; + } + + var moduleJsonFileContent = File.ReadAllText(modulePath); + using var moduleDoc = JsonDocument.Parse(moduleJsonFileContent); + + if (!moduleDoc.RootElement.TryGetProperty("imports", out var imports)) + { + continue; + } + + foreach (var import in imports.EnumerateObject()) + { + modules.Add(new Dictionary + { + { ActivityPropertyNames.ModuleName, import.Name }, + { ActivityPropertyNames.ModuleVersion, TelemetryJsonExtensions.GetStringOrNull(import.Value, "version") }, + { ActivityPropertyNames.ModuleInstallationTime, TelemetryJsonExtensions.GetDateTimeOffsetOrNull(import.Value, "creationTime") } + }); + } + } + + context.Current[ActivityPropertyNames.InstalledModules] = modules; + } + + private static string? GetModuleFilePath(string solutionPath, JsonProperty module) + { + var path = TelemetryJsonExtensions.GetStringOrNull(module.Value, "path"); + if (path.IsNullOrEmpty()) + { + return null; + } + + var fullPath = Path.Combine(Path.GetDirectoryName(solutionPath)!, path); + return File.Exists(fullPath) ? fullPath : null; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/FailedActivityInfo.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/FailedActivityInfo.cs new file mode 100644 index 0000000000..d12565d5d2 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/FailedActivityInfo.cs @@ -0,0 +1,18 @@ +using System; + +namespace Volo.Abp.Internal.Telemetry.Activity.Storage; + +internal class FailedActivityInfo +{ + public DateTimeOffset FirstFailTime { get; set; } + public DateTimeOffset LastFailTime { get; set; } + public int RetryCount { get; set; } + + public bool IsExpired() + { + var now = DateTimeOffset.UtcNow; + + return RetryCount >= TelemetryPeriod.MaxActivityRetryCount || + now - FirstFailTime > TelemetryPeriod.MaxFailedActivityAge; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs new file mode 100644 index 0000000000..2ccc0e552d --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Helpers; + +namespace Volo.Abp.Internal.Telemetry.Activity.Storage; + +public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDependency +{ + private TelemetryActivityStorageState State { get; } + private readonly static JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + public TelemetryActivityStorage() + { + CreateDirectoryIfNotExist(); + + State = LoadState(); + } + + public void SaveActivity(ActivityEvent activityEvent) + { + State.Activities.Add(activityEvent); + + var activityName = activityEvent.Get(ActivityPropertyNames.ActivityName); + + if (activityName == ActivityNameConsts.AbpStudioClose) + { + State.SessionId = null; + } + + if (activityEvent.HasDeviceInfo()) + { + State.LastDeviceInfoAddTime = DateTimeOffset.UtcNow; + } + + if (activityEvent.HasSolutionInfo()) + { + var solutionId = activityEvent.Get(ActivityPropertyNames.SolutionId); + State.Solutions[solutionId] = DateTimeOffset.UtcNow; + } + + if (activityEvent.HasProjectInfo()) + { + var projectId = activityEvent.Get(ActivityPropertyNames.ProjectId); + State.Projects[projectId] = DateTimeOffset.UtcNow; + } + + SaveState(); + } + + public List GetActivities() + { + return State.Activities; + } + + public Guid InitializeOrGetSession() + { + if (State.SessionId.HasValue) + { + return State.SessionId.Value; + } + + State.SessionId = Guid.NewGuid(); + SaveState(); + + return State.SessionId.Value; + } + + public void DeleteActivities(ActivityEvent[] activities) + { + var activityIds = new HashSet(activities.Select(x => x.Get(ActivityPropertyNames.Id))); + + State.Activities.RemoveAll(x => activityIds.Contains(x.Get(ActivityPropertyNames.Id))); + + SaveState(); + } + + public void MarkActivitiesAsFailed(ActivityEvent[] activities) + { + var now = DateTimeOffset.UtcNow; + + foreach (var activity in activities) + { + var activityId = activity.Get(ActivityPropertyNames.Id); + + if (State.FailedActivities.TryGetValue(activityId, out var failedActivityInfo)) + { + failedActivityInfo.RetryCount++; + failedActivityInfo.LastFailTime = now; + + if (!failedActivityInfo.IsExpired()) + { + continue; + } + + State.Activities.RemoveAll(x=> x.Get(ActivityPropertyNames.Id) == activityId); + State.FailedActivities.Remove(activityId); + } + else + { + State.FailedActivities[activityId] = new FailedActivityInfo + { + FirstFailTime = now, + LastFailTime = now, + RetryCount = 1 + }; + } + } + + SaveState(); + } + + public bool ShouldAddDeviceInfo() + { + return State.LastDeviceInfoAddTime is null || + DateTimeOffset.UtcNow - State.LastDeviceInfoAddTime > TelemetryPeriod.InformationSendPeriod; + } + + public bool ShouldAddSolutionInformation(Guid solutionId) + { + return !State.Solutions.TryGetValue(solutionId, out var lastSend) || + DateTimeOffset.UtcNow - lastSend > TelemetryPeriod.InformationSendPeriod; + } + + public bool ShouldAddProjectInfo(Guid projectId) + { + return !State.Projects.TryGetValue(projectId, out var lastSend) || + DateTimeOffset.UtcNow - lastSend > TelemetryPeriod.InformationSendPeriod; + } + + public bool ShouldSendActivities() + { + return State.ActivitySendTime is null || + DateTimeOffset.UtcNow - State.ActivitySendTime > TelemetryPeriod.ActivitySendPeriod; + } + + private void SaveState() + { + try + { + var json = JsonSerializer.Serialize(State, JsonSerializerOptions); + var encryptedJson = Cryptography.Encrypt(json); + File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8); + } + catch + { + // Ignored + } + } + + private static TelemetryActivityStorageState LoadState() + { + try + { + if (!File.Exists(TelemetryPaths.ActivityStorage)) + { + return new TelemetryActivityStorageState(); + } + + var fileContent = MutexExecutor.ReadFileSafely(TelemetryPaths.ActivityStorage); + + if (fileContent.IsNullOrEmpty()) + { + return new TelemetryActivityStorageState(); + } + + var json = Cryptography.Decrypt(fileContent); + + return JsonSerializer.Deserialize(json, JsonSerializerOptions)!; + } + catch + { + return new TelemetryActivityStorageState(); + } + } + + private static void CreateDirectoryIfNotExist() + { + try + { + var storageDirectory = Path.GetDirectoryName(TelemetryPaths.ActivityStorage)!; + + if (!Directory.Exists(storageDirectory)) + { + Directory.CreateDirectory(storageDirectory); + } + } + catch + { + // Ignored + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorageState.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorageState.cs new file mode 100644 index 0000000000..f0d1dfd4a5 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorageState.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Internal.Telemetry.Activity.Storage; + +internal class TelemetryActivityStorageState +{ + public DateTimeOffset? ActivitySendTime { get; set; } + public DateTimeOffset? LastDeviceInfoAddTime { get; set; } + public Guid? SessionId { get; set; } + public List Activities { get; set; } = new(); + public Dictionary Solutions { get; set; } = new(); + public Dictionary Projects { get; set; } = new(); + public Dictionary FailedActivities { get; set; } = new(); + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryPeriod.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryPeriod.cs new file mode 100644 index 0000000000..131180a3a4 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryPeriod.cs @@ -0,0 +1,34 @@ +using System; + +namespace Volo.Abp.Internal.Telemetry.Activity.Storage; + +static internal class TelemetryPeriod +{ + private const string TestModeEnvironmentVariable = "ABP_TELEMETRY_TEST_MODE"; + + static TelemetryPeriod() + { + var isTestMode = IsTestModeEnabled(); + + InformationSendPeriod = isTestMode + ? TimeSpan.FromSeconds(15) + : TimeSpan.FromDays(7); + + ActivitySendPeriod = isTestMode + ? TimeSpan.FromSeconds(5) + : TimeSpan.FromDays(1); + } + + public static TimeSpan ActivitySendPeriod { get; } + public static TimeSpan InformationSendPeriod { get; } + + public static int MaxActivityRetryCount { get; set; } = 3; + public static TimeSpan MaxFailedActivityAge { get; set; } = TimeSpan.FromDays(30); + + private static bool IsTestModeEnabled() + { + var testModeVariable = + Environment.GetEnvironmentVariable(TestModeEnvironmentVariable, EnvironmentVariableTarget.User); + return bool.TryParse(testModeVariable, out var isTestMode) && isTestMode; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/TelemetryJsonExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/TelemetryJsonExtensions.cs new file mode 100644 index 0000000000..6f8b5c2a10 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/TelemetryJsonExtensions.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json; + +namespace Volo.Abp.Internal.Telemetry.Activity; + +static internal class TelemetryJsonExtensions +{ + static internal string? GetStringOrNull(JsonElement element, string propertyName) + { + return element.TryGetProperty(propertyName, out var property) + ? property.GetString() ?? null + : null; + } + + static internal bool? GetBooleanOrNull(JsonElement element, string propertyName) + { + if (element.TryGetProperty(propertyName, out var property) && bool.TryParse(property.GetString(), out var boolValue)) + { + return boolValue; + } + + return null; + } + + static internal DateTimeOffset? GetDateTimeOffsetOrNull(JsonElement element, string propertyName) + { + if (element.TryGetProperty(propertyName, out var date) && DateTimeOffset.TryParse(date.GetString(), out var dateTimeValue)) + { + return dateTimeValue; + } + + return null; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/AbpPlatformUrls.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/AbpPlatformUrls.cs new file mode 100644 index 0000000000..61f2907e8f --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/AbpPlatformUrls.cs @@ -0,0 +1,12 @@ +namespace Volo.Abp.Internal.Telemetry.Constants; + +public static class AbpPlatformUrls +{ +#if DEBUG + public const string AbpTelemetryApiUrl = "https://localhost:44393/"; +#else + public const string AbpTelemetryApiUrl = "https://telemetry.abp.io/"; +#endif + + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs new file mode 100644 index 0000000000..c1cc986da3 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs @@ -0,0 +1,77 @@ +namespace Volo.Abp.Internal.Telemetry.Constants; + +public static class ActivityNameConsts +{ + public const string AbpStudioOpen = "AbpStudio.Open"; + public const string AbpStudioOpenFirstTimeForDevice = "AbpStudio.Open.FirstTimeForDevice"; + public const string AbpStudioOpenFirstTimeForUser = "AbpStudio.Open.FirstTimeForUser"; + public const string AbpStudioClose = "AbpStudio.Close"; + public const string AbpStudioCloseWithoutLogin = "AbpStudio.Close.WithoutLogin"; + public const string AbpStudioLogin = "AbpStudio.Login"; + public const string AbpStudioCampaignClick = "AbpStudio.Campaing.Click"; + public const string AbpStudioCommunityPostClick = "AbpStudio.CommunityPost.Click"; + public const string AbpStudioSolutionInitExisting = "AbpStudio.Solution.InitExisting"; + public const string AbpStudioSolutionNew = "AbpStudio.Solution.New"; + public const string AbpStudioSolutionNewMicroservice = "AbpStudio.Solution.New.Microservice"; + public const string AbpStudioSolutionAddModule = "AbpStudio.Solution.Add.Module"; + public const string AbpStudioSolutionAddModuleEmpty = "AbpStudio.Solution.Add.Module.Empty"; + public const string AbpStudioSolutionAddModuleDdd = "AbpStudio.Solution.Add.Module.Ddd"; + public const string AbpStudioSolutionAddModuleStandard = "AbpStudio.Solution.Add.Module.Standard"; + public const string AbpStudioSolutionAddMicroservice = "AbpStudio.Solution.Add.Microservice"; + public const string AbpStudioSolutionAddGateway = "AbpStudio.Solution.Add.Gateway"; + public const string AbpStudioSolutionAddWeb = "AbpStudio.Solution.Add.Web"; + public const string AbpStudioSolutionAddPackage = "AbpStudio.Solution.Add.Package"; + public const string AbpStudioSolutionAddPackageHttpApiLayer = "AbpStudio.Solution.Add.Package.HttpApiLayer"; + public const string AbpStudioAbpCliInstallLibs = "AbpStudio.AbpCli.InstallLibs"; + public const string AbpStudioAbpCliUpgradeAbp = "AbpStudio.AbpCli.UpgradeAbp"; + public const string AbpStudioAbpCliSwitchToStable = "AbpStudio.AbpCli.SwitchToStable"; + public const string AbpStudioAbpCliSwitchToPreview = "AbpStudio.AbpCli.SwitchToPreview"; + public const string AbpStudioAbpCliSwitchToNightly = "AbpStudio.AbpCli.SwitchToNightly"; + public const string AbpStudioAbpCliClean = "AbpStudio.AbpCli.Clean"; + public const string AbpStudioDotnetCliBuild = "AbpStudio.DotnetCli.Build"; + public const string AbpStudioDotnetCliGraphBuild = "AbpStudio.DotnetCli.GraphBuild"; + public const string AbpStudioDotnetCliClean = "AbpStudio.DotnetCli.Clean"; + public const string AbpStudioDotnetCliRestore = "AbpStudio.DotnetCli.Restore"; + public const string AbpStudioSolutionRunnerRunApp = "AbpStudio.SolutionRunner.RunApp"; + public const string AbpStudioSolutionRunnerAddCsharpApp = "AbpStudio.SolutionRunner.Add.CsharpApp"; + public const string AbpStudioSolutionRunnerAddCliApp = "AbpStudio.SolutionRunner.Add.CliApp"; + public const string AbpStudioSolutionRunnerAddProfile = "AbpStudio.SolutionRunner.Add.Profile"; + public const string AbpStudioSolutionRunnerAppManageMetadata = "AbpStudio.SolutionRunner.App.Manage.Metadata"; + public const string AbpStudioSolutionRunnerAppManageSecrets = "AbpStudio.SolutionRunner.App.Manage.Secrets"; + public const string AbpStudioSolutionOpen = "AbpStudio.Solution.Open"; + public const string AbpStudioMonitoringBrowse = "AbpStudio.Monitoring.Browse"; + public const string AbpStudioMonitoringHttpRequests = "AbpStudio.Monitoring.HttpRequests"; + public const string AbpStudioMonitoringHttpRequestsDetail = "AbpStudio.Monitoring.HttpRequests.Detail"; + public const string AbpStudioMonitoringEvents = "AbpStudio.Monitoring.Events"; + public const string AbpStudioMonitoringEventsDetail = "AbpStudio.Monitoring.Events.Detail"; + public const string AbpStudioMonitoringExceptions = "AbpStudio.Monitoring.Exceptions"; + public const string AbpStudioMonitoringExceptionsDetail = "AbpStudio.Monitoring.Exceptions.Detail"; + public const string AbpStudioMonitoringLogs = "AbpStudio.Monitoring.Logs"; + public const string AbpStudioMonitoringLogsFiltered = "AbpStudio.Monitoring.Logs.Filtered"; + public const string AbpStudioKubernetesAddProfile = "AbpStudio.Kubernetes.Add.Profile"; + public const string AbpStudioKubernetesConnect = "AbpStudio.Kubernetes.Connect"; + public const string AbpStudioKubernetesIntercept = "AbpStudio.Kubernetes.Intercept"; + public const string AbpStudioKubernetesHelmCommandsInstall = "AbpStudio.Kubernetes.Helm.Commands.Install"; + public const string AbpStudioKubernetesHelmCommandsBuildImages = "AbpStudio.Kubernetes.Helm.Commands.BuildImages"; + public const string AbpStudioKubernetesHelmChartsRefreshSubCharts = "AbpStudio.Kubernetes.Helm.Charts.RefreshSubCharts"; + public const string AbpStudioKubernetesHelmChartsManageMetadata = "AbpStudio.Kubernetes.Helm.Charts.Manage.Metadata"; + public const string AbpStudioLogsShow = "AbpStudio.Logs.Show"; + public const string AbpStudioSuiteOpen = "AbpStudio.Suite.Open"; + public const string AbpStudioGlobalSecretsManage = "AbpStudio.GlobalSecrets.Manage"; + public const string AbpStudioGlobalMetadataManage = "AbpStudio.GlobalMetadata.Manage"; + public const string AbpCliCommandsNewSolution = "AbpCli.Comands.NewSolution"; + public const string AbpCliCommandsNewModule = "AbpCli.Comands.NewModule"; + public const string AbpCliCommandsNewPackage = "AbpCli.Comands.NewPackage"; + public const string AbpCliCommandsUpdate = "AbpCli.Comands.Update"; + public const string AbpCliCommandsClean = "AbpCli.Comands.Clean"; + public const string AbpCliCommandsAddPackage = "AbpCli.Comands.AddPackage"; + public const string AbpCliCommandsAddPackageRef = "AbpCli.Comands.AddPackageRef"; + public const string AbpCliCommandsInstallModule = "AbpCli.Comands.InstallModule"; + public const string AbpCliCommandsInstallLocalModule = "AbpCli.Comands.InstallLocalModule"; + public const string AbpCliCommandsListModules = "AbpCli.Comands.ListModules"; + public const string AbpCliRun = "AbpCli.Run"; + public const string AbpCliExit = "AbpCli.Exit"; + public const string ApplicationRun = "Application.Run"; + public const string AbpStudioBrowserOpen = "AbpStudio.Browser.Open"; + public const string Error = "Error"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs new file mode 100644 index 0000000000..77b1bbdef6 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs @@ -0,0 +1,77 @@ +namespace Volo.Abp.Internal.Telemetry.Constants; + +public static class ActivityPropertyNames +{ + public const string SessionId = "SessionId"; + public const string ActivityName = "ActivityName"; + public const string Error = "Error"; + public const string ErrorDetail = "ErrorDetail"; + public const string Id = "Id"; + public const string UserId = "UserId"; + public const string OrganizationId = "OrganizationId"; + public const string IpAddress = "IpAddress"; + public const string IsFirstSession = "IsFirstSession"; + public const string DeviceId = "DeviceId"; + public const string DeviceLanguage = "DeviceLanguage"; + public const string OperatingSystem = "OperatingSystem"; + public const string CountryIsoCode = "CountryIsoCode"; + public const string InstalledSoftwares = "InstalledSoftwares"; + public const string ControllerCount = "ControllerCount"; + public const string EntityCount = "EntityCount"; + public const string ProjectCount = "ProjectCount"; + public const string ModuleCount = "ModuleCount"; + public const string PermissionCount = "PermissionCount"; + public const string AppServiceCount = "AppServiceCount"; + public const string ProjectType = "ProjectType"; + public const string ProjectId = "ProjectId"; + public const string SolutionId = "SolutionId"; + public const string Template = "Template"; + public const string CreatedAbpStudioVersion = "CreatedAbpStudioVersion"; + public const string IsTiered = "IsTiered"; + public const string UiFramework = "UiFramework"; + public const string DatabaseProvider = "DatabaseProvider"; + public const string DatabaseManagementSystem = "DatabaseManagementSystem"; + public const string IsSeparateTenantSchema = "IsSeparateTenantSchema"; + public const string Theme = "Theme"; + public const string ThemeStyle = "ThemeStyle"; + public const string MobileFramework = "MobileFramework"; + public const string HasPublicWebsite = "HasPublicWebsite"; + public const string IncludeTests = "IncludeTests"; + public const string MultiTenancy = "MultiTenancy"; + public const string DynamicLocalization = "DynamicLocalization"; + public const string KubernetesConfiguration = "KubernetesConfiguration"; + public const string GrafanaDashboard = "GrafanaDashboard"; + public const string SocialLogins = "SocialLogins"; + public const string InstalledModules = "InstalledModules"; + public const string SolutionPath = "SolutionPath"; + public const string LicenseType = "LicenseType"; + public const string SessionType = "SessionType"; + public const string HasError = "HasError"; + public const string ActivityDuration = "ActivityDuration"; + public const string ActivityDetails = "ActivityDetails"; + public const string Time = "Time"; + public const string SoftwareName = "Name"; + public const string SoftwareVersion = "Version"; + public const string SoftwareUiTheme = "UiTheme"; + public const string SoftwareType = "SoftwareType"; + public const string WebFramework = "WebFramework"; + public const string Dbms = "Dbms"; + public const string UiTheme = "UiTheme"; + public const string UiThemeStyle = "UiThemeStyle"; + public const string MobileApp = "MobileApp"; + public const string SampleCrudPage = "SampleCrudPage"; + public const string FirstAbpVersion = "FirstAbpVersion"; + public const string FirstDotnetVersion = "FirstDotnetVersion"; + public const string CreationTool = "CreationTool"; + public const string ModuleName = "ModuleName"; + public const string ModuleVersion = "ModuleVersion"; + public const string ModuleInstallationTime = "ModuleInstallationTime"; + public const string ExtraProperties = "ExtraProperties"; + public const string HasSolutionInfo = "HasSolutionInfo"; + public const string HasDeviceInfo = "HasDeviceInfo"; + public const string HasProjectInfo = "HasProjectInfo"; + public const string ErrorMessage = "ErrorMessage"; + public const string FailingActivity = "FailingActivity"; + public const string OperatingSystemArchitecture = "OperatingSystemArchitecture"; + public const string AdditionalProperties = "AdditionalProperties"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs new file mode 100644 index 0000000000..adf110ba64 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs @@ -0,0 +1,286 @@ +namespace Volo.Abp.Internal.Telemetry.Constants; + +static internal class DeviceManager +{ + public static string GetUniquePhysicalKey(bool shouldHash) + { + char platformId = '?'; + char osArchitecture = '?'; + string operatingSystem = "?"; + + try + { + string osPrefix; + string uniqueKey; + + platformId = GetPlatformIdOrDefault(); + osArchitecture = GetOsArchitectureOrDefault(); + + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform + .Windows)) + { + operatingSystem = "Windows"; + uniqueKey = GetUniqueKeyForWindows(); + osPrefix = "W"; + } + else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices + .OSPlatform.Linux)) + { + operatingSystem = "Linux"; + uniqueKey = GetHarddiskSerialForLinux(); + osPrefix = "L"; + } + else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices + .OSPlatform.OSX)) //MAC + { + operatingSystem = "OSX"; + uniqueKey = GetHarddiskSerialForOsX(); + osPrefix = "O"; + } + else + { + operatingSystem = "Other"; + uniqueKey = GetNetworkAdapterSerial(); + osPrefix = "X"; + } + + if (shouldHash) + { + uniqueKey = ConvertToMd5(uniqueKey).ToUpperInvariant(); + } + + return osPrefix + platformId + osArchitecture + "-" + uniqueKey; + } + catch (System.Exception ex) + { + System.Console.WriteLine("WARNING ABP-LIC-0025! Contact to license@abp.io with the below information" + + System.Environment.NewLine + + "* Architecture: " + + System.Runtime.InteropServices.RuntimeInformation.OSArchitecture + + System.Environment.NewLine + + "* Description: " + + System.Runtime.InteropServices.RuntimeInformation.OSDescription + + System.Environment.NewLine + + "* Platform Id: " + platformId + System.Environment.NewLine + + "* OS architecture: " + osArchitecture + System.Environment.NewLine + + "* Operating system: " + operatingSystem + System.Environment.NewLine + + "* Framework description: " + + System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription + + System.Environment.NewLine + + "* Error: " + ex.ToString()); + + return "95929008-b147-454a-8737-efed71fa2241"; + } + } + private static string GetNetworkAdapterSerial() + { + string macAddress = string.Empty; + + var networkInterfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); + foreach (var networkInterface in networkInterfaces) + { + if (networkInterface.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback) + { + continue; + } + + var physicalAddress = networkInterface.GetPhysicalAddress().ToString(); + if (string.IsNullOrEmpty(physicalAddress)) + { + continue; + } + + macAddress = physicalAddress; + break; + } + + return macAddress!; + } + /// + /// 0 - Win32S + /// 1 - Win32Windows + /// 2 - Win32NT + /// 3 - WinCE + /// 4 - Unix + /// 5 - Xbox + /// 6 - MacOSX + /// + private static char GetPlatformIdOrDefault(char defaultValue = '*') + { + try + { + return ((int)System.Environment.OSVersion.Platform).ToString()[0]; + } + catch + { + return defaultValue; + } + } + private static string ConvertToMd5(string text) + { + using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider()) + { + return EncodeBase64(md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text))); + } + } + + private static string EncodeBase64(byte[] ba) + { + var hex = new System.Text.StringBuilder(ba.Length * 2); + + foreach (var b in ba) + { + hex.AppendFormat("{0:x2}", b); + } + + return hex.ToString(); + } + + /// + /// 0 - X86 (An Intel-based 32-bit processor architecture) + /// 1 - X64 (A 64-bit ARM processor architecture) + /// 2 - Arm (A 32-bit ARM processor architecture) + /// 3 - Arm64 (A 64-bit ARM processor architecture) + /// + private static char GetOsArchitectureOrDefault(char defaultValue = '*') + { + try + { + return ((int)System.Runtime.InteropServices.RuntimeInformation.OSArchitecture).ToString()[0]; + } + catch + { + return defaultValue; + } + } + + private static string GetUniqueKeyForWindows() + { + try + { + return GetProcessorIdForWindows(); + } + catch + { + //couldn't get processor id when logon user has no required permission. try other methods... + } + + return GetWindowsMachineUniqueId(); + } + + private static string GetProcessorIdForWindows() + { + using (var managementObjectSearcher = + new System.Management.ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor")) + { + using (var searcherObj = managementObjectSearcher.Get()) + { + if (searcherObj.Count == 0) + { + throw new System.Exception("No unique computer ID found for this computer!"); + } + + var managementObjectEnumerator = searcherObj.GetEnumerator(); + managementObjectEnumerator.MoveNext(); + return managementObjectEnumerator.Current.GetPropertyValue("ProcessorId").ToString()!; + } + } + } + + private static string GetWindowsMachineUniqueId() + { + return RunCommandAndGetOutput("powershell (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID"); + } + + + private static string GetHarddiskSerialForLinux() + { + return RunCommandAndGetOutput( + "udevadm info --query=all --name=/dev/sda | grep ID_SERIAL_SHORT | tr -d \"ID_SERIAL_SHORT=:\""); + } + + private static string GetHarddiskSerialForOsX() + { + var command = + "ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/ { split($0, line, \"\\\"\"); printf(\"%s\\n\", line[4]); }'"; + + command = System.Text.RegularExpressions.Regex.Replace(command, @"(\\*)" + "\"", @"$1$1\" + "\""); + + return RunCommandAndGetOutput(command); + } + + private static string RunCommandAndGetOutput(string command) + { + var output = ""; + + using (var process = new System.Diagnostics.Process()) + { + process.StartInfo = new System.Diagnostics.ProcessStartInfo(GetFileName()) + { + Arguments = GetArguments(command), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + process.Start(); + process?.WaitForExit(); + + using (var stdOut = process!.StandardOutput) + { + using (var stdErr = process.StandardError) + { + output = stdOut.ReadToEnd(); + output += stdErr.ReadToEnd(); + } + } + } + + return output.Trim(); + } + + private static string GetFileName() + { + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + System.Runtime.InteropServices.OSPlatform.OSX) || + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform + .Linux)) + { + string[] fileNames = { "/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh" }; + foreach (var fileName in fileNames) + { + try + { + if (System.IO.File.Exists(fileName)) + { + return fileName; + } + } + catch + { + //ignore + } + } + + return "/bin/bash"; + } + + //Windows default. + return "cmd.exe"; + } + + private static string GetArguments(string command) + { + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + System.Runtime.InteropServices.OSPlatform.OSX) || + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform + .Linux)) + { + return "-c \"" + command + "\""; + } + + //Windows default. + return "/C \"" + command + "\""; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/AbpTool.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/AbpTool.cs new file mode 100644 index 0000000000..f2b20e3a03 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/AbpTool.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Internal.Telemetry.Constants.Enums; + +public enum AbpTool : byte +{ + Unknown = 0, + StudioUI = 1, + StudioCli = 2, + OldCli = 3 +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/OperationSystem.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/OperationSystem.cs new file mode 100644 index 0000000000..312eccec69 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/OperationSystem.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Internal.Telemetry.Constants.Enums; + +public enum OperationSystem +{ + Unknown = 0, + Windows = 1, + MacOS = 2, + Linux = 3, +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SessionType.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SessionType.cs new file mode 100644 index 0000000000..568fdb4f4d --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SessionType.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Internal.Telemetry.Constants.Enums; + +public enum SessionType +{ + Unknown = 0, + AbpStudio = 1, + AbpCli = 2, + ApplicationRuntime = 3 +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SoftwareType.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SoftwareType.cs new file mode 100644 index 0000000000..0e334513da --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SoftwareType.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.Internal.Telemetry.Constants.Enums; + +public enum SoftwareType : byte +{ + Others = 0, + AbpStudio = 1, + DotnetSdk = 2, + OperatingSystem = 3, + Ide = 4, + Browser = 5 +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryConsts.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryConsts.cs new file mode 100644 index 0000000000..c2797efd4a --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryConsts.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Internal.Telemetry.Constants; + +public class TelemetryConsts +{ + public const string VoloNameSpaceFilter = "Volo."; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryPaths.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryPaths.cs new file mode 100644 index 0000000000..07bc408043 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryPaths.cs @@ -0,0 +1,13 @@ +using System; +using System.IO; + +namespace Volo.Abp.Internal.Telemetry.Constants; + +public static class TelemetryPaths +{ + public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin"); + public static string ComputerId => Path.Combine(AbpRootPath, "cli", "computer-id.bin"); + public static string ActivityStorage => Path.Combine(AbpRootPath , "telemetry", "activity-storage.bin"); + public static string Studio => Path.Combine(AbpRootPath, "studio"); + private readonly static string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp"); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs new file mode 100644 index 0000000000..8942bf359d --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; + +internal interface ISoftwareDetector +{ + string Name { get; } + Task DetectAsync(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs new file mode 100644 index 0000000000..ae5d75a099 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; + +internal interface ISoftwareInfoProvider +{ + Task> GetSoftwareInfoAsync(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs new file mode 100644 index 0000000000..bbfa3fe414 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Internal.Telemetry.Constants.Enums; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; + +internal class SoftwareInfo(string name, string? version, string? uiTheme, SoftwareType softwareType) +{ + public string Name { get; set; } = name; + public string? Version { get; set; } = version; + public string? UiTheme { get; set; } = uiTheme; + public SoftwareType SoftwareType { get; set; } = softwareType; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Core/SoftwareDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Core/SoftwareDetector.cs new file mode 100644 index 0000000000..6b02c22c7c --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Core/SoftwareDetector.cs @@ -0,0 +1,78 @@ +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +[ExposeServices(typeof(ISoftwareDetector))] +abstract internal class SoftwareDetector: ISoftwareDetector , ISingletonDependency +{ + public abstract string Name { get; } + public abstract Task DetectAsync(); + + protected virtual async Task ExecuteCommandAsync(string command, string? arg) + { + var outputBuilder = new StringBuilder(); + + var processStartInfo = new ProcessStartInfo + { + FileName = command, + Arguments = arg ?? "", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = new Process(); + process.StartInfo = processStartInfo; + process.EnableRaisingEvents = true; + + var tcs = new TaskCompletionSource(); + + process.OutputDataReceived += (sender, e) => + { + if (e.Data != null) + { + outputBuilder.AppendLine(e.Data); + } + }; + + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + { + outputBuilder.AppendLine(e.Data); + } + }; + + process.Exited += (sender, e) => + { + tcs.TrySetResult(true); + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await tcs.Task; + + var output = outputBuilder.ToString().Trim(); + return string.IsNullOrWhiteSpace(output) ? null : output; + } + + protected string? GetFileVersion(string filePath) + { + try + { + var versionInfo = FileVersionInfo.GetVersionInfo(filePath); + return versionInfo.FileVersion; + } + catch + { + return string.Empty; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs new file mode 100644 index 0000000000..3d35c7192a --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs @@ -0,0 +1,76 @@ +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class AbpStudioDetector : SoftwareDetector +{ + public override string Name => "Abp Studio"; + private const string AbpStudioVersionExtensionName = "Volo.Abp.Studio.Extensions.StandardSolutionTemplates"; + + public override Task DetectAsync() + { + try + { + var uiTheme = GetAbpStudioUiTheme(); + var version = GetAbpStudioVersion(); + + return Task.FromResult(new SoftwareInfo(Name, version, uiTheme, SoftwareType.AbpStudio)); + } + catch + { + return Task.FromResult(null); + } + } + + private string? GetAbpStudioUiTheme() + { + var ideStateJsonPath = Path.Combine( + TelemetryPaths.Studio, + "ui", + "ide-state.json" + ); + if (!File.Exists(ideStateJsonPath)) + { + return null; + } + using var fs = new FileStream(ideStateJsonPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var doc = JsonDocument.Parse(fs); + + return doc.RootElement.TryGetProperty("theme", out var themeElement) ? themeElement.GetString() : null; + } + + private string? GetAbpStudioVersion() + { + var extensionsFilePath = Path.Combine(TelemetryPaths.Studio, "extensions.json"); + + if (!File.Exists(extensionsFilePath)) + { + return null; + } + + using var fs = new FileStream(extensionsFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var doc = JsonDocument.Parse(fs); + + if (doc.RootElement.TryGetProperty("Extensions", out var extensionsElement) && + extensionsElement.ValueKind == JsonValueKind.Array) + { + foreach (var extension in extensionsElement.EnumerateArray()) + { + if (extension.TryGetProperty("name", out var nameProp) && + nameProp.GetString() == AbpStudioVersionExtensionName && + extension.TryGetProperty("version", out var versionProp)) + { + return versionProp.GetString(); + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs new file mode 100644 index 0000000000..fa7f8cff74 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class ChromeDetector : SoftwareDetector +{ + public override string Name => "Chrome"; + + public async override Task DetectAsync() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var chromePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Google", "Chrome", "Application", "chrome.exe"); + if (File.Exists(chromePath)) + { + return new SoftwareInfo(Name, GetFileVersion(chromePath), null, SoftwareType.Browser); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + if (File.Exists(chromePath)) + { + var version = await ExecuteCommandAsync(chromePath, "--version"); + return new SoftwareInfo(Name, version, null, SoftwareType.Browser); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var chromePath = "/usr/bin/google-chrome"; + if (File.Exists(chromePath)) + { + var version = await ExecuteCommandAsync(chromePath, "--version"); + return new SoftwareInfo(Name, version, null, SoftwareType.Browser); + } + } + + return null; + } + + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs new file mode 100644 index 0000000000..4f03eac248 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class DotnetSdkDetector : SoftwareDetector +{ + public override string Name => "DotnetSdk"; + + public async override Task DetectAsync() + { + return new SoftwareInfo(Name, Environment.Version.ToString(), null, SoftwareType.DotnetSdk); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs new file mode 100644 index 0000000000..261e36c7e5 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class FireFoxDetector : SoftwareDetector +{ + public override string Name => "Firefox"; + public async override Task DetectAsync() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var firefoxPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Mozilla Firefox", "firefox.exe"); + if (File.Exists(firefoxPath)) + { + return new SoftwareInfo(Name, GetFileVersion(firefoxPath), null, SoftwareType.Browser); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var firefoxPath = "/Applications/Firefox.app/Contents/MacOS/firefox"; + if (File.Exists(firefoxPath)) + { + var version = await ExecuteCommandAsync(firefoxPath, "--version"); + return new SoftwareInfo(Name, version, null, SoftwareType.Browser); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var firefoxPath = "/usr/bin/firefox"; + if (File.Exists(firefoxPath)) + { + var version = await ExecuteCommandAsync(firefoxPath, "--version"); + return new SoftwareInfo(Name, version, null, SoftwareType.Browser); + } + } + + return null; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs new file mode 100644 index 0000000000..e1e67c516e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class MsEdgeDetector : SoftwareDetector +{ + public override string Name => "MsEdge"; + public async override Task DetectAsync() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var firefoxPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + "Microsoft", "Edge", "Application", "msedge.exe"); + + if (File.Exists(firefoxPath)) + { + return new SoftwareInfo(Name, GetFileVersion(firefoxPath), null, SoftwareType.Browser); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var edgePath = "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"; + if (File.Exists(edgePath)) + { + var version = await ExecuteCommandAsync(edgePath, "--version"); + return new SoftwareInfo(Name, version, null, SoftwareType.Browser); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var edgePath = "/usr/bin/microsoft-edge"; + if (File.Exists(edgePath)) + { + var version = await ExecuteCommandAsync(edgePath, "--version"); + return new SoftwareInfo(Name, version, null, SoftwareType.Browser); + } + } + + return null; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs new file mode 100644 index 0000000000..85a4e05cce --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal class NodeJsDetector : SoftwareDetector +{ + public override string Name => "Node.js"; + + public async override Task DetectAsync() + { + try + { + var output = await ExecuteCommandAsync("node", "-v"); + + if (output.IsNullOrWhiteSpace()) + { + return null; + } + + var version = output.Trim().TrimStart('v'); + + return new SoftwareInfo(Name, version, uiTheme: null, SoftwareType.Others); + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs new file mode 100644 index 0000000000..9dfc77f8f2 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs @@ -0,0 +1,77 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class OperatingSystemDetector : SoftwareDetector +{ + public override string Name => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows" : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "macOS" : "Linux"; + + public async override Task DetectAsync() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return new SoftwareInfo(Name, Environment.OSVersion.Version.ToString(), null, SoftwareType.OperatingSystem); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var version = await ExecuteCommandAsync("sw_vers", "-productVersion"); + return new SoftwareInfo(Name, version, GetMacUiTheme(), SoftwareType.OperatingSystem); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var version = await ExecuteCommandAsync("lsb_release", "-ds") ?? await ExecuteCommandAsync("uname", "-r"); + return new SoftwareInfo(Name, version, await GetLinuxUiTheme(), SoftwareType.OperatingSystem); + } + + return null; + } + + private async Task GetLinuxUiTheme() + { + var output = await ExecuteCommandAsync("gsettings", "get org.gnome.desktop.interface gtk-theme"); + + if (!output.IsNullOrWhiteSpace() && output.ToLowerInvariant().Contains("dark")) + { + return "Dark"; + } + + return "Light"; + } + + + private string? GetMacUiTheme() + { + try + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo + { + FileName = "defaults", + Arguments = "read -g AppleInterfaceStyle", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + return output == "Dark" ? "Dark" : "Light"; + } + catch + { + return "Light"; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs new file mode 100644 index 0000000000..a36dd8a0ab --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using System.Xml.Linq; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class RiderDetector : SoftwareDetector +{ + public override string Name => "Rider"; + + public override Task DetectAsync() + { + try + { + string baseConfigDir; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + baseConfigDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "JetBrains"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + baseConfigDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + "Library", "Application Support", "JetBrains"); + } + else + { + baseConfigDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + ".config", "JetBrains"); + } + + if (!Directory.Exists(baseConfigDir)) + { + return Task.FromResult(null); + } + + var riderDirs = Directory + .GetDirectories(baseConfigDir, "Rider*") + .Select(dir => + { + var name = Path.GetFileName(dir); + var verStr = name.Substring("Rider".Length); + return Version.TryParse(verStr, out var v) + ? (Path: dir, Version: v) + : (Path: null, Version: null); + }) + .Where(x => x.Path != null) + .ToList(); + + if (!riderDirs.Any()) + { + return Task.FromResult(null); + } + + var latest = riderDirs + .OrderByDescending(x => x.Version) + .First(); + + var theme = string.Empty; + var colorsFile = Path.Combine(latest.Path!, "options", "colors.scheme.xml"); + if (File.Exists(colorsFile)) + { + try + { + var doc = XDocument.Load(colorsFile); + var schemeEl = doc + .Descendants("global_color_scheme") + .FirstOrDefault(); + var schemeName = schemeEl?.Attribute("name")?.Value; + if (!schemeName.IsNullOrEmpty()) + { + theme = schemeName.IndexOf("dark", StringComparison.OrdinalIgnoreCase) >= 0 + ? "Dark" + : "Light"; + } + } + catch + { + //ignored + } + } + + return Task.FromResult(new SoftwareInfo(Name, latest.Version?.ToString(), theme, + SoftwareType.Ide)); + } + catch (Exception e) + { + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs new file mode 100644 index 0000000000..72fda0f500 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class VisualStudioCodeDetector : SoftwareDetector +{ + public override string Name => "Visual Studio Code"; + + public async override Task DetectAsync() + { + string? installDir = null; + string? settingsPath = null; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var progFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var candidates = new[] + { + Path.Combine(localAppData, "Programs", "Microsoft VS Code"), + Path.Combine(progFiles, "Microsoft VS Code") + }; + installDir = candidates.FirstOrDefault(Directory.Exists); + + settingsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Code", "User", "globalStorage" ,"storage.json" + ); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var app = "/Applications/Visual Studio Code.app"; + if (Directory.Exists(app)) + { + installDir = app; + } + + settingsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + "Library", "Application Support", "Code", "User", "globalStorage", "storage.json" + ); + } + else + { + var candidate = "/usr/share/code"; + if (Directory.Exists(candidate)) + { + installDir = candidate; + } + + settingsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + ".config", "Code", "User", "globalStorage", "storage.json" + ); + } + + if (installDir == null) + { + return null; + } + + + Version? version = null; + var productJson = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ? Path.Combine(installDir, "Contents", "Resources", "app", "product.json") + : Path.Combine(installDir, "resources", "app", "product.json"); + + if (File.Exists(productJson)) + { + try + { + using var jsonDoc = JsonDocument.Parse(File.ReadAllText(productJson)); + var root = jsonDoc.RootElement; + if (root.TryGetProperty("version", out var versionProp)) + { + var versionStr = versionProp.GetString(); + if (Version.TryParse(versionStr, out var v)) + { + version = v; + } + } + } + catch + { + + } + } + + if (version == null) + { + return null; + } + + var theme = "Unknown"; + + if (File.Exists(settingsPath)) + { + try + { + using var json = JsonDocument.Parse( File.ReadAllText(settingsPath)); + var root = json.RootElement; + if (root.TryGetProperty("theme", out var themeProp)) + { + var themeName = themeProp.GetString() ?? ""; + if (themeName.IndexOf("dark", StringComparison.OrdinalIgnoreCase) >= 0) + { + theme = "Dark"; + } + else if (themeName.IndexOf("light", StringComparison.OrdinalIgnoreCase) >= 0) + { + theme = "Light"; + } + } + } + catch + { + // ignored + } + } + + return new SoftwareInfo(Name, version?.ToString(), theme, SoftwareType.Ide); + + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs new file mode 100644 index 0000000000..fbbb596157 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Volo.Abp.Internal.Telemetry.Constants.Enums; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; + +internal sealed class VisualStudioDetector : SoftwareDetector +{ + public override string Name => "Visual Studio"; + + public override Task DetectAsync() + { + var version = GetVisualStudioVersionViaVsWhere(); + var theme = GetVisualStudioTheme(); + + if (version == null) + { + return Task.FromResult(null); + } + + return Task.FromResult(new SoftwareInfo( + name: Name, + version: version, + uiTheme: theme, + softwareType: SoftwareType.Ide)); + } + + private string? GetVisualStudioVersionViaVsWhere() + { + var vswherePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + "Microsoft Visual Studio", + "Installer", + "vswhere.exe"); + + if (!File.Exists(vswherePath)) + { + return null; + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = vswherePath, + Arguments = "-latest -property catalog_productDisplayVersion", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + return string.IsNullOrWhiteSpace(output) ? null : output; + } + + private string? GetVisualStudioTheme() + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + var vsSettingsDir = Path.Combine(localAppData, "Microsoft", "VisualStudio"); + + if (!Directory.Exists(vsSettingsDir)) + { + return null; + } + + var settingsPath = Directory.GetFiles(vsSettingsDir, "CurrentSettings*.vssettings", SearchOption.AllDirectories) + .OrderByDescending(File.GetLastWriteTime) + .FirstOrDefault(); + + if (string.IsNullOrEmpty(settingsPath)) + { + return null; + } + + try + { + var doc = XDocument.Load(settingsPath); + + var themeId = doc.Descendants("Theme") + .FirstOrDefault()?.Attribute("Id")?.Value; + + return themeId?.ToUpperInvariant() switch + { + "{1DED0138-47CE-435E-84EF-9EC1F439B749}" => "Dark", + "{DE3DBBCD-F642-433C-8353-8F1DF4370ABA}" => "Light", + "{2DED0138-47CE-435E-84EF-9EC1F439B749}" => "Blue", + _ => "Unknown" + }; + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Providers/SoftwareInfoProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Providers/SoftwareInfoProvider.cs new file mode 100644 index 0000000000..3a56ed420e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Providers/SoftwareInfoProvider.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Providers; + +internal class SoftwareInfoProvider : ISoftwareInfoProvider , ISingletonDependency +{ + private readonly IEnumerable _softwareDetectors; + + public SoftwareInfoProvider(IEnumerable softwareDetectors) + { + _softwareDetectors = softwareDetectors; + } + + public async Task> GetSoftwareInfoAsync() + { + var result = new List(); + + foreach (var softwareDetector in _softwareDetectors) + { + try + { + var softwareInfo = await softwareDetector.DetectAsync(); + if (softwareInfo is not null) + { + result.Add(softwareInfo); + } + } + catch + { + //ignored + } + } + return result; + } + + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpPackageMetadataReader.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpPackageMetadataReader.cs new file mode 100644 index 0000000000..5f505324b0 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpPackageMetadataReader.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; + +namespace Volo.Abp.Internal.Telemetry.Helpers; + +static internal class AbpProjectMetadataReader +{ + private const string AbpPackageSearchPattern = "*.abppkg"; + private const string AbpSolutionSearchPattern = "*.abpsln"; + private const int MaxDepth = 5; + public static AbpProjectMetaData? ReadProjectMetadata(Assembly assembly) + { + var assemblyPath = assembly.Location; + try + { + var projectDirectory = Path.GetDirectoryName(assemblyPath); + if (projectDirectory == null) + { + return null; + } + + var abpPackagePath = FindFileUpwards(projectDirectory, AbpPackageSearchPattern); + + if (abpPackagePath.IsNullOrEmpty()) + { + return null; + } + + var projectMetaData = ReadOrCreateMetadata(abpPackagePath); + + var abpSolutionPath = FindFileUpwards(projectDirectory, AbpSolutionSearchPattern); + + if (!abpSolutionPath.IsNullOrEmpty()) + { + projectMetaData.AbpSlnPath = abpSolutionPath; + } + + return projectMetaData; + } + catch + { + return null; + } + } + + private static AbpProjectMetaData ReadOrCreateMetadata(string packagePath) + { + + var fileContent = File.ReadAllText(packagePath); + var metadata = new AbpProjectMetaData(); + + using var document = JsonDocument.Parse(fileContent); + var root = document.RootElement; + + if (TryGetProjectId(root,out var projectId)) + { + metadata.ProjectId = projectId; + } + else + { + metadata.ProjectId = Guid.NewGuid(); + WriteProjectIdToPackageFile(root, packagePath, metadata.ProjectId.Value); + } + + if (root.TryGetProperty("role", out var roleElement) && + roleElement.ValueKind == JsonValueKind.String) + { + metadata.Role = roleElement.GetString()!; + } + + return metadata; + } + + private static void WriteProjectIdToPackageFile(JsonElement root, string packagePath, Guid projectId) + { + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true })) + { + writer.WriteStartObject(); + + if (root.ValueKind == JsonValueKind.Object) + { + foreach (var property in root.EnumerateObject()) + { + if (property.Name != "projectId") + { + property.WriteTo(writer); + } + } + } + + writer.WriteString("projectId", projectId.ToString()); + writer.WriteEndObject(); + } + + var json = Encoding.UTF8.GetString(stream.ToArray()); + File.WriteAllText(packagePath, json); + } + + private static string? FindFileUpwards(string startingDir, string searchPattern) + { + var currentDir = new DirectoryInfo(startingDir); + var currentDepth = 0; + + while (currentDir != null && currentDepth < MaxDepth) + { + var file = currentDir.GetFiles(searchPattern).FirstOrDefault(); + if (file != null) + { + return file.FullName; + } + + currentDir = currentDir.Parent; + currentDepth++; + } + + return null; + } + private static bool TryGetProjectId(JsonElement element, out Guid projectId) + { + if (element.TryGetProperty("projectId", out var projectIdElement) && + projectIdElement.ValueKind == JsonValueKind.String && + Guid.TryParse(projectIdElement.GetString(), out projectId)) + { + return true; + } + + projectId = Guid.Empty; + return false; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpProjectMetaData.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpProjectMetaData.cs new file mode 100644 index 0000000000..d56ecf6106 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpProjectMetaData.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Internal.Telemetry.Helpers; + +internal class AbpProjectMetaData +{ + public Guid? ProjectId { get; set; } + public string? Role { get; set; } + public string? AbpSlnPath { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/Cryptography.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/Cryptography.cs new file mode 100644 index 0000000000..3d402b651b --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/Cryptography.cs @@ -0,0 +1,42 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Volo.Abp.Internal.Telemetry.Helpers; + +static internal class Cryptography +{ + private const string EncryptionKey = "AbpTelemetryStorageKey"; + + public static string Encrypt(string plainText) + { + Check.NotNullOrEmpty(plainText, nameof(plainText)); + using var aes = Aes.Create(); + using var sha256 = SHA256.Create(); + + aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.PKCS7; + + var encryptor = aes.CreateEncryptor(); + var inputBytes = Encoding.UTF8.GetBytes(plainText); + var encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); + return Convert.ToBase64String(encryptedBytes); + } + + public static string Decrypt(string cipherText) + { + Check.NotNullOrEmpty(cipherText, nameof(cipherText)); + using var aes = Aes.Create(); + using var sha256 = SHA256.Create(); + + aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.PKCS7; + + var decryptor = aes.CreateDecryptor(); + var inputBytes = Convert.FromBase64String(cipherText); + var decryptedBytes = decryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); + return Encoding.UTF8.GetString(decryptedBytes); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/MutexExecutor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/MutexExecutor.cs new file mode 100644 index 0000000000..d8eb28b40e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/MutexExecutor.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Threading; + +namespace Volo.Abp.Internal.Telemetry.Helpers; + +static internal class MutexExecutor +{ + private const string MutexName = "Global\\MyFileReadMutex"; + private const int TimeoutMilliseconds = 3000; + + public static string? ReadFileSafely(string filePath) + { + using var mutex = new Mutex(false, MutexName); + + if (!mutex.WaitOne(TimeoutMilliseconds)) + { + return null; + } + + try + { + if (!File.Exists(filePath)) + { + return null; + } + + return File.ReadAllText(filePath); + } + catch (IOException) + { + return null; + } + finally + { + try + { + mutex.ReleaseMutex(); + } + catch + { + // Already released or abandoned + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryActivitySender.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryActivitySender.cs new file mode 100644 index 0000000000..46171de7ad --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryActivitySender.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Internal.Telemetry; + +public interface ITelemetryActivitySender +{ + Task TrySendQueuedActivitiesAsync(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryService.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryService.cs new file mode 100644 index 0000000000..32ec6f706a --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.Internal.Telemetry; +public interface ITelemetryService +{ + IAsyncDisposable TrackActivityAsync(string activityName, Action>? additionalProperties = null); + Task AddActivityAsync(string activityName, Action>? additionalProperties = null); + Task AddErrorActivityAsync(Action> additionalProperties); + Task AddErrorActivityAsync(string errorMessage); + Task AddErrorForActivityAsync(string failingActivity, string errorMessage); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryActivitySender.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryActivitySender.cs new file mode 100644 index 0000000000..d5efc1ccfd --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryActivitySender.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; + +namespace Volo.Abp.Internal.Telemetry; + +public class TelemetryActivitySender : ITelemetryActivitySender, IScopedDependency +{ + private readonly ITelemetryActivityStorage _telemetryActivityStorage; + private readonly ILogger _logger; + + private const int ActivitySendBatchSize = 50; + private const int MaxRetryAttempts = 3; + private const int RetryDelayMilliseconds = 1000; + + public TelemetryActivitySender(ITelemetryActivityStorage telemetryActivityStorage, ILogger logger) + { + _telemetryActivityStorage = telemetryActivityStorage; + _logger = logger; + } + + public async Task TrySendQueuedActivitiesAsync() + { + if (!_telemetryActivityStorage.ShouldSendActivities()) + { + return; + } + + await SendActivitiesAsync(); + } + + + private async Task SendActivitiesAsync() + { + try + { + var activities = _telemetryActivityStorage.GetActivities(); + var batches = CreateActivityBatches(activities); + + using var httpClient = new HttpClient(); + ConfigureHttpClientAuthentication(httpClient); + + foreach (var batch in batches) + { + var isSuccessful = await TrySendBatchWithRetriesAsync(httpClient, batch); + + if (!isSuccessful) + { + break; + } + } + } + catch + { + //ignored + } + } + + + private async Task TrySendBatchWithRetriesAsync(HttpClient httpClient, ActivityEvent[] activities) + { + var currentAttempt = 0; + + while (currentAttempt < MaxRetryAttempts) + { + try + { + var response = await httpClient.PostAsync($"{AbpPlatformUrls.AbpTelemetryApiUrl}api/telemetry/collect", new StringContent(JsonSerializer.Serialize(activities), Encoding.UTF8, "application/json")); + + if (response.IsSuccessStatusCode) + { + _telemetryActivityStorage.DeleteActivities(activities); + } + else + { + _logger.LogWithLevel(LogLevel.Trace, + $"Failed to send telemetry activities. Status code: {response.StatusCode}, Reason: {response.ReasonPhrase}"); + _telemetryActivityStorage.MarkActivitiesAsFailed(activities); + } + + return true; + } + catch (Exception ex) + { + _logger.LogWithLevel(LogLevel.Trace, $"Error sending telemetry activities: {ex.Message}"); + currentAttempt++; + await Task.Delay(currentAttempt * RetryDelayMilliseconds); + } + } + + _logger.LogWithLevel(LogLevel.Trace, "Max retries reached. Failed to send telemetry activities."); + + return false; + } + + private static IEnumerable CreateActivityBatches(List activities) + { + return activities + .Select((x, i) => new { Index = i, Value = x }) + .GroupBy(x => x.Index / ActivitySendBatchSize) + .Select(x => x.Select(v => v.Value).ToArray()); + } + + private static void ConfigureHttpClientAuthentication(HttpClient httpClient) + { + if (!File.Exists(TelemetryPaths.AccessToken)) + { + return; + } + + var accessToken = File.ReadAllText(TelemetryPaths.AccessToken, Encoding.UTF8); + + if (accessToken.IsNullOrEmpty()) + { + return; + } + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs new file mode 100644 index 0000000000..8a2e3b4382 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Constants; +using ActivityContext = Volo.Abp.Internal.Telemetry.Activity.ActivityContext; + +namespace Volo.Abp.Internal.Telemetry; + +public class TelemetryService : ITelemetryService, IScopedDependency +{ + private readonly ITelemetryActivitySender _telemetryActivitySender; + private readonly ITelemetryActivityEventBuilder _telemetryActivityEventBuilder; + private readonly ITelemetryActivityStorage _telemetryActivityStorage; + + public TelemetryService(ITelemetryActivitySender telemetryActivitySender, + ITelemetryActivityEventBuilder telemetryActivityEventBuilder, + ITelemetryActivityStorage telemetryActivityStorage) + { + _telemetryActivitySender = telemetryActivitySender; + _telemetryActivityEventBuilder = telemetryActivityEventBuilder; + _telemetryActivityStorage = telemetryActivityStorage; + } + + + public IAsyncDisposable TrackActivityAsync(string activityName, + Action>? additionalProperties = null) + { + Check.NotNullOrEmpty(activityName, nameof(activityName)); + var stopwatch = Stopwatch.StartNew(); + var context = ActivityContext.Create(activityName, additionalProperties: additionalProperties); + + return new AsyncDisposeFunc(async () => + { + stopwatch.Stop(); + context.Current[ActivityPropertyNames.ActivityDuration] = stopwatch.ElapsedMilliseconds; + await AddActivityAsync(context); + }); + } + + public async Task AddActivityAsync(string activityName, + Action>? additionalProperties = null) + { + Check.NotNullOrEmpty(activityName, nameof(activityName)); + var context = ActivityContext.Create(activityName, additionalProperties: additionalProperties); + await AddActivityAsync(context); + } + + public async Task AddErrorActivityAsync(Action> additionalProperties) + { + var context = ActivityContext.Create(ActivityNameConsts.Error, additionalProperties: additionalProperties); + await AddActivityAsync(context); + } + + public async Task AddErrorActivityAsync(string errorMessage) + { + var context = ActivityContext.Create(ActivityNameConsts.Error, errorMessage); + await AddActivityAsync(context); + } + + public async Task AddErrorForActivityAsync(string failingActivity, string errorMessage) + { + Check.NotNullOrEmpty(failingActivity, nameof(failingActivity)); + var context = ActivityContext.Create(ActivityNameConsts.Error, errorMessage, configure => + { + configure[ActivityPropertyNames.FailingActivity] = failingActivity; + }); + await AddActivityAsync(context); + } + + private Task AddActivityAsync(ActivityContext context) + { + _ = Task.Run(async () => + { + await BuildAndSendActivityAsync(context); + }); + + return Task.CompletedTask; + } + + private async Task BuildAndSendActivityAsync(ActivityContext context) + { + try + { + var activityEvent = await _telemetryActivityEventBuilder.BuildAsync(context); + if (activityEvent is null) + { + return; + } + + _telemetryActivityStorage.SaveActivity(activityEvent); + await _telemetryActivitySender.TrySendQueuedActivitiesAsync(); + } + catch + { + //ignored + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Telemetry/TelemetryDomainInfoEnricher.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Telemetry/TelemetryDomainInfoEnricher.cs new file mode 100644 index 0000000000..543cf8e406 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Telemetry/TelemetryDomainInfoEnricher.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Internal.Telemetry.Activity; +using Volo.Abp.Internal.Telemetry.Activity.Contracts; +using Volo.Abp.Internal.Telemetry.Activity.Providers; +using Volo.Abp.Internal.Telemetry.Constants; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Domain.Telemetry; + +[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))] +public class TelemetryDomainInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher +{ + private readonly ITypeFinder _typeFinder; + + public TelemetryDomainInfoEnricher(ITypeFinder typeFinder, IServiceProvider serviceProvider) + : base(serviceProvider) + { + _typeFinder = typeFinder; + } + + protected override Task CanExecuteAsync(ActivityContext context) + { + return Task.FromResult(context.ProjectId.HasValue); + } + + protected override Task ExecuteAsync(ActivityContext context) + { + var entityCount = _typeFinder.Types.Count(t => + typeof(IEntity).IsAssignableFrom(t) && !t.IsAbstract && + !t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter)); + + context.Current[ActivityPropertyNames.EntityCount] = entityCount; + + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs index 28cacd4be0..b2a9179415 100644 --- a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs +++ b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs @@ -15,8 +15,12 @@ public class AuthorizationTestPermissionDefinitionProvider : PermissionDefinitio var group = context.AddGroup("TestGroup"); - group.AddPermission("MyAuthorizedService1"); + group[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName].ShouldBe(typeof(AuthorizationTestPermissionDefinitionProvider).FullName); + + var permission1 = group.AddPermission("MyAuthorizedService1"); + permission1[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName].ShouldBe(typeof(AuthorizationTestPermissionDefinitionProvider).FullName); + group.AddPermission("MyPermission1").StateCheckers.Add(new TestRequireEditionPermissionSimpleStateChecker()); group.AddPermission("MyPermission2"); group.AddPermission("MyPermission3"); diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/ActivityEvent_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/ActivityEvent_Tests.cs new file mode 100644 index 0000000000..42083ceaa1 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/ActivityEvent_Tests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using Shouldly; +using Volo.Abp.Internal.Telemetry.Activity; +using Volo.Abp.Internal.Telemetry.Constants; +using Xunit; + +namespace Volo.Abp.Telemetry; + +public class ActivityEvent_Tests +{ + [Fact] + public void Should_Create_ActivityEvent_With_Required_Parameters() + { + // Arrange + var activityName = "TestActivity"; + var details = "Test Details"; + + // Act + var activityEvent = new ActivityEvent(activityName, details); + + // Assert + activityEvent[ActivityPropertyNames.ActivityName].ShouldBe(activityName); + activityEvent[ActivityPropertyNames.ActivityDetails].ShouldBe(details); + activityEvent[ActivityPropertyNames.Id].ShouldNotBe(Guid.Empty); + activityEvent[ActivityPropertyNames.Time].ShouldNotBe(default); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Should_Throw_Exception_When_ActivityName_Is_Invalid(string invalidName) + { + // Act & Assert + Should.Throw(() => + new ActivityEvent(invalidName)); + } + + [Fact] + public void Should_Set_And_Get_AdditionalProperties() + { + // Arrange + var activityEvent = new ActivityEvent("TestActivity"); + var additionalProps = new Dictionary + { + { "key1", "value1" }, + { "key2", 42 } + }; + + // Act + activityEvent[ActivityPropertyNames.AdditionalProperties] = additionalProps; + + // Assert + + var activityAdditionalProps = activityEvent[ActivityPropertyNames.AdditionalProperties] as Dictionary; + activityAdditionalProps.ShouldNotBeNull(); + activityAdditionalProps.Count.ShouldBe(2); + activityAdditionalProps["key1"].ShouldBe("value1"); + activityAdditionalProps["key2"].ShouldBe(42); + } + + [Fact] + public void Should_Return_Default_Values_When_Properties_Not_Set() + { + // Arrange + var activityEvent = new ActivityEvent("TestActivity"); + + // Assert + activityEvent[ActivityPropertyNames.ActivityDetails].ShouldBeNull(); + } + + [Fact] + public void Should_Behave_Like_Dictionary() + { + // Arrange + var activityEvent = new ActivityEvent("TestActivity"); + + // Act + activityEvent["CustomKey"] = "CustomValue"; + + // Assert + activityEvent["CustomKey"].ShouldBe("CustomValue"); + activityEvent.ContainsKey("CustomKey").ShouldBeTrue(); + } + + +} + diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/Cryptography_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/Cryptography_Tests.cs new file mode 100644 index 0000000000..fd90e1221a --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Telemetry/Cryptography_Tests.cs @@ -0,0 +1,52 @@ +using System; +using Shouldly; +using Volo.Abp.Internal.Telemetry.Helpers; +using Xunit; + +namespace Volo.Abp.Telemetry; + +public class Cryptography_Tests +{ + [Fact] + public void Should_Encrypt_And_Decrypt_Text_Successfully() + { + // Arrange + const string plainText = "Test message 123!"; + + // Act + var encryptedText = Cryptography.Encrypt(plainText); + var decryptedText = Cryptography.Decrypt(encryptedText); + + // Assert + decryptedText.ShouldBe(plainText); + } + + [Fact] + public void Should_Generate_Different_Encrypted_Values_For_Different_Inputs() + { + // Arrange + const string text1 = "Message 1"; + const string text2 = "Message 2"; + + // Act + var encrypted1 = Cryptography.Encrypt(text1); + var encrypted2 = Cryptography.Encrypt(text2); + + // Assert + encrypted1.ShouldNotBe(encrypted2); + } + [Fact] + public void Encrypt_Should_Throw_ArgumentException_When_Input_Is_Null() + { + // Act & Assert + Should.Throw(() => Cryptography.Encrypt(null)); + } + + [Fact] + public void Decrypt_Should_Throw_ArgumentException_When_Input_Is_Null() + { + // Act & Assert + Should.Throw(() => Cryptography.Decrypt(null)); + } + +} \ No newline at end of file From 5654f9b2ec7b9af3a65ad5d2c395175178ccb19a Mon Sep 17 00:00:00 2001 From: Emre KARA Date: Fri, 25 Jul 2025 14:08:21 +0300 Subject: [PATCH 2/5] simplify device manager error handling and remove unused comments --- .../Telemetry/Constants/DeviceManager.cs | 48 +++++-------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs index adf110ba64..e3993886a0 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs @@ -1,4 +1,6 @@ -namespace Volo.Abp.Internal.Telemetry.Constants; +using System; + +namespace Volo.Abp.Internal.Telemetry.Constants; static internal class DeviceManager { @@ -51,27 +53,12 @@ static internal class DeviceManager return osPrefix + platformId + osArchitecture + "-" + uniqueKey; } - catch (System.Exception ex) + catch { - System.Console.WriteLine("WARNING ABP-LIC-0025! Contact to license@abp.io with the below information" + - System.Environment.NewLine + - "* Architecture: " + - System.Runtime.InteropServices.RuntimeInformation.OSArchitecture + - System.Environment.NewLine + - "* Description: " + - System.Runtime.InteropServices.RuntimeInformation.OSDescription + - System.Environment.NewLine + - "* Platform Id: " + platformId + System.Environment.NewLine + - "* OS architecture: " + osArchitecture + System.Environment.NewLine + - "* Operating system: " + operatingSystem + System.Environment.NewLine + - "* Framework description: " + - System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription + - System.Environment.NewLine + - "* Error: " + ex.ToString()); - - return "95929008-b147-454a-8737-efed71fa2241"; + return Guid.NewGuid().ToString(); } } + private static string GetNetworkAdapterSerial() { string macAddress = string.Empty; @@ -96,15 +83,7 @@ static internal class DeviceManager return macAddress!; } - /// - /// 0 - Win32S - /// 1 - Win32Windows - /// 2 - Win32NT - /// 3 - WinCE - /// 4 - Unix - /// 5 - Xbox - /// 6 - MacOSX - /// + private static char GetPlatformIdOrDefault(char defaultValue = '*') { try @@ -116,6 +95,7 @@ static internal class DeviceManager return defaultValue; } } + private static string ConvertToMd5(string text) { using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider()) @@ -123,7 +103,7 @@ static internal class DeviceManager return EncodeBase64(md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text))); } } - + private static string EncodeBase64(byte[] ba) { var hex = new System.Text.StringBuilder(ba.Length * 2); @@ -136,12 +116,7 @@ static internal class DeviceManager return hex.ToString(); } - /// - /// 0 - X86 (An Intel-based 32-bit processor architecture) - /// 1 - X64 (A 64-bit ARM processor architecture) - /// 2 - Arm (A 32-bit ARM processor architecture) - /// 3 - Arm64 (A 64-bit ARM processor architecture) - /// + private static char GetOsArchitectureOrDefault(char defaultValue = '*') { try @@ -162,7 +137,6 @@ static internal class DeviceManager } catch { - //couldn't get processor id when logon user has no required permission. try other methods... } return GetWindowsMachineUniqueId(); @@ -192,7 +166,7 @@ static internal class DeviceManager return RunCommandAndGetOutput("powershell (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID"); } - + private static string GetHarddiskSerialForLinux() { return RunCommandAndGetOutput( From 610ff9085310284381ec8cc1000e4e6262a9e528 Mon Sep 17 00:00:00 2001 From: Emre KARA Date: Mon, 4 Aug 2025 11:00:37 +0300 Subject: [PATCH 3/5] Update ActivityPropertyNames.cs --- .../Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs index 77b1bbdef6..68f7e95f16 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs @@ -2,7 +2,7 @@ public static class ActivityPropertyNames { - public const string SessionId = "SessionId"; + public const string SessionId = "SessionId"; public const string ActivityName = "ActivityName"; public const string Error = "Error"; public const string ErrorDetail = "ErrorDetail"; From 2a9d93a3d52789749478cd4c6904f41af75922e2 Mon Sep 17 00:00:00 2001 From: Emre KARA Date: Wed, 6 Aug 2025 10:43:24 +0300 Subject: [PATCH 4/5] Filter null activities after deserialization --- .../Telemetry/Activity/Storage/TelemetryActivityStorage.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs index 2ccc0e552d..70455b2d45 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs @@ -178,7 +178,9 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep var json = Cryptography.Decrypt(fileContent); - return JsonSerializer.Deserialize(json, JsonSerializerOptions)!; + var state = JsonSerializer.Deserialize(json, JsonSerializerOptions)!; + state.Activities = state.Activities.Where(x => x != null).ToList(); + return state; } catch { From 039a928bf377161ed203b9b6e2662a8d0a5d3de8 Mon Sep 17 00:00:00 2001 From: Emre KARA Date: Tue, 19 Aug 2025 11:29:19 +0300 Subject: [PATCH 5/5] Update ActivityNameConsts.cs --- .../Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs index c1cc986da3..64b22ef78f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs @@ -47,7 +47,6 @@ public static class ActivityNameConsts public const string AbpStudioMonitoringExceptions = "AbpStudio.Monitoring.Exceptions"; public const string AbpStudioMonitoringExceptionsDetail = "AbpStudio.Monitoring.Exceptions.Detail"; public const string AbpStudioMonitoringLogs = "AbpStudio.Monitoring.Logs"; - public const string AbpStudioMonitoringLogsFiltered = "AbpStudio.Monitoring.Logs.Filtered"; public const string AbpStudioKubernetesAddProfile = "AbpStudio.Kubernetes.Add.Profile"; public const string AbpStudioKubernetesConnect = "AbpStudio.Kubernetes.Connect"; public const string AbpStudioKubernetesIntercept = "AbpStudio.Kubernetes.Intercept";