Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1648412 - Implement the experiments API in C# #1145

Merged
merged 4 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement the C# experiments API
This closely follows the Kotlin implementation.
  • Loading branch information
Dexterp37 committed Aug 7, 2020
commit 1263a0b3aacb69492c1270226c9d9d010b6b3c5b
82 changes: 82 additions & 0 deletions glean-core/csharp/Glean/Glean.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -324,6 +325,87 @@ bool GetUploadEnabled()
}
}

/// <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.
Expand Down
57 changes: 57 additions & 0 deletions glean-core/csharp/Glean/Metrics/RecordedExperimentData.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
56 changes: 56 additions & 0 deletions glean-core/csharp/GleanTests/GleanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,6 +35,61 @@ public void SendAPing()
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()
{
Expand Down