Bug 1724686 - Add exposure ping to Nimbus platform API r=emilio,chutten

Differential Revision: https://phabricator.services.mozilla.com/D122235
This commit is contained in:
Andrei Oprea 2021-08-18 13:16:39 +00:00
Родитель e84c71c45a
Коммит b7e0b276b2
4 изменённых файлов: 157 добавлений и 2 удалений

Просмотреть файл

@ -5,9 +5,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/browser/NimbusFeatures.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& aFeatureId,
const nsACString& aVariable,
nsACString& aPref) {
@ -16,8 +23,10 @@ void NimbusFeatures::GetPrefName(const nsACString& aFeatureId,
aPref.Truncate();
aPref.Append(kSyncDataPrefBranch);
aPref.Append(aFeatureId);
aPref.Append(".");
aPref.Append(aVariable);
if (!aVariable.IsEmpty()) {
aPref.Append(".");
aPref.Append(aVariable);
}
}
bool NimbusFeatures::GetBool(const nsACString& aFeatureId,
@ -52,4 +61,93 @@ nsresult NimbusFeatures::OffUpdate(const nsACString& aFeatureId,
return Preferences::UnregisterCallback(aUserCallback, pref, aUserData);
}
/**
* 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(aFeatureId, ""_ns, 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::RootedValue expSlugValue(cx);
if (!JS_GetProperty(cx, experimentJSON, "slug", &expSlugValue)) {
return NS_ERROR_UNEXPECTED;
}
AssignJSString(cx, aExperimentSlug, expSlugValue.toString());
JS::RootedValue branchJSON(cx);
if (!JS_GetProperty(cx, experimentJSON, "branch", &branchJSON) &&
!branchJSON.isObject()) {
return NS_ERROR_UNEXPECTED;
}
JS::Rooted<JSObject*> branchObj(cx, branchJSON.toObjectOrNull());
JS::RootedValue 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 we only attempt to send once. For some usecases it might be useful
* to send multiple times or retry to send (when for example we are enrolled
* after the first call to this function) in which case set the optional
* aForce to `true`.
*/
nsresult NimbusFeatures::RecordExposureEvent(const nsACString& aFeatureId,
const bool aForce) {
nsAutoCString featureName(aFeatureId);
if (!sExposureFeatureSet.EnsureInserted(featureName) && !aForce) {
// 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

Просмотреть файл

@ -8,6 +8,7 @@
#define mozilla_NimbusFeatures_h
#include "mozilla/Preferences.h"
#include "nsTHashSet.h"
namespace mozilla {
@ -16,6 +17,10 @@ class NimbusFeatures {
static void GetPrefName(const nsACString& aFeatureId,
const nsACString& aVariable, nsACString& aPref);
static nsresult GetExperimentSlug(const nsACString& aFeatureId,
nsACString& aExperimentSlug,
nsACString& aBranchSlug);
public:
static bool GetBool(const nsACString& aFeatureId, const nsACString& aVariable,
bool aDefault);
@ -30,6 +35,9 @@ class NimbusFeatures {
static nsresult OffUpdate(const nsACString& aFeatureId,
const nsACString& aVariable,
PrefChangedFunc aUserCallback, void* aUserData);
static nsresult RecordExposureEvent(const nsACString& aFeatureId,
const bool aForce = false);
};
} // namespace mozilla

Просмотреть файл

@ -0,0 +1,44 @@
/* -*- 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 "gtest/gtest.h"
#include "mozilla/Preferences.h"
#include "mozilla/browser/NimbusFeatures.h"
#include "js/Array.h"
#include "js/PropertyAndElement.h"
#include "js/TypeDecls.h"
#include "TelemetryFixture.h"
#include "TelemetryTestHelpers.h"
using namespace mozilla;
using namespace TelemetryTestHelpers;
class NimbusTelemetryFixture : public TelemetryTestFixture {};
TEST_F(NimbusTelemetryFixture, NimbusFeaturesTelemetry) {
constexpr auto prefName = "nimbus.syncdatastore.foo"_ns;
constexpr auto prefValue =
R"({"slug":"experiment-slug","branch":{"slug":"branch-slug"}})";
AutoJSContextWithGlobal cx(mCleanGlobal);
Unused << mTelemetry->ClearEvents();
ASSERT_EQ(NimbusFeatures::RecordExposureEvent("foo"_ns), NS_ERROR_UNEXPECTED)
<< "Should fail because not enrolled in experiment";
// Set the experiment info for `foo`
Preferences::SetCString(prefName.get(), prefValue);
ASSERT_EQ(NimbusFeatures::RecordExposureEvent("foo"_ns), NS_ERROR_ABORT)
<< "Should fail even though enrolled because this is the 2nd call";
ASSERT_EQ(NimbusFeatures::RecordExposureEvent("foo"_ns, true), NS_OK)
<< "Should work because we set aForce=true";
ASSERT_EQ(NimbusFeatures::RecordExposureEvent("bar"_ns), NS_ERROR_UNEXPECTED)
<< "Should fail because we don't have an experiment for bar";
ASSERT_EQ(NimbusFeatures::RecordExposureEvent("foo"_ns), NS_ERROR_ABORT)
<< "Should abort because we've already send exposure for this featureId";
JS::RootedValue eventsSnapshot(cx.GetJSContext());
GetEventSnapshot(cx.GetJSContext(), &eventsSnapshot);
ASSERT_TRUE(EventPresent(cx.GetJSContext(), eventsSnapshot, "normandy"_ns,
"expose"_ns, "nimbus_experiment"_ns));
}

Просмотреть файл

@ -6,6 +6,11 @@
UNIFIED_SOURCES += [
"NimbusFeatures_GetTest.cpp",
"NimbusFeatures_RecordExposure.cpp",
]
LOCAL_INCLUDES += [
"/toolkit/components/telemetry/tests/gtest",
]
FINAL_LIBRARY = "xul-gtest"