From c07ffb68fc987a758160a5aa44128dbd3c00d3cb Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:06:10 +0200 Subject: [PATCH] v9: Implement telemetry levels (#12267) * Add initial classes * Add TelemetryProviders * Add new NodeCountService.cs and NodeTelemetryProvider * Add data contract attribute to UsageInformation Otherwise it wont serialize correctly * Implement more providers * Fix builders and propertyEditorTelemetry * Add MediaTelemetryProvider * Add MediaTelemetryProvider * Fix doubling of media telemetry * Move contentCount from NodeCountTelemetryProvider and move to ContentTelemetryProvider * Revert ContentTelemetryProvider changes * Add detailed information to TelemetryService * Add integration tests * Add more tests and todos for tests * Fix stylecop warnings * Use yield return instead of instantiating local list * Implement Macro test * Inject interface instead of implementation in TelemetryService * Fix TelemetryServiceTests.cs * Implement media tests * Implement propertyTypeTests * Implement constants instead of hardcoded strings * Add SystemInformationTelemetryProvider * Use SystemInformationTableDataProvider in UserDataService * Implement more properties * Add UsageInformation * Replace UserDataService with SystemInformationTelemetryProvider * Undo changes to UserDataService and obsolete it * Remove ISystemInformationTableDataProvider * Register SystemInformationTelemetryProvider as telemetry provider * Use constants for telemetry names * Make UserDataServiceTests test SystemInformationTelemetryProvider instead * Update UserDataServiceTests to cover new data * Add unit tests * Add integration test testing expected data is returned * Implement Analytics dashboard * Improve assertion message * Add text and styling to analyticspage * Rename consent to analytic * implement save button for consent level * Implement save button * Fix system information test * Add TelemetryResource * Move telemetry providers to infrastructure * Add database provider to system information * Set startvalue for slider * Fix unit tests * Implement MetricsConsentService using KeyValueService * Return void hen setting the telemetry level * fix startposition when not reloading * Add a couple tests * Update src/Umbraco.Core/Services/MetricsConsentService.cs * Rename ConsentLevel.cs * Use direct Enum instead of parsing * rename consent resource * add lazy database * refactor slider * Implement ng-if and propers pips * Make classes internal * Fix slider not loading when navigating to tab * Add telemetry level check to TelemetryService.cs * Add Consent for analytics text * Fix build errors for unit tests * Fix TelemetryServiceTests * revert package-lock.json * Fix integration test * Update slider * Update TelemetryService.cs * Apply suggestions from code review Co-authored-by: Mole Co-authored-by: Nikolaj Geisle Co-authored-by: nikolajlauridsen --- .../Configuration/Models/GlobalSettings.cs | 2 +- src/Umbraco.Core/Constants-System.cs | 4 +- src/Umbraco.Core/Constants-Telemetry.cs | 32 ++ .../Dashboards/AnalyticsDashboard.cs | 15 + .../DependencyInjection/UmbracoBuilder.cs | 5 +- src/Umbraco.Core/Models/TelemetryLevel.cs | 12 + src/Umbraco.Core/Models/TelemetryResource.cs | 11 + src/Umbraco.Core/Models/UsageInformation.cs | 20 + .../Services/IExamineIndexCountService.cs | 7 + .../Services/IMetricsConsentService.cs | 11 + .../Services/INodeCountService.cs | 10 + .../Services/IUsageInformationService.cs | 10 + .../Services/MetricsConsentService.cs | 34 ++ src/Umbraco.Core/Services/UserDataService.cs | 5 +- .../Telemetry/Models/TelemetryReportData.cs | 4 + .../Telemetry/TelemetryService.cs | 28 +- .../UmbracoBuilder.CoreServices.cs | 2 + .../UmbracoBuilder.Services.cs | 4 + .../UmbracoBuilder.TelemetryProviders.cs | 25 ++ .../Implement/ExamineIndexCountService.cs | 21 ++ .../Services/Implement/NodeCountService.cs | 51 +++ .../Interfaces/IDetailedTelemetryProvider.cs | 10 + .../Providers/ContentTelemetryProvider.cs | 23 ++ .../Providers/DomainTelemetryProvider.cs | 22 ++ .../Providers/ExamineTelemetryProvider.cs | 21 ++ .../Providers/LanguagesTelemetryProvider.cs | 25 ++ .../Providers/MacroTelemetryProvider.cs | 25 ++ .../Providers/MediaTelemetryProvider.cs | 20 + .../Providers/NodeCountTelemetryProvider.cs | 24 ++ .../PropertyEditorTelemetryProvider.cs | 28 ++ .../SystemInformationTelemetryProvider.cs | 106 ++++++ .../Providers/UserTelemetryProvider.cs | 28 ++ .../Services/UsageInformationService.cs | 36 ++ .../Controllers/AnalyticsController.cs | 36 ++ .../Controllers/BackOfficeServerVariables.cs | 6 +- .../UmbracoBuilderExtensions.cs | 1 + .../components/umbrangeslider.directive.js | 12 +- .../common/mocks/umbraco.servervariables.js | 3 +- .../src/common/resources/analytic.resource.js | 57 +++ .../common/services/localization.service.js | 4 + .../settings/analytics.controller.js | 98 +++++ .../views/dashboard/settings/analytics.html | 59 +++ .../umbraco/config/lang/en_us.xml | 5 + .../HelpPanel/systemInformation.ts | 2 +- .../Services/MetricsConsentServiceTest.cs | 40 ++ .../Services/TelemetryProviderTests.cs | 351 ++++++++++++++++++ .../Telemetry/TelemetryServiceTests.cs | 77 ++++ .../Services/UserDataServiceTests.cs | 51 ++- ...SystemInformationTelemetryProviderTests.cs | 121 ++++++ .../Telemetry/TelemetryServiceTests.cs | 19 +- 50 files changed, 1597 insertions(+), 26 deletions(-) create mode 100644 src/Umbraco.Core/Constants-Telemetry.cs create mode 100644 src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs create mode 100644 src/Umbraco.Core/Models/TelemetryLevel.cs create mode 100644 src/Umbraco.Core/Models/TelemetryResource.cs create mode 100644 src/Umbraco.Core/Models/UsageInformation.cs create mode 100644 src/Umbraco.Core/Services/IExamineIndexCountService.cs create mode 100644 src/Umbraco.Core/Services/IMetricsConsentService.cs create mode 100644 src/Umbraco.Core/Services/INodeCountService.cs create mode 100644 src/Umbraco.Core/Services/IUsageInformationService.cs create mode 100644 src/Umbraco.Core/Services/MetricsConsentService.cs create mode 100644 src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs create mode 100644 src/Umbraco.Infrastructure/Services/Implement/ExamineIndexCountService.cs create mode 100644 src/Umbraco.Infrastructure/Services/Implement/NodeCountService.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/ContentTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/DomainTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/ExamineTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/LanguagesTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/MacroTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/MediaTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/NodeCountTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Providers/UserTelemetryProvider.cs create mode 100644 src/Umbraco.Infrastructure/Telemetry/Services/UsageInformationService.cs create mode 100644 src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/analytic.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MetricsConsentServiceTest.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 5e42d3b8be..2d5508f106 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticHideTopLevelNodeFromPath = true; internal const bool StaticUseHttps = false; internal const int StaticVersionCheckPeriod = 7; - internal const string StaticUmbracoPath = "~/umbraco"; + internal const string StaticUmbracoPath = Constants.System.DefaultUmbracoPath; internal const string StaticIconsPath = "~/umbraco/assets/icons"; internal const string StaticUmbracoCssPath = "~/css"; internal const string StaticUmbracoScriptsPath = "~/scripts"; diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index ddff380c08..657b4b6f2d 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -59,7 +59,9 @@ public const string RecycleBinMediaPathPrefix = "-1,-21,"; public const int DefaultLabelDataTypeId = -92; - public const string UmbracoConnectionName = "umbracoDbDSN"; + public const string UmbracoConnectionName = "umbracoDbDSN"; + + public const string DefaultUmbracoPath = "~/umbraco"; } } } diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs new file mode 100644 index 0000000000..6fc474d9ae --- /dev/null +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -0,0 +1,32 @@ +namespace Umbraco.Cms.Core +{ + public static partial class Constants + { + public static class Telemetry + { + + public static string RootCount = "RootCount"; + public static string DomainCount = "DomainCount"; + public static string ExamineIndexCount = "ExamineIndexCount"; + public static string LanguageCount = "LanguageCount"; + public static string MacroCount = "MacroCount"; + public static string MediaCount = "MediaCount"; + public static string MemberCount = "MemberCount"; + public static string TemplateCount = "TemplateCount"; + public static string ContentCount = "ContentCount"; + public static string DocumentTypeCount = "DocumentTypeCount"; + public static string Properties = "Properties"; + public static string UserCount = "UserCount"; + public static string UserGroupCount = "UserGroupCount"; + public static string ServerOs = "ServerOs"; + public static string ServerFramework = "ServerFramework"; + public static string OsLanguage = "OsLanguage"; + public static string WebServer = "WebServer"; + public static string ModelsBuilderMode = "ModelBuilderMode"; + public static string CustomUmbracoPath = "CustomUmbracoPath"; + public static string AspEnvironment = "AspEnvironment"; + public static string IsDebug = "IsDebug"; + public static string DatabaseProvider = "DatabaseProvider"; + } + } +} diff --git a/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs new file mode 100644 index 0000000000..1be6e045d0 --- /dev/null +++ b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Cms.Core.Dashboards +{ + public class AnalyticsDashboard : IDashboard + { + public string Alias => "settingsAnalytics"; + + public string[] Sections => new [] { "settings" }; + + public string View => "views/dashboard/settings/analytics.html"; + + public IAccessRule[] AccessRules => Array.Empty(); + } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 235dc71252..abbadcc5e8 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -25,7 +25,7 @@ using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Mail; -using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.PropertyEditors; @@ -39,7 +39,6 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection @@ -182,7 +181,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); // will be injected in controllers when needed to invoke rest endpoints on Our Services.AddUnique(); diff --git a/src/Umbraco.Core/Models/TelemetryLevel.cs b/src/Umbraco.Core/Models/TelemetryLevel.cs new file mode 100644 index 0000000000..26a714b385 --- /dev/null +++ b/src/Umbraco.Core/Models/TelemetryLevel.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public enum TelemetryLevel + { + Minimal, + Basic, + Detailed, + } +} diff --git a/src/Umbraco.Core/Models/TelemetryResource.cs b/src/Umbraco.Core/Models/TelemetryResource.cs new file mode 100644 index 0000000000..401e07848f --- /dev/null +++ b/src/Umbraco.Core/Models/TelemetryResource.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public class TelemetryResource + { + [DataMember] + public TelemetryLevel TelemetryLevel { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/UsageInformation.cs b/src/Umbraco.Core/Models/UsageInformation.cs new file mode 100644 index 0000000000..e2bedd6f0f --- /dev/null +++ b/src/Umbraco.Core/Models/UsageInformation.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public class UsageInformation + { + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "data")] + public object Data { get; } + + public UsageInformation(string name, object data) + { + Name = name; + Data = data; + } + } +} diff --git a/src/Umbraco.Core/Services/IExamineIndexCountService.cs b/src/Umbraco.Core/Services/IExamineIndexCountService.cs new file mode 100644 index 0000000000..05c5f7d554 --- /dev/null +++ b/src/Umbraco.Core/Services/IExamineIndexCountService.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services +{ + public interface IExamineIndexCountService + { + public int GetCount(); + } +} diff --git a/src/Umbraco.Core/Services/IMetricsConsentService.cs b/src/Umbraco.Core/Services/IMetricsConsentService.cs new file mode 100644 index 0000000000..e55cfd71d0 --- /dev/null +++ b/src/Umbraco.Core/Services/IMetricsConsentService.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface IMetricsConsentService + { + TelemetryLevel GetConsentLevel(); + + void SetConsentLevel(TelemetryLevel telemetryLevel); + } +} diff --git a/src/Umbraco.Core/Services/INodeCountService.cs b/src/Umbraco.Core/Services/INodeCountService.cs new file mode 100644 index 0000000000..50d91c1512 --- /dev/null +++ b/src/Umbraco.Core/Services/INodeCountService.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Cms.Core.Services +{ + public interface INodeCountService + { + int GetNodeCount(Guid nodeType); + int GetMediaCount(); + } +} diff --git a/src/Umbraco.Core/Services/IUsageInformationService.cs b/src/Umbraco.Core/Services/IUsageInformationService.cs new file mode 100644 index 0000000000..fbc988d6b4 --- /dev/null +++ b/src/Umbraco.Core/Services/IUsageInformationService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface IUsageInformationService + { + IEnumerable GetDetailed(); + } +} diff --git a/src/Umbraco.Core/Services/MetricsConsentService.cs b/src/Umbraco.Core/Services/MetricsConsentService.cs new file mode 100644 index 0000000000..3e93a34d8a --- /dev/null +++ b/src/Umbraco.Core/Services/MetricsConsentService.cs @@ -0,0 +1,34 @@ +using System; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public class MetricsConsentService : IMetricsConsentService + { + internal const string Key = "UmbracoAnalyticsLevel"; + + private readonly IKeyValueService _keyValueService; + + public MetricsConsentService(IKeyValueService keyValueService) + { + _keyValueService = keyValueService; + } + + public TelemetryLevel GetConsentLevel() + { + var analyticsLevelString = _keyValueService.GetValue(Key); + + if (analyticsLevelString is null || Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false) + { + return TelemetryLevel.Basic; + } + + return analyticsLevel; + } + + public void SetConsentLevel(TelemetryLevel telemetryLevel) + { + _keyValueService.SetValue(Key, telemetryLevel.ToString()); + } + } +} diff --git a/src/Umbraco.Core/Services/UserDataService.cs b/src/Umbraco.Core/Services/UserDataService.cs index 490b5af6a8..a3c6bd11b4 100644 --- a/src/Umbraco.Core/Services/UserDataService.cs +++ b/src/Umbraco.Core/Services/UserDataService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -9,11 +10,13 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services { + [Obsolete("Use the IUserDataService interface instead")] public class UserDataService : IUserDataService { private readonly IUmbracoVersion _version; private readonly ILocalizationService _localizationService; + public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) { _version = version; diff --git a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs index d19e24695b..3c88147e8b 100644 --- a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs +++ b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Telemetry.Models { @@ -30,5 +31,8 @@ namespace Umbraco.Cms.Core.Telemetry.Models /// [DataMember(Name = "packages")] public IEnumerable Packages { get; set; } + + [DataMember(Name = "detailed")] + public IEnumerable Detailed { get; set; } } } diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index d5a3acac98..e854e62180 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Extensions; @@ -13,6 +15,8 @@ namespace Umbraco.Cms.Core.Telemetry private readonly IManifestParser _manifestParser; private readonly IUmbracoVersion _umbracoVersion; private readonly ISiteIdentifierService _siteIdentifierService; + private readonly IUsageInformationService _usageInformationService; + private readonly IMetricsConsentService _metricsConsentService; /// /// Initializes a new instance of the class. @@ -20,11 +24,15 @@ namespace Umbraco.Cms.Core.Telemetry public TelemetryService( IManifestParser manifestParser, IUmbracoVersion umbracoVersion, - ISiteIdentifierService siteIdentifierService) + ISiteIdentifierService siteIdentifierService, + IUsageInformationService usageInformationService, + IMetricsConsentService metricsConsentService) { _manifestParser = manifestParser; _umbracoVersion = umbracoVersion; _siteIdentifierService = siteIdentifierService; + _usageInformationService = usageInformationService; + _metricsConsentService = metricsConsentService; } /// @@ -39,14 +47,30 @@ namespace Umbraco.Cms.Core.Telemetry telemetryReportData = new TelemetryReportData { Id = telemetryId, - Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(), + Version = GetVersion(), Packages = GetPackageTelemetry(), + Detailed = _usageInformationService.GetDetailed(), }; return true; } + private string GetVersion() + { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; + } + + return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + } + private IEnumerable GetPackageTelemetry() { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; + } + List packages = new(); IEnumerable manifests = _manifestParser.GetManifests(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index c4b9a6367c..68ed50ec10 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -52,6 +52,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; +using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DependencyInjection @@ -196,6 +197,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddSingleton(); + builder.Services.AddTransient(); builder.AddInstaller(); // Services required to run background jobs (with out the handler) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 157d49fd39..eb19adb5b1 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -20,6 +20,7 @@ using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Packaging; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Services.Implement; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; using Umbraco.Cms.Infrastructure.Templates; using Umbraco.Extensions; @@ -93,6 +94,9 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs new file mode 100644 index 0000000000..f0ab1ec344 --- /dev/null +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; + +namespace Umbraco.Cms.Infrastructure.DependencyInjection +{ + public static class UmbracoBuilder_TelemetryProviders + { + public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExamineIndexCountService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExamineIndexCountService.cs new file mode 100644 index 0000000000..eed1a4f5c2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/ExamineIndexCountService.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Examine; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Services.Implement +{ + public class ExamineIndexCountService : IExamineIndexCountService + { + private readonly IExamineManager _examineManager; + + public ExamineIndexCountService(IExamineManager examineManager) + { + _examineManager = examineManager; + } + + public int GetCount() + { + return _examineManager.Indexes.Count(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/NodeCountService.cs b/src/Umbraco.Infrastructure/Services/Implement/NodeCountService.cs new file mode 100644 index 0000000000..1de813900b --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/NodeCountService.cs @@ -0,0 +1,51 @@ +using System; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Services.Implement +{ + public class NodeCountService : INodeCountService + { + private readonly IScopeProvider _scopeProvider; + + public NodeCountService(IScopeProvider scopeProvider) => _scopeProvider = scopeProvider; + + public int GetNodeCount(Guid nodeType) + { + int count = 0; + using (IScope scope = _scopeProvider.CreateScope(autoComplete: true)) + { + var query = scope.Database.SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.NodeObjectType == nodeType && x.Trashed == false); + + count = scope.Database.ExecuteScalar(query); + } + + return count; + } + + public int GetMediaCount() + { + using (IScope scope = _scopeProvider.CreateScope(autoComplete: true)) + { + var query = scope.Database.SqlContext.Sql() + .SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.ContentTypeId, right => right.NodeId) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media) + .Where(x => !x.Trashed) + .Where(x => x.Alias != Constants.Conventions.MediaTypes.Folder); + + return scope.Database.ExecuteScalar(query); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs new file mode 100644 index 0000000000..0936dc14a2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Interfaces +{ + internal interface IDetailedTelemetryProvider + { + IEnumerable GetInformation(); + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/ContentTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/ContentTelemetryProvider.cs new file mode 100644 index 0000000000..23971aec99 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/ContentTelemetryProvider.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class ContentTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IContentService _contentService; + + public ContentTelemetryProvider(IContentService contentService) => _contentService = contentService; + + public IEnumerable GetInformation() + { + var rootNodes = _contentService.GetRootContent(); + int nodes = rootNodes.Count(); + yield return new UsageInformation(Constants.Telemetry.RootCount, nodes); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/DomainTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/DomainTelemetryProvider.cs new file mode 100644 index 0000000000..0fc845b490 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/DomainTelemetryProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class DomainTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IDomainService _domainService; + + public DomainTelemetryProvider(IDomainService domainService) => _domainService = domainService; + + public IEnumerable GetInformation() + { + var domains = _domainService.GetAll(true).Count(); + yield return new UsageInformation(Constants.Telemetry.DomainCount, domains); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/ExamineTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/ExamineTelemetryProvider.cs new file mode 100644 index 0000000000..fd64b7dce1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/ExamineTelemetryProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class ExamineTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IExamineIndexCountService _examineIndexCountService; + + public ExamineTelemetryProvider(IExamineIndexCountService examineIndexCountService) => _examineIndexCountService = examineIndexCountService; + + public IEnumerable GetInformation() + { + var indexes = _examineIndexCountService.GetCount(); + yield return new UsageInformation(Constants.Telemetry.ExamineIndexCount, indexes); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/LanguagesTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/LanguagesTelemetryProvider.cs new file mode 100644 index 0000000000..b3b18e3488 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/LanguagesTelemetryProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class LanguagesTelemetryProvider : IDetailedTelemetryProvider + { + private readonly ILocalizationService _localizationService; + + public LanguagesTelemetryProvider(ILocalizationService localizationService) + { + _localizationService = localizationService; + } + + public IEnumerable GetInformation() + { + int languages = _localizationService.GetAllLanguages().Count(); + yield return new UsageInformation(Constants.Telemetry.LanguageCount, languages); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/MacroTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/MacroTelemetryProvider.cs new file mode 100644 index 0000000000..ee96acd1e7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/MacroTelemetryProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class MacroTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IMacroService _macroService; + + public MacroTelemetryProvider(IMacroService macroService) + { + _macroService = macroService; + } + + public IEnumerable GetInformation() + { + var macros = _macroService.GetAll().Count(); + yield return new UsageInformation(Constants.Telemetry.MacroCount, macros); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/MediaTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/MediaTelemetryProvider.cs new file mode 100644 index 0000000000..9e690ce461 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/MediaTelemetryProvider.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class MediaTelemetryProvider : IDetailedTelemetryProvider + { + private readonly INodeCountService _nodeCountService; + + public MediaTelemetryProvider(INodeCountService nodeCountService) => _nodeCountService = nodeCountService; + + public IEnumerable GetInformation() + { + yield return new UsageInformation(Constants.Telemetry.MediaCount, _nodeCountService.GetMediaCount()); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/NodeCountTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/NodeCountTelemetryProvider.cs new file mode 100644 index 0000000000..8e27c39eed --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/NodeCountTelemetryProvider.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + /// + public class NodeCountTelemetryProvider : IDetailedTelemetryProvider + { + private readonly INodeCountService _nodeCountService; + + public NodeCountTelemetryProvider(INodeCountService nodeCountService) => _nodeCountService = nodeCountService; + + public IEnumerable GetInformation() + { + yield return new UsageInformation(Constants.Telemetry.MemberCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.Member)); + yield return new UsageInformation(Constants.Telemetry.TemplateCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.Template)); + yield return new UsageInformation(Constants.Telemetry.ContentCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.Document)); + yield return new UsageInformation(Constants.Telemetry.DocumentTypeCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.DocumentType)); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs new file mode 100644 index 0000000000..b78ede7851 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class PropertyEditorTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IContentTypeService _contentTypeService; + + public PropertyEditorTelemetryProvider(IContentTypeService contentTypeService) => _contentTypeService = contentTypeService; + + public IEnumerable GetInformation() + { + var contentTypes = _contentTypeService.GetAll(); + var propertyTypes = new HashSet(); + foreach (IContentType contentType in contentTypes) + { + propertyTypes.UnionWith(contentType.PropertyTypes.Select(x => x.PropertyEditorAlias)); + } + + yield return new UsageInformation(Constants.Telemetry.Properties, propertyTypes); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs new file mode 100644 index 0000000000..55b69df851 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, IUserDataService + { + private readonly IUmbracoVersion _version; + private readonly ILocalizationService _localizationService; + private readonly IHostEnvironment _hostEnvironment; + private readonly Lazy _database; + private readonly GlobalSettings _globalSettings; + private readonly HostingSettings _hostingSettings; + private readonly ModelsBuilderSettings _modelsBuilderSettings; + + public SystemInformationTelemetryProvider( + IUmbracoVersion version, + ILocalizationService localizationService, + IOptions modelsBuilderSettings, + IOptions hostingSettings, + IOptions globalSettings, + IHostEnvironment hostEnvironment, + Lazy database) + { + _version = version; + _localizationService = localizationService; + _hostEnvironment = hostEnvironment; + _database = database; + _globalSettings = globalSettings.Value; + _hostingSettings = hostingSettings.Value; + _modelsBuilderSettings = modelsBuilderSettings.Value; + } + + private string CurrentWebServer => IsRunningInProcessIIS() ? "IIS" : "Kestrel"; + + private string ServerFramework => RuntimeInformation.FrameworkDescription; + + private string ModelsBuilderMode => _modelsBuilderSettings.ModelsMode.ToString(); + + private string CurrentCulture => Thread.CurrentThread.CurrentCulture.ToString(); + + private bool IsDebug => _hostingSettings.Debug; + + private bool UmbracoPathCustomized => _globalSettings.UmbracoPath != Constants.System.DefaultUmbracoPath; + + private string AspEnvironment => _hostEnvironment.EnvironmentName; + + private string ServerOs => RuntimeInformation.OSDescription; + + private string DatabaseProvider => _database.Value.DatabaseType.GetProviderName(); + + public IEnumerable GetInformation() => + new UsageInformation[] + { + new(Constants.Telemetry.ServerOs, ServerOs), + new(Constants.Telemetry.ServerFramework, ServerFramework), + new(Constants.Telemetry.OsLanguage, CurrentCulture), + new(Constants.Telemetry.WebServer, CurrentWebServer), + new(Constants.Telemetry.ModelsBuilderMode, ModelsBuilderMode), + new(Constants.Telemetry.CustomUmbracoPath, UmbracoPathCustomized), + new(Constants.Telemetry.AspEnvironment, AspEnvironment), + new(Constants.Telemetry.IsDebug, IsDebug), + new(Constants.Telemetry.DatabaseProvider, DatabaseProvider), + }; + + public IEnumerable GetUserData() => + new UserData[] + { + new("Server OS", ServerOs), + new("Server Framework", ServerFramework), + new("Default Language", _localizationService.GetDefaultLanguageIsoCode()), + new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()), + new("Current Culture", CurrentCulture), + new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), + new("Current Webserver", CurrentWebServer), + new("Models Builder Mode", ModelsBuilderMode), + new("Debug Mode", IsDebug.ToString()), + new("Database Provider", DatabaseProvider), + }; + + private bool IsRunningInProcessIIS() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + + string processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); + return (processName.Contains("w3wp") || processName.Contains("iisexpress")); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/UserTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/UserTelemetryProvider.cs new file mode 100644 index 0000000000..66f697daef --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/UserTelemetryProvider.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class UserTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IUserService _userService; + + public UserTelemetryProvider(IUserService userService) + { + _userService = userService; + } + + public IEnumerable GetInformation() + { + _userService.GetAll(1, 1, out long total); + int userGroups = _userService.GetAllUserGroups().Count(); + + yield return new UsageInformation(Constants.Telemetry.UserCount, total); + yield return new UsageInformation(Constants.Telemetry.UserGroupCount, userGroups); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Services/UsageInformationService.cs b/src/Umbraco.Infrastructure/Telemetry/Services/UsageInformationService.cs new file mode 100644 index 0000000000..486a071af7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Services/UsageInformationService.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Core.Services +{ + internal class UsageInformationService : IUsageInformationService + { + private readonly IMetricsConsentService _metricsConsentService; + private readonly IEnumerable _providers; + + public UsageInformationService( + IMetricsConsentService metricsConsentService, + IEnumerable providers) + { + _metricsConsentService = metricsConsentService; + _providers = providers; + } + + public IEnumerable GetDetailed() + { + if (_metricsConsentService.GetConsentLevel() != TelemetryLevel.Detailed) + { + return null; + } + + var detailedUsageInformation = new List(); + foreach (IDetailedTelemetryProvider provider in _providers) + { + detailedUsageInformation.AddRange(provider.GetInformation()); + } + + return detailedUsageInformation; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs b/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs new file mode 100644 index 0000000000..e1aac7319b --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + public class AnalyticsController : UmbracoAuthorizedJsonController + { + private readonly IMetricsConsentService _metricsConsentService; + public AnalyticsController(IMetricsConsentService metricsConsentService) + { + _metricsConsentService = metricsConsentService; + } + + public TelemetryLevel GetConsentLevel() + { + return _metricsConsentService.GetConsentLevel(); + } + + [HttpPost] + public IActionResult SetConsentLevel([FromBody]TelemetryResource telemetryResource) + { + if (!ModelState.IsValid) + { + return BadRequest(); + } + + _metricsConsentService.SetConsentLevel(telemetryResource.TelemetryLevel); + return Ok(); + } + + public IEnumerable GetAllLevels() => new[] { TelemetryLevel.Minimal, TelemetryLevel.Basic, TelemetryLevel.Detailed }; + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index cbb17dce99..e2229d4222 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -385,7 +385,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { "trackedReferencesApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetPagedReferences(0, 1, 1, false)) - } + }, + { + "analyticsApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetConsentLevel()) + }, } }, { diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index d2cbf5bd14..6c5126f97b 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -160,6 +160,7 @@ namespace Umbraco.Extensions )); builder.AddCoreInitialServices(); + builder.AddTelemetryProviders(); // aspnet app lifetime mgmt builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js index d5c791281c..262f70f62b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js @@ -13,7 +13,7 @@ For extra details about options and events take a look here: https://refreshless
 	
- @@ -229,11 +229,13 @@ For extra details about options and events take a look here: https://refreshless var origins = slider.noUiSlider.getOrigins(); // Move tooltips into the origin element. The default stylesheet handles this. + if(tooltips && tooltips.length !== 0){ tooltips.forEach(function (tooltip, index) { - if (tooltip) { - origins[index].appendChild(tooltip); - } + if (tooltip) { + origins[index].appendChild(tooltip); + } }); + } slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) { @@ -283,7 +285,7 @@ For extra details about options and events take a look here: https://refreshless offset = (textIsRtl && !isVertical ? 100 : 0) + (offset / handlesInPool) - lastOffset; // Filter to unique values - var tooltipValues = poolValues[poolIndex].filter((v, i, a) => a.indexOf(v) === i); + var tooltipValues = poolValues[poolIndex].filter((v, i, a) => a.indexOf(v) === i); // Center this tooltip over the affected handles tooltips[handleNumber].innerHTML = tooltipValues.join(separator); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js index 868bc4c6d5..7abefedfab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js @@ -20,7 +20,8 @@ Umbraco.Sys.ServerVariables = { "updateCheckApiBaseUrl": "/umbraco/Api/UpdateCheck/", "relationApiBaseUrl": "/umbraco/UmbracoApi/Relation/", "rteApiBaseUrl": "/umbraco/UmbracoApi/RichTextPreValue/", - "iconApiBaseUrl": "/umbraco/UmbracoApi/Icon/" + "iconApiBaseUrl": "/umbraco/UmbracoApi/Icon/", + "analyticsApiBaseUrl": "/umbraco/UmbracoApi/Consent/" }, umbracoSettings: { "umbracoPath": "/umbraco", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/analytic.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/analytic.resource.js new file mode 100644 index 0000000000..fa3b203df2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/analytic.resource.js @@ -0,0 +1,57 @@ +/** + * @ngdoc service + * @name umbraco.resources.consentResource + * @function + * + * @description + * Used by the health check dashboard to get checks and send requests to fix checks. + */ +(function () { + 'use strict'; + + function analyticResource($http, umbRequestHelper) { + + function getConsentLevel () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "analyticsApiBaseUrl", + "GetConsentLevel")), + 'Server call failed for getting current consent level'); + } + + function getAllConsentLevels () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "analyticsApiBaseUrl", + "GetAllLevels")), + 'Server call failed for getting current consent level'); + } + + function saveConsentLevel (value) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "analyticsApiBaseUrl", + "SetConsentLevel"), + { telemetryLevel : value } + ), + 'Server call failed for getting current consent level'); + } + + var resource = { + getConsentLevel: getConsentLevel, + getAllConsentLevels : getAllConsentLevels, + saveConsentLevel : saveConsentLevel + }; + + return resource; + + } + + + angular.module('umbraco.resources').factory('analyticResource', analyticResource); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js index 805afae5b8..6911651af9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js @@ -200,6 +200,10 @@ angular.module('umbraco.services') * localizationService.localizeMany(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){ * var header = data[0]; * var message = data[1]; + * + * + * + * * notificationService.error(header, message); * }); *
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js new file mode 100644 index 0000000000..094941b63a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js @@ -0,0 +1,98 @@ +(function () { + "use strict"; + + function AnalyticsController($q, analyticResource, localizationService, notificationsService) { + + let sliderRef = null; + + var vm = this; + vm.getConsentLevel = getConsentLevel; + vm.getAllConsentLevels = getAllConsentLevels; + vm.saveConsentLevel = saveConsentLevel; + vm.sliderChange = sliderChange; + vm.setup = setup; + vm.loading = true; + vm.consentLevel = ''; + vm.consentLevels = []; + vm.val = 1; + vm.sliderOptions = + { + "start": 1, + "step": 1, + "tooltips": [false], + "range": { + "min": 1, + "max": 3 + }, + pips: { + mode: 'values', + density: 50, + values: [1, 2, 3], + format: { + to: function (value) { + return vm.consentLevels[value - 1]; + }, + from: function (value) { + return Number(value); + } + } + } + }; + $q.all( + [getConsentLevel(), + getAllConsentLevels() + ]).then( () => { + vm.startPos = calculateStartPositionForSlider(); + vm.sliderVal = vm.consentLevels[vm.startPos - 1]; + vm.sliderOptions.start = vm.startPos; + vm.val = vm.startPos; + vm.sliderOptions.pips.format = { + to: function (value) { + return vm.consentLevels[value - 1]; + }, + from: function (value) { + return Number(value); + } + } + vm.loading = false; + if (sliderRef) { + sliderRef.noUiSlider.set(vm.startPos); + } + + }); + function setup(slider) { + sliderRef = slider; + } + + function getConsentLevel() { + return analyticResource.getConsentLevel().then(function (response) { + vm.consentLevel = response; + }) + } + function getAllConsentLevels(){ + return analyticResource.getAllConsentLevels().then(function (response) { + vm.consentLevels = response; + }) + } + function saveConsentLevel(){ + analyticResource.saveConsentLevel(vm.sliderVal); + localizationService.localize("analytics_analyticsLevelSavedSuccess").then(function(value) { + notificationsService.success(value); + }); + } + + function sliderChange(values) { + const result = Number(values[0]); + vm.sliderVal = vm.consentLevels[result - 1]; + } + + function calculateStartPositionForSlider(){ + let startPosition = vm.consentLevels.indexOf(vm.consentLevel) + 1; + if(startPosition === 0){ + return 2;// Default start value + } + return startPosition; + } + } + angular.module("umbraco").controller("Umbraco.Dashboard.AnalyticsController", AnalyticsController); + })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html new file mode 100644 index 0000000000..361b0c8bdc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html @@ -0,0 +1,59 @@ +
+ + +

+ Consent for analytics +

+
+

In order to improve Umbraco and add new functionality based on as relevant information as possible, +
we would like to collect system- and usage information from your installation. +
We will NOT collect any personal data like content, code or users, and all data will be fully anonymous. +
+
We will on a regular basis share some of the overall learnings from these metrics. + Hopefully, you'll help us collect some valuable data.

+
+ + +
+ +

+

+ {{vm.sliderVal}} +
We'll only send an anonymous site ID to let us know that the site exists. +
+
+ {{vm.sliderVal}} +
We'll send site ID, umbraco version and packages installed +
+
+ {{vm.sliderVal}} + +
We'll send: +
- Site ID, umbraco version and packages installed +
- System information like Server OS and Webserver +
- Statistics, like number of content nodes and number of media items +
- Configuration settings, like modelsbuilder mode and used languages +
+
We might change/extend what we send on the detailed level in the future, but if so, it will be listed in + this view. + By choosing "detailed" I accept these future changes +
+

+
+
+ + +
+
+
+
+ + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index c901b1040a..bc8f38d408 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -2508,6 +2508,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Published Status Models Builder Health Check + Analytics Profiling Getting Started Install Umbraco Forms @@ -2863,4 +2864,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont item returned items returned + + Consent for analytics + Analytics level saved! + diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts index 363244e12f..dbc9d19427 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts @@ -20,7 +20,7 @@ context('System Information', () => { it('Check System Info Displays', () => { openSystemInformation(); - cy.get('.table').find('tr').should('have.length', 10); + cy.get('.table').find('tr').should('have.length', 13); cy.contains('Current Culture').parent().should('contain', 'en-US'); cy.contains('Current UI Culture').parent().should('contain', 'en-US'); }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MetricsConsentServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MetricsConsentServiceTest.cs new file mode 100644 index 0000000000..70d0e80a33 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MetricsConsentServiceTest.cs @@ -0,0 +1,40 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class MetricsConsentServiceTest : UmbracoIntegrationTest + { + private IMetricsConsentService MetricsConsentService => GetRequiredService(); + + private IKeyValueService KeyValueService => GetRequiredService(); + + [Test] + [TestCase(TelemetryLevel.Minimal)] + [TestCase(TelemetryLevel.Basic)] + [TestCase(TelemetryLevel.Detailed)] + public void Can_Store_Consent(TelemetryLevel level) + { + MetricsConsentService.SetConsentLevel(level); + + var actual = MetricsConsentService.GetConsentLevel(); + Assert.IsNotNull(actual); + Assert.AreEqual(level, actual); + } + + [Test] + public void Enum_Stored_as_string() + { + MetricsConsentService.SetConsentLevel(TelemetryLevel.Detailed); + + var stringValue = KeyValueService.GetValue(Cms.Core.Services.MetricsConsentService.Key); + + Assert.AreEqual("Detailed", stringValue); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs new file mode 100644 index 0000000000..e6287c80fa --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs @@ -0,0 +1,351 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services +{ + /// + /// Tests covering the SectionService + /// + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class TelemetryProviderTests : UmbracoIntegrationTest + { + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IFileService FileService => GetRequiredService(); + + private IDomainService DomainService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private DomainTelemetryProvider DetailedTelemetryProviders => GetRequiredService(); + + private ContentTelemetryProvider ContentTelemetryProvider => GetRequiredService(); + + private LanguagesTelemetryProvider LanguagesTelemetryProvider => GetRequiredService(); + + private UserTelemetryProvider UserTelemetryProvider => GetRequiredService(); + + private MacroTelemetryProvider MacroTelemetryProvider => GetRequiredService(); + + private MediaTelemetryProvider MediaTelemetryProvider => GetRequiredService(); + + private PropertyEditorTelemetryProvider PropertyEditorTelemetryProvider => GetRequiredService(); + + private ILocalizationService LocalizationService => GetRequiredService(); + + private IUserService UserService => GetRequiredService(); + + private IMediaService MediaService => GetRequiredService(); + + private IMediaTypeService MediaTypeService => GetRequiredService(); + + private LanguageBuilder _languageBuilder = new(); + + private UserBuilder _userBuilder = new(); + + private UserGroupBuilder _userGroupBuilder = new(); + + private ContentTypeBuilder _contentTypeBuilder = new (); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + base.CustomTestSetup(builder); + } + + [Test] + public void Domain_Telemetry_Provider_Can_Get_Domains() + { + // Arrange + DomainService.Save(new UmbracoDomain("danish", "da-DK")); + + IEnumerable result = null; + // Act + result = DetailedTelemetryProviders.GetInformation(); + + + // Assert + Assert.AreEqual(1, result.First().Data); + } + + [Test] + public void SectionService_Can_Get_Allowed_Sections_For_User() + { + // Arrange + Template template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + ContentType contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id); + ContentTypeService.Save(contentType); + + Content blueprint = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + ContentService.SaveBlueprint(blueprint); + + IContent fromBlueprint = ContentService.CreateContentFromBlueprint(blueprint, "My test content"); + ContentService.Save(fromBlueprint); + + IEnumerable result = null; + // Act + result = ContentTelemetryProvider.GetInformation(); + + + // Assert + // TODO : Test multiple roots, with children + grandchildren + Assert.AreEqual(1, result.First().Data); + } + + [Test] + public void Language_Telemetry_Can_Get_Languages() + { + // Arrange + var langTwo = _languageBuilder.WithCultureInfo("da-DK").Build(); + var langThree = _languageBuilder.WithCultureInfo("se-SV").Build(); + + LocalizationService.Save(langTwo); + LocalizationService.Save(langThree); + + IEnumerable result = null; + + // Act + result = LanguagesTelemetryProvider.GetInformation(); + + // Assert + Assert.AreEqual(3, result.First().Data); + } + + [Test] + public void MacroTelemetry_Can_Get_Macros() + { + BuildMacros(); + var result = MacroTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.MacroCount); + Assert.AreEqual(3, result.Data); + } + + [Test] + public void MediaTelemetry_Can_Get_Media_In_Folders() + { + IMediaType folderType = MediaTypeService.Get(1031); + IMediaType imageMediaType = MediaTypeService.Get(1032); + + Media root = MediaBuilder.CreateMediaFolder(folderType, -1); + MediaService.Save(root); + int createdMediaCount = 10; + for (int i = 0; i < createdMediaCount; i++) + { + Media c1 = MediaBuilder.CreateMediaImage(imageMediaType, root.Id); + MediaService.Save(c1); + } + + var result = MediaTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.MediaCount); + Assert.AreEqual(createdMediaCount, result.Data); + } + + [Test] + public void MediaTelemetry_Can_Get_Media_In_Root() + { + IMediaType imageMediaType = MediaTypeService.Get(1032); + int createdMediaCount = 10; + for (int i = 0; i < createdMediaCount; i++) + { + Media c1 = MediaBuilder.CreateMediaImage(imageMediaType, -1); + MediaService.Save(c1); + } + + var result = MediaTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.MediaCount); + Assert.AreEqual(createdMediaCount, result.Data); + } + + [Test] + public void PropertyEditorTelemetry_Counts_Same_Editor_As_One() + { + ContentType ct2 = ContentTypeBuilder.CreateBasicContentType("ct2", "CT2", null); + AddPropType("title", -88, ct2); + ContentType ct3 = ContentTypeBuilder.CreateBasicContentType("ct3", "CT3", null); + AddPropType("title",-88, ct3); + ContentType ct5 = ContentTypeBuilder.CreateBasicContentType("ct5", "CT5", null); + AddPropType("blah", -88, ct5); + + ContentTypeService.Save(ct2); + ContentTypeService.Save(ct3); + ContentTypeService.Save(ct5); + + var properties = PropertyEditorTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.Properties); + var result = properties.Data as IEnumerable; + Assert.AreEqual(1, result?.Count()); + } + + [Test] + public void PropertyEditorTelemetry_Can_Get_All_PropertyTypes() + { + ContentType ct2 = ContentTypeBuilder.CreateBasicContentType("ct2", "CT2", null); + AddPropType("title", -88, ct2); + AddPropType("title",-99, ct2); + ContentType ct5 = ContentTypeBuilder.CreateBasicContentType("ct5", "CT5", null); + AddPropType("blah", -88, ct5); + + ContentTypeService.Save(ct2); + ContentTypeService.Save(ct5); + + var properties = PropertyEditorTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.Properties); + var result = properties.Data as IEnumerable; + Assert.AreEqual(2, result?.Count()); + } + + [Test] + public void UserTelemetry_Can_Get_Default_User() + { + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserCount); + + Assert.AreEqual(1, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_With_Saved_User() + { + var user = BuildUser(0); + + UserService.Save(user); + + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserCount); + + Assert.AreEqual(2, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_More_Users() + { + int totalUsers = 99; + var users = BuildUsers(totalUsers); + UserService.Save(users); + + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserCount); + + Assert.AreEqual(totalUsers + 1, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_Default_UserGroups() + { + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserGroupCount); + Assert.AreEqual(5, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_With_Saved_UserGroups() + { + var userGroup = BuildUserGroup("testGroup"); + + UserService.Save(userGroup); + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserGroupCount); + + Assert.AreEqual(6, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_More_UserGroups() + { + var userGroups = BuildUserGroups(100); + + + foreach (var userGroup in userGroups) + { + UserService.Save(userGroup); + } + + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserGroupCount); + + Assert.AreEqual(105, result.Data); + } + + private User BuildUser(int count) => + _userBuilder + .WithLogin($"username{count}", "test pass") + .WithName("Test" + count) + .WithEmail($"test{count}@test.com") + .Build(); + + private IEnumerable BuildUsers(int count) + { + for (int i = 0; i < count; i++) + { + yield return BuildUser(count); + } + } + + private IUserGroup BuildUserGroup(string alias) => + _userGroupBuilder + .WithAlias(alias) + .WithName(alias) + .WithAllowedSections(new List(){"A", "B"}) + .Build(); + + private IEnumerable BuildUserGroups(int count) + { + for (int i = 0; i < count; i++) + { + yield return BuildUserGroup(i.ToString()); + } + } + + private void BuildMacros() + { + IScopeProvider scopeProvider = ScopeProvider; + using (IScope scope = scopeProvider.CreateScope()) + { + var repository = new MacroRepository((IScopeAccessor)scopeProvider, AppCaches.Disabled, Mock.Of>(), ShortStringHelper); + + repository.Save(new Macro(ShortStringHelper, "test1", "Test1", "~/views/macropartials/test1.cshtml")); + repository.Save(new Macro(ShortStringHelper, "test2", "Test2", "~/views/macropartials/test2.cshtml")); + repository.Save(new Macro(ShortStringHelper, "test3", "Tet3", "~/views/macropartials/test3.cshtml")); + scope.Complete(); + } + } + + private void AddPropType(string alias, int dataTypeId, IContentType ct) + { + var contentCollection = new PropertyTypeCollection(true) + { + new PropertyType(ShortStringHelper, Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext) { Alias = alias, Name = "Title", Description = string.Empty, Mandatory = false, SortOrder = 1, DataTypeId = dataTypeId }, + }; + var pg = new PropertyGroup(contentCollection) + { + Alias = "test", + Name = "test", + SortOrder = 1 + }; + ct.PropertyGroups.Add(pg); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs new file mode 100644 index 0000000000..e898ba49ce --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Telemetry +{ + + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class TelemetryServiceTests : UmbracoIntegrationTest + { + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.Configure(options => options.Id = Guid.NewGuid().ToString()); + } + + private ITelemetryService TelemetryService => GetRequiredService(); + private IMetricsConsentService MetricsConsentService => GetRequiredService(); + + [Test] + public void Expected_Detailed_Telemetry_Exists() + { + var expectedData = new string[] + { + Constants.Telemetry.RootCount, + Constants.Telemetry.DomainCount, + Constants.Telemetry.ExamineIndexCount, + Constants.Telemetry.LanguageCount, + Constants.Telemetry.MacroCount, + Constants.Telemetry.MediaCount, + Constants.Telemetry.MediaCount, + Constants.Telemetry.TemplateCount, + Constants.Telemetry.ContentCount, + Constants.Telemetry.DocumentTypeCount, + Constants.Telemetry.Properties, + Constants.Telemetry.UserCount, + Constants.Telemetry.UserGroupCount, + Constants.Telemetry.ServerOs, + Constants.Telemetry.ServerFramework, + Constants.Telemetry.OsLanguage, + Constants.Telemetry.WebServer, + Constants.Telemetry.ModelsBuilderMode, + Constants.Telemetry.CustomUmbracoPath, + Constants.Telemetry.AspEnvironment, + Constants.Telemetry.IsDebug, + Constants.Telemetry.DatabaseProvider, + }; + + MetricsConsentService.SetConsentLevel(TelemetryLevel.Detailed); + var success = TelemetryService.TryGetTelemetryReportData(out var telemetryReportData); + var detailed = telemetryReportData.Detailed.ToArray(); + + Assert.IsTrue(success); + Assert.Multiple(() => + { + Assert.IsNotNull(detailed); + Assert.AreEqual(expectedData.Length, detailed.Length); + + foreach (var expectedInfo in expectedData) + { + var expected = detailed.FirstOrDefault(x => x.Name == expectedInfo); + Assert.IsNotNull(expected, $"Expected {expectedInfo} to exists in the detailed list"); + } + }); + } + + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs index 7417976369..4a8c77edb8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs @@ -1,13 +1,19 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services { @@ -86,10 +92,49 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services }); } - private UserDataService CreateUserDataService(string culture) + [Test] + [TestCase(ModelsMode.Nothing)] + [TestCase(ModelsMode.InMemoryAuto)] + [TestCase(ModelsMode.SourceCodeAuto)] + [TestCase(ModelsMode.SourceCodeManual)] + public void ReportsModelsModeCorrectly(ModelsMode modelsMode) + { + var userDataService = CreateUserDataService(modelsMode: modelsMode); + UserData[] userData = userDataService.GetUserData().ToArray(); + + var actual = userData.FirstOrDefault(x => x.Name == "Models Builder Mode"); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(modelsMode.ToString(), actual.Data); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void ReportsDebugModeCorrectly(bool isDebug) + { + var userDataService = CreateUserDataService(isDebug: isDebug); + UserData[] userData = userDataService.GetUserData().ToArray(); + + var actual = userData.FirstOrDefault(x => x.Name == "Debug Mode"); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(isDebug.ToString(), actual.Data); + } + + private SystemInformationTelemetryProvider CreateUserDataService(string culture = "", ModelsMode modelsMode = ModelsMode.InMemoryAuto, bool isDebug = true) { var localizationService = CreateILocalizationService(culture); - return new UserDataService(_umbracoVersion, localizationService); + + var databaseMock = new Mock(); + databaseMock.Setup(x => x.DatabaseType.GetProviderName()).Returns("SQL"); + + return new SystemInformationTelemetryProvider( + _umbracoVersion, + localizationService, + Mock.Of>(x => x.Value == new ModelsBuilderSettings { ModelsMode = modelsMode }), + Mock.Of>(x => x.Value == new HostingSettings { Debug = isDebug }), + Mock.Of>(x => x.Value == new GlobalSettings()), + Mock.Of(), + new Lazy(databaseMock.Object)); } private ILocalizationService CreateILocalizationService(string culture) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs new file mode 100644 index 0000000000..8d33236ce9 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs @@ -0,0 +1,121 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry +{ + [TestFixture] + public class SystemInformationTelemetryProviderTests + { + [Test] + [TestCase(ModelsMode.Nothing)] + [TestCase(ModelsMode.InMemoryAuto)] + [TestCase(ModelsMode.SourceCodeAuto)] + [TestCase(ModelsMode.SourceCodeManual)] + public void ReportsModelsModeCorrectly(ModelsMode modelsMode) + { + var telemetryProvider = CreateProvider(modelsMode: modelsMode); + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.ModelsBuilderMode); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(modelsMode.ToString(), actual.Data); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void ReportsDebugModeCorrectly(bool isDebug) + { + var telemetryProvider = CreateProvider(isDebug: isDebug); + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.IsDebug); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(isDebug, actual.Data); + } + + [Test] + [TestCase("en-US")] + [TestCase("de-DE")] + [TestCase("en-NZ")] + [TestCase("sv-SE")] + public void ReportsOsLanguageCorrectly(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + var telemetryProvider = CreateProvider(); + + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.OsLanguage); + + Assert.NotNull(actual?.Data); + Assert.AreEqual(culture, actual.Data.ToString()); + } + + [Test] + [TestCase(GlobalSettings.StaticUmbracoPath, false)] + [TestCase("mycustompath", true)] + [TestCase("~/notUmbraco", true)] + [TestCase("/umbraco", true)] + [TestCase("umbraco", true)] + public void ReportsCustomUmbracoPathCorrectly(string path, bool isCustom) + { + var telemetryProvider = CreateProvider(umbracoPath: path); + + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.CustomUmbracoPath); + + Assert.NotNull(actual?.Data); + Assert.AreEqual(isCustom, actual.Data); + } + + [Test] + [TestCase("Development")] + [TestCase("Staging")] + [TestCase("Production")] + public void ReportsCorrectAspEnvironment(string environment) + { + var telemetryProvider = CreateProvider(environment: environment); + + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.AspEnvironment); + + Assert.NotNull(actual?.Data); + Assert.AreEqual(environment, actual.Data); + } + + private SystemInformationTelemetryProvider CreateProvider( + ModelsMode modelsMode = ModelsMode.InMemoryAuto, + bool isDebug = true, + string umbracoPath = "", + string environment = "") + { + var hostEnvironment = new Mock(); + hostEnvironment.Setup(x => x.EnvironmentName).Returns(environment); + + var databaseMock = new Mock(); + databaseMock.Setup(x => x.DatabaseType.GetProviderName()).Returns("SQL"); + + return new SystemInformationTelemetryProvider( + Mock.Of(), + Mock.Of(), + Mock.Of>(x => x.Value == new ModelsBuilderSettings{ ModelsMode = modelsMode }), + Mock.Of>(x => x.Value == new HostingSettings { Debug = isDebug }), + Mock.Of>(x => x.Value == new GlobalSettings{ UmbracoPath = umbracoPath }), + hostEnvironment.Object, + new Lazy(databaseMock.Object)); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 910ca7c792..430f383646 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry @@ -20,7 +20,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry { var version = CreateUmbracoVersion(9, 3, 1); var siteIdentifierServiceMock = new Mock(); - var sut = new TelemetryService(Mock.Of(), version, siteIdentifierServiceMock.Object); + var usageInformationServiceMock = new Mock(); + var sut = new TelemetryService(Mock.Of(), version, siteIdentifierServiceMock.Object, usageInformationServiceMock.Object, Mock.Of()); Guid guid; var result = sut.TryGetTelemetryReportData(out var telemetryReportData); @@ -31,7 +32,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry public void SkipsIfCantGetOrCreateId() { var version = CreateUmbracoVersion(9, 3, 1); - var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService(false)); + var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService(false), Mock.Of(), Mock.Of()); var result = sut.TryGetTelemetryReportData(out var telemetry); @@ -44,7 +45,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry { var version = CreateUmbracoVersion(9, 1, 1, "-rc", "-ad2f4k2d"); - var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService()); + var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService(), Mock.Of(), Mock.Of()); var result = sut.TryGetTelemetryReportData(out var telemetry); @@ -65,7 +66,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry new () { PackageName = noVersionPackageName } }; var manifestParser = CreateManifestParser(manifests); - var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService()); + var metricsConsentService = new Mock(); + metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic); + var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService(), Mock.Of(), metricsConsentService.Object); var success = sut.TryGetTelemetryReportData(out var telemetry); @@ -93,7 +96,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry new () { PackageName = "TrackingAllowed", AllowPackageTelemetry = true }, }; var manifestParser = CreateManifestParser(manifests); - var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService()); + var metricsConsentService = new Mock(); + metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic); + var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService(), Mock.Of(), metricsConsentService.Object); var success = sut.TryGetTelemetryReportData(out var telemetry);