Implement the C# experiments API

This closely follows the Kotlin implementation.
This commit is contained in:
Alessio Placitelli 2020-08-07 18:36:47 +02:00
Родитель 74964cc36b
Коммит 1263a0b3aa
3 изменённых файлов: 195 добавлений и 0 удалений

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

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Mozilla.Glean.FFI;
@ -324,6 +325,87 @@ namespace Mozilla.Glean
}
}
/// <summary>
/// Indicate that an experiment is running. Glean will then add an
/// experiment annotation to the environment which is sent with pings. This
/// information is not persisted between runs.
/// </summary>
/// <param name="experimentId">The id of the active experiment (maximum 100 bytes)</param>
/// <param name="branch">The experiment branch (maximum 100 bytes)</param>
/// <param name="extra">Optional metadata to output with the ping</param>
public void SetExperimentActive(string experimentId, string branch, Dictionary<string, string> extra = null)
{
// The Map is sent over FFI as a pair of arrays, one containing the
// keys, and the other containing the values.
string[] keys = null;
string[] values = null;
Int32 numKeys = 0;
if (extra != null)
{
// While the `ToArray` functions below could throw `ArgumentNullException`, this would
// only happen if `extra` (and `extra.Keys|Values`) is null. Which is not the case, since
// we're specifically checking this.
// Note that the order of `extra.Keys` and `extra.Values` is unspecified, but guaranteed
// to be the same. See
// https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.values?view=netstandard-2.0#remarks
keys = extra.Keys.ToArray();
values = extra.Values.ToArray();
numKeys = extra.Count();
}
// We dispatch this asynchronously so that, if called before the Glean SDK is
// initialized, it doesn't get ignored and will be replayed after init.
Dispatchers.LaunchAPI(() => {
LibGleanFFI.glean_set_experiment_active(
experimentId,
branch,
keys,
values,
numKeys
);
});
}
/// <summary>
/// Indicate that an experiment is no longer running.
/// </summary>
/// <param name="experimentId">The id of the experiment to deactivate.</param>
public void SetExperimentInactive(string experimentId)
{
// We dispatch this asynchronously so that, if called before the Glean SDK is
// initialized, it doesn't get ignored and will be replayed after init.
Dispatchers.LaunchAPI(() => {
LibGleanFFI.glean_set_experiment_inactive(experimentId);
});
}
/// <summary>
/// Tests whether an experiment is active, for testing purposes only.
/// </summary>
/// <param name="experimentId">The id of the experiment to look for.</param>
/// <returns>true if the experiment is active and reported in pings, otherwise false</returns>
public bool TestIsExperimentActive(string experimentId)
{
Dispatchers.AssertInTestingMode();
return LibGleanFFI.glean_experiment_test_is_active(experimentId) != 0;
}
/// <summary>
/// Returns the stored data for the requested active experiment, for testing purposes only.
/// </summary>
/// <param name="experimentId">The id of the experiment to look for.</param>
/// <exception cref="System.NullReferenceException">Thrown when there is no data for the experiment.</exception>
/// <returns>The `RecordedExperimentData` for the experiment</returns>
public RecordedExperimentData TestGetExperimentData(string experimentId)
{
Dispatchers.AssertInTestingMode();
string rawData = LibGleanFFI.glean_experiment_test_get_data(experimentId).AsString();
return RecordedExperimentData.FromJsonString(rawData);
}
/// <summary>
/// TEST ONLY FUNCTION.
/// Resets the Glean state and triggers init again.

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

@ -0,0 +1,57 @@
/* 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/. */
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace Mozilla.Glean.Private
{
/// <summary>
/// Deserialized experiment data.
/// </summary>
public sealed class RecordedExperimentData
{
/// <summary>
/// The experiment's branch as set through `SetExperimentActive`.
/// </summary>
public readonly string Branch;
/// <summary>
/// Any extra data associated with this experiment through `SetExperimentActive`.
/// </summary>
public readonly Dictionary<string, string> Extra;
// This constructor is only useful for tests.
internal RecordedExperimentData() { }
RecordedExperimentData(string branch, Dictionary<string, string> extra)
{
Branch = branch;
Extra = extra;
}
public static RecordedExperimentData FromJsonString(string json)
{
try
{
JsonDocument data = JsonDocument.Parse(json);
JsonElement root = data.RootElement;
string branch = root.GetProperty("branch").GetString();
Dictionary<string, string> processedExtra = new Dictionary<string, string>();
JsonElement rawExtraMap = root.GetProperty("extra");
foreach (var entry in rawExtraMap.EnumerateObject())
{
processedExtra.Add(entry.Name, entry.Value.GetString());
}
return new RecordedExperimentData(branch, processedExtra);
}
catch (Exception)
{
return null;
}
}
}
}

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

@ -2,6 +2,7 @@
// 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/.
using System.Collections.Generic;
using System.IO;
using Xunit;
using static Mozilla.Glean.Glean;
@ -34,6 +35,61 @@ namespace Mozilla.Glean.Tests
GleanInstance.HandleBackgroundEvent();
}
[Fact]
public void TestExperimentsRecording()
{
GleanInstance.SetExperimentActive(
"experiment_test", "branch_a"
);
GleanInstance.SetExperimentActive(
"experiment_api", "branch_b",
new Dictionary<string, string>() { { "test_key", "value" } }
);
Assert.True(GleanInstance.TestIsExperimentActive("experiment_api"));
Assert.True(GleanInstance.TestIsExperimentActive("experiment_test"));
GleanInstance.SetExperimentInactive("experiment_test");
Assert.True(GleanInstance.TestIsExperimentActive("experiment_api"));
Assert.False(GleanInstance.TestIsExperimentActive("experiment_test"));
var storedData = GleanInstance.TestGetExperimentData("experiment_api");
Assert.Equal("branch_b", storedData.Branch);
Assert.Single(storedData.Extra);
Assert.Equal("value", storedData.Extra["test_key"]);
}
[Fact]
public void TestExperimentsRecordingBeforeGleanInits()
{
// This test relies on Glean not being initialized and task queuing to be on.
GleanInstance.TestDestroyGleanHandle();
Dispatchers.QueueInitialTasks = true;
GleanInstance.SetExperimentActive(
"experiment_set_preinit", "branch_a"
);
GleanInstance.SetExperimentActive(
"experiment_preinit_disabled", "branch_a"
);
GleanInstance.SetExperimentInactive("experiment_preinit_disabled");
// This will init glean and flush the dispatcher's queue.
string tempDataDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
GleanInstance.Reset(
applicationId: "org.mozilla.csharp.tests",
applicationVersion: "1.0-test",
uploadEnabled: true,
configuration: new Configuration(),
dataDir: tempDataDir
);
Assert.True(GleanInstance.TestIsExperimentActive("experiment_set_preinit"));
Assert.False(GleanInstance.TestIsExperimentActive("experiment_preinit_disabled"));
}
[Fact]
public void SettingMaxEventsDoesNotCrash()
{