Bug 1845834 - Expose toolkit attribution event API to GV r=geckoview-reviewers,owlish

Differential Revision: https://phabricator.services.mozilla.com/D191135
This commit is contained in:
Cathy Lu 2023-10-31 17:11:37 +00:00
parent a8213d2010
commit f25a8b0ffa
8 changed files with 170 additions and 14 deletions

View file

@ -268,6 +268,13 @@ pref("formhelper.autozoom", true);
// Optionally send web console output to logcat (bug 1415318)
pref("geckoview.console.enabled", false);
#ifdef NIGHTLY_BUILD
pref("geckoview.shopping.enabled", true);
// Testing flag for geckoview shopping product. When true, sendAttributionEvent
// will return "TEST_AID_RESPONSE" for products with "TEST_AID" id.
pref("geckoview.shopping.test_response", false);
#endif
pref("image.cache.size", 1048576); // bytes
// Inherit locale from the OS, used for multi-locale builds

View file

@ -1010,6 +1010,8 @@ package org.mozilla.geckoview {
method @AnyThread @NonNull public GeckoResult<List<GeckoSession.Recommendation>> requestRecommendations(@NonNull String);
method @AnyThread public void restoreState(@NonNull GeckoSession.SessionState);
method @AnyThread @NonNull public GeckoResult<InputStream> saveAsPdf();
method @AnyThread @NonNull public GeckoResult<Boolean> sendClickAttributionEvent(@NonNull String);
method @AnyThread @NonNull public GeckoResult<Boolean> sendImpressionAttributionEvent(@NonNull String);
method @AnyThread public void setActive(boolean);
method @UiThread public void setAutofillDelegate(@Nullable Autofill.Delegate);
method @AnyThread public void setContentBlockingDelegate(@Nullable ContentBlocking.Delegate);

View file

@ -823,4 +823,50 @@ class ContentDelegateTest : BaseSessionTest() {
}
}
}
@Test
fun sendAttributionEvents() {
// TODO (bug 1861175): enable in automation
if (!sessionRule.env.isAutomation) {
assumeThat(sessionRule.env.isNightly, equalTo(true))
// Checks that the pref value is also consistent with the runtime settings
val originalPrefs = sessionRule.getPrefs(
"geckoview.shopping.test_response",
)
assertThat("Pref is correct", originalPrefs[0] as Boolean, equalTo(false))
val aid = "TEST_AID"
val invalidClickResult = mainSession.sendClickAttributionEvent(aid)
assertThat(
"Click event success result should be false",
sessionRule.waitForResult(invalidClickResult),
equalTo(false),
)
val invalidImpressionResult = mainSession.sendImpressionAttributionEvent(aid)
assertThat(
"Impression event result result should be false",
sessionRule.waitForResult(invalidImpressionResult),
equalTo(false),
)
sessionRule.setPrefsUntilTestEnd(
mapOf(
"geckoview.shopping.test_response" to true,
),
)
val validClickResult = mainSession.sendClickAttributionEvent(aid)
assertThat(
"Click event success result should be true",
sessionRule.waitForResult(validClickResult),
equalTo(true),
)
val validImpressionResult = mainSession.sendImpressionAttributionEvent(aid)
assertThat(
"Impression event success result should be true",
sessionRule.waitForResult(validImpressionResult),
equalTo(true),
)
}
}
}

View file

@ -3041,6 +3041,32 @@ public class GeckoSession {
return mEventDispatcher.queryString("GeckoView:PollForAnalysisCompleted", bundle);
}
/**
* Send a click event to the Ad Attribution API.
*
* @param aid Ad id of the recommended product.
* @return a {@link GeckoResult} result of whether or not sending the event was successful.
*/
@AnyThread
public @NonNull GeckoResult<Boolean> sendClickAttributionEvent(@NonNull final String aid) {
final GeckoBundle bundle = new GeckoBundle(1);
bundle.putString("aid", aid);
return mEventDispatcher.queryBoolean("GeckoView:SendClickAttributionEvent", bundle);
}
/**
* Send an impression event to the Ad Attribution API.
*
* @param aid Ad id of the recommended product.
* @return a {@link GeckoResult} result of whether or not sending the event was successful.
*/
@AnyThread
public @NonNull GeckoResult<Boolean> sendImpressionAttributionEvent(@NonNull final String aid) {
final GeckoBundle bundle = new GeckoBundle(1);
bundle.putString("aid", aid);
return mEventDispatcher.queryBoolean("GeckoView:SendImpressionAttributionEvent", bundle);
}
/**
* Request product recommendations given a specific product url.
*

View file

@ -16,10 +16,14 @@ exclude: true
## v121
- Added runtime controller functions. [`RuntimeTranslation`][121.1] has options for retrieving translation languages and managing language models.
- Added support for controlling `cookiebanners.service.enableGlobalRules` and `cookiebanners.service.enableGlobalRules.subFrames` via [`GeckoSession.ContentDelegate.cookieBannerGlobalRulesEnabled`][121.2] and [`GeckoSession.ContentDelegate.cookieBannerGlobalRulesSubFramesEnabled`][121.3].
- Added [`GeckoSession.sendClickAttributionEvent`][121.4] for sending click attribution event for a given product recommendation.
- Added [`GeckoSession.sendImpressionAttributionEvent`][121.5] for sending impression attribution event for a given product recommendation.
[121.1]: {{javadoc_uri}}/TranslationsController.RuntimeTranslation.html
[121.2]: {{javadoc_uri}}/ContentBlocking.Settings.Builder.html#cookieBannerGlobalRulesEnabled(boolean)
[121.3]: {{javadoc_uri}}/ContentBlocking.Settings.Builder.html#cookieBannerGlobalRulesSubFramesEnabled(boolean)
[121.4]: {{javadoc_uri}}/GeckoSession.html#sendClickAttributionEvent(String)
[121.5: {{javadoc_uri}}/GeckoSession.html#sendImpressionAttributionEvent(String)
## v120
- Added [`disableExtensionProcessSpawning`][120.1] for disabling the extension process spawning. ([bug 1855405]({{bugzilla}}1855405))
@ -1457,4 +1461,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]: d303487577022d159dbb7a4b18c339b555a48a45
[api-version]: c7dae91b8e81ae247eeec1bcf419f3cab0b76e40

View file

@ -1305,6 +1305,9 @@ public class GeckoViewActivity extends AppCompatActivity
case R.id.request_shopping_analysis:
requestAnalysis(session, mCurrentUri);
break;
case R.id.request_shopping_recommendations:
requestRecommendations(session, mCurrentUri);
break;
case R.id.create_shopping_analysis:
requestCreateAnalysis(session, mCurrentUri);
break;
@ -2450,7 +2453,41 @@ public class GeckoViewActivity extends AppCompatActivity
public void requestRecommendations(
@NonNull final GeckoSession session, @NonNull final String url) {
session.requestRecommendations(url);
GeckoResult<List<GeckoSession.Recommendation>> result = session.requestRecommendations(url);
result.map(
recs -> {
List<String> aids = new ArrayList<>();
for (int i = 0; i < recs.size(); ++i) {
aids.add(recs.get(i).aid);
}
if (aids.size() >= 1) {
Log.d(LOGTAG, "Sending attribution events to first AID: " + aids.get(0));
session
.sendClickAttributionEvent(aids.get(0))
.then(
new GeckoResult.OnValueListener<Boolean, Void>() {
@Override
public GeckoResult<Void> onValue(final Boolean isSuccessful) {
Log.d(LOGTAG, "Success of click attribution event: " + isSuccessful);
return null;
}
});
session
.sendImpressionAttributionEvent(aids.get(0))
.then(
new GeckoResult.OnValueListener<Boolean, Void>() {
@Override
public GeckoResult<Void> onValue(final Boolean isSuccessful) {
Log.d(LOGTAG, "Success of impression attribution event: " + isSuccessful);
return null;
}
});
} else {
Log.d(LOGTAG, "No shopping recommendations. No attribution events were sent.");
}
return recs;
});
}
private class ExampleNavigationDelegate implements GeckoSession.NavigationDelegate {
@ -2465,7 +2502,6 @@ public class GeckoViewActivity extends AppCompatActivity
mTrackingProtectionPermission = getTrackingProtectionPermission(perms);
mCurrentUri = url;
requestAnalysis(session, url);
requestRecommendations(session, url);
}
@Override

View file

@ -16,6 +16,7 @@
<item android:title="@string/save_pdf" android:id="@+id/save_pdf"/>
<item android:title="@string/print_page" android:id="@+id/print_page"/>
<item android:title="Request Shopping Analysis" android:id="@+id/request_shopping_analysis"/>
<item android:title="Get Shopping Recommendations" android:id="@+id/request_shopping_recommendations"/>
<item android:title="Create Shopping Analysis" android:id="@+id/create_shopping_analysis"/>
<item android:title="Get Shopping Analysis Status" android:id="@+id/get_shopping_analysis_status"/>
<item android:title="Poll Until Analysis Completed" android:id="@+id/poll_shopping_analysis_status"/>

View file

@ -3,7 +3,6 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@ -24,6 +23,8 @@ export class GeckoViewContent extends GeckoViewModule {
"GeckoView:RequestCreateAnalysis",
"GeckoView:RequestAnalysisCreationStatus",
"GeckoView:PollForAnalysisCompleted",
"GeckoView:SendClickAttributionEvent",
"GeckoView:SendImpressionAttributionEvent",
"GeckoView:RequestAnalysis",
"GeckoView:RequestRecommendations",
"GeckoView:ScrollBy",
@ -213,6 +214,12 @@ export class GeckoViewContent extends GeckoViewModule {
case "GeckoView:PollForAnalysisCompleted":
this._pollForAnalysisCompleted(aData, aCallback);
break;
case "GeckoView:SendClickAttributionEvent":
this._sendAttributionEvent("click", aData, aCallback);
break;
case "GeckoView:SendImpressionAttributionEvent":
this._sendAttributionEvent("impression", aData, aCallback);
break;
case "GeckoView:RequestRecommendations":
this._requestRecommendations(aData, aCallback);
break;
@ -333,8 +340,8 @@ export class GeckoViewContent extends GeckoViewModule {
}
async _requestAnalysis(aData, aCallback) {
if (!AppConstants.NIGHTLY_BUILD) {
aCallback.onError(`This API enabled for Nightly builds only.`);
if (!Services.prefs.getBoolPref("geckoview.shopping.enabled", false)) {
aCallback.onError(`This API enabled for Shopping only.`);
return;
}
const url = Services.io.newURI(aData.url);
@ -352,8 +359,8 @@ export class GeckoViewContent extends GeckoViewModule {
}
async _requestCreateAnalysis(aData, aCallback) {
if (!AppConstants.NIGHTLY_BUILD) {
aCallback.onError(`This API enabled for Nightly builds only.`);
if (!Services.prefs.getBoolPref("geckoview.shopping.enabled", false)) {
aCallback.onError(`This API enabled for Shopping only.`);
return;
}
const url = Services.io.newURI(aData.url);
@ -371,8 +378,8 @@ export class GeckoViewContent extends GeckoViewModule {
}
async _requestAnalysisCreationStatus(aData, aCallback) {
if (!AppConstants.NIGHTLY_BUILD) {
aCallback.onError(`This API enabled for Nightly builds only.`);
if (!Services.prefs.getBoolPref("geckoview.shopping.enabled", false)) {
aCallback.onError(`This API enabled for Shopping only.`);
return;
}
const url = Services.io.newURI(aData.url);
@ -394,8 +401,8 @@ export class GeckoViewContent extends GeckoViewModule {
}
async _pollForAnalysisCompleted(aData, aCallback) {
if (!AppConstants.NIGHTLY_BUILD) {
aCallback.onError(`This API enabled for Nightly builds only.`);
if (!Services.prefs.getBoolPref("geckoview.shopping.enabled", false)) {
aCallback.onError(`This API enabled for Shopping only.`);
return;
}
const url = Services.io.newURI(aData.url);
@ -416,9 +423,36 @@ export class GeckoViewContent extends GeckoViewModule {
}
}
async _sendAttributionEvent(aEvent, aData, aCallback) {
if (!Services.prefs.getBoolPref("geckoview.shopping.enabled", false)) {
aCallback.onError(`This API enabled for Shopping only.`);
return;
}
// TODO (bug1859055): remove product object once sendAttributionEvent() is static
const product = new lazy.ShoppingProduct(
"http://example.com/dp/ABCDEFG123"
);
let result;
if (Services.prefs.getBoolPref("geckoview.shopping.test_response", true)) {
result = { TEST_AID: "TEST_AID_RESPONSE" };
} else {
// TODO (bug 1860897): source will be changed to geckoview_android
result = await product.sendAttributionEvent(
aEvent,
aData.aid,
"firefox_android"
);
}
if (!result || !(aData.aid in result) || !result[aData.aid]) {
aCallback.onSuccess(false);
return;
}
aCallback.onSuccess(true);
}
async _requestRecommendations(aData, aCallback) {
if (!AppConstants.NIGHTLY_BUILD) {
aCallback.onError(`This API enabled for Nightly builds only.`);
if (!Services.prefs.getBoolPref("geckoview.shopping.enabled", false)) {
aCallback.onError(`This API enabled for Shopping only.`);
return;
}
const url = Services.io.newURI(aData.url);