summaryrefslogtreecommitdiffstats
path: root/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp')
-rw-r--r--toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp392
1 files changed, 392 insertions, 0 deletions
diff --git a/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp b/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp
new file mode 100644
index 0000000000..23906650f4
--- /dev/null
+++ b/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp
@@ -0,0 +1,392 @@
+/* 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 "nsCookieBannerTelemetryService.h"
+
+#include "cookieBanner.pb.h"
+
+#include "mozilla/Base64.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Logging.h"
+#include "Cookie.h"
+#include "nsCRT.h"
+#include "nsError.h"
+#include "nsICookie.h"
+#include "nsICookieManager.h"
+#include "nsICookieNotification.h"
+#include "nsTHashSet.h"
+#include "nsIObserverService.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsISearchService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(nsCookieBannerTelemetryService,
+ nsICookieBannerTelemetryService, nsIObserver)
+
+static LazyLogModule gCookieBannerTelemetryLog(
+ "nsCookieBannerTelemetryService");
+
+static StaticRefPtr<nsCookieBannerTelemetryService>
+ sCookieBannerTelemetryServiceSingleton;
+
+// A hash set used to tell whether a base domain is a Google domain.
+static StaticAutoPtr<nsTHashSet<nsCString>> sGoogleDomainsSet;
+
+namespace {
+
+// A helper function that decodes Google's SOCS cookie and returns the GDPR
+// choice and the region. The choice and be either "Accept", "Reject", or
+// "Custom".
+nsresult DecodeSOCSGoogleCookie(const nsACString& aCookie, nsACString& aChoice,
+ nsACString& aRegion) {
+ aChoice.Truncate();
+ aRegion.Truncate();
+
+ FallibleTArray<uint8_t> decoded;
+ nsresult rv =
+ Base64URLDecode(aCookie, Base64URLDecodePaddingPolicy::Ignore, decoded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ std::string buf(reinterpret_cast<const char*>(decoded.Elements()),
+ decoded.Length());
+ cookieBanner::GoogleSOCSCookie socs;
+
+ if (!socs.ParseFromString(buf)) {
+ return NS_ERROR_CANNOT_CONVERT_DATA;
+ }
+
+ aRegion.Assign(socs.data().region().c_str(), socs.data().region().length());
+
+ // The first field represents the gdpr choice, 1 means "Reject" and 2 means
+ // either "Accept" or "Custom". We need to check a following field to decide.
+ if (socs.gdpr_choice() == 1) {
+ aChoice.AssignLiteral("Reject");
+ return NS_OK;
+ }
+
+ // The platform field represents where does this GDPR consent is selected. The
+ // value can be either "gws_*" or "boq_identityfrontenduiserver_*". If the
+ // field value starts with "gws_", it means the consent is from the Google
+ // search page. Otherwise, it's from the consent.google.com, which is used for
+ // the custom setting.
+ std::string prefix = "gws_";
+
+ if (socs.data().platform().compare(0, prefix.length(), prefix) == 0) {
+ aChoice.AssignLiteral("Accept");
+ return NS_OK;
+ }
+
+ aChoice.AssignLiteral("Custom");
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+// static
+already_AddRefed<nsCookieBannerTelemetryService>
+nsCookieBannerTelemetryService::GetSingleton() {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("GetSingleton."));
+
+ if (!sCookieBannerTelemetryServiceSingleton) {
+ sCookieBannerTelemetryServiceSingleton =
+ new nsCookieBannerTelemetryService();
+
+ RunOnShutdown([] {
+ DebugOnly<nsresult> rv =
+ sCookieBannerTelemetryServiceSingleton->Shutdown();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCookieBannerTelemetryService::Shutdown failed.");
+
+ sCookieBannerTelemetryServiceSingleton = nullptr;
+ });
+ }
+
+ return do_AddRef(sCookieBannerTelemetryServiceSingleton);
+}
+
+nsresult nsCookieBannerTelemetryService::Init() {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Init."));
+ if (mIsInitialized) {
+ return NS_OK;
+ }
+
+ mIsInitialized = true;
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
+
+ nsresult rv = obsSvc->AddObserver(this, "browser-search-service", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obsSvc->AddObserver(this, "idle-daily", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obsSvc->AddObserver(this, "cookie-changed", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obsSvc->AddObserver(this, "private-cookie-changed", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult nsCookieBannerTelemetryService::Shutdown() {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Shutdown."));
+
+ if (!mIsInitialized) {
+ return NS_OK;
+ }
+
+ mIsInitialized = false;
+
+ sGoogleDomainsSet = nullptr;
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
+
+ nsresult rv = obsSvc->RemoveObserver(this, "browser-search-service");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obsSvc->RemoveObserver(this, "idle-daily");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obsSvc->RemoveObserver(this, "cookie-changed");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obsSvc->RemoveObserver(this, "private-cookie-changed");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCookieBannerTelemetryService::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ if (nsCRT::strcmp(aTopic, "profile-after-change") == 0) {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
+ ("Observe profile-after-change"));
+
+ return Init();
+ }
+
+ if (nsCRT::strcmp(aTopic, "idle-daily") == 0) {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("idle-daily"));
+
+ return MaybeReportGoogleGDPRChoiceTelemetry();
+ }
+
+ if (nsCRT::strcmp(aTopic, "browser-search-service") == 0 &&
+ nsDependentString(aData).EqualsLiteral("init-complete")) {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
+ ("Observe browser-search-service::init-complete."));
+ mIsSearchServiceInitialized = true;
+
+ return MaybeReportGoogleGDPRChoiceTelemetry();
+ }
+
+ if (nsCRT::strcmp(aTopic, "cookie-changed") == 0 ||
+ nsCRT::strcmp(aTopic, "private-cookie-changed") == 0) {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Observe %s", aTopic));
+
+ nsCOMPtr<nsICookieNotification> notification = do_QueryInterface(aSubject);
+ NS_ENSURE_TRUE(notification, NS_ERROR_FAILURE);
+
+ if (notification->GetAction() != nsICookieNotification::COOKIE_ADDED &&
+ notification->GetAction() != nsICookieNotification::COOKIE_CHANGED) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsICookie> cookie;
+ nsresult rv = notification->GetCookie(getter_AddRefs(cookie));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString name;
+ rv = cookie->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Bail out early if this is not a SOCS cookie.
+ if (!name.EqualsLiteral("SOCS")) {
+ return NS_OK;
+ }
+
+ // A "SOCS" cookie is added. We record the GDPR choice as well as an event
+ // telemetry for recording a choice has been made at the moment.
+ return MaybeReportGoogleGDPRChoiceTelemetry(cookie, true);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsCookieBannerTelemetryService::MaybeReportGoogleGDPRChoiceTelemetry(
+ nsICookie* aCookie, bool aReportEvent) {
+ MOZ_ASSERT(mIsInitialized);
+ nsresult rv;
+
+ // Don't report the telemetry if the search service is not yet initialized.
+ if (!mIsSearchServiceInitialized) {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
+ ("Search Service is not yet initialized."));
+ return NS_OK;
+ }
+
+ // We only collect Google GDPR choice if Google is the default search engine.
+ nsCOMPtr<nsISearchService> searchService(
+ do_GetService("@mozilla.org/browser/search-service;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISearchEngine> engine;
+ rv = searchService->GetDefaultEngine(getter_AddRefs(engine));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Bail out early if no default search engine is available. This could happen
+ // if no search engine is shipped with the Gecko.
+ if (!engine) {
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
+ ("No default search engine is available."));
+ return NS_OK;
+ }
+
+ nsAutoString id;
+ rv = engine->GetId(id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Bail out early if the default search engine is not Google.
+ if (!id.EqualsLiteral("google@search.mozilla.orgdefault")) {
+ return NS_OK;
+ }
+
+ // Build up the Google domain set from the alternative Google search domains
+ if (!sGoogleDomainsSet) {
+ sGoogleDomainsSet = new nsTHashSet<nsCString>();
+
+ nsTArray<nsCString> googleDomains;
+ rv = searchService->GetAlternateDomains("www.google.com"_ns, googleDomains);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (const auto& domain : googleDomains) {
+ // We need to trim down the preceding "www" to get the host because the
+ // alternate domains provided by search service always have leading "www".
+ // However, the "SOCS" cookie is always set for all subdomains, e.g.
+ // ".google.com". Therefore, we need to trim down "www" to match the host
+ // of the cookie.
+ NS_ENSURE_TRUE(domain.Length() > 3, NS_ERROR_FAILURE);
+ sGoogleDomainsSet->Insert(Substring(domain, 3, domain.Length() - 3));
+ }
+ }
+
+ nsTArray<RefPtr<nsICookie>> cookies;
+ if (aCookie) {
+ const auto& attrs = aCookie->AsCookie().OriginAttributesRef();
+
+ // We only report cookies for the default originAttributes or private
+ // browsing mode.
+ if (attrs.mPrivateBrowsingId !=
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID ||
+ attrs == OriginAttributes()) {
+ cookies.AppendElement(RefPtr<nsICookie>(aCookie));
+ }
+ } else {
+ // If no cookie is given, we will iterate all cookies under Google Search
+ // domains.
+ nsCOMPtr<nsICookieManager> cookieManager =
+ do_GetService("@mozilla.org/cookiemanager;1");
+ if (!cookieManager) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& domain : *sGoogleDomainsSet) {
+ // Getting Google Search cookies only for normal windows. We will exclude
+ // cookies of non-default originAttributes, like cookies from containers.
+ //
+ // Note that we need to trim the leading "." from the domain here.
+ nsTArray<RefPtr<nsICookie>> googleCookies;
+ rv = cookieManager->GetCookiesWithOriginAttributes(
+ u"{ \"privateBrowsingId\": 0, \"userContextId\": 0 }"_ns,
+ Substring(domain, 1, domain.Length() - 1), googleCookies);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ cookies.AppendElements(googleCookies);
+ }
+ }
+
+ for (const auto& cookie : cookies) {
+ nsAutoCString name;
+ rv = cookie->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!name.EqualsLiteral("SOCS")) {
+ continue;
+ }
+
+ nsAutoCString host;
+ rv = cookie->GetHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!sGoogleDomainsSet->Contains(host)) {
+ continue;
+ }
+
+ nsAutoCString value;
+ rv = cookie->GetValue(value);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString choice;
+ nsAutoCString region;
+ rv = DecodeSOCSGoogleCookie(value, choice, region);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isPrivateBrowsing =
+ cookie->AsCookie().OriginAttributesRef().mPrivateBrowsingId !=
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID;
+
+ MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
+ ("Record the Google GDPR choice %s on the host %s in region %s for "
+ "the %s window",
+ choice.get(), host.get(), region.get(),
+ isPrivateBrowsing ? "private" : "normal"));
+
+ // We rely on the dynamical labelled string which can send most 16 different
+ // labels per report. In average cases, 16 labels is sufficient because we
+ // expect a user might have only 1 or 2 "SOCS" cookies on different Google
+ // Search domains.
+ //
+ // Note that we only report event telemetry for private browsing windows
+ // because the private session is ephemeral. People can change GDPR
+ // choice across different private sessions, so it's hard to collect the
+ // state correctly.
+ if (!isPrivateBrowsing) {
+ glean::cookie_banners::google_gdpr_choice_cookie.Get(host).Set(choice);
+ }
+
+ if (aReportEvent) {
+ if (isPrivateBrowsing) {
+ glean::cookie_banners::GoogleGdprChoiceCookieEventPbmExtra extra = {
+ .choice = Some(choice),
+ };
+ glean::cookie_banners::google_gdpr_choice_cookie_event_pbm.Record(
+ Some(extra));
+ } else {
+ glean::cookie_banners::GoogleGdprChoiceCookieEventExtra extra = {
+ .choice = Some(choice),
+ .region = Some(region),
+ .searchDomain = Some(host),
+ };
+ glean::cookie_banners::google_gdpr_choice_cookie_event.Record(
+ Some(extra));
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla