summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/defaultagent/Telemetry.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/defaultagent/Telemetry.cpp')
-rw-r--r--toolkit/mozapps/defaultagent/Telemetry.cpp498
1 files changed, 498 insertions, 0 deletions
diff --git a/toolkit/mozapps/defaultagent/Telemetry.cpp b/toolkit/mozapps/defaultagent/Telemetry.cpp
new file mode 100644
index 0000000000..23f684d486
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Telemetry.cpp
@@ -0,0 +1,498 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Telemetry.h"
+
+#include <fstream>
+#include <string>
+
+#include <windows.h>
+
+#include <knownfolders.h>
+#include <shlobj_core.h>
+
+#include "common.h"
+#include "Cache.h"
+#include "EventLog.h"
+#include "Notification.h"
+#include "Policy.h"
+#include "UtfConvert.h"
+#include "Registry.h"
+
+#include "json/json.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/HelperMacros.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+#define TELEMETRY_BASE_URL "https://incoming.telemetry.mozilla.org/submit"
+#define TELEMETRY_NAMESPACE "default-browser-agent"
+#define TELEMETRY_PING_VERSION "1"
+#define TELEMETRY_PING_DOCTYPE "default-browser"
+
+// This is almost the complete URL, just needs a UUID appended.
+#define TELEMETRY_PING_URL \
+ TELEMETRY_BASE_URL "/" TELEMETRY_NAMESPACE "/" TELEMETRY_PING_DOCTYPE \
+ "/" TELEMETRY_PING_VERSION "/"
+
+// We only want to send one ping per day. However, this is slightly less than 24
+// hours so that we have a little bit of wiggle room on our task, which is also
+// supposed to run every 24 hours.
+#define MINIMUM_PING_PERIOD_SEC ((23 * 60 * 60) + (45 * 60))
+
+#define PREV_NOTIFICATION_ACTION_REG_NAME L"PrevNotificationAction"
+
+#if !defined(RRF_SUBKEY_WOW6464KEY)
+# define RRF_SUBKEY_WOW6464KEY 0x00010000
+#endif // !defined(RRF_SUBKEY_WOW6464KEY)
+
+using TelemetryFieldResult = mozilla::WindowsErrorResult<std::string>;
+using BoolResult = mozilla::WindowsErrorResult<bool>;
+
+// This function was copied from the implementation of
+// nsITelemetry::isOfficialTelemetry, currently found in the file
+// toolkit/components/telemetry/core/Telemetry.cpp.
+static bool IsOfficialTelemetry() {
+#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && \
+ !defined(DEBUG)
+ return true;
+#else
+ return false;
+#endif
+}
+
+static TelemetryFieldResult GetOSVersion() {
+ OSVERSIONINFOEXW osv = {sizeof(osv)};
+ if (::GetVersionExW(reinterpret_cast<OSVERSIONINFOW*>(&osv))) {
+ std::ostringstream oss;
+ oss << osv.dwMajorVersion << "." << osv.dwMinorVersion << "."
+ << osv.dwBuildNumber;
+
+ if (osv.dwMajorVersion == 10 && osv.dwMinorVersion == 0) {
+ // Get the "Update Build Revision" (UBR) value
+ DWORD ubrValue;
+ DWORD ubrValueLen = sizeof(ubrValue);
+ LSTATUS ubrOk =
+ ::RegGetValueW(HKEY_LOCAL_MACHINE,
+ L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
+ L"UBR", RRF_RT_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr,
+ &ubrValue, &ubrValueLen);
+ if (ubrOk == ERROR_SUCCESS) {
+ oss << "." << ubrValue;
+ }
+ }
+
+ return oss.str();
+ }
+
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+}
+
+static TelemetryFieldResult GetOSLocale() {
+ wchar_t localeName[LOCALE_NAME_MAX_LENGTH] = L"";
+ if (!GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH)) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ // We'll need the locale string in UTF-8 to be able to submit it.
+ Utf16ToUtf8Result narrowLocaleName = Utf16ToUtf8(localeName);
+
+ return narrowLocaleName.unwrapOr("");
+}
+
+static FilePathResult GetPingFilePath(std::wstring& uuid) {
+ wchar_t* rawAppDataPath;
+ HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr,
+ &rawAppDataPath);
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> appDataPath(
+ rawAppDataPath);
+
+ // The Path* functions don't set LastError, but this is the only thing that
+ // can really cause them to fail, so if they ever do we assume this is why.
+ hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+
+ wchar_t pingFilePath[MAX_PATH] = L"";
+ if (!PathCombineW(pingFilePath, appDataPath.get(), L"" MOZ_APP_VENDOR)) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ if (!PathAppendW(pingFilePath, L"" MOZ_APP_BASENAME)) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ if (!PathAppendW(pingFilePath, L"Pending Pings")) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ if (!PathAppendW(pingFilePath, uuid.c_str())) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return std::wstring(pingFilePath);
+}
+
+static mozilla::WindowsError SendPing(
+ const std::string defaultBrowser, const std::string previousDefaultBrowser,
+ const std::string defaultPdf, const std::string osVersion,
+ const std::string osLocale, const std::string notificationType,
+ const std::string notificationShown, const std::string notificationAction,
+ const std::string prevNotificationAction) {
+ // Fill in the ping JSON object.
+ Json::Value ping;
+ ping["build_channel"] = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL);
+ ping["build_version"] = MOZILLA_VERSION;
+ ping["default_browser"] = defaultBrowser;
+ ping["previous_default_browser"] = previousDefaultBrowser;
+ ping["default_pdf_viewer_raw"] = defaultPdf;
+ ping["os_version"] = osVersion;
+ ping["os_locale"] = osLocale;
+ ping["notification_type"] = notificationType;
+ ping["notification_shown"] = notificationShown;
+ ping["notification_action"] = notificationAction;
+ ping["previous_notification_action"] = prevNotificationAction;
+
+ // Stringify the JSON.
+ Json::StreamWriterBuilder jsonStream;
+ jsonStream["indentation"] = "";
+ std::string pingStr = Json::writeString(jsonStream, ping);
+
+ // Generate a UUID for the ping.
+ FilePathResult uuidResult = GenerateUUIDStr();
+ if (uuidResult.isErr()) {
+ return uuidResult.unwrapErr();
+ }
+ std::wstring uuid = uuidResult.unwrap();
+
+ // Write the JSON string to a file. Use the UUID in the file name so that if
+ // multiple instances of this task are running they'll have their own files.
+ FilePathResult pingFilePathResult = GetPingFilePath(uuid);
+ if (pingFilePathResult.isErr()) {
+ return pingFilePathResult.unwrapErr();
+ }
+ std::wstring pingFilePath = pingFilePathResult.unwrap();
+
+ {
+ std::ofstream outFile(pingFilePath);
+ outFile << pingStr;
+ if (outFile.fail()) {
+ // We have no way to get a specific error code out of a file stream
+ // other than to catch an exception, so substitute a generic error code.
+ HRESULT hr = HRESULT_FROM_WIN32(ERROR_IO_DEVICE);
+ LOG_ERROR(hr);
+ return mozilla::WindowsError::FromHResult(hr);
+ }
+ }
+
+ // Hand the file off to pingsender to submit.
+ FilePathResult pingsenderPathResult =
+ GetRelativeBinaryPath(L"pingsender.exe");
+ if (pingsenderPathResult.isErr()) {
+ return pingsenderPathResult.unwrapErr();
+ }
+ std::wstring pingsenderPath = pingsenderPathResult.unwrap();
+
+ std::wstring url(L"" TELEMETRY_PING_URL);
+ url.append(uuid);
+
+ const wchar_t* pingsenderArgs[] = {pingsenderPath.c_str(), url.c_str(),
+ pingFilePath.c_str()};
+ mozilla::UniquePtr<wchar_t[]> pingsenderCmdLine(
+ mozilla::MakeCommandLine(mozilla::ArrayLength(pingsenderArgs),
+ const_cast<wchar_t**>(pingsenderArgs)));
+
+ PROCESS_INFORMATION pi;
+ STARTUPINFOW si = {sizeof(si)};
+ si.dwFlags = STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_HIDE;
+ if (!::CreateProcessW(pingsenderPath.c_str(), pingsenderCmdLine.get(),
+ nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si,
+ &pi)) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return mozilla::WindowsError::FromHResult(hr);
+ }
+
+ CloseHandle(pi.hThread);
+ CloseHandle(pi.hProcess);
+
+ return mozilla::WindowsError::CreateSuccess();
+}
+
+// This function checks if a ping has already been sent today. If one has not,
+// it assumes that we are about to send one and sets a registry entry that will
+// cause this function to return true for the next day.
+// This function uses unprefixed registry entries, so a RegistryMutex should be
+// held before calling.
+static BoolResult GetPingAlreadySentToday() {
+ const wchar_t* valueName = L"LastPingSentAt";
+ MaybeQwordResult readResult =
+ RegistryGetValueQword(IsPrefixed::Unprefixed, valueName);
+ if (readResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr);
+ return BoolResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::Maybe<ULONGLONG> maybeValue = readResult.unwrap();
+ ULONGLONG now = GetCurrentTimestamp();
+ if (maybeValue.isSome()) {
+ ULONGLONG lastPingTime = maybeValue.value();
+ if (SecondsPassedSince(lastPingTime, now) < MINIMUM_PING_PERIOD_SEC) {
+ return true;
+ }
+ }
+
+ mozilla::WindowsErrorResult<mozilla::Ok> writeResult =
+ RegistrySetValueQword(IsPrefixed::Unprefixed, valueName, now);
+ if (writeResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr);
+ return BoolResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ return false;
+}
+
+// This both retrieves a value from the registry and writes new data
+// (currentDefault) to the same value. If there is no value stored, the value
+// passed for prevDefault will be converted to a string and returned instead.
+//
+// Although we already store and retrieve a cached previous default browser
+// value elsewhere, it may be updated when we don't send a ping. The value we
+// retrieve here will only be updated when we are sending a ping to ensure
+// that pings don't miss a default browser transition.
+static TelemetryFieldResult GetAndUpdatePreviousDefaultBrowser(
+ const std::string& currentDefault, Browser prevDefault) {
+ const wchar_t* registryValueName = L"PingCurrentDefault";
+
+ MaybeStringResult readResult =
+ RegistryGetValueString(IsPrefixed::Unprefixed, registryValueName);
+ if (readResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::Maybe<std::string> maybeValue = readResult.unwrap();
+ std::string oldCurrentDefault;
+ if (maybeValue.isSome()) {
+ oldCurrentDefault = maybeValue.value();
+ } else {
+ oldCurrentDefault = GetStringForBrowser(prevDefault);
+ }
+
+ mozilla::WindowsErrorResult<mozilla::Ok> writeResult = RegistrySetValueString(
+ IsPrefixed::Unprefixed, registryValueName, currentDefault.c_str());
+ if (writeResult.isErr()) {
+ HRESULT hr = writeResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ return oldCurrentDefault;
+}
+
+// If notifications actions occurred, we want to make sure a ping gets sent for
+// them. If we aren't sending a ping right now, we want to cache the ping values
+// for the next time the ping is sent.
+// The values passed will only be cached if actions were actually taken
+// (i.e. not when notificationShown == "not-shown")
+HRESULT MaybeCache(Cache& cache, const std::string& notificationType,
+ const std::string& notificationShown,
+ const std::string& notificationAction,
+ const std::string& prevNotificationAction) {
+ std::string notShown =
+ GetStringForNotificationShown(NotificationShown::NotShown);
+ if (notificationShown == notShown) {
+ return S_OK;
+ }
+
+ Cache::Entry entry{
+ .notificationType = notificationType,
+ .notificationShown = notificationShown,
+ .notificationAction = notificationAction,
+ .prevNotificationAction = prevNotificationAction,
+ };
+ VoidResult result = cache.Enqueue(entry);
+ if (result.isErr()) {
+ return result.unwrapErr().AsHResult();
+ }
+ return S_OK;
+}
+
+// This function retrieves values cached by MaybeCache. If any values were
+// loaded from the cache, the values passed in to this function are passed to
+// MaybeCache so that they are not lost. If there are no values in the cache,
+// the values passed will not be changed.
+// Values retrieved from the cache will also be removed from it.
+HRESULT MaybeSwapForCached(Cache& cache, std::string& notificationType,
+ std::string& notificationShown,
+ std::string& notificationAction,
+ std::string& prevNotificationAction) {
+ Cache::MaybeEntryResult result = cache.Dequeue();
+ if (result.isErr()) {
+ HRESULT hr = result.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Failed to read cache: %#X", hr);
+ return hr;
+ }
+ Cache::MaybeEntry maybeEntry = result.unwrap();
+ if (maybeEntry.isNothing()) {
+ return S_OK;
+ }
+
+ MaybeCache(cache, notificationType, notificationShown, notificationAction,
+ prevNotificationAction);
+ notificationType = maybeEntry.value().notificationType;
+ notificationShown = maybeEntry.value().notificationShown;
+ notificationAction = maybeEntry.value().notificationAction;
+ if (maybeEntry.value().prevNotificationAction.isSome()) {
+ prevNotificationAction = maybeEntry.value().prevNotificationAction.value();
+ } else {
+ prevNotificationAction =
+ GetStringForNotificationAction(NotificationAction::NoAction);
+ }
+ return S_OK;
+}
+
+HRESULT ReadPreviousNotificationAction(std::string& prevAction) {
+ MaybeStringResult maybePrevActionResult = RegistryGetValueString(
+ IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME);
+ if (maybePrevActionResult.isErr()) {
+ HRESULT hr = maybePrevActionResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to read prev action from registry: %#X", hr);
+ return hr;
+ }
+ mozilla::Maybe<std::string> maybePrevAction = maybePrevActionResult.unwrap();
+ if (maybePrevAction.isNothing()) {
+ prevAction = GetStringForNotificationAction(NotificationAction::NoAction);
+ } else {
+ prevAction = maybePrevAction.value();
+ // There's no good reason why there should be an invalid value stored here.
+ // But it's also not worth aborting the whole ping over. This function will
+ // silently change it to "no-action" if the value isn't valid to prevent us
+ // from sending unexpected telemetry values.
+ EnsureValidNotificationAction(prevAction);
+ }
+ return S_OK;
+}
+
+// Writes the previous notification action to the registry, but only if a
+// notification was shown.
+HRESULT MaybeWritePreviousNotificationAction(
+ const NotificationActivities& activitiesPerformed) {
+ if (activitiesPerformed.shown != NotificationShown::Shown) {
+ return S_OK;
+ }
+ std::string notificationAction =
+ GetStringForNotificationAction(activitiesPerformed.action);
+ mozilla::WindowsErrorResult<mozilla::Ok> result = RegistrySetValueString(
+ IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME,
+ notificationAction.c_str());
+ if (result.isErr()) {
+ HRESULT hr = result.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to write prev action to registry: %#X", hr);
+ return hr;
+ }
+ return S_OK;
+}
+
+HRESULT SendDefaultBrowserPing(
+ const DefaultBrowserInfo& browserInfo, const DefaultPdfInfo& pdfInfo,
+ const NotificationActivities& activitiesPerformed) {
+ std::string currentDefaultBrowser =
+ GetStringForBrowser(browserInfo.currentDefaultBrowser);
+ std::string currentDefaultPdf = pdfInfo.currentDefaultPdf;
+ std::string notificationType =
+ GetStringForNotificationType(activitiesPerformed.type);
+ std::string notificationShown =
+ GetStringForNotificationShown(activitiesPerformed.shown);
+ std::string notificationAction =
+ GetStringForNotificationAction(activitiesPerformed.action);
+
+ TelemetryFieldResult osVersionResult = GetOSVersion();
+ if (osVersionResult.isErr()) {
+ return osVersionResult.unwrapErr().AsHResult();
+ }
+ std::string osVersion = osVersionResult.unwrap();
+
+ TelemetryFieldResult osLocaleResult = GetOSLocale();
+ if (osLocaleResult.isErr()) {
+ return osLocaleResult.unwrapErr().AsHResult();
+ }
+ std::string osLocale = osLocaleResult.unwrap();
+
+ std::string prevNotificationAction;
+ HRESULT hr = ReadPreviousNotificationAction(prevNotificationAction);
+ if (FAILED(hr)) {
+ return hr;
+ }
+ // Intentionally discard the result of this write. There's no real reason
+ // to abort sending the ping in the error case and it already wrote an error
+ // message. So there isn't really anything to do at this point.
+ MaybeWritePreviousNotificationAction(activitiesPerformed);
+
+ Cache cache;
+
+ // Do not send the ping if we are not an official telemetry-enabled build;
+ // don't even generate the ping in fact, because if we write the file out
+ // then some other build might find it later and decide to submit it.
+ if (!IsOfficialTelemetry() || IsTelemetryDisabled()) {
+ return MaybeCache(cache, notificationType, notificationShown,
+ notificationAction, prevNotificationAction);
+ }
+
+ // Pings are limited to one per day (across all installations), so check if we
+ // already sent one today.
+ // This will also set a registry entry indicating that the last ping was
+ // just sent, to prevent another one from being sent today. We'll do this
+ // now even though we haven't sent the ping yet. After this check, we send
+ // a ping unconditionally. The only exception is for errors, and any error
+ // that we get now will probably be hit every time.
+ // Because unsent pings attempted with pingsender can get automatically
+ // re-sent later, we don't even want to try again on transient network
+ // failures.
+ BoolResult pingAlreadySentResult = GetPingAlreadySentToday();
+ if (pingAlreadySentResult.isErr()) {
+ return pingAlreadySentResult.unwrapErr().AsHResult();
+ }
+ bool pingAlreadySent = pingAlreadySentResult.unwrap();
+ if (pingAlreadySent) {
+ return MaybeCache(cache, notificationType, notificationShown,
+ notificationAction, prevNotificationAction);
+ }
+
+ hr = MaybeSwapForCached(cache, notificationType, notificationShown,
+ notificationAction, prevNotificationAction);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ // Don't update the registry's default browser data until we are sure we
+ // want to send a ping. Otherwise it could be updated to reflect a ping we
+ // never sent.
+ TelemetryFieldResult previousDefaultBrowserResult =
+ GetAndUpdatePreviousDefaultBrowser(currentDefaultBrowser,
+ browserInfo.previousDefaultBrowser);
+ if (previousDefaultBrowserResult.isErr()) {
+ return previousDefaultBrowserResult.unwrapErr().AsHResult();
+ }
+ std::string previousDefaultBrowser = previousDefaultBrowserResult.unwrap();
+
+ return SendPing(currentDefaultBrowser, previousDefaultBrowser,
+ currentDefaultPdf, osVersion, osLocale, notificationType,
+ notificationShown, notificationAction, prevNotificationAction)
+ .AsHResult();
+}