Bug 1845824 - ExperimentDelegate GeckoSessionHandler r=geckoview-reviewers,tthibaud,jonalmeida,owlish

This patch adds a an ExperimentDelegate GeckoSessionHandler, a JS
Experiment Delegate Parent/Child, and a way to get/set the Experiment
Delegate on the session.

Differential Revision: https://phabricator.services.mozilla.com/D184841
This commit is contained in:
Olivia Hall 2023-08-11 15:30:10 +00:00
parent e56c50d203
commit 713fd4f612
12 changed files with 441 additions and 16 deletions

View File

@ -0,0 +1,69 @@
/* 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/. */
import { GeckoViewActorParent } from "resource://gre/modules/GeckoViewActorParent.sys.mjs";
export class GeckoViewExperimentDelegateParent extends GeckoViewActorParent {
constructor() {
super();
}
/**
* Gets experiment information on a given feature.
*
* @param feature the experiment item to retrieve information on
* @returns a promise of success with a JSON message or failure
*/
async getExperimentFeature(feature) {
return this.eventDispatcher.sendRequestForResult({
type: "GeckoView:GetExperimentFeature",
feature,
});
}
/**
* Records an exposure event, that the experiment area was encountered, on a given feature.
*
* @param feature the experiment item to record an exposure event of
* @returns a promise of success or failure
*/
async recordExposure(feature) {
return this.eventDispatcher.sendRequestForResult({
type: "GeckoView:RecordExposure",
feature,
});
}
/**
* Records an exposure event on a specific experiment feature and element.
*
* Note: Use recordExposure, if the slug is not known.
*
* @param feature the experiment item to record an exposure event of
* @param slug a specific experiment element
* @returns a promise of success or failure
*/
async recordExperimentExposure(feature, slug) {
return this.eventDispatcher.sendRequestForResult({
type: "GeckoView:RecordExperimentExposure",
feature,
slug,
});
}
/**
* For recording malformed configuration.
*
* @param feature the experiment item to record an exposure event of
* @param part malformed information to send
* @returns a promise of success or failure
*/
async recordExperimentMalformedConfig(feature, part) {
return this.eventDispatcher.sendRequestForResult({
type: "GeckoView:RecordMalformedConfig",
feature,
part,
});
}
}

View File

@ -14,6 +14,7 @@ FINAL_TARGET_FILES.actors += [
"GeckoViewClipboardPermissionParent.sys.mjs",
"GeckoViewContentChild.sys.mjs",
"GeckoViewContentParent.sys.mjs",
"GeckoViewExperimentDelegateParent.sys.mjs",
"GeckoViewFormValidationChild.sys.mjs",
"GeckoViewPermissionChild.sys.mjs",
"GeckoViewPermissionParent.sys.mjs",

View File

@ -5,4 +5,7 @@ prefs =
dom.enable_window_print=true
skip-if =
os != 'android'
[test_geckoview_actor_telemetry.html]
[test_geckoview_actor_telemetry.html]
skip-if =
os != 'android'
[test_geckoview_experiment_delegate.html]

View File

@ -0,0 +1,108 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1845824
-->
<head>
<meta charset="utf-8">
<title>Test Experiment Delegate</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="head.js" type="application/javascript"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script class="testbody" type="text/javascript">
// Note: TestRunnerActivity provides a pseudo Experiment Delegate for this test.
async function requestExperiment(message) {
const chromeScript = SpecialPowers.loadChromeScript(_ => {
/* eslint-env mozilla/chrome-script */
addMessageListener("experiment", (msg) => {
var result;
const navigator = Services.wm.getMostRecentWindow("navigator:geckoview");
const experimentActor = navigator.window.moduleManager.getActor("GeckoViewExperimentDelegate");
switch (msg.endpoint) {
case 'getExperimentFeature':
result = experimentActor.getExperimentFeature(msg.feature);
break;
case 'recordExposure':
result = experimentActor.recordExposure(msg.feature);
break
case 'recordExperimentExposure':
result = experimentActor.recordExperimentExposure(msg.feature, msg.slug);
break;
case 'recordExperimentMalformedConfig':
result = experimentActor.recordExperimentMalformedConfig(msg.feature, msg.part);
break;
default:
result = null;
break;
}
return result;
});
});
const result = await chromeScript.sendQuery("experiment", message);
chromeScript.destroy();
return result;
}
add_task(async function test_getExperimentFeature() {
const success = await requestExperiment({endpoint: "getExperimentFeature", feature: "test"});
is(success["item-one"], true, "Retrieved TestRunnerActivity experiment feature 'test' for 'item-one'.");
is(success["item-two"], 5, "Retrieved TestRunnerActivity experiment feature 'test' for 'item-two'.");
var didErrorOccur = false;
try {
await requestExperiment({endpoint: "getExperimentFeature", feature: "no-feature"});
} catch (error) {
is(error, "An error occurred while retrieving feature data.", "Correctly failed when the feature did not exist.");
didErrorOccur = true;
}
is(didErrorOccur, true, "Error was caught when no feature existed.");
});
add_task(async function test_recordExposure() {
const success = await requestExperiment({endpoint: "recordExposure", feature: "test"});
is(success, true, "Recorded exposure for the feature.");
var didErrorOccur = false;
try {
await requestExperiment({endpoint: "recordExposure", feature: "no-feature"});
} catch (error) {
is(error, "An error occurred while recording feature.", "Correctly failed when the feature did not exist.");
didErrorOccur = true;
}
is(didErrorOccur, true, "Error was caught when no feature existed.");
});
add_task(async function test_recordExperimentExposure() {
const success = await requestExperiment({endpoint: "recordExperimentExposure", feature: "test", slug: "test"});
is(success, true, "Recorded experiment exposure for the feature.");
var didErrorOccur = false;
try {
await requestExperiment({endpoint: "recordExperimentExposure", feature: "no-feature", slug: "no-slug"});
} catch (error) {
is(error, "An error occurred while recording experiment feature.", "Correctly failed when the feature did not exist.");
didErrorOccur = true;
}
is(didErrorOccur, true, "Error was caught when no feature existed.");
});
add_task(async function test_recordExperimentMalformedConfig() {
const success = await requestExperiment({endpoint: "recordExperimentMalformedConfig", feature: "test", part: "test"});
is(success, true, "Recorded exposure for the feature.");
var didErrorOccur = false;
try {
await requestExperiment({endpoint: "recordExperimentMalformedConfig", feature: "no-feature", part:"no-part"});
} catch (error) {
is(error, "An error occurred while recording malformed feature config.", "Correctly failed when the feature did not exist.");
didErrorOccur = true;
}
is(didErrorOccur, true, "Error was caught when no feature existed.");
});
</script>
</body>
</html>

View File

@ -842,6 +842,20 @@ function startup() {
},
},
},
{
name: "GeckoViewExperimentDelegate",
onInit: {
actors: {
GeckoViewExperimentDelegate: {
parent: {
esModuleURI:
"resource:///actors/GeckoViewExperimentDelegateParent.sys.mjs",
},
allFrames: true,
},
},
},
},
]);
if (!Services.appinfo.sessionHistoryInParent) {

View File

@ -49,6 +49,7 @@ import java.io.InputStream;
import java.lang.Boolean;
import java.lang.CharSequence;
import java.lang.Class;
import java.lang.Deprecated;
import java.lang.Double;
import java.lang.Exception;
import java.lang.Float;
@ -83,6 +84,7 @@ import org.mozilla.geckoview.CompositorController;
import org.mozilla.geckoview.ContentBlocking;
import org.mozilla.geckoview.ContentBlockingController;
import org.mozilla.geckoview.CrashHandler;
import org.mozilla.geckoview.DeprecationSchedule;
import org.mozilla.geckoview.ExperimentDelegate;
import org.mozilla.geckoview.GeckoDisplay;
import org.mozilla.geckoview.GeckoResult;
@ -660,6 +662,7 @@ package org.mozilla.geckoview {
public static class ExperimentDelegate.ExperimentException extends Exception {
ctor public ExperimentException(int);
field public static final int ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED = -4;
field public static final int ERROR_EXPERIMENT_SLUG_NOT_FOUND = -3;
field public static final int ERROR_FEATURE_NOT_FOUND = -2;
field public static final int ERROR_UNKNOWN = -1;
@ -942,6 +945,7 @@ package org.mozilla.geckoview {
method @AnyThread @Nullable public ContentBlocking.Delegate getContentBlockingDelegate();
method @Nullable @UiThread public GeckoSession.ContentDelegate getContentDelegate();
method @AnyThread @NonNull public static String getDefaultUserAgent();
method @AnyThread @Nullable public ExperimentDelegate getExperimentDelegate();
method @AnyThread @NonNull public SessionFinder getFinder();
method @AnyThread @Nullable public GeckoSession.HistoryDelegate getHistoryDelegate();
method @AnyThread @Nullable public GeckoSession.MediaDelegate getMediaDelegate();
@ -987,6 +991,7 @@ package org.mozilla.geckoview {
method @UiThread public void setAutofillDelegate(@Nullable Autofill.Delegate);
method @AnyThread public void setContentBlockingDelegate(@Nullable ContentBlocking.Delegate);
method @UiThread public void setContentDelegate(@Nullable GeckoSession.ContentDelegate);
method @AnyThread public void setExperimentDelegate(@Nullable ExperimentDelegate);
method @AnyThread public void setFocused(boolean);
method @AnyThread public void setHistoryDelegate(@Nullable GeckoSession.HistoryDelegate);
method @AnyThread public void setMediaDelegate(@Nullable GeckoSession.MediaDelegate);
@ -1038,7 +1043,7 @@ package org.mozilla.geckoview {
method @UiThread default public void onFirstContentfulPaint(@NonNull GeckoSession);
method @UiThread default public void onFocusRequest(@NonNull GeckoSession);
method @UiThread default public void onFullScreen(@NonNull GeckoSession, boolean);
method @AnyThread @Nullable default public JSONObject onGetNimbusFeature(@NonNull GeckoSession, @NonNull String);
method @AnyThread @Deprecated @DeprecationSchedule(version=122,id="session-nimbus") @Nullable default public JSONObject onGetNimbusFeature(@NonNull GeckoSession, @NonNull String);
method @UiThread default public void onKill(@NonNull GeckoSession);
method @UiThread default public void onMetaViewportFitChange(@NonNull GeckoSession, @NonNull String);
method @UiThread default public void onPaintStatusReset(@NonNull GeckoSession);

View File

@ -29,6 +29,7 @@ class ExperimentDelegateTest : BaseSessionTest() {
mainSession.loadTestPath(TRACEMONKEY_PDF_PATH)
sessionRule.waitUntilCalled(object : ContentDelegate {
@Deprecated("Changing to Experiment Delegate in Bug 1840658.")
override fun onGetNimbusFeature(session: GeckoSession, featureId: String): JSONObject? {
assertThat(
"Feature id should match",

View File

@ -80,7 +80,7 @@ public class RuntimeCreator {
if (delegate != null) {
return delegate.onGetExperimentFeature(feature);
}
return null;
return ExperimentDelegate.super.onGetExperimentFeature(feature);
}
@Override
@ -88,7 +88,7 @@ public class RuntimeCreator {
if (delegate != null) {
return delegate.onRecordExposureEvent(feature);
}
return null;
return ExperimentDelegate.super.onRecordExposureEvent(feature);
}
@Override
@ -97,7 +97,7 @@ public class RuntimeCreator {
if (delegate != null) {
return delegate.onRecordExperimentExposureEvent(feature, slug);
}
return null;
return ExperimentDelegate.super.onRecordExperimentExposureEvent(feature, slug);
}
@Override
@ -106,7 +106,7 @@ public class RuntimeCreator {
if (delegate != null) {
return delegate.onRecordMalformedConfigurationEvent(feature, part);
}
return null;
return ExperimentDelegate.super.onRecordMalformedConfigurationEvent(feature, part);
}
}

View File

@ -6,6 +6,8 @@
package org.mozilla.geckoview;
import static org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED;
import androidx.annotation.AnyThread;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
@ -42,7 +44,10 @@ public interface ExperimentDelegate {
*/
@AnyThread
default @NonNull GeckoResult<JSONObject> onGetExperimentFeature(@NonNull String feature) {
return null;
final GeckoResult<JSONObject> result = new GeckoResult<>();
result.completeExceptionally(
new ExperimentException(ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED));
return result;
}
/**
@ -64,7 +69,10 @@ public interface ExperimentDelegate {
*/
@AnyThread
default @NonNull GeckoResult<Void> onRecordExposureEvent(@NonNull String feature) {
return null;
final GeckoResult<Void> result = new GeckoResult<>();
result.completeExceptionally(
new ExperimentException(ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED));
return result;
}
/**
@ -86,7 +94,10 @@ public interface ExperimentDelegate {
@AnyThread
default @NonNull GeckoResult<Void> onRecordExperimentExposureEvent(
@NonNull String feature, @NonNull String slug) {
return null;
final GeckoResult<Void> result = new GeckoResult<>();
result.completeExceptionally(
new ExperimentException(ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED));
return result;
}
/**
@ -102,7 +113,10 @@ public interface ExperimentDelegate {
@AnyThread
default @NonNull GeckoResult<Void> onRecordMalformedConfigurationEvent(
@NonNull String feature, @NonNull String part) {
return null;
final GeckoResult<Void> result = new GeckoResult<>();
result.completeExceptionally(
new ExperimentException(ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED));
return result;
}
/**
@ -129,9 +143,18 @@ public interface ExperimentDelegate {
/** The experiment slug was not available. */
public static final int ERROR_EXPERIMENT_SLUG_NOT_FOUND = -3;
/** The experiment delegate is not implemented. */
public static final int ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED = -4;
/** Experiment exception error codes. */
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {ERROR_UNKNOWN, ERROR_FEATURE_NOT_FOUND, ERROR_EXPERIMENT_SLUG_NOT_FOUND})
@IntDef(
value = {
ERROR_UNKNOWN,
ERROR_FEATURE_NOT_FOUND,
ERROR_EXPERIMENT_SLUG_NOT_FOUND,
ERROR_EXPERIMENT_DELEGATE_NOT_IMPLEMENTED
})
public @interface Codes {}
/** One of {@link Codes} that provides more information about this exception. */

View File

@ -839,6 +839,95 @@ public class GeckoSession {
}
};
private final GeckoSessionHandler<ExperimentDelegate> mExperimentHandler =
new GeckoSessionHandler<ExperimentDelegate>(
"GeckoViewExperiment",
this,
new String[] {
"GeckoView:GetExperimentFeature",
"GeckoView:RecordExposure",
"GeckoView:RecordExperimentExposure",
"GeckoView:RecordMalformedConfig"
}) {
@Override
public void handleMessage(
final ExperimentDelegate delegate,
final String event,
final GeckoBundle message,
final EventCallback callback) {
if (delegate == null) {
if (callback != null) {
callback.sendError("No experiment delegate registered.");
}
Log.w(LOGTAG, "No experiment delegate registered.");
return;
}
final String feature = message.getString("feature", "");
if ("GeckoView:GetExperimentFeature".equals(event) && callback != null) {
final GeckoResult<JSONObject> result = delegate.onGetExperimentFeature(feature);
result
.accept(
json -> {
try {
callback.sendSuccess(GeckoBundle.fromJSONObject(json));
} catch (final JSONException e) {
callback.sendError("An error occured when serializing the feature data.");
}
})
.exceptionally(
e -> {
callback.sendError("An error occurred while retrieving feature data.");
return null;
});
} else if ("GeckoView:RecordExposure".equals(event) && callback != null) {
final GeckoResult<Void> result = delegate.onRecordExposureEvent(feature);
result
.accept(
a -> {
callback.sendSuccess(true);
})
.exceptionally(
e -> {
callback.sendError("An error occurred while recording feature.");
return null;
});
} else if ("GeckoView:RecordExperimentExposure".equals(event) && callback != null) {
final String slug = message.getString("slug", "");
final GeckoResult<Void> result =
delegate.onRecordExperimentExposureEvent(feature, slug);
result
.accept(
a -> {
callback.sendSuccess(true);
})
.exceptionally(
e -> {
callback.sendError("An error occurred while recording experiment feature.");
return null;
});
} else if ("GeckoView:RecordMalformedConfig".equals(event) && callback != null) {
final String part = message.getString("part", "");
final GeckoResult<Void> result =
delegate.onRecordMalformedConfigurationEvent(feature, part);
result
.accept(
a -> {
callback.sendSuccess(true);
})
.exceptionally(
e -> {
callback.sendError(
"An error occurred while recording malformed feature config.");
return null;
});
}
}
};
private final GeckoSessionHandler<ContentDelegate> mProcessHangHandler =
new GeckoSessionHandler<ContentDelegate>(
"GeckoViewProcessHangMonitor", this, new String[] {"GeckoView:HangReport"}) {
@ -1146,7 +1235,8 @@ public class GeckoSession {
mScrollHandler,
mSelectionActionDelegate,
mContentBlockingHandler,
mMediaSessionHandler
mMediaSessionHandler,
mExperimentHandler
};
private static class PermissionCallback
@ -1650,6 +1740,7 @@ public class GeckoSession {
mId = id;
mWindow = new Window(runtime, this, mNativeQueue);
mWebExtensionController.setRuntime(runtime);
mExperimentHandler.setDelegate(getRuntimeExperimentDelegate(), this);
onWindowChanged(WINDOW_OPEN, /* inProgress */ true);
@ -3908,13 +3999,18 @@ public class GeckoSession {
default void onCookieBannerHandled(@NonNull final GeckoSession session) {}
/**
* This method is called when GeckoView is requesting a specific Nimbus feature in using message
* `GeckoView:GetNimbusFeature`.
* This method is scheduled for deprecation, see Bug 1846074 for details. Please switch to the
* [ExperimentDelegate.onGetExperimentFeature] for the same functionality.
*
* <p>This method is called when GeckoView is requesting a specific Nimbus feature in using
* message `GeckoView:GetNimbusFeature`.
*
* @param session GeckoSession that initiated the callback.
* @param featureId Nimbus feature id of the collected data.
* @return A {@link JSONObject} with the feature.
*/
@Deprecated
@DeprecationSchedule(version = 122, id = "session-nimbus")
@AnyThread
default @Nullable JSONObject onGetNimbusFeature(
@NonNull final GeckoSession session, @NonNull final String featureId) {
@ -7679,6 +7775,45 @@ public class GeckoSession {
mPrintHandler.setDelegate(delegate, this);
}
/**
* Gets the experiment delegate for this session.
*
* @return The current {@link ExperimentDelegate} for this session, if any.
*/
@AnyThread
public @Nullable ExperimentDelegate getExperimentDelegate() {
return mExperimentHandler.getDelegate();
}
/**
* Gets the experiment delegate from the runtime.
*
* @return The current {@link ExperimentDelegate} for the runtime or null.
*/
@AnyThread
private @Nullable ExperimentDelegate getRuntimeExperimentDelegate() {
final GeckoRuntime runtime = this.getRuntime();
if (runtime != null) {
final GeckoRuntimeSettings runtimeSettings = runtime.getSettings();
if (runtimeSettings != null) {
return runtimeSettings.getExperimentDelegate();
}
}
Log.w(LOGTAG, "Could not retrieve experiment delegate from runtime.");
return null;
}
/**
* Sets the experiment delegate for this session. Default is set to the runtime experiment
* delegate.
*
* @param delegate An instance of {@link ExperimentDelegate}.
*/
@AnyThread
public void setExperimentDelegate(final @Nullable ExperimentDelegate delegate) {
mExperimentHandler.setDelegate(delegate, this);
}
/** Thrown when failure occurs when printing from a website. */
@WrapForJNI
public static class GeckoPrintException extends Exception {

View File

@ -22,6 +22,8 @@ exclude: true
- Added [`ERROR_INCOMPATIBLE`][118.6] to `WebExtension.InstallException.ErrorCodes`. ([bug 1845749]({{bugzilla}}1845749))
- Added [`GeckoRuntimeSettings.Builder.extensionsWebAPIEnabled`][118.7]. ([bug 1847173]({{bugzilla}}1847173))
- Changed [`GeckoSession.AccountSelectorPrompt`][118.8]: added the Provider to which the Account belongs ([bug 1847059]({{bugzilla}}1847059))
- Added [`getExperimentDelegate`][118.9] and [`setExperimentDelegate`][118.10] to the GeckoSession allow GeckoView to get and set the experiment delegate for the session. Default is to use the runtime delegate.
- ⚠️ Deprecated [`onGetNimbusFeature`][115.5] by 122, please use `ExperimentDelegate.onGetExperimentFeature` instead.
[118.1]: {{javadoc_uri}}/ExperimentDelegate.html
[118.2]: {{javadoc_uri}}/WebExtension.InstallException.ErrorCodes.html#ERROR_BLOCKLISTED
@ -31,6 +33,8 @@ exclude: true
[118.6]: {{javadoc_uri}}/WebExtension.InstallException.ErrorCodes.html#ERROR_INCOMPATIBLE
[118.7]: {{javadoc_uri}}/GeckoRuntimeSettings.Builder.html#extensionsWebAPIEnabled(boolean)
[118.8]: {{javadoc_uri}}/GeckoSession.html#AccountSelectorPrompt
[118.9]: {{javadoc_uri}}/GeckoSession.html#getExperimentDelegate()
[118.10]: {{javadoc_uri}}/GeckoSession.html#setExperimentDelegate(org.mozilla.geckoview.ExperimentDelegate)
## v116
- Added [`GeckoSession.didPrintPageContent`][116.1] to included extra print status for a standard print and new `GeckoPrintException.ERROR_NO_PRINT_DELEGATE`
@ -1411,4 +1415,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: d735eedb188b91b0495d3c2667807f29f6915263
[api-version]: 4a2f7cc3f89c0baa015d23f9d17fbea7e69e4aff

View File

@ -3,6 +3,10 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.geckoview.test_runner;
import static org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_EXPERIMENT_SLUG_NOT_FOUND;
import static org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_FEATURE_NOT_FOUND;
import static org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_UNKNOWN;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@ -16,8 +20,11 @@ import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.ContentBlocking;
import org.mozilla.geckoview.ExperimentDelegate;
import org.mozilla.geckoview.GeckoDisplay;
import org.mozilla.geckoview.GeckoResult;
import org.mozilla.geckoview.GeckoRuntime;
@ -415,7 +422,8 @@ public class TestRunnerActivity extends Activity {
.arguments(new String[] {"-purgecaches"})
.displayDpiOverride(160)
.displayDensityOverride(1.0f)
.remoteDebuggingEnabled(true);
.remoteDebuggingEnabled(true)
.experimentDelegate(new TestRunnerExperimentDelegate());
final Bundle extras = intent.getExtras();
if (extras != null) {
@ -650,4 +658,58 @@ public class TestRunnerActivity extends Activity {
public GeckoSession getGeckoSession() {
return mSession;
}
class TestRunnerExperimentDelegate implements ExperimentDelegate {
@Override
public GeckoResult<JSONObject> onGetExperimentFeature(@NonNull String feature) {
GeckoResult<JSONObject> result = new GeckoResult<>();
if (feature.equals("test")) {
try {
result.complete(new JSONObject().put("item-one", true).put("item-two", 5));
} catch (JSONException e) {
result.completeExceptionally(new ExperimentException(ERROR_UNKNOWN));
}
} else {
result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND));
}
return result;
}
@Override
public GeckoResult<Void> onRecordExposureEvent(@NonNull String feature) {
GeckoResult<Void> result = new GeckoResult<>();
if (feature.equals("test")) {
result.complete(null);
} else {
result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND));
}
return result;
}
@Override
public GeckoResult<Void> onRecordExperimentExposureEvent(
@NonNull String feature, @NonNull String slug) {
GeckoResult<Void> result = new GeckoResult<>();
if (feature.equals("test") && slug.equals("test")) {
result.complete(null);
} else if (!slug.equals("test") && feature.equals("test")) {
result.completeExceptionally(new ExperimentException(ERROR_EXPERIMENT_SLUG_NOT_FOUND));
} else {
result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND));
}
return result;
}
@Override
public GeckoResult<Void> onRecordMalformedConfigurationEvent(
@NonNull String feature, @NonNull String part) {
GeckoResult<Void> result = new GeckoResult<>();
if (feature.equals("test")) {
result.complete(null);
} else {
result.completeExceptionally(new ExperimentException(ERROR_FEATURE_NOT_FOUND));
}
return result;
}
}
}