/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "mozilla/browser/NimbusFeatures.h" #include "mozilla/browser/NimbusFeatureManifest.h" #include "mozilla/Telemetry.h" #include "mozilla/Try.h" #include "mozilla/dom/ScriptSettings.h" #include "jsapi.h" #include "js/JSON.h" #include "nsJSUtils.h" namespace mozilla { static nsTHashSet sExposureFeatureSet; void NimbusFeatures::GetPrefName(const nsACString& branchPrefix, const nsACString& aFeatureId, const nsACString& aVariable, nsACString& aPref) { nsAutoCString featureAndVariable; featureAndVariable.Append(aFeatureId); if (!aVariable.IsEmpty()) { featureAndVariable.Append("."); featureAndVariable.Append(aVariable); } aPref.Truncate(); aPref.Append(branchPrefix); aPref.Append(featureAndVariable); } /** * Returns the variable value configured via experiment or rollout. * If a fallback pref is defined in the FeatureManifest and it * has a user value set this takes precedence over remote configurations. */ bool NimbusFeatures::GetBool(const nsACString& aFeatureId, const nsACString& aVariable, bool aDefault) { nsAutoCString experimentPref; GetPrefName(kSyncDataPrefBranch, aFeatureId, aVariable, experimentPref); if (Preferences::HasUserValue(experimentPref.get())) { return Preferences::GetBool(experimentPref.get(), aDefault); } nsAutoCString rolloutPref; GetPrefName(kSyncRolloutsPrefBranch, aFeatureId, aVariable, rolloutPref); if (Preferences::HasUserValue(rolloutPref.get())) { return Preferences::GetBool(rolloutPref.get(), aDefault); } auto prefName = GetNimbusFallbackPrefName(aFeatureId, aVariable); if (prefName.isSome()) { return Preferences::GetBool(prefName->get(), aDefault); } return aDefault; } /** * Returns the variable value configured via experiment or rollout. * If a fallback pref is defined in the FeatureManifest and it * has a user value set this takes precedence over remote configurations. */ int NimbusFeatures::GetInt(const nsACString& aFeatureId, const nsACString& aVariable, int aDefault) { nsAutoCString experimentPref; GetPrefName(kSyncDataPrefBranch, aFeatureId, aVariable, experimentPref); if (Preferences::HasUserValue(experimentPref.get())) { return Preferences::GetInt(experimentPref.get(), aDefault); } nsAutoCString rolloutPref; GetPrefName(kSyncRolloutsPrefBranch, aFeatureId, aVariable, rolloutPref); if (Preferences::HasUserValue(rolloutPref.get())) { return Preferences::GetInt(rolloutPref.get(), aDefault); } auto prefName = GetNimbusFallbackPrefName(aFeatureId, aVariable); if (prefName.isSome()) { return Preferences::GetInt(prefName->get(), aDefault); } return aDefault; } nsresult NimbusFeatures::OnUpdate(const nsACString& aFeatureId, const nsACString& aVariable, PrefChangedFunc aUserCallback, void* aUserData) { nsAutoCString experimentPref; nsAutoCString rolloutPref; GetPrefName(kSyncDataPrefBranch, aFeatureId, aVariable, experimentPref); GetPrefName(kSyncRolloutsPrefBranch, aFeatureId, aVariable, rolloutPref); nsresult rv = Preferences::RegisterCallback(aUserCallback, experimentPref, aUserData); NS_ENSURE_SUCCESS(rv, rv); rv = Preferences::RegisterCallback(aUserCallback, rolloutPref, aUserData); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult NimbusFeatures::OffUpdate(const nsACString& aFeatureId, const nsACString& aVariable, PrefChangedFunc aUserCallback, void* aUserData) { nsAutoCString experimentPref; nsAutoCString rolloutPref; GetPrefName(kSyncDataPrefBranch, aFeatureId, aVariable, experimentPref); GetPrefName(kSyncRolloutsPrefBranch, aFeatureId, aVariable, rolloutPref); nsresult rv = Preferences::UnregisterCallback(aUserCallback, experimentPref, aUserData); NS_ENSURE_SUCCESS(rv, rv); rv = Preferences::UnregisterCallback(aUserCallback, rolloutPref, aUserData); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * Attempt to read Nimbus preference to determine experiment and branch slug. * Nimbus will store a pref with experiment metadata in the following format: * { * slug: "experiment slug", * branch: { slug: "branch slug" }, * ... * } * The naming convention for preference names is: * `nimbus.syncdatastore.` * These values are used to send `exposure` telemetry pings. */ nsresult NimbusFeatures::GetExperimentSlug(const nsACString& aFeatureId, nsACString& aExperimentSlug, nsACString& aBranchSlug) { nsAutoCString prefName; nsAutoString prefValue; aExperimentSlug.Truncate(); aBranchSlug.Truncate(); GetPrefName(kSyncDataPrefBranch, aFeatureId, EmptyCString(), prefName); MOZ_TRY(Preferences::GetString(prefName.get(), prefValue)); if (prefValue.IsEmpty()) { return NS_ERROR_UNEXPECTED; } dom::AutoJSAPI jsapi; if (!jsapi.Init(xpc::PrivilegedJunkScope())) { return NS_ERROR_UNEXPECTED; } JSContext* cx = jsapi.cx(); JS::Rooted json(cx, JS::NullValue()); if (JS_ParseJSON(cx, prefValue.BeginReading(), prefValue.Length(), &json) && json.isObject()) { JS::Rooted experimentJSON(cx, json.toObjectOrNull()); JS::Rooted expSlugValue(cx); if (!JS_GetProperty(cx, experimentJSON, "slug", &expSlugValue)) { return NS_ERROR_UNEXPECTED; } AssignJSString(cx, aExperimentSlug, expSlugValue.toString()); JS::Rooted branchJSON(cx); if (!JS_GetProperty(cx, experimentJSON, "branch", &branchJSON) && !branchJSON.isObject()) { return NS_ERROR_UNEXPECTED; } JS::Rooted branchObj(cx, branchJSON.toObjectOrNull()); JS::Rooted branchSlugValue(cx); if (!JS_GetProperty(cx, branchObj, "slug", &branchSlugValue)) { return NS_ERROR_UNEXPECTED; } AssignJSString(cx, aBranchSlug, branchSlugValue.toString()); } return NS_OK; } /** * Sends an exposure event for aFeatureId when enrolled in an experiment. * By default attempt to send once per function call. For some usecases it might * be useful to send only once, in which case set the optional aOnce to `true`. */ nsresult NimbusFeatures::RecordExposureEvent(const nsACString& aFeatureId, const bool aOnce) { nsAutoCString featureName(aFeatureId); if (!sExposureFeatureSet.EnsureInserted(featureName) && aOnce) { // We already sent (or tried to send) an exposure ping for this featureId return NS_ERROR_ABORT; } nsAutoCString slugName; nsAutoCString branchName; MOZ_TRY(GetExperimentSlug(aFeatureId, slugName, branchName)); if (slugName.IsEmpty() || branchName.IsEmpty()) { // Failed getting experiment metadata or not enrolled in an experiment for // this featureId return NS_ERROR_UNEXPECTED; } Telemetry::SetEventRecordingEnabled("normandy"_ns, true); nsTArray extra(2); extra.AppendElement(Telemetry::EventExtraEntry{"branchSlug"_ns, branchName}); extra.AppendElement(Telemetry::EventExtraEntry{"featureId"_ns, featureName}); Telemetry::RecordEvent(Telemetry::EventID::Normandy_Expose_NimbusExperiment, Some(slugName), Some(std::move(extra))); return NS_OK; } } // namespace mozilla