diff --git a/Directory.Packages.props b/Directory.Packages.props index b6286d6657..0a9142ad2c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -167,6 +167,7 @@ + @@ -183,5 +184,7 @@ + + - + \ No newline at end of file diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index ca471bb304..01cd566536 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -485,6 +485,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny.Tests", "test\Volo.Abp.BlobStoring.Bunny.Tests\Volo.Abp.BlobStoring.Bunny.Tests.csproj", "{BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Telemetry", "src\Volo.Abp.Telemetry\Volo.Abp.Telemetry.csproj", "{C71F0E34-A7D8-49D0-B9FB-8B617CD1FC95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Telemetry.Tests", "test\Volo.Abp.Telemetry.Tests\Volo.Abp.Telemetry.Tests.csproj", "{C4E7CFFE-6770-4A92-928C-FC6E91EA6296}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1447,6 +1451,14 @@ Global {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Release|Any CPU.Build.0 = Release|Any CPU + {C71F0E34-A7D8-49D0-B9FB-8B617CD1FC95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C71F0E34-A7D8-49D0-B9FB-8B617CD1FC95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C71F0E34-A7D8-49D0-B9FB-8B617CD1FC95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C71F0E34-A7D8-49D0-B9FB-8B617CD1FC95}.Release|Any CPU.Build.0 = Release|Any CPU + {C4E7CFFE-6770-4A92-928C-FC6E91EA6296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4E7CFFE-6770-4A92-928C-FC6E91EA6296}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4E7CFFE-6770-4A92-928C-FC6E91EA6296}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4E7CFFE-6770-4A92-928C-FC6E91EA6296}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1691,6 +1703,8 @@ Global {C753DDD6-5699-45F8-8669-08CE0BB816DE} = {447C8A77-E5F0-4538-8687-7383196D04EA} {1BBCBA72-CDB6-4882-96EE-D4CD149433A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {C71F0E34-A7D8-49D0-B9FB-8B617CD1FC95} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {C4E7CFFE-6770-4A92-928C-FC6E91EA6296} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliUrls.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliUrls.cs index 35ae34b895..bb9621b91f 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliUrls.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliUrls.cs @@ -7,6 +7,7 @@ public static class CliUrls { public const string WwwAbpIo = WwwAbpIoProduction; public const string AccountAbpIo = AccountAbpIoProduction; + public const string TelemetryAbpIo = TelemetryAbpIoDevelopment; public const string NuGetRootPath = NuGetRootPathProduction; public const string LatestVersionCheckFullPath = "https://raw.githubusercontent.com/abpframework/abp/dev/latest-versions.json"; @@ -14,10 +15,12 @@ public static class CliUrls public const string WwwAbpIoProduction = "https://abp.io/"; public const string AccountAbpIoProduction = "https://account.abp.io/"; public const string NuGetRootPathProduction = "https://nuget.abp.io/"; + public const string TelemetryAbpIoProduction = "https://telemetry.abp.io/"; public const string WwwAbpIoDevelopment = "https://localhost:44328/"; public const string AccountAbpIoDevelopment = "https://localhost:44333/"; public const string NuGetRootPathDevelopment = "https://localhost:44373/"; + public const string TelemetryAbpIoDevelopment = "https://localhost:44393/"; public static string GetNuGetServiceIndexUrl(string apiKey) { diff --git a/framework/src/Volo.Abp.Telemetry/AbpTelemetryModule.cs b/framework/src/Volo.Abp.Telemetry/AbpTelemetryModule.cs new file mode 100644 index 0000000000..6d571d2b11 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/AbpTelemetryModule.cs @@ -0,0 +1,48 @@ +using System; +using System.IO.Abstractions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Application; +using Volo.Abp.Autofac; +using Volo.Abp.Domain; +using Volo.Abp.Modularity; +using Volo.Abp.Telemetry.Activity; +using Volo.Abp.Telemetry.Helpers; + +namespace Volo.Abp.Telemetry; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpDddDomainModule), + typeof(AbpDddApplicationContractsModule) +)] +public class AbpTelemetryModule : AbpModule +{ + + public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var packageMetadata = AbpPackageMetadataHelper.GetMetaData(Assembly.GetCallingAssembly()); + + if (packageMetadata != null) + { + var activityStorage = context.ServiceProvider.GetRequiredService(); + var lastApplicationInfoSendTime = await activityStorage.GetApplicationInfoLastActivitySendTimeAsync(packageMetadata.ProjectId.To()); + if (lastApplicationInfoSendTime is null || + DateTimeOffset.UtcNow - lastApplicationInfoSendTime > TimeSpan.FromDays(7)) + { + var telemetryService = context.ServiceProvider.GetRequiredService(); + + await using var _ = telemetryService.TrackActivity(ActivityNameConsts.ApplicationRun, c => + { + c.Add("ProjectAssemblyForScan", Assembly.GetCallingAssembly()); + c.Add("ProjectId", packageMetadata.ProjectId); + c.Add("ProjectType", packageMetadata.Role); + }); + } + + } + } + +} + diff --git a/framework/src/Volo.Abp.Telemetry/Activity/ActivityData.cs b/framework/src/Volo.Abp.Telemetry/Activity/ActivityData.cs new file mode 100644 index 0000000000..f06e8a2870 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/Activity/ActivityData.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using System.Reflection; + +namespace Volo.Abp.Telemetry.Activity; + +public class ActivityData : Dictionary +{ + public string ActivityName + { + get => (string) this[nameof(ActivityName)]; + init => this[nameof(ActivityName)] = value; + } + + public string? ActivityDetails + { + get => (string?) this[nameof(ActivityDetails)]; + internal set + { + if (value is not null) this[nameof(ActivityDetails)] = value; + } + } + + public long? ActivityDuration + { + get => (long?) this[nameof(ActivityDuration)]; + internal set + { + if (value is not null) this[nameof(ActivityDuration)] = value; + } + } + + public DateTimeOffset Time = DateTimeOffset.UtcNow; + + public ActivityData(string activityName, string? details = null) + { + if (activityName.IsNullOrWhiteSpace()) + throw new ArgumentNullException(nameof(activityName)); + + ActivityName = activityName; + ActivityDetails = details; + } +} + +public static class ActivityPropertyNameConstants +{ + public const string SolutionId = nameof(SolutionId); + public const string SessionId = nameof(SessionId); + public const string ActivityDuration = nameof(ActivityDuration); + public const string OperationSystem = nameof(OperationSystem); + public const string DeviceType = nameof(DeviceType); + public const string DeviceLanguage = nameof(DeviceLanguage); +} + +public static class ActivityDataExtensions +{ + public static IAsyncDisposable BeginActivity(this ActivityData activity, ITelemetryService telemetryService) + { + var stopwatch = Stopwatch.StartNew(); + return new AsyncDisposeFunc(async () => + { + stopwatch.Stop(); + activity.ActivityDuration = stopwatch.ElapsedMilliseconds; + await telemetryService.AddActivityAsync(activity); + }); + } +} + +public class ApplicationRunActivityData : ActivityData +{ + public Assembly ProjectAssemblyForScan { get; set; } + public ApplicationRunActivityData(Assembly projectAssemblyForScan) : base(ActivityNameConsts.ApplicationRun) + { + ProjectAssemblyForScan = projectAssemblyForScan; + } + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/Activity/ActivityDataProvider.cs b/framework/src/Volo.Abp.Telemetry/Activity/ActivityDataProvider.cs new file mode 100644 index 0000000000..cbf20227a3 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/Activity/ActivityDataProvider.cs @@ -0,0 +1,205 @@ +using System.Reflection; +using Newtonsoft.Json.Linq; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Telemetry.EnvironmentInspection; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; +using DatabaseProvider = Volo.Abp.Telemetry.EnvironmentInspection.DatabaseProvider; +using MobileApp = Volo.Abp.Telemetry.EnvironmentInspection.MobileApp; + +namespace Volo.Abp.Telemetry.Activity; + +public class ActivityDataProvider : IActivityDataProvider +{ + private IActivityStorage _activityStorage; + public IAbpLazyServiceProvider ServiceProvider { protected get; set; } + + public virtual async Task AddExtraInformationAsync(ActivityData activityData, + CancellationToken cancellationToken = default) + { + _activityStorage = ServiceProvider.LazyGetRequiredService(); + try + { + + var (isFirstSession, sessionId) = await _activityStorage.GetOrCreateSessionInfoAsync(cancellationToken); + + activityData.Add("SessionId", sessionId); + activityData.Add("IsFirstSession", isFirstSession); + await AddDeviceInformationAsync(activityData); + + AddApplicationInformation(activityData); + + await AddSolutionInformationAsync(activityData); + + return activityData; + } + catch + { + return activityData; + } + } + + private async Task AddDeviceInformationAsync(ActivityData activityData) + { + var deviceInfoProvider = ServiceProvider.LazyGetRequiredService(); + var softwareInfoProvider = ServiceProvider.LazyGetRequiredService(); + + var lastDeviceInfoSendTime = await _activityStorage.GetLastDeviceInfoSendTimeAsync(); + activityData.Add("DeviceId", await deviceInfoProvider.GetDeviceIdAsync()); + + if (lastDeviceInfoSendTime is null || DateTimeOffset.UtcNow - lastDeviceInfoSendTime > TimeSpan.FromDays(7)) + { + activityData.Add("DeviceType", deviceInfoProvider.GetDeviceType()); + activityData.Add("DeviceLanguage", deviceInfoProvider.GetLanguage()); + activityData.Add("OperatingSystem", deviceInfoProvider.GetOperatingSystem()); + activityData.Add("Country", deviceInfoProvider.GetCountry()); + + var softwareList = await softwareInfoProvider.GetSoftwareInfoAsync(); + activityData.Add("InstalledSoftwares" , softwareList); + await _activityStorage.MarkDeviceInfoAsSentAsync(); + } + } + + private void AddApplicationInformation(ActivityData activityData) + { + if (activityData.ContainsKey("ProjectAssemblyForScan")) + { + var assembly = activityData["ProjectAssemblyForScan"] as Assembly; + if (assembly != null) + { + var applicationInfoProvider = ServiceProvider.LazyGetRequiredService(); + var info = applicationInfoProvider.Scan(); + + activityData.Add(nameof(Telemetry.ApplicationInfo.ControllerCount), info.ControllerCount); + activityData.Add(nameof(Telemetry.ApplicationInfo.EntityCount), info.EntityCount); + activityData.Add(nameof(Telemetry.ApplicationInfo.AbpModuleCount), info.AbpModuleCount); + activityData.Add(nameof(Telemetry.ApplicationInfo.PermissionCount), info.PermissionCount); + activityData.Add(nameof(Telemetry.ApplicationInfo.AppServiceCount), info.AppServiceCount); + activityData.Add("ProjectType", activityData["ProjectType"]); + activityData.Add("ProjectId", activityData["ProjectId"]); + + activityData.Remove("ProjectAssemblyForScan"); + } + } + } + + private async Task AddSolutionInformationAsync(ActivityData activityData) + { + if (activityData.TryGetValue("SolutionPath", out var value)) + { + var solutionPath = value as string; + + var rootJObject = JObject.Parse(await File.ReadAllTextAsync(solutionPath)); + var solutionId = rootJObject["id"]!.To(); + var lastSolutionInfoSendTime = + await _activityStorage.GetLastSolutionInfoSendTimeAsync(solutionId); + activityData.Add("SolutionId", solutionId); + + if (lastSolutionInfoSendTime is null || + DateTimeOffset.UtcNow - lastSolutionInfoSendTime > TimeSpan.FromDays(7)) + { + var infoJObject = (JObject) rootJObject["creatingStudioConfiguration"]!; + activityData.Add("Template", GetSolutionTemplate(infoJObject["template"]?.ToString())); + activityData.Add("CreatedAbpStudioVersion", infoJObject["createdAbpStudioVersion"]?.ToString()); + activityData.Add("IsTiered", infoJObject.Value("Tiered")); + activityData.Add("UiFramework", GetUiFramework(infoJObject["uiFramework"]?.ToString())); + activityData.Add("DatabaseProvider", GetDatabaseProvider(infoJObject["databaseProvider"]?.ToString())); + activityData.Add("DatabaseManagementSystem", GetDbms(infoJObject["databaseManagementSystem"]?.ToString())); + activityData.Add("IsSeparateTenantSchema", infoJObject.Value("separateTenantSchema")); + activityData.Add("Theme", GetUiTheme(infoJObject["theme"]?.ToString())); + activityData.Add("ThemeStyle", GetUiThemeStyle(infoJObject["themeStyle"]?.ToString())); + activityData.Add("MobileFramework", GetMobileApp(infoJObject["mobileFramework"]?.ToString())); + activityData.Add("HasPublicWebsite", infoJObject.Value("publicWebsite")); + activityData.Add("IncludeTests", infoJObject.Value("includeTests")); + activityData.Add("MultiTenancy", infoJObject.Value("multiTenancy")); + activityData.Add("DynamicLocalization", infoJObject.Value("dynamicLocalization")); + activityData.Add("KubernetesConfiguration", infoJObject.Value("kubernetesConfiguration")); + activityData.Add("GrafanaDashboard", infoJObject.Value("grafanaDashboard")); + activityData.Add("SocialLogins", infoJObject.Value("socialLogin")); + } + } + } + private Dbms GetDbms(string? databaseManagementSystem) + { + return databaseManagementSystem switch + { + "mysql" => Dbms.MySql, + "oracle" => Dbms.Oracle, + "oracle-devart" => Dbms.OracleDevart, + "postgresql" => Dbms.PostgreSql, + "sqlserver" => Dbms.SqlServer, + "sqlite" => Dbms.Sqlite, + _ => Dbms.Unknown + }; + } + + private UiTheme GetUiTheme(string? theme) + { + return theme switch + { + "none" => UiTheme.None, + "basic" => UiTheme.Basic, + "leptonx" => UiTheme.LeptonX, + "leptonx-lite" => UiTheme.LeptonXLite, + _ => UiTheme.Unknown + }; + } + + private MobileApp GetMobileApp(string? mobileFramework) + { + return mobileFramework switch + { + "maui" => MobileApp.Maui, + "react-native" => MobileApp.ReactNative, + "none" => MobileApp.None, + _ => MobileApp.Unknown + }; + } + + private UiThemeStyle GetUiThemeStyle(string? themeStyle) + { + return themeStyle switch + { + "dim" => UiThemeStyle.Dim, + "style" => UiThemeStyle.System, + "dark" => UiThemeStyle.Dark, + "light" => UiThemeStyle.Light, + _ => UiThemeStyle.Unknown + }; + } + private SolutionTemplate GetSolutionTemplate(string? templateName) + { + return templateName switch + { + "microservice" => SolutionTemplate.Microservice, + "app-nolayers" => SolutionTemplate.AppNoLayers, + "app" => SolutionTemplate.AppLayered, + _ => SolutionTemplate.Unknown + }; + } + + private UiFramework GetUiFramework(string? uiFramework) + { + return uiFramework switch + { + "mvc" => UiFramework.MvcRazorPages, + "blazor" => UiFramework.BlazorWasm, + "angular" => UiFramework.Angular, + "blazor-server" => UiFramework.BlazorServer, + "blazor-webapp" => UiFramework.BlazorWebApp, + "maui-blazor" => UiFramework.BlazorMaUI, + "none" => UiFramework.None, + _ => UiFramework.Unknown, + }; + } + + private DatabaseProvider GetDatabaseProvider(string? databaseProvider) + { + return databaseProvider switch + { + "ef" => DatabaseProvider.EfCore, + "mongodb" => DatabaseProvider.MongoDb, + "none" => DatabaseProvider.None, + _ => DatabaseProvider.Unknown + }; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/Activity/ActivityStorage.cs b/framework/src/Volo.Abp.Telemetry/Activity/ActivityStorage.cs new file mode 100644 index 0000000000..2c4cfeb473 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/Activity/ActivityStorage.cs @@ -0,0 +1,215 @@ +using System.Text; +using Volo.Abp.Cli; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace Volo.Abp.Telemetry.Activity; + +public class ActivityStorage : IActivityStorage, ISingletonDependency +{ + private readonly IJsonSerializer _jsonSerializer; + private readonly string _filePath; + private ActivityStorageState? _cachedState; + + public ActivityStorage(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + _filePath = Path.Combine(CliPaths.AbpRootPath, "activity-storage.json"); + } + + public async Task BufferActivityAsync(ActivityData activityData, CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + state.Activities.Add(activityData); + await SaveAsync(); + } + + public async Task> GetBufferedActivitiesAsync(CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + return state.Activities; + } + + public async Task RemoveActivityAsync(string activityName, CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + + var existing = state.Activities + .FirstOrDefault(x => string.Equals(x.ActivityName, activityName, StringComparison.OrdinalIgnoreCase)); + + if (existing is not null) + { + state.Activities.Remove(existing); + await SaveAsync(); + } + } + + public async Task MarkApplicationInfoAsSentAsync(Guid applicationId, + CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + state.ApplicationInfos[applicationId] = DateTimeOffset.UtcNow; + await SaveAsync(); + } + + public async Task GetApplicationInfoLastActivitySendTimeAsync(Guid applicationId, + CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + if (state.ApplicationInfos.TryGetValue(applicationId, out var lastActivitySendTime)) + { + return lastActivitySendTime; + } + + return null; + } + + public async Task GetLastActivitySendTimeAsync(CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + return state.ActivitySendTime; + } + + public async Task GetSessionIdAsync(CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + return state.SessionId ?? null; + } + + public async Task<(bool isFirstSession, Guid sessionId)> GetOrCreateSessionInfoAsync( + CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + state.IsFirstSession ??= true; + state.SessionId ??= Guid.NewGuid(); + await SaveAsync(); + return (state.IsFirstSession.Value, state.SessionId.Value); + } + + public async Task SetSessionIdAsync(Guid sessionId, bool isFirstSession = false, + CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + state.SessionId = sessionId; + state.IsFirstSession = isFirstSession; + await SaveAsync(); + } + + public async Task MarkActivitiesAsSentAsync(CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + state.ActivitySendTime = DateTimeOffset.UtcNow; + state.Activities.Clear(); + state.SessionId = null; + + await SaveAsync(); + } + + public async Task GetLastSolutionInfoSendTimeAsync(Guid id, + CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + + if (state.Solutions.TryGetValue(id, out var date)) + { + return date; + } + + return null; + } + + public async Task GetLastDeviceInfoSendTimeAsync(CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + return state.LastDeviceInfoSendTime; + } + + public async Task MarkSolutionInfoAsSentAsync(Guid id, CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + state.Solutions[id] = DateTimeOffset.UtcNow; + await SaveAsync(); + } + + public async Task MarkDeviceInfoAsSentAsync(CancellationToken cancellationToken = default) + { + var state = await GetStateAsync(); + state.LastDeviceInfoSendTime = DateTimeOffset.UtcNow; + await SaveAsync(); + } + + private async Task GetStateAsync() + { + if (_cachedState != null) + return _cachedState; + + await EnsureFileExistsAsync(); + _cachedState = await WithExclusiveFileLockAsync(async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true); + var json = await reader.ReadToEndAsync(); + return _jsonSerializer.Deserialize(json) ?? new ActivityStorageState(); + }); + return _cachedState; + } + + private async Task WithExclusiveFileLockAsync(Func> action) + { + const int maxRetries = 10; + const int retryDelayMs = 100; + + for (int i = 0; i < maxRetries; i++) + { + try + { + await using var stream = new FileStream( + _filePath, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.None + ); + + return await action(stream); + } + catch (IOException) + { + if (i == maxRetries - 1) + throw; + + await Task.Delay(retryDelayMs); + } + } + + throw new IOException("Unable to acquire file lock for ActivityStorage."); + } + + private async Task SaveAsync() + { + var json = _jsonSerializer.Serialize(_cachedState ?? new ActivityStorageState(), indented: true); + await File.WriteAllTextAsync(_filePath, json, Encoding.UTF8); + } + + + private async Task EnsureFileExistsAsync() + { + try + { + var directory = Path.GetDirectoryName(_filePath); + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + if (!File.Exists(_filePath)) + { + var json = _jsonSerializer.Serialize(new ActivityStorageState(), indented: true); + await File.WriteAllTextAsync(_filePath, json, Encoding.UTF8); + } + } + catch + { + //ignored + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/Activity/ActivityStorageState.cs b/framework/src/Volo.Abp.Telemetry/Activity/ActivityStorageState.cs new file mode 100644 index 0000000000..6ae57343fe --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/Activity/ActivityStorageState.cs @@ -0,0 +1,40 @@ +namespace Volo.Abp.Telemetry.Activity; + +public class ActivityStorageState +{ + public DateTimeOffset? ActivitySendTime { get; set; } + public DateTimeOffset? LastDeviceInfoSendTime { get; set; } + public Guid? SessionId { get; set; } + public bool? IsFirstSession { get; set; } + public List Activities { get; set; } = new(); + public Dictionary Solutions { get; set; } = new(); + + public Dictionary ApplicationInfos { get; set; } = new(); +} + + + +public class ApplicationInfo +{ + public Guid ApplicationId { get; set; } + public ApplicationType Type { get; set; } + public int EntityCount { get; set; } // Number of entities in the application, to understand its size + public int AppServiceCount { get; set; } // ... + public int ControllerCount { get; set; } // ... + public int PermissionCount { get; set; } // ... + public int AbpModuleCount { get; set; } + +} + +public enum ApplicationType +{ + Unknown = 0, + Console = 1, + MvcUi = 2, + BlazorServerUi = 3, + BlazorWebAssemblyUi = 4, + BlazorWebAppUi = 5, + HttpApi = 6, + AspNetCore = 7 //We could not understand if it is HTTP API or MVC UI or whatever + //TODO: More? +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/Activity/IActivityDataProvider.cs b/framework/src/Volo.Abp.Telemetry/Activity/IActivityDataProvider.cs new file mode 100644 index 0000000000..72c26bdfb8 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/Activity/IActivityDataProvider.cs @@ -0,0 +1,8 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Telemetry.Activity; + +public interface IActivityDataProvider : ITransientDependency +{ + Task AddExtraInformationAsync(ActivityData activityData, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/Activity/IActivityStorage.cs b/framework/src/Volo.Abp.Telemetry/Activity/IActivityStorage.cs new file mode 100644 index 0000000000..4003c564cf --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/Activity/IActivityStorage.cs @@ -0,0 +1,17 @@ +namespace Volo.Abp.Telemetry.Activity; + +public interface IActivityStorage +{ + Task GetLastActivitySendTimeAsync(CancellationToken cancellationToken = default); + Task<(bool isFirstSession, Guid sessionId)> GetOrCreateSessionInfoAsync(CancellationToken cancellationToken = default); + Task MarkActivitiesAsSentAsync(CancellationToken cancellationToken = default); + Task GetLastSolutionInfoSendTimeAsync(Guid solutionId, CancellationToken cancellationToken = default); + Task GetLastDeviceInfoSendTimeAsync(CancellationToken cancellationToken = default); + Task MarkDeviceInfoAsSentAsync(CancellationToken cancellationToken = default); + Task MarkSolutionInfoAsSentAsync(Guid solutionId , CancellationToken cancellationToken = default); + Task BufferActivityAsync(ActivityData activityData, CancellationToken cancellationToken = default); + Task> GetBufferedActivitiesAsync(CancellationToken cancellationToken = default); + Task MarkApplicationInfoAsSentAsync(Guid applicationId, CancellationToken cancellationToken = default); + Task GetApplicationInfoLastActivitySendTimeAsync(Guid applicationId, CancellationToken cancellationToken = default); +} + diff --git a/framework/src/Volo.Abp.Telemetry/ActivityEventHandler.cs b/framework/src/Volo.Abp.Telemetry/ActivityEventHandler.cs new file mode 100644 index 0000000000..4b1666768c --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/ActivityEventHandler.cs @@ -0,0 +1,41 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus; +using Volo.Abp.Telemetry.Activity; + +namespace Volo.Abp.Telemetry; + +public class ActivityEventHandler : ILocalEventHandler, ITransientDependency +{ + private readonly IActivityStorage _activityStorage; + private readonly IActivityDataProvider _activityDataProvider; + private readonly ITelemetryDataSender _telemetryDataSender; + + public ActivityEventHandler(IActivityDataProvider activityDataProvider, IActivityStorage activityStorage, ITelemetryDataSender telemetryDataSender) + { + _activityDataProvider = activityDataProvider; + _activityStorage = activityStorage; + _telemetryDataSender = telemetryDataSender; + } + + public async Task HandleEventAsync(ActivityData eventData) + { + var build = await _activityDataProvider.AddExtraInformationAsync(eventData); + await _activityStorage.BufferActivityAsync(build); + + await CheckIfActivitySendTimeIsDueAsync(); + } + + private async Task CheckIfActivitySendTimeIsDueAsync() + { + var lastActivitySendTime = await _activityStorage.GetLastActivitySendTimeAsync(); + if (lastActivitySendTime is null) + { + await _telemetryDataSender.SendAsync(); + } + + if (lastActivitySendTime is not null && lastActivitySendTime > DateTimeOffset.UtcNow.AddDays(7) ) + { + await _telemetryDataSender.SendAsync(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/ActivityNameConsts.cs b/framework/src/Volo.Abp.Telemetry/ActivityNameConsts.cs new file mode 100644 index 0000000000..ecc4280f52 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/ActivityNameConsts.cs @@ -0,0 +1,74 @@ +namespace Volo.Abp.Telemetry; + + 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 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 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.Telemetry/ApplicationInfo.cs b/framework/src/Volo.Abp.Telemetry/ApplicationInfo.cs new file mode 100644 index 0000000000..553cba0928 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/ApplicationInfo.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.Telemetry; + +public class ApplicationInfo +{ + public int EntityCount { get; set; } + public int AppServiceCount { get; set; } + public int ControllerCount { get; set; } + public int PermissionCount { get; set; } + public int AbpModuleCount { get; set; } + public int ProjectCount { get; set; } // TODO: implement if needed +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/ApplicationInfoProvider.cs b/framework/src/Volo.Abp.Telemetry/ApplicationInfoProvider.cs new file mode 100644 index 0000000000..683084e6c5 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/ApplicationInfoProvider.cs @@ -0,0 +1,36 @@ +using Volo.Abp.Application.Services; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Modularity; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Telemetry; + +public class ApplicationInfoProvider : ISingletonDependency +{ + private readonly ITypeFinder _typeFinder; + + + public ApplicationInfoProvider(ITypeFinder typeFinder) + { + _typeFinder = typeFinder; + } + + public ApplicationInfo Scan() + { + var types = _typeFinder.Types; + + return new ApplicationInfo + { + EntityCount = types.Count(t => typeof(IEntity).IsAssignableFrom(t) && !t.IsAbstract), + AppServiceCount = types.Count(t => typeof(IApplicationService).IsAssignableFrom(t) && !t.IsAbstract), + ControllerCount = types.Count(t => typeof(AbpController).IsAssignableFrom(t) && !t.IsAbstract), + PermissionCount = types.Count(t => typeof(PermissionDefinitionProvider).IsAssignableFrom(t) && !t.IsAbstract), + AbpModuleCount = types.Count(t => typeof(AbpModule).IsAssignableFrom(t) && !t.IsAbstract), + ProjectCount = types.Select(t => t.Assembly.GetName().Name).Distinct().Count() + + }; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs new file mode 100644 index 0000000000..b83d0e56a2 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs @@ -0,0 +1,9 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +internal interface ISoftwareDetector : IScopedDependency +{ + string Name { get; } + Task DetectAsync(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs new file mode 100644 index 0000000000..8867ef859b --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +public interface ISoftwareInfoProvider +{ + Task> GetSoftwareInfoAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs new file mode 100644 index 0000000000..b75edc038a --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +public 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.Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs new file mode 100644 index 0000000000..41220daf90 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json.Linq; +using Volo.Abp.Cli; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class AbpStudioDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => "Abp Studio"; + + public async override Task DetectAsync() + { + try + { + var uiTheme = await GetAbpStudioUiThemeAsync(); + var version = await GetAbpStudioVersionAsync(); + + return new SoftwareInfo(Name, version, uiTheme, SoftwareType.AbpStudio); + } + catch (Exception e) + { + return null; + } + } + + private async Task GetAbpStudioUiThemeAsync() + { + var ideStateJsonPath = Path.Combine( + CliPaths.AbpRootPath, + "studio", + "ui", + "ide-state.json" + ); + + var jObject = JObject.Parse(await File.ReadAllTextAsync(ideStateJsonPath)); + return jObject["theme"]?.Value(); + } + + private async Task GetAbpStudioVersionAsync() + { + var extensionsFilePath = Path.Combine( + CliPaths.AbpRootPath, + "studio", + "extensions.json" + ); + + var jObject = JObject.Parse(await File.ReadAllTextAsync(extensionsFilePath)); + + return jObject["version"]?.Value(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs new file mode 100644 index 0000000000..6f9386ee20 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs @@ -0,0 +1,43 @@ +using System.Runtime.InteropServices; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class ChromeDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => "Chrome"; + + public override async Task DetectAsync() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string 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)) + { + string 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)) + { + string 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.Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs new file mode 100644 index 0000000000..6659e66f0a --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs @@ -0,0 +1,19 @@ +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class DotnetSdkDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => "DotnetSdk"; + + public override async Task DetectAsync() + { + var output = await ExecuteCommandAsync("dotnet", "--version"); + if (output.IsNullOrWhiteSpace()) + { + return null; + } + + return new SoftwareInfo(Name, output, null, SoftwareType.DotnetSdk); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs new file mode 100644 index 0000000000..1183c12967 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class FireFoxDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => "Firefox"; + public override async Task DetectAsync() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string 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)) + { + string 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)) + { + string 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.Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs new file mode 100644 index 0000000000..6f0f9f8ce0 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs @@ -0,0 +1,42 @@ +using System.Runtime.InteropServices; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class MsEdgeDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => "MsEdge"; + public override async Task DetectAsync() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string 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)) + { + string 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)) + { + string 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.Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs new file mode 100644 index 0000000000..d406d0c1e2 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs @@ -0,0 +1,27 @@ +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class NodeJsDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => "Node.js"; + + public override async 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.Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs new file mode 100644 index 0000000000..19d60a383b --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs @@ -0,0 +1,92 @@ +using System.Diagnostics; +using Microsoft.Win32; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class OperatingSystemDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => OperatingSystem.IsWindows() ? "Windows" : OperatingSystem.IsMacOS() ? "macOS" : "Linux"; + + public override async Task DetectAsync() + { + if (OperatingSystem.IsWindows()) + { + return new SoftwareInfo(Name, Environment.OSVersion.Version.ToString(),GetWindowsUiTheme(), SoftwareType.OperatingSystem); + } + + if (OperatingSystem.IsMacOS()) + { + var version = await ExecuteCommandAsync("sw_vers", "-productVersion"); + return new SoftwareInfo(Name, version, GetMacUiTheme(), SoftwareType.OperatingSystem); + } + + if (OperatingSystem.IsLinux()) + { + 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"; + } + } + private string? GetWindowsUiTheme() + { + try + { + const string key = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + const string value = "AppsUseLightTheme"; + + using var registryKey = Registry.CurrentUser.OpenSubKey(key); + var result = registryKey?.GetValue(value); + + return result switch + { + 0 => "Dark", + 1 => "Light", + _ => null + }; + } + catch (Exception e) + { + return null; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs new file mode 100644 index 0000000000..ffd9192ae9 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs @@ -0,0 +1,94 @@ +using System.Runtime.InteropServices; +using System.Xml.Linq; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class RiderDetector : SoftwareDetector, ISoftwareDetector +{ + 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 = "Unknown"; + 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 (!string.IsNullOrEmpty(schemeName)) + { + theme = schemeName.IndexOf("dark", StringComparison.OrdinalIgnoreCase) >= 0 + ? "Dark" + : "Light"; + } + } + catch + { + // Hata olursa Unknown bırakılsın + } + } + + 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.Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs new file mode 100644 index 0000000000..a0646a751f --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs @@ -0,0 +1,118 @@ +using System.Runtime.InteropServices; +using System.Text.Json; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class VisualStudioCodeDetector : SoftwareDetector, ISoftwareDetector +{ + 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(await File.ReadAllTextAsync(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.Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs new file mode 100644 index 0000000000..cae9becb66 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection.Detectors; + +internal class VisualStudioDetector : SoftwareDetector, ISoftwareDetector +{ + public override string Name => "Visual Studio"; + + public async override Task DetectAsync() + { + return null; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/SoftwareDetector.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/SoftwareDetector.cs new file mode 100644 index 0000000000..b080de197d --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/SoftwareDetector.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; +using CliWrap; +using CliWrap.Buffered; + +namespace Volo.Abp.Telemetry.EnvironmentInspection; + +abstract internal class SoftwareDetector : ISoftwareDetector +{ + public abstract string Name { get; } + public abstract Task DetectAsync(); + + protected async Task ExecuteCommandAsync(string command, string? arg) + { + var result = await CliWrap.Cli.Wrap(command) + .WithArguments(arg) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(); + + return result.StandardOutput.IsNullOrEmpty() ? null : result.StandardOutput.Trim(); + } + + protected static 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.Telemetry/EnvironmentInspection/SoftwareInfoProvider.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/SoftwareInfoProvider.cs new file mode 100644 index 0000000000..47f8d31770 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/SoftwareInfoProvider.cs @@ -0,0 +1,37 @@ +using Volo.Abp.Telemetry.EnvironmentInspection.Contracts; + +namespace Volo.Abp.Telemetry.EnvironmentInspection; + +internal class SoftwareInfoProvider : ISoftwareInfoProvider +{ + private readonly IEnumerable _softwareDetectors; + + public SoftwareInfoProvider(IEnumerable softwareDetectors) + { + _softwareDetectors = softwareDetectors; + } + + public async Task> GetSoftwareInfoAsync(CancellationToken cancellationToken = default) + { + 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.Telemetry/EnvironmentInspection/SoftwareType.cs b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/SoftwareType.cs new file mode 100644 index 0000000000..7607f93259 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/EnvironmentInspection/SoftwareType.cs @@ -0,0 +1,251 @@ +using System.Diagnostics; +using System.Globalization; +using System.Management; +using System.Runtime.InteropServices; +using Volo.Abp.Cli; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Telemetry.EnvironmentInspection; + + +public enum MobileApp +{ + Unknown = 0, + None = 1, + Maui = 2, + ReactNative = 3 + +} + + +public enum SolutionTemplate +{ + Unknown, + AppNoLayers, + AppLayered, + Microservice +} + +public enum UiFramework +{ + Unknown = 0, + None = 1, + MvcRazorPages = 2, + Angular = 3, + BlazorWasm = 4, + BlazorServer = 5, + BlazorWebApp = 6, + BlazorMaUI = 7, +} + +public enum LicenseType +{ + Unknown, + Free, + Team, + Business, + Enterprise +} +public enum DatabaseProvider +{ + Unknown = 0, + None = 1, + EfCore = 2, + MongoDb = 3 +} + +public enum Dbms +{ + Unknown = 0, + None = 1, + SqlServer = 2, + PostgreSql = 3, + Oracle = 4, + OracleDevart = 5, + MySql = 6, + Sqlite = 7, +} +public enum UiTheme +{ + Unknown = 0, + None = 1, + Basic = 2, + LeptonX = 3, + LeptonXLite = 4 +} + +public enum UiThemeStyle +{ + Unknown = 0, + System = 1, + Dim = 2, + Dark = 3, + Light = 4 +} + + +public enum AbpTool : byte +{ + Unknown = 0, + StudioUI = 1, + StudioCli = 2, + OldCli = 3 +} +public enum SoftwareType : byte +{ + Others = 0, + AbpStudio = 1, + DotnetSdk = 2, + OperatingSystem = 3, + Ide = 4, + Browser = 5 +} + +public enum OperationSystem +{ + Unknown = 0, + Windows = 1, + MacOS = 2, + Linux = 3, +} + +public enum DeviceType +{ + Unknown = 0, + Desktop = 1, + Laptop = 2 +} + +public interface IDeviceInfoProvider +{ + Task GetDeviceIdAsync(); + OperationSystem GetOperatingSystem(); + DeviceType GetDeviceType(); + string GetLanguage(); + string? GetCountry(); +} + +public class DeviceInfoProvider : IDeviceInfoProvider , ISingletonDependency +{ + public async Task GetDeviceIdAsync() + { + var deviceId = + await File.ReadAllTextAsync(Path.Combine(CliPaths.AbpRootPath, "cli", "computer-id.bin")); + return deviceId.To(); + } + + public OperationSystem GetOperatingSystem() + { + if (OperatingSystem.IsWindows()) return OperationSystem.Windows; + if (OperatingSystem.IsLinux()) return OperationSystem.Linux; + if (OperatingSystem.IsMacOS()) return OperationSystem.MacOS; + return OperationSystem.Unknown; + } + + public DeviceType GetDeviceType() + { + if (OperatingSystem.IsWindows()) + { + return DetectDeviceTypeOnWindows(); + } + + if (OperatingSystem.IsLinux()) + { + return DetectDeviceTypeOnLinux(); + } + + if (OperatingSystem.IsMacOS()) + { + return DetectDeviceTypeOnMacOS(); + } + + if (RuntimeInformation.OSDescription.Contains("iOS", StringComparison.OrdinalIgnoreCase)) + { + return DeviceType.Laptop; + } + + return DeviceType.Unknown; + } + + public string GetLanguage() + { + return CultureInfo.CurrentUICulture.Name; + } + + public string? GetCountry() + { + try + { + var region = new RegionInfo(CultureInfo.CurrentUICulture.Name); + return region.TwoLetterISORegionName; + } + catch + { + return null; + } + } + + private DeviceType DetectDeviceTypeOnWindows() + { + try + { + using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_SystemEnclosure"); + foreach (var obj in searcher.Get()) + { + var chassisTypes = obj["ChassisTypes"] as ushort[]; + if (chassisTypes != null && chassisTypes.Any(t => t is 8 or 9 or 10 or 14)) + return DeviceType.Laptop; + } + } + catch { } + + return DeviceType.Desktop; + } + + private DeviceType DetectDeviceTypeOnLinux() + { + try + { + var type = File.ReadAllText("/sys/class/dmi/id/chassis_type"); + if (int.TryParse(type.Trim(), out var code) && code is 8 or 9 or 10 or 14) + return DeviceType.Laptop; + } + catch + { + // ignored + } + + return DeviceType.Desktop; + } + + private DeviceType DetectDeviceTypeOnMacOS() + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "system_profiler", + Arguments = "SPHardwareDataType", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (output.Contains("MacBook")) return DeviceType.Laptop; + if (output.Contains("iMac") || output.Contains("Mac Pro")) return DeviceType.Desktop; + } + catch + { + // ignored + } + + return DeviceType.Unknown; + } +} diff --git a/framework/src/Volo.Abp.Telemetry/Helpers/AbpPackageMetadataHelper.cs b/framework/src/Volo.Abp.Telemetry/Helpers/AbpPackageMetadataHelper.cs new file mode 100644 index 0000000000..88ef306309 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/Helpers/AbpPackageMetadataHelper.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using System.Text.Json; + +namespace Volo.Abp.Telemetry.Helpers; + +public static class AbpPackageMetadataHelper +{ + public static AbpPackageMetadata? GetMetaData(Assembly assembly) + { + var assemblyPath = assembly.Location; + + if (string.IsNullOrWhiteSpace(assemblyPath)) + return null; + + var dir = Path.GetDirectoryName(assemblyPath); + if (dir == null) + return null; + + var abppkgPath = Directory.GetFiles(dir, "*.abppkg").FirstOrDefault(); + if (abppkgPath == null) + return null; + + return ReadOrCreateMetadata(abppkgPath); + } + + private static AbpPackageMetadata ReadOrCreateMetadata(string path) + { + var json = File.ReadAllText(path); + var doc = JsonSerializer.Deserialize>(json) ?? new(); + + var metadata = new AbpPackageMetadata(); + + if (doc.TryGetValue("projectId", out var existingProjectId) && existingProjectId is string id && + !string.IsNullOrWhiteSpace(id)) + { + metadata.ProjectId = id; + } + else + { + metadata.ProjectId = Guid.NewGuid().ToString(); + doc["projectId"] = metadata.ProjectId; + } + + // Role + if (doc.TryGetValue("role", out var existingRole) && existingRole is string role) + { + metadata.Role = role; + } + + // Save if new ProjectId was generated + var updatedJson = JsonSerializer.Serialize(doc, new JsonSerializerOptions {WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); + File.WriteAllText(path, updatedJson); + + return metadata; + } +} + +public class AbpPackageMetadata +{ + public string ProjectId { get; set; } + public string Role { get; set; } + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/IApplicationScanner.cs b/framework/src/Volo.Abp.Telemetry/IApplicationScanner.cs new file mode 100644 index 0000000000..ad18d4f4c3 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/IApplicationScanner.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Telemetry; + +public interface IApplicationScanner +{ + ApplicationInfo Scan(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/ITelemetryDataSender.cs b/framework/src/Volo.Abp.Telemetry/ITelemetryDataSender.cs new file mode 100644 index 0000000000..751ee0f9db --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/ITelemetryDataSender.cs @@ -0,0 +1,8 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Telemetry; + +public interface ITelemetryDataSender : IScopedDependency +{ + Task SendAsync(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/ITelemetryService.cs b/framework/src/Volo.Abp.Telemetry/ITelemetryService.cs new file mode 100644 index 0000000000..c4e4f33517 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/ITelemetryService.cs @@ -0,0 +1,12 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.Telemetry.Activity; + +namespace Volo.Abp.Telemetry; + +public interface ITelemetryService : IScopedDependency +{ + IAsyncDisposable TrackActivity(string activityName, Action? configure = null); + IAsyncDisposable TrackActivity(ActivityData activityData); + Task AddActivityAsync(ActivityData data, CancellationToken cancellationToken = default); + Task AddActivityAsync(string activityName, string? details = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/TelemetryDataSender.cs b/framework/src/Volo.Abp.Telemetry/TelemetryDataSender.cs new file mode 100644 index 0000000000..fd95dc9e92 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/TelemetryDataSender.cs @@ -0,0 +1,47 @@ +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using Volo.Abp.Cli; +using Volo.Abp.Cli.Http; +using Volo.Abp.Telemetry.Activity; + +namespace Volo.Abp.Telemetry; + +public class TelemetryDataSender : ITelemetryDataSender +{ + private readonly CliHttpClientFactory _cliHttpClientFactory; + private readonly IActivityStorage _activityStorage; + private readonly IActivityDataProvider _activityDataProvider; + + public TelemetryDataSender(CliHttpClientFactory cliHttpClientFactory, IActivityStorage activityStorage, IActivityDataProvider activityDataProvider) + { + _cliHttpClientFactory = cliHttpClientFactory; + _activityStorage = activityStorage; + _activityDataProvider = activityDataProvider; + } + + public async Task SendAsync() + { + using var client = _cliHttpClientFactory.CreateClient(); + client.AddAbpAuthenticationToken(); + + var activities = await _activityStorage.GetBufferedActivitiesAsync(); + + if (activities.Count > 0) + { + foreach (var activity in activities) + { + await _activityDataProvider.AddExtraInformationAsync(activity); + await client.PostAsync(CliUrls.TelemetryAbpIo, + new StringContent(JsonSerializer.Serialize(activity), Encoding.UTF8, + MediaTypeNames.Application.Json)); + + } + + await _activityStorage.MarkActivitiesAsSentAsync(); + } + + + } + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Telemetry/TelemetryService.cs b/framework/src/Volo.Abp.Telemetry/TelemetryService.cs new file mode 100644 index 0000000000..71410b53a9 --- /dev/null +++ b/framework/src/Volo.Abp.Telemetry/TelemetryService.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using Volo.Abp.Application.Services; +using Volo.Abp.EventBus.Local; +using Volo.Abp.Telemetry.Activity; + +namespace Volo.Abp.Telemetry; + +public class TelemetryService : ApplicationService, ITelemetryService +{ + + private readonly ILocalEventBus _localEventBus; + public TelemetryService( ILocalEventBus localEventBus) + { + _localEventBus = localEventBus; + } + + + public IAsyncDisposable TrackActivity(string activityName, Action? configure = null) + { + var stopwatch = Stopwatch.StartNew(); + var activityData = new ActivityData(activityName) + { + Time = DateTimeOffset.UtcNow + }; + + configure?.Invoke(activityData); + + return new AsyncDisposeFunc(async () => + { + stopwatch.Stop(); + activityData.ActivityDuration = stopwatch.ElapsedMilliseconds; + + await AddActivityAsync(activityData); + }); + } + + public IAsyncDisposable TrackActivity(ActivityData activityData) + { + var stopwatch = Stopwatch.StartNew(); + activityData.Time = DateTimeOffset.UtcNow; + + return new AsyncDisposeFunc(async () => + { + stopwatch.Stop(); + activityData.ActivityDuration = stopwatch.ElapsedMilliseconds; + await AddActivityAsync(activityData); + }); + } + + + + public async Task AddActivityAsync(ActivityData data, CancellationToken cancellationToken = default) + { + await _localEventBus.PublishAsync(data); + } + + public async Task AddActivityAsync(string activityName, string? details = null, CancellationToken cancellationToken = default) + { + await AddActivityAsync(new ActivityData(activityName, details),cancellationToken); + } +} \ No newline at end of file