diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
index 94b8d88ed3..97fb91b0ec 100644
--- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
@@ -130,6 +130,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// Gets or sets a value for the main dom lock.
///
public string MainDomLock { get; set; } = string.Empty;
+
public string Id { get; set; } = string.Empty;
///
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
index 36d7918531..1af25e16e8 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
@@ -36,6 +36,7 @@ using Umbraco.Cms.Core.Runtime;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
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;
@@ -259,6 +260,9 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Register ValueEditorCache used for validation
Services.AddSingleton();
+
+ // Register telemetry service used to gather data about installed packages
+ Services.AddUnique();
}
}
}
diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs
index 753ec0613a..ae6ffdc8ba 100644
--- a/src/Umbraco.Core/Manifest/PackageManifest.cs
+++ b/src/Umbraco.Core/Manifest/PackageManifest.cs
@@ -48,6 +48,19 @@ namespace Umbraco.Cms.Core.Manifest
///
[IgnoreDataMember]
public string Source { get; set; }
+
+ ///
+ /// Gets or sets the version of the package
+ ///
+ [DataMember(Name = "version")]
+ public string Version { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether telemetry is allowed
+ ///
+ [DataMember(Name = "allowPackageTelemetry")]
+ public bool AllowPackageTelemetry { get; set; } = true;
+
[DataMember(Name = "bundleOptions")]
public BundleOptions BundleOptions { get; set; }
diff --git a/src/Umbraco.Core/Telemetry/ITelemetryService.cs b/src/Umbraco.Core/Telemetry/ITelemetryService.cs
new file mode 100644
index 0000000000..60070481f3
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/ITelemetryService.cs
@@ -0,0 +1,15 @@
+using Umbraco.Cms.Core.Telemetry.Models;
+
+namespace Umbraco.Cms.Core.Telemetry
+{
+ ///
+ /// Service which gathers the data for telemetry reporting
+ ///
+ public interface ITelemetryService
+ {
+ ///
+ /// Try and get the
+ ///
+ bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData);
+ }
+}
diff --git a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs
new file mode 100644
index 0000000000..8b7aa4bc0c
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs
@@ -0,0 +1,26 @@
+using System.Runtime.Serialization;
+
+namespace Umbraco.Cms.Core.Telemetry.Models
+{
+ ///
+ /// Serializable class containing information about an installed package.
+ ///
+ [DataContract(Name = "packageTelemetry")]
+ public class PackageTelemetry
+ {
+ ///
+ /// Gets or sets the name of the installed package.
+ ///
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the version of the installed package.
+ ///
+ ///
+ /// This may be an empty string if no version is specified, or if package telemetry has been restricted.
+ ///
+ [DataMember(Name = "version")]
+ public string Version { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs
new file mode 100644
index 0000000000..d19e24695b
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Cms.Core.Telemetry.Models
+{
+ ///
+ /// Serializable class containing telemetry information.
+ ///
+ [DataContract]
+ public class TelemetryReportData
+ {
+ ///
+ /// Gets or sets a random GUID to prevent an instance posting multiple times pr. day.
+ ///
+ [DataMember(Name = "id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the Umbraco CMS version.
+ ///
+ [DataMember(Name = "version")]
+ public string Version { get; set; }
+
+ ///
+ /// Gets or sets an enumerable containing information about packages.
+ ///
+ ///
+ /// Contains only the name and version of the packages, unless no version is specified.
+ ///
+ [DataMember(Name = "packages")]
+ public IEnumerable Packages { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs
new file mode 100644
index 0000000000..63e4e1ff49
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Manifest;
+using Umbraco.Cms.Core.Telemetry.Models;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.Telemetry
+{
+ ///
+ internal class TelemetryService : ITelemetryService
+ {
+ private readonly IOptionsMonitor _globalSettings;
+ private readonly IManifestParser _manifestParser;
+ private readonly IUmbracoVersion _umbracoVersion;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TelemetryService(
+ IOptionsMonitor globalSettings,
+ IManifestParser manifestParser,
+ IUmbracoVersion umbracoVersion)
+ {
+ _manifestParser = manifestParser;
+ _umbracoVersion = umbracoVersion;
+ _globalSettings = globalSettings;
+ }
+
+ ///
+ public bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData)
+ {
+ if (TryGetTelemetryId(out Guid telemetryId) is false)
+ {
+ telemetryReportData = null;
+ return false;
+ }
+
+ telemetryReportData = new TelemetryReportData
+ {
+ Id = telemetryId,
+ Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(),
+ Packages = GetPackageTelemetry()
+ };
+ return true;
+ }
+
+ private bool TryGetTelemetryId(out Guid telemetryId)
+ {
+ // Parse telemetry string as a GUID & verify its a GUID and not some random string
+ // since users may have messed with or decided to empty the app setting or put in something random
+ if (Guid.TryParse(_globalSettings.CurrentValue.Id, out var parsedTelemetryId) is false)
+ {
+ telemetryId = Guid.Empty;
+ return false;
+ }
+
+ telemetryId = parsedTelemetryId;
+ return true;
+ }
+
+ private IEnumerable GetPackageTelemetry()
+ {
+ List packages = new ();
+ IEnumerable manifests = _manifestParser.GetManifests();
+
+ foreach (PackageManifest manifest in manifests)
+ {
+ if (manifest.AllowPackageTelemetry is false)
+ {
+ continue;
+ }
+
+ packages.Add(new PackageTelemetry
+ {
+ Name = manifest.PackageName,
+ Version = manifest.Version ?? string.Empty
+ });
+ }
+
+ return packages;
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
index 6eab3a60bc..7f88d063d8 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
@@ -1,33 +1,27 @@
using System;
using System.Net.Http;
-using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using Newtonsoft.Json;
-using Umbraco.Cms.Core.Configuration;
-using Umbraco.Cms.Core.Configuration.Models;
-using Umbraco.Extensions;
+using Umbraco.Cms.Core.Telemetry;
+using Umbraco.Cms.Core.Telemetry.Models;
namespace Umbraco.Cms.Infrastructure.HostedServices
{
public class ReportSiteTask : RecurringHostedServiceBase
{
private readonly ILogger _logger;
- private readonly IUmbracoVersion _umbracoVersion;
- private readonly IOptions _globalSettings;
+ private readonly ITelemetryService _telemetryService;
private static HttpClient s_httpClient;
public ReportSiteTask(
ILogger logger,
- IUmbracoVersion umbracoVersion,
- IOptions globalSettings)
+ ITelemetryService telemetryService)
: base(TimeSpan.FromDays(1), TimeSpan.FromMinutes(1))
{
_logger = logger;
- _umbracoVersion = umbracoVersion;
- _globalSettings = globalSettings;
+ _telemetryService = telemetryService;
s_httpClient = new HttpClient();
}
@@ -37,14 +31,8 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
///
public override async Task PerformExecuteAsync(object state)
{
- // Try & get a value stored in umbracoSettings.config on the backoffice XML element ID attribute
- var backofficeIdentifierRaw = _globalSettings.Value.Id;
-
- // Parse as a GUID & verify its a GUID and not some random string
- // In case of users may have messed or decided to empty the file contents or put in something random
- if (Guid.TryParse(backofficeIdentifierRaw, out var telemetrySiteIdentifier) == false)
+ if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData telemetryReportData) is false)
{
- // Some users may have decided to mess with the XML attribute and put in something else
_logger.LogWarning("No telemetry marker found");
return;
@@ -52,7 +40,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
try
{
-
if (s_httpClient.BaseAddress is null)
{
// Send data to LIVE telemetry
@@ -64,9 +51,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
#if DEBUG
// Send data to DEBUG telemetry service
s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
-
-
-
#endif
}
@@ -75,8 +59,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/"))
{
- var postData = new TelemetryReportData { Id = telemetrySiteIdentifier, Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild() };
- request.Content = new StringContent(JsonConvert.SerializeObject(postData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header
+ request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header
// Make a HTTP Post to telemetry service
// https://telemetry.umbraco.com/installs/
@@ -94,16 +77,5 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
_logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service");
}
}
- [DataContract]
- private class TelemetryReportData
- {
- [DataMember(Name = "id")]
- public Guid Id { get; set; }
-
- [DataMember(Name = "version")]
- public string Version { get; set; }
- }
-
-
}
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ManifestParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ManifestParserTests.cs
index c93e5087b7..fda216e51a 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ManifestParserTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ManifestParserTests.cs
@@ -484,5 +484,27 @@ javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2
Assert.AreEqual("Content", manifest.Sections[0].Name);
Assert.AreEqual("World", manifest.Sections[1].Name);
}
+
+ [Test]
+ public void CanParseManifest_Version()
+ {
+ const string json = @"{""name"": ""VersionPackage"", ""version"": ""1.0.0""}";
+ PackageManifest manifest = _parser.ParseManifest(json);
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual("VersionPackage", manifest.PackageName);
+ Assert.AreEqual("1.0.0", manifest.Version);
+ });
+ }
+
+ [Test]
+ public void CanParseManifest_TrackingAllowed()
+ {
+ const string json = @"{""allowPackageTelemetry"": false }";
+ PackageManifest manifest = _parser.ParseManifest(json);
+
+ Assert.IsFalse(manifest.AllowPackageTelemetry);
+ }
}
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs
new file mode 100644
index 0000000000..1c92569695
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs
@@ -0,0 +1,135 @@
+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.Semver;
+using Umbraco.Cms.Core.Telemetry;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry
+{
+ [TestFixture]
+ public class TelemetryServiceTests
+ {
+ [TestCase("0F1785C5-7BA0-4C52-AB62-863BD2C8F3FE", true)]
+ [TestCase("This is not a guid", false)]
+ [TestCase("", false)]
+ public void OnlyParsesIfValidId(string guidString, bool shouldSucceed)
+ {
+ var globalSettings = CreateGlobalSettings(guidString);
+ var version = CreateUmbracoVersion(9, 1, 1);
+ var sut = new TelemetryService(globalSettings, Mock.Of(), version);
+
+ var result = sut.TryGetTelemetryReportData(out var telemetry);
+
+ Assert.AreEqual(shouldSucceed, result);
+ if (shouldSucceed)
+ {
+ // When toString is called on a GUID it will to lower, so do the same to our guidString
+ Assert.AreEqual(guidString.ToLower(), telemetry.Id.ToString());
+ }
+ else
+ {
+ Assert.IsNull(telemetry);
+ }
+ }
+
+ [Test]
+ public void ReturnsSemanticVersionWithoutBuild()
+ {
+ var globalSettings = CreateGlobalSettings();
+ var version = CreateUmbracoVersion(9, 1, 1, "-rc", "-ad2f4k2d");
+
+ var sut = new TelemetryService(globalSettings, Mock.Of(), version);
+
+ var result = sut.TryGetTelemetryReportData(out var telemetry);
+
+ Assert.IsTrue(result);
+ Assert.AreEqual("9.1.1-rc", telemetry.Version);
+ }
+
+ [Test]
+ public void CanGatherPackageTelemetry()
+ {
+ var globalSettings = CreateGlobalSettings();
+ var version = CreateUmbracoVersion(9, 1, 1);
+ var versionPackageName = "VersionPackage";
+ var packageVersion = "1.0.0";
+ var noVersionPackageName = "NoVersionPackage";
+ PackageManifest[] manifests =
+ {
+ new () { PackageName = versionPackageName, Version = packageVersion },
+ new () { PackageName = noVersionPackageName }
+ };
+ var manifestParser = CreateManifestParser(manifests);
+ var sut = new TelemetryService(globalSettings, manifestParser, version);
+
+ var success = sut.TryGetTelemetryReportData(out var telemetry);
+
+ Assert.IsTrue(success);
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, telemetry.Packages.Count());
+ var versionPackage = telemetry.Packages.FirstOrDefault(x => x.Name == versionPackageName);
+ Assert.AreEqual(versionPackageName, versionPackage.Name);
+ Assert.AreEqual(packageVersion, versionPackage.Version);
+
+ var noVersionPackage = telemetry.Packages.FirstOrDefault(x => x.Name == noVersionPackageName);
+ Assert.AreEqual(noVersionPackageName, noVersionPackage.Name);
+ Assert.AreEqual(string.Empty, noVersionPackage.Version);
+ });
+ }
+
+ [Test]
+ public void RespectsAllowPackageTelemetry()
+ {
+ var globalSettings = CreateGlobalSettings();
+ var version = CreateUmbracoVersion(9, 1, 1);
+ PackageManifest[] manifests =
+ {
+ new () { PackageName = "DoNotTrack", AllowPackageTelemetry = false },
+ new () { PackageName = "TrackingAllowed", AllowPackageTelemetry = true }
+ };
+ var manifestParser = CreateManifestParser(manifests);
+ var sut = new TelemetryService(globalSettings, manifestParser, version);
+
+ var success = sut.TryGetTelemetryReportData(out var telemetry);
+
+ Assert.IsTrue(success);
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(1, telemetry.Packages.Count());
+ Assert.AreEqual("TrackingAllowed", telemetry.Packages.First().Name);
+ });
+ }
+
+
+ private IManifestParser CreateManifestParser(IEnumerable manifests)
+ {
+ var manifestParserMock = new Mock();
+ manifestParserMock.Setup(x => x.GetManifests()).Returns(manifests);
+ return manifestParserMock.Object;
+ }
+
+ private IUmbracoVersion CreateUmbracoVersion(int major, int minor, int patch, string prerelease = "", string build = "")
+ {
+ var version = new SemVersion(major, minor, patch, prerelease, build);
+ return Mock.Of(x => x.SemanticVersion == version);
+ }
+
+ private IOptionsMonitor CreateGlobalSettings(string guidString = null)
+ {
+ if (guidString is null)
+ {
+ guidString = Guid.NewGuid().ToString();
+ }
+
+ var globalSettings = new GlobalSettings { Id = guidString };
+ return Mock.Of>(x => x.CurrentValue == globalSettings);
+ }
+ }
+}