summaryrefslogtreecommitdiffstats
path: root/toolkit/components/nimbus/lib/NimbusFeatures.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/nimbus/lib/NimbusFeatures.cpp')
-rw-r--r--toolkit/components/nimbus/lib/NimbusFeatures.cpp207
1 files changed, 207 insertions, 0 deletions
diff --git a/toolkit/components/nimbus/lib/NimbusFeatures.cpp b/toolkit/components/nimbus/lib/NimbusFeatures.cpp
new file mode 100644
index 0000000000..4b580149f5
--- /dev/null
+++ b/toolkit/components/nimbus/lib/NimbusFeatures.cpp
@@ -0,0 +1,207 @@
+/* -*- 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/dom/ScriptSettings.h"
+#include "jsapi.h"
+#include "js/JSON.h"
+#include "nsJSUtils.h"
+
+namespace mozilla {
+
+static nsTHashSet<nsCString> 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.<feature_id>`
+ * 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<JS::Value> json(cx, JS::NullValue());
+ if (JS_ParseJSON(cx, prefValue.BeginReading(), prefValue.Length(), &json) &&
+ json.isObject()) {
+ JS::Rooted<JSObject*> experimentJSON(cx, json.toObjectOrNull());
+ JS::Rooted<JS::Value> expSlugValue(cx);
+ if (!JS_GetProperty(cx, experimentJSON, "slug", &expSlugValue)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ AssignJSString(cx, aExperimentSlug, expSlugValue.toString());
+
+ JS::Rooted<JS::Value> branchJSON(cx);
+ if (!JS_GetProperty(cx, experimentJSON, "branch", &branchJSON) &&
+ !branchJSON.isObject()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ JS::Rooted<JSObject*> branchObj(cx, branchJSON.toObjectOrNull());
+ JS::Rooted<JS::Value> 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<Telemetry::EventExtraEntry> 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