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 <nikolajlauridsen@protonmail.ch>

Co-authored-by: Nikolaj Geisle <niko737@edu.ucl.dk>
Co-authored-by: nikolajlauridsen <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Nikolaj Geisle
2022-04-19 15:06:10 +02:00
committed by GitHub
parent 2d31913837
commit c07ffb68fc
50 changed files with 1597 additions and 26 deletions

View File

@@ -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";

View File

@@ -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";
}
}
}

View File

@@ -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";
}
}
}

View File

@@ -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<IAccessRule>();
}
}

View File

@@ -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<UriUtility>();
Services.AddUnique<IDashboardService, DashboardService>();
Services.AddUnique<IUserDataService, UserDataService>();
Services.AddSingleton<IMetricsConsentService, MetricsConsentService>();
// will be injected in controllers when needed to invoke rest endpoints on Our
Services.AddUnique<IInstallationService, InstallationService>();

View File

@@ -0,0 +1,12 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models
{
[DataContract]
public enum TelemetryLevel
{
Minimal,
Basic,
Detailed,
}
}

View File

@@ -0,0 +1,11 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models
{
[DataContract]
public class TelemetryResource
{
[DataMember]
public TelemetryLevel TelemetryLevel { get; set; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Core.Services
{
public interface IExamineIndexCountService
{
public int GetCount();
}
}

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services
{
public interface IMetricsConsentService
{
TelemetryLevel GetConsentLevel();
void SetConsentLevel(TelemetryLevel telemetryLevel);
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace Umbraco.Cms.Core.Services
{
public interface INodeCountService
{
int GetNodeCount(Guid nodeType);
int GetMediaCount();
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services
{
public interface IUsageInformationService
{
IEnumerable<UsageInformation> GetDetailed();
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;

View File

@@ -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
/// </remarks>
[DataMember(Name = "packages")]
public IEnumerable<PackageTelemetry> Packages { get; set; }
[DataMember(Name = "detailed")]
public IEnumerable<UsageInformation> Detailed { get; set; }
}
}

View File

@@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="TelemetryService"/> 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;
}
/// <inheritdoc/>
@@ -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<PackageTelemetry> GetPackageTelemetry()
{
if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal)
{
return null;
}
List<PackageTelemetry> packages = new();
IEnumerable<PackageManifest> manifests = _manifestParser.GetManifests();

View File

@@ -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<PackageDataInstallation>();
builder.Services.AddTransient<INodeCountService, NodeCountService>();
builder.AddInstaller();
// Services required to run background jobs (with out the handler)

View File

@@ -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<PackageDataInstallation>();
builder.Services.AddUnique<IPackageInstallation, PackageInstallation>();
builder.Services.AddUnique<IHtmlMacroParameterParser, HtmlMacroParameterParser>();
builder.Services.AddTransient<IExamineIndexCountService, ExamineIndexCountService>();
builder.Services.AddUnique<IUserDataService, SystemInformationTelemetryProvider>();
builder.Services.AddTransient<IUsageInformationService, UsageInformationService>();
return builder;
}

View File

@@ -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<IDetailedTelemetryProvider, ContentTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, DomainTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, ExamineTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, LanguagesTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, MacroTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, MediaTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, NodeCountTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, PropertyEditorTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, UserTelemetryProvider>();
builder.Services.AddTransient<IDetailedTelemetryProvider, SystemInformationTelemetryProvider>();
return builder;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == nodeType && x.Trashed == false);
count = scope.Database.ExecuteScalar<int>(query);
}
return count;
}
public int GetMediaCount()
{
using (IScope scope = _scopeProvider.CreateScope(autoComplete: true))
{
var query = scope.Database.SqlContext.Sql()
.SelectCount()
.From<NodeDto>()
.InnerJoin<ContentDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<ContentTypeDto>()
.On<ContentDto, ContentTypeDto>(left => left.ContentTypeId, right => right.NodeId)
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Media)
.Where<NodeDto>(x => !x.Trashed)
.Where<ContentTypeDto>(x => x.Alias != Constants.Conventions.MediaTypes.Folder);
return scope.Database.ExecuteScalar<int>(query);
}
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Telemetry.Interfaces
{
internal interface IDetailedTelemetryProvider
{
IEnumerable<UsageInformation> GetInformation();
}
}

View File

@@ -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<UsageInformation> GetInformation()
{
var rootNodes = _contentService.GetRootContent();
int nodes = rootNodes.Count();
yield return new UsageInformation(Constants.Telemetry.RootCount, nodes);
}
}
}

View File

@@ -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<UsageInformation> GetInformation()
{
var domains = _domainService.GetAll(true).Count();
yield return new UsageInformation(Constants.Telemetry.DomainCount, domains);
}
}
}

View File

@@ -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<UsageInformation> GetInformation()
{
var indexes = _examineIndexCountService.GetCount();
yield return new UsageInformation(Constants.Telemetry.ExamineIndexCount, indexes);
}
}
}

View File

@@ -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<UsageInformation> GetInformation()
{
int languages = _localizationService.GetAllLanguages().Count();
yield return new UsageInformation(Constants.Telemetry.LanguageCount, languages);
}
}
}

View File

@@ -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<UsageInformation> GetInformation()
{
var macros = _macroService.GetAll().Count();
yield return new UsageInformation(Constants.Telemetry.MacroCount, macros);
}
}
}

View File

@@ -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<UsageInformation> GetInformation()
{
yield return new UsageInformation(Constants.Telemetry.MediaCount, _nodeCountService.GetMediaCount());
}
}
}

View File

@@ -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
{
/// <inheritdoc />
public class NodeCountTelemetryProvider : IDetailedTelemetryProvider
{
private readonly INodeCountService _nodeCountService;
public NodeCountTelemetryProvider(INodeCountService nodeCountService) => _nodeCountService = nodeCountService;
public IEnumerable<UsageInformation> 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));
}
}
}

View File

@@ -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<UsageInformation> GetInformation()
{
var contentTypes = _contentTypeService.GetAll();
var propertyTypes = new HashSet<string>();
foreach (IContentType contentType in contentTypes)
{
propertyTypes.UnionWith(contentType.PropertyTypes.Select(x => x.PropertyEditorAlias));
}
yield return new UsageInformation(Constants.Telemetry.Properties, propertyTypes);
}
}
}

View File

@@ -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<IUmbracoDatabase> _database;
private readonly GlobalSettings _globalSettings;
private readonly HostingSettings _hostingSettings;
private readonly ModelsBuilderSettings _modelsBuilderSettings;
public SystemInformationTelemetryProvider(
IUmbracoVersion version,
ILocalizationService localizationService,
IOptions<ModelsBuilderSettings> modelsBuilderSettings,
IOptions<HostingSettings> hostingSettings,
IOptions<GlobalSettings> globalSettings,
IHostEnvironment hostEnvironment,
Lazy<IUmbracoDatabase> 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<UsageInformation> 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<UserData> 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"));
}
}
}

View File

@@ -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<UsageInformation> 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);
}
}
}

View File

@@ -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<IDetailedTelemetryProvider> _providers;
public UsageInformationService(
IMetricsConsentService metricsConsentService,
IEnumerable<IDetailedTelemetryProvider> providers)
{
_metricsConsentService = metricsConsentService;
_providers = providers;
}
public IEnumerable<UsageInformation> GetDetailed()
{
if (_metricsConsentService.GetConsentLevel() != TelemetryLevel.Detailed)
{
return null;
}
var detailedUsageInformation = new List<UsageInformation>();
foreach (IDetailedTelemetryProvider provider in _providers)
{
detailedUsageInformation.AddRange(provider.GetInformation());
}
return detailedUsageInformation;
}
}
}

View File

@@ -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<TelemetryLevel> GetAllLevels() => new[] { TelemetryLevel.Minimal, TelemetryLevel.Basic, TelemetryLevel.Detailed };
}
}

View File

@@ -385,7 +385,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
"trackedReferencesApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<TrackedReferencesController>(
controller => controller.GetPagedReferences(0, 1, 1, false))
}
},
{
"analyticsApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<AnalyticsController>(
controller => controller.GetConsentLevel())
},
}
},
{

View File

@@ -160,6 +160,7 @@ namespace Umbraco.Extensions
));
builder.AddCoreInitialServices();
builder.AddTelemetryProviders();
// aspnet app lifetime mgmt
builder.Services.AddUnique<IUmbracoApplicationLifetime, AspNetCoreUmbracoApplicationLifetime>();

View File

@@ -13,7 +13,7 @@ For extra details about options and events take a look here: https://refreshless
<pre>
<div ng-controller="My.Controller as vm">
<umb-range-slider
<umb-range-slider
ng-model="vm.value"
on-end="vm.slideEnd(values)">
</umb-range-slider>
@@ -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);

View File

@@ -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",

View File

@@ -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);
})();

View File

@@ -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);
* });
* </pre>

View File

@@ -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);
})();

View File

@@ -0,0 +1,59 @@
<div ng-controller="Umbraco.Dashboard.AnalyticsController as vm">
<umb-box>
<umb-box-content>
<h3 class="bold">
<localize key="analytics_consentForAnalytics">Consent for analytics</localize>
</h3>
<div class="umb-healthcheck-help-text">
<p>In order to improve Umbraco and add new functionality based on as relevant information as possible,
<br>we would like to collect system- and usage information from your installation.
<br>We will NOT collect any personal data like content, code or users, and all data will be fully anonymous.
<br>
<br>We will on a regular basis share some of the overall learnings from these metrics.
Hopefully, you'll help us collect some valuable data.</p>
<div ng-if="!vm.loading" style="padding-left: 12px;padding-top: 50px; padding-bottom: 50px; width: 25%">
<umb-range-slider
ng-model="vm.val"
on-setup="vm.setup(slider)"
options="vm.sliderOptions"
on-update="vm.sliderChange(values)">
</umb-range-slider>
</div>
<p>
<div ng-if="vm.sliderVal === 'Minimal'">
<b>{{vm.sliderVal}}</b>
<br>We'll only send an anonymous site ID to let us know that the site exists.
</div>
<div ng-if="vm.sliderVal === 'Basic'">
<b>{{vm.sliderVal}}</b>
<br>We'll send site ID, umbraco version and packages installed
</div>
<div ng-if="vm.sliderVal === 'Detailed'">
<b>{{vm.sliderVal}}</b>
<br> We'll send:
<br>- Site ID, umbraco version and packages installed
<br>- System information like Server OS and Webserver
<br>- Statistics, like number of content nodes and number of media items
<br>- Configuration settings, like modelsbuilder mode and used languages
<br>
<br>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
</div>
</p>
</div>
<div class="umb-panel-group__details-status-actions">
<umb-button type="button"
button-style="success"
label="Save"
action="vm.saveConsentLevel()"
ng-model="vm.sliderVal" ng-if="vm.consentLevel">
</umb-button>
</div>
</umb-box-content>
</umb-box>
</div>

View File

@@ -2508,6 +2508,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="settingsPublishedStatus">Published Status</key>
<key alias="settingsModelsBuilder">Models Builder</key>
<key alias="settingsHealthCheck">Health Check</key>
<key alias="settingsAnalytics">Analytics</key>
<key alias="settingsProfiler">Profiling</key>
<key alias="memberIntro">Getting Started</key>
<key alias="formsInstall">Install Umbraco Forms</key>
@@ -2863,4 +2864,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="searchResult">item returned</key>
<key alias="searchResults">items returned</key>
</area>
<area alias="analytics">
<key alias="consentForAnalytics">Consent for analytics</key>
<key alias="analyticsLevelSavedSuccess">Analytics level saved!</key>
</area>
</language>