diff --git a/.gitignore b/.gitignore index eee2a46a44..3ae099a82f 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,4 @@ cypress.env.json /src/Umbraco.Tests.AcceptanceTest/package-lock.json /src/Umbraco.Tests.AcceptanceTest/cypress/videos/ /src/Umbraco.Tests.AcceptanceTest/cypress/screenshots/ +src/Umbraco.Web.UI/Umbraco/telemetrics-id.umb diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs index 12e3f57d99..d33e9dfdfc 100644 --- a/src/Umbraco.Core/IO/SystemFiles.cs +++ b/src/Umbraco.Core/IO/SystemFiles.cs @@ -7,6 +7,8 @@ namespace Umbraco.Core.IO { public static string TinyMceConfig => SystemDirectories.Config + "/tinyMceConfig.config"; + public static string TelemetricsIdentifier => SystemDirectories.Data + "/telemetrics-id.umb"; + // TODO: Kill this off we don't have umbraco.config XML cache we now have NuCache public static string GetContentCacheXml(IGlobalSettings globalSettings) { diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 0a67a5726d..3f9e511ec4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -199,7 +199,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.9.0 To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); - // to 8.10.0... + // to 8.10.0 To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); //FINAL diff --git a/src/Umbraco.Web/Telemetry/ReportSiteTask.cs b/src/Umbraco.Web/Telemetry/ReportSiteTask.cs new file mode 100644 index 0000000000..fcac8570d2 --- /dev/null +++ b/src/Umbraco.Web/Telemetry/ReportSiteTask.cs @@ -0,0 +1,132 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Web.Telemetry +{ + public class ReportSiteTask : RecurringTaskBase + { + private IProfilingLogger _logger; + private static HttpClient _httpClient; + + public ReportSiteTask(IBackgroundTaskRunner runner, int delayBeforeWeStart, int howOftenWeRepeat, IProfilingLogger logger) + : base(runner, delayBeforeWeStart, howOftenWeRepeat) + { + _logger = logger; + _httpClient = new HttpClient(); + } + + /// + /// Runs the background task to send the anynomous ID + /// to telemetry service + /// + /// A value indicating whether to repeat the task. + public override async Task PerformRunAsync(CancellationToken token) + { + // Try & find file at '/umbraco/telemetrics-id.umb' + var telemetricsFilePath = IOHelper.MapPath(SystemFiles.TelemetricsIdentifier); + + if (File.Exists(telemetricsFilePath) == false) + { + // Some users may have decided to not be tracked by deleting/removing the marker file + _logger.Warn("No telemetry marker file found at '{filePath}' and will not report site to telemetry service", telemetricsFilePath); + + // Stop repeating this task (no need to keep checking) + // The only time it will recheck when the site is recycled + return false; + } + + + var telemetricsFileContents = string.Empty; + try + { + // Open file & read its contents + // It may throw due to file permissions or file locking + telemetricsFileContents = File.ReadAllText(telemetricsFilePath); + } + catch (Exception ex) + { + // Silently swallow ex - but lets log it (ReadAllText throws a ton of different types of ex) + // Hence the use of general exception type + _logger.Error(ex, "Error in reading file contents of telemetry marker file found at '{filePath}'", telemetricsFilePath); + + // Exit out early, but mark this task to be repeated in case its a file lock so it can be rechecked the next time round + return true; + } + + + // 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(telemetricsFileContents, out var telemetrySiteIdentifier) == false) + { + // Some users may have decided to mess with file contents + _logger.Warn("The telemetry marker file found at '{filePath}' with '{telemetrySiteId}' is not a valid identifier for the telemetry service", telemetricsFilePath, telemetrySiteIdentifier); + + // Stop repeating this task (no need to keep checking) + // The only time it will recheck when the site is recycled + return false; + } + + try + { + + // Send data to LIVE telemetry + _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); + +#if DEBUG + // Send data to DEBUG telemetry service + _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); +#endif + + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + + using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) + { + var postData = new TelemetryReportData { Id = telemetrySiteIdentifier, Version = UmbracoVersion.SemanticVersion.ToSemanticString() }; + request.Content = new StringContent(JsonConvert.SerializeObject(postData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header + + // Set a low timeout - no need to use a larger default timeout for this POST request + _httpClient.Timeout = new TimeSpan(0, 0, 1); + + // Make a HTTP Post to telemetry service + // https://telemetry.umbraco.com/installs/ + // Fire & Forget, do not need to know if its a 200, 500 etc + var result = await _httpClient.SendAsync(request); + } + } + catch + { + // Silently swallow + // The user does not need the logs being polluted if our service has fallen over or is down etc + // Hence only loggigng this at a more verbose level (Which users should not be using in prod) + _logger.Debug("There was a problem sending a request to the Umbraco telemetry service"); + } + + // Keep recurring this task & pinging the telemetry service + return true; + } + + public override bool IsAsync => true; + + + [DataContract] + private class TelemetryReportData + { + [DataMember(Name = "id")] + public Guid Id { get; set; } + + [DataMember(Name = "version")] + public string Version { get; set; } + } + } +} diff --git a/src/Umbraco.Web/Telemetry/TelemetryComponent.cs b/src/Umbraco.Web/Telemetry/TelemetryComponent.cs new file mode 100644 index 0000000000..4ea4c6573a --- /dev/null +++ b/src/Umbraco.Web/Telemetry/TelemetryComponent.cs @@ -0,0 +1,34 @@ +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Web.Telemetry +{ + public class TelemetryComponent : IComponent + { + private IProfilingLogger _logger; + private BackgroundTaskRunner _telemetryReporterRunner; + + public TelemetryComponent(IProfilingLogger logger) + { + _logger = logger; + } + + public void Initialize() + { + // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly + _telemetryReporterRunner = new BackgroundTaskRunner("TelemetryReporter", _logger); + + int delayBeforeWeStart = 60 * 1000; // 60 * 1000ms = 1min (60,000) + int howOftenWeRepeat = 60 * 1000 * 60 * 24; // 60 * 1000 * 60 * 24 = 24hrs (86400000) + + // As soon as we add our task to the runner it will start to run (after its delay period) + var task = new ReportSiteTask(_telemetryReporterRunner, delayBeforeWeStart, howOftenWeRepeat, _logger); + _telemetryReporterRunner.TryAdd(task); + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web/Telemetry/TelemetryComposer.cs b/src/Umbraco.Web/Telemetry/TelemetryComposer.cs new file mode 100644 index 0000000000..933b302e1c --- /dev/null +++ b/src/Umbraco.Web/Telemetry/TelemetryComposer.cs @@ -0,0 +1,10 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Telemetry +{ + + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class TelemetryComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Web/Telemetry/TelemetryMarkerComponent.cs b/src/Umbraco.Web/Telemetry/TelemetryMarkerComponent.cs new file mode 100644 index 0000000000..36cae322ce --- /dev/null +++ b/src/Umbraco.Web/Telemetry/TelemetryMarkerComponent.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Telemetry +{ + public class TelemetryMarkerComponent : IComponent + { + private IProfilingLogger _logger; + private IRuntimeState _runtime; + + public TelemetryMarkerComponent(IProfilingLogger logger, IRuntimeState runtime) + { + _logger = logger; + _runtime = runtime; + } + + public void Initialize() + { + var telemetricsFilePath = IOHelper.MapPath(SystemFiles.TelemetricsIdentifier); + + // Verify file does not exist already (if we are upgrading) + // In a clean install we know it would not exist + // If the site is upgraded and the file was removed it would re-create one + // NOTE: If user removed the marker file to opt out it would re-create a new guid marker file & potentially skew + if (_runtime.Level == RuntimeLevel.Upgrade && File.Exists(telemetricsFilePath)) + { + _logger.Warn("When upgrading the anonymous telemetry file already existsed on disk at {filePath}", telemetricsFilePath); + return; + } + else if (_runtime.Level == RuntimeLevel.Install && File.Exists(telemetricsFilePath)) + { + // No need to log for when level is install if file exists (As this component hit several times during install process) + return; + } + + // We are a clean install or an upgrade without the marker file + // Generate GUID + var telemetrySiteIdentifier = Guid.NewGuid(); + + // Write file contents + try + { + File.WriteAllText(telemetricsFilePath, telemetrySiteIdentifier.ToString()); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to create telemetry file at {filePath}", telemetricsFilePath); + } + + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web/Telemetry/TelemetryMarkerComposer.cs b/src/Umbraco.Web/Telemetry/TelemetryMarkerComposer.cs new file mode 100644 index 0000000000..e01b4a7f10 --- /dev/null +++ b/src/Umbraco.Web/Telemetry/TelemetryMarkerComposer.cs @@ -0,0 +1,9 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Telemetry +{ + [RuntimeLevel(MinLevel = RuntimeLevel.Install, MaxLevel = RuntimeLevel.Upgrade)] + public class TelemetryMarkerComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index fa98cebd05..1f8d4d7881 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -295,6 +295,11 @@ + + + + +