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