diff --git a/.cargo/config.toml.in b/.cargo/config.toml.in
index 88ebcafdc3990..5dfd02e06a62e 100644
--- a/.cargo/config.toml.in
+++ b/.cargo/config.toml.in
@@ -75,9 +75,9 @@ git = "https://github.com/jfkthame/mapped_hyph.git"
rev = "eff105f6ad7ec9b79816cfc1985a28e5340ad14b"
replace-with = "vendored-sources"
-[source."git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56"]
+[source."git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8"]
git = "https://github.com/mozilla/application-services"
-rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
replace-with = "vendored-sources"
[source."git+https://github.com/mozilla/audioipc?rev=c8d3f03cb5f889e4cab18cc1360ad0daa074f17a"]
diff --git a/Cargo.lock b/Cargo.lock
index 4920d73538d3f..aa2a8008c983b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -978,7 +978,7 @@ dependencies = [
[[package]]
name = "context_id"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"chrono",
"error-support",
@@ -1940,14 +1940,13 @@ dependencies = [
[[package]]
name = "error-support"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"env_logger",
"error-support-macros",
"lazy_static",
"log",
"parking_lot",
- "tracing",
"tracing-support",
"uniffi",
]
@@ -1955,7 +1954,7 @@ dependencies = [
[[package]]
name = "error-support-macros"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"proc-macro2",
"quote",
@@ -2050,7 +2049,7 @@ dependencies = [
[[package]]
name = "filter_adult"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"base64 0.22.1",
"error-support",
@@ -2086,7 +2085,7 @@ dependencies = [
[[package]]
name = "firefox-versioning"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"serde_json",
"thiserror 2.0.12",
@@ -3461,7 +3460,7 @@ dependencies = [
[[package]]
name = "init_rust_components"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"nss",
"uniffi",
@@ -3470,7 +3469,7 @@ dependencies = [
[[package]]
name = "interrupt-support"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"lazy_static",
"parking_lot",
@@ -3629,7 +3628,7 @@ dependencies = [
[[package]]
name = "jwcrypto"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"base64 0.21.999",
"error-support",
@@ -3997,7 +3996,7 @@ checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "logins"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"anyhow",
"async-trait",
@@ -5035,7 +5034,7 @@ dependencies = [
[[package]]
name = "nss"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"base64 0.21.999",
"error-support",
@@ -5064,12 +5063,12 @@ dependencies = [
[[package]]
name = "nss_build_common"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
[[package]]
name = "nss_sys"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"libsqlite3-sys",
"nss_build_common",
@@ -5417,7 +5416,7 @@ checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
name = "payload-support"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"serde",
"serde_derive",
@@ -5982,7 +5981,7 @@ dependencies = [
[[package]]
name = "rc_crypto"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"base64 0.21.999",
"error-support",
@@ -6052,7 +6051,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relevancy"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"anyhow",
"base64 0.21.999",
@@ -6076,7 +6075,7 @@ dependencies = [
[[package]]
name = "remote_settings"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"anyhow",
"camino",
@@ -6347,7 +6346,7 @@ dependencies = [
[[package]]
name = "search"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"error-support",
"firefox-versioning",
@@ -6665,7 +6664,7 @@ dependencies = [
[[package]]
name = "sql-support"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"error-support",
"interrupt-support",
@@ -6856,7 +6855,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "suggest"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"anyhow",
"chrono",
@@ -6909,7 +6908,7 @@ dependencies = [
[[package]]
name = "sync-guid"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"base64 0.21.999",
"rand",
@@ -6920,7 +6919,7 @@ dependencies = [
[[package]]
name = "sync15"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"anyhow",
"base16",
@@ -6964,7 +6963,7 @@ dependencies = [
[[package]]
name = "tabs"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"anyhow",
"error-support",
@@ -7313,7 +7312,7 @@ dependencies = [
[[package]]
name = "tracing-support"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"parking_lot",
"serde_json",
@@ -7393,7 +7392,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "types"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"rusqlite 0.37.0",
"serde",
@@ -7774,7 +7773,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "viaduct"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"async-trait",
"error-support",
@@ -7949,7 +7948,7 @@ dependencies = [
[[package]]
name = "webext-storage"
version = "0.1.0"
-source = "git+https://github.com/mozilla/application-services?rev=900d0b03aeb82245c28a11a9adc1cbbead31ce56#900d0b03aeb82245c28a11a9adc1cbbead31ce56"
+source = "git+https://github.com/mozilla/application-services?rev=42c175c2ecb5aa39346dcd9c7b45ff02325562d8#42c175c2ecb5aa39346dcd9c7b45ff02325562d8"
dependencies = [
"anyhow",
"error-support",
diff --git a/Cargo.toml b/Cargo.toml
index 5ade903144c78..d663c8193c398 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -270,21 +270,21 @@ objc = { git = "https://github.com/glandium/rust-objc", rev = "4de89f5aa9851ceca
allocator-api2 = { git = "https://github.com/glandium/allocator-api2", rev = "ad5f3d56a5a4519eff52af4ff85293431466ef5c" }
# application-services overrides to make updating them all simpler.
-context_id = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-error-support = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-filter_adult = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-relevancy = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-search = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-sql-support = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-suggest = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-sync15 = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-tabs = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-tracing-support = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-viaduct = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-webext-storage = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-logins = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
-init_rust_components = { git = "https://github.com/mozilla/application-services", rev = "900d0b03aeb82245c28a11a9adc1cbbead31ce56" }
+context_id = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+error-support = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+filter_adult = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+relevancy = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+search = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+sql-support = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+suggest = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+sync15 = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+tabs = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+tracing-support = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+viaduct = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+webext-storage = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+logins = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
+init_rust_components = { git = "https://github.com/mozilla/application-services", rev = "42c175c2ecb5aa39346dcd9c7b45ff02325562d8" }
# Patched version of zip 2.4.2 to allow for reading omnijars.
zip = { path = "third_party/rust/zip" }
diff --git a/browser/components/aboutwelcome/.eslintrc.mjs b/browser/components/aboutwelcome/.eslintrc.mjs
index 13027c4b06d8d..260445d68f908 100644
--- a/browser/components/aboutwelcome/.eslintrc.mjs
+++ b/browser/components/aboutwelcome/.eslintrc.mjs
@@ -72,7 +72,7 @@ export default [
"guard-for-in": "error",
"max-nested-callbacks": ["error", 4],
"max-params": ["error", 6],
- "max-statements": ["error", 50],
+ "max-statements": "off",
"new-cap": ["error", { newIsCap: true, capIsNew: false }],
"no-alert": "error",
"no-div-regex": "error",
diff --git a/browser/components/aboutwelcome/content-src/aboutwelcome.jsx b/browser/components/aboutwelcome/content-src/aboutwelcome.jsx
index 1798a867bf46c..b20b3a23bab32 100644
--- a/browser/components/aboutwelcome/content-src/aboutwelcome.jsx
+++ b/browser/components/aboutwelcome/content-src/aboutwelcome.jsx
@@ -35,6 +35,7 @@ class AboutWelcome extends React.PureComponent {
mountStart: performance.getEntriesByName("mount").pop().startTime,
domState,
source: this.props.UTMTerm,
+ writeInMicrosurvey: this.props.write_in_microsurvey,
});
};
if (document.readyState === "complete") {
@@ -65,6 +66,7 @@ class AboutWelcome extends React.PureComponent {
addonIconURL={props.iconURL}
themeScreenshots={props.screenshots}
message_id={props.messageId}
+ writeInMicrosurvey={props.write_in_microsurvey}
defaultScreens={props.screens}
updateHistory={!props.disableHistoryUpdates}
metricsFlowUri={this.state.metricsFlowUri}
diff --git a/browser/components/aboutwelcome/content-src/aboutwelcome.scss b/browser/components/aboutwelcome/content-src/aboutwelcome.scss
index c3e3783c2298f..eee837474b89e 100644
--- a/browser/components/aboutwelcome/content-src/aboutwelcome.scss
+++ b/browser/components/aboutwelcome/content-src/aboutwelcome.scss
@@ -2142,6 +2142,31 @@ html {
}
}
+ .textarea-container {
+ display: flex;
+ flex-flow: column nowrap;
+
+ .textarea-char-counter {
+ font-size: 13px;
+ color: var(--text-color-deemphasized);
+ text-align: end;
+ margin-block: -10px 4px;
+
+ &.invalid {
+ color: var(--text-color-error)
+ }
+ }
+
+ .textarea-input {
+ resize: none;
+
+ &.invalid {
+ border-color: var(--outline-color-error);
+ outline-color: var(--outline-color-error);
+ }
+ }
+ }
+
.confirmation-checklist-section {
display: flex;
flex-direction: column;
diff --git a/browser/components/aboutwelcome/content-src/components/ActionChecklist.jsx b/browser/components/aboutwelcome/content-src/components/ActionChecklist.jsx
index dc97404dcc9eb..65a83ad244937 100644
--- a/browser/components/aboutwelcome/content-src/components/ActionChecklist.jsx
+++ b/browser/components/aboutwelcome/content-src/components/ActionChecklist.jsx
@@ -80,7 +80,11 @@ export const ActionChecklistProgressBar = ({ progress }) => {
);
};
-export const ActionChecklist = ({ content, message_id }) => {
+export const ActionChecklist = ({
+ content,
+ message_id,
+ writeInMicrosurvey,
+}) => {
const tiles = content.tiles.data;
const [progressValue, setProgressValue] = useState(0);
const [numberOfCompletedActions, setNumberOfCompletedActions] = useState(0);
@@ -119,7 +123,12 @@ export const ActionChecklist = ({ content, message_id }) => {
setNumberOfCompletedActions(numberOfCompletedActions + 1);
AboutWelcomeUtils.handleUserAction({ type, data });
- AboutWelcomeUtils.sendActionTelemetry(message_id, source_id);
+ AboutWelcomeUtils.sendActionTelemetry(
+ message_id,
+ source_id,
+ "CLICK_BUTTON",
+ { writeInMicrosurvey }
+ );
}
return (
diff --git a/browser/components/aboutwelcome/content-src/components/AdditionalCTA.jsx b/browser/components/aboutwelcome/content-src/components/AdditionalCTA.jsx
index 99f3336c0c806..eb9884303f973 100644
--- a/browser/components/aboutwelcome/content-src/components/AdditionalCTA.jsx
+++ b/browser/components/aboutwelcome/content-src/components/AdditionalCTA.jsx
@@ -6,7 +6,12 @@ import React from "react";
import { Localized } from "./MSLocalized";
import { SubmenuButton } from "./SubmenuButton";
-export const AdditionalCTA = ({ content, handleAction }) => {
+export const AdditionalCTA = ({
+ content,
+ handleAction,
+ activeMultiSelect,
+ textInputs,
+}) => {
let buttonStyle = "";
const isSplitButton =
content.submenu_button?.attached_to === "additional_button";
@@ -24,6 +29,36 @@ export const AdditionalCTA = ({ content, handleAction }) => {
: content.additional_button?.style;
}
+ const computeDisabled = React.useCallback(
+ disabledValue => {
+ if (disabledValue === "hasActiveMultiSelect") {
+ if (!activeMultiSelect) {
+ return true;
+ }
+
+ for (const key in activeMultiSelect) {
+ if (activeMultiSelect[key]?.length > 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ if (disabledValue === "hasTextInput") {
+ // For text input, we check if the user has entered any text in the
+ // textarea(s) present on the screen.
+ if (!textInputs) {
+ return true;
+ }
+ return Object.values(textInputs).every(
+ input => !input.isValid || input.value.trim().length === 0
+ );
+ }
+ return disabledValue;
+ },
+ [activeMultiSelect, textInputs]
+ );
+
return (
@@ -32,7 +67,7 @@ export const AdditionalCTA = ({ content, handleAction }) => {
className={`${buttonStyle} additional-cta`}
onClick={handleAction}
value="additional_button"
- disabled={content.additional_button?.disabled === true}
+ disabled={computeDisabled(content.additional_button?.disabled)}
/>
{isSplitButton ? (
diff --git a/browser/components/aboutwelcome/content-src/components/AddonsPicker.jsx b/browser/components/aboutwelcome/content-src/components/AddonsPicker.jsx
index 1f16529cf8b50..df550ce23dc6a 100644
--- a/browser/components/aboutwelcome/content-src/components/AddonsPicker.jsx
+++ b/browser/components/aboutwelcome/content-src/components/AddonsPicker.jsx
@@ -15,7 +15,7 @@ export const AddonsPicker = props => {
}
function handleAction(event) {
- const { message_id } = props;
+ const { message_id, writeInMicrosurvey } = props;
let { action, source_id } = content.tiles.data[event.currentTarget.value];
let { type, data } = action;
@@ -26,7 +26,12 @@ export const AddonsPicker = props => {
}
AboutWelcomeUtils.handleUserAction({ type, data });
- AboutWelcomeUtils.sendActionTelemetry(message_id, source_id);
+ AboutWelcomeUtils.sendActionTelemetry(
+ message_id,
+ source_id,
+ "CLICK_BUTTON",
+ { writeInMicrosurvey }
+ );
}
function handleAuthorClick(event, authorId) {
diff --git a/browser/components/aboutwelcome/content-src/components/ContentTiles.jsx b/browser/components/aboutwelcome/content-src/components/ContentTiles.jsx
index 244873afb819c..3eb15d27545c1 100644
--- a/browser/components/aboutwelcome/content-src/components/ContentTiles.jsx
+++ b/browser/components/aboutwelcome/content-src/components/ContentTiles.jsx
@@ -8,6 +8,7 @@ import { AddonsPicker } from "./AddonsPicker";
import { SingleSelect } from "./SingleSelect";
import { MobileDownloads } from "./MobileDownloads";
import { MultiSelect } from "./MultiSelect";
+import { TextAreaTile } from "./TextAreaTile";
import { EmbeddedMigrationWizard } from "./EmbeddedMigrationWizard";
import { EmbeddedFxBackupOptIn } from "./EmbeddedFxBackupOptIn";
import { ActionChecklist } from "./ActionChecklist";
@@ -170,8 +171,12 @@ export const ContentTiles = props => {
const toggleTile = (index, tile) => {
const tileId = `${tile.type}${tile.id ? "_" : ""}${tile.id ?? ""}_header`;
- AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId);
-
+ AboutWelcomeUtils.sendActionTelemetry(
+ props.messageId,
+ tileId,
+ "CLICK_BUTTON",
+ { writeInMicrosurvey: props.writeInMicrosurvey }
+ );
if (tile.type === "link" && tile.action) {
props.handleAction(
{
@@ -190,7 +195,9 @@ export const ContentTiles = props => {
setTilesHeaderExpanded(prev => !prev);
AboutWelcomeUtils.sendActionTelemetry(
props.messageId,
- "content_tiles_header"
+ "content_tiles_header",
+ "CLICK_BUTTON",
+ { writeInMicrosurvey: props.writeInMicrosurvey }
);
};
@@ -273,6 +280,7 @@ export const ContentTiles = props => {
message_id={props.messageId}
handleAction={props.handleAction}
layout={content.position}
+ writeInMicrosurvey={props.writeInMicrosurvey}
/>
)}
{["theme", "single-select"].includes(tile.type) && tile.data && (
@@ -311,6 +319,14 @@ export const ContentTiles = props => {
multiSelectId={`tile-${index}`}
/>
)}
+ {tile.type === "textarea" && tile.data && (
+
+ )}
{tile.type === "migration-wizard" && (
{
/>
)}
{tile.type === "action_checklist" && tile.data && (
-
+
)}
{tile.type === "embedded_browser" && tile.data?.url && (
diff --git a/browser/components/aboutwelcome/content-src/components/LanguageSwitcher.jsx b/browser/components/aboutwelcome/content-src/components/LanguageSwitcher.jsx
index 6db47daec4d35..78d454ec16574 100644
--- a/browser/components/aboutwelcome/content-src/components/LanguageSwitcher.jsx
+++ b/browser/components/aboutwelcome/content-src/components/LanguageSwitcher.jsx
@@ -168,6 +168,7 @@ export function LanguageSwitcher(props) {
negotiatedLanguage,
langPackInstallPhase,
messageId,
+ writeInMicrosurvey,
} = props;
const [isAwaitingLangpack, setIsAwaitingLangpack] = useState(false);
@@ -271,7 +272,9 @@ export function LanguageSwitcher(props) {
onClick={() => {
AboutWelcomeUtils.sendActionTelemetry(
messageId,
- "download_langpack"
+ "download_langpack",
+ "CLICK_BUTTON",
+ { writeInMicrosurvey }
);
setIsAwaitingLangpack(true);
}}
diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx
index e848a4db8c6a2..349f2d14ae8fb 100644
--- a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx
+++ b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx
@@ -75,7 +75,9 @@ export const MultiStageAboutWelcome = props => {
.AWGetUnhandledCampaignAction?.()
.then(action => {
if (typeof action === "string") {
- AboutWelcomeUtils.handleCampaignAction(action, props.message_id);
+ AboutWelcomeUtils.handleCampaignAction(action, props.message_id, {
+ writeInMicrosurvey: props.writeInMicrosurvey,
+ });
}
})
.catch(error => {
@@ -96,6 +98,7 @@ export const MultiStageAboutWelcome = props => {
screen_index: order,
screen_id: screen.id,
screen_initials: screenInitials,
+ writeInMicrosurvey: props.writeInMicrosurvey,
});
window.AWAddScreenImpression?.(screen);
@@ -215,6 +218,10 @@ export const MultiStageAboutWelcome = props => {
const [activeSingleSelectSelections, setActiveSingleSelectSelections] =
useState({});
+ // Save textarea inputs for each screen as an object keyed by screen id. It's
+ // structured like this: { screenId: { textareaId: { value, isValid } } }
+ const [textInputs, setTextInputs] = useState({});
+
// Get the active theme so the rendering code can make it selected
// by default.
const [activeTheme, setActiveTheme] = useState(null);
@@ -319,6 +326,20 @@ export const MultiStageAboutWelcome = props => {
});
};
+ const setTextInput = (value, inputId) => {
+ setTextInputs(prevState => {
+ const currentScreenInputs = prevState[currentScreen.id] || {};
+
+ return {
+ ...prevState,
+ [currentScreen.id]: {
+ ...currentScreenInputs,
+ [inputId]: value,
+ },
+ };
+ });
+ };
+
return index === order ? (
{
previousOrder={previousOrder}
content={currentScreen.content}
navigate={handleTransition}
+ autoAdvance={currentScreen.auto_advance}
messageId={`${props.message_id}_${order}_${currentScreen.id}`}
+ writeInMicrosurvey={props.writeInMicrosurvey}
UTMTerm={props.utm_term}
flowParams={flowParams}
activeTheme={activeTheme}
@@ -342,11 +365,12 @@ export const MultiStageAboutWelcome = props => {
setScreenMultiSelects={setScreenMultiSelects}
activeMultiSelect={activeMultiSelects[currentScreen.id]}
setActiveMultiSelect={setActiveMultiSelect}
- autoAdvance={currentScreen.auto_advance}
activeSingleSelectSelections={
activeSingleSelectSelections[currentScreen.id]
}
setActiveSingleSelectSelection={setActiveSingleSelectSelection}
+ textInputs={textInputs[currentScreen.id]}
+ setTextInput={setTextInput}
negotiatedLanguage={negotiatedLanguage}
langPackInstallPhase={langPackInstallPhase}
forceHideStepsIndicator={currentScreen.force_hide_steps_indicator}
@@ -378,6 +402,7 @@ const renderSingleSecondaryCTAButton = ({
position,
handleAction,
activeMultiSelect,
+ textInputs,
isArrayItem,
index = null,
}) => {
@@ -413,6 +438,16 @@ const renderSingleSecondaryCTAButton = ({
return true;
}
+ if (disabledValue === "hasTextInput") {
+ // For text input, we check if the user has entered any text in the
+ // textarea(s) present on the screen.
+ if (!textInputs) {
+ return true;
+ }
+ return Object.values(textInputs).every(
+ input => !input.isValid || input.value.trim().length === 0
+ );
+ }
return disabledValue;
};
@@ -516,6 +551,7 @@ export const SecondaryCTA = props => {
position,
handleAction: props.handleAction,
activeMultiSelect: props.activeMultiSelect,
+ textInputs: props.textInputs,
isArrayItem: true,
index,
})
@@ -531,6 +567,7 @@ export const SecondaryCTA = props => {
position,
handleAction: props.handleAction,
activeMultiSelect: props.activeMultiSelect,
+ textInputs: props.textInputs,
isArrayItem: false,
});
};
@@ -603,13 +640,17 @@ export class WelcomeScreen extends React.PureComponent {
}
logTelemetry({ value, event, source, props }) {
- AboutWelcomeUtils.sendActionTelemetry(props.messageId, source, event.name);
+ AboutWelcomeUtils.sendActionTelemetry(props.messageId, source, event.name, {
+ writeInMicrosurvey: props.writeInMicrosurvey,
+ });
// Send additional telemetry if a messaging surface like feature callout is
// dismissed via the dismiss button. Other causes of dismissal will be
// handled separately by the messaging surface's own code.
if (value === "dismiss_button" && !event.name) {
- AboutWelcomeUtils.sendDismissTelemetry(props.messageId, source);
+ AboutWelcomeUtils.sendDismissTelemetry(props.messageId, source, {
+ writeInMicrosurvey: props.writeInMicrosurvey,
+ });
}
}
@@ -620,7 +661,12 @@ export class WelcomeScreen extends React.PureComponent {
if (hasMigrate(action)) {
await window.AWWaitForMigrationClose();
- AboutWelcomeUtils.sendActionTelemetry(props.messageId, "migrate_close");
+ AboutWelcomeUtils.sendActionTelemetry(
+ props.messageId,
+ "migrate_close",
+ "CLICK_BUTTON",
+ { writeInMicrosurvey: props.writeInMicrosurvey }
+ );
}
}
@@ -709,6 +755,10 @@ export class WelcomeScreen extends React.PureComponent {
this.setMultiSelectActions(action);
}
+ if (action.collectTextInput && Object.values(props.textInputs).length) {
+ this.setTextInputActions(action);
+ }
+
let actionResult;
if (["OPEN_URL", "SHOW_FIREFOX_ACCOUNTS"].includes(action.type)) {
this.handleOpenURL(action, props.flowParams, props.UTMTerm);
@@ -721,7 +771,8 @@ export class WelcomeScreen extends React.PureComponent {
AboutWelcomeUtils.sendActionTelemetry(
props.messageId,
actionResult ? "sign_in" : "sign_in_cancel",
- "FXA_SIGNIN_FLOW"
+ "FXA_SIGNIN_FLOW",
+ { writeInMicrosurvey: props.writeInMicrosurvey }
);
}
@@ -858,9 +909,80 @@ export class WelcomeScreen extends React.PureComponent {
AboutWelcomeUtils.sendActionTelemetry(
props.messageId,
value.flat(),
- "SELECT_CHECKBOX"
+ "SELECT_CHECKBOX",
+ { writeInMicrosurvey: props.writeInMicrosurvey }
+ );
+ }
+ }
+
+ setTextInputActions(action) {
+ let { props } = this;
+
+ if (action.type !== "MULTI_ACTION") {
+ console.error(
+ "collectTextInput is only supported for MULTI_ACTION type actions"
);
+ action.type = "MULTI_ACTION";
}
+ if (!Array.isArray(action.data?.actions)) {
+ console.error(
+ "collectTextInput is only supported for MULTI_ACTION type actions with an array of actions"
+ );
+ action.data = { actions: [] };
+ }
+
+ const collectedActions = [];
+
+ // If there is no character_limit, we still need to limit the size of the
+ // input to avoid sending huge payloads. We'll go with 8KB.
+ const truncateToByteSize = (str, maxBytes) => {
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode(str);
+ if (encoded.length <= maxBytes) {
+ return str;
+ }
+ let end = maxBytes;
+ // Step back until we find a valid UTF-8 start byte
+ while (end > 0 && (encoded[end] & 0b11000000) === 0b10000000) {
+ end--; // this is a continuation byte
+ }
+ return new TextDecoder().decode(encoded.subarray(0, end));
+ };
+
+ const processTile = (tile, tileIndex) => {
+ if (tile?.type !== "textarea" || !tile.data) {
+ return;
+ }
+
+ const inputId = tile.data.id || `tile-${tileIndex}`;
+ const inputData = props.textInputs[inputId];
+ if (inputData?.isValid && inputData.value.trim().length) {
+ if (tile.data.action) {
+ collectedActions.push(tile.data.action);
+ }
+ AboutWelcomeUtils.sendActionTelemetry(
+ props.messageId,
+ inputId,
+ "TEXT_INPUT",
+ {
+ value: truncateToByteSize(inputData.value, 8192),
+ writeInMicrosurvey: props.writeInMicrosurvey,
+ }
+ );
+ }
+ };
+
+ if (props.content?.tiles) {
+ if (Array.isArray(props.content.tiles)) {
+ for (const [index, tile] of props.content.tiles.entries()) {
+ processTile(tile, index);
+ }
+ } else {
+ processTile(props.content.tiles, 0);
+ }
+ }
+
+ action.data.actions.unshift(...collectedActions);
}
render() {
@@ -880,12 +1002,15 @@ export class WelcomeScreen extends React.PureComponent {
setActiveSingleSelectSelection={
this.props.setActiveSingleSelectSelection
}
+ textInputs={this.props.textInputs}
+ setTextInput={this.props.setTextInput}
totalNumberOfScreens={this.props.totalNumberOfScreens}
appAndSystemLocaleInfo={this.props.appAndSystemLocaleInfo}
negotiatedLanguage={this.props.negotiatedLanguage}
langPackInstallPhase={this.props.langPackInstallPhase}
handleAction={this.handleAction}
messageId={this.props.messageId}
+ writeInMicrosurvey={this.props.writeInMicrosurvey}
isFirstScreen={this.props.isFirstScreen}
isLastScreen={this.props.isLastScreen}
isSingleScreen={this.props.isSingleScreen}
diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx
index 800ce64dede42..dcacf3d0d35ec 100644
--- a/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx
+++ b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx
@@ -86,6 +86,8 @@ export const MultiStageProtonScreen = props => {
setActiveMultiSelect={props.setActiveMultiSelect}
activeSingleSelectSelections={props.activeSingleSelectSelections}
setActiveSingleSelectSelection={props.setActiveSingleSelectSelection}
+ textInputs={props.textInputs}
+ setTextInput={props.setTextInput}
totalNumberOfScreens={props.totalNumberOfScreens}
handleAction={props.handleAction}
isFirstScreen={props.isFirstScreen}
@@ -101,6 +103,7 @@ export const MultiStageProtonScreen = props => {
addonIconURL={props.addonIconURL}
themeScreenshots={props.themeScreenshots}
messageId={props.messageId}
+ writeInMicrosurvey={props.writeInMicrosurvey}
negotiatedLanguage={props.negotiatedLanguage}
langPackInstallPhase={props.langPackInstallPhase}
forceHideStepsIndicator={props.forceHideStepsIndicator}
@@ -119,6 +122,7 @@ export const ProtonScreenActionButtons = props => {
addonType,
addonName,
activeMultiSelect,
+ textInputs,
installedAddons,
} = props;
const defaultValue = content.checkbox?.defaultValue;
@@ -150,8 +154,8 @@ export const ProtonScreenActionButtons = props => {
// If we have a multi-select screen, we want to disable the primary button
// until the user has selected at least one item.
- const isPrimaryDisabled = primaryDisabledValue => {
- if (primaryDisabledValue === "hasActiveMultiSelect") {
+ const isPrimaryDisabled = disabledValue => {
+ if (disabledValue === "hasActiveMultiSelect") {
if (!activeMultiSelect) {
return true;
}
@@ -164,7 +168,17 @@ export const ProtonScreenActionButtons = props => {
}
return true;
}
- return primaryDisabledValue;
+ if (disabledValue === "hasTextInput") {
+ // For text input, we check if the user has entered any text in the
+ // textarea(s) present on the screen.
+ if (!textInputs) {
+ return true;
+ }
+ return Object.values(textInputs).every(
+ input => !input.isValid || input.value.trim().length === 0
+ );
+ }
+ return disabledValue;
};
return (
@@ -212,7 +226,12 @@ export const ProtonScreenActionButtons = props => {
)}
{content.additional_button ? (
-
+
) : null}
{content.checkbox ? (
@@ -234,6 +253,7 @@ export const ProtonScreenActionButtons = props => {
content={content}
handleAction={props.handleAction}
activeMultiSelect={activeMultiSelect}
+ textInputs={textInputs}
/>
) : null}
@@ -372,6 +392,7 @@ export class ProtonScreen extends React.PureComponent {
negotiatedLanguage={this.props.negotiatedLanguage}
langPackInstallPhase={this.props.langPackInstallPhase}
messageId={this.props.messageId}
+ writeInMicrosurvey={this.props.writeInMicrosurvey}
/>
) : null;
}
@@ -615,6 +636,7 @@ export class ProtonScreen extends React.PureComponent {
addonType={this.props.addonType}
handleAction={this.props.handleAction}
activeMultiSelect={this.props.activeMultiSelect}
+ textInputs={this.props.textInputs}
/>
) : null;
}
diff --git a/browser/components/aboutwelcome/content-src/components/TextAreaTile.jsx b/browser/components/aboutwelcome/content-src/components/TextAreaTile.jsx
new file mode 100644
index 0000000000000..a0258b9bddfd4
--- /dev/null
+++ b/browser/components/aboutwelcome/content-src/components/TextAreaTile.jsx
@@ -0,0 +1,103 @@
+/* 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 React, { useEffect, useCallback, useMemo, useState } from "react";
+import { AboutWelcomeUtils } from "../lib/aboutwelcome-utils.mjs";
+
+const CONFIGURABLE_STYLES = [
+ "color",
+ "display",
+ "fontSize",
+ "fontWeight",
+ "letterSpacing",
+ "lineHeight",
+ "marginBlock",
+ "marginInline",
+ "paddingBlock",
+ "paddingInline",
+ "textAlign",
+ "whiteSpace",
+ "width",
+ "border",
+ "borderRadius",
+ "minHeight",
+ "minWidth",
+];
+
+export const TextAreaTile = ({
+ content,
+ textInputs,
+ setTextInput,
+ tileIndex,
+}) => {
+ const { data } = content.tiles;
+ const id = data.id || `tile-${tileIndex}`;
+
+ const [isValid, setIsValid] = useState(true);
+ const [charCounter, setCharCounter] = useState(data.character_limit || 0);
+
+ const textInput = useMemo(() => {
+ if (textInputs) {
+ return textInputs?.[id];
+ }
+ return null;
+ }, [textInputs, id]);
+
+ const handleChange = useCallback(
+ event => {
+ let valid = isValid;
+ if (data.character_limit) {
+ setCharCounter(data.character_limit - event.target.value.length);
+ valid = event.target.value.length <= data.character_limit;
+ }
+ setIsValid(valid);
+ setTextInput({ value: event.target.value, isValid: valid }, id);
+ },
+ [isValid, data.character_limit, id, setTextInput]
+ );
+
+ useEffect(() => {
+ if (!textInput) {
+ setTextInput({ value: "", isValid: true }, id);
+ }
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return (
+
+ {data.character_limit && (
+
+ {charCounter}
+
+ )}
+
+
+ );
+};
diff --git a/browser/components/aboutwelcome/content-src/lib/aboutwelcome-utils.mjs b/browser/components/aboutwelcome/content-src/lib/aboutwelcome-utils.mjs
index 25db675be559b..f7876e3b75c21 100644
--- a/browser/components/aboutwelcome/content-src/lib/aboutwelcome-utils.mjs
+++ b/browser/components/aboutwelcome/content-src/lib/aboutwelcome-utils.mjs
@@ -14,7 +14,7 @@ export const AboutWelcomeUtils = {
handleUserAction(action) {
return window.AWSendToParent("SPECIAL_ACTION", action);
},
- sendImpressionTelemetry(messageId, context) {
+ sendImpressionTelemetry(messageId, context = {}) {
window.AWSendEventTelemetry?.({
event: "IMPRESSION",
event_context: {
@@ -24,22 +24,28 @@ export const AboutWelcomeUtils = {
message_id: messageId,
});
},
- sendActionTelemetry(messageId, elementId, eventName = "CLICK_BUTTON") {
+ sendActionTelemetry(
+ messageId,
+ elementId,
+ eventName = "CLICK_BUTTON",
+ context = {}
+ ) {
const ping = {
event: eventName,
event_context: {
source: elementId,
page,
+ ...context,
},
message_id: messageId,
};
window.AWSendEventTelemetry?.(ping);
},
- sendDismissTelemetry(messageId, elementId) {
+ sendDismissTelemetry(messageId, elementId, context = {}) {
// Don't send DISMISS telemetry in spotlight modals since they already send
// their own equivalent telemetry.
if (page !== "spotlight") {
- this.sendActionTelemetry(messageId, elementId, "DISMISS");
+ this.sendActionTelemetry(messageId, elementId, "DISMISS", context);
}
},
async fetchFlowParams(metricsFlowUri) {
@@ -70,10 +76,15 @@ export const AboutWelcomeUtils = {
getLoadingStrategyFor(url) {
return url?.startsWith("http") ? "lazy" : "eager";
},
- handleCampaignAction(action, messageId) {
+ handleCampaignAction(action, messageId, context) {
window.AWSendToParent("HANDLE_CAMPAIGN_ACTION", action).then(handled => {
if (handled) {
- this.sendActionTelemetry(messageId, "CAMPAIGN_ACTION");
+ this.sendActionTelemetry(
+ messageId,
+ "CAMPAIGN_ACTION",
+ "CLICK_BUTTON",
+ context
+ );
}
});
},
diff --git a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js
index 87ac1bf731e12..8d9282803c34d 100644
--- a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js
+++ b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js
@@ -43,7 +43,7 @@ const AboutWelcomeUtils = {
handleUserAction(action) {
return window.AWSendToParent("SPECIAL_ACTION", action);
},
- sendImpressionTelemetry(messageId, context) {
+ sendImpressionTelemetry(messageId, context = {}) {
window.AWSendEventTelemetry?.({
event: "IMPRESSION",
event_context: {
@@ -53,22 +53,28 @@ const AboutWelcomeUtils = {
message_id: messageId,
});
},
- sendActionTelemetry(messageId, elementId, eventName = "CLICK_BUTTON") {
+ sendActionTelemetry(
+ messageId,
+ elementId,
+ eventName = "CLICK_BUTTON",
+ context = {}
+ ) {
const ping = {
event: eventName,
event_context: {
source: elementId,
page,
+ ...context,
},
message_id: messageId,
};
window.AWSendEventTelemetry?.(ping);
},
- sendDismissTelemetry(messageId, elementId) {
+ sendDismissTelemetry(messageId, elementId, context = {}) {
// Don't send DISMISS telemetry in spotlight modals since they already send
// their own equivalent telemetry.
if (page !== "spotlight") {
- this.sendActionTelemetry(messageId, elementId, "DISMISS");
+ this.sendActionTelemetry(messageId, elementId, "DISMISS", context);
}
},
async fetchFlowParams(metricsFlowUri) {
@@ -99,10 +105,15 @@ const AboutWelcomeUtils = {
getLoadingStrategyFor(url) {
return url?.startsWith("http") ? "lazy" : "eager";
},
- handleCampaignAction(action, messageId) {
+ handleCampaignAction(action, messageId, context) {
window.AWSendToParent("HANDLE_CAMPAIGN_ACTION", action).then(handled => {
if (handled) {
- this.sendActionTelemetry(messageId, "CAMPAIGN_ACTION");
+ this.sendActionTelemetry(
+ messageId,
+ "CAMPAIGN_ACTION",
+ "CLICK_BUTTON",
+ context
+ );
}
});
},
@@ -151,7 +162,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
/* harmony import */ var _LanguageSwitcher__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7);
/* harmony import */ var _SubmenuButton__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(12);
-/* harmony import */ var _lib_addUtmParams_mjs__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(28);
+/* harmony import */ var _lib_addUtmParams_mjs__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(29);
/* 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/. */
@@ -211,7 +222,9 @@ const MultiStageAboutWelcome = props => {
// blocking the thread.
window.AWGetUnhandledCampaignAction?.().then(action => {
if (typeof action === "string") {
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleCampaignAction(action, props.message_id);
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleCampaignAction(action, props.message_id, {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
}
}).catch(error => {
console.error("Failed to get unhandled campaign action:", error);
@@ -228,7 +241,8 @@ const MultiStageAboutWelcome = props => {
screen_family: props.message_id,
screen_index: order,
screen_id: screen.id,
- screen_initials: screenInitials
+ screen_initials: screenInitials,
+ writeInMicrosurvey: props.writeInMicrosurvey
});
window.AWAddScreenImpression?.(screen);
}
@@ -342,6 +356,10 @@ const MultiStageAboutWelcome = props => {
// screens, and allows us to have multiple single selects on a screen.
const [activeSingleSelectSelections, setActiveSingleSelectSelections] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
+ // Save textarea inputs for each screen as an object keyed by screen id. It's
+ // structured like this: { screenId: { textareaId: { value, isValid } } }
+ const [textInputs, setTextInputs] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({});
+
// Get the active theme so the rendering code can make it selected
// by default.
const [activeTheme, setActiveTheme] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
@@ -420,6 +438,18 @@ const MultiStageAboutWelcome = props => {
};
});
};
+ const setTextInput = (value, inputId) => {
+ setTextInputs(prevState => {
+ const currentScreenInputs = prevState[currentScreen.id] || {};
+ return {
+ ...prevState,
+ [currentScreen.id]: {
+ ...currentScreenInputs,
+ [inputId]: value
+ }
+ };
+ });
+ };
return index === order ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(WelcomeScreen, {
key: currentScreen.id + order,
id: currentScreen.id,
@@ -431,7 +461,9 @@ const MultiStageAboutWelcome = props => {
previousOrder: previousOrder,
content: currentScreen.content,
navigate: handleTransition,
+ autoAdvance: currentScreen.auto_advance,
messageId: `${props.message_id}_${order}_${currentScreen.id}`,
+ writeInMicrosurvey: props.writeInMicrosurvey,
UTMTerm: props.utm_term,
flowParams: flowParams,
activeTheme: activeTheme,
@@ -442,9 +474,10 @@ const MultiStageAboutWelcome = props => {
setScreenMultiSelects: setScreenMultiSelects,
activeMultiSelect: activeMultiSelects[currentScreen.id],
setActiveMultiSelect: setActiveMultiSelect,
- autoAdvance: currentScreen.auto_advance,
activeSingleSelectSelections: activeSingleSelectSelections[currentScreen.id],
setActiveSingleSelectSelection: setActiveSingleSelectSelection,
+ textInputs: textInputs[currentScreen.id],
+ setTextInput: setTextInput,
negotiatedLanguage: negotiatedLanguage,
langPackInstallPhase: langPackInstallPhase,
forceHideStepsIndicator: currentScreen.force_hide_steps_indicator,
@@ -469,6 +502,7 @@ const renderSingleSecondaryCTAButton = ({
position,
handleAction,
activeMultiSelect,
+ textInputs,
isArrayItem,
index = null
}) => {
@@ -495,6 +529,14 @@ const renderSingleSecondaryCTAButton = ({
}
return true;
}
+ if (disabledValue === "hasTextInput") {
+ // For text input, we check if the user has entered any text in the
+ // textarea(s) present on the screen.
+ if (!textInputs) {
+ return true;
+ }
+ return Object.values(textInputs).every(input => !input.isValid || input.value.trim().length === 0);
+ }
return disabledValue;
};
if (isTextLink) {
@@ -578,6 +620,7 @@ const SecondaryCTA = props => {
position,
handleAction: props.handleAction,
activeMultiSelect: props.activeMultiSelect,
+ textInputs: props.textInputs,
isArrayItem: true,
index
})));
@@ -589,6 +632,7 @@ const SecondaryCTA = props => {
position,
handleAction: props.handleAction,
activeMultiSelect: props.activeMultiSelect,
+ textInputs: props.textInputs,
isArrayItem: false
});
};
@@ -675,20 +719,26 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
source,
props
}) {
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, source, event.name);
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, source, event.name, {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
// Send additional telemetry if a messaging surface like feature callout is
// dismissed via the dismiss button. Other causes of dismissal will be
// handled separately by the messaging surface's own code.
if (value === "dismiss_button" && !event.name) {
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendDismissTelemetry(props.messageId, source);
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendDismissTelemetry(props.messageId, source, {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
}
}
async handleMigrationIfNeeded(action, props) {
const hasMigrate = a => a.type === "SHOW_MIGRATION_WIZARD" || a.type === "MULTI_ACTION" && a.data?.actions?.some(hasMigrate);
if (hasMigrate(action)) {
await window.AWWaitForMigrationClose();
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, "migrate_close");
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, "migrate_close", "CLICK_BUTTON", {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
}
}
applyThemeIfNeeded(action, event) {
@@ -758,6 +808,9 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
if (action.collectSelect) {
this.setMultiSelectActions(action);
}
+ if (action.collectTextInput && Object.values(props.textInputs).length) {
+ this.setTextInputActions(action);
+ }
let actionResult;
if (["OPEN_URL", "SHOW_FIREFOX_ACCOUNTS"].includes(action.type)) {
this.handleOpenURL(action, props.flowParams, props.UTMTerm);
@@ -767,7 +820,9 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
actionResult = await actionPromise;
}
if (action.type === "FXA_SIGNIN_FLOW") {
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, actionResult ? "sign_in" : "sign_in_cancel", "FXA_SIGNIN_FLOW");
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, actionResult ? "sign_in" : "sign_in_cancel", "FXA_SIGNIN_FLOW", {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
}
if (action.type === "INSTALL_ADDON_FROM_URL") {
const url = props.addonURL;
@@ -887,8 +942,68 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
action.data.actions.unshift(...multiSelectActions);
for (const value of Object.values(props.activeMultiSelect)) {
// Send telemetry with selected checkbox ids
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, value.flat(), "SELECT_CHECKBOX");
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, value.flat(), "SELECT_CHECKBOX", {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
+ }
+ }
+ setTextInputActions(action) {
+ let {
+ props
+ } = this;
+ if (action.type !== "MULTI_ACTION") {
+ console.error("collectTextInput is only supported for MULTI_ACTION type actions");
+ action.type = "MULTI_ACTION";
+ }
+ if (!Array.isArray(action.data?.actions)) {
+ console.error("collectTextInput is only supported for MULTI_ACTION type actions with an array of actions");
+ action.data = {
+ actions: []
+ };
}
+ const collectedActions = [];
+
+ // If there is no character_limit, we still need to limit the size of the
+ // input to avoid sending huge payloads. We'll go with 8KB.
+ const truncateToByteSize = (str, maxBytes) => {
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode(str);
+ if (encoded.length <= maxBytes) {
+ return str;
+ }
+ let end = maxBytes;
+ // Step back until we find a valid UTF-8 start byte
+ while (end > 0 && (encoded[end] & 0b11000000) === 0b10000000) {
+ end--; // this is a continuation byte
+ }
+ return new TextDecoder().decode(encoded.subarray(0, end));
+ };
+ const processTile = (tile, tileIndex) => {
+ if (tile?.type !== "textarea" || !tile.data) {
+ return;
+ }
+ const inputId = tile.data.id || `tile-${tileIndex}`;
+ const inputData = props.textInputs[inputId];
+ if (inputData?.isValid && inputData.value.trim().length) {
+ if (tile.data.action) {
+ collectedActions.push(tile.data.action);
+ }
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, inputId, "TEXT_INPUT", {
+ value: truncateToByteSize(inputData.value, 8192),
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
+ }
+ };
+ if (props.content?.tiles) {
+ if (Array.isArray(props.content.tiles)) {
+ for (const [index, tile] of props.content.tiles.entries()) {
+ processTile(tile, index);
+ }
+ } else {
+ processTile(props.content.tiles, 0);
+ }
+ }
+ action.data.actions.unshift(...collectedActions);
}
render() {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_3__.MultiStageProtonScreen, {
@@ -904,12 +1019,15 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
setActiveMultiSelect: this.props.setActiveMultiSelect,
activeSingleSelectSelections: this.props.activeSingleSelectSelections,
setActiveSingleSelectSelection: this.props.setActiveSingleSelectSelection,
+ textInputs: this.props.textInputs,
+ setTextInput: this.props.setTextInput,
totalNumberOfScreens: this.props.totalNumberOfScreens,
appAndSystemLocaleInfo: this.props.appAndSystemLocaleInfo,
negotiatedLanguage: this.props.negotiatedLanguage,
langPackInstallPhase: this.props.langPackInstallPhase,
handleAction: this.handleAction,
messageId: this.props.messageId,
+ writeInMicrosurvey: this.props.writeInMicrosurvey,
isFirstScreen: this.props.isFirstScreen,
isLastScreen: this.props.isLastScreen,
isSingleScreen: this.props.isSingleScreen,
@@ -1136,6 +1254,8 @@ const MultiStageProtonScreen = props => {
setActiveMultiSelect: props.setActiveMultiSelect,
activeSingleSelectSelections: props.activeSingleSelectSelections,
setActiveSingleSelectSelection: props.setActiveSingleSelectSelection,
+ textInputs: props.textInputs,
+ setTextInput: props.setTextInput,
totalNumberOfScreens: props.totalNumberOfScreens,
handleAction: props.handleAction,
isFirstScreen: props.isFirstScreen,
@@ -1151,6 +1271,7 @@ const MultiStageProtonScreen = props => {
addonIconURL: props.addonIconURL,
themeScreenshots: props.themeScreenshots,
messageId: props.messageId,
+ writeInMicrosurvey: props.writeInMicrosurvey,
negotiatedLanguage: props.negotiatedLanguage,
langPackInstallPhase: props.langPackInstallPhase,
forceHideStepsIndicator: props.forceHideStepsIndicator,
@@ -1167,6 +1288,7 @@ const ProtonScreenActionButtons = props => {
addonType,
addonName,
activeMultiSelect,
+ textInputs,
installedAddons
} = props;
const defaultValue = content.checkbox?.defaultValue;
@@ -1187,8 +1309,8 @@ const ProtonScreenActionButtons = props => {
// If we have a multi-select screen, we want to disable the primary button
// until the user has selected at least one item.
- const isPrimaryDisabled = primaryDisabledValue => {
- if (primaryDisabledValue === "hasActiveMultiSelect") {
+ const isPrimaryDisabled = disabledValue => {
+ if (disabledValue === "hasActiveMultiSelect") {
if (!activeMultiSelect) {
return true;
}
@@ -1201,7 +1323,15 @@ const ProtonScreenActionButtons = props => {
}
return true;
}
- return primaryDisabledValue;
+ if (disabledValue === "hasTextInput") {
+ // For text input, we check if the user has entered any text in the
+ // textarea(s) present on the screen.
+ if (!textInputs) {
+ return true;
+ }
+ return Object.values(textInputs).every(input => !input.isValid || input.value.trim().length === 0);
+ }
+ return disabledValue;
};
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: `action-buttons ${content.additional_button ? "additional-cta-container" : ""}`,
@@ -1235,7 +1365,9 @@ const ProtonScreenActionButtons = props => {
}) : ""
})), content.additional_button ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_AdditionalCTA__WEBPACK_IMPORTED_MODULE_8__.AdditionalCTA, {
content: content,
- handleAction: props.handleAction
+ handleAction: props.handleAction,
+ activeMultiSelect: activeMultiSelect,
+ textInputs: textInputs
}) : null, content.checkbox ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "checkbox-container"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", {
@@ -1252,7 +1384,8 @@ const ProtonScreenActionButtons = props => {
}))) : null, content.secondary_button ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_3__.SecondaryCTA, {
content: content,
handleAction: props.handleAction,
- activeMultiSelect: activeMultiSelect
+ activeMultiSelect: activeMultiSelect,
+ textInputs: textInputs
}) : null);
};
class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
@@ -1372,7 +1505,8 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
handleAction: this.props.handleAction,
negotiatedLanguage: this.props.negotiatedLanguage,
langPackInstallPhase: this.props.langPackInstallPhase,
- messageId: this.props.messageId
+ messageId: this.props.messageId,
+ writeInMicrosurvey: this.props.writeInMicrosurvey
}) : null;
}
renderDismissButton() {
@@ -1540,7 +1674,8 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
addonName: this.props.addonName,
addonType: this.props.addonType,
handleAction: this.props.handleAction,
- activeMultiSelect: this.props.activeMultiSelect
+ activeMultiSelect: this.props.activeMultiSelect,
+ textInputs: this.props.textInputs
}) : null;
}
@@ -1773,7 +1908,8 @@ function LanguageSwitcher(props) {
handleAction,
negotiatedLanguage,
langPackInstallPhase,
- messageId
+ messageId,
+ writeInMicrosurvey
} = props;
const [isAwaitingLangpack, setIsAwaitingLangpack] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
@@ -1869,7 +2005,9 @@ function LanguageSwitcher(props) {
className: "primary",
value: "primary_button",
onClick: () => {
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(messageId, "download_langpack");
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(messageId, "download_langpack", "CLICK_BUTTON", {
+ writeInMicrosurvey
+ });
setIsAwaitingLangpack(true);
}
}, content.languageSwitcher.switch ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
@@ -2040,7 +2178,9 @@ __webpack_require__.r(__webpack_exports__);
const AdditionalCTA = ({
content,
- handleAction
+ handleAction,
+ activeMultiSelect,
+ textInputs
}) => {
let buttonStyle = "";
const isSplitButton = content.submenu_button?.attached_to === "additional_button";
@@ -2053,6 +2193,28 @@ const AdditionalCTA = ({
} else {
buttonStyle = content.additional_button?.style === "link" ? "cta-link" : content.additional_button?.style;
}
+ const computeDisabled = react__WEBPACK_IMPORTED_MODULE_0___default().useCallback(disabledValue => {
+ if (disabledValue === "hasActiveMultiSelect") {
+ if (!activeMultiSelect) {
+ return true;
+ }
+ for (const key in activeMultiSelect) {
+ if (activeMultiSelect[key]?.length > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (disabledValue === "hasTextInput") {
+ // For text input, we check if the user has entered any text in the
+ // textarea(s) present on the screen.
+ if (!textInputs) {
+ return true;
+ }
+ return Object.values(textInputs).every(input => !input.isValid || input.value.trim().length === 0);
+ }
+ return disabledValue;
+ }, [activeMultiSelect, textInputs]);
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: className
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
@@ -2062,7 +2224,7 @@ const AdditionalCTA = ({
className: `${buttonStyle} additional-cta`,
onClick: handleAction,
value: "additional_button",
- disabled: content.additional_button?.disabled === true
+ disabled: computeDisabled(content.additional_button?.disabled)
})), isSplitButton ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_SubmenuButton__WEBPACK_IMPORTED_MODULE_2__.SubmenuButton, {
content: content,
handleAction: handleAction
@@ -2306,13 +2468,14 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _SingleSelect__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(17);
/* harmony import */ var _MobileDownloads__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20);
/* harmony import */ var _MultiSelect__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(21);
-/* harmony import */ var _EmbeddedMigrationWizard__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(22);
-/* harmony import */ var _EmbeddedFxBackupOptIn__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(23);
-/* harmony import */ var _ActionChecklist__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(24);
-/* harmony import */ var _EmbeddedBrowser__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(25);
-/* harmony import */ var _ConfirmationChecklist__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(26);
-/* harmony import */ var _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(3);
-/* harmony import */ var _EmbeddedBackupRestore__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(27);
+/* harmony import */ var _TextAreaTile__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(22);
+/* harmony import */ var _EmbeddedMigrationWizard__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(23);
+/* harmony import */ var _EmbeddedFxBackupOptIn__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(24);
+/* harmony import */ var _ActionChecklist__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(25);
+/* harmony import */ var _EmbeddedBrowser__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(26);
+/* harmony import */ var _ConfirmationChecklist__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(27);
+/* harmony import */ var _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(3);
+/* harmony import */ var _EmbeddedBackupRestore__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(28);
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
/* 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,
@@ -2331,6 +2494,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
+
const HEADER_STYLES = ["backgroundColor", "border", "padding", "margin", "width", "height"];
const TILE_STYLES = ["marginBlock", "marginInline", "paddingBlock", "paddingInline"];
const CONTAINER_STYLES = ["padding", "margin", "marginBlock", "marginInline", "paddingBlock", "paddingInline", "flexDirection", "flexWrap", "flexFlow", "flexGrow", "flexShrink", "justifyContent", "alignItems", "gap"];
@@ -2448,7 +2612,9 @@ const ContentTiles = props => {
}, []);
const toggleTile = (index, tile) => {
const tileId = `${tile.type}${tile.id ? "_" : ""}${tile.id ?? ""}_header`;
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId);
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_12__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, tileId, "CLICK_BUTTON", {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
if (tile.type === "link" && tile.action) {
props.handleAction({
currentTarget: {
@@ -2461,7 +2627,9 @@ const ContentTiles = props => {
};
const toggleTiles = () => {
setTilesHeaderExpanded(prev => !prev);
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, "content_tiles_header");
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_12__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, "content_tiles_header", "CLICK_BUTTON", {
+ writeInMicrosurvey: props.writeInMicrosurvey
+ });
};
function getTileMultiSelects(screenMultiSelects, index) {
return screenMultiSelects?.[`tile-${index}`];
@@ -2485,12 +2653,12 @@ const ContentTiles = props => {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
key: index,
className: `content-tile ${header ? "has-header" : ""}`,
- style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.getTileStyle(tile, TILE_STYLES)
+ style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_12__.AboutWelcomeUtils.getTileStyle(tile, TILE_STYLES)
}, header?.title && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", _extends({
className: "tile-header secondary",
onClick: () => toggleTile(index, tile)
}, tileHeaderProps, {
- style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.getValidStyle(header.style, HEADER_STYLES)
+ style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_12__.AboutWelcomeUtils.getValidStyle(header.style, HEADER_STYLES)
}), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "header-text-container"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
@@ -2525,7 +2693,8 @@ const ContentTiles = props => {
installedAddons: props.installedAddons,
message_id: props.messageId,
handleAction: props.handleAction,
- layout: content.position
+ layout: content.position,
+ writeInMicrosurvey: props.writeInMicrosurvey
}), ["theme", "single-select"].includes(tile.type) && tile.data && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_SingleSelect__WEBPACK_IMPORTED_MODULE_3__.SingleSelect, {
content: {
tiles: tile
@@ -2547,32 +2716,40 @@ const ContentTiles = props => {
activeMultiSelect: getTileActiveMultiSelect(props.activeMultiSelect, index),
setActiveMultiSelect: props.setActiveMultiSelect,
multiSelectId: `tile-${index}`
- }), tile.type === "migration-wizard" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedMigrationWizard__WEBPACK_IMPORTED_MODULE_6__.EmbeddedMigrationWizard, {
+ }), tile.type === "textarea" && tile.data && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_TextAreaTile__WEBPACK_IMPORTED_MODULE_6__.TextAreaTile, {
+ content: {
+ tiles: tile
+ },
+ textInputs: props.textInputs,
+ setTextInput: props.setTextInput,
+ tileIndex: index
+ }), tile.type === "migration-wizard" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedMigrationWizard__WEBPACK_IMPORTED_MODULE_7__.EmbeddedMigrationWizard, {
handleAction: props.handleAction,
content: {
tiles: tile
}
- }), tile.type === "action_checklist" && tile.data && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_ActionChecklist__WEBPACK_IMPORTED_MODULE_8__.ActionChecklist, {
+ }), tile.type === "action_checklist" && tile.data && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_ActionChecklist__WEBPACK_IMPORTED_MODULE_9__.ActionChecklist, {
content: content,
- message_id: props.messageId
- }), tile.type === "embedded_browser" && tile.data?.url && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedBrowser__WEBPACK_IMPORTED_MODULE_9__.EmbeddedBrowser, {
+ message_id: props.messageId,
+ writeInMicrosurvey: props.writeInMicrosurvey
+ }), tile.type === "embedded_browser" && tile.data?.url && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedBrowser__WEBPACK_IMPORTED_MODULE_10__.EmbeddedBrowser, {
url: tile.data.url,
style: tile.data.style
- }), tile.type === "backup_restore" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedBackupRestore__WEBPACK_IMPORTED_MODULE_12__.EmbeddedBackupRestore, {
+ }), tile.type === "backup_restore" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedBackupRestore__WEBPACK_IMPORTED_MODULE_13__.EmbeddedBackupRestore, {
handleAction: props.handleAction,
content: {
tiles: tile
},
skipButton: props.content.skip_button
- }), tile.type === "fx_backup_file_path" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedFxBackupOptIn__WEBPACK_IMPORTED_MODULE_7__.EmbeddedFxBackupOptIn, {
+ }), tile.type === "fx_backup_file_path" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedFxBackupOptIn__WEBPACK_IMPORTED_MODULE_8__.EmbeddedFxBackupOptIn, {
handleAction: props.handleAction,
isEncryptedBackup: content.isEncryptedBackup,
options: tile.options
- }), tile.type === "fx_backup_password" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedFxBackupOptIn__WEBPACK_IMPORTED_MODULE_7__.EmbeddedFxBackupOptIn, {
+ }), tile.type === "fx_backup_password" && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_EmbeddedFxBackupOptIn__WEBPACK_IMPORTED_MODULE_8__.EmbeddedFxBackupOptIn, {
handleAction: props.handleAction,
isEncryptedBackup: content.isEncryptedBackup,
options: tile.options
- }), tile.type === "confirmation-checklist" && tile.data && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_ConfirmationChecklist__WEBPACK_IMPORTED_MODULE_10__.ConfirmationChecklist, {
+ }), tile.type === "confirmation-checklist" && tile.data && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_ConfirmationChecklist__WEBPACK_IMPORTED_MODULE_11__.ConfirmationChecklist, {
content: tile.data,
handleAction: props.handleAction
})) : null);
@@ -2582,7 +2759,7 @@ const ContentTiles = props => {
const containerStyle = content?.tiles_container?.style;
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "content-tiles-container",
- style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_11__.AboutWelcomeUtils.getValidStyle(containerStyle, CONTAINER_STYLES)
+ style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_12__.AboutWelcomeUtils.getValidStyle(containerStyle, CONTAINER_STYLES)
}, tiles.map((tile, index) => renderContentTile(tile, index)));
}
// If tiles is not an array render the tile alone without a container
@@ -2637,7 +2814,8 @@ const AddonsPicker = props => {
}
function handleAction(event) {
const {
- message_id
+ message_id,
+ writeInMicrosurvey
} = props;
let {
action,
@@ -2656,7 +2834,9 @@ const AddonsPicker = props => {
type,
data
});
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.sendActionTelemetry(message_id, source_id);
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.sendActionTelemetry(message_id, source_id, "CLICK_BUTTON", {
+ writeInMicrosurvey
+ });
}
function handleAuthorClick(event, authorId) {
event.stopPropagation();
@@ -3347,6 +3527,81 @@ const MultiSelect = ({
/* 22 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ TextAreaTile: () => (/* binding */ TextAreaTile)
+/* harmony export */ });
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
+/* 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/. */
+
+
+
+const CONFIGURABLE_STYLES = ["color", "display", "fontSize", "fontWeight", "letterSpacing", "lineHeight", "marginBlock", "marginInline", "paddingBlock", "paddingInline", "textAlign", "whiteSpace", "width", "border", "borderRadius", "minHeight", "minWidth"];
+const TextAreaTile = ({
+ content,
+ textInputs,
+ setTextInput,
+ tileIndex
+}) => {
+ const {
+ data
+ } = content.tiles;
+ const id = data.id || `tile-${tileIndex}`;
+ const [isValid, setIsValid] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(true);
+ const [charCounter, setCharCounter] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(data.character_limit || 0);
+ const textInput = (0,react__WEBPACK_IMPORTED_MODULE_0__.useMemo)(() => {
+ if (textInputs) {
+ return textInputs?.[id];
+ }
+ return null;
+ }, [textInputs, id]);
+ const handleChange = (0,react__WEBPACK_IMPORTED_MODULE_0__.useCallback)(event => {
+ let valid = isValid;
+ if (data.character_limit) {
+ setCharCounter(data.character_limit - event.target.value.length);
+ valid = event.target.value.length <= data.character_limit;
+ }
+ setIsValid(valid);
+ setTextInput({
+ value: event.target.value,
+ isValid: valid
+ }, id);
+ }, [isValid, data.character_limit, id, setTextInput]);
+ (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
+ if (!textInput) {
+ setTextInput({
+ value: "",
+ isValid: true
+ }, id);
+ }
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
+ className: "textarea-container",
+ style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.getValidStyle(data.container_style, CONFIGURABLE_STYLES, true)
+ }, data.character_limit && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
+ className: `textarea-char-counter ${isValid ? "" : "invalid"}`,
+ style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.getValidStyle(data.char_counter_style, CONFIGURABLE_STYLES, true)
+ }, charCounter), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", {
+ name: id,
+ className: `textarea-input ${isValid ? "" : "invalid"}`,
+ rows: data.rows,
+ cols: data.cols,
+ onChange: handleChange,
+ value: textInput?.value || "",
+ placeholder: data.placeholder,
+ style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_1__.AboutWelcomeUtils.getValidStyle(data.textarea_style, CONFIGURABLE_STYLES, true)
+ }));
+};
+
+/***/ }),
+/* 23 */
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ EmbeddedMigrationWizard: () => (/* binding */ EmbeddedMigrationWizard)
@@ -3443,7 +3698,7 @@ const EmbeddedMigrationWizard = ({
};
/***/ }),
-/* 23 */
+/* 24 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
@@ -3544,7 +3799,7 @@ const EmbeddedFxBackupOptIn = ({
};
/***/ }),
-/* 24 */
+/* 25 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
@@ -3634,7 +3889,8 @@ const ActionChecklistProgressBar = ({
};
const ActionChecklist = ({
content,
- message_id
+ message_id,
+ writeInMicrosurvey
}) => {
const tiles = content.tiles.data;
const [progressValue, setProgressValue] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(0);
@@ -3678,7 +3934,9 @@ const ActionChecklist = ({
type,
data
});
- _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(message_id, source_id);
+ _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(message_id, source_id, "CLICK_BUTTON", {
+ writeInMicrosurvey
+ });
}
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "action-checklist"
@@ -3702,7 +3960,7 @@ const ActionChecklist = ({
};
/***/ }),
-/* 25 */
+/* 26 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
@@ -3770,7 +4028,7 @@ const EmbeddedBrowserInner = ({
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (EmbeddedBrowser);
/***/ }),
-/* 26 */
+/* 27 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
@@ -3832,7 +4090,7 @@ const ConfirmationChecklist = props => {
};
/***/ }),
-/* 27 */
+/* 28 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
@@ -3912,7 +4170,7 @@ const EmbeddedBackupRestore = ({
};
/***/ }),
-/* 28 */
+/* 29 */
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
@@ -4073,7 +4331,8 @@ class AboutWelcome extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
domInteractive,
mountStart: performance.getEntriesByName("mount").pop().startTime,
domState,
- source: this.props.UTMTerm
+ source: this.props.UTMTerm,
+ writeInMicrosurvey: this.props.write_in_microsurvey
});
};
if (document.readyState === "complete") {
@@ -4104,6 +4363,7 @@ class AboutWelcome extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
addonIconURL: props.iconURL,
themeScreenshots: props.screenshots,
message_id: props.messageId,
+ writeInMicrosurvey: props.write_in_microsurvey,
defaultScreens: props.screens,
updateHistory: !props.disableHistoryUpdates,
metricsFlowUri: this.state.metricsFlowUri,
diff --git a/browser/components/aboutwelcome/content/aboutwelcome.css b/browser/components/aboutwelcome/content/aboutwelcome.css
index 36f3f58c2bdad..6d41368ea02ef 100644
--- a/browser/components/aboutwelcome/content/aboutwelcome.css
+++ b/browser/components/aboutwelcome/content/aboutwelcome.css
@@ -480,6 +480,9 @@ div#feature-callout.hidden {
margin: -4px 0 0;
color: inherit;
}
+#feature-callout .screen[pos=callout] .textarea-container .textarea-char-counter {
+ font-size: 0.813em;
+}
#feature-callout .screen[pos=callout] .cta-link {
background: none;
text-decoration: underline;
@@ -2989,6 +2992,26 @@ html {
flex-grow: 0;
flex-shrink: 0;
}
+.onboardingContainer .textarea-container {
+ display: flex;
+ flex-flow: column nowrap;
+}
+.onboardingContainer .textarea-container .textarea-char-counter {
+ font-size: 13px;
+ color: var(--text-color-deemphasized);
+ text-align: end;
+ margin-block: -10px 4px;
+}
+.onboardingContainer .textarea-container .textarea-char-counter.invalid {
+ color: var(--text-color-error);
+}
+.onboardingContainer .textarea-container .textarea-input {
+ resize: none;
+}
+.onboardingContainer .textarea-container .textarea-input.invalid {
+ border-color: var(--outline-color-error);
+ outline-color: var(--outline-color-error);
+}
.onboardingContainer .confirmation-checklist-section {
display: flex;
flex-direction: column;
diff --git a/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
index 4e45c5b73701f..63a268c2cd3ac 100644
--- a/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
+++ b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
@@ -9,6 +9,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AttributionCode:
"moz-src:///browser/components/attribution/AttributionCode.sys.mjs",
+ ClientEnvironmentBase:
+ "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs",
ClientID: "resource://gre/modules/ClientID.sys.mjs",
TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
});
@@ -16,6 +18,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
ChromeUtils.defineLazyGetter(lazy, "telemetryClientId", () =>
lazy.ClientID.getClientID()
);
+ChromeUtils.defineLazyGetter(lazy, "impressionId", () => {
+ const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId";
+ let impressionId = Services.prefs.getCharPref(PREF_IMPRESSION_ID, "");
+ if (!impressionId) {
+ impressionId = String(Services.uuid.generateUUID());
+ Services.prefs.setCharPref(PREF_IMPRESSION_ID, impressionId);
+ }
+ return impressionId;
+});
ChromeUtils.defineLazyGetter(
lazy,
"browserSessionId",
@@ -60,9 +71,6 @@ export class AboutWelcomeTelemetry {
}
async _createPing(event) {
- if (event.event_context && typeof event.event_context === "object") {
- event.event_context = JSON.stringify(event.event_context);
- }
let ping = {
...event,
addon_version: Services.appinfo.appBuildID,
@@ -85,8 +93,8 @@ export class AboutWelcomeTelemetry {
* containing a nested structure of data for reporting as
* telemetry, as documented in
* https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/data_events.html
- * Does not have all of its data (`_createPing` will augment
- * with ids and attribution if available).
+ * Does not have all of its data yet (`_createPing` will
+ * augment with ids and attribution if available).
*/
async sendTelemetry(event) {
if (!this.telemetryEnabled) {
@@ -94,6 +102,51 @@ export class AboutWelcomeTelemetry {
}
const ping = await this._createPing(event);
+ this.parseAndSubmitPing(ping);
+ }
+
+ parseAndSubmitPing(ping) {
+ let pingKey = "messagingSystem";
+ if (ping.event_context) {
+ if (typeof ping.event_context === "string") {
+ try {
+ ping.event_context = JSON.parse(ping.event_context);
+ } catch (e) {
+ // The Empty JSON strings and non-objects often provided by the existing
+ // telemetry we need to send failing to parse do not fit in the spirit
+ // of what this error is meant to capture. Instead, we want to capture
+ // when what we got should have been an object, but failed to parse.
+
+ // Try to determine if this error should be recorded on
+ // messaging-system or microsurvey. This type of error *should* never
+ // happen on microsurvey, since write_in_microsurvey is always passed
+ // in an object event_context, but because the data is potentially
+ // sensitive, we should fail safe.
+ const eventContextStr = ping.event_context;
+ if (eventContextStr.length) {
+ if (eventContextStr.includes("write_in_microsurvey")) {
+ pingKey = "microsurvey";
+ ping.write_in_microsurvey = true;
+ }
+ if (eventContextStr.includes("{")) {
+ Glean[pingKey].eventContextParseError.add(1);
+ }
+ }
+ }
+ }
+ if (typeof ping.event_context === "object") {
+ pingKey = "microsurvey";
+ ping.write_in_microsurvey =
+ ping.event_context.writeInMicrosurvey ?? false;
+ delete ping.event_context.writeInMicrosurvey;
+ }
+ }
+ if (ping.write_in_microsurvey) {
+ ping.impression_id = lazy.impressionId;
+ // Remove potentially identifying information
+ delete ping.client_id;
+ delete ping.browser_session_id;
+ }
try {
this.submitGleanPingForPing(ping);
@@ -101,13 +154,25 @@ export class AboutWelcomeTelemetry {
// Though Glean APIs are forbidden to throw, it may be possible that a
// mismatch between the shape of `ping` and the defined metrics is not
// adequately handled.
- Glean.messagingSystem.gleanPingForPingFailures.add(1);
+
+ // If the message is a write-in microsurvey, we record failures on the
+ // restricted microsurvey ping. This isn't ideal, since it's a counter,
+ // but if we recorded it on the unrestricted messaging-system ping, it
+ // would be possible to line up the submission timestamps between the
+ // unrestricted failure ping and the restricted write-in response ping to
+ // link the two (and thus deanonymize the write-in response).
+ Glean[pingKey].gleanPingForPingFailures.add(1);
}
}
/**
* Tries to infer appropriate Glean metrics on the "messaging-system" ping,
- * sets them, and submits a "messaging-system" ping.
+ * sets them, and submits a "messaging-system" ping. This is mostly used to
+ * send "messaging-system" telemetry via Glean.messagingSystem, but it can
+ * also send "microsurvey" pings via Glean.microsurvey, when the event
+ * includes an "event_input_value" (which happens if the message uses the
+ * "textarea" content tile). Telemetry from such messages must be kept on a
+ * separate ping with a different data policy.
*
* Does not check if telemetry is enabled.
* (Though Glean will check the global prefs).
@@ -118,67 +183,72 @@ export class AboutWelcomeTelemetry {
submitGleanPingForPing(ping) {
lazy.log.debug(`Submitting Glean ping for ${JSON.stringify(ping)}`);
// event.event_context is an object, but it may have been stringified.
- let event_context = ping?.event_context;
+ let { event_context } = ping;
+ const writeInMicrosurvey = !!ping.write_in_microsurvey;
+ // This is the ping we record metrics in. Since write-in microsurveys can
+ // contain sensitive data, they have their own restricted ping.
+ const pingKey = writeInMicrosurvey ? "microsurvey" : "messagingSystem";
+ delete ping.write_in_microsurvey;
- if (typeof event_context === "string") {
- try {
- event_context = JSON.parse(event_context);
- } catch (e) {
- // The Empty JSON strings and non-objects often provided by the
- // existing telemetry we need to send failing to parse do not fit in
- // the spirit of what this error is meant to capture. Instead, we want
- // to capture when what we got should have been an object,
- // but failed to parse.
- if (event_context.length && event_context.includes("{")) {
- Glean.messagingSystem.eventContextParseError.add(1);
- }
- }
+ if (event_context && typeof event_context === "object") {
+ event_context = { ...event_context };
}
- // We echo certain properties from event_context into their own metrics
- // to aid analysis.
+ // We echo certain properties from event_context into their own metrics to
+ // aid analysis. Most of these are recorded in microsurvey when the message
+ // includes `write_in_microsurvey: true`.
if (event_context?.reason) {
- Glean.messagingSystem.eventReason.set(event_context.reason);
+ Glean[pingKey].eventReason.set(event_context.reason);
}
if (event_context?.page) {
- Glean.messagingSystem.eventPage.set(event_context.page);
+ Glean[pingKey].eventPage.set(event_context.page);
}
if (event_context?.source) {
- Glean.messagingSystem.eventSource.set(event_context.source);
+ Glean[pingKey].eventSource.set(event_context.source);
}
if (event_context?.screen_family) {
- Glean.messagingSystem.eventScreenFamily.set(event_context.screen_family);
+ Glean[pingKey].eventScreenFamily.set(event_context.screen_family);
+ }
+ // Do not record this metric in messagingSystem, only microsurvey
+ if (event_context?.value && writeInMicrosurvey) {
+ Glean.microsurvey.eventInputValue.set(event_context.value);
}
+ // Delete the value in event_context, because it should only be recorded in
+ // the dedicated metric above, in the microsurvey ping.
+ delete event_context?.value;
// Screen_index was being coerced into a boolean value
// which resulted in 0 (first screen index) being ignored.
if (Number.isInteger(event_context?.screen_index)) {
- Glean.messagingSystem.eventScreenIndex.set(event_context.screen_index);
+ Glean[pingKey].eventScreenIndex.set(event_context.screen_index);
}
if (event_context?.screen_id) {
- Glean.messagingSystem.eventScreenId.set(event_context.screen_id);
+ Glean[pingKey].eventScreenId.set(event_context.screen_id);
}
if (event_context?.screen_initials) {
- Glean.messagingSystem.eventScreenInitials.set(
- event_context.screen_initials
- );
+ Glean[pingKey].eventScreenInitials.set(event_context.screen_initials);
}
// The event_context is also provided as-is as stringified JSON.
if (event_context) {
- Glean.messagingSystem.eventContext.set(JSON.stringify(event_context));
+ let stringifiedEC =
+ typeof event_context === "string"
+ ? event_context
+ : JSON.stringify(event_context);
+ Glean[pingKey].eventContext.set(stringifiedEC);
}
if ("attribution" in ping) {
for (const [key, value] of Object.entries(ping.attribution)) {
const camelKey = this._snakeToCamelCase(key);
+ const attributionKey = `${pingKey}Attribution`;
try {
- Glean.messagingSystemAttribution[camelKey].set(value);
+ Glean[attributionKey][camelKey].set(value);
} catch (e) {
// We here acknowledge that we don't know the full breadth of data
// being collected. Ideally AttributionCode will later centralize
// definition and reporting of attribution data and we can be rid of
// this fail-safe for collecting the names of unknown keys.
- Glean.messagingSystemAttribution.unknownKeys[camelKey].add(1);
+ Glean[attributionKey].unknownKeys[camelKey].add(1);
}
}
}
@@ -198,22 +268,36 @@ export class AboutWelcomeTelemetry {
// Ideally this can later be removed after running for a version or two
// with no values seen in messaging_system.invalid_nested_data
if (typeof value === "object") {
- Glean.messagingSystem.invalidNestedData[camelKey].add(1);
+ Glean[pingKey].invalidNestedData[camelKey].add(1);
} else {
- Glean.messagingSystem[camelKey].set(value);
+ Glean[pingKey][camelKey].set(value);
}
} catch (e) {
// We here acknowledge that we don't know the full breadth of data being
// collected. Ideally we will later gain that confidence and can remove
// this fail-safe for collecting the names of unknown keys.
- Glean.messagingSystem.unknownKeys[camelKey].add(1);
+ Glean[pingKey].unknownKeys[camelKey].add(1);
// TODO(bug 1600008): For testing, also record the overall count.
- Glean.messagingSystem.unknownKeyCount.add(1);
+ Glean[pingKey].unknownKeyCount.add(1);
+ }
+ }
+
+ // The microsurvey ping has some special handling, because it uses OHTTP to
+ // anonymize user data. This causes it to be sent without certain metadata
+ // that we actually need. So we must reconstruct that metadata as metrics.
+ if (writeInMicrosurvey) {
+ let { os, version, channel } = lazy.ClientEnvironmentBase;
+ Glean.microsurvey.os.set(Services.appinfo.OS);
+ Glean.microsurvey.osVersion.set(os.version);
+ if (os.isWindows) {
+ Glean.microsurvey.windowsBuildNumber.set(os.windowsBuildNumber);
}
+ Glean.microsurvey.appDisplayVersion.set(version);
+ Glean.microsurvey.appChannel.set(channel);
}
// With all the metrics set, now it's time to submit this ping.
- GleanPings.messagingSystem.submit();
+ GleanPings[pingKey].submit();
}
_snakeToCamelCase(s) {
diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx
index 2a796a7dc11c4..c8717e38584ac 100644
--- a/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx
+++ b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx
@@ -518,6 +518,88 @@ describe("MultiStageAboutWelcomeProton module", () => {
);
});
+ it("Additional button with disabled: hasActiveMultiSelect property", () => {
+ // All of the above should apply to AdditionalCTA as well
+ const SCREEN_PROPS = {
+ content: {
+ title: "test title",
+ tiles: {
+ type: "multiselect",
+ data: [
+ {
+ id: "checkbox-1",
+ label: "Option 1",
+ },
+ {
+ id: "checkbox-2",
+ label: "Option 2",
+ },
+ ],
+ },
+ additional_button: {
+ label: "test additional button",
+ disabled: "hasActiveMultiSelect",
+ },
+ },
+ setScreenMultiSelects: sandbox.stub(),
+ setActiveMultiSelect: sandbox.stub(),
+ };
+ const wrapper = mount();
+ assert.ok(wrapper.exists());
+ assert.isTrue(
+ wrapper.find("button.additional-cta").prop("disabled"),
+ "Button is disabled when activeMultiSelect is null"
+ );
+
+ // should be enabled when activeMultiSelect has selections
+ wrapper.setProps({
+ activeMultiSelect: { "tile-0": ["checkbox-1"] },
+ });
+ wrapper.update();
+ assert.isFalse(
+ wrapper.find("button.additional-cta").prop("disabled"),
+ "enabled when checkboxes are selected"
+ );
+ });
+
+ it("Additional button with disabled: hasTextInput property", () => {
+ const SCREEN_PROPS = {
+ content: {
+ title: "test title",
+ tiles: {
+ type: "textarea",
+ data: {
+ id: "text-input-test",
+ character_limit: 20,
+ },
+ },
+ additional_button: {
+ label: "test additional button",
+ disabled: "hasTextInput",
+ },
+ },
+ setTextInput: sandbox.stub(),
+ };
+ const wrapper = mount();
+ assert.ok(wrapper.exists());
+ assert.isTrue(
+ wrapper.find("button.additional-cta").prop("disabled"),
+ "Button is disabled when textInputs is empty"
+ );
+
+ // should be enabled when textInputs has input
+ wrapper.setProps({
+ textInputs: {
+ "text-input-test": { value: "Some input", isValid: true },
+ },
+ });
+ wrapper.update();
+ assert.isFalse(
+ wrapper.find("button.additional-cta").prop("disabled"),
+ "enabled when textInputs has input"
+ );
+ });
+
it("should not render a progress bar if there is 1 step", () => {
const SCREEN_PROPS = {
content: {
diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx
index f9ac0b0bc43f0..8e1779eab444c 100644
--- a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx
+++ b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx
@@ -264,6 +264,163 @@ describe("MultiStageAboutWelcome module", () => {
stub.restore();
});
+ it("should send telemetry ping on collectTextInput", () => {
+ const screens = [
+ {
+ id: "TEXT_INPUT_TEST",
+ content: {
+ tiles: {
+ type: "textarea",
+ data: {
+ id: "text-input-test",
+ },
+ },
+ primary_button: {
+ label: "Test Button",
+ action: {
+ collectTextInput: true,
+ },
+ },
+ },
+ },
+ ];
+ const COMPONENT_PROPS = {
+ defaultScreens: screens,
+ message_id: "DEFAULT_ABOUTWELCOME",
+ startScreen: 0,
+ };
+ const stub = sinon.stub(AboutWelcomeUtils, "sendActionTelemetry");
+ let wrapper = mount();
+ wrapper.update();
+
+ let welcomeScreenWrapper = wrapper.find(WelcomeScreen);
+ // Set some text in the text area to simulate user input
+ const textArea = welcomeScreenWrapper.find("textarea.textarea-input");
+ textArea.simulate("change", {
+ target: { value: "Some user input text" },
+ });
+ wrapper.update();
+ welcomeScreenWrapper = wrapper.find(WelcomeScreen);
+
+ const btnPrimary = welcomeScreenWrapper.find(".primary");
+ btnPrimary.simulate("click");
+ assert.calledTwice(stub);
+ assert.equal(
+ stub.firstCall.args[0],
+ welcomeScreenWrapper.props().messageId
+ );
+ assert.equal(stub.firstCall.args[1], "primary_button");
+ assert.equal(
+ stub.lastCall.args[0],
+ welcomeScreenWrapper.props().messageId
+ );
+ assert.ok(stub.lastCall.args[1].includes("text-input-test"));
+ assert.equal(stub.lastCall.args[2], "TEXT_INPUT");
+ assert.equal(stub.lastCall.args[3].value, "Some user input text");
+ stub.restore();
+ });
+
+ it("should apply character limit to textarea tile", () => {
+ const screens = [
+ {
+ id: "TEXT_INPUT_TEST",
+ content: {
+ tiles: {
+ type: "textarea",
+ data: {
+ id: "text-input-test",
+ character_limit: 20,
+ },
+ },
+ primary_button: {
+ label: "Test Button",
+ action: {
+ collectTextInput: true,
+ },
+ disabled: "hasTextInput",
+ },
+ },
+ },
+ ];
+ const COMPONENT_PROPS = {
+ defaultScreens: screens,
+ message_id: "DEFAULT_ABOUTWELCOME",
+ startScreen: 0,
+ };
+ let wrapper = mount();
+ wrapper.update();
+
+ // Input a 10,000 character long string to exceed the character limit
+ const textArea = wrapper.find("textarea.textarea-input");
+ const value = "A".repeat(21);
+ textArea.simulate("change", { target: { value } });
+ wrapper.update();
+
+ // Check the char counter, it should show a negative value and be invalid
+ const charCounter = wrapper.find(".textarea-char-counter");
+ assert.equal(charCounter.text(), "-1");
+ assert.ok(charCounter.hasClass("invalid"));
+
+ // The primary button should be disabled due to hasTextInput rule
+ const btnPrimary = wrapper.find(".primary");
+ assert.ok(btnPrimary.props().disabled);
+ });
+
+ it("should truncate long text input values in telemetry ping on collectTextInput", () => {
+ const screens = [
+ {
+ id: "TEXT_INPUT_TEST",
+ content: {
+ tiles: {
+ type: "textarea",
+ data: {},
+ },
+ primary_button: {
+ label: "Test Button",
+ action: {
+ collectTextInput: true,
+ },
+ },
+ },
+ },
+ ];
+ const COMPONENT_PROPS = {
+ defaultScreens: screens,
+ message_id: "DEFAULT_ABOUTWELCOME",
+ startScreen: 0,
+ };
+ const stub = sinon.stub(AboutWelcomeUtils, "sendActionTelemetry");
+ let wrapper = mount();
+ wrapper.update();
+
+ let welcomeScreenWrapper = wrapper.find(WelcomeScreen);
+ // Input a 10,000 character long string to exceed the 8KB limit
+ const textArea = welcomeScreenWrapper.find("textarea.textarea-input");
+ const value = "A".repeat(10_000);
+ textArea.simulate("change", { target: { value } });
+ wrapper.update();
+ welcomeScreenWrapper = wrapper.find(WelcomeScreen);
+
+ // Record telemetry - this is where the value should be truncated
+ const btnPrimary = welcomeScreenWrapper.find(".primary");
+ btnPrimary.simulate("click");
+ assert.calledTwice(stub);
+ assert.equal(
+ stub.firstCall.args[0],
+ welcomeScreenWrapper.props().messageId
+ );
+ assert.equal(stub.firstCall.args[1], "primary_button");
+ assert.equal(
+ stub.lastCall.args[0],
+ welcomeScreenWrapper.props().messageId
+ );
+ // Should generate a tile id automatically if omitted
+ assert.ok(stub.lastCall.args[1].includes("tile-0"));
+ assert.equal(stub.lastCall.args[2], "TEXT_INPUT");
+ assert.equal(stub.lastCall.args[3].value.length, 8192);
+ stub.restore();
+ });
+
it("does not render anything until targeting/filtering resolves (gated first paint)", async () => {
let resolveTargeting;
const targetingPromise = new Promise(r => (resolveTargeting = r));
diff --git a/browser/components/aboutwelcome/tests/xpcshell/test_AboutWelcomeTelemetry.js b/browser/components/aboutwelcome/tests/xpcshell/test_AboutWelcomeTelemetry.js
index 227ffef0f1a33..3091a716c3efa 100644
--- a/browser/components/aboutwelcome/tests/xpcshell/test_AboutWelcomeTelemetry.js
+++ b/browser/components/aboutwelcome/tests/xpcshell/test_AboutWelcomeTelemetry.js
@@ -18,8 +18,9 @@ const { sinon } = ChromeUtils.importESModule(
);
const TELEMETRY_PREF = "browser.newtabpage.activity-stream.telemetry";
-add_setup(async () => {
+add_setup(async function setup() {
do_get_profile();
+ await TelemetryController.testReset();
Services.fog.initializeFOG();
await TelemetryController.testSetup();
});
@@ -58,6 +59,61 @@ add_task(async function test_pingPayload() {
ok(pingSubmitted, "Glean ping was submitted");
});
+add_task(async function test_pingPayload_writeInMicrosurvey() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ let pingSubmitted = false;
+ GleanPings.microsurvey.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(Glean.microsurvey.event.testGetValue(), "MOCHITEST");
+ Assert.ok(
+ Glean.microsurvey.impressionId.testGetValue(),
+ "impression_id should be set"
+ );
+ });
+ await AWTelemetry.sendTelemetry({
+ event: "MOCHITEST",
+ event_context: { writeInMicrosurvey: true },
+ });
+
+ ok(pingSubmitted, "Glean ping was submitted");
+});
+
+add_task(async function test_pingPayload_nowriteInMicrosurvey() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(Glean.messagingSystem.event.testGetValue(), "MOCHITEST");
+ Assert.ok(
+ Glean.messagingSystem.clientId.testGetValue(),
+ "client_id should be set"
+ );
+ Assert.ok(
+ Glean.messagingSystem.browserSessionId.testGetValue(),
+ "browser_session_id should be set"
+ );
+ Assert.ok(
+ !Glean.messagingSystem.impressionId.testGetValue(),
+ "impression_id should be excluded"
+ );
+ });
+ await AWTelemetry.sendTelemetry({
+ event: "MOCHITEST",
+ });
+
+ ok(pingSubmitted, "Glean ping was submitted");
+});
+
add_task(async function test_mayAttachAttribution() {
const sandbox = sinon.createSandbox();
Services.prefs.setBoolPref(TELEMETRY_PREF, true);
@@ -127,3 +183,256 @@ add_task(async function test_mayAttachAttribution() {
}
);
});
+
+// We recognize two kinds of unexpected data that might reach
+// `submitGleanPingForPing`: unknown keys, and keys with unexpectedly-complex
+// data (ie, non-scalar).
+// We report the keys in special metrics to aid in system health monitoring.
+add_task(function test_weird_data() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ const unknownKey = "some_unknown_key";
+ const camelUnknownKey = AWTelemetry._snakeToCamelCase(unknownKey);
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.unknownKeys[camelUnknownKey].testGetValue(),
+ 1,
+ "caught the unknown key"
+ );
+ // TODO(bug 1600008): Also check the for-testing overall count.
+ Assert.equal(Glean.messagingSystem.unknownKeyCount.testGetValue(), 1);
+ });
+ AWTelemetry.parseAndSubmitPing({
+ [unknownKey]: "value doesn't matter",
+ });
+
+ Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
+
+ const invalidNestedDataKey = "event";
+ pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.invalidNestedData[
+ invalidNestedDataKey
+ ].testGetValue("messaging-system"),
+ 1,
+ "caught the invalid nested data"
+ );
+ });
+ AWTelemetry.parseAndSubmitPing({
+ [invalidNestedDataKey]: { this_should: "not be", complex: "data" },
+ });
+
+ Assert.ok(pingSubmitted, "Ping with invalid nested data submitted");
+});
+
+// `event_context` is weird. It's an object, but it might have been stringified
+// before being provided for recording.
+add_task(async function test_event_context() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ const eventContext = {
+ reason: "reason",
+ page: "page",
+ source: "source",
+ value: "input value",
+ something_else: "not specifically handled",
+ screen_family: "family",
+ screen_id: "screen_id",
+ screen_index: 0,
+ screen_initials: "screen_initials",
+ };
+ let expectedEC = { ...eventContext };
+ // we delete it from context to avoid raising the metric's sensitivity
+ delete expectedEC.value;
+ const stringifiedExpectedEC = JSON.stringify(expectedEC);
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventReason.testGetValue(),
+ eventContext.reason,
+ "event_context.reason also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventPage.testGetValue(),
+ eventContext.page,
+ "event_context.page also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventSource.testGetValue(),
+ eventContext.source,
+ "event_context.source also in own metric."
+ );
+ Assert.ok(
+ !Glean.messagingSystem.eventInputValue?.testGetValue(),
+ "event_context.value is scrubbed from messagingSystem pings unless they have write_in_microsurvey: true."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenFamily.testGetValue(),
+ eventContext.screen_family,
+ "event_context.screen_family also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenId.testGetValue(),
+ eventContext.screen_id,
+ "event_context.screen_id also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenIndex.testGetValue(),
+ eventContext.screen_index,
+ "event_context.screen_index also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenInitials.testGetValue(),
+ eventContext.screen_initials,
+ "event_context.screen_initials also in own metric."
+ );
+
+ Assert.equal(
+ Glean.messagingSystem.eventContext.testGetValue(),
+ stringifiedExpectedEC,
+ "whole event_context added as text."
+ );
+ });
+ AWTelemetry.parseAndSubmitPing({
+ event_context: eventContext,
+ });
+ Assert.ok(pingSubmitted, "Ping with object event_context submitted");
+
+ pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventReason.testGetValue(),
+ eventContext.reason,
+ "event_context.reason also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventPage.testGetValue(),
+ eventContext.page,
+ "event_context.page also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventSource.testGetValue(),
+ eventContext.source,
+ "event_context.source also in own metric."
+ );
+ Assert.ok(
+ !Glean.messagingSystem.eventInputValue?.testGetValue(),
+ "event_context.value is scrubbed from messagingSystem pings unless they have write_in_microsurvey: true."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenFamily.testGetValue(),
+ eventContext.screen_family,
+ "event_context.screen_family also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenId.testGetValue(),
+ eventContext.screen_id,
+ "event_context.screen_id also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenIndex.testGetValue(),
+ eventContext.screen_index,
+ "event_context.screen_index also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenInitials.testGetValue(),
+ eventContext.screen_initials,
+ "event_context.screen_initials also in own metric."
+ );
+
+ Assert.equal(
+ Glean.messagingSystem.eventContext.testGetValue(),
+ stringifiedExpectedEC,
+ "whole event_context added as text."
+ );
+ });
+ AWTelemetry.parseAndSubmitPing({
+ event_context: JSON.stringify(eventContext),
+ });
+ Assert.ok(pingSubmitted, "Ping with string event_context submitted");
+
+ eventContext.writeInMicrosurvey = true;
+ pingSubmitted = false;
+ GleanPings.microsurvey.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.microsurvey.eventContext.testGetValue(),
+ stringifiedExpectedEC,
+ "whole event_context added as text."
+ );
+ Assert.equal(
+ Glean.microsurvey.eventInputValue.testGetValue(),
+ "input value",
+ "event_context.value is included in microsurvey pings."
+ );
+ });
+ await AWTelemetry.sendTelemetry({ event_context: eventContext });
+ Assert.ok(pingSubmitted, "Ping with writeInMicrosurvey submitted");
+});
+
+// For event_context to be more useful, we want to make sure we don't error
+// in cases where it doesn't make much sense, such as a plain string that
+// doesnt attempt to represent a valid object.
+add_task(function test_context_errors() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ let weird_context_ping = {
+ event_context: "oops, this string isn't a valid JS object!",
+ };
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventContextParseError.testGetValue(),
+ undefined,
+ "this poorly formed context shouldn't register because it was not an object!"
+ );
+ });
+
+ AWTelemetry.parseAndSubmitPing(weird_context_ping);
+
+ Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
+
+ weird_context_ping = {
+ event_context:
+ "{oops : {'this string isn't a valid JS object, but it sure looks like one!}}'",
+ };
+
+ pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventContextParseError.testGetValue(),
+ 1,
+ "this poorly formed context should register because it was not an object!"
+ );
+ });
+
+ AWTelemetry.parseAndSubmitPing(weird_context_ping);
+
+ Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
+});
diff --git a/browser/components/aboutwelcome/tests/xpcshell/test_AboutWelcomeTelemetry_glean.js b/browser/components/aboutwelcome/tests/xpcshell/test_AboutWelcomeTelemetry_glean.js
deleted file mode 100644
index 5191f05d04ca4..0000000000000
--- a/browser/components/aboutwelcome/tests/xpcshell/test_AboutWelcomeTelemetry_glean.js
+++ /dev/null
@@ -1,238 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-"use strict";
-
-const { AboutWelcomeTelemetry } = ChromeUtils.importESModule(
- "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs"
-);
-const TELEMETRY_PREF = "browser.newtabpage.activity-stream.telemetry";
-
-add_setup(function setup() {
- do_get_profile();
- Services.fog.initializeFOG();
-});
-
-// We recognize two kinds of unexpected data that might reach
-// `submitGleanPingForPing`: unknown keys, and keys with unexpectedly-complex
-// data (ie, non-scalar).
-// We report the keys in special metrics to aid in system health monitoring.
-add_task(function test_weird_data() {
- registerCleanupFunction(() => {
- Services.prefs.clearUserPref(TELEMETRY_PREF);
- });
- Services.prefs.setBoolPref(TELEMETRY_PREF, true);
-
- const AWTelemetry = new AboutWelcomeTelemetry();
-
- const unknownKey = "some_unknown_key";
- const camelUnknownKey = AWTelemetry._snakeToCamelCase(unknownKey);
-
- let pingSubmitted = false;
- GleanPings.messagingSystem.testBeforeNextSubmit(() => {
- pingSubmitted = true;
- Assert.equal(
- Glean.messagingSystem.unknownKeys[camelUnknownKey].testGetValue(),
- 1,
- "caught the unknown key"
- );
- // TODO(bug 1600008): Also check the for-testing overall count.
- Assert.equal(Glean.messagingSystem.unknownKeyCount.testGetValue(), 1);
- });
- AWTelemetry.submitGleanPingForPing({
- [unknownKey]: "value doesn't matter",
- });
-
- Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
-
- const invalidNestedDataKey = "event";
- pingSubmitted = false;
- GleanPings.messagingSystem.testBeforeNextSubmit(() => {
- pingSubmitted = true;
- Assert.equal(
- Glean.messagingSystem.invalidNestedData[
- invalidNestedDataKey
- ].testGetValue("messaging-system"),
- 1,
- "caught the invalid nested data"
- );
- });
- AWTelemetry.submitGleanPingForPing({
- [invalidNestedDataKey]: { this_should: "not be", complex: "data" },
- });
-
- Assert.ok(pingSubmitted, "Ping with invalid nested data submitted");
-});
-
-// `event_context` is weird. It's an object, but it might have been stringified
-// before being provided for recording.
-add_task(function test_event_context() {
- registerCleanupFunction(() => {
- Services.prefs.clearUserPref(TELEMETRY_PREF);
- });
- Services.prefs.setBoolPref(TELEMETRY_PREF, true);
-
- const AWTelemetry = new AboutWelcomeTelemetry();
-
- const eventContext = {
- reason: "reason",
- page: "page",
- source: "source",
- something_else: "not specifically handled",
- screen_family: "family",
- screen_id: "screen_id",
- screen_index: 0,
- screen_initlals: "screen_initials",
- };
- const stringifiedEC = JSON.stringify(eventContext);
-
- let pingSubmitted = false;
- GleanPings.messagingSystem.testBeforeNextSubmit(() => {
- pingSubmitted = true;
- Assert.equal(
- Glean.messagingSystem.eventReason.testGetValue(),
- eventContext.reason,
- "event_context.reason also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventPage.testGetValue(),
- eventContext.page,
- "event_context.page also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventSource.testGetValue(),
- eventContext.source,
- "event_context.source also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenFamily.testGetValue(),
- eventContext.screen_family,
- "event_context.screen_family also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenId.testGetValue(),
- eventContext.screen_id,
- "event_context.screen_id also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenIndex.testGetValue(),
- eventContext.screen_index,
- "event_context.screen_index also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenInitials.testGetValue(),
- eventContext.screen_initials,
- "event_context.screen_initials also in own metric."
- );
-
- Assert.equal(
- Glean.messagingSystem.eventContext.testGetValue(),
- stringifiedEC,
- "whole event_context added as text."
- );
- });
- AWTelemetry.submitGleanPingForPing({
- event_context: eventContext,
- });
- Assert.ok(pingSubmitted, "Ping with object event_context submitted");
-
- pingSubmitted = false;
- GleanPings.messagingSystem.testBeforeNextSubmit(() => {
- pingSubmitted = true;
- Assert.equal(
- Glean.messagingSystem.eventReason.testGetValue(),
- eventContext.reason,
- "event_context.reason also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventPage.testGetValue(),
- eventContext.page,
- "event_context.page also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventSource.testGetValue(),
- eventContext.source,
- "event_context.source also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenFamily.testGetValue(),
- eventContext.screen_family,
- "event_context.screen_family also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenId.testGetValue(),
- eventContext.screen_id,
- "event_context.screen_id also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenIndex.testGetValue(),
- eventContext.screen_index,
- "event_context.screen_index also in own metric."
- );
- Assert.equal(
- Glean.messagingSystem.eventScreenInitials.testGetValue(),
- eventContext.screen_initials,
- "event_context.screen_initials also in own metric."
- );
-
- Assert.equal(
- Glean.messagingSystem.eventContext.testGetValue(),
- stringifiedEC,
- "whole event_context added as text."
- );
- });
- AWTelemetry.submitGleanPingForPing({
- event_context: stringifiedEC,
- });
- Assert.ok(pingSubmitted, "Ping with string event_context submitted");
-});
-
-// For event_context to be more useful, we want to make sure we don't error
-// in cases where it doesn't make much sense, such as a plain string that
-// doesnt attempt to represent a valid object.
-add_task(function test_context_errors() {
- registerCleanupFunction(() => {
- Services.prefs.clearUserPref(TELEMETRY_PREF);
- });
- Services.prefs.setBoolPref(TELEMETRY_PREF, true);
-
- const AWTelemetry = new AboutWelcomeTelemetry();
-
- let weird_context_ping = {
- event_context: "oops, this string isn't a valid JS object!",
- };
-
- let pingSubmitted = false;
- GleanPings.messagingSystem.testBeforeNextSubmit(() => {
- pingSubmitted = true;
- Assert.equal(
- Glean.messagingSystem.eventContextParseError.testGetValue(),
- undefined,
- "this poorly formed context shouldn't register because it was not an object!"
- );
- });
-
- AWTelemetry.submitGleanPingForPing(weird_context_ping);
-
- Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
-
- weird_context_ping = {
- event_context:
- "{oops : {'this string isn't a valid JS object, but it sure looks like one!}}'",
- };
-
- pingSubmitted = false;
- GleanPings.messagingSystem.testBeforeNextSubmit(() => {
- pingSubmitted = true;
- Assert.equal(
- Glean.messagingSystem.eventContextParseError.testGetValue(),
- 1,
- "this poorly formed context should register because it was not an object!"
- );
- });
-
- AWTelemetry.submitGleanPingForPing(weird_context_ping);
-
- Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
-});
diff --git a/browser/components/aboutwelcome/tests/xpcshell/xpcshell.toml b/browser/components/aboutwelcome/tests/xpcshell/xpcshell.toml
index 67e7ca2da8dc2..daa56baede181 100644
--- a/browser/components/aboutwelcome/tests/xpcshell/xpcshell.toml
+++ b/browser/components/aboutwelcome/tests/xpcshell/xpcshell.toml
@@ -9,5 +9,3 @@ firefox-appdir = "browser"
["test_AboutWelcomeTelemetry.js"]
["test_AboutWelcomeTelemetry_exposure.js"]
-
-["test_AboutWelcomeTelemetry_glean.js"]
diff --git a/browser/components/aiwindow/ui/assets/model-choice-1.svg b/browser/components/aiwindow/ui/assets/model-choice-1.svg
index 5ce23b42cd2c7..4d71dec7d5543 100644
--- a/browser/components/aiwindow/ui/assets/model-choice-1.svg
+++ b/browser/components/aiwindow/ui/assets/model-choice-1.svg
@@ -1,4 +1,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/browser/components/aiwindow/ui/assets/model-choice-2.svg b/browser/components/aiwindow/ui/assets/model-choice-2.svg
index aef2128f84c9e..8891154af6484 100644
--- a/browser/components/aiwindow/ui/assets/model-choice-2.svg
+++ b/browser/components/aiwindow/ui/assets/model-choice-2.svg
@@ -1,4 +1,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/browser/components/aiwindow/ui/assets/model-choice-3.svg b/browser/components/aiwindow/ui/assets/model-choice-3.svg
index 03180ef986954..eb87231cbd8da 100644
--- a/browser/components/aiwindow/ui/assets/model-choice-3.svg
+++ b/browser/components/aiwindow/ui/assets/model-choice-3.svg
@@ -1,4 +1,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/browser/components/aiwindow/ui/content/ai-window-content.css b/browser/components/aiwindow/ui/content/ai-window-content.css
index 4f9ed84054f94..edc319b77379e 100644
--- a/browser/components/aiwindow/ui/content/ai-window-content.css
+++ b/browser/components/aiwindow/ui/content/ai-window-content.css
@@ -6,12 +6,11 @@
/* override --background-color-canvas from tokens-shared.css */
background-color: transparent;
- /* Brand colors */
- --aiwindow-brand-soft-purple: #f1e7f8;
-
/* AI Window gradients */
--aiwindow-gradient-button: linear-gradient(97deg, #8341ca -31.39%, #656fff 90.52%);
+ --aiwindow-gradient-button-dark: linear-gradient(64deg, #4e2e9d -5.71%, #c1a9ff 122.83%);
--aiwindow-input-gradient: linear-gradient(117deg, #321bfd -17.87%, #cf30e2 52.93%, #f90 89.02%, #f5c451 109.44%);
+ --aiwindow-input-gradient-dark: linear-gradient(90deg, #945af2, #f37e49);
}
html,
diff --git a/browser/components/aiwindow/ui/content/firstrun.css b/browser/components/aiwindow/ui/content/firstrun.css
index 69cde60500504..e1f1262116194 100644
--- a/browser/components/aiwindow/ui/content/firstrun.css
+++ b/browser/components/aiwindow/ui/content/firstrun.css
@@ -14,9 +14,15 @@
}
#multi-stage-message-root {
- --bg-white: light-dark(#fff, #fff);
--bg-transparent: transparent;
- --shadow-color: rgb(59 34 121);
+ --card-bg: light-dark(#fff, #191622);
+ --card-bg-translucent: light-dark(color-mix(in srgb, #fff 60%, var(--bg-transparent)), color-mix(in srgb, #191622 40%, var(--bg-transparent)));
+ --card-border-color: light-dark(#f1e7f8, #736a8a);
+ --card-border-gradient: light-dark(var(--aiwindow-input-gradient), var(--aiwindow-input-gradient-dark));
+ --card-selected-shadow: light-dark(0 3px 16px 0 color-mix(in srgb, rgb(59 34 121) 12%, var(--bg-transparent)), 0 27px 124px 0 rgba(117, 67, 227, 0.18));
+ --brand-text-color: light-dark(#210340, #e4dcf7);
+ --button-text-color: light-dark(var(--button-text-color-primary), #fbfbfe);
+ --button-gradient: light-dark(var(--aiwindow-gradient-button), var(--aiwindow-gradient-button-dark));
.main-content,
.section-main,
@@ -31,7 +37,7 @@
#mainContentHeader {
/* stylelint-disable-next-line -- using linear gradient for text effect */
- background: linear-gradient(91deg, #7630c0 22.04%, #5c66ee 78.7%);
+ background: light-dark(linear-gradient(91deg, #7630c0 22.04%, #5c66ee 78.7%), linear-gradient(65deg, #9165ff 18.85%, #d8c9ff 78.79%));
background-clip: text;
/* stylelint-disable-next-line -- transparent needed for gradient text effect */
color: var(--bg-transparent);
@@ -50,6 +56,8 @@
p {
letter-spacing: 0.3px;
line-height: normal;
+ /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens -- custom brand color per Figma design */
+ color: var(--brand-text-color);
}
.welcome-text h2 {
@@ -66,16 +74,16 @@
gap: var(--space-small);
padding: var(--space-xsmall) var(--space-large);
margin: 0 auto;
- border: var(--border-width) solid var(--border-color-transparent);
border-radius: var(--border-radius-medium);
- /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens -- gradient defined in ai-window-content.css */
- background: var(--aiwindow-gradient-button);
- color: var(--button-text-color-primary);
+ /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens -- custom gradient per Figma design */
+ background: var(--button-gradient);
+ color: var(--button-text-color);
text-align: center;
font-size: var(--button-font-size);
font-weight: var(--button-font-weight);
letter-spacing: -0.3px;
cursor: pointer;
+ border: none;
}
&:not(:has(.icon.selected)) button.primary {
@@ -95,7 +103,7 @@
height: calc(var(--size-item-medium) * 11);
padding-block: calc(var(--space-xsmall) * 9) 0;
/* stylelint-disable-next-line -- custom border color per Figma design */
- border: var(--border-width) solid var(--aiwindow-brand-soft-purple);
+ border: var(--border-width) solid var(--card-border-color);
display: flex;
flex-direction: column;
justify-content: start;
@@ -104,8 +112,10 @@
padding-inline: var(--space-medium);
box-sizing: border-box;
border-radius: var(--border-radius-large);
- /* stylelint-disable-next-line -- using white for consistent background */
- background: color-mix(in srgb, var(--bg-white) 60%, var(--bg-transparent));
+ /* stylelint-disable-next-line -- custom background per Figma design */
+ background: var(--card-bg-translucent);
+ /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens -- custom brand color per Figma design */
+ color: var(--brand-text-color);
.label-text {
margin-top: calc(var(--space-xsmall) * 5);
@@ -130,8 +140,8 @@
}
&:hover:not(:has(.selected)) {
- /* stylelint-disable-next-line -- using white for hover state */
- background: var(--bg-white);
+ /* stylelint-disable-next-line -- custom background for hover state */
+ background: var(--card-bg);
cursor: pointer;
}
@@ -139,12 +149,10 @@
border: calc(var(--border-width) * 2) solid var(--border-color-transparent);
/* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens -- gradient defined in ai-window-content.css */
background:
- linear-gradient(#fff, #fff) padding-box,
- var(--aiwindow-input-gradient) border-box;
- /* stylelint-disable-next-line -- custom shadow color per Figma design */
- box-shadow:
- 0 3px 16px 0 color-mix(in srgb, var(--shadow-color) 12%, var(--bg-transparent)),
- 0 1px 2px 0 color-mix(in srgb, var(--shadow-color) 20%, var(--bg-transparent));
+ linear-gradient(var(--card-bg), var(--card-bg)) padding-box,
+ var(--card-border-gradient) border-box;
+ /* stylelint-disable-next-line -- custom shadow per Figma design */
+ box-shadow: var(--card-selected-shadow);
}
}
}
diff --git a/browser/components/aiwindow/ui/content/firstrun.js b/browser/components/aiwindow/ui/content/firstrun.js
index 19d9f95921def..978fc3a1fbd05 100644
--- a/browser/components/aiwindow/ui/content/firstrun.js
+++ b/browser/components/aiwindow/ui/content/firstrun.js
@@ -14,7 +14,6 @@ const MODEL_PREF = "browser.smartwindow.firstrun.modelChoice";
const AUTO_ADVANCE_PREF = "browser.smartwindow.firstrun.autoAdvanceMS";
const FIRST_RUN_COMPLETE_PREF = "browser.smartwindow.firstrun.hasCompleted";
const EXPLAINER_PAGE_PREF = "browser.smartwindow.firstrun.explainerURL";
-const BRAND_DARK_PURPLE = "#210340";
const autoAdvanceMS = Services.prefs.getIntPref(AUTO_ADVANCE_PREF);
@@ -77,7 +76,6 @@ const AI_WINDOW_CONFIG = {
string_id: "aiwindow-firstrun-model-subtitle",
fontSize: "17px",
fontWeight: 320,
- color: BRAND_DARK_PURPLE,
},
tiles: {
type: "single-select",
@@ -93,7 +91,6 @@ const AI_WINDOW_CONFIG = {
string_id: "aiwindow-firstrun-model-fast-label",
fontSize: "20px",
fontWeight: 613,
- color: BRAND_DARK_PURPLE,
},
icon: {
background:
@@ -101,7 +98,6 @@ const AI_WINDOW_CONFIG = {
},
body: {
string_id: "aiwindow-firstrun-model-fast-body",
- color: BRAND_DARK_PURPLE,
fontSize: "15px",
fontWeight: 320,
},
@@ -121,7 +117,6 @@ const AI_WINDOW_CONFIG = {
string_id: "aiwindow-firstrun-model-allpurpose-label",
fontSize: "20px",
fontWeight: 613,
- color: BRAND_DARK_PURPLE,
},
icon: {
background:
@@ -129,7 +124,6 @@ const AI_WINDOW_CONFIG = {
},
body: {
string_id: "aiwindow-firstrun-model-allpurpose-body",
- color: BRAND_DARK_PURPLE,
fontSize: "15px",
fontWeight: 320,
},
@@ -149,7 +143,6 @@ const AI_WINDOW_CONFIG = {
string_id: "aiwindow-firstrun-model-personal-label",
fontSize: "20px",
fontWeight: 613,
- color: BRAND_DARK_PURPLE,
},
icon: {
background:
@@ -157,7 +150,6 @@ const AI_WINDOW_CONFIG = {
},
body: {
string_id: "aiwindow-firstrun-model-personal-body",
- color: BRAND_DARK_PURPLE,
fontSize: "15px",
fontWeight: 320,
},
diff --git a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_firstrun.js b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_firstrun.js
index 18f5ac8703897..c89623716bfe0 100644
--- a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_firstrun.js
+++ b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_firstrun.js
@@ -199,6 +199,7 @@ add_task(async function test_firstrun_explainer_page_opens() {
set: [
["browser.smartwindow.enabled", true],
["browser.smartwindow.firstrun.hasCompleted", false],
+ ["browser.smartwindow.firstrun.modelChoice", ""],
[explainerPref, exampleURL],
],
});
diff --git a/browser/components/asrouter/.eslintrc.mjs b/browser/components/asrouter/.eslintrc.mjs
index a8a209c40cecf..b4cba8b64fd3d 100644
--- a/browser/components/asrouter/.eslintrc.mjs
+++ b/browser/components/asrouter/.eslintrc.mjs
@@ -53,7 +53,6 @@ export default [
"guard-for-in": "error",
"max-nested-callbacks": ["error", 4],
"max-params": ["error", 6],
- "max-statements": ["error", 50],
"new-cap": ["error", { newIsCap: true, capIsNew: false }],
"no-alert": "error",
"no-div-regex": "error",
diff --git a/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json b/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json
index 48695751cab5e..2f25362674773 100644
--- a/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json
+++ b/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json
@@ -585,7 +585,7 @@
"properties": {
"where": {
"type": "string",
- "description": "Where to open—e.g. 'tab', 'window', etc."
+ "description": "Where to open - e.g. 'tab', 'window', etc."
}
},
"additionalProperties": false
@@ -779,7 +779,7 @@
},
"dismiss": {
"type": "boolean",
- "description": "If true, dismiss the infobar after handling this link’s action. Defaults to false."
+ "description": "If true, dismiss the infobar after handling this link's action. Defaults to false."
}
},
"required": [
@@ -1327,6 +1327,10 @@
"type": "boolean",
"description": "Don't allow the message to be dismissed using the ESC key - for limited use in Spotlight modals only when the message content clearly informs the user that a decision is required to proceed"
},
+ "write_in_microsurvey": {
+ "type": "boolean",
+ "description": "Set to true to apply the write-in microsurvey data policy. This is required for write-in microsurveys. Messages using the textarea tile should always set this to true. It prevents client_id from being recorded with any telemetry events for the message, recording a unique impression_id instead. It also sends the events on the more restrictive microsurvey ping instead of the messaging-system ping."
+ },
"disableHistoryUpdates": {
"type": "boolean",
"description": "Don't alter the browser session's history stack - used with messaging surfaces like Feature Callouts"
diff --git a/browser/components/asrouter/content-src/styles/_feature-callout.scss b/browser/components/asrouter/content-src/styles/_feature-callout.scss
index 78473bc802017..65454d13cbacc 100644
--- a/browser/components/asrouter/content-src/styles/_feature-callout.scss
+++ b/browser/components/asrouter/content-src/styles/_feature-callout.scss
@@ -310,6 +310,12 @@
}
}
+ .textarea-container {
+ .textarea-char-counter {
+ font-size: 0.813em;
+ }
+ }
+
.cta-link {
background: none;
text-decoration: underline;
diff --git a/browser/components/asrouter/content-src/templates/CFR/templates/InfoBar.schema.json b/browser/components/asrouter/content-src/templates/CFR/templates/InfoBar.schema.json
index 404697d695355..d4c793c0bcd1c 100644
--- a/browser/components/asrouter/content-src/templates/CFR/templates/InfoBar.schema.json
+++ b/browser/components/asrouter/content-src/templates/CFR/templates/InfoBar.schema.json
@@ -36,7 +36,7 @@
"properties": {
"where": {
"type": "string",
- "description": "Where to open—e.g. 'tab', 'window', etc."
+ "description": "Where to open - e.g. 'tab', 'window', etc."
}
},
"additionalProperties": false
@@ -188,7 +188,7 @@
"data": { "type": "object" },
"dismiss": {
"type": "boolean",
- "description": "If true, dismiss the infobar after handling this link’s action. Defaults to false."
+ "description": "If true, dismiss the infobar after handling this link's action. Defaults to false."
}
},
"required": ["type"]
diff --git a/browser/components/asrouter/content-src/templates/OnboardingMessage/Spotlight.schema.json b/browser/components/asrouter/content-src/templates/OnboardingMessage/Spotlight.schema.json
index edfc98d312956..2825487658740 100644
--- a/browser/components/asrouter/content-src/templates/OnboardingMessage/Spotlight.schema.json
+++ b/browser/components/asrouter/content-src/templates/OnboardingMessage/Spotlight.schema.json
@@ -48,6 +48,10 @@
"type": "boolean",
"description": "Don't allow the message to be dismissed using the ESC key - for limited use in Spotlight modals only when the message content clearly informs the user that a decision is required to proceed"
},
+ "write_in_microsurvey": {
+ "type": "boolean",
+ "description": "Set to true to apply the write-in microsurvey data policy. This is required for write-in microsurveys. Messages using the textarea tile should always set this to true. It prevents client_id from being recorded with any telemetry events for the message, recording a unique impression_id instead. It also sends the events on the more restrictive microsurvey ping instead of the messaging-system ping."
+ },
"disableHistoryUpdates": {
"type": "boolean",
"description": "Don't alter the browser session's history stack - used with messaging surfaces like Feature Callouts"
diff --git a/browser/components/asrouter/docs/feature-callout.md b/browser/components/asrouter/docs/feature-callout.md
index 2fc887d4289e9..147803f216351 100644
--- a/browser/components/asrouter/docs/feature-callout.md
+++ b/browser/components/asrouter/docs/feature-callout.md
@@ -1,6 +1,7 @@
# Feature Callout
## Table of Contents
+
- [Feature Callouts](#feature-callouts)
- [Content Elements](#content-elements)
- [Arrow Positioning](#arrow-positioning)
@@ -17,7 +18,6 @@
- [Triggers](#triggers)
- [Special Message Actions](#special-message-actions)
-
## Feature Callouts
Feature Callouts point to and describe features in content pages or the browser chrome. They can consist of a single message or of a sequence of messages. Callouts are different from [Spotlights](./spotlight.md) or other dialogs in that they do not block other interactions with the browser. Feature Callouts are currently only available for experimentation in the browser chrome. For example, callouts can easily be configured to point to toolbar buttons in the browser chrome.
@@ -43,12 +43,14 @@ The callout's arrow (the triangle-shaped caret pointing to the anchor) can be po
## Use Cases
Feature Callouts have been used in a variety of ways. Some common use cases are:
+
- Highlighting underused functionality
- Displaying short surveys
- Displaying informative toast messages
- Guiding users through new features
## Examples
+
A Feature Callout highlighting a feature

@@ -67,8 +69,8 @@ A Feature Callout displaying a user feedback survey
4. You should see an example JSON message labeled `TEST_FEATURE_TOUR`. Clicking `Show` next to it should show the callout
5. You can directly modify the message in the text area with your changes or by pasting your custom message JSON. Clicking `Modify` shows your updated message. Make sure it's valid JSON and be careful not to add unnecessary commas after the final member in an array or the final property of an object, as they will invalidate the message.
6. For these testing purposes, targeting and trigger are ignored, as the message will be triggered by pressing the "Modify" button. So you won't be able to test triggers and targeting by this method.
-6. Ensure that all required properties are covered according to the schema below
-7. Clicking `Share` copies a link to your clipboard that can be pasted in the urlbar to preview the message and can be shared to get feedback from your team
+7. Ensure that all required properties are covered according to the schema below
+8. Clicking `Share` copies a link to your clipboard that can be pasted in the urlbar to preview the message and can be shared to get feedback from your team
- **Note:** Only one Feature Callout can be shown at a time. You must dismiss existing callouts before new ones can be shown.
@@ -140,6 +142,19 @@ interface FeatureCallout {
template: "multistage";
backdrop: "transparent";
transitions: false;
+ // Set to true to apply the write-in microsurvey data policy. This is
+ // REQUIRED for all write-in microsurveys. Messages using the textarea tile
+ // should always set this to true. It prevents `client_id` from being
+ // recorded with any telemetry events for the message, recording a unique
+ // `impression_id` instead. It also sends the events on the `microsurvey`
+ // ping instead of the `messaging-system` ping, which is anonymized by
+ // OHTTP, has stricter access control and is retained for a shorter period.
+ // It still allows counting unique impressions and joining pings from the
+ // same message, but it can't be joined to any other telemetry data. So all
+ // events coming from a message with this set to true will be joinable by
+ // `impression_id`, but disconnected from other datasets. Optional; defaults
+ // to false if omitted.
+ write_in_microsurvey?: boolean;
disableHistoryUpdates: true;
// The name of a preference that will be used to store screen progress. Only
// relevant if your callout has multiple screens and serves as a tour. This
@@ -226,7 +241,7 @@ interface FeatureCallout {
// be focused when the callout is shown. Use sparingly, as it can make
// callouts much more disruptive for users.
autofocus?: AutoFocusOptions;
- }
+ },
];
content: {
position: "callout";
@@ -269,8 +284,10 @@ interface FeatureCallout {
// since there's no logic to enable the button. However, if your
// screen uses the "multiselect" tile (see tiles), you can use
// "hasActiveMultiSelect" to disable the button until the user
- // selects something.
- disabled?: boolean | "hasActiveMultiSelect";
+ // selects something. If your screen has a textarea tile, you can use
+ // "hasTextInput" to disable the button while the textarea is empty or
+ // exceeds the character limit.
+ disabled?: boolean | "hasActiveMultiSelect" | "hasTextInput";
// Primary buttons can have a "primary" or "secondary" style. This
// is useful because you can't change the order of the buttons, but
// you can swap the primary and secondary buttons' styles.
@@ -283,7 +300,7 @@ interface FeatureCallout {
// Extra text to show before the button.
text: Label;
has_arrow_icon?: boolean;
- disabled?: boolean | "hasActiveMultiSelect";
+ disabled?: boolean | "hasActiveMultiSelect" | "hasTextInput";
style?: "primary" | "secondary";
action: Action;
};
@@ -332,12 +349,11 @@ interface FeatureCallout {
// button it's attached to. Defaults to "secondary".
style?: "primary" | "secondary";
};
- // Predefined content modules. The only one currently supported in
- // feature callout is "multiselect", which allows you to show a series
- // of checkboxes and/or radio buttons.
+ // Predefined content modules. These are poorly documented but can be
+ // investigated in ContentTiles.jsx. The example here is a multiselect
+ // tile, which shows a list of checkboxes or radio buttons.
tiles?: {
type: "multiselect";
- // Depends on the type, but we only support "multiselect" currently.
data: MultiSelectItem[];
// Allows CSS overrides of the multiselect container.
style?: {
@@ -364,6 +380,28 @@ interface FeatureCallout {
"--some-variable"?: string;
};
};
+ tiles_container: {
+ // Position of the tiles container relative to supporting content
+ // like `above_button_content`. By default, it comes before supporting
+ // content. Setting to "after_supporting_content" places it after.
+ position?: null | "after_supporting_content";
+ style?: {
+ padding: string;
+ margin: string;
+ marginBlock: string;
+ marginInline: string;
+ paddingBlock: string;
+ paddingInline: string;
+ flexDirection: string;
+ flexWrap: string;
+ flexFlow: string;
+ flexGrow: string;
+ flexShrink: string;
+ justifyContent: string;
+ alignItems: string;
+ gap: string;
+ };
+ };
// The dots in the corner that show what screen you're on and how many
// screens there are in total. This property is only used to override
// the ARIA attributes or tooltip. Not recommended.
@@ -371,8 +409,8 @@ interface FeatureCallout {
string_id: string;
};
// An extra block of configurable content below the title/subtitle but
- // above the optional `tiles` section and the main buttons. Styles not
- // yet implemented; not recommended.
+ // above the main buttons. Can be placed above the `tiles` by setting
+ // `tiles_container.position` to "after_supporting_content".
above_button_content?: LinkParagraphOrImage[];
// An optional array of event listeners to add to the page where the
// feature callout is shown. This can be used to perform actions in
@@ -709,7 +747,7 @@ interface SubmenuItem {
}
}
]
- },
+ }
}
```
diff --git a/browser/components/asrouter/docs/telemetry.md b/browser/components/asrouter/docs/telemetry.md
index c28b370bb6b9e..4e69caa57f651 100644
--- a/browser/components/asrouter/docs/telemetry.md
+++ b/browser/components/asrouter/docs/telemetry.md
@@ -4,13 +4,11 @@ This document (combined with the [messaging system ping section of the Glean Dic
## Collection with Glean
-Code all over the messaging system passes JSON ping objects up to a few
-central spots. It may be [annotated with
-attribution](https://searchfox.org/mozilla-central/search?q=symbol:AboutWelcomeTelemetry%23_maybeAttachAttribution&redirect=false)
-along the way, and/or adjusted by some [policy
-routines](https://searchfox.org/mozilla-central/search?q=symbol:TelemetryFeed%23createASRouterEvent&redirect=false)
-before it's sent. The JSON will be transformed slightly further before being [sent to
-Glean][submit-glean-for-glean].
+Code all over the messaging system passes JSON ping objects up to a few central
+spots. It may be [annotated with attribution](https://searchfox.org/firefox-main/search?q=symbol%3AAboutWelcomeTelemetry%23_maybeAttachAttribution)
+along the way, and/or adjusted by some [policy routines](https://searchfox.org/firefox-main/search?q=symbol%3AASRouterTelemetry%23createASRouterEvent)
+before it's sent. The JSON will be transformed slightly further before being
+[sent to Glean][submit-glean-for-glean].
## Design of Messaging System Data Collections
@@ -33,20 +31,18 @@ file.
A general process overview can be found in the
[Activity Stream telemetry document](/browser/extensions/newtab/docs/v2-system-addon/telemetry.md).
-Note that when you need to add new metrics (i.e. JSON keys),
-they MUST to be
-[added](https://mozilla.github.io/glean/book/user/metrics/adding-new-metrics.html) to
-[browser/components/newtab/metrics.yaml][metrics-yaml]
-in order to show up correctly in the Glean data.
+Note that when you need to add new metrics (i.e. JSON keys), they MUST to be
+[added](https://mozilla.github.io/glean/book/user/metrics/adding-new-metrics.html)
+to [browser/components/newtab/metrics.yaml][metrics-yaml] in order to show up
+correctly in the Glean data.
Avoid adding any new nested objects, because Glean can't handle these. In the best case, any such additions will end up being flattened or stringified before being sent.
## Monitoring FxMS Telemetry Health
-The OMC team owns an [OpMon](https://github.com/mozilla/opmon) dashboard for the FxMS Desktop Glean telemetry with
-alerts. Note that it can only show one channel at any given time, here's a link
-to [Windows
-Release](https://mozilla.cloud.looker.com/dashboards/operational_monitoring::firefox_messaging_system?Percentile=50&Normalized+Channel=release&Normalized+Os=Windows).
+The OMC team owns an [OpMon](https://github.com/mozilla/opmon) dashboard for the
+FxMS Desktop Glean telemetry with alerts. Note that it can only show one channel
+at any given time, here's a link to [Windows Release](https://mozilla.cloud.looker.com/dashboards/operational_monitoring::firefox_messaging_system?Percentile=50&Normalized+Channel=release&Normalized+Os=Windows).
The dashboard is specified in
[firefox-messaging-system.toml](https://github.com/mozilla/metric-hub/blob/main/opmon/firefox-messaging-system.toml),
and reading the source can help clarify exactly what it means. We are the owner
@@ -54,10 +50,10 @@ of this file, and are encouraged to adjust it to our needs, though it's probably
a good idea to get review from someone in Data Science.
The current plan is to review the OpMon dashboard as a group in our weekly
-triage meeting, note anything that seems unusual to our [Google docs
-log](https://docs.google.com/document/d/1d16GCuul9sENMOMDAcD1kKNBtnJLouDxZtIgz2u-70U/edit),
-and, if we want to investigate further, file [a bug that blocks
-`fxms-glean`](https://bugzilla.mozilla.org/showdependencytree.cgi?id=1843409&hide_resolved=1).
+triage meeting, note anything that seems unusual to our
+[Google docs log](https://docs.google.com/document/d/1d16GCuul9sENMOMDAcD1kKNBtnJLouDxZtIgz2u-70U/edit),
+and, if we want to investigate further, file
+[a bug that blocks `fxms-glean`](https://bugzilla.mozilla.org/showdependencytree.cgi?id=1843409&hide_resolved=1).
The dashboard is configured to alert in various cases, and those alerts can be
seen at the bottom of the dashboard. As of this writing, the alerts have some
@@ -91,12 +87,11 @@ To monitor events using `about:glean` you can use the following mach command. Th
Dictionary, so write long rich-text Descriptions and augment them off-train
with Glean Annotations.
* Schemas for ingestion are automatically generated. You can go from landing a
- new ping to querying the data being sent [within two
- days](https://blog.mozilla.org/data/2021/12/14/this-week-in-glean-how-long-must-i-wait-before-i-can-see-my-data/).
+ new ping to querying the data being sent [within two days](https://blog.mozilla.org/data/2021/12/14/this-week-in-glean-how-long-must-i-wait-before-i-can-see-my-data/).
* Make a mistake? No worries. Changes are quick and easy and are reflected in
the received data within a day.
* If you have any questions, the Glean Team is available across a lot of
- timezones on the [`#glean:mozilla.org` channel](https://chat.mozilla.org/#/room/#glean:mozilla.org) on Matrix and Slack `#data-help`.
+ timezones on the [`#glean:mozilla.org` channel](https://chat.mozilla.org/#/room/#glean:mozilla.org) on Matrix and [Slack `#data-help`](https://mozilla.enterprise.slack.com/archives/C4D5ZA91B).
- [submit-glean-for-glean]: https://searchfox.org/mozilla-central/search?q=.submitGleanPingForPing&path=*.sys.mjs&case=false®exp=false
- [metrics-yaml]: https://searchfox.org/mozilla-central/source/browser/components/newtab/metrics.yaml
+ [submit-glean-for-glean]: https://searchfox.org/firefox-main/search?q=symbol%3AAboutWelcomeTelemetry%23submitGleanPingForPing
+ [metrics-yaml]: https://searchfox.org/firefox-main/source/browser/components/asrouter/metrics.yaml
diff --git a/browser/components/asrouter/metrics.yaml b/browser/components/asrouter/metrics.yaml
index c8e07a1198053..0613be25f7798 100644
--- a/browser/components/asrouter/metrics.yaml
+++ b/browser/components/asrouter/metrics.yaml
@@ -8,10 +8,15 @@
---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
- - 'Firefox :: Messaging System'
+ - "Firefox :: Messaging System"
+# Keys added to messaging_system should also be added to microsurvey below,
+# unless they carry sensitive/identifying information. The microsurvey ping
+# basically mirrors the messaging_system ping, except that it includes
+# event_input_value and excludes client_id and browser_session_id, to protect
+# the user's privacy. When in doubt, ask in the #omc Slack channel.
messaging_system:
- event_context_parse_error:
+ event_context_parse_error: &event_context_parse_error
type: counter
lifetime: ping
description: |
@@ -29,7 +34,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_reason:
+ event_reason: &event_reason
type: string
lifetime: ping
description: |
@@ -48,7 +53,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_page:
+ event_page: &event_page
type: string
lifetime: ping
description: |
@@ -66,7 +71,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_source:
+ event_source: &event_source
type: string
lifetime: ping
description: |
@@ -84,7 +89,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_context:
+ event_context: &event_context
type: text
lifetime: ping
description: |
@@ -102,7 +107,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_screen_family:
+ event_screen_family: &event_screen_family
type: text
lifetime: ping
description: |
@@ -122,7 +127,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_screen_id:
+ event_screen_id: &event_screen_id
type: text
lifetime: ping
description: |
@@ -142,7 +147,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_screen_initials:
+ event_screen_initials: &event_screen_initials
type: text
lifetime: ping
description: |
@@ -162,7 +167,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event_screen_index:
+ event_screen_index: &event_screen_index
type: quantity
unit: integer
lifetime: ping
@@ -183,7 +188,7 @@ messaging_system:
send_in_pings:
- messaging-system
- message_id:
+ message_id: &message_id
type: text
lifetime: ping
description: |
@@ -201,7 +206,7 @@ messaging_system:
send_in_pings:
- messaging-system
- event:
+ event: &event
type: string
description: >
The type of event. Any user defined string
@@ -219,7 +224,7 @@ messaging_system:
send_in_pings:
- messaging-system
- ping_type:
+ ping_type: &ping_type
type: string
description: >
Type of event the ping is capturing.
@@ -237,7 +242,7 @@ messaging_system:
send_in_pings:
- messaging-system
- source:
+ source: &source
type: string
description: >
The source of the interaction described by the other metrics.
@@ -255,6 +260,25 @@ messaging_system:
send_in_pings:
- messaging-system
+ locale: &locale
+ type: string
+ lifetime: ping
+ description: >
+ The locale as supplied to the messaging system by
+ `Services.locale.appLocaleAsBCP47`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
+ expires: never
+ send_in_pings:
+ - messaging-system
+
client_id:
type: uuid
lifetime: ping
@@ -282,25 +306,6 @@ messaging_system:
send_in_pings:
- messaging-system
- locale:
- type: string
- lifetime: ping
- description: >
- The locale as supplied to the messaging system by
- `Services.locale.appLocaleAsBCP47`.
- bugs:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863
- data_sensitivity:
- - technical
- notification_emails:
- - pmcmanis@mozilla.com
- - dmosedale@mozilla.com
- expires: never
- send_in_pings:
- - messaging-system
-
browser_session_id:
type: uuid
lifetime: ping
@@ -325,7 +330,7 @@ messaging_system:
send_in_pings:
- messaging-system
- impression_id:
+ impression_id: &impression_id
type: uuid
lifetime: ping
description: >
@@ -343,14 +348,13 @@ messaging_system:
send_in_pings:
- messaging-system
- bucket_id:
+ bucket_id: &bucket_id
type: string
lifetime: ping
description: >
A name shared between multiple messages that may individually be too
- targetted.
- e.g. a message that gets shown on specific websites or a message asking
- about personal information.
+ targeted. e.g. a message that gets shown on specific websites or a message
+ asking about personal information.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1825863
data_reviews:
@@ -364,7 +368,7 @@ messaging_system:
send_in_pings:
- messaging-system
- addon_version:
+ addon_version: &addon_version
type: string
lifetime: ping
description: >
@@ -383,7 +387,7 @@ messaging_system:
send_in_pings:
- messaging-system
- unknown_key_count:
+ unknown_key_count: &unknown_key_count
type: counter
description: |
The sum of all unknown keys counted.
@@ -401,7 +405,7 @@ messaging_system:
send_in_pings:
- messaging-system
- unknown_keys:
+ unknown_keys: &unknown_keys
type: labeled_counter
description: |
Ping keys supplied to the messaging system for which
@@ -422,7 +426,7 @@ messaging_system:
send_in_pings:
- messaging-system
- glean_ping_for_ping_failures:
+ glean_ping_for_ping_failures: &glean_ping_for_ping_failures
type: counter
description: |
How often something went awry within
@@ -441,7 +445,7 @@ messaging_system:
send_in_pings:
- metrics
- invalid_nested_data:
+ invalid_nested_data: &invalid_nested_data
type: labeled_counter
description: |
We received a ping with non-scalar data on a field of this name.
@@ -484,9 +488,8 @@ messaging_system:
expires: never
telemetry_mirror: MS_MESSAGE_REQUEST_TIME_MS
-
messaging_system.attribution:
- source:
+ source: &attribution_source
type: string
lifetime: ping
description: |
@@ -505,7 +508,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- medium:
+ medium: &attribution_medium
type: string
lifetime: ping
description: |
@@ -524,7 +527,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- campaign:
+ campaign: &attribution_campaign
type: string
lifetime: ping
description: |
@@ -543,7 +546,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- content:
+ content: &attribution_content
type: string
lifetime: ping
description: |
@@ -562,7 +565,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- experiment:
+ experiment: &attribution_experiment
type: string
lifetime: ping
description: |
@@ -580,7 +583,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- variation:
+ variation: &attribution_variation
type: string
lifetime: ping
description: |
@@ -598,7 +601,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- ua:
+ ua: &attribution_ua
type: string
lifetime: ping
description: |
@@ -616,7 +619,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- dltoken:
+ dltoken: &attribution_dltoken
type: string
lifetime: ping
description: |
@@ -636,7 +639,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- msstoresignedin:
+ msstoresignedin: &attribution_msstoresignedin
type: string
lifetime: ping
description: |
@@ -658,13 +661,13 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- msclkid:
+ msclkid: &attribution_msclkid
type: string
lifetime: ping
description: |
A string containing the attribution for a Microsoft Store Ads Campaign ID.
This differs from a Campaign ID originating from a Microsoft Store URL
- containing attribution.
+ containing attribution_
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=2004494
data_reviews:
@@ -678,7 +681,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- dlsource:
+ dlsource: &attribution_dlsource
type: string
lifetime: ping
description: |
@@ -700,7 +703,7 @@ messaging_system.attribution:
send_in_pings:
- messaging-system
- unknown_keys:
+ unknown_keys: &attribution_unknown_keys
type: labeled_counter
description: |
Attribution keys supplied to the messaging system for which
@@ -721,3 +724,313 @@ messaging_system.attribution:
expires: never
send_in_pings:
- messaging-system
+
+# Most keys in microsurvey are alias nodes corresponding to those in
+# messaging_system, to keep the two pings in sync. The main exceptions are
+# event_input_value, which is unique to microsurvey, and client_id and
+# browser_session_id, which are omitted from microsurvey for privacy reasons.
+# Some other keys are excluded from microsurvey as they are not relevant there,
+# e.g. message_request_time. These are used for high-level monitoring of the
+# messaging system as a whole, rather than for analyzing individual messages.
+microsurvey:
+ event_input_value:
+ type: text
+ lifetime: ping
+ description: |
+ Text input by the user in a write-in textarea produced by the textarea
+ tile type. Truncated to 8KB (though the textarea tile normally has a
+ separate character limit enforced at display time).
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010984
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010984
+ data_sensitivity:
+ - web_activity
+ - highly_sensitive
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
+ expires: never
+ send_in_pings:
+ - microsurvey
+
+ event_context_parse_error:
+ <<: *event_context_parse_error
+ send_in_pings:
+ - microsurvey
+
+ event_reason:
+ <<: *event_reason
+ send_in_pings:
+ - microsurvey
+
+ event_page:
+ <<: *event_page
+ send_in_pings:
+ - microsurvey
+
+ event_source:
+ <<: *event_source
+ send_in_pings:
+ - microsurvey
+
+ event_context:
+ <<: *event_context
+ send_in_pings:
+ - microsurvey
+
+ event_screen_family:
+ <<: *event_screen_family
+ send_in_pings:
+ - microsurvey
+
+ event_screen_id:
+ <<: *event_screen_id
+ send_in_pings:
+ - microsurvey
+
+ event_screen_initials:
+ <<: *event_screen_initials
+ send_in_pings:
+ - microsurvey
+
+ event_screen_index:
+ <<: *event_screen_index
+ send_in_pings:
+ - microsurvey
+
+ message_id:
+ <<: *message_id
+ send_in_pings:
+ - microsurvey
+
+ event:
+ <<: *event
+ send_in_pings:
+ - microsurvey
+
+ ping_type:
+ <<: *ping_type
+ send_in_pings:
+ - microsurvey
+
+ source:
+ <<: *source
+ send_in_pings:
+ - microsurvey
+
+ locale:
+ <<: *locale
+ send_in_pings:
+ - microsurvey
+
+ impression_id:
+ <<: *impression_id
+ send_in_pings:
+ - microsurvey
+
+ bucket_id:
+ <<: *bucket_id
+ send_in_pings:
+ - microsurvey
+
+ addon_version:
+ <<: *addon_version
+ send_in_pings:
+ - microsurvey
+
+ unknown_key_count:
+ <<: *unknown_key_count
+ send_in_pings:
+ - microsurvey
+
+ unknown_keys:
+ <<: *unknown_keys
+ send_in_pings:
+ - microsurvey
+
+ glean_ping_for_ping_failures:
+ <<: *glean_ping_for_ping_failures
+ send_in_pings:
+ - microsurvey
+
+ invalid_nested_data:
+ <<: *invalid_nested_data
+ send_in_pings:
+ - microsurvey
+
+ # These metrics are copies of certain metadata fields that get removed because
+ # the microsurvey ping has `include_info_sections: false` (see pings.yaml).
+ os:
+ type: string
+ lifetime: ping
+ description: |
+ The name of the operating system.
+ Possible values:
+ Android, iOS, Linux, Darwin, Windows,
+ FreeBSD, NetBSD, OpenBSD, Solaris, Unknown
+ bugs:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/1497894
+ - https://bugzilla.mozilla.org/1944090
+ data_reviews:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
+ expires: never
+ send_in_pings:
+ - microsurvey
+
+ os_version:
+ type: string
+ lifetime: ping
+ description: |
+ The user-visible version of the operating system (e.g. "1.2.3").
+ If the version detection fails, this metric gets set to `Unknown`.
+ bugs:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/1497894
+ - https://bugzilla.mozilla.org/1944090
+ data_reviews:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
+ expires: never
+ send_in_pings:
+ - microsurvey
+
+ windows_build_number:
+ type: quantity
+ unit: build_number
+ lifetime: ping
+ description: |
+ The optional Windows build number, reported by Windows
+ (e.g. 22000) and not set for other platforms.
+ bugs:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/1802094
+ - https://bugzilla.mozilla.org/1944090
+ data_reviews:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/1802094
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
+ expires: never
+ send_in_pings:
+ - microsurvey
+
+ app_display_version:
+ type: string
+ lifetime: ping
+ description: |
+ The user visible version string (e.g. "1.0.3").
+ If the value was not provided through configuration,
+ this metric gets set to `Unknown`.
+ bugs:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/1508305
+ - https://bugzilla.mozilla.org/1944090
+ data_reviews:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1508305#c9
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
+ expires: never
+ send_in_pings:
+ - microsurvey
+
+ app_channel:
+ type: string
+ lifetime: ping
+ description: |
+ The channel the application is being distributed on.
+ bugs:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/1520741
+ - https://bugzilla.mozilla.org/1944090
+ data_reviews:
+ - https://bugzilla.mozilla.org/2010984
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1520741#c18
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
+ expires: never
+ send_in_pings:
+ - microsurvey
+
+microsurvey.attribution:
+ source:
+ <<: *attribution_source
+ send_in_pings:
+ - microsurvey
+
+ medium:
+ <<: *attribution_medium
+ send_in_pings:
+ - microsurvey
+
+ campaign:
+ <<: *attribution_campaign
+ send_in_pings:
+ - microsurvey
+
+ content:
+ <<: *attribution_content
+ send_in_pings:
+ - microsurvey
+
+ experiment:
+ <<: *attribution_experiment
+ send_in_pings:
+ - microsurvey
+
+ variation:
+ <<: *attribution_variation
+ send_in_pings:
+ - microsurvey
+
+ ua:
+ <<: *attribution_ua
+ send_in_pings:
+ - microsurvey
+
+ dltoken:
+ <<: *attribution_dltoken
+ send_in_pings:
+ - microsurvey
+
+ msstoresignedin:
+ <<: *attribution_msstoresignedin
+ send_in_pings:
+ - microsurvey
+
+ msclkid:
+ <<: *attribution_msclkid
+ send_in_pings:
+ - microsurvey
+
+ dlsource:
+ <<: *attribution_dlsource
+ send_in_pings:
+ - microsurvey
+
+ unknown_keys:
+ <<: *attribution_unknown_keys
+ send_in_pings:
+ - microsurvey
diff --git a/browser/components/asrouter/modules/ASRouterTelemetry.sys.mjs b/browser/components/asrouter/modules/ASRouterTelemetry.sys.mjs
index 7dd2f166e3129..04b0c8b50f991 100644
--- a/browser/components/asrouter/modules/ASRouterTelemetry.sys.mjs
+++ b/browser/components/asrouter/modules/ASRouterTelemetry.sys.mjs
@@ -93,9 +93,6 @@ export class ASRouterTelemetry {
locale: Services.locale.appLocaleAsBCP47,
};
- if (event.event_context && typeof event.event_context === "object") {
- event.event_context = JSON.stringify(event.event_context);
- }
switch (event.action) {
case "cfr_user_event":
event = await this.applyCFRPolicy(event);
@@ -238,7 +235,7 @@ export class ASRouterTelemetry {
// Now that the action has become a ping, we can echo it to Glean.
if (this.telemetryEnabled) {
- lazy.Telemetry.submitGleanPingForPing({ ...ping, pingType });
+ lazy.Telemetry.parseAndSubmitPing({ ...ping, pingType });
}
}
diff --git a/browser/components/asrouter/modules/CFRPageActions.sys.mjs b/browser/components/asrouter/modules/CFRPageActions.sys.mjs
index 63708f529c7cb..a9ecbb8a5c0e8 100644
--- a/browser/components/asrouter/modules/CFRPageActions.sys.mjs
+++ b/browser/components/asrouter/modules/CFRPageActions.sys.mjs
@@ -527,7 +527,6 @@ export class PageAction {
);
}
- // eslint-disable-next-line max-statements
async _renderPopup(message, browser) {
this.maybeLoadCustomElement(this.window);
diff --git a/browser/components/asrouter/modules/PanelTestProvider.sys.mjs b/browser/components/asrouter/modules/PanelTestProvider.sys.mjs
index 97f881dc8edd7..cd034a670fd50 100644
--- a/browser/components/asrouter/modules/PanelTestProvider.sys.mjs
+++ b/browser/components/asrouter/modules/PanelTestProvider.sys.mjs
@@ -19,6 +19,104 @@ const isMSIX =
Services.sysinfo.getProperty("hasWinPackageId", false);
const MESSAGES = () => [
+ {
+ id: "WRITE_IN_MICROSURVEY_TEST",
+ template: "feature_callout",
+ groups: [],
+ content: {
+ id: "WRITE_IN_MICROSURVEY_TEST",
+ template: "multistage",
+ backdrop: "transparent",
+ transitions: false,
+ write_in_microsurvey: true,
+ screens: [
+ {
+ id: "WRITE_IN_MICROSURVEY_TEST_SCREEN_1",
+ force_hide_steps_indicator: true,
+ anchors: [
+ {
+ selector: "hbox#browser",
+ no_open_on_anchor: true,
+ hide_arrow: true,
+ panel_position: {
+ anchor_attachment: "bottomright",
+ callout_attachment: "bottomright",
+ offset_y: -24,
+ offset_x: -20,
+ },
+ },
+ ],
+ content: {
+ position: "callout",
+ width: "312px",
+ padding: 24,
+ title_logo: {
+ imageURL: "chrome://branding/content/about-logo.png",
+ alignment: "top",
+ },
+ title: {
+ raw: "Help Firefox improve this feature",
+ },
+ subtitle: {
+ raw: "Is there anything you'd like to add? (optional)",
+ fontSize: "0.9375em",
+ },
+ tiles: {
+ type: "textarea",
+ data: {
+ id: "feature-feedback",
+ character_limit: 1000,
+ rows: 4,
+ },
+ },
+ above_button_content: [
+ {
+ type: "text",
+ text: {
+ raw: "Note: Do not include personal information.",
+ color: "var(--text-color-deemphasized)",
+ fontSize: "0.6875em",
+ textAlign: "start",
+ marginBlock: "-4px",
+ },
+ },
+ ],
+ secondary_button: {
+ label: { raw: "Submit" },
+ style: "primary",
+ action: {
+ type: "MULTI_ACTION",
+ collectTextInput: true,
+ data: { actions: [] },
+ },
+ disabled: "hasTextInput",
+ },
+ additional_button: {
+ label: { raw: "Privacy notice" },
+ style: "link",
+ alignment: "space-between",
+ action: {
+ data: {
+ args: "https://www.mozilla.org/privacy/firefox/",
+ where: "tabshifted",
+ },
+ type: "OPEN_URL",
+ },
+ },
+ dismiss_button: {
+ action: {
+ dismiss: true,
+ },
+ background: true,
+ size: "small",
+ marginInline: "0 20px",
+ marginBlock: "20px 0",
+ },
+ },
+ },
+ ],
+ },
+ },
{
id: "TEST_BACKUP_SPOTLIGHT",
groups: [],
diff --git a/browser/components/asrouter/modules/Spotlight.sys.mjs b/browser/components/asrouter/modules/Spotlight.sys.mjs
index bdb0dee705486..c568ff239e451 100644
--- a/browser/components/asrouter/modules/Spotlight.sys.mjs
+++ b/browser/components/asrouter/modules/Spotlight.sys.mjs
@@ -20,6 +20,7 @@ export const Spotlight = {
const ping = {
message_id: message.content.id,
event,
+ event_context: { writeInMicrosurvey: message.content.writeInMicrosurvey },
};
dispatch({
type: "SPOTLIGHT_TELEMETRY",
diff --git a/browser/components/asrouter/pings.yaml b/browser/components/asrouter/pings.yaml
index 4dc9a79474247..15c86c26c5676 100644
--- a/browser/components/asrouter/pings.yaml
+++ b/browser/components/asrouter/pings.yaml
@@ -18,3 +18,24 @@ messaging-system:
notification_emails:
- pmcmanis@mozilla.com
- dmosedale@mozilla.com
+
+microsurvey:
+ description: |
+ This is a ping representing single events emitted by microsurvey messages,
+ which are triggered by the messaging system. It is largely identical to
+ messaging-system, except that it includes write-in responses from
+ microsurveys, and it applies the write-in microsurvey data policy: reduced
+ retention, stricter access control, OHTTP, and no client_id.
+ include_client_id: false
+ send_if_empty: false
+ metadata:
+ include_info_sections: false
+ uploader_capabilities:
+ - ohttp
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010984
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010984
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - dmosedale@mozilla.com
diff --git a/browser/components/asrouter/tests/browser/head.js b/browser/components/asrouter/tests/browser/head.js
index 78be23f3a5bcd..c78a049995893 100644
--- a/browser/components/asrouter/tests/browser/head.js
+++ b/browser/components/asrouter/tests/browser/head.js
@@ -96,7 +96,10 @@ class TelemetrySpy {
* @param {object} expectedData
*/
assertCalledWith(expectedData) {
- let match = this.spy.calledWith("AWPage:TELEMETRY_EVENT", expectedData);
+ let match = this.spy.calledWith(
+ "AWPage:TELEMETRY_EVENT",
+ sinon.match(expectedData)
+ );
if (match) {
ok(true, "Expected telemetry sent");
} else if (this.spy.called) {
diff --git a/browser/components/asrouter/tests/xpcshell/test_ASRouterTelemetry.js b/browser/components/asrouter/tests/xpcshell/test_ASRouterTelemetry.js
index 28f3e574663d3..9da52521f5220 100644
--- a/browser/components/asrouter/tests/xpcshell/test_ASRouterTelemetry.js
+++ b/browser/components/asrouter/tests/xpcshell/test_ASRouterTelemetry.js
@@ -441,7 +441,7 @@ add_task(async function test_createASRouterEvent_call_correctPolicy() {
);
let sandbox = sinon.createSandbox();
let instance = new ASRouterTelemetry();
- sandbox.stub(instance, expectedPolicyFnName);
+ sandbox.spy(instance, expectedPolicyFnName);
let action = { type: msg.AS_ROUTER_TELEMETRY_USER_EVENT, data };
await instance.createASRouterEvent(action);
@@ -453,80 +453,42 @@ add_task(async function test_createASRouterEvent_call_correctPolicy() {
sandbox.restore();
};
- testCallCorrectPolicy("applyCFRPolicy", {
+ await testCallCorrectPolicy("applyCFRPolicy", {
action: "cfr_user_event",
event: "IMPRESSION",
message_id: "cfr_message_01",
});
- testCallCorrectPolicy("applyToolbarBadgePolicy", {
+ await testCallCorrectPolicy("applyToolbarBadgePolicy", {
action: "badge_user_event",
event: "IMPRESSION",
message_id: "badge_message_01",
});
- testCallCorrectPolicy("applyMomentsPolicy", {
+ await testCallCorrectPolicy("applyMomentsPolicy", {
action: "moments_user_event",
event: "CLICK_BUTTON",
message_id: "moments_message_01",
});
- testCallCorrectPolicy("applySpotlightPolicy", {
+ await testCallCorrectPolicy("applySpotlightPolicy", {
action: "spotlight_user_event",
event: "CLICK",
message_id: "SPOTLIGHT_MESSAGE_93",
});
- testCallCorrectPolicy("applyToastNotificationPolicy", {
+ await testCallCorrectPolicy("applyToastNotificationPolicy", {
action: "toast_notification_user_event",
event: "IMPRESSION",
message_id: "TEST_TOAST_NOTIFICATION1",
});
- testCallCorrectPolicy("applyUndesiredEventPolicy", {
+ await testCallCorrectPolicy("applyUndesiredEventPolicy", {
action: "asrouter_undesired_event",
event: "UNDESIRED_EVENT",
});
});
-add_task(async function test_createASRouterEvent_stringify_event_context() {
- info(
- "ASRouterTelemetry.createASRouterEvent should stringify event_context if " +
- "it is an Object"
- );
- let instance = new ASRouterTelemetry();
- let action = {
- type: msg.AS_ROUTER_TELEMETRY_USER_EVENT,
- data: {
- action: "asrouter_undesired_event",
- event: "UNDESIRED_EVENT",
- event_context: { foo: "bar" },
- },
- };
- let { ping } = await instance.createASRouterEvent(action);
-
- Assert.equal(ping.event_context, JSON.stringify({ foo: "bar" }));
-});
-
-add_task(async function test_createASRouterEvent_not_stringify_event_context() {
- info(
- "ASRouterTelemetry.createASRouterEvent should not stringify event_context " +
- "if it is a String"
- );
- let instance = new ASRouterTelemetry();
- let action = {
- type: msg.AS_ROUTER_TELEMETRY_USER_EVENT,
- data: {
- action: "asrouter_undesired_event",
- event: "UNDESIRED_EVENT",
- event_context: "foo",
- },
- };
- let { ping } = await instance.createASRouterEvent(action);
-
- Assert.equal(ping.event_context, "foo");
-});
-
add_task(async function test_onAction_calls_handleASRouterUserEvent() {
let actions = [
msg.AS_ROUTER_TELEMETRY_USER_EVENT,
diff --git a/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
index c9b12b4207c2b..97a694fb607a4 100644
--- a/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
+++ b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
@@ -23,7 +23,7 @@ add_task(async function test_PanelTestProvider() {
milestone_message: 0,
update_action: 1,
spotlight: 8,
- feature_callout: 11,
+ feature_callout: 12,
pb_newtab: 2,
toast_notification: 3,
bookmarks_bar_button: 1,
diff --git a/js/public/Promise.h b/js/public/Promise.h
index bb4317b2192e0..5b7ddffb19180 100644
--- a/js/public/Promise.h
+++ b/js/public/Promise.h
@@ -66,25 +66,6 @@ class JS_PUBLIC_API JobQueue {
virtual bool getHostDefinedGlobal(
JSContext* cx, JS::MutableHandle data) const = 0;
- /**
- * Enqueue a reaction job `job` for `promise`, which was allocated at
- * `allocationSite`. Provide `hostDefineData` as the host defined data for
- * the reaction job's execution.
- *
- * The `hostDefinedData` value comes from `getHostDefinedData` method.
- * The object is unwrapped, and it can belong to a different compartment
- * than the current compartment. It can be `nullptr` if `getHostDefinedData`
- * returns `nullptr`.
- *
- * `promise` can be null if the promise is optimized out.
- * `promise` is guaranteed not to be optimized out if the promise has
- * non-default user-interaction flag.
- */
- virtual bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
- JS::HandleObject job,
- JS::HandleObject allocationSite,
- JS::HandleObject hostDefinedData) = 0;
-
/**
* Run all jobs in the queue. Running one job may enqueue others; continue to
* run jobs until the queue is empty.
@@ -102,11 +83,6 @@ class JS_PUBLIC_API JobQueue {
*/
virtual void runJobs(JSContext* cx) = 0;
- /**
- * Return true if the job queue is empty, false otherwise.
- */
- virtual bool empty() const = 0;
-
/**
* Returns true if the job queue stops draining, which results in `empty()`
* being false after `runJobs()`.
diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp
index abfeee96888c6..29ed0721a3d9c 100644
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -123,37 +123,6 @@ enum PromiseCombinatorElementFunctionSlots {
PromiseCombinatorElementFunctionSlot_Data
};
-enum ReactionJobSlots {
- ReactionJobSlot_ReactionRecord = 0,
-};
-
-// Extended function slots used to pass arguments through to either
-// PromiseResolveThenableJob, or PromiseResolveBuiltinThenableJob when calling
-// the built-in `then`.
-enum ThenableJobSlots {
- // The Promise to resolve using the given thenable.
- //
- // This can be a CCW when used for PromiseResolveThenableJob, otherwise it is
- // guaranteed not to be.
- ThenableJobSlot_Promise = 0,
-
- // The thenable to use as the receiver when calling the `then` function.
- //
- // This can be a CCW when used for PromiseResolveThenableJob, otherwise it is
- // guaranteed not to be.
- ThenableJobSlot_Thenable,
-
- // The handler to use as the Promise reaction, when not calling the built-in
- // `then`. It is a callable object that's guaranteed to be from the same
- // compartment as the PromiseReactionJob.
- ThenableJobSlot_Handler,
-
- ThenableJobSlot_Count
-};
-
-static_assert(size_t(ThenableJobSlot_Count) <=
- size_t(FunctionExtended::SlotCount));
-
struct PromiseCapability {
JSObject* promise = nullptr;
JSObject* resolve = nullptr;
@@ -1703,8 +1672,6 @@ static bool EnqueueJob(JSContext* cx, JS::JSMicroTask* job) {
ObjectValue(*rootedJob));
}
-static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
-
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
@@ -1869,94 +1836,66 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
// Step 1 (reordered). Let job be a new Job Abstract Closure with no
// parameters that captures reaction and argument
// and performs the following steps when called:
- if (JS::Prefs::use_js_microtask_queue()) {
- MOZ_ASSERT(reactionVal.isObject());
+ MOZ_ASSERT(reactionVal.isObject());
- // Get a representative object for this global: We will use this later
- // to extract the target global for execution. We don't store the global
- // directly because CCWs to globals can change identity.
- //
- // So instead we simply store Object.prototype from the target global,
- // an object which always exists.
- RootedField globalRepresentative(
- roots, &cx->global()->getObjectPrototype());
-
- // PromiseReactionJob job will use the existence of a CCW as a signal
- // to change to the reactionVal's realm for execution. I believe
- // this is the right thing to do. As a result however we don't actually
- // need to track the global. We simply allow PromiseReactionJob to
- // do the right thing. We will need to enqueue a CCW however
- {
- AutoRealm ar(cx, reaction);
+ // Get a representative object for this global: We will use this later
+ // to extract the target global for execution. We don't store the global
+ // directly because CCWs to globals can change identity.
+ //
+ // So instead we simply store Object.prototype from the target global,
+ // an object which always exists.
+ RootedField globalRepresentative(
+ roots, &cx->global()->getObjectPrototype());
+
+ // PromiseReactionJob job will use the existence of a CCW as a signal
+ // to change to the reactionVal's realm for execution. I believe
+ // this is the right thing to do. As a result however we don't actually
+ // need to track the global. We simply allow PromiseReactionJob to
+ // do the right thing. We will need to enqueue a CCW however
+ {
+ AutoRealm ar(cx, reaction);
- RootedField stack(
- roots,
- JS::MaybeGetPromiseAllocationSiteFromPossiblyWrappedPromise(promise));
- if (!cx->compartment()->wrap(cx, &stack)) {
+ RootedField stack(
+ roots,
+ JS::MaybeGetPromiseAllocationSiteFromPossiblyWrappedPromise(promise));
+ if (!cx->compartment()->wrap(cx, &stack)) {
+ return false;
+ }
+ reaction->setAllocationStack(stack);
+
+ if (!reaction->getHostDefinedData().isObject()) {
+ // We do need to still provide an incumbentGlobal here
+ // MG:XXX: I'm pretty sure this can be appreciably more elegant later.
+ RootedField hostGlobal(roots);
+ if (!cx->jobQueue->getHostDefinedGlobal(cx, &hostGlobal)) {
return false;
}
- reaction->setAllocationStack(stack);
-
- if (!reaction->getHostDefinedData().isObject()) {
- // We do need to still provide an incumbentGlobal here
- // MG:XXX: I'm pretty sure this can be appreciably more elegant later.
- RootedField hostGlobal(roots);
- if (!cx->jobQueue->getHostDefinedGlobal(cx, &hostGlobal)) {
- return false;
- }
-
- if (hostGlobal) {
- MOZ_ASSERT(hostGlobal->is());
- // Recycle the root -- we store the prototype for the same
- // reason as EnqueueGlobalRepresentative.
- hostGlobal = &hostGlobal->as().getObjectPrototype();
- }
- if (!cx->compartment()->wrap(cx, &hostGlobal)) {
- return false;
- }
- reaction->setHostDefinedGlobalRepresentative(hostGlobal);
+ if (hostGlobal) {
+ MOZ_ASSERT(hostGlobal->is());
+ // Recycle the root -- we store the prototype for the same
+ // reason as EnqueueGlobalRepresentative.
+ hostGlobal = &hostGlobal->as().getObjectPrototype();
}
- if (!cx->compartment()->wrap(cx, &globalRepresentative)) {
+ if (!cx->compartment()->wrap(cx, &hostGlobal)) {
return false;
}
- reaction->setEnqueueGlobalRepresentative(globalRepresentative);
+ reaction->setHostDefinedGlobalRepresentative(hostGlobal);
}
- if (!cx->compartment()->wrap(cx, &reactionVal)) {
+ if (!cx->compartment()->wrap(cx, &globalRepresentative)) {
return false;
}
-
- // HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
- return EnqueueJob(cx, &reactionVal.toObject());
- }
-
- RootedField hostDefinedData(roots);
- if (JSObject* hostDefined = reaction->getAndClearHostDefinedData()) {
- hostDefined = CheckedUnwrapStatic(hostDefined);
- MOZ_ASSERT(hostDefined);
- // If the hostDefined object becomes a dead wrapper here, the target
- // global has already gone, and the job queue won't run the promise job
- // anyway.
- if (JS_IsDeadWrapper(hostDefined)) {
- return true;
- }
- hostDefinedData = hostDefined;
+ reaction->setEnqueueGlobalRepresentative(globalRepresentative);
}
- Handle funName = cx->names().empty_;
- RootedField job(
- roots,
- NewNativeFunction(cx, PromiseReactionJob, 0, funName,
- gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
- if (!job) {
+ if (!cx->compartment()->wrap(cx, &reactionVal)) {
return false;
}
- job->setExtendedSlot(ReactionJobSlot_ReactionRecord, reactionVal);
-
- return cx->runtime()->enqueuePromiseJob(cx, job, promise, hostDefinedData);
+ // HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
+ return EnqueueJob(cx, &reactionVal.toObject());
}
[[nodiscard]] static bool TriggerPromiseReactions(JSContext* cx,
@@ -2696,19 +2635,6 @@ static bool PromiseReactionJob(JSContext* cx, HandleObject reactionObjIn) {
reaction->unhandledRejectionBehavior());
}
-static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) {
- CallArgs args = CallArgsFromVp(argc, vp);
-
- RootedFunction job(cx, &args.callee().as());
-
- // Promise reactions don't return any value.
- args.rval().setUndefined();
-
- RootedObject reactionObj(
- cx, &job->getExtendedSlot(ReactionJobSlot_ReactionRecord).toObject());
- return PromiseReactionJob(cx, reactionObj);
-}
-
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
@@ -2762,25 +2688,6 @@ static bool PromiseResolveThenableJob(JSContext* cx, HandleObject promise,
return Call(cx, rejectVal, UndefinedHandleValue, rval, &rval);
}
-/*
- * Usage of the function's extended slots is described in the ThenableJobSlots
- * enum.
- */
-static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) {
- CallArgs args = CallArgsFromVp(argc, vp);
-
- RootedTuple roots(cx);
- RootedField job(roots, &args.callee().as());
- RootedField promise(
- roots, &job->getExtendedSlot(ThenableJobSlot_Promise).toObject());
- RootedField thenable(
- roots, job->getExtendedSlot(ThenableJobSlot_Thenable));
- RootedField then(
- roots, &job->getExtendedSlot(ThenableJobSlot_Handler).toObject());
-
- return PromiseResolveThenableJob(cx, promise, thenable, then);
-}
-
[[nodiscard]] static bool OriginalPromiseThenWithoutSettleHandlers(
JSContext* cx, Handle promise,
Handle promiseToResolve);
@@ -2851,21 +2758,6 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx,
stack);
}
-static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
- Value* vp) {
- CallArgs args = CallArgsFromVp(argc, vp);
-
- RootedFunction job(cx, &args.callee().as());
- RootedObject promise(
- cx, &job->getExtendedSlot(ThenableJobSlot_Promise).toObject());
- RootedObject thenable(
- cx, &job->getExtendedSlot(ThenableJobSlot_Thenable).toObject());
- // The handler slot is not used for builtin `then`.
- MOZ_ASSERT(job->getExtendedSlot(ThenableJobSlot_Handler).isUndefined());
-
- return PromiseResolveBuiltinThenableJob(cx, promise, thenable);
-}
-
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
@@ -2931,59 +2823,35 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
// compartment.
RootedField promise(roots, &promiseToResolve.toObject());
- if (JS::Prefs::use_js_microtask_queue()) {
- RootedField hostDefinedGlobalRepresentative(roots);
- {
- RootedField hostDefinedGlobal(roots);
- if (!cx->jobQueue->getHostDefinedGlobal(cx, &hostDefinedGlobal)) {
- return false;
- }
-
- MOZ_ASSERT_IF(hostDefinedGlobal, hostDefinedGlobal->is());
- if (hostDefinedGlobal) {
- hostDefinedGlobalRepresentative =
- &hostDefinedGlobal->as().getObjectPrototype();
- }
- }
-
- // Wrap the representative.
- if (!cx->compartment()->wrap(cx, &hostDefinedGlobalRepresentative)) {
+ RootedField hostDefinedGlobalRepresentative(roots);
+ {
+ RootedField hostDefinedGlobal(roots);
+ if (!cx->jobQueue->getHostDefinedGlobal(cx, &hostDefinedGlobal)) {
return false;
}
- ThenableJob* thenableJob =
- NewThenableJob(cx, ThenableJob::PromiseResolveThenableJob, promise,
- thenable, then, HostDefinedDataIsOptimizedOut);
- if (!thenableJob) {
- return false;
+ MOZ_ASSERT_IF(hostDefinedGlobal, hostDefinedGlobal->is());
+ if (hostDefinedGlobal) {
+ hostDefinedGlobalRepresentative =
+ &hostDefinedGlobal->as().getObjectPrototype();
}
-
- thenableJob->setHostDefinedGlobalRepresentative(
- hostDefinedGlobalRepresentative);
- return EnqueueJob(cx, thenableJob);
}
- // Step 1. Let job be a new Job Abstract Closure with no parameters that
- // captures promiseToResolve, thenable, and then and performs the
- // following steps when called:
- Handle funName = cx->names().empty_;
- RootedField job(
- roots,
- NewNativeFunction(cx, PromiseResolveThenableJob, 0, funName,
- gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
- if (!job) {
+ // Wrap the representative.
+ if (!cx->compartment()->wrap(cx, &hostDefinedGlobalRepresentative)) {
return false;
}
- // Set the `promiseToResolve`, `thenable` and `then` arguments on the
- // callback.
- job->setExtendedSlot(ThenableJobSlot_Promise, promiseToResolve);
- job->setExtendedSlot(ThenableJobSlot_Thenable, thenable);
- job->setExtendedSlot(ThenableJobSlot_Handler, ObjectValue(*then));
+ ThenableJob* thenableJob =
+ NewThenableJob(cx, ThenableJob::PromiseResolveThenableJob, promise,
+ thenable, then, HostDefinedDataIsOptimizedOut);
+ if (!thenableJob) {
+ return false;
+ }
- // Step X. HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
- return cx->runtime()->enqueuePromiseJob(cx, job, promise,
- HostDefinedDataIsOptimizedOut);
+ thenableJob->setHostDefinedGlobalRepresentative(
+ hostDefinedGlobalRepresentative);
+ return EnqueueJob(cx, thenableJob);
}
/**
@@ -3005,53 +2873,23 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
MOZ_ASSERT(promiseToResolve->is());
MOZ_ASSERT(thenable->is());
- if (JS::Prefs::use_js_microtask_queue()) {
- // Step 1. Let job be a new Job Abstract Closure with no parameters that
-
- Rooted hostDefinedData(cx);
- if (!cx->runtime()->getHostDefinedData(cx, &hostDefinedData)) {
- return false;
- }
-
- RootedValue thenableValue(cx, ObjectValue(*thenable));
- ThenableJob* thenableJob = NewThenableJob(
- cx, ThenableJob::PromiseResolveBuiltinThenableJob, promiseToResolve,
- thenableValue, nullptr, hostDefinedData);
- if (!thenableJob) {
- return false;
- }
-
- return EnqueueJob(cx, thenableJob);
- }
-
// Step 1. Let job be a new Job Abstract Closure with no parameters that
// captures promiseToResolve, thenable, and then and performs the
// following steps when called:
- Handle funName = cx->names().empty_;
- RootedFunction job(
- cx, NewNativeFunction(cx, PromiseResolveBuiltinThenableJob, 0, funName,
- gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
- if (!job) {
+ Rooted hostDefinedData(cx);
+ if (!cx->runtime()->getHostDefinedData(cx, &hostDefinedData)) {
return false;
}
- // Steps 2-5.
- // (implicit)
- // `then` is built-in Promise.prototype.then in the current realm.,
- // thus `thenRealm` is also current realm, and we have nothing to do here.
-
- // Store the promise and the thenable on the reaction job.
- job->setExtendedSlot(ThenableJobSlot_Promise, ObjectValue(*promiseToResolve));
- job->setExtendedSlot(ThenableJobSlot_Thenable, ObjectValue(*thenable));
-
- Rooted hostDefinedData(cx);
- if (!cx->runtime()->getHostDefinedData(cx, &hostDefinedData)) {
+ RootedValue thenableValue(cx, ObjectValue(*thenable));
+ ThenableJob* thenableJob =
+ NewThenableJob(cx, ThenableJob::PromiseResolveBuiltinThenableJob,
+ promiseToResolve, thenableValue, nullptr, hostDefinedData);
+ if (!thenableJob) {
return false;
}
- // HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
- return cx->runtime()->enqueuePromiseJob(cx, job, promiseToResolve,
- hostDefinedData);
+ return EnqueueJob(cx, thenableJob);
}
[[nodiscard]] static bool AddDummyPromiseReactionForDebugger(
@@ -7922,9 +7760,7 @@ JS::AutoDebuggerJobQueueInterruption::AutoDebuggerJobQueueInterruption()
JS::AutoDebuggerJobQueueInterruption::~AutoDebuggerJobQueueInterruption() {
#ifdef DEBUG
if (initialized() && !cx->jobQueue->isDrainingStopped()) {
- MOZ_ASSERT_IF(JS::Prefs::use_js_microtask_queue(),
- !JS::HasRegularMicroTasks(cx));
- MOZ_ASSERT_IF(!JS::Prefs::use_js_microtask_queue(), cx->jobQueue->empty());
+ MOZ_ASSERT(!JS::HasRegularMicroTasks(cx));
}
#endif
}
diff --git a/js/src/jit-test/tests/js_microtask/microtask-dead.js b/js/src/jit-test/tests/js_microtask/microtask-dead.js
index 823073f4d62ed..5ec2fdc7bf7c9 100644
--- a/js/src/jit-test/tests/js_microtask/microtask-dead.js
+++ b/js/src/jit-test/tests/js_microtask/microtask-dead.js
@@ -1,4 +1,4 @@
-// |jit-test| --more-compartments; -P use_js_microtask_queue=true
+// |jit-test| --more-compartments;
var called = false;
// A dead wrapper in the job queue shouldn't crash.
diff --git a/js/src/jit-test/tests/js_microtask/microtask-smoke-test.js b/js/src/jit-test/tests/js_microtask/microtask-smoke-test.js
index 20e426665e5db..f206484166540 100644
--- a/js/src/jit-test/tests/js_microtask/microtask-smoke-test.js
+++ b/js/src/jit-test/tests/js_microtask/microtask-smoke-test.js
@@ -1,4 +1,3 @@
-// |jit-test| --setpref=use_js_microtask_queue=true;
// Promise microtask queue smoke tests
// Test basic promise resolution and microtask ordering
diff --git a/js/src/jit-test/tests/wasm/excessive-inlining.js b/js/src/jit-test/tests/wasm/excessive-inlining.js
index a823a56ecde39..91ec710e4e46c 100644
--- a/js/src/jit-test/tests/wasm/excessive-inlining.js
+++ b/js/src/jit-test/tests/wasm/excessive-inlining.js
@@ -1,15 +1,9 @@
-// |jit-test| test-also=--setpref=wasm_lazy_tiering --setpref=wasm_lazy_tiering_synchronous; skip-if: wasmCompileMode() != "baseline+ion" || !getPrefValue("wasm_lazy_tiering")
+// |jit-test| test-also=--setpref=wasm_lazy_tiering --setpref=wasm_lazy_tiering_synchronous; skip-if: wasmCompileMode() != "baseline+ion" || !getPrefValue("wasm_lazy_tiering") || helperThreadCount() === 0
// Tests the inliner on a recursive function, in particular to establish that
// the inlining heuristics have some way to stop the compiler looping
// indefinitely and, more constrainingly, that it has some way to stop
-// excessive but finite inlining.
-
-// `func $recursive` has 95 bytecode bytes. With module- and function-level
-// inlining budgeting disabled, it is inlined into itself 1110 times,
-// processing an extra 105450 bytecode bytes. This is definitely excessive.
-//
-// With budgeting re-enabled, it is inlined just 9 times, as intended.
+// excessive but finite inlining. See comments below.
let t = `
(module
@@ -44,15 +38,84 @@ let t = `
i32.add
(call $recursive (i32.sub (local.get 0) (i32.const 2)))
i32.add
+
+ (call $recursive (i32.sub (local.get 0) (i32.const 1)))
+ i32.add
+ (call $recursive (i32.sub (local.get 0) (i32.const 2)))
+ i32.add
end
)
)`;
-let i = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary(t)));
+let m = new WebAssembly.Module(wasmTextToBinary(t));
+let i = new WebAssembly.Instance(m);
+
+// Make the function do small amounts of work, until optimized code is
+// available.
+let numIters = 0;
+while (wasmFunctionTier(i.exports.recursive) !== "optimized") {
+ assertEq(i.exports.recursive(6), 27805);
+ // Cause the test to fail if we run excessively long while waiting for
+ // optimized code.
+ numIters++;
+ assertEq(numIters < 10000, true);
+}
-assertEq(i.exports.recursive(10), 14517361);
+let ma = wasmMetadataAnalysis(m);
+
+let tier1codeBytesUsed = ma["tier1 code bytes used"];
+let tier2codeBytesUsed = ma["tier2 code bytes used"];
+
+// We should have at least some baseline code.
+assertEq(tier1codeBytesUsed > 500, true);
+
+// We should have at least some some optimized code.
+assertEq(tier2codeBytesUsed > 2000, true);
+
+// But not an excessive amount. This is the assertion that checks that
+// the inlining-budget cutoff mechanism is working.
+assertEq(tier2codeBytesUsed < 15000, true);
+
+// The thresholds above are based on the following measurements.
+//
+// tier1codeBytesUsed (baseline size)
+//
+// x64 x32 arm64 arm32
+//
+// 1378 1010 1408 1008 --enable-debug build
+// 1218 866 1248 856 --disable-debug build
+//
+// tier2codeBytesUsed (optimized size), with inline-size budgeting enabled
+//
+// x64 x32 arm64 arm32
+//
+// 5186 6994 7136 5472 --enable-debug build
+// 3698 3730 5472 3888 --disable-debug build
+//
+// tier2codeBytesUsed (optimized size), with inline-size budgeting disabled
+//
+// x64 x32 arm64 arm32
+//
+// 64786 91906 89680 69424 --enable-debug build
+// 45634 47266 68752 48560 --disable-debug build
+//
+//
+// Given these numbers, it seems safe to claim, with a wide margin of error,
+// that:
+//
+// (1) the baseline size will be at least 500 bytes
+//
+// (2) the optimized size will be at least 2000 bytes
+//
+// (3) if the inline-budget mechanism is working as intended, the optimized
+// size will be less than 15000 bytes
+//
+//
+// Note (for future testing): inline-size budgeting was disabled by changing
+// two C++ constants as follows:
+//
+// static constexpr int64_t PerModuleMaxInliningRatio = 1000*1;
+// static constexpr int64_t PerFunctionMaxInliningRatio = 1000*99;
+//
+// (by default they are 1 and 99 respectively).
-// This assertion will fail if there is runaway recursion, because the
-// optimised compilation will run long and hence not be completed by the time
-// the assertion is evaluated.
-assertEq(wasmFunctionTier(i.exports.recursive), "optimized");
diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp
index 23f4f2f592de6..51c24c340a8db 100644
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -1510,47 +1510,30 @@ static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) {
static bool GlobalOfFirstJobInQueue(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
- if (JS::Prefs::use_js_microtask_queue()) {
- if (cx->microTaskQueues->microTaskQueue.empty()) {
- JS_ReportErrorASCII(cx, "Job queue is empty");
- return false;
- }
-
- auto& genericJob = cx->microTaskQueues->microTaskQueue.front();
- JS::JSMicroTask* job = JS::ToUnwrappedJSMicroTask(genericJob);
- if (!job) {
- JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
- JSMSG_DEAD_OBJECT);
-
- return false;
- }
-
- RootedObject global(cx, JS::GetExecutionGlobalFromJSMicroTask(job));
- if (!global) {
- JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
- JSMSG_DEAD_OBJECT);
- return false;
- }
- MOZ_ASSERT(global);
- if (!cx->compartment()->wrap(cx, &global)) {
- return false;
- }
+ if (cx->microTaskQueues->microTaskQueue.empty()) {
+ JS_ReportErrorASCII(cx, "Job queue is empty");
+ return false;
+ }
- args.rval().setObject(*global);
- } else {
- RootedObject job(cx, cx->internalJobQueue->maybeFront());
- if (!job) {
- JS_ReportErrorASCII(cx, "Job queue is empty");
- return false;
- }
+ auto& genericJob = cx->microTaskQueues->microTaskQueue.front();
+ JS::JSMicroTask* job = JS::ToUnwrappedJSMicroTask(genericJob);
+ if (!job) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
- RootedObject global(cx, &job->nonCCWGlobal());
- if (!cx->compartment()->wrap(cx, &global)) {
- return false;
- }
+ return false;
+ }
- args.rval().setObject(*global);
+ RootedObject global(cx, JS::GetExecutionGlobalFromJSMicroTask(job));
+ if (!global) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
+ return false;
}
+ MOZ_ASSERT(global);
+ if (!cx->compartment()->wrap(cx, &global)) {
+ return false;
+ }
+
+ args.rval().setObject(*global);
return true;
}
diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp
index cddfff5d4335e..2068b4bea8a5d 100644
--- a/js/src/vm/JSContext.cpp
+++ b/js/src/vm/JSContext.cpp
@@ -781,48 +781,35 @@ JSObject* InternalJobQueue::copyJobs(JSContext* cx) {
return nullptr;
}
- if (JS::Prefs::use_js_microtask_queue()) {
- auto& queues = cx->microTaskQueues;
- auto addToArray = [&](auto& queue) -> bool {
- for (const auto& e : queue) {
- JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(e);
- if (task) {
- // All any test cares about is the global of the job so let's do it.
- RootedObject global(cx, JS::GetExecutionGlobalFromJSMicroTask(task));
- if (!global) {
- JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
- JSMSG_DEAD_OBJECT);
- return false;
- }
- if (!cx->compartment()->wrap(cx, &global)) {
- return false;
- }
- if (!NewbornArrayPush(cx, jobs, ObjectValue(*global))) {
- return false;
- }
+ auto& queues = cx->microTaskQueues;
+ auto addToArray = [&](auto& queue) -> bool {
+ for (const auto& e : queue) {
+ JS::JSMicroTask* task = JS::ToUnwrappedJSMicroTask(e);
+ if (task) {
+ // All any test cares about is the global of the job so let's do it.
+ RootedObject global(cx, JS::GetExecutionGlobalFromJSMicroTask(task));
+ if (!global) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DEAD_OBJECT);
+ return false;
+ }
+ if (!cx->compartment()->wrap(cx, &global)) {
+ return false;
+ }
+ if (!NewbornArrayPush(cx, jobs, ObjectValue(*global))) {
+ return false;
}
}
-
- return true;
- };
-
- if (!addToArray(queues->debugMicroTaskQueue)) {
- return nullptr;
- }
- if (!addToArray(queues->microTaskQueue)) {
- return nullptr;
}
- } else {
- for (const JSObject* unwrappedJob : queue.get()) {
- RootedObject job(cx, const_cast(unwrappedJob));
- if (!cx->compartment()->wrap(cx, &job)) {
- return nullptr;
- }
- if (!NewbornArrayPush(cx, jobs, ObjectValue(*job))) {
- return nullptr;
- }
- }
+ return true;
+ };
+
+ if (!addToArray(queues->debugMicroTaskQueue)) {
+ return nullptr;
+ }
+ if (!addToArray(queues->microTaskQueue)) {
+ return nullptr;
}
return jobs;
@@ -862,21 +849,6 @@ bool InternalJobQueue::getHostDefinedData(
return true;
}
-bool InternalJobQueue::enqueuePromiseJob(JSContext* cx,
- JS::HandleObject promise,
- JS::HandleObject job,
- JS::HandleObject allocationSite,
- JS::HandleObject hostDefinedData) {
- MOZ_ASSERT(job);
- if (!queue.pushBack(job)) {
- ReportOutOfMemory(cx);
- return false;
- }
-
- JS::JobQueueMayNotBeEmpty(cx);
- return true;
-}
-
void InternalJobQueue::runJobs(JSContext* cx) {
if (draining_ || interrupted_) {
return;
@@ -891,96 +863,49 @@ void InternalJobQueue::runJobs(JSContext* cx) {
// so we simply ignore nested calls of drainJobQueue.
draining_ = true;
- if (JS::Prefs::use_js_microtask_queue()) {
- // Execute jobs in a loop until we've reached the end of the queue.
- JS::Rooted job(cx);
- JS::Rooted dequeueJob(cx);
- while (JS::HasAnyMicroTasks(cx)) {
- MOZ_ASSERT(queue.empty());
- // A previous job might have set this flag. E.g., the js shell
- // sets it if the `quit` builtin function is called.
- if (interrupted_) {
- break;
- }
-
- cx->runtime()->offThreadPromiseState.ref().internalDrain(cx);
+ // Execute jobs in a loop until we've reached the end of the queue.
+ JS::Rooted job(cx);
+ JS::Rooted dequeueJob(cx);
+ while (JS::HasAnyMicroTasks(cx)) {
+ // A previous job might have set this flag. E.g., the js shell
+ // sets it if the `quit` builtin function is called.
+ if (interrupted_) {
+ break;
+ }
- dequeueJob = JS::DequeueNextMicroTask(cx);
- MOZ_ASSERT(!dequeueJob.isNull());
- job = JS::ToMaybeWrappedJSMicroTask(dequeueJob);
- MOZ_ASSERT(job);
+ cx->runtime()->offThreadPromiseState.ref().internalDrain(cx);
- // If the next job is the last job in the job queue, allow
- // skipping the standard job queuing behavior.
- if (!JS::HasAnyMicroTasks(cx)) {
- JS::JobQueueIsEmpty(cx);
- }
+ dequeueJob = JS::DequeueNextMicroTask(cx);
+ MOZ_ASSERT(!dequeueJob.isNull());
+ job = JS::ToMaybeWrappedJSMicroTask(dequeueJob);
+ MOZ_ASSERT(job);
- if (!JS::GetExecutionGlobalFromJSMicroTask(job)) {
- continue;
- }
- AutoRealm ar(cx, JS::GetExecutionGlobalFromJSMicroTask(job));
- {
- if (!JS::RunJSMicroTask(cx, job)) {
- // Nothing we can do about uncatchable exceptions.
- if (!cx->isExceptionPending()) {
- continue;
- }
-
- // Always clear the exception, because
- // PrepareScriptEnvironmentAndInvoke will assert that we don't have
- // one.
- RootedValue exn(cx);
- bool success = cx->getPendingException(&exn);
- cx->clearPendingException();
- if (success) {
- js::ReportExceptionClosure reportExn(exn);
- PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
- }
- }
- }
+ // If the next job is the last job in the job queue, allow
+ // skipping the standard job queuing behavior.
+ if (!JS::HasAnyMicroTasks(cx)) {
+ JS::JobQueueIsEmpty(cx);
}
- } else {
- RootedObject job(cx);
- JS::HandleValueArray args(JS::HandleValueArray::empty());
- RootedValue rval(cx);
- // Execute jobs in a loop until we've reached the end of the queue.
- while (!queue.empty()) {
- // A previous job might have set this flag. E.g., the js shell
- // sets it if the `quit` builtin function is called.
- if (interrupted_) {
- break;
- }
-
- cx->runtime()->offThreadPromiseState.ref().internalDrain(cx);
-
- job = queue.front();
- queue.popFront();
- // If the next job is the last job in the job queue, allow
- // skipping the standard job queuing behavior.
- if (queue.empty()) {
- JS::JobQueueIsEmpty(cx);
- }
+ if (!JS::GetExecutionGlobalFromJSMicroTask(job)) {
+ continue;
+ }
+ AutoRealm ar(cx, JS::GetExecutionGlobalFromJSMicroTask(job));
+ {
+ if (!JS::RunJSMicroTask(cx, job)) {
+ // Nothing we can do about uncatchable exceptions.
+ if (!cx->isExceptionPending()) {
+ continue;
+ }
- AutoRealm ar(cx, &job->as());
- {
- if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval)) {
- // Nothing we can do about uncatchable exceptions.
- if (!cx->isExceptionPending()) {
- continue;
- }
-
- // Always clear the exception, because
- // PrepareScriptEnvironmentAndInvoke will assert that we don't have
- // one.
- RootedValue exn(cx);
- bool success = cx->getPendingException(&exn);
- cx->clearPendingException();
- if (success) {
- js::ReportExceptionClosure reportExn(exn);
- PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
- }
+ // Always clear the exception, because
+ // PrepareScriptEnvironmentAndInvoke will assert that we don't have
+ // one.
+ RootedValue exn(cx);
+ bool success = cx->getPendingException(&exn);
+ cx->clearPendingException();
+ if (success) {
+ js::ReportExceptionClosure reportExn(exn);
+ PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
}
}
}
@@ -992,12 +917,8 @@ void InternalJobQueue::runJobs(JSContext* cx) {
break;
}
- if (JS::Prefs::use_js_microtask_queue()) {
- // MG:XXX: Should use public API here.
- cx->microTaskQueues->clear();
- } else {
- queue.clear();
- }
+ // MG:XXX: Should use public API here.
+ cx->microTaskQueues->clear();
// It's possible a job added a new off-thread promise task.
if (!cx->runtime()->offThreadPromiseState.ref().internalHasPending()) {
@@ -1006,59 +927,34 @@ void InternalJobQueue::runJobs(JSContext* cx) {
}
}
-bool InternalJobQueue::empty() const { return queue.empty(); }
-
-JSObject* InternalJobQueue::maybeFront() const {
- if (queue.empty()) {
- return nullptr;
- }
-
- return queue.get().front();
-}
-
class js::InternalJobQueue::SavedQueue : public JobQueue::SavedJobQueue {
public:
- SavedQueue(JSContext* cx, Queue&& saved, MicroTaskQueueSet&& queueSet,
- bool draining)
- : cx(cx),
- saved(cx, std::move(saved)),
- savedQueues(cx, std::move(queueSet)),
- draining_(draining) {
+ SavedQueue(JSContext* cx, MicroTaskQueueSet&& queueSet, bool draining)
+ : cx(cx), savedQueues(cx, std::move(queueSet)), draining_(draining) {
MOZ_ASSERT(cx->internalJobQueue.ref());
- if (JS::Prefs::use_js_microtask_queue()) {
- MOZ_ASSERT(saved.empty());
- } else {
- MOZ_ASSERT(queueSet.empty());
- }
}
~SavedQueue() {
MOZ_ASSERT(cx->internalJobQueue.ref());
- cx->internalJobQueue->queue = std::move(saved.get());
cx->internalJobQueue->draining_ = draining_;
*cx->microTaskQueues.get() = std::move(savedQueues.get());
}
private:
JSContext* cx;
- PersistentRooted saved;
PersistentRooted savedQueues;
bool draining_;
};
js::UniquePtr InternalJobQueue::saveJobQueue(
JSContext* cx) {
- auto saved = js::MakeUnique(
- cx, std::move(queue.get()), std::move(*cx->microTaskQueues), draining_);
+ auto saved = js::MakeUnique(cx, std::move(*cx->microTaskQueues),
+ draining_);
if (!saved) {
- // When MakeUnique's allocation fails, the SavedQueue constructor is never
- // called, so this->queue is still initialized. (The move doesn't occur
- // until the constructor gets called.)
ReportOutOfMemory(cx);
return nullptr;
}
- queue = Queue(SystemAllocPolicy());
draining_ = false;
return saved;
}
diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h
index cf408855543b1..57e5528dab576 100644
--- a/js/src/vm/JSContext.h
+++ b/js/src/vm/JSContext.h
@@ -88,7 +88,7 @@ struct AutoResolving;
class InternalJobQueue : public JS::JobQueue {
public:
explicit InternalJobQueue(JSContext* cx)
- : queue(cx, SystemAllocPolicy()), draining_(false), interrupted_(false) {}
+ : draining_(false), interrupted_(false) {}
~InternalJobQueue() = default;
// JS::JobQueue methods.
@@ -98,11 +98,8 @@ class InternalJobQueue : public JS::JobQueue {
bool getHostDefinedGlobal(JSContext*,
JS::MutableHandle) const override;
- bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
- JS::HandleObject job, JS::HandleObject allocationSite,
- JS::HandleObject hostDefinedData) override;
void runJobs(JSContext* cx) override;
- bool empty() const override;
+
bool isDrainingStopped() const override { return interrupted_; }
// If we are currently in a call to runJobs(), make that call stop processing
@@ -112,19 +109,11 @@ class InternalJobQueue : public JS::JobQueue {
void uninterrupt() { interrupted_ = false; }
- // Return the front element of the queue, or nullptr if the queue is empty.
- // This is only used by shell testing functions.
- JSObject* maybeFront() const;
-
#ifdef DEBUG
JSObject* copyJobs(JSContext* cx);
#endif
private:
- using Queue = js::TraceableFifo;
-
- JS::PersistentRooted queue;
-
// True if we are in the midst of draining jobs from this queue. We use this
// to avoid re-entry (nested calls simply return immediately).
bool draining_;
diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp
index 638670943faea..6f3eb8985df85 100644
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -621,25 +621,6 @@ JS::MaybeGetPromiseAllocationSiteFromPossiblyWrappedPromise(
return nullptr;
}
-bool JSRuntime::enqueuePromiseJob(JSContext* cx, HandleFunction job,
- HandleObject promise,
- HandleObject hostDefinedData) {
- MOZ_ASSERT(cx->jobQueue,
- "Must select a JobQueue implementation using JS::JobQueue "
- "or js::UseInternalJobQueues before using Promises");
-
- if (promise) {
-#ifdef DEBUG
- AssertSameCompartment(job, promise);
-#endif
- }
-
- RootedObject allocationSite(
- cx, JS::MaybeGetPromiseAllocationSiteFromPossiblyWrappedPromise(promise));
- return cx->jobQueue->enqueuePromiseJob(cx, promise, job, allocationSite,
- hostDefinedData);
-}
-
void JSRuntime::addUnhandledRejectedPromise(JSContext* cx,
js::HandleObject promise) {
MOZ_ASSERT(promise->is());
diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h
index fb48582dd2ed5..ff75da5eac337 100644
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -432,9 +432,6 @@ struct JSRuntime {
bool getHostDefinedData(JSContext* cx,
JS::MutableHandle data) const;
- bool enqueuePromiseJob(JSContext* cx, js::HandleFunction job,
- js::HandleObject promise,
- js::HandleObject hostDefinedData);
void addUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
void removeUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
diff --git a/js/src/wasm/WasmCode.cpp b/js/src/wasm/WasmCode.cpp
index 024c7fb0003b9..7803134d7860e 100644
--- a/js/src/wasm/WasmCode.cpp
+++ b/js/src/wasm/WasmCode.cpp
@@ -1533,7 +1533,7 @@ void Code::disassemble(JSContext* cx, Tier tier, int kindSelection,
// Return a map with names and associated statistics
MetadataAnalysisHashMap Code::metadataAnalysis(JSContext* cx) const {
MetadataAnalysisHashMap hashmap;
- if (!hashmap.reserve(14)) {
+ if (!hashmap.reserve(16)) {
return hashmap;
}
@@ -1589,6 +1589,16 @@ MetadataAnalysisHashMap Code::metadataAnalysis(JSContext* cx) const {
codeBlock.funcExports.sizeOfExcludingThis(mallocSizeOf));
}
+ size_t codeBytesUsedInTier1 = 0;
+ size_t codeBytesUsedInTier2 = 0;
+ {
+ auto guard = data_.readLock();
+ codeBytesUsedInTier1 = guard->tier1Stats.codeBytesUsed;
+ codeBytesUsedInTier2 = guard->tier2Stats.codeBytesUsed;
+ }
+ hashmap.putNewInfallible("tier1 code bytes used", codeBytesUsedInTier1);
+ hashmap.putNewInfallible("tier2 code bytes used", codeBytesUsedInTier2);
+
return hashmap;
}
diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore
index cac3c94b8a1f5..fd0d85462b3c6 100644
--- a/mobile/android/.gitignore
+++ b/mobile/android/.gitignore
@@ -68,7 +68,6 @@ test_artifacts/
.mls_token
.nimbus
.wallpaper_url
-.pocket_consumer_key
fenix/app/src/**/res/values/fenix_firebase_push_credentials.xml
# Python Byte-compiled / optimized / DLL files
diff --git a/mobile/android/android-components/.buildconfig.yml b/mobile/android/android-components/.buildconfig.yml
index 6993f55b1fe8e..e1e08dfaee9c0 100644
--- a/mobile/android/android-components/.buildconfig.yml
+++ b/mobile/android/android-components/.buildconfig.yml
@@ -2166,6 +2166,7 @@ projects:
- components:service-firefox-accounts
- components:support-base
- components:support-ktx
+ - components:support-test
- components:support-utils
- components:tooling-lint
- components:ui-colors
diff --git a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt
index 7ef087c86e3f3..e8082bbd36273 100644
--- a/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt
+++ b/mobile/android/android-components/components/concept/sync/src/main/java/mozilla/components/concept/sync/OAuthAccount.kt
@@ -149,6 +149,15 @@ interface OAuthAccount : AutoCloseable {
*/
suspend fun getAccessToken(singleScope: String): AccessTokenInfo?
+ /**
+ * Get the list of all client applications attached to the user's account.
+ *
+ * This method returns a list of AttachedClient structs representing all the applications
+ * connected to the user's account. This includes applications that are registered as a
+ * device as well as server-side services that the user has connected.
+ */
+ suspend fun getAttachedClient(): List
+
/**
* Call this whenever an authentication error was encountered while using an access token
* issued by [getAccessToken].
@@ -375,3 +384,17 @@ data class AccessTokenInfo(
val key: OAuthScopedKey?,
val expiresAt: Long,
)
+
+/**
+ * The result of a request to get attached clients.
+ */
+data class AttachedClient(
+ val clientId: String?,
+ val deviceId: String?,
+ val deviceType: DeviceType,
+ val isCurrentSession: Boolean,
+ val name: String?,
+ val createdTime: Long?,
+ val lastAccessTime: Long?,
+ val scope: List?,
+)
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
index 209fd346cc40d..fec94ef472b7c 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
@@ -202,6 +202,15 @@ class FirefoxAccount internal constructor(
}
}
+ /**
+ * See [OAuthAccount.getAttachedClient].
+ */
+ override suspend fun getAttachedClient() = withContext(scope.coroutineContext) {
+ handleFxaExceptions(logger, "get attached client", { emptyList() }) {
+ inner.getAttachedClients().map { it.into() }
+ }
+ }
+
override fun authErrorDetected() {
// fxalib maintains some internal token caches that need to be cleared whenever we
// hit an auth problem. Call below makes that clean-up happen.
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
index 376361d77513f..95a7d8e7df4ad 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
@@ -7,6 +7,7 @@ package mozilla.components.service.fxa
import mozilla.appservices.fxaclient.AccessTokenInfo
import mozilla.appservices.fxaclient.AccountEvent
+import mozilla.appservices.fxaclient.AttachedClient
import mozilla.appservices.fxaclient.Device
import mozilla.appservices.fxaclient.IncomingDeviceCommand
import mozilla.appservices.fxaclient.Profile
@@ -71,6 +72,22 @@ fun AccessTokenInfo.into(): mozilla.components.concept.sync.AccessTokenInfo {
)
}
+/**
+ * Converts from rust data type to the [mozilla.components.concept.sync.AttachedClient].
+ */
+fun AttachedClient.into(): mozilla.components.concept.sync.AttachedClient {
+ return mozilla.components.concept.sync.AttachedClient(
+ clientId = this.clientId,
+ deviceId = this.deviceId,
+ deviceType = this.deviceType.into(),
+ isCurrentSession = isCurrentSession,
+ name = this.name,
+ createdTime = this.createdTime,
+ lastAccessTime = this.lastAccessTime,
+ scope = this.scope,
+ )
+}
+
/**
* Converts a generic [AccessTokenInfo] into a Firefox Sync-friendly [SyncAuthInfo] instance which
* may be used for data synchronization.
diff --git a/mobile/android/android-components/components/service/firefox-relay/build.gradle b/mobile/android/android-components/components/service/firefox-relay/build.gradle
index 6aaab51730708..92bb084213de4 100644
--- a/mobile/android/android-components/components/service/firefox-relay/build.gradle
+++ b/mobile/android/android-components/components/service/firefox-relay/build.gradle
@@ -42,6 +42,8 @@ dependencies {
implementation libs.androidx.compose.ui
implementation libs.androidx.compose.ui.tooling.preview
+ testImplementation project(':components:support-test')
+
testImplementation libs.androidx.test.core
testImplementation libs.androidx.test.junit
testImplementation libs.androidx.work.testing
diff --git a/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/RelayFeature.kt b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/RelayFeature.kt
index 0464859acb0da..8ac1e976b0e6f 100644
--- a/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/RelayFeature.kt
+++ b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/RelayFeature.kt
@@ -16,11 +16,13 @@ import mozilla.components.service.fxrelay.EmailMask
import mozilla.components.service.fxrelay.FxRelay
import mozilla.components.service.fxrelay.FxRelayImpl
import mozilla.components.service.fxrelay.RelayAccountDetails
+import mozilla.components.service.fxrelay.eligibility.ext.relayClient
+import mozilla.components.service.fxrelay.eligibility.ext.shouldCheckStatus
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
-private const val FETCH_TIMEOUT_MS: Long = 300_000L
+internal const val FETCH_TIMEOUT_MS: Long = 300_000L
/**
* Feature for accessing Firefox Relay service.
@@ -58,14 +60,22 @@ class RelayFeature(
}
private suspend fun checkRelayStatus(state: RelayState) {
- val loggedIn = state.eligibilityState !is Ineligible.FirefoxAccountNotLoggedIn
- val lastCheck = state.lastEntitlementCheckMs
- val now = System.currentTimeMillis()
- val ttlExpired = lastCheck == NO_ENTITLEMENT_CHECK_YET_MS || now - lastCheck >= fetchTimeoutMs
+ logger.debug("Request to check for relay status..")
+ val shouldCheck = state.shouldCheckStatus(fetchTimeoutMs)
+ if (!shouldCheck) {
+ logger.info("Check status conditions not satisfied.")
+ return
+ }
- if (!loggedIn || !ttlExpired) return
+ val existingClient = accountManager.authenticatedAccount()?.relayClient() != null
+ if (!existingClient) {
+ logger.info("Account does not have an existing relay service.")
+ return
+ }
val relayDetails: RelayAccountDetails? = fxRelay?.fetchAccountDetails()
+ logger.info("Updating Relay account state..")
+
store.dispatch(
RelayEligibilityAction.RelayStatusResult(
fetchSucceeded = relayDetails != null,
diff --git a/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ServiceClientId.kt b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ServiceClientId.kt
new file mode 100644
index 0000000000000..b6122a3013751
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ServiceClientId.kt
@@ -0,0 +1,16 @@
+/* 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/. */
+
+package mozilla.components.service.fxrelay.eligibility
+
+/**
+ * Firefox Relay service client identifiers for different environments.
+ *
+ * Source: https://github.com/mozilla/fxa/blob/35c7a999010a4ff053fc65edbc39d343b8e6f143/packages/fxa-settings/src/models/integrations/client-matching.ts#L12-L16
+ */
+enum class ServiceClientId(val id: String) {
+ Stage("41b4363ae36440a9"),
+ Dev("723aa3bce05884d8"),
+ Production("9ebfe2c2f9ea3c58"),
+}
diff --git a/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ext/OAuthAccount.kt b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ext/OAuthAccount.kt
new file mode 100644
index 0000000000000..b8a965f7c6079
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ext/OAuthAccount.kt
@@ -0,0 +1,22 @@
+/* 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/. */
+
+package mozilla.components.service.fxrelay.eligibility.ext
+
+import mozilla.components.concept.sync.AttachedClient
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.service.fxrelay.eligibility.ServiceClientId
+
+/**
+ * Retrieves the Firefox Relay client from the list of attached clients.
+ *
+ * @return The attached relay client if found, null otherwise.
+ */
+internal suspend fun OAuthAccount.relayClient(): AttachedClient? {
+ val serviceIds = ServiceClientId.entries.map { it.id }.toSet()
+ return getAttachedClient()
+ .firstOrNull { client ->
+ serviceIds.contains(client.clientId)
+ }
+}
diff --git a/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ext/RelayState.kt b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ext/RelayState.kt
new file mode 100644
index 0000000000000..b0edbe5240f88
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-relay/src/main/java/mozilla/components/service/fxrelay/eligibility/ext/RelayState.kt
@@ -0,0 +1,26 @@
+/* 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/. */
+
+package mozilla.components.service.fxrelay.eligibility.ext
+
+import mozilla.components.service.fxrelay.eligibility.FETCH_TIMEOUT_MS
+import mozilla.components.service.fxrelay.eligibility.Ineligible
+import mozilla.components.service.fxrelay.eligibility.NO_ENTITLEMENT_CHECK_YET_MS
+import mozilla.components.service.fxrelay.eligibility.RelayState
+
+/**
+ * Determines if the relay entitlement status should be checked.
+ *
+ * @param timeout Time-to-live for entitlement checks in milliseconds.
+ * @return True if the user is logged in and the TTL has expired, or if there hasn't been any
+ * entitlement checks yet, false otherwise.
+ */
+internal fun RelayState.shouldCheckStatus(timeout: Long = FETCH_TIMEOUT_MS): Boolean {
+ val loggedIn = eligibilityState !is Ineligible.FirefoxAccountNotLoggedIn
+ val lastCheck = lastEntitlementCheckMs
+ val now = System.currentTimeMillis()
+ val ttlExpired = lastCheck == NO_ENTITLEMENT_CHECK_YET_MS || now - lastCheck >= timeout
+
+ return loggedIn && ttlExpired
+}
diff --git a/mobile/android/android-components/components/service/firefox-relay/src/test/java/mozilla/components/service/fxrelay/eligibility/ext/OAuthAccountRelayClientTest.kt b/mobile/android/android-components/components/service/firefox-relay/src/test/java/mozilla/components/service/fxrelay/eligibility/ext/OAuthAccountRelayClientTest.kt
new file mode 100644
index 0000000000000..554ed6baa6f6d
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-relay/src/test/java/mozilla/components/service/fxrelay/eligibility/ext/OAuthAccountRelayClientTest.kt
@@ -0,0 +1,86 @@
+/* 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/. */
+
+package mozilla.components.service.fxrelay.eligibility.ext
+
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.sync.AttachedClient
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.service.fxrelay.eligibility.ServiceClientId
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class OAuthAccountRelayClientTest {
+
+ @Test
+ fun `GIVEN account with relay client using any client ID WHEN relayClient is called THEN returns the client`() = runTest {
+ val relayClient = createAttachedClient(clientId = ServiceClientId.Production.id)
+ val otherClient = createAttachedClient(clientId = "other-client-id")
+ val account: OAuthAccount = mock()
+ whenever(account.getAttachedClient()).thenReturn(listOf(otherClient, relayClient))
+
+ val result = account.relayClient()
+
+ assertEquals(relayClient, result)
+ }
+
+ @Test
+ fun `GIVEN account with no relay client WHEN relayClient is called THEN returns null`() = runTest {
+ val client1 = createAttachedClient(clientId = "some-client-id")
+ val client2 = createAttachedClient(clientId = "another-client-id")
+ val account: OAuthAccount = mock()
+ whenever(account.getAttachedClient()).thenReturn(listOf(client1, client2))
+
+ val result = account.relayClient()
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `GIVEN account with no attached clients WHEN relayClient is called THEN returns null`() = runTest {
+ val account: OAuthAccount = mock()
+ whenever(account.getAttachedClient()).thenReturn(emptyList())
+
+ val result = account.relayClient()
+
+ assertNull(result)
+ }
+
+ @Test
+ fun `GIVEN account with multiple relay clients WHEN relayClient is called THEN returns the first one`() = runTest {
+ val relayClient1 = createAttachedClient(clientId = ServiceClientId.Stage.id)
+ val relayClient2 = createAttachedClient(clientId = ServiceClientId.Dev.id)
+ val otherClient = createAttachedClient(clientId = "other-client-id")
+ val account: OAuthAccount = mock()
+ whenever(account.getAttachedClient()).thenReturn(listOf(otherClient, relayClient1, relayClient2))
+
+ val result = account.relayClient()
+
+ assertEquals(relayClient1, result)
+ }
+
+ private fun createAttachedClient(
+ clientId: String?,
+ deviceId: String? = "device-id",
+ deviceType: DeviceType = DeviceType.DESKTOP,
+ isCurrentSession: Boolean = false,
+ name: String? = "Test Client",
+ createdTime: Long? = 1234567890L,
+ lastAccessTime: Long? = 1234567890L,
+ scope: List? = null,
+ ) = AttachedClient(
+ clientId = clientId,
+ deviceId = deviceId,
+ deviceType = deviceType,
+ isCurrentSession = isCurrentSession,
+ name = name,
+ createdTime = createdTime,
+ lastAccessTime = lastAccessTime,
+ scope = scope,
+ )
+}
diff --git a/mobile/android/android-components/components/service/firefox-relay/src/test/java/mozilla/components/service/fxrelay/eligibility/ext/RelayStateTest.kt b/mobile/android/android-components/components/service/firefox-relay/src/test/java/mozilla/components/service/fxrelay/eligibility/ext/RelayStateTest.kt
new file mode 100644
index 0000000000000..d9a3daa20998c
--- /dev/null
+++ b/mobile/android/android-components/components/service/firefox-relay/src/test/java/mozilla/components/service/fxrelay/eligibility/ext/RelayStateTest.kt
@@ -0,0 +1,67 @@
+/* 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/. */
+
+package mozilla.components.service.fxrelay.eligibility.ext
+
+import mozilla.components.service.fxrelay.eligibility.Eligible
+import mozilla.components.service.fxrelay.eligibility.FETCH_TIMEOUT_MS
+import mozilla.components.service.fxrelay.eligibility.Ineligible
+import mozilla.components.service.fxrelay.eligibility.NO_ENTITLEMENT_CHECK_YET_MS
+import mozilla.components.service.fxrelay.eligibility.RelayState
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class RelayStateTest {
+
+ @Test
+ fun `GIVEN user not logged in WHEN shouldCheckStatus is called THEN returns false`() {
+ val state = RelayState(
+ eligibilityState = Ineligible.FirefoxAccountNotLoggedIn,
+ lastEntitlementCheckMs = NO_ENTITLEMENT_CHECK_YET_MS,
+ )
+
+ val result = state.shouldCheckStatus()
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN logged in user never checked WHEN shouldCheckStatus is called THEN returns true`() {
+ val state = RelayState(
+ eligibilityState = Ineligible.NoRelay,
+ lastEntitlementCheckMs = NO_ENTITLEMENT_CHECK_YET_MS,
+ )
+
+ val result = state.shouldCheckStatus()
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN logged in user with expired TTL WHEN shouldCheckStatus is called THEN returns true`() {
+ val oldTimestamp = System.currentTimeMillis() - FETCH_TIMEOUT_MS - 1000L
+ val state = RelayState(
+ eligibilityState = Eligible.Premium,
+ lastEntitlementCheckMs = oldTimestamp,
+ )
+
+ val result = state.shouldCheckStatus()
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN logged in user with recent check WHEN shouldCheckStatus is called THEN returns false`() {
+ val recentTimestamp = System.currentTimeMillis() - 1000L
+ val state = RelayState(
+ eligibilityState = Eligible.Free(remaining = 5),
+ lastEntitlementCheckMs = recentTimestamp,
+ )
+
+ val result = state.shouldCheckStatus()
+
+ assertFalse(result)
+ }
+}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt
index 7e34e1266cd1f..a345aeb95f35e 100644
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt
@@ -7,7 +7,6 @@ package mozilla.components.service.pocket
import android.annotation.SuppressLint
import mozilla.components.service.pocket.mars.SponsoredContentsUseCases
import mozilla.components.service.pocket.recommendations.ContentRecommendationsUseCases
-import mozilla.components.service.pocket.spocs.SpocsUseCases
import mozilla.components.service.pocket.stories.PocketStoriesUseCases
/**
@@ -42,33 +41,6 @@ internal object GlobalDependencyProvider {
}
}
- internal object SponsoredStories {
- /**
- * Possible actions regarding the list of sponsored stories.
- */
- @SuppressLint("StaticFieldLeak")
- internal var useCases: SpocsUseCases? = null
- private set
-
- /**
- * Convenience method for setting all details used when communicating with the Pocket server.
- *
- * @param useCases [SpocsUseCases] containing all possible actions regarding the list of sponsored stories.
- */
- internal fun initialize(
- useCases: SpocsUseCases,
- ) {
- this.useCases = useCases
- }
-
- /**
- * Convenience method for cleaning up any resources held for communicating with the Pocket server.
- */
- internal fun reset() {
- useCases = null
- }
- }
-
internal object ContentRecommendations {
/**
* Possible actions regarding the list of content recommendations.
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
index e4c54399b2cdc..521f2acda7fa3 100644
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
@@ -7,10 +7,8 @@ package mozilla.components.service.pocket
import mozilla.components.concept.fetch.Client
import mozilla.components.service.pocket.mars.api.MarsSpocsRequestConfig
import mozilla.components.support.base.worker.Frequency
-import java.util.UUID
import java.util.concurrent.TimeUnit
-internal const val DEFAULT_SPONSORED_STORIES_SITE_ID = "1240699"
internal const val DEFAULT_REFRESH_INTERVAL = 4L
internal const val DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL = 4L
internal const val DEFAULT_CONTENT_RECOMMENDATIONS_REFRESH_INTERNAL = 4L
@@ -31,10 +29,8 @@ internal val DEFAULT_CONTENT_RECOMMENDATIONS_REFRESH_TIMEUNIT = TimeUnit.HOURS
*
* @param client [Client] implementation used for downloading the Pocket stories.
* @param frequency Optional - The interval at which to try and refresh items. Defaults to 4 hours.
- * @param profile Optional - The profile used for downloading sponsored Pocket stories.
* @param sponsoredStoriesRefreshFrequency Optional - The interval at which to try and refresh sponsored stories.
* Defaults to 4 hours.
- * @param sponsoredStoriesParams Optional - Configuration containing parameters used to get the spoc content.
* @param contentRecommendationsRefreshFrequency Optional - The interval at which to try and refresh
* content recommendations. Defaults to 4 hours.
* @param contentRecommendationsParams Optional - Configuration containing parameters used to fetch
@@ -48,12 +44,10 @@ class PocketStoriesConfig(
DEFAULT_REFRESH_INTERVAL,
DEFAULT_REFRESH_TIMEUNIT,
),
- val profile: Profile? = null,
val sponsoredStoriesRefreshFrequency: Frequency = Frequency(
DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL,
DEFAULT_SPONSORED_STORIES_REFRESH_TIMEUNIT,
),
- val sponsoredStoriesParams: PocketStoriesRequestConfig = PocketStoriesRequestConfig(),
val contentRecommendationsRefreshFrequency: Frequency = Frequency(
DEFAULT_CONTENT_RECOMMENDATIONS_REFRESH_INTERNAL,
DEFAULT_CONTENT_RECOMMENDATIONS_REFRESH_TIMEUNIT,
@@ -62,30 +56,6 @@ class PocketStoriesConfig(
val marsSponsoredContentsParams: MarsSpocsRequestConfig = MarsSpocsRequestConfig(),
)
-/**
- * Configuration for sponsored stories request indicating parameters used to get spoc content.
- *
- * @property siteId Optional - ID of the site parameter, should be used with care as it changes the
- * set of sponsored stories fetched from the server.
- * @property country Optional - Value of the country parameter, shall be used with care as it allows
- * overriding the IP location and receiving a set of sponsored stories not suited for the real location.
- * @property city Optional - Value of the city parameter, shall be used with care as it allows
- * overriding the IP location and receiving a set of sponsored stories not suited for the real location.
- */
-class PocketStoriesRequestConfig(
- val siteId: String = DEFAULT_SPONSORED_STORIES_SITE_ID,
- val country: String = "",
- val city: String = "",
-)
-
-/**
- * Sponsored stories configuration data.
- *
- * @param profileId Unique profile identifier which will be presented with sponsored stories.
- * @param appId Unique identifier of the application using this feature.
- */
-class Profile(val profileId: UUID, val appId: String)
-
/**
* Configuration for content recommendations request.
*
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
index f57099752b55e..46f3ec1723ead 100644
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
@@ -8,11 +8,9 @@ import android.content.Context
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.mars.SponsoredContentsUseCases
import mozilla.components.service.pocket.recommendations.ContentRecommendationsUseCases
-import mozilla.components.service.pocket.spocs.SpocsUseCases
import mozilla.components.service.pocket.stories.PocketStoriesUseCases
import mozilla.components.service.pocket.update.ContentRecommendationsRefreshScheduler
import mozilla.components.service.pocket.update.PocketStoriesRefreshScheduler
@@ -45,21 +43,6 @@ class PocketStoriesService(
fetchClient = pocketStoriesConfig.client,
)
- @VisibleForTesting
- internal var spocsUseCases = when (pocketStoriesConfig.profile) {
- null -> {
- logger.debug("Missing profile for sponsored stories")
- null
- }
- else -> SpocsUseCases(
- appContext = context,
- fetchClient = pocketStoriesConfig.client,
- profileId = pocketStoriesConfig.profile.profileId,
- appId = pocketStoriesConfig.profile.appId,
- sponsoredStoriesParams = pocketStoriesConfig.sponsoredStoriesParams,
- )
- }
-
@VisibleForTesting
internal var contentRecommendationsUseCases = ContentRecommendationsUseCases(
appContext = context,
@@ -112,34 +95,6 @@ class PocketStoriesService(
return storiesUseCases.getStories()
}
- /**
- * Fetch sponsored Pocket stories and refresh the locally persisted list.
- */
- suspend fun refreshSponsoredStories() {
- spocsUseCases?.refreshStories?.invoke()
- }
-
- /**
- * Get a list of Pocket sponsored stories based on the initial configuration.
- */
- suspend fun getSponsoredStories(): List {
- return spocsUseCases?.getStories?.invoke() ?: emptyList()
- }
-
- /**
- * Delete all stored user data used for downloading personalized sponsored stories.
- * This returns immediately but will handle the profile deletion in background.
- */
- fun deleteProfile() {
- val useCases = spocsUseCases
- if (useCases == null) {
- logger.warn("Cannot delete sponsored stories profile. Service has incomplete setup")
- return
- }
-
- GlobalDependencyProvider.SponsoredStories.initialize(useCases)
- }
-
/**
* Update how many times certain stories were shown to the user.
*
@@ -150,15 +105,6 @@ class PocketStoriesService(
storiesUseCases.updateTimesShown(updatedStories)
}
- /**
- * Persist locally that the sponsored Pocket stories containing the ids from [storiesShown]
- * were shown to the user.
- * This is safe to call with any ids, even ones for stories not currently persisted anymore.
- */
- suspend fun recordStoriesImpressions(storiesShown: List) {
- spocsUseCases?.recordImpression?.invoke(storiesShown)
- }
-
/**
* Starts a work request in the background to periodically update the list of content
* recommendations.
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt
index 7d5f2a8443abf..7a3d89e44ee16 100644
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt
@@ -7,9 +7,6 @@ package mozilla.components.service.pocket.ext
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.PocketStory.SponsoredContentCallbacks
import mozilla.components.service.pocket.PocketStory.SponsoredContentFrequencyCaps
@@ -18,8 +15,6 @@ import mozilla.components.service.pocket.mars.db.SponsoredContentEntity
import mozilla.components.service.pocket.recommendations.api.ContentRecommendationResponseItem
import mozilla.components.service.pocket.recommendations.db.ContentRecommendationEntity
import mozilla.components.service.pocket.recommendations.db.ContentRecommendationImpression
-import mozilla.components.service.pocket.spocs.api.ApiSpoc
-import mozilla.components.service.pocket.spocs.db.SpocEntity
import mozilla.components.service.pocket.stories.api.PocketApiStory
import mozilla.components.service.pocket.stories.db.PocketLocalStoryTimesShown
import mozilla.components.service.pocket.stories.db.PocketStoryEntity
@@ -68,48 +63,6 @@ internal fun PocketStoryEntity.toPocketRecommendedStory(): PocketRecommendedStor
internal fun PocketRecommendedStory.toPartialTimeShownUpdate(): PocketLocalStoryTimesShown =
PocketLocalStoryTimesShown(url, timesShown)
-/**
- * Map sponsored Pocket stories to the object type that we persist locally.
- */
-internal fun ApiSpoc.toLocalSpoc(): SpocEntity =
- SpocEntity(
- id = id,
- url = url,
- title = title,
- imageUrl = imageSrc,
- sponsor = sponsor,
- clickShim = shim.click,
- impressionShim = shim.impression,
- priority = priority,
- lifetimeCapCount = caps.lifetimeCount,
- flightCapCount = caps.flightCount,
- flightCapPeriod = caps.flightPeriod,
- )
-
-/**
- * Map Room entities to the object type that we expose to service clients.
- */
-internal fun SpocEntity.toPocketSponsoredStory(
- impressions: List = emptyList(),
-) = PocketSponsoredStory(
- id = id,
- title = title,
- url = url,
- imageUrl = imageUrl,
- sponsor = sponsor,
- shim = PocketSponsoredStoryShim(
- click = clickShim,
- impression = impressionShim,
- ),
- priority = priority,
- caps = PocketSponsoredStoryCaps(
- currentImpressions = impressions,
- lifetimeCount = lifetimeCapCount,
- flightCount = flightCapCount,
- flightPeriod = flightCapPeriod,
- ),
-)
-
/**
* Maps the sponsored content Room entities to the object type we expose to service clients.
*/
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt
index 6927182959cf8..80a8915d241da 100644
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt
@@ -4,53 +4,10 @@
package mozilla.components.service.pocket.ext
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.PocketStory.SponsoredContentFrequencyCaps
import java.util.concurrent.TimeUnit
-/**
- * Get a list of all story impressions (expressed in seconds from Epoch) in the period between
- * `now` down to [PocketSponsoredStoryCaps.flightPeriod].
- */
-fun PocketSponsoredStory.getCurrentFlightImpressions(): List {
- val now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
- return caps.currentImpressions.filter {
- now - it < caps.flightPeriod
- }
-}
-
-/**
- * Get if this story was already shown for the maximum number of times available in it's lifetime.
- */
-fun PocketSponsoredStory.hasLifetimeImpressionsLimitReached(): Boolean {
- return caps.currentImpressions.size >= caps.lifetimeCount
-}
-
-/**
- * Get if this story was already shown for the maximum number of times available in the period
- * specified by [PocketSponsoredStoryCaps.flightPeriod].
- */
-fun PocketSponsoredStory.hasFlightImpressionsLimitReached(): Boolean {
- return getCurrentFlightImpressions().size >= caps.flightCount
-}
-
-/**
- * Record a new impression at this instant time and get this story back with updated impressions details.
- * This only updates the in-memory data.
- *
- * It's recommended to use this method anytime a new impression needs to be recorded for a `PocketSponsoredStory`
- * to ensure values consistency.
- */
-fun PocketSponsoredStory.recordNewImpression(): PocketSponsoredStory {
- return this.copy(
- caps = caps.copy(
- currentImpressions = caps.currentImpressions + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
- ),
- )
-}
-
/**
* Returns a list of all sponsored content impressions (expressed in seconds from Epoch) in the
* period between `now` down to [SponsoredContentFrequencyCaps.flightPeriod].
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt
deleted file mode 100644
index 609b6ae935432..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs
-
-import android.content.Context
-import androidx.annotation.VisibleForTesting
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
-import mozilla.components.service.pocket.ext.toLocalSpoc
-import mozilla.components.service.pocket.ext.toPocketSponsoredStory
-import mozilla.components.service.pocket.spocs.api.ApiSpoc
-import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
-import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
-
-/**
- * Wrapper over our local database containing Spocs.
- * Allows for easy CRUD operations.
- */
-internal class SpocsRepository(context: Context) {
- private val database: Lazy = lazy { PocketRecommendationsDatabase.get(context) }
-
- @VisibleForTesting
- internal val spocsDao by lazy { database.value.spocsDao() }
-
- /**
- * Get the current locally persisted list of sponsored Pocket stories
- * complete with the list of all locally persisted impressions data.
- */
- suspend fun getAllSpocs(): List {
- val spocs = spocsDao.getAllSpocs()
- val impressions = spocsDao.getSpocsImpressions().groupBy { it.spocId }
-
- return spocs.map { spoc ->
- spoc.toPocketSponsoredStory(
- impressions[spoc.id]
- ?.map { impression -> impression.impressionDateInSeconds }
- ?: emptyList(),
- )
- }
- }
-
- /**
- * Delete all currently persisted sponsored Pocket stories.
- */
- suspend fun deleteAllSpocs() {
- spocsDao.deleteAllSpocs()
- }
-
- /**
- * Replace the current list of locally persisted sponsored Pocket stories.
- *
- * @param spocs The list of sponsored Pocket stories to persist locally.
- */
- suspend fun addSpocs(spocs: List) {
- spocsDao.cleanOldAndInsertNewSpocs(spocs.map { it.toLocalSpoc() })
- }
-
- /**
- * Add a new impression record for each of the spocs identified by the ids from [spocsShown].
- * Will ignore adding new entries if the intended spocs are not persisted locally anymore.
- * Recorded entries will automatically be cleaned when the spoc they target is deleted.
- *
- * @param spocsShown List of [PocketSponsoredStory.id] for which to record new impressions.
- */
- suspend fun recordImpressions(spocsShown: List) {
- spocsDao.recordImpressions(spocsShown.map { SpocImpressionEntity(it) })
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt
deleted file mode 100644
index d0f7e8fa754b9..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs
-
-import android.content.Context
-import androidx.annotation.VisibleForTesting
-import mozilla.components.concept.fetch.Client
-import mozilla.components.service.pocket.PocketStoriesRequestConfig
-import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
-import mozilla.components.service.pocket.spocs.api.SpocsEndpoint
-import mozilla.components.service.pocket.stories.api.PocketResponse.Failure
-import mozilla.components.service.pocket.stories.api.PocketResponse.Success
-import java.util.UUID
-
-/**
- * Possible actions regarding the list of sponsored stories.
- *
- * @param appContext Android Context. Prefer sending application context to limit the possibility of even small leaks.
- * @param fetchClient the HTTP client to use for network requests.
- * @param profileId Unique profile identifier used for downloading sponsored Pocket stories.
- * @param appId Unique app identifier used for downloading sponsored Pocket stories.
- * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
- */
-internal class SpocsUseCases(
- private val appContext: Context,
- private val fetchClient: Client,
- private val profileId: UUID,
- private val appId: String,
- private val sponsoredStoriesParams: PocketStoriesRequestConfig,
-) {
- /**
- * Download and persist an updated list of sponsored stories.
- */
- internal val refreshStories by lazy {
- RefreshSponsoredStories(appContext, fetchClient, profileId, appId)
- }
-
- /**
- * Get the list of available Pocket sponsored stories.
- */
- internal val getStories by lazy {
- GetSponsoredStories(appContext)
- }
-
- internal val recordImpression by lazy {
- RecordImpression(appContext)
- }
-
- /**
- * Delete all stored user data used for downloading sponsored stories.
- */
- internal val deleteProfile by lazy {
- DeleteProfile(appContext, fetchClient, profileId, appId)
- }
-
- /**
- * Allows for refreshing the list of Pocket sponsored stories we have cached.
- *
- * @param appContext Android Context. Prefer sending application context to limit the possibility
- * of even small leaks.
- * @param fetchClient the HTTP client to use for network requests.
- * @param profileId Unique profile identifier when using this feature.
- * @param appId Unique identifier of the application using this feature.
- * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
- */
- internal inner class RefreshSponsoredStories(
- @get:VisibleForTesting
- internal val appContext: Context = this@SpocsUseCases.appContext,
- @get:VisibleForTesting
- internal val fetchClient: Client = this@SpocsUseCases.fetchClient,
- @get:VisibleForTesting
- internal val profileId: UUID = this@SpocsUseCases.profileId,
- @get:VisibleForTesting
- internal val appId: String = this@SpocsUseCases.appId,
- @get:VisibleForTesting
- internal val sponsoredStoriesParams: PocketStoriesRequestConfig = this@SpocsUseCases.sponsoredStoriesParams,
- ) {
- /**
- * Do a full download from Pocket -> persist locally cycle for sponsored stories.
- */
- suspend operator fun invoke(): Boolean {
- val provider = getSpocsProvider(fetchClient, profileId, appId, sponsoredStoriesParams)
- val response = provider.getSponsoredStories()
-
- if (response is Success) {
- getSpocsRepository(appContext).addSpocs(response.data)
- return true
- }
-
- return false
- }
- }
-
- /**
- * Allows for querying the list of available Pocket sponsored stories.
- *
- * @param context [Context] used for various system interactions and libraries initializations.
-
- */
- internal inner class GetSponsoredStories(
- @get:VisibleForTesting
- internal val context: Context = this@SpocsUseCases.appContext,
- ) {
- /**
- * Do an internet query for a list of Pocket sponsored stories.
- */
- suspend operator fun invoke(): List {
- return getSpocsRepository(context).getAllSpocs()
- }
- }
-
- /**
- * Allows for atomically updating the [PocketRecommendedStory.timesShown] property of some recommended stories.
- *
- * @param context [Context] used for various system interactions and libraries initializations.
- */
- internal inner class RecordImpression(
- @get:VisibleForTesting
- internal val context: Context = this@SpocsUseCases.appContext,
- ) {
- /**
- * Update how many times certain stories were shown to the user.
- */
- suspend operator fun invoke(storiesShown: List) {
- if (storiesShown.isNotEmpty()) {
- getSpocsRepository(context).recordImpressions(storiesShown)
- }
- }
- }
-
- /**
- * Allows deleting all stored user data used for downloading sponsored stories.
- *
- * @param context [Context] used for various system interactions and libraries initializations.
- * @param fetchClient the HTTP client to use for network requests.
- * @param profileId Unique profile identifier previously used for downloading sponsored Pocket stories.
- * @param appId Unique app identifier previously used for downloading sponsored Pocket stories.
- * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
- */
- internal inner class DeleteProfile(
- @get:VisibleForTesting
- internal val context: Context = this@SpocsUseCases.appContext,
- @get:VisibleForTesting
- internal val fetchClient: Client = this@SpocsUseCases.fetchClient,
- @get:VisibleForTesting
- internal val profileId: UUID = this@SpocsUseCases.profileId,
- @get:VisibleForTesting
- internal val appId: String = this@SpocsUseCases.appId,
- @get:VisibleForTesting
- internal val sponsoredStoriesParams: PocketStoriesRequestConfig = this@SpocsUseCases.sponsoredStoriesParams,
- ) {
- /**
- * Delete all stored user data used for downloading personalized sponsored stories.
- */
- suspend operator fun invoke(): Boolean {
- val provider = getSpocsProvider(fetchClient, profileId, appId, sponsoredStoriesParams)
- return when (provider.deleteProfile()) {
- is Success -> {
- getSpocsRepository(context).deleteAllSpocs()
- true
- }
- is Failure -> {
- // Don't attempt to delete locally persisted stories to prevent mismatching issues
- // with profile deletion failing - applications still "showing it" but
- // with no sponsored articles to show.
- false
- }
- }
- }
- }
-
- @VisibleForTesting
- internal fun getSpocsRepository(context: Context) = SpocsRepository(context)
-
- @VisibleForTesting
- internal fun getSpocsProvider(
- client: Client,
- profileId: UUID,
- appId: String,
- sponsoredStoriesParams: PocketStoriesRequestConfig,
- ) =
- SpocsEndpoint.newInstance(client, profileId, appId, sponsoredStoriesParams)
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt
deleted file mode 100644
index 7f89df2ab72bd..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-/**
- * A Pocket sponsored as downloaded from the sponsored stories endpoint.
- *
- * @property id Unique id of this story.
- * @property title the title of the story.
- * @property url 3rd party url containing the original story.
- * @property imageSrc a url to a still image representing the story.
- * Contains a "resize" parameter in the form of "resize=w618-h310" allowing to get the image
- * with a specific resolution and the CENTER_CROP ScaleType.
- * @property sponsor 3rd party sponsor of this story, e.g. "NextAdvisor".
- * @property shim Unique identifiers for when the user interacts with this story.
- * @property priority Priority level in deciding which stories to be shown first.
- * A lowest number means a higher priority.
- * @property caps Story caps indented to control the maximum number of times the story should be shown.
- */
-internal data class ApiSpoc(
- val id: Int,
- val title: String,
- val url: String,
- val imageSrc: String,
- val sponsor: String,
- val shim: ApiSpocShim,
- val priority: Int,
- val caps: ApiSpocCaps,
-)
-
-/**
- * Sponsored story unique identifiers intended to be used in telemetry.
- *
- * @property click Unique identifier for when the sponsored story is clicked.
- * @property impression Unique identifier for when the user sees this sponsored story.
- */
-internal data class ApiSpocShim(
- val click: String,
- val impression: String,
-)
-
-/**
- * Sponsored story caps indented to control the maximum number of times the story should be shown.
- *
- * @property lifetimeCount Lifetime maximum number of times this story should be shown.
- * This is independent from the count based on [flightCount] and [flightPeriod] and must never be reset.
- * @property flightCount Maximum number of times this story should be shown in [flightPeriod].
- * @property flightPeriod Period expressed as a number of seconds in which this story should be shown
- * for at most [flightCount] times.
- * Any time the period comes to an end the [flightCount] count should be restarted.
- * Even if based on [flightCount] and [flightCount] this story can still be shown a couple more times
- * if [lifetimeCount] was met then the story should not be shown anymore.
- */
-internal data class ApiSpocCaps(
- val lifetimeCount: Int,
- val flightCount: Int,
- val flightPeriod: Int,
-)
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt
deleted file mode 100644
index a17bebd6ba62d..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-import androidx.annotation.VisibleForTesting
-import androidx.annotation.WorkerThread
-import mozilla.components.concept.fetch.Client
-import mozilla.components.service.pocket.PocketStoriesRequestConfig
-import mozilla.components.service.pocket.spocs.api.SpocsEndpoint.Companion.newInstance
-import mozilla.components.service.pocket.stories.api.PocketEndpoint.Companion.newInstance
-import mozilla.components.service.pocket.stories.api.PocketResponse
-import java.util.UUID
-
-/**
- * Makes requests to the sponsored stories API and returns the requested data.
- *
- * @see [newInstance] to retrieve an instance.
- */
-internal class SpocsEndpoint internal constructor(
- @get:VisibleForTesting internal val rawEndpoint: SpocsEndpointRaw,
- private val jsonParser: SpocsJSONParser,
-) : SpocsProvider {
-
- /**
- * Download a new list of sponsored Pocket stories.
- *
- * If the API returns unexpectedly formatted results, these entries will be omitted and the rest of the items are
- * returned.
- *
- * @return a [PocketResponse.Success] with the sponsored Pocket stories (list may be empty)
- * or [PocketResponse.Failure] if the request didn't complete successfully.
- */
- @WorkerThread
- override suspend fun getSponsoredStories(): PocketResponse> {
- val response = rawEndpoint.getSponsoredStories()
- val spocs = if (response.isNullOrBlank()) null else jsonParser.jsonToSpocs(response)
- return PocketResponse.wrap(spocs)
- }
-
- @WorkerThread
- override suspend fun deleteProfile(): PocketResponse {
- val response = rawEndpoint.deleteProfile()
- return PocketResponse.wrap(response)
- }
-
- companion object {
- /**
- * Returns a new instance of [SpocsEndpoint].
- *
- * @param client the HTTP client to use for network requests.
- * @param profileId Unique profile identifier which will be presented with sponsored stories.
- * @param appId Unique identifier of the application using this feature.
- * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
- */
- fun newInstance(
- client: Client,
- profileId: UUID,
- appId: String,
- sponsoredStoriesParams: PocketStoriesRequestConfig,
- ): SpocsEndpoint {
- val rawEndpoint = SpocsEndpointRaw.newInstance(client, profileId, appId, sponsoredStoriesParams)
- return SpocsEndpoint(rawEndpoint, SpocsJSONParser)
- }
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt
deleted file mode 100644
index 81bf2dd7e224c..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-import android.net.Uri
-import androidx.annotation.VisibleForTesting
-import androidx.annotation.WorkerThread
-import mozilla.components.concept.fetch.Client
-import mozilla.components.concept.fetch.MutableHeaders
-import mozilla.components.concept.fetch.Request
-import mozilla.components.concept.fetch.Request.Body
-import mozilla.components.concept.fetch.Request.Method
-import mozilla.components.concept.fetch.Response
-import mozilla.components.concept.fetch.isSuccess
-import mozilla.components.service.pocket.BuildConfig
-import mozilla.components.service.pocket.PocketStoriesRequestConfig
-import mozilla.components.service.pocket.logger
-import mozilla.components.service.pocket.spocs.api.SpocsEndpointRaw.Companion.newInstance
-import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion.newInstance
-import mozilla.components.support.base.ext.fetchBodyOrNull
-import org.json.JSONObject
-import java.io.IOException
-import java.util.UUID
-
-private const val SPOCS_ENDPOINT_DEV_BASE_URL = "https://spocs.getpocket.dev/"
-private const val SPOCS_ENDPOINT_PROD_BASE_URL = "https://spocs.getpocket.com/"
-private const val SPOCS_ENDPOINT_DOWNLOAD_SPOCS_PATH = "spocs"
-private const val SPOCS_ENDPOINT_DELETE_PROFILE_PATH = "user"
-private const val SPOCS_PROXY_VERSION_KEY = "version"
-private const val SPOCS_PROXY_VERSION_VALUE = 2
-private const val SPOCS_PROXY_PROFILE_KEY = "pocket_id"
-private const val SPOCS_PROXY_APP_KEY = "consumer_key"
-private const val SPOCS_PROXY_SITE_KEY = "site"
-private const val SPOCS_PROXY_COUNTRY_KEY = "country"
-private const val SPOCS_PROXY_CITY_KEY = "city"
-
-/**
- * Makes requests to the Pocket endpoint and returns the raw JSON data.
- *
- * @see [SpocsEndpoint], which wraps this to make it more practical.
- * @see [newInstance] to retrieve an instance.
- */
-internal class SpocsEndpointRaw internal constructor(
- @get:VisibleForTesting internal val client: Client,
- @get:VisibleForTesting internal val profileId: UUID,
- @get:VisibleForTesting internal val appId: String,
- @get:VisibleForTesting internal val sponsoredStoriesParams: PocketStoriesRequestConfig,
-) {
- /**
- * Gets the current sponsored stories recommendations from the Pocket server.
- *
- * @return The stories recommendations as a raw JSON string or null on error.
- */
- @WorkerThread
- fun getSponsoredStories(): String? {
- val url = Uri.Builder()
- .encodedPath(baseUrl + SPOCS_ENDPOINT_DOWNLOAD_SPOCS_PATH)
- if (sponsoredStoriesParams.siteId.isNotBlank()) {
- url.appendQueryParameter(SPOCS_PROXY_SITE_KEY, sponsoredStoriesParams.siteId)
- }
- if (sponsoredStoriesParams.country.isNotBlank()) {
- url.appendQueryParameter(SPOCS_PROXY_COUNTRY_KEY, sponsoredStoriesParams.country)
- }
- if (sponsoredStoriesParams.city.isNotBlank()) {
- url.appendQueryParameter(SPOCS_PROXY_CITY_KEY, sponsoredStoriesParams.city)
- }
- url.build()
-
- val request = Request(
- url = url.toString(),
- method = Method.POST,
- headers = getRequestHeaders(),
- body = getDownloadStoriesRequestBody(),
- conservative = true,
- )
- return client.fetchBodyOrNull(request)
- }
-
- /**
- * Request to delete all data stored on server about [profileId].
- *
- * @return [Boolean] indicating whether the delete operation was successful or not.
- */
- @WorkerThread
- fun deleteProfile(): Boolean {
- val url = Uri.Builder()
- .encodedPath(baseUrl + SPOCS_ENDPOINT_DELETE_PROFILE_PATH)
- if (sponsoredStoriesParams.siteId.isNotBlank()) {
- url.appendQueryParameter(SPOCS_PROXY_SITE_KEY, sponsoredStoriesParams.siteId)
- }
- if (sponsoredStoriesParams.country.isNotBlank()) {
- url.appendQueryParameter(SPOCS_PROXY_COUNTRY_KEY, sponsoredStoriesParams.country)
- }
- if (sponsoredStoriesParams.city.isNotBlank()) {
- url.appendQueryParameter(SPOCS_PROXY_CITY_KEY, sponsoredStoriesParams.city)
- }
- url.build()
-
- val request = Request(
- url = url.toString(),
- method = Method.DELETE,
- headers = getRequestHeaders(),
- body = getDeleteProfileRequestBody(),
- conservative = true,
- )
-
- val response: Response? = try {
- client.fetch(request)
- } catch (e: IOException) {
- logger.debug("Network error", e)
- null
- }
-
- response?.close()
- return response?.isSuccess ?: false
- }
-
- private fun getRequestHeaders() = MutableHeaders(
- "Content-Type" to "application/json; charset=UTF-8",
- "Accept" to "*/*",
- )
-
- private fun getDownloadStoriesRequestBody(): Body {
- val params = mapOf(
- SPOCS_PROXY_VERSION_KEY to SPOCS_PROXY_VERSION_VALUE,
- SPOCS_PROXY_PROFILE_KEY to profileId.toString(),
- SPOCS_PROXY_APP_KEY to appId,
- )
-
- return Body(JSONObject(params).toString().byteInputStream())
- }
-
- private fun getDeleteProfileRequestBody(): Body {
- val params = mapOf(
- SPOCS_PROXY_PROFILE_KEY to profileId.toString(),
- )
-
- return Body(JSONObject(params).toString().byteInputStream())
- }
-
- companion object {
- /**
- * Returns a new instance of [SpocsEndpointRaw].
- *
- * @param client HTTP client to use for network requests.
- * @param profileId Unique profile identifier which will be presented with sponsored stories.
- * @param appId Unique identifier of the application using this feature.
- * @param sponsoredStoriesParams Configuration containing parameters used to get the spoc content.
- */
- fun newInstance(
- client: Client,
- profileId: UUID,
- appId: String,
- sponsoredStoriesParams: PocketStoriesRequestConfig,
- ): SpocsEndpointRaw {
- return SpocsEndpointRaw(client, profileId, appId, sponsoredStoriesParams)
- }
-
- /**
- * Convenience for checking whether the current build is a debug build and overwriting this in tests.
- */
- @VisibleForTesting
- internal var isDebugBuild = BuildConfig.DEBUG
-
- /**
- * Get the base url for sponsored stories specific to development or production.
- */
- @VisibleForTesting
- internal val baseUrl
- get() = if (isDebugBuild) {
- SPOCS_ENDPOINT_DEV_BASE_URL
- } else {
- SPOCS_ENDPOINT_PROD_BASE_URL
- }
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt
deleted file mode 100644
index b8fe1ea4e350f..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-import androidx.annotation.VisibleForTesting
-import mozilla.components.service.pocket.logger
-import mozilla.components.support.ktx.android.org.json.mapNotNull
-import org.json.JSONArray
-import org.json.JSONException
-import org.json.JSONObject
-
-@VisibleForTesting
-internal const val KEY_ARRAY_SPOCS = "spocs"
-
-@VisibleForTesting
-internal const val JSON_SPOC_SHIMS_KEY = "shim"
-
-@VisibleForTesting
-internal const val JSON_SPOC_CAPS_KEY = "caps"
-
-@VisibleForTesting
-internal const val JSON_SPOC_CAPS_LIFETIME_KEY = "lifetime"
-
-@VisibleForTesting
-internal const val JSON_SPOC_CAPS_FLIGHT_KEY = "campaign"
-
-@VisibleForTesting
-internal const val JSON_SPOC_CAPS_FLIGHT_COUNT_KEY = "count"
-
-@VisibleForTesting
-internal const val JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY = "period"
-private const val JSON_SPOC_ID_KEY = "id"
-private const val JSON_SPOC_TITLE_KEY = "title"
-private const val JSON_SPOC_SPONSOR_KEY = "sponsor"
-private const val JSON_SPOC_URL_KEY = "url"
-private const val JSON_SPOC_IMAGE_SRC_KEY = "image_src"
-private const val JSON_SPOC_SHIM_CLICK_KEY = "click"
-private const val JSON_SPOC_SHIM_IMPRESSION_KEY = "impression"
-private const val JSON_SPOC_PRIORITY = "priority"
-
-/**
- * Holds functions that parse the JSON returned by the Pocket API and converts them to more usable Kotlin types.
- */
-internal object SpocsJSONParser {
- /**
- * @return The stories, removing entries that are invalid, or null on error; the list will never be empty.
- */
- fun jsonToSpocs(json: String): List? = try {
- val rawJSON = JSONObject(json)
- val spocsJSON = rawJSON.getJSONArray(KEY_ARRAY_SPOCS)
- val spocs = spocsJSON.mapNotNull(JSONArray::getJSONObject) { jsonToSpoc(it) }
-
- // We return null, rather than the empty list, because devs might forget to check an empty list.
- spocs.ifEmpty { null }
- } catch (e: JSONException) {
- logger.warn("invalid JSON from the SPOCS endpoint", e)
- null
- }
-
- private fun jsonToSpoc(json: JSONObject): ApiSpoc? = try {
- ApiSpoc(
- id = json.getInt(JSON_SPOC_ID_KEY),
- title = json.getString(JSON_SPOC_TITLE_KEY),
- sponsor = json.getString(JSON_SPOC_SPONSOR_KEY),
- url = json.getString(JSON_SPOC_URL_KEY),
- imageSrc = json.getString(JSON_SPOC_IMAGE_SRC_KEY),
- shim = jsonToShim(json.getJSONObject(JSON_SPOC_SHIMS_KEY)),
- priority = json.getInt(JSON_SPOC_PRIORITY),
- caps = jsonToCaps(json.getJSONObject(JSON_SPOC_CAPS_KEY)),
- )
- } catch (e: JSONException) {
- null
- }
-
- private fun jsonToShim(json: JSONObject) = ApiSpocShim(
- click = json.getString(JSON_SPOC_SHIM_CLICK_KEY),
- impression = json.getString(JSON_SPOC_SHIM_IMPRESSION_KEY),
- )
-
- private fun jsonToCaps(json: JSONObject): ApiSpocCaps {
- val flightCaps = json.getJSONObject(JSON_SPOC_CAPS_FLIGHT_KEY)
-
- return ApiSpocCaps(
- lifetimeCount = json.getInt(JSON_SPOC_CAPS_LIFETIME_KEY),
- flightCount = flightCaps.getInt(JSON_SPOC_CAPS_FLIGHT_COUNT_KEY),
- flightPeriod = flightCaps.getInt(JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY),
- )
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt
deleted file mode 100644
index dcb5819cd97e0..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-import mozilla.components.service.pocket.stories.api.PocketResponse
-
-/**
- * All possible operations related to SPocs - Sponsored Pocket stories.
- */
-internal interface SpocsProvider {
- /**
- * Download new sponsored stories.
- *
- * @return [PocketResponse.Success] containing a list of sponsored stories or
- * [PocketResponse.Failure] if the request didn't complete successfully.
- */
- suspend fun getSponsoredStories(): PocketResponse>
-
- /**
- * Delete all data associated with [profileId].
- *
- * @return [PocketResponse.Success] if the request completed successfully, [PocketResponse.Failure] otherwise.
- */
- suspend fun deleteProfile(): PocketResponse
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt
deleted file mode 100644
index fadfec87c3f0e..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.db
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import androidx.room.Transaction
-import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
-import java.util.concurrent.TimeUnit
-
-@Dao
-internal interface SpocsDao {
- @Transaction
- suspend fun cleanOldAndInsertNewSpocs(spocs: List) {
- val newSpocs = spocs.map { it.id }
- val oldStoriesToDelete = getAllSpocs()
- .filterNot { newSpocs.contains(it.id) }
-
- deleteSpocs(oldStoriesToDelete)
- insertSpocs(spocs)
- }
-
- @Insert(onConflict = OnConflictStrategy.REPLACE) // Maybe some details changed
- suspend fun insertSpocs(stories: List)
-
- @Transaction
- suspend fun recordImpressions(stories: List) {
- stories.forEach {
- recordImpression(it.spocId, it.impressionDateInSeconds)
- }
- }
-
- /**
- * INSERT OR IGNORE method needed to prevent against "FOREIGN KEY constraint failed" exceptions
- * if clients try to insert new impressions spocs not existing anymore in the database in cases where
- * a different list of spocs were downloaded but the client operates with stale in-memory data.
- *
- * @param targetSpocId The `id` of the [SpocEntity] to add a new impression for.
- * A new impression will be persisted only if a story with the indicated [targetSpocId] currently exists.
- * @param targetImpressionDateInSeconds The timestamp expressed in seconds from Epoch for this impression.
- * Defaults to the current time expressed in seconds as get from `System.currentTimeMillis / 1000`.
- */
- @Query(
- "WITH newImpression(spocId, impressionDateInSeconds) AS (VALUES" +
- "(:targetSpocId, :targetImpressionDateInSeconds)" +
- ")" +
- "INSERT INTO spocs_impressions(spocId, impressionDateInSeconds) " +
- "SELECT impression.spocId, impression.impressionDateInSeconds " +
- "FROM newImpression impression " +
- "WHERE EXISTS (SELECT 1 FROM spocs spoc WHERE spoc.id = impression.spocId)",
- )
- suspend fun recordImpression(
- targetSpocId: Int,
- targetImpressionDateInSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
- )
-
- @Query("DELETE FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}")
- suspend fun deleteAllSpocs()
-
- @Delete
- suspend fun deleteSpocs(stories: List)
-
- @Query("SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}")
- suspend fun getAllSpocs(): List
-
- @Query("SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}")
- suspend fun getSpocsImpressions(): List
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
index 5a3e4f0efee49..8285ee57de69f 100644
--- a/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
+++ b/mobile/android/android-components/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
@@ -12,7 +12,6 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import mozilla.components.service.pocket.spocs.db.SpocEntity
import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
-import mozilla.components.service.pocket.spocs.db.SpocsDao
/**
* Internal database for storing Pocket items.
@@ -27,7 +26,6 @@ import mozilla.components.service.pocket.spocs.db.SpocsDao
)
internal abstract class PocketRecommendationsDatabase : RoomDatabase() {
abstract fun pocketRecommendationsDao(): PocketRecommendationsDao
- abstract fun spocsDao(): SpocsDao
companion object {
private const val DATABASE_NAME = "pocket_recommendations"
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt
index 32fe5f760ae72..599f166bf3b7b 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt
@@ -6,7 +6,6 @@ package mozilla.components.service.pocket
import mozilla.components.service.pocket.mars.SponsoredContentsUseCases
import mozilla.components.service.pocket.recommendations.ContentRecommendationsUseCases
-import mozilla.components.service.pocket.spocs.SpocsUseCases
import mozilla.components.service.pocket.stories.PocketStoriesUseCases
import mozilla.components.support.test.mock
import org.junit.Assert.assertNull
@@ -32,24 +31,6 @@ class GlobalDependencyProviderTest {
assertNull(GlobalDependencyProvider.RecommendedStories.useCases)
}
- @Test
- fun `GIVEN SponsoredStories WHEN initializing THEN store the provided arguments`() {
- val useCases: SpocsUseCases = mock()
-
- GlobalDependencyProvider.SponsoredStories.initialize(useCases)
-
- assertSame(useCases, GlobalDependencyProvider.SponsoredStories.useCases)
- }
-
- @Test
- fun `GIVEN SponsoredStories WHEN resetting THEN clear all current state`() {
- GlobalDependencyProvider.SponsoredStories.initialize(mock())
-
- GlobalDependencyProvider.SponsoredStories.reset()
-
- assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
- }
-
@Test
fun `GIVEN ContentRecommendations WHEN initializing THEN store the provided arguments`() {
val useCases: ContentRecommendationsUseCases = mock()
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
index edac0a4a81112..9cb9d7b3e0a35 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
@@ -8,7 +8,6 @@ import mozilla.components.service.pocket.helpers.assertClassVisibility
import mozilla.components.support.base.worker.Frequency
import mozilla.components.support.test.mock
import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.reflect.KVisibility
@@ -52,27 +51,11 @@ class PocketStoriesConfigTest {
assertEquals(defaultFrequency.repeatIntervalTimeUnit, config.contentRecommendationsRefreshFrequency.repeatIntervalTimeUnit)
}
- @Test
- fun `WHEN instantiating a PocketStoriesConfig THEN profile is by default null`() {
- val config = PocketStoriesConfig(mock())
-
- assertNull(config.profile)
- }
-
@Test
fun `GIVEN a Frequency THEN its visibility is internal`() {
assertClassVisibility(Frequency::class, KVisibility.PUBLIC)
}
- @Test
- fun `WHEN instantiating a PocketStoriesConfig THEN sponsoredStoriesParams default value is used`() {
- val config = PocketStoriesConfig(mock())
-
- assertEquals(DEFAULT_SPONSORED_STORIES_SITE_ID, config.sponsoredStoriesParams.siteId)
- assertEquals("", config.sponsoredStoriesParams.country)
- assertEquals("", config.sponsoredStoriesParams.city)
- }
-
@Test
fun `WHEN instantiating a PocketStoriesConfig THEN contentRecommendationsParams default value is used`() {
val config = PocketStoriesConfig(mock())
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
index d3006dcbb8848..273532f3e8fbd 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
@@ -6,10 +6,8 @@ package mozilla.components.service.pocket
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.test.runTest
-import mozilla.components.concept.fetch.Client
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.helpers.assertConstructorsVisibility
import mozilla.components.service.pocket.mars.SponsoredContentsUseCases
@@ -18,9 +16,6 @@ import mozilla.components.service.pocket.mars.SponsoredContentsUseCases.RecordIm
import mozilla.components.service.pocket.recommendations.ContentRecommendationsUseCases
import mozilla.components.service.pocket.recommendations.ContentRecommendationsUseCases.GetContentRecommendations
import mozilla.components.service.pocket.recommendations.ContentRecommendationsUseCases.UpdateRecommendationsImpressions
-import mozilla.components.service.pocket.spocs.SpocsUseCases
-import mozilla.components.service.pocket.spocs.SpocsUseCases.GetSponsoredStories
-import mozilla.components.service.pocket.spocs.SpocsUseCases.RecordImpression
import mozilla.components.service.pocket.stories.PocketStoriesUseCases
import mozilla.components.service.pocket.stories.PocketStoriesUseCases.GetPocketStories
import mozilla.components.service.pocket.stories.PocketStoriesUseCases.UpdateStoriesTimesShown
@@ -31,18 +26,15 @@ import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.verify
-import java.util.UUID
import kotlin.reflect.KVisibility
@RunWith(AndroidJUnit4::class)
class PocketStoriesServiceTest {
private val storiesUseCases: PocketStoriesUseCases = mock()
- private val spocsUseCases: SpocsUseCases = mock()
private val contentRecommendationsUseCases: ContentRecommendationsUseCases = mock()
private val sponsoredContentsUseCases: SponsoredContentsUseCases = mock()
private val service = PocketStoriesService(testContext, PocketStoriesConfig(mock())).also {
@@ -50,7 +42,6 @@ class PocketStoriesServiceTest {
it.contentRecommendationsRefreshScheduler = mock()
it.sponsoredContentsRefreshScheduler = mock()
it.storiesUseCases = storiesUseCases
- it.spocsUseCases = spocsUseCases
it.contentRecommendationsUseCases = contentRecommendationsUseCases
it.sponsoredContentsUseCases = sponsoredContentsUseCases
}
@@ -58,7 +49,6 @@ class PocketStoriesServiceTest {
@After
fun teardown() {
GlobalDependencyProvider.ContentRecommendations.reset()
- GlobalDependencyProvider.SponsoredStories.reset()
GlobalDependencyProvider.RecommendedStories.reset()
GlobalDependencyProvider.SponsoredContents.reset()
}
@@ -84,16 +74,6 @@ class PocketStoriesServiceTest {
assertNull(GlobalDependencyProvider.RecommendedStories.useCases)
}
- @Test
- fun `WHEN called to refresh locally saved sponsored stories THEN refresh usecase is invoked`() = runTest {
- val refreshStories: SpocsUseCases.RefreshSponsoredStories = mock()
- doReturn(refreshStories).`when`(spocsUseCases).refreshStories
-
- service.refreshSponsoredStories()
-
- verify(refreshStories).invoke()
- }
-
@Test
fun `GIVEN PocketStoriesService WHEN getStories THEN stories useCases should return`() = runTest {
val stories = listOf(mock())
@@ -117,66 +97,6 @@ class PocketStoriesServiceTest {
verify(updateTimesShownUseCase).invoke(stories)
}
- @Test
- fun `GIVEN PocketStoriesService WHEN getSponsoredStories THEN delegate to spocs useCases`() = runTest {
- val noProfileResponse = service.getSponsoredStories()
- assertTrue(noProfileResponse.isEmpty())
-
- val stories = listOf(mock())
- val getStoriesUseCase: GetSponsoredStories = mock()
- doReturn(stories).`when`(getStoriesUseCase).invoke()
- doReturn(getStoriesUseCase).`when`(spocsUseCases).getStories
- val existingProfileResponse = service.getSponsoredStories()
- assertEquals(stories, existingProfileResponse)
- }
-
- @Test
- fun `GIVEN PocketStoriesService is initialized with a valid profile WHEN called to delete profile THEN persist dependencies, cancel stories refresh and schedule profile deletion`() {
- val client: Client = mock()
- val profileId = UUID.randomUUID()
- val appId = "test"
- val service = PocketStoriesService(
- context = testContext,
- pocketStoriesConfig = PocketStoriesConfig(
- client = client,
- profile = Profile(
- profileId = profileId,
- appId = appId,
- ),
- ),
- )
-
- service.deleteProfile()
-
- assertNotNull(GlobalDependencyProvider.SponsoredStories.useCases)
- }
-
- @Test
- fun `GIVEN PocketStoriesService is initialized with an invalid profile WHEN called to delete profile THEN don't schedule profile deletion and don't persist dependencies`() {
- val service = PocketStoriesService(
- context = testContext,
- pocketStoriesConfig = PocketStoriesConfig(
- client = mock(),
- profile = null,
- ),
- )
-
- service.deleteProfile()
-
- assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
- }
-
- @Test
- fun `GIVEN PocketStoriesService WHEN recordStoriesImpressions THEN delegate to spocs useCases`() = runTest {
- val recordImpressionsUseCase: RecordImpression = mock()
- doReturn(recordImpressionsUseCase).`when`(spocsUseCases).recordImpression
- val storiesIds = listOf(22, 33)
-
- service.recordStoriesImpressions(storiesIds)
-
- verify(recordImpressionsUseCase).invoke(storiesIds)
- }
-
@Test
fun `WHEN start periodic content recommendations refresh is invoked THEN schedule content recommendations refreshes`() {
service.startPeriodicContentRecommendationsRefresh()
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
index ac8a53aebb5a4..0bc5e6288be28 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
@@ -73,45 +73,6 @@ class MappersKtTest {
assertSame(story.timesShown, result.timesShown)
}
- @Test
- fun `GIVEN a spoc downloaded from Internet WHEN it is converted to a local spoc THEN a one to one mapping is made`() {
- val apiStory = PocketTestResources.apiExpectedPocketSpocs[0]
-
- val result = apiStory.toLocalSpoc()
-
- assertEquals(apiStory.id, result.id)
- assertSame(apiStory.title, result.title)
- assertSame(apiStory.url, result.url)
- assertSame(apiStory.imageSrc, result.imageUrl)
- assertSame(apiStory.sponsor, result.sponsor)
- assertSame(apiStory.shim.click, result.clickShim)
- assertSame(apiStory.shim.impression, result.impressionShim)
- assertEquals(apiStory.priority, result.priority)
- assertEquals(apiStory.caps.lifetimeCount, result.lifetimeCapCount)
- assertEquals(apiStory.caps.flightCount, result.flightCapCount)
- assertEquals(apiStory.caps.flightPeriod, result.flightCapPeriod)
- }
-
- @Test
- fun `GIVEN a local spoc WHEN it is converted to be exposed to clients THEN a one to one mapping is made`() {
- val localStory = PocketTestResources.dbExpectedPocketSpoc
-
- val result = localStory.toPocketSponsoredStory()
-
- assertEquals(localStory.id, result.id)
- assertSame(localStory.title, result.title)
- assertSame(localStory.url, result.url)
- assertSame(localStory.imageUrl, result.imageUrl)
- assertSame(localStory.sponsor, result.sponsor)
- assertSame(localStory.clickShim, result.shim.click)
- assertSame(localStory.impressionShim, result.shim.impression)
- assertEquals(localStory.priority, result.priority)
- assertEquals(localStory.lifetimeCapCount, result.caps.lifetimeCount)
- assertEquals(localStory.flightCapCount, result.caps.flightCount)
- assertEquals(localStory.flightCapPeriod, result.caps.flightPeriod)
- assertTrue(result.caps.currentImpressions.isEmpty())
- }
-
@Test
fun `GIVEN a ContentRecommendationEntity WHEN it is converted to be exposed to clients THEN a one to one mapping is made`() {
val recommendation = PocketTestResources.contentRecommendationEntity
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt
index a1b3ff7a36da8..c5e228dc36432 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt
@@ -4,8 +4,6 @@
package mozilla.components.service.pocket.ext
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.PocketStory.SponsoredContentFrequencyCaps
import mozilla.components.service.pocket.helpers.PocketTestResources
@@ -28,113 +26,6 @@ class PocketStoryKtTest {
flightImpression2,
)
- @Test
- fun `GIVEN sponsored story impressions recorded WHEN asking for the current flight impression THEN return all impressions in flight period`() {
- val storyCaps = PocketSponsoredStoryCaps(
- currentImpressions = currentImpressions,
- lifetimeCount = 10,
- flightCount = 5,
- flightPeriod = flightPeriod,
- )
- val story: PocketSponsoredStory = mock()
- doReturn(storyCaps).`when`(story).caps
-
- val result = story.getCurrentFlightImpressions()
-
- assertEquals(listOf(flightImpression1, flightImpression2), result)
- }
-
- @Test
- fun `GIVEN sponsored story impressions recorded WHEN asking if lifetime impressions reached THEN return false if not`() {
- val storyCaps = PocketSponsoredStoryCaps(
- currentImpressions = currentImpressions,
- lifetimeCount = 10,
- flightCount = 5,
- flightPeriod = flightPeriod,
- )
- val story: PocketSponsoredStory = mock()
- doReturn(storyCaps).`when`(story).caps
-
- val result = story.hasLifetimeImpressionsLimitReached()
-
- assertFalse(result)
- }
-
- @Test
- fun `GIVEN sponsored story impressions recorded WHEN asking if lifetime impressions reached THEN return true if so`() {
- val storyCaps = PocketSponsoredStoryCaps(
- currentImpressions = currentImpressions,
- lifetimeCount = 3,
- flightCount = 3,
- flightPeriod = flightPeriod,
- )
- val story: PocketSponsoredStory = mock()
- doReturn(storyCaps).`when`(story).caps
-
- val result = story.hasLifetimeImpressionsLimitReached()
-
- assertTrue(result)
- }
-
- @Test
- fun `GIVEN sponsored story impressions recorded WHEN asking if flight impressions reached THEN return false if not`() {
- val storyCaps = PocketSponsoredStoryCaps(
- currentImpressions = currentImpressions,
- lifetimeCount = 10,
- flightCount = 5,
- flightPeriod = flightPeriod,
- )
- val story: PocketSponsoredStory = mock()
- doReturn(storyCaps).`when`(story).caps
-
- val result = story.hasFlightImpressionsLimitReached()
-
- assertFalse(result)
- }
-
- @Test
- fun `GIVEN sponsored story impressions recorded WHEN asking if flight impressions reached THEN return true if so`() {
- val storyCaps = PocketSponsoredStoryCaps(
- currentImpressions = currentImpressions,
- lifetimeCount = 3,
- flightCount = 2,
- flightPeriod = flightPeriod,
- )
- val story: PocketSponsoredStory = mock()
- doReturn(storyCaps).`when`(story).caps
-
- val result = story.hasFlightImpressionsLimitReached()
-
- assertTrue(result)
- }
-
- @Test
- fun `GIVEN a sponsored story WHEN recording a new impression THEN update the same story to contain a new impression recorded in seconds`() {
- val story = PocketTestResources.dbExpectedPocketSpoc.toPocketSponsoredStory(currentImpressions)
-
- assertEquals(3, story.caps.currentImpressions.size)
- val result = story.recordNewImpression()
-
- assertEquals(story.id, result.id)
- assertSame(story.title, result.title)
- assertSame(story.url, result.url)
- assertSame(story.imageUrl, result.imageUrl)
- assertSame(story.sponsor, result.sponsor)
- assertSame(story.shim, result.shim)
- assertEquals(story.priority, result.priority)
- assertEquals(story.caps.lifetimeCount, result.caps.lifetimeCount)
- assertEquals(story.caps.flightCount, result.caps.flightCount)
- assertEquals(story.caps.flightPeriod, result.caps.flightPeriod)
-
- assertEquals(4, result.caps.currentImpressions.size)
- assertEquals(currentImpressions, result.caps.currentImpressions.take(3))
- // Check if a new impression has been added for around this current time.
- assertTrue(
- LongRange(nowInSeconds - 5, nowInSeconds + 5)
- .contains(result.caps.currentImpressions[3]),
- )
- }
-
@Test
fun `GIVEN sponsored content impressions are recorded WHEN asking for the current flight impressions THEN return all impressions in the flight period`() {
val frequencyCaps = SponsoredContentFrequencyCaps(
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
index 7dc62a069b2b9..8354aac4dc6a8 100644
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
+++ b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
@@ -18,9 +18,6 @@ import mozilla.components.service.pocket.mars.db.SponsoredContentEntity
import mozilla.components.service.pocket.recommendations.api.ContentRecommendationResponseItem
import mozilla.components.service.pocket.recommendations.api.ContentRecommendationsResponse
import mozilla.components.service.pocket.recommendations.db.ContentRecommendationEntity
-import mozilla.components.service.pocket.spocs.api.ApiSpoc
-import mozilla.components.service.pocket.spocs.api.ApiSpocCaps
-import mozilla.components.service.pocket.spocs.api.ApiSpocShim
import mozilla.components.service.pocket.spocs.db.SpocEntity
import mozilla.components.service.pocket.stories.api.PocketApiStory
import mozilla.components.service.pocket.stories.db.PocketStoryEntity
@@ -110,60 +107,6 @@ internal object PocketTestResources {
),
)
- val apiExpectedPocketSpocs: List = listOf(
- ApiSpoc(
- id = 193815086,
- title = "Eating Keto Has Never Been So Easy With Green Chef",
- url = "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
- imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
- sponsor = "Green Chef",
- shim = ApiSpocShim(
- click = "193815086ClickShim",
- impression = "193815086ImpressionShim",
- ),
- priority = 3,
- caps = ApiSpocCaps(
- lifetimeCount = 50,
- flightPeriod = 86400,
- flightCount = 10,
- ),
- ),
- ApiSpoc(
- id = 177986195,
- title = "This Leading Cash Back Card Is a Slam Dunk if You Want a One-Card Wallet",
- url = "https://www.fool.com/the-ascent/credit-cards/landing/discover-it-cash-back-review-v2-csr/?utm_site=theascent&utm_campaign=ta-cc-co-pocket-discb-04012022-5-na-firefox&utm_medium=cpc&utm_source=pocket",
- imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/359f56a5423c4926ab3aa148e448d839.webp&resize=w618-h310",
- sponsor = "The Ascent",
- shim = ApiSpocShim(
- click = "177986195ClickShim",
- impression = "177986195ImpressionShim",
- ),
- priority = 2,
- caps = ApiSpocCaps(
- lifetimeCount = 50,
- flightPeriod = 86400,
- flightCount = 10,
- ),
- ),
- ApiSpoc(
- id = 192560056,
- title = "The Incredible Lawn Hack That Can Make Your Neighbors Green With Envy Over Your Lawn",
- url = "https://go.lawnbuddy.org/zf/50/7673?campaign=SUN_Pocket2022&creative=SUN_LawnCompare4-TheIncredibleLawnHackThatCanMakeYourNeighborsGreenWithEnvyOverYourLawn-WithoutSpendingAFortuneOnNewGrassAndWithoutBreakingASweat-20220420",
- imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/ce16302e184342cda0619c08b7604c9c.jpg&resize=w618-h310",
- sponsor = "Sunday",
- shim = ApiSpocShim(
- click = "192560056ClickShim",
- impression = "192560056ImpressionShim",
- ),
- priority = 1,
- caps = ApiSpocCaps(
- lifetimeCount = 50,
- flightPeriod = 86400,
- flightCount = 10,
- ),
- ),
- )
-
val dbExpectedPocketStory = PocketStoryEntity(
title = "How to Get Rid of Black Mold Naturally",
url = "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt
deleted file mode 100644
index 5678bf51c7564..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runTest
-import mozilla.components.service.pocket.ext.toLocalSpoc
-import mozilla.components.service.pocket.helpers.PocketTestResources
-import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
-import mozilla.components.service.pocket.spocs.db.SpocsDao
-import mozilla.components.support.test.argumentCaptor
-import mozilla.components.support.test.robolectric.testContext
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertSame
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
-
-@RunWith(AndroidJUnit4::class)
-class SpocsRepositoryTest {
- private val spocsRepo = spy(SpocsRepository(testContext))
- private val dao = mock(SpocsDao::class.java)
-
- @Before
- fun setUp() {
- doReturn(dao).`when`(spocsRepo).spocsDao
- }
-
- @Test
- fun `GIVEN SpocsRepository WHEN asking for all spocs THEN return db entities mapped to domain type`() = runTest {
- val spoc = PocketTestResources.dbExpectedPocketSpoc
- val impressions = listOf(
- SpocImpressionEntity(spoc.id),
- SpocImpressionEntity(333),
- SpocImpressionEntity(spoc.id),
- )
- doReturn(listOf(spoc)).`when`(dao).getAllSpocs()
- doReturn(impressions).`when`(dao).getSpocsImpressions()
-
- val result = spocsRepo.getAllSpocs()
-
- verify(dao).getAllSpocs()
- assertEquals(1, result.size)
- assertSame(spoc.title, result[0].title)
- assertSame(spoc.url, result[0].url)
- assertSame(spoc.imageUrl, result[0].imageUrl)
- assertSame(spoc.impressionShim, result[0].shim.impression)
- assertSame(spoc.clickShim, result[0].shim.click)
- assertEquals(spoc.priority, result[0].priority)
- assertEquals(2, result[0].caps.currentImpressions.size)
- assertEquals(spoc.lifetimeCapCount, result[0].caps.lifetimeCount)
- assertEquals(spoc.flightCapCount, result[0].caps.flightCount)
- assertEquals(spoc.flightCapPeriod, result[0].caps.flightPeriod)
- }
-
- @Test
- fun `GIVEN SpocsRepository WHEN asking to delete all spocs THEN delete all from the database`() = runTest {
- spocsRepo.deleteAllSpocs()
-
- verify(dao).deleteAllSpocs()
- }
-
- @Test
- fun `GIVEN SpocsRepository WHEN adding a new list of spocs THEN replace all present in the database`() = runTest {
- val spoc = PocketTestResources.apiExpectedPocketSpocs[0]
-
- spocsRepo.addSpocs(listOf(spoc))
-
- verify(dao).cleanOldAndInsertNewSpocs(listOf(spoc.toLocalSpoc()))
- }
-
- @Test
- fun `GIVEN SpocsRepository WHEN recording new spocs impressions THEN add this to the database`() = runTest {
- val spocsIds = listOf(3, 33, 444)
- val impressionsCaptor = argumentCaptor>()
-
- spocsRepo.recordImpressions(spocsIds)
-
- verify(dao).recordImpressions(impressionsCaptor.capture())
- assertEquals(spocsIds.size, impressionsCaptor.value.size)
- assertEquals(spocsIds[0], impressionsCaptor.value[0].spocId)
- assertEquals(spocsIds[1], impressionsCaptor.value[1].spocId)
- assertEquals(spocsIds[2], impressionsCaptor.value[2].spocId)
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt
deleted file mode 100644
index 4b5b2d91593f1..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt
+++ /dev/null
@@ -1,312 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs
-
-import android.content.Context
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runTest
-import mozilla.components.concept.fetch.Client
-import mozilla.components.service.pocket.PocketStoriesRequestConfig
-import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.helpers.PocketTestResources
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import mozilla.components.service.pocket.spocs.SpocsUseCases.RefreshSponsoredStories
-import mozilla.components.service.pocket.spocs.api.SpocsEndpoint
-import mozilla.components.service.pocket.stories.api.PocketResponse
-import mozilla.components.service.pocket.stories.api.PocketResponse.Failure
-import mozilla.components.service.pocket.stories.api.PocketResponse.Success
-import mozilla.components.support.test.any
-import mozilla.components.support.test.argumentCaptor
-import mozilla.components.support.test.mock
-import mozilla.components.support.test.robolectric.testContext
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.never
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
-import java.util.UUID
-import kotlin.reflect.KVisibility
-
-@RunWith(AndroidJUnit4::class)
-class SpocsUseCasesTest {
- private val fetchClient: Client = mock()
- private val profileId = UUID.randomUUID()
- private val appId = "test"
- private val sponsoredStoriesParams = PocketStoriesRequestConfig("123", "US", "NY")
- private val useCases = spy(SpocsUseCases(testContext, fetchClient, profileId, appId, sponsoredStoriesParams))
- private val spocsProvider: SpocsEndpoint = mock()
- private val spocsRepo: SpocsRepository = mock()
-
- @Before
- fun setup() {
- doReturn(spocsProvider).`when`(useCases).getSpocsProvider(any(), any(), any(), any())
- doReturn(spocsRepo).`when`(useCases).getSpocsRepository(any())
- }
-
- @Test
- fun `GIVEN a SpocsUseCases THEN its visibility is internal`() {
- assertClassVisibility(SpocsUseCases::class, KVisibility.INTERNAL)
- }
-
- @Test
- fun `GIVEN a RefreshSponsoredStories THEN its visibility is internal`() {
- assertClassVisibility(RefreshSponsoredStories::class, KVisibility.INTERNAL)
- }
-
- @Test
- fun `GIVEN a GetSponsoredStories THEN its visibility is internal`() {
- assertClassVisibility(SpocsUseCases.GetSponsoredStories::class, KVisibility.INTERNAL)
- }
-
- @Test
- fun `GIVEN a DeleteProfile THEN its visibility is internal`() {
- assertClassVisibility(SpocsUseCases.DeleteProfile::class, KVisibility.INTERNAL)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is constructed THEN use the same parameters`() {
- val refreshUseCase = useCases.refreshStories
-
- assertSame(testContext, refreshUseCase.appContext)
- assertSame(fetchClient, refreshUseCase.fetchClient)
- assertSame(profileId, refreshUseCase.profileId)
- assertSame(appId, refreshUseCase.appId)
- assertSame(sponsoredStoriesParams, refreshUseCase.sponsoredStoriesParams)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN RefreshSponsoredStories is constructed separately THEN default to use the same parameters`() {
- val refreshUseCase = useCases.RefreshSponsoredStories()
-
- assertSame(testContext, refreshUseCase.appContext)
- assertSame(fetchClient, refreshUseCase.fetchClient)
- assertSame(profileId, refreshUseCase.profileId)
- assertSame(appId, refreshUseCase.appId)
- assertSame(sponsoredStoriesParams, refreshUseCase.sponsoredStoriesParams)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN RefreshSponsoredStories is constructed separately THEN allow using different parameters`() {
- val context2: Context = mock()
- val fetchClient2: Client = mock()
- val profileId2 = UUID.randomUUID()
- val appId2 = "test"
- val sponsoredStoriesParams2 = PocketStoriesRequestConfig("1", "CA", "OW")
-
- val refreshUseCase = useCases.RefreshSponsoredStories(context2, fetchClient2, profileId2, appId2, sponsoredStoriesParams2)
-
- assertSame(context2, refreshUseCase.appContext)
- assertSame(fetchClient2, refreshUseCase.fetchClient)
- assertSame(profileId2, refreshUseCase.profileId)
- assertSame(appId2, refreshUseCase.appId)
- assertSame(sponsoredStoriesParams2, refreshUseCase.sponsoredStoriesParams)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is called THEN download stories from API and return early if unsuccessful response`() = runTest {
- val refreshUseCase = useCases.RefreshSponsoredStories()
- val unsuccessfulResponse = getFailedSponsoredStories()
- doReturn(unsuccessfulResponse).`when`(spocsProvider).getSponsoredStories()
-
- val result = refreshUseCase.invoke()
-
- assertFalse(result)
- verify(spocsProvider).getSponsoredStories()
- verify(spocsRepo, never()).addSpocs(any())
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is called THEN download stories from API and save a successful response locally`() = runTest {
- val refreshUseCase = useCases.RefreshSponsoredStories()
- val successfulResponse = getSuccessfulSponsoredStories()
- doReturn(successfulResponse).`when`(spocsProvider).getSponsoredStories()
-
- val result = refreshUseCase.invoke()
-
- assertTrue(result)
- verify(spocsProvider).getSponsoredStories()
- verify(spocsRepo).addSpocs((successfulResponse as Success).data)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is constructed THEN use the same parameters`() {
- val sponsoredStoriesUseCase = useCases.getStories
-
- assertSame(testContext, sponsoredStoriesUseCase.context)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN GetSponsoredStories is constructed separately THEN default to use the same parameters`() {
- val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
-
- assertSame(testContext, sponsoredStoriesUseCase.context)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN GetSponsoredStories is constructed separately THEN allow using different parameters`() {
- val context2: Context = mock()
-
- val sponsoredStoriesUseCase = useCases.GetSponsoredStories(context2)
-
- assertSame(context2, sponsoredStoriesUseCase.context)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is called THEN return the stories from repository`() = runTest {
- val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
- val stories = listOf(PocketTestResources.clientExpectedPocketStory)
- doReturn(stories).`when`(spocsRepo).getAllSpocs()
-
- val result = sponsoredStoriesUseCase.invoke()
-
- verify(spocsRepo).getAllSpocs()
- assertEquals(result, stories)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is called THEN return return an empty list if none are available in the repository`() = runTest {
- val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
- doReturn(emptyList()).`when`(spocsRepo).getAllSpocs()
-
- val result = sponsoredStoriesUseCase.invoke()
-
- verify(spocsRepo).getAllSpocs()
- assertTrue(result.isEmpty())
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN RecordImpression is constructed THEN use the same parameters`() {
- val recordImpressionsUseCase = useCases.getStories
-
- assertSame(testContext, recordImpressionsUseCase.context)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN RecordImpression is constructed separately THEN default to use the same parameters`() {
- val recordImpressionsUseCase = useCases.RecordImpression()
-
- assertSame(testContext, recordImpressionsUseCase.context)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN RecordImpression is constructed separately THEN allow using different parameters`() {
- val context2: Context = mock()
-
- val recordImpressionsUseCase = useCases.RecordImpression(context2)
-
- assertSame(context2, recordImpressionsUseCase.context)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN RecordImpression is called THEN record impressions in database`() = runTest {
- val recordImpressionsUseCase = useCases.RecordImpression()
- val storiesIds = listOf(5, 55, 4321)
- val spocsIdsCaptor = argumentCaptor>()
-
- recordImpressionsUseCase(storiesIds)
-
- verify(spocsRepo).recordImpressions(spocsIdsCaptor.capture())
- assertEquals(3, spocsIdsCaptor.value.size)
- assertEquals(storiesIds[0], spocsIdsCaptor.value[0])
- assertEquals(storiesIds[1], spocsIdsCaptor.value[1])
- assertEquals(storiesIds[2], spocsIdsCaptor.value[2])
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN DeleteProfile is constructed THEN use the same parameters`() {
- val deleteProfileUseCase = useCases.deleteProfile
-
- assertSame(testContext, deleteProfileUseCase.context)
- assertSame(fetchClient, deleteProfileUseCase.fetchClient)
- assertSame(profileId, deleteProfileUseCase.profileId)
- assertSame(appId, deleteProfileUseCase.appId)
- assertSame(sponsoredStoriesParams, deleteProfileUseCase.sponsoredStoriesParams)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN DeleteProfile is constructed separately THEN default to use the same parameters`() {
- val deleteProfileUseCase = useCases.DeleteProfile()
-
- assertSame(testContext, deleteProfileUseCase.context)
- assertSame(fetchClient, deleteProfileUseCase.fetchClient)
- assertSame(profileId, deleteProfileUseCase.profileId)
- assertSame(appId, deleteProfileUseCase.appId)
- assertSame(sponsoredStoriesParams, deleteProfileUseCase.sponsoredStoriesParams)
- }
-
- @Test
- fun `GIVEN SpocsUseCases constructed WHEN DeleteProfile is constructed separately THEN allow using different parameters`() {
- val context2: Context = mock()
- val fetchClient2: Client = mock()
- val profileId2 = UUID.randomUUID()
- val appId2 = "test"
- val sponsoredStoriesParams2 = PocketStoriesRequestConfig("1", "CA", "OW")
-
- val deleteProfileUseCase = useCases.DeleteProfile(context2, fetchClient2, profileId2, appId2, sponsoredStoriesParams2)
-
- assertSame(context2, deleteProfileUseCase.context)
- assertSame(fetchClient2, deleteProfileUseCase.fetchClient)
- assertSame(profileId2, deleteProfileUseCase.profileId)
- assertSame(appId2, deleteProfileUseCase.appId)
- assertSame(sponsoredStoriesParams2, deleteProfileUseCase.sponsoredStoriesParams)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN DeleteProfile is called THEN return true if profile deletion was successful`() = runTest {
- val deleteProfileUseCase = useCases.DeleteProfile()
- val successfulResponse = Success(true)
- doReturn(successfulResponse).`when`(spocsProvider).deleteProfile()
-
- val result = deleteProfileUseCase.invoke()
-
- verify(spocsProvider).deleteProfile()
- assertTrue(result)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN DeleteProfile is called THEN return false if profile deletion was not successful`() = runTest {
- val deleteProfileUseCase = useCases.DeleteProfile()
- val unsuccessfulResponse = Failure()
- doReturn(unsuccessfulResponse).`when`(spocsProvider).deleteProfile()
-
- val result = deleteProfileUseCase.invoke()
-
- verify(spocsProvider).deleteProfile()
- assertFalse(result)
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN profile deletion is succesfull THEN delete all locally persisted spocs`() = runTest {
- val deleteProfileUseCase = useCases.DeleteProfile()
- val successfulResponse = Success(true)
- doReturn(successfulResponse).`when`(spocsProvider).deleteProfile()
-
- deleteProfileUseCase.invoke()
-
- verify(spocsRepo).deleteAllSpocs()
- }
-
- @Test
- fun `GIVEN SpocsUseCases WHEN profile deletion is not succesfull THEN keep all locally persisted spocs`() = runTest {
- val deleteProfileUseCase = useCases.DeleteProfile()
- val unsuccessfulResponse = Failure()
- doReturn(unsuccessfulResponse).`when`(spocsProvider).deleteProfile()
-
- deleteProfileUseCase.invoke()
-
- verify(spocsRepo, never()).deleteAllSpocs()
- }
-
- private fun getSuccessfulSponsoredStories() =
- PocketResponse.wrap(PocketTestResources.apiExpectedPocketSpocs)
-
- private fun getFailedSponsoredStories() = PocketResponse.wrap(null)
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt
deleted file mode 100644
index 32dd76f22eb0a..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import mozilla.components.concept.fetch.Client
-import mozilla.components.concept.fetch.Request
-import mozilla.components.concept.fetch.Response
-import mozilla.components.service.pocket.PocketStoriesRequestConfig
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import mozilla.components.service.pocket.helpers.assertRequestParams
-import mozilla.components.service.pocket.helpers.assertResponseIsClosed
-import mozilla.components.service.pocket.helpers.assertSuccessfulRequestReturnsResponseBody
-import mozilla.components.service.pocket.stories.api.PocketEndpointRaw
-import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion
-import mozilla.components.support.test.any
-import mozilla.components.support.test.helpers.MockResponses
-import mozilla.components.support.test.mock
-import mozilla.components.support.test.whenever
-import org.json.JSONObject
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.doThrow
-import java.io.IOException
-import java.util.UUID
-import kotlin.reflect.KVisibility
-
-@RunWith(AndroidJUnit4::class)
-class SpocsEndpointRawTest {
- private val profileId = UUID.randomUUID()
- private val appId = "test"
- private val sponsoredStoriesParams: PocketStoriesRequestConfig = mock()
-
- private lateinit var endpoint: SpocsEndpointRaw
- private lateinit var client: Client
-
- private lateinit var errorResponse: Response
- private lateinit var successResponse: Response
- private lateinit var defaultResponse: Response
-
- @Before
- fun setUp() {
- errorResponse = MockResponses.getError()
- successResponse = MockResponses.getSuccess()
- defaultResponse = errorResponse
-
- client = mock().also {
- doReturn(defaultResponse).`when`(it).fetch(any())
- }
-
- whenever(sponsoredStoriesParams.siteId).thenReturn("")
- whenever(sponsoredStoriesParams.country).thenReturn("")
- whenever(sponsoredStoriesParams.city).thenReturn("")
-
- endpoint = SpocsEndpointRaw(client, profileId, appId, sponsoredStoriesParams)
- }
-
- @Test
- fun `GIVEN a PocketEndpointRaw THEN its visibility is internal`() {
- assertClassVisibility(PocketEndpointRaw::class, KVisibility.INTERNAL)
- }
-
- @Test
- fun `GIVEN a debug build WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
- SpocsEndpointRaw.isDebugBuild = true
- val expectedUrl = "https://spocs.getpocket.dev/spocs"
-
- assertRequestParams(
- client,
- makeRequest = {
- endpoint.getSponsoredStories()
- },
- assertParams = { request ->
- assertEquals(expectedUrl, request.url)
- assertEquals(Request.Method.POST, request.method)
-
- val requestBody = JSONObject(
- request.body!!.useStream {
- it.bufferedReader().readText()
- },
- )
- assertEquals(2, requestBody["version"])
- assertEquals(appId, requestBody["consumer_key"])
- assertEquals(profileId.toString(), requestBody["pocket_id"])
-
- request.headers!!.first {
- it.name.equals("Content-Type", true)
- }.value.contains("application/json", true)
- },
- )
- }
-
- @Test
- fun `GIVEN a debug build AND a request configuration WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
- SpocsEndpointRaw.isDebugBuild = true
- val expectedUrl = "https://spocs.getpocket.dev/spocs?site=123&country=US&city=NY"
- whenever(sponsoredStoriesParams.siteId).thenReturn("123")
- whenever(sponsoredStoriesParams.country).thenReturn("US")
- whenever(sponsoredStoriesParams.city).thenReturn("NY")
-
- assertRequestParams(
- client,
- makeRequest = {
- endpoint.getSponsoredStories()
- },
- assertParams = { request ->
- assertEquals(expectedUrl, request.url)
- assertEquals(Request.Method.POST, request.method)
-
- val requestBody = JSONObject(
- request.body!!.useStream {
- it.bufferedReader().readText()
- },
- )
- assertEquals(2, requestBody["version"])
- assertEquals(appId, requestBody["consumer_key"])
- assertEquals(profileId.toString(), requestBody["pocket_id"])
-
- request.headers!!.first {
- it.name.equals("Content-Type", true)
- }.value.contains("application/json", true)
- },
- )
- }
-
- @Test
- fun `GIVEN a release build WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
- SpocsEndpointRaw.isDebugBuild = false
- val expectedUrl = "https://spocs.getpocket.com/spocs"
-
- assertRequestParams(
- client,
- makeRequest = {
- endpoint.getSponsoredStories()
- },
- assertParams = { request ->
- assertEquals(expectedUrl, request.url)
- assertEquals(Request.Method.POST, request.method)
-
- val requestBody = JSONObject(
- request.body!!.useStream {
- it.bufferedReader().readText()
- },
- )
- assertEquals(2, requestBody["version"])
- assertEquals(appId, requestBody["consumer_key"])
- assertEquals(profileId.toString(), requestBody["pocket_id"])
-
- request.headers!!.first {
- it.name.equals("Content-Type", true)
- }.value.contains("application/json", true)
- },
- )
- }
-
- @Test
- fun `GIVEN a release build AND a request configuration WHEN requesting spocs THEN the appropriate pocket proxy url is used`() {
- SpocsEndpointRaw.isDebugBuild = false
- val expectedUrl = "https://spocs.getpocket.com/spocs?site=123&country=US&city=NY"
- whenever(sponsoredStoriesParams.siteId).thenReturn("123")
- whenever(sponsoredStoriesParams.country).thenReturn("US")
- whenever(sponsoredStoriesParams.city).thenReturn("NY")
-
- assertRequestParams(
- client,
- makeRequest = {
- endpoint.getSponsoredStories()
- },
- assertParams = { request ->
- assertEquals(expectedUrl, request.url)
- assertEquals(Request.Method.POST, request.method)
-
- val requestBody = JSONObject(
- request.body!!.useStream {
- it.bufferedReader().readText()
- },
- )
- assertEquals(2, requestBody["version"])
- assertEquals(appId, requestBody["consumer_key"])
- assertEquals(profileId.toString(), requestBody["pocket_id"])
-
- request.headers!!.first {
- it.name.equals("Content-Type", true)
- }.value.contains("application/json", true)
- },
- )
- }
-
- @Test
- fun `WHEN requesting spocs and the client throws an IOException THEN null is returned`() {
- doThrow(IOException::class.java).`when`(client).fetch(any())
-
- assertNull(endpoint.getSponsoredStories())
- }
-
- @Test
- fun `WHEN requesting spocs and the response is null THEN null is returned`() {
- doReturn(null).`when`(client).fetch(any())
-
- assertNull(endpoint.getSponsoredStories())
- }
-
- @Test
- fun `WHEN requesting spocs and the response is not a success THEN null is returned`() {
- doReturn(errorResponse).`when`(client).fetch(any())
-
- assertNull(endpoint.getSponsoredStories())
- }
-
- @Test
- fun `GIVEN a debug build WHEN requesting profile deletion THEN the appropriate pocket proxy url is used`() {
- SpocsEndpointRaw.isDebugBuild = true
- val expectedUrl = "https://spocs.getpocket.dev/user"
-
- assertRequestParams(
- client,
- makeRequest = {
- endpoint.deleteProfile()
- },
- assertParams = { request ->
- assertEquals(expectedUrl, request.url)
- assertEquals(Request.Method.DELETE, request.method)
- },
- )
- }
-
- @Test
- fun `GIVEN a release build WHEN requesting profile deletion THEN the appropriate pocket proxy url is used`() {
- SpocsEndpointRaw.isDebugBuild = false
- val expectedUrl = "https://spocs.getpocket.com/user"
-
- assertRequestParams(
- client,
- makeRequest = {
- endpoint.deleteProfile()
- },
- assertParams = { request ->
- assertEquals(expectedUrl, request.url)
- assertEquals(Request.Method.DELETE, request.method)
- },
- )
- }
-
- @Test
- fun `WHEN requesting profile deletion and the client throws an IOException THEN false is returned`() {
- doThrow(IOException::class.java).`when`(client).fetch(any())
-
- assertFalse(endpoint.deleteProfile())
- }
-
- @Test
- fun `WHEN requesting account deletion and the response is not a success THEN false is returned`() {
- doReturn(errorResponse).`when`(client).fetch(any())
-
- assertFalse(endpoint.deleteProfile())
- }
-
- @Test
- fun `WHEN requesting spocs and the response is a success THEN the response body is returned`() {
- assertSuccessfulRequestReturnsResponseBody(client, endpoint::getSponsoredStories)
- }
-
- @Test
- fun `WHEN requesting profile deletion and the response is a success THEN true is returned`() {
- val response = MockResponses.getSuccess()
- doReturn(response).`when`(client).fetch(any())
-
- assertTrue(endpoint.deleteProfile())
- }
-
- @Test
- fun `WHEN requesting spocs and the response is an error THEN response is closed`() {
- assertResponseIsClosed(client, errorResponse) {
- endpoint.getSponsoredStories()
- }
- }
-
- @Test
- fun `GIVEN a response from the request to delete profile WHEN inferring it's success THEN don't use the reponse body`() {
- // Leverage the fact that a stream can only be read once to know if it was previously read.
-
- doReturn(errorResponse).`when`(client).fetch(any())
- errorResponse.use { "Only the response status should be used, not the response body" }
-
- doReturn(successResponse).`when`(client).fetch(any())
- successResponse.use { "Only the response status should be used, not the response body" }
- }
-
- @Test
- fun `WHEN requesting spocs and the response is a success THEN response is closed`() {
- assertResponseIsClosed(client, successResponse) {
- endpoint.getSponsoredStories()
- }
- }
-
- @Test
- fun `WHEN newInstance is called THEN a new instance configured with the client provided is returned`() {
- val result = Companion.newInstance(client)
-
- assertSame(client, result.client)
- }
-
- @Test
- fun `GIVEN a debug build WHEN querying the base url THEN use the development endpoint`() {
- SpocsEndpointRaw.isDebugBuild = true
- val expectedUrl = "https://spocs.getpocket.dev/"
-
- assertEquals(expectedUrl, SpocsEndpointRaw.baseUrl)
- }
-
- @Test
- fun `GIVEN a release build WHEN querying the base url THEN use the production endpoint`() {
- SpocsEndpointRaw.isDebugBuild = false
- val expectedUrl = "https://spocs.getpocket.com/"
-
- assertEquals(expectedUrl, SpocsEndpointRaw.baseUrl)
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt
deleted file mode 100644
index 11c09613f7844..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runTest
-import mozilla.components.concept.fetch.Client
-import mozilla.components.service.pocket.PocketStoriesRequestConfig
-import mozilla.components.service.pocket.helpers.PocketTestResources
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import mozilla.components.service.pocket.helpers.assertResponseIsFailure
-import mozilla.components.service.pocket.helpers.assertResponseIsSuccess
-import mozilla.components.service.pocket.stories.api.PocketResponse
-import mozilla.components.support.test.any
-import mozilla.components.support.test.mock
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.doThrow
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import java.util.UUID
-import kotlin.reflect.KVisibility
-
-@RunWith(AndroidJUnit4::class)
-class SpocsEndpointTest {
-
- private lateinit var endpoint: SpocsEndpoint
- private var raw: SpocsEndpointRaw = mock() // we shorten the name to avoid confusion with endpoint.
- private var jsonParser: SpocsJSONParser = mock()
- private var client: Client = mock()
-
- @Before
- fun setUp() {
- endpoint = SpocsEndpoint(raw, jsonParser)
- }
-
- @Test
- fun `GIVEN a SpocsEndpoint THEN its visibility is internal`() {
- assertClassVisibility(SpocsEndpoint::class, KVisibility.INTERNAL)
- }
-
- @Test
- fun `GIVEN a request for spocs WHEN getting a null response THEN a failure is returned`() = runTest {
- doReturn(null).`when`(raw).getSponsoredStories()
-
- assertResponseIsFailure(endpoint.getSponsoredStories())
- }
-
- @Test
- fun `GIVEN a request for spocs WHEN getting a null response THEN we do not attempt to parse stories`() = runTest {
- doReturn(null).`when`(raw).getSponsoredStories()
-
- doThrow(
- AssertionError("JSONParser should not be called for a null endpoint response"),
- ).`when`(jsonParser).jsonToSpocs(any())
-
- endpoint.getSponsoredStories()
- }
-
- @Test
- fun `GIVEN a request for deleting profile WHEN the response is unsuccessful THEN a failure is returned`() = runTest {
- doReturn(false).`when`(raw).deleteProfile()
-
- assertResponseIsFailure(endpoint.deleteProfile())
- }
-
- @Test
- fun `GIVEN a request for deleting profile WHEN the response is successful THEN success is returned`() = runTest {
- doReturn(true).`when`(raw).deleteProfile()
-
- assertResponseIsSuccess(endpoint.deleteProfile())
- }
-
- @Test
- fun `GIVEN a request for spocs WHEN getting an empty response THEN a failure is returned`() = runTest {
- arrayOf(
- "",
- " ",
- ).forEach { response ->
- doReturn(response).`when`(raw).getSponsoredStories()
-
- assertResponseIsFailure(endpoint.getSponsoredStories())
- }
- }
-
- @Test
- fun `GIVEN a request for spocs WHEN getting an empty response THEN we do not attempt to parse stories`() = runTest {
- arrayOf(
- "",
- " ",
- ).forEach { response ->
- doReturn(response).`when`(raw).getSponsoredStories()
- doThrow(
- AssertionError("JSONParser should not be called for an empty endpoint response"),
- ).`when`(jsonParser).jsonToSpocs(any())
-
- endpoint.getSponsoredStories()
- }
- }
-
- @Test
- fun `GIVEN a request for stories WHEN getting a response THEN parse it through PocketJSONParser`() = runTest {
- arrayOf(
- "{}",
- """{"expectedJSON": 101}""",
- """{ "spocs": [] }""",
- ).forEach { response ->
- doReturn(response).`when`(raw).getSponsoredStories()
-
- endpoint.getSponsoredStories()
-
- verify(jsonParser, times(1)).jsonToSpocs(response)
- }
- }
-
- @Test
- fun `GIVEN a request for stories WHEN getting a valid response THEN success is returned`() = runTest {
- endpoint = SpocsEndpoint(raw, SpocsJSONParser)
- val response = PocketTestResources.pocketEndpointThreeSpocsResponse
- doReturn(response).`when`(raw).getSponsoredStories()
-
- val result = endpoint.getSponsoredStories()
-
- assertTrue(result is PocketResponse.Success)
- }
-
- @Test
- fun `GIVEN a request for stories WHEN getting a valid response THEN a success response with parsed stories is returned`() = runTest {
- endpoint = SpocsEndpoint(raw, SpocsJSONParser)
- val response = PocketTestResources.pocketEndpointThreeSpocsResponse
- doReturn(response).`when`(raw).getSponsoredStories()
- val expected = PocketTestResources.apiExpectedPocketSpocs
-
- val result = endpoint.getSponsoredStories()
-
- assertEquals(expected, (result as? PocketResponse.Success)?.data)
- }
-
- @Test
- fun `WHEN newInstance is called THEN a new SpocsEndpoint is returned as a wrapper over a configured SpocsEndpointRaw`() {
- val profileId = UUID.randomUUID()
- val appId = "test"
- val sponsoredStoriesParams = PocketStoriesRequestConfig("123")
-
- val result = SpocsEndpoint.Companion.newInstance(client, profileId, appId, sponsoredStoriesParams)
-
- assertSame(client, result.rawEndpoint.client)
- assertSame(profileId, result.rawEndpoint.profileId)
- assertSame(appId, result.rawEndpoint.appId)
- assertSame(sponsoredStoriesParams, result.rawEndpoint.sponsoredStoriesParams)
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt
deleted file mode 100644
index a49d9bd96e3a8..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt
+++ /dev/null
@@ -1,200 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.api
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import mozilla.components.service.pocket.helpers.PocketTestResources
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import org.json.JSONObject
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.reflect.KVisibility
-
-@RunWith(AndroidJUnit4::class)
-class SpocsJSONParserTest {
- @Test
- fun `GIVEN a SpocsJSONParser THEN its visibility is internal`() {
- assertClassVisibility(SpocsJSONParser::class, KVisibility.INTERNAL)
- }
-
- @Test
- fun `GIVEN SpocsJSONParser WHEN parsing spocs THEN ApiSpocs are returned`() {
- val expectedSpocs = PocketTestResources.apiExpectedPocketSpocs
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val actualSpocs = SpocsJSONParser.jsonToSpocs(pocketJSON)
-
- assertNotNull(actualSpocs)
- assertEquals(3, actualSpocs!!.size)
- assertEquals(expectedSpocs, actualSpocs)
- }
-
- @Test
- fun `WHEN parsing spocs with missing titles THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(2) }
- val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("title", 2, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing urls THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(1) }
- val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("url", 1, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing image urls THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(0) }
- val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("image_src", 0, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing sponsors THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(1) }
- val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("sponsor", 1, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing click shims THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(2) }
- val pocketJsonWithMissingTitle = removeShimFromSpoc("click", 2, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing impression shims THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(1) }
- val pocketJsonWithMissingTitle = removeShimFromSpoc("impression", 1, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing priority THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingPriority = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(1) }
- val pocketJsonWithMissingPriority = removeJsonFieldFromArrayIndex("priority", 1, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingPriority)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingPriority.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing a lifetime count cap THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingLifetimeCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(0) }
- val pocketJsonWithMissingLifetimeCap = removeCapFromSpoc(JSON_SPOC_CAPS_LIFETIME_KEY, 0, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingLifetimeCap)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingLifetimeCap.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing a flight count cap THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingFlightCountCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(1) }
- val pocketJsonWithMissingFlightCountCap = removeCapFromSpoc(JSON_SPOC_CAPS_FLIGHT_COUNT_KEY, 1, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingFlightCountCap)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingFlightCountCap.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs with missing a flight period cap THEN those entries are dropped`() {
- val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
- val expectedSpocsIfMissingFlightPeriodCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
- .apply { removeAt(2) }
- val pocketJsonWithMissingFlightPeriodCap = removeCapFromSpoc(JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY, 2, pocketJSON)
-
- val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingFlightPeriodCap)
-
- assertEquals(2, result!!.size)
- assertEquals(expectedSpocsIfMissingFlightPeriodCap.joinToString(), result.joinToString())
- }
-
- @Test
- fun `WHEN parsing spocs for an invalid JSON String THEN null is returned`() {
- assertNull(SpocsJSONParser.jsonToSpocs("{!!}}"))
- }
-}
-
-private fun removeJsonFieldFromArrayIndex(fieldName: String, indexInArray: Int, json: String): String {
- val obj = JSONObject(json)
- val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
- spocsJson.getJSONObject(indexInArray).remove(fieldName)
- return obj.toString()
-}
-
-private fun removeShimFromSpoc(shimName: String, spocIndex: Int, json: String): String {
- val obj = JSONObject(json)
- val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
- val spocJson = spocsJson.getJSONObject(spocIndex)
- spocJson.getJSONObject(JSON_SPOC_SHIMS_KEY).remove(shimName)
- return obj.toString()
-}
-
-private fun removeCapFromSpoc(cap: String, spocIndex: Int, json: String): String {
- val obj = JSONObject(json)
- val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
- val spocJson = spocsJson.getJSONObject(spocIndex)
- val capsJSON = spocJson.getJSONObject(JSON_SPOC_CAPS_KEY)
-
- if (cap == JSON_SPOC_CAPS_LIFETIME_KEY) {
- capsJSON.remove(cap)
- } else {
- capsJSON.getJSONObject(JSON_SPOC_CAPS_FLIGHT_KEY).remove(cap)
- }
-
- return obj.toString()
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt
deleted file mode 100644
index f7dee0141800e..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.db
-
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import org.junit.Test
-import kotlin.reflect.KVisibility.INTERNAL
-
-class SpocEntityTest {
- // This is the data type persisted locally. No need to be public
- @Test
- fun `GIVEN a spoc entity THEN it's visibility is internal`() {
- assertClassVisibility(SpocEntity::class, INTERNAL)
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt
deleted file mode 100644
index 4e119b0bb2b33..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.db
-
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import kotlin.reflect.KVisibility.INTERNAL
-
-class SpocImpressionEntityTest {
- // This is the data type persisted locally. No need to be public
- @Test
- fun `GIVEN a spoc entity THEN it's visibility is internal`() {
- assertClassVisibility(SpocImpressionEntity::class, INTERNAL)
- }
-
- @Test
- fun `WHEN a new impression is created THEN the timestamp should be seconds from Epoch`() {
- val nowInSeconds = System.currentTimeMillis() / 1000
- val impression = SpocImpressionEntity(2)
-
- assertTrue(
- LongRange(nowInSeconds - 5, nowInSeconds + 5)
- .contains(impression.impressionDateInSeconds),
- )
- }
-}
diff --git a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt b/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt
deleted file mode 100644
index e0758cbe66952..0000000000000
--- a/mobile/android/android-components/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt
+++ /dev/null
@@ -1,511 +0,0 @@
-/* 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/. */
-
-package mozilla.components.service.pocket.spocs.db
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.room.Room
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runTest
-import mozilla.components.service.pocket.helpers.PocketTestResources
-import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
-import mozilla.components.support.test.robolectric.testContext
-import org.junit.After
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-
-@RunWith(AndroidJUnit4::class)
-class SpocsDaoTest {
- private lateinit var database: PocketRecommendationsDatabase
- private lateinit var dao: SpocsDao
- private lateinit var executor: ExecutorService
-
- @get:Rule
- var instantTaskExecutorRule = InstantTaskExecutorRule()
-
- @Before
- fun setUp() {
- executor = Executors.newSingleThreadExecutor()
- database = Room
- .inMemoryDatabaseBuilder(testContext, PocketRecommendationsDatabase::class.java)
- .allowMainThreadQueries()
- .build()
- dao = database.spocsDao()
- }
-
- @After
- fun tearDown() {
- database.close()
- executor.shutdown()
- }
-
- @Test
- fun `GIVEN an empty table WHEN a story is inserted and then queried THEN return the same story`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
-
- dao.insertSpocs(listOf(story))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(story), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different id is tried to be inserted THEN add that to the table`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- id = 1,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory, story), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different url is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- title = "updated" + story.url,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different title is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- title = "updated" + story.title,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different image url is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- imageUrl = "updated" + story.imageUrl,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different sponsor is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- sponsor = "updated" + story.sponsor,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different click shim is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- clickShim = "updated" + story.clickShim,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different impression shim is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- impressionShim = "updated" + story.impressionShim,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with different priority is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- priority = 765,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with a different lifetime cap count is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- lifetimeCapCount = 123,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with a different flight count cap is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- flightCapCount = 999,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN a story already persisted WHEN another story with a different flight period cap is tried to be inserted THEN replace the existing`() = runTest {
- val story = PocketTestResources.dbExpectedPocketSpoc
- val newStory = story.copy(
- flightCapPeriod = 1,
- )
- dao.insertSpocs(listOf(story))
-
- dao.insertSpocs(listOf(newStory))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(newStory), result)
- }
-
- @Test
- fun `GIVEN no persisted storied WHEN asked to insert a list of stories THEN add them all to the table`() = runTest {
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
- val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
-
- dao.insertSpocs(listOf(story1, story2, story3, story4))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(story1, story2, story3, story4), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to delete them THEN remove all from the table`() = runTest {
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
- val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
- dao.insertSpocs(listOf(story1, story2, story3, story4))
-
- dao.deleteAllSpocs()
- val result = dao.getAllSpocs()
-
- assertTrue(result.isEmpty())
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to delete some THEN remove remove the ones already persisted`() = runTest {
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
- val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
- val story5 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 5)
- dao.insertSpocs(listOf(story1, story2, story3, story4))
-
- dao.deleteSpocs(listOf(story2, story3, story5))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(story1, story4), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN remove from table all stories not found in the new list`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
- val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
- dao.insertSpocs(listOf(story1, story2, story3, story4))
-
- dao.cleanOldAndInsertNewSpocs(listOf(story2, story4))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(story2, story4), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new ids`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- id = story1.id * 3,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- // Order gets reversed because the original story is replaced and another one is added.
- assertEquals(listOf(story2, updatedStory1), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only url changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- url = "updated" + story1.url,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only title changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- title = "updated" + story1.title,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only image url changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- imageUrl = "updated" + story1.imageUrl,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only sponsor changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- sponsor = "updated" + story1.sponsor,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the click shim changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- clickShim = "updated" + story1.clickShim,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the impression shim changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- impressionShim = "updated" + story1.impressionShim,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only priority changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- priority = 678,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the lifetime count cap changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- lifetimeCapCount = 4322,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the flight count cap changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- flightCapCount = 111111,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the flight period cap changed`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val updatedStory1 = story1.copy(
- flightCapPeriod = 7,
- )
- dao.insertSpocs(listOf(story1, story2))
-
- dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
- val result = dao.getAllSpocs()
-
- assertEquals(listOf(updatedStory1, story2), result)
- }
-
- @Test
- fun `GIVEN no stories are persisted WHEN asked to record an impression THEN don't persist data and don't throw errors`() = runTest {
- dao.recordImpression(6543321)
-
- val result = dao.getSpocsImpressions()
-
- assertTrue(result.isEmpty())
- }
-
- @Test
- fun `GIVEN stories are persisted WHEN asked to record impressions for other stories also THEN persist impression only for existing stories`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
- dao.insertSpocs(listOf(story1, story3))
-
- dao.recordImpressions(
- listOf(
- SpocImpressionEntity(story1.id),
- SpocImpressionEntity(story2.id),
- SpocImpressionEntity(story3.id),
- ),
- )
- val result = dao.getSpocsImpressions()
-
- assertEquals(2, result.size)
- assertEquals(story1.id, result[0].spocId)
- assertEquals(story3.id, result[1].spocId)
- }
-
- @Test
- fun `GIVEN stories are persisted WHEN asked to record impressions for existing stories THEN persist the impressions`() = runTest {
- setupDatabseForTransactions()
- val story1 = PocketTestResources.dbExpectedPocketSpoc
- val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
- val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
- dao.insertSpocs(listOf(story1, story2, story3))
-
- dao.recordImpressions(
- listOf(
- SpocImpressionEntity(story1.id),
- SpocImpressionEntity(story3.id),
- ),
- )
- val result = dao.getSpocsImpressions()
-
- assertEquals(2, result.size)
- assertEquals(story1.id, result[0].spocId)
- assertEquals(story3.id, result[1].spocId)
- }
-
- /**
- * Sets an executor to be used for database transactions.
- * Needs to be used along with "runTest" to ensure waiting for transactions to finish but not hang tests.
- */
- private fun setupDatabseForTransactions() {
- database = Room
- .inMemoryDatabaseBuilder(testContext, PocketRecommendationsDatabase::class.java)
- .setTransactionExecutor(executor)
- .allowMainThreadQueries()
- .build()
- dao = database.spocsDao()
- }
-}
diff --git a/mobile/android/fenix/app/build.gradle b/mobile/android/fenix/app/build.gradle
index 1068f0ea56a6d..4d5527ca8535c 100644
--- a/mobile/android/fenix/app/build.gradle
+++ b/mobile/android/fenix/app/build.gradle
@@ -473,21 +473,6 @@ android.applicationVariants.configureEach { variant ->
project.logger.debug("--")
}
-// -------------------------------------------------------------------------------------------------
-// BuildConfig: Set the Pocket consumer key from a local file if it exists
-// -------------------------------------------------------------------------------------------------
-
- project.logger.debug("Pocket consumer key: ")
-
- try {
- def token = new File("${rootDir}/.pocket_consumer_key").text.trim()
- buildConfigField 'String', 'POCKET_CONSUMER_KEY', '"' + token + '"'
- project.logger.debug("(Added from .pocket_consumer_key file)")
- } catch (FileNotFoundException ignored) {
- buildConfigField 'String', 'POCKET_CONSUMER_KEY', '""'
- project.logger.debug("--")
- }
-
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set flag to disable LeakCanary in debug (on CI builds)
// -------------------------------------------------------------------------------------------------
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Core.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Core.kt
index b17f6815cf6d6..088c85520a8c4 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Core.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Core.kt
@@ -95,9 +95,7 @@ import mozilla.components.service.mars.Placement
import mozilla.components.service.mars.contile.ContileTopSitesUpdater
import mozilla.components.service.pocket.ContentRecommendationsRequestConfig
import mozilla.components.service.pocket.PocketStoriesConfig
-import mozilla.components.service.pocket.PocketStoriesRequestConfig
import mozilla.components.service.pocket.PocketStoriesService
-import mozilla.components.service.pocket.Profile
import mozilla.components.service.pocket.mars.api.MarsSpocsRequestConfig
import mozilla.components.service.pocket.mars.api.NEW_TAB_SPOCS_PLACEMENT_KEY
import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
@@ -137,7 +135,6 @@ import org.mozilla.fenix.share.SaveToPDFMiddleware
import org.mozilla.fenix.telemetry.TelemetryMiddleware
import org.mozilla.fenix.utils.getUndoDelay
import org.mozilla.geckoview.GeckoRuntime
-import java.util.UUID
import java.util.concurrent.TimeUnit
import mozilla.components.service.pocket.mars.api.Placement as MarsSpocsPlacement
@@ -571,19 +568,6 @@ class Core(
PocketStoriesConfig(
client,
Frequency(4, TimeUnit.HOURS),
- Profile(
- profileId = UUID.fromString(context.settings().pocketSponsoredStoriesProfileId),
- appId = BuildConfig.POCKET_CONSUMER_KEY,
- ),
- sponsoredStoriesParams = if (context.settings().useCustomConfigurationForSponsoredStories) {
- PocketStoriesRequestConfig(
- context.settings().pocketSponsoredStoriesSiteId,
- context.settings().pocketSponsoredStoriesCountry,
- context.settings().pocketSponsoredStoriesCity,
- )
- } else {
- PocketStoriesRequestConfig()
- },
contentRecommendationsParams = ContentRecommendationsRequestConfig(
locale = LocaleManager.getSelectedLocale(context).toLanguageTag(),
),
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt
index 707f4617abc4d..64717cb08e99a 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt
@@ -16,7 +16,6 @@ import mozilla.components.lib.state.Action
import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.service.nimbus.messaging.MessageSurfaceId
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import org.mozilla.fenix.bookmarks.BookmarksGlobalResultReport
import org.mozilla.fenix.browser.StandardSnackbarError
@@ -586,15 +585,6 @@ sealed class AppAction : Action {
*/
data object PocketStoriesClean : ContentRecommendationsAction()
- /**
- * Replaces the current list of Pocket sponsored stories.
- *
- * @property sponsoredStories The new list of [PocketSponsoredStory] that was fetched.
- */
- data class PocketSponsoredStoriesChange(
- val sponsoredStories: List,
- ) : ContentRecommendationsAction()
-
/**
* Replaces the current list of [SponsoredContent]s.
*
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsReducer.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsReducer.kt
index 216df97f3fe43..f35fd86bf0014 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsReducer.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsReducer.kt
@@ -7,7 +7,6 @@ package org.mozilla.fenix.components.appstate.recommendations
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.ext.recordNewImpression
import org.mozilla.fenix.components.appstate.AppAction.ContentRecommendationsAction
@@ -105,26 +104,11 @@ internal object ContentRecommendationsReducer {
pocketStoriesCategories = emptyList(),
pocketStoriesCategoriesSelections = emptyList(),
pocketStories = emptyList(),
- pocketSponsoredStories = emptyList(),
contentRecommendations = emptyList(),
sponsoredContents = emptyList(),
)
}
- is ContentRecommendationsAction.PocketSponsoredStoriesChange -> {
- val updatedStoriesState = state.copyWithRecommendationsState {
- it.copy(
- pocketSponsoredStories = action.sponsoredStories,
- )
- }
-
- updatedStoriesState.copyWithRecommendationsState {
- it.copy(
- pocketStories = updatedStoriesState.getStories(),
- )
- }
- }
-
is ContentRecommendationsAction.SponsoredContentsChange -> {
val updatedSponsoredContentsState = state.copyWithRecommendationsState {
it.copy(
@@ -134,9 +118,7 @@ internal object ContentRecommendationsReducer {
updatedSponsoredContentsState.copyWithRecommendationsState {
it.copy(
- pocketStories = updatedSponsoredContentsState.getStories(
- useSponsoredStoriesState = false,
- ),
+ pocketStories = updatedSponsoredContentsState.getStories(),
)
}
}
@@ -176,16 +158,6 @@ internal object ContentRecommendationsReducer {
}
}
- var updatedSponsoredStories = state.recommendationState.pocketSponsoredStories
- stories.filterIsInstance().forEach { shownStory ->
- updatedSponsoredStories = updatedSponsoredStories.map { story ->
- when (story.id == shownStory.id) {
- true -> story.recordNewImpression()
- false -> story
- }
- }
- }
-
val sponsoredContentShown = stories.filterIsInstance()
val updatedSponsoredContents = state.recommendationState.sponsoredContents.map { spoc ->
if (sponsoredContentShown.contains(spoc)) {
@@ -198,7 +170,6 @@ internal object ContentRecommendationsReducer {
state.copyWithRecommendationsState {
it.copy(
pocketStoriesCategories = updatedCategories,
- pocketSponsoredStories = updatedSponsoredStories,
contentRecommendations = updatedRecommendations,
sponsoredContents = updatedSponsoredContents,
)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsState.kt
index 108c7f99c8f34..1784af89c180f 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsState.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/recommendations/ContentRecommendationsState.kt
@@ -7,7 +7,6 @@ package org.mozilla.fenix.components.appstate.recommendations
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
@@ -18,7 +17,6 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
* @property pocketStories The list of currently shown [PocketRecommendedStory]s.
* @property pocketStoriesCategories All [PocketRecommendedStory] categories.
* @property pocketStoriesCategoriesSelections Current Pocket recommended stories categories selected by the user.
- * @property pocketSponsoredStories All [PocketSponsoredStory]s.
* @property contentRecommendations The list of [ContentRecommendation]s that could be displayed.
* @property sponsoredContents The list of [SponsoredContent]s that could be displayed.
*/
@@ -26,7 +24,6 @@ data class ContentRecommendationsState(
val pocketStories: List = emptyList(),
val pocketStoriesCategories: List = emptyList(),
val pocketStoriesCategoriesSelections: List = emptyList(),
- val pocketSponsoredStories: List = emptyList(),
val contentRecommendations: List = emptyList(),
val sponsoredContents: List = emptyList(),
)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt
index 406adcdb39662..2442237704369 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt
@@ -7,10 +7,8 @@ package org.mozilla.fenix.ext
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.ext.hasFlightImpressionsLimitReached
-import mozilla.components.service.pocket.ext.hasLifetimeImpressionsLimitReached
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.home.blocklist.BlocklistHandler
import org.mozilla.fenix.home.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
@@ -40,23 +38,14 @@ internal val SPONSORED_STORIES_TO_SHOW_COUNT = SPONSORED_STORIES_INDEXES.size
/**
* Get the list of stories to be displayed based on the user selected categories.
*
- * @param useSponsoredStoriesState Whether or not to pull sponsored stories from
- * [recommendationState.pocketSponsoredStories] or [recommendationState.sponsoredContents] state.
* @return a list of [PocketStory]es from the currently selected categories.
*/
-fun AppState.getFilteredStories(useSponsoredStoriesState: Boolean = true): List {
+fun AppState.getFilteredStories(): List {
val recommendedStories = getFilteredRecommendedStories()
- val sponsoredStories = if (useSponsoredStoriesState) {
- getFilteredSponsoredStories(
- stories = recommendationState.pocketSponsoredStories,
- limit = SPONSORED_STORIES_TO_SHOW_COUNT,
- )
- } else {
- getFilteredSponsoredContents(
- sponsoredContents = recommendationState.sponsoredContents,
- limit = SPONSORED_STORIES_TO_SHOW_COUNT,
- )
- }
+ val sponsoredStories = getFilteredSponsoredContents(
+ sponsoredContents = recommendationState.sponsoredContents,
+ limit = SPONSORED_STORIES_TO_SHOW_COUNT,
+ )
return combineRecommendationsAndSponsoredContents(
recommendations = recommendedStories,
@@ -105,26 +94,17 @@ private fun AppState.getFilteredRecommendedStories(): List {
+fun AppState.getStories(): List {
val recommendations = recommendationState.contentRecommendations
.sortedBy { it.impressions }
.take(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT)
- val sponsoredStories = if (useSponsoredStoriesState) {
- getFilteredSponsoredStories(
- stories = recommendationState.pocketSponsoredStories,
- limit = SPONSORED_STORIES_TO_SHOW_COUNT,
- )
- } else {
- getFilteredSponsoredContents(
- sponsoredContents = recommendationState.sponsoredContents,
- limit = SPONSORED_STORIES_TO_SHOW_COUNT,
- )
- }
+ val sponsoredStories = getFilteredSponsoredContents(
+ sponsoredContents = recommendationState.sponsoredContents,
+ limit = SPONSORED_STORIES_TO_SHOW_COUNT,
+ )
return combineRecommendationsAndSponsoredContents(
recommendations = recommendations,
@@ -212,22 +192,6 @@ internal fun getFilteredStoriesCount(
return emptyMap()
}
-/**
- * Handle pacing and rotation of sponsored stories.
- */
-@VisibleForTesting
-internal fun getFilteredSponsoredStories(
- stories: List,
- limit: Int,
-): List {
- return stories.asSequence()
- .filterNot { it.hasLifetimeImpressionsLimitReached() }
- .sortedByDescending { it.priority }
- .filterNot { it.hasFlightImpressionsLimitReached() }
- .take(limit)
- .toList()
-}
-
/**
* Handle pacing and rotation of sponsored contents.
*
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/PocketMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/PocketMiddleware.kt
index 9e09fc8ee71c4..ef263dd2f24ac 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/PocketMiddleware.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/PocketMiddleware.kt
@@ -18,7 +18,6 @@ import mozilla.components.service.pocket.PocketStoriesService
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.components.AppStore
@@ -160,11 +159,6 @@ internal fun persistStoriesImpressions(
},
)
- pocketStoriesService.recordStoriesImpressions(
- updatedStories.filterIsInstance()
- .map { it.id },
- )
-
pocketStoriesService.recordSponsoredContentImpressions(
impressions = updatedStories.filterIsInstance().map { it.url },
)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt
index c8036102c9a52..1e6e02d0aae86 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt
@@ -11,11 +11,9 @@ import kotlinx.coroutines.launch
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.ext.getCurrentFlightImpressions
import mozilla.telemetry.glean.private.NoExtras
-import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.GleanMetrics.Pocket
import org.mozilla.fenix.GleanMetrics.StoriesLibrary
import org.mozilla.fenix.R
@@ -119,18 +117,6 @@ internal class DefaultPocketStoriesController(
)
when (storyShown) {
- is PocketSponsoredStory -> {
- Pocket.homeRecsSpocShown.record(
- Pocket.HomeRecsSpocShownExtra(
- spocId = storyShown.id.toString(),
- position = "${storyPosition.first}x${storyPosition.second}",
- timesShown = storyShown.getCurrentFlightImpressions().size.inc().toString(),
- ),
- )
- Pocket.spocShim.set(storyShown.shim.impression)
- Pings.spoc.submit(Pings.spocReasonCodes.impression)
- }
-
is SponsoredContent -> {
Pocket.homeRecsSpocShown.record(
Pocket.HomeRecsSpocShownExtra(
@@ -227,18 +213,6 @@ internal class DefaultPocketStoriesController(
)
}
- is PocketSponsoredStory -> {
- Pocket.homeRecsSpocClicked.record(
- Pocket.HomeRecsSpocClickedExtra(
- spocId = storyClicked.id.toString(),
- position = "${storyPosition.first}x${storyPosition.second}",
- timesShown = storyClicked.getCurrentFlightImpressions().size.inc().toString(),
- ),
- )
- Pocket.spocShim.set(storyClicked.shim.click)
- Pings.spoc.submit(Pings.spocReasonCodes.click)
- }
-
is ContentRecommendation -> {
appStore.dispatch(
ContentRecommendationsAction.ContentRecommendationClicked(
@@ -260,6 +234,10 @@ internal class DefaultPocketStoriesController(
marsUseCases.recordInteraction(storyClicked.callbacks.clickUrl)
}
}
+
+ else -> {
+ // no-op
+ }
}
}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt
index 049ab996f0ab4..a1879e128ed63 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt
@@ -8,7 +8,6 @@ import android.os.Build
import android.os.Bundle
import androidx.core.content.edit
import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@@ -24,7 +23,6 @@ import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
import org.mozilla.fenix.ext.components
-import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.GleanMetrics.DebugDrawer as DebugDrawerMetrics
@@ -347,10 +345,6 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
isVisible = Config.channel.isNightlyOrDebug && BuildConfig.GLEAN_CUSTOM_URL.isNullOrEmpty()
}
- requirePreference(R.string.pref_key_custom_sponsored_stories_parameters).apply {
- isVisible = Config.channel.isNightlyOrDebug
- }
-
requirePreference(R.string.pref_key_remote_server_prod).apply {
isVisible = true
isChecked = context.settings().useProductionRemoteSettingsServer
@@ -472,17 +466,6 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
}
}
- override fun onPreferenceTreeClick(preference: Preference): Boolean {
- when (preference.key) {
- getString(R.string.pref_key_custom_sponsored_stories_parameters) ->
- findNavController().nav(
- R.id.secretSettingsPreference,
- SecretSettingsFragmentDirections.actionSecretSettingsFragmentToSponsoredStoriesSettings(),
- )
- }
- return super.onPreferenceTreeClick(preference)
- }
-
override fun onDisplayPreferenceDialog(preference: Preference) {
val handled = showCustomEditTextPreferenceDialog(preference)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SponsoredStoriesSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SponsoredStoriesSettingsFragment.kt
deleted file mode 100644
index 888e64fa7d0a0..0000000000000
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SponsoredStoriesSettingsFragment.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/* 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/. */
-
-package org.mozilla.fenix.settings
-
-import android.os.Bundle
-import androidx.navigation.fragment.navArgs
-import androidx.preference.EditTextPreference
-import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
-import androidx.preference.SwitchPreference
-import org.mozilla.fenix.Config
-import org.mozilla.fenix.R
-import org.mozilla.fenix.ext.settings
-
-/**
- * Allows customizing sponsored stories fetch parameters.
- */
-class SponsoredStoriesSettingsFragment : PreferenceFragmentCompat() {
- private val args by navArgs()
-
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.sponsored_stories_settings, rootKey)
-
- requirePreference(R.string.pref_key_custom_sponsored_stories_parameters_enabled).apply {
- isVisible = Config.channel.isNightlyOrDebug
- isChecked = context.settings().useCustomConfigurationForSponsoredStories
- onPreferenceChangeListener = SharedPreferenceUpdater()
- }
-
- requirePreference(R.string.pref_key_custom_sponsored_stories_site_id).apply {
- isVisible = Config.channel.isNightlyOrDebug
- onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
- context.settings().pocketSponsoredStoriesSiteId = (newValue as String)
- true
- }
- }
-
- requirePreference(R.string.pref_key_custom_sponsored_stories_country).apply {
- isVisible = Config.channel.isNightlyOrDebug
- onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
- context.settings().pocketSponsoredStoriesCountry = (newValue as String)
- true
- }
- }
-
- requirePreference(R.string.pref_key_custom_sponsored_stories_city).apply {
- isVisible = Config.channel.isNightlyOrDebug
- onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
- context.settings().pocketSponsoredStoriesCity = (newValue as String)
- true
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- args.preferenceToScrollTo?.let {
- scrollToPreference(it)
- }
- }
-
- override fun onDisplayPreferenceDialog(preference: Preference) {
- val handled = showCustomEditTextPreferenceDialog(preference)
-
- if (!handled) {
- super.onDisplayPreferenceDialog(preference)
- }
- }
-}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
index 6168f3da898a2..b94177a9e2868 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
@@ -75,7 +75,6 @@ import org.mozilla.fenix.termsofuse.TOU_VERSION
import org.mozilla.fenix.termsofuse.getApplicationInstalledTime
import org.mozilla.fenix.wallpapers.Wallpaper
import java.security.InvalidParameterException
-import java.util.UUID
import java.util.concurrent.TimeUnit.MILLISECONDS
private const val AUTOPLAY_USER_SETTING = "AUTOPLAY_USER_SETTING"
@@ -2066,47 +2065,6 @@ class Settings(
default = false,
)
- /**
- * Get the profile id to use in the sponsored stories communications with the Pocket endpoint.
- */
- val pocketSponsoredStoriesProfileId by stringPreference(
- appContext.getPreferenceKey(R.string.pref_key_pocket_sponsored_stories_profile),
- default = { UUID.randomUUID().toString() },
- persistDefaultIfNotExists = true,
- )
-
- /**
- * Whether or not to display the Pocket sponsored stories parameter secret settings.
- */
- var useCustomConfigurationForSponsoredStories by booleanPreference(
- appContext.getPreferenceKey(R.string.pref_key_custom_sponsored_stories_parameters_enabled),
- default = false,
- )
-
- /**
- * Site parameter used to set the spoc content.
- */
- var pocketSponsoredStoriesSiteId by stringPreference(
- appContext.getPreferenceKey(R.string.pref_key_custom_sponsored_stories_site_id),
- default = "",
- )
-
- /**
- * Country parameter used to set the spoc content.
- */
- var pocketSponsoredStoriesCountry by stringPreference(
- appContext.getPreferenceKey(R.string.pref_key_custom_sponsored_stories_country),
- default = "",
- )
-
- /**
- * City parameter used to set the spoc content.
- */
- var pocketSponsoredStoriesCity by stringPreference(
- appContext.getPreferenceKey(R.string.pref_key_custom_sponsored_stories_city),
- default = "",
- )
-
/**
* Indicates if the Contile functionality should be visible.
*/
diff --git a/mobile/android/fenix/app/src/main/res/navigation/nav_graph.xml b/mobile/android/fenix/app/src/main/res/navigation/nav_graph.xml
index 6d9888fee1010..9609bca393ab4 100644
--- a/mobile/android/fenix/app/src/main/res/navigation/nav_graph.xml
+++ b/mobile/android/fenix/app/src/main/res/navigation/nav_graph.xml
@@ -967,13 +967,6 @@
android:id="@+id/secretSettingsPreference"
android:name="org.mozilla.fenix.settings.SecretSettingsFragment"
android:label="@string/preferences_debug_settings">
-
-
-
-
diff --git a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml
index b69956f8c8c18..d2de296c8a739 100644
--- a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml
+++ b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml
@@ -427,7 +427,6 @@
pref_key_pocket_homescreen_recommendations
pref_key_pocket_sponsored_stories
- pref_key_pocket_sponsored_stories_profile
pref_key_secret_debug_info
@@ -439,11 +438,6 @@
pref_key_nimbus_use_preview
pref_key_history_metadata_feature
pref_key_custom_glean_server_url
- pref_key_custom_sponsored_stories_parameters
- pref_key_custom_sponsored_stories_parameters_enabled
- pref_key_custom_sponsored_stories_site_id
- pref_key_custom_sponsored_stories_country
- pref_key_custom_sponsored_stories_city
pref_key_enable_homepage_searchbar
pref_key_should_show_custom_tab_extensions
pref_key_enable_homepage_as_new_tab
diff --git a/mobile/android/fenix/app/src/main/res/values/static_strings.xml b/mobile/android/fenix/app/src/main/res/values/static_strings.xml
index a1aa71b26a9fe..4f6703eb352cb 100644
--- a/mobile/android/fenix/app/src/main/res/values/static_strings.xml
+++ b/mobile/android/fenix/app/src/main/res/values/static_strings.xml
@@ -45,16 +45,6 @@
Use Nimbus Preview Collection (requires restart)
Custom Glean server URL (requires restart)
-
- Pocket sponsored stories
-
- Enable custom Pocket sponsored stories parameters (requires restart)
-
- Site parameter
-
- Country parameter
-
- City parameter
Sync Debug
diff --git a/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml b/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml
index f6fb849b65a5b..2a98bf0850399 100644
--- a/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml
+++ b/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml
@@ -141,11 +141,6 @@
android:inputType="textUri"
app:useSimpleSummaryProvider="true"
app:iconSpaceReserved="false" />
-
-
-
-
-
-
-
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt
index c2a74996b688d..265fd6b99d40d 100644
--- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt
@@ -18,9 +18,6 @@ import mozilla.components.service.nimbus.messaging.MessageData
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.ContentRecommendation
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.PocketStory.SponsoredContentCallbacks
import mozilla.components.service.pocket.PocketStory.SponsoredContentFrequencyCaps
@@ -434,7 +431,6 @@ class AppStoreTest {
pocketStoriesCategories = listOf(mockk()),
pocketStoriesCategoriesSelections = listOf(mockk()),
pocketStories = listOf(mockk()),
- pocketSponsoredStories = listOf(mockk()),
contentRecommendations = listOf(mockk()),
sponsoredContents = listOf(mockk()),
),
@@ -446,90 +442,10 @@ class AppStoreTest {
assertTrue(appStore.state.recommendationState.pocketStoriesCategories.isEmpty())
assertTrue(appStore.state.recommendationState.pocketStoriesCategoriesSelections.isEmpty())
assertTrue(appStore.state.recommendationState.pocketStories.isEmpty())
- assertTrue(appStore.state.recommendationState.pocketSponsoredStories.isEmpty())
assertTrue(appStore.state.recommendationState.contentRecommendations.isEmpty())
assertTrue(appStore.state.recommendationState.sponsoredContents.isEmpty())
}
- @Test
- fun `GIVEN content recommendations are enabled WHEN updating the list of Pocket sponsored stories THEN the list of stories to show is updated`() = runTest {
- val baseRecommendation = mockk(name = "baseRec_PST").apply {
- every { url } returns "http://example.com/baseRecPST"
- every { title } returns "Base Recommendation PST Title"
- every { corpusItemId } returns "corpusId_PST"
- every { scheduledCorpusItemId } returns "scheduledId_PST"
- every { excerpt } returns "Base PST excerpt."
- every { topic } returns "Base PST Topic"
- every { publisher } returns "Base PST Publisher"
- every { isTimeSensitive } returns false
- every { imageUrl } returns "http://example.com/image_pst.jpg"
- every { tileId } returns 278L
- every { receivedRank } returns 1
- every { recommendedAt } returns System.currentTimeMillis() / 1000
- every { impressions } returns 0L
- }
-
- val shimMock = mockk(relaxed = true)
- val pocketSponsoredStoryCapsFilterOut = PocketSponsoredStoryCaps(
- currentImpressions = listOf(System.currentTimeMillis() / 1000),
- lifetimeCount = 1,
- flightCount = 1,
- flightPeriod = 86400,
- )
-
- val sponsoredStory1 = PocketSponsoredStory(
- id = 3,
- title = "Sponsored Story 1",
- url = "url_story1",
- imageUrl = "imageUrl_story1",
- sponsor = "Sponsor 1",
- shim = shimMock,
- priority = 33,
- caps = pocketSponsoredStoryCapsFilterOut,
- )
- val sponsoredStory2 = sponsoredStory1.copy(id = 4, imageUrl = "imageUrl_story2")
-
- appStore = AppStore(
- AppState(
- recommendationState = ContentRecommendationsState(
- contentRecommendations = listOf(baseRecommendation),
- ),
- ),
- )
-
- appStore.dispatch(
- ContentRecommendationsAction.PocketSponsoredStoriesChange(
- sponsoredStories = listOf(sponsoredStory1, sponsoredStory2),
- ),
- )
-
- assertTrue(
- appStore.state.recommendationState.pocketSponsoredStories.containsAll(
- listOf(sponsoredStory1, sponsoredStory2),
- ),
- )
- assertEquals(
- listOf(baseRecommendation),
- appStore.state.recommendationState.pocketStories,
- )
-
- val updatedSponsoredStories = listOf(sponsoredStory1.copy(id = 5, title = "Updated Sponsored Story"))
-
- appStore.dispatch(
- ContentRecommendationsAction.PocketSponsoredStoriesChange(
- sponsoredStories = updatedSponsoredStories,
- ),
- )
-
- assertTrue(
- appStore.state.recommendationState.pocketSponsoredStories.containsAll(updatedSponsoredStories),
- )
- assertEquals(
- listOf(baseRecommendation),
- appStore.state.recommendationState.pocketStories,
- )
- }
-
@Test
fun `GIVEN content recommendations are enabled WHEN updating the list of sponsored contents THEN update the list of stories to show`() = runTest {
val baseRecommendation = mockk(name = "baseRec_277").apply {
@@ -616,50 +532,6 @@ class AppStoreTest {
)
}
- @Test
- fun `Test updating sponsored Pocket stories after being shown to the user`() = runTest {
- val story1 = PocketSponsoredStory(
- id = 3,
- title = "title",
- url = "url",
- imageUrl = "imageUrl",
- sponsor = "sponsor",
- shim = mockk(),
- priority = 33,
- caps = PocketSponsoredStoryCaps(
- currentImpressions = listOf(1, 2),
- lifetimeCount = 11,
- flightCount = 2,
- flightPeriod = 11,
- ),
- )
- val story2 = story1.copy(id = 22)
- val story3 = story1.copy(id = 33)
- val story4 = story1.copy(id = 44)
- appStore = AppStore(
- AppState(
- recommendationState = ContentRecommendationsState(
- pocketSponsoredStories = listOf(story1, story2, story3, story4),
- ),
- ),
- )
-
- appStore.dispatch(
- ContentRecommendationsAction.PocketStoriesShown(
- impressions = listOf(
- PocketImpression(story = story1, position = 0),
- PocketImpression(story = story3, position = 2),
- ),
- ),
- )
-
- assertEquals(4, appStore.state.recommendationState.pocketSponsoredStories.size)
- assertEquals(3, appStore.state.recommendationState.pocketSponsoredStories[0].caps.currentImpressions.size)
- assertEquals(2, appStore.state.recommendationState.pocketSponsoredStories[1].caps.currentImpressions.size)
- assertEquals(3, appStore.state.recommendationState.pocketSponsoredStories[2].caps.currentImpressions.size)
- assertEquals(2, appStore.state.recommendationState.pocketSponsoredStories[3].caps.currentImpressions.size)
- }
-
@Test
fun `WHEN sponsored contents are shown THEN update the impressions of sponsored contents`() = runTest {
val sponsoredContent = SponsoredContent(
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt
index cd2d086254ef4..99d8893def1e2 100644
--- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt
@@ -8,9 +8,6 @@ import io.mockk.every
import io.mockk.mockk
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
-import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim
import mozilla.components.service.pocket.PocketStory.SponsoredContent
import mozilla.components.service.pocket.PocketStory.SponsoredContentCallbacks
import mozilla.components.service.pocket.PocketStory.SponsoredContentFrequencyCaps
@@ -23,7 +20,6 @@ import org.junit.Test
import org.mozilla.fenix.TestUtils.getFakeContentRecommendations
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.appstate.recommendations.ContentRecommendationsState
-import org.mozilla.fenix.components.appstate.recommendations.copyWithRecommendationsState
import org.mozilla.fenix.home.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
@@ -84,70 +80,6 @@ class AppStateTest {
assertEquals(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT, result.size)
}
- @Test
- fun `GIVEN no category is selected and 1 sponsored story available WHEN getFilteredStories is called THEN get stories from the default category combined with the sponsored one`() {
- val defaultStoriesCategoryWithManyStories = PocketRecommendedStoriesCategory(
- POCKET_STORIES_DEFAULT_CATEGORY_NAME,
- getFakePocketStories(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT),
- )
- val sponsoredStories = getFakeSponsoredStories(1)
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketStoriesCategories = listOf(
- otherStoriesCategory,
- anotherStoriesCategory,
- defaultStoriesCategoryWithManyStories,
- ),
- pocketSponsoredStories = sponsoredStories,
- ),
- )
-
- val result = state.getFilteredStories().toMutableList()
-
- assertEquals(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT, result.size)
- assertEquals(sponsoredStories[0], result[1]) // second story should be a sponsored one
- result.removeAt(1) // remove the sponsored story to hopefully only remain with general recommendations
- assertNull(
- result.firstOrNull {
- it is PocketRecommendedStory && it.category != POCKET_STORIES_DEFAULT_CATEGORY_NAME
- },
- )
- }
-
- @Test
- fun `GIVEN no category is selected and 2 sponsored stories available WHEN getFilteredStories is called THEN get stories from the default category combined with the sponsored stories`() {
- val defaultStoriesCategoryWithManyStories = PocketRecommendedStoriesCategory(
- POCKET_STORIES_DEFAULT_CATEGORY_NAME,
- getFakePocketStories(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT),
- )
- val sponsoredStories = getFakeSponsoredStories(4)
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketStoriesCategories = listOf(
- otherStoriesCategory,
- anotherStoriesCategory,
- defaultStoriesCategoryWithManyStories,
- ),
- pocketSponsoredStories = sponsoredStories,
- ),
- )
-
- val result = state.getFilteredStories().toMutableList()
-
- assertEquals(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT, result.size)
- // second story should be a sponsored one
- assertEquals(sponsoredStories[1], result[1])
- assertEquals(sponsoredStories[3], result[8])
- // remove the sponsored stories to hopefully only remain with general recommendations
- result.removeAt(7)
- result.removeAt(1)
- assertNull(
- result.firstOrNull {
- it is PocketRecommendedStory && it.category != POCKET_STORIES_DEFAULT_CATEGORY_NAME
- },
- )
- }
-
@Test
fun `GIVEN no category is selected and sponsored contents are available WHEN getFilteredStories is called THEN return stories from the default category combined with the sponsored contents`() {
val defaultStoriesCategoryWithManyStories = PocketRecommendedStoriesCategory(
@@ -166,7 +98,7 @@ class AppStateTest {
),
)
- var result = state.getFilteredStories(useSponsoredStoriesState = false).toMutableList()
+ var result = state.getFilteredStories().toMutableList()
assertEquals(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT, result.size)
assertEquals(sponsoredContents[1], result[1])
@@ -181,73 +113,6 @@ class AppStateTest {
)
}
- @Test
- fun `GIVEN a list of sponsored stories WHEN filtering them THEN have them ordered by priority`() {
- val stories = getFakeSponsoredStories(4).mapIndexed { index, story ->
- story.copy(priority = index)
- }
-
- val result = getFilteredSponsoredStories(stories, 10)
-
- assertEquals(4, result.size)
- assertEquals(stories.reversed(), result)
- }
-
- @Test
- fun `GIVEN a list of sponsored stories WHEN filtering them THEN drop the ones already shown for the maximum number of times in lifetime`() {
- val stories = getFakeSponsoredStories(4).mapIndexed { index, story ->
- when (index % 2 == 0) {
- true -> story.copy(
- caps = story.caps.copy(
- currentImpressions = listOf(1, 2, 3),
- lifetimeCount = 3,
- ),
- )
- false -> story
- }
- }
-
- val result = getFilteredSponsoredStories(stories, 10)
-
- assertEquals(2, result.size)
- assertEquals(stories[1], result[0])
- assertEquals(stories[3], result[1])
- }
-
- @Test
- fun `GIVEN a list of sponsored stories WHEN filtering them THEN drop the ones already shown for the maximum number of times in flight`() {
- val stories = getFakeSponsoredStories(4).mapIndexed { index, story ->
- when (index % 2 == 0) {
- true -> story
- false -> story.copy(
- caps = story.caps.copy(
- currentImpressions = listOf(
- TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
- TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
- TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
- ),
- flightCount = 3,
- ),
- )
- }
- }
-
- val result = getFilteredSponsoredStories(stories, 10)
-
- assertEquals(2, result.size)
- assertEquals(stories[0], result[0])
- assertEquals(stories[2], result[1])
- }
-
- @Test
- fun `GIVEN a list of sponsored stories WHEN filtering them THEN return up to limit of stories asked`() {
- val stories = getFakeSponsoredStories(4)
-
- val result = getFilteredSponsoredStories(stories, 2)
-
- assertEquals(2, result.size)
- }
-
@Test
fun `WHEN filtering the sponsored contents THEN return the list of sponsored contents sorted by descending priority`() {
val sponsoredContents = getFakeSponsoredContents(4).mapIndexed { index, sponsoredContent ->
@@ -291,135 +156,6 @@ class AppStateTest {
assertEquals(2, result.size)
}
- @Test
- fun `GIVEN multiple stories of both types WHEN combining them THEN show sponsored stories at position 2 and 9`() {
- val recommendedStories = getFakePocketStories(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT, "other")
- val sponsoredStories = getFakeSponsoredStories(4)
-
- val result = combineRecommendationsAndSponsoredContents(recommendedStories, sponsoredStories)
-
- assertCombinedStories(
- recommendedStories = recommendedStories,
- sponsoredStories = sponsoredStories,
- result = result,
- )
- }
-
- @Test
- fun `GIVEN content recommendations and sponsored stories WHEN combining them THEN show sponsored stories at position 2 and 9`() {
- val recommendedStories = getFakeContentRecommendations(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT)
- val sponsoredStories = getFakeSponsoredStories(4)
-
- val result = combineRecommendationsAndSponsoredContents(
- recommendedStories,
- sponsoredStories,
- )
-
- assertCombinedStories(
- recommendedStories = recommendedStories,
- sponsoredStories = sponsoredStories,
- result = result,
- )
- }
-
- @Test
- fun `GIVEN a category is selected and 1 sponsored story is available WHEN getFilteredStories is called THEN only stories from that category and the sponsored story are returned`() {
- val sponsoredStories = getFakeSponsoredStories(1)
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory),
- pocketStoriesCategoriesSelections = listOf(PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name)),
- pocketSponsoredStories = sponsoredStories,
- ),
- )
-
- val result = state.getFilteredStories().toMutableList()
-
- assertEquals(4, result.size)
- assertEquals(sponsoredStories[0], result[1]) // second story should be a sponsored one
- // remove the sponsored story to hopefully only remain with stories from the selected category
- result.removeAt(1)
- assertNull(
- result.firstOrNull {
- it is PocketRecommendedStory && it.category != otherStoriesCategory.name
- },
- )
- }
-
- @Test
- fun `GIVEN two categories selected and 1 sponsored story available WHEN getFilteredStories is called THEN only stories from the selected categories and the sponsored story are returned`() {
- val sponsoredStories = getFakeSponsoredStories(1)
- val yetAnotherStoriesCategory =
- PocketRecommendedStoriesCategory("yetAnother", getFakePocketStories(3, "yetAnother"))
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketStoriesCategories = listOf(
- otherStoriesCategory,
- anotherStoriesCategory,
- yetAnotherStoriesCategory,
- defaultStoriesCategory,
- ),
- pocketStoriesCategoriesSelections = listOf(
- PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name),
- PocketRecommendedStoriesSelectedCategory(anotherStoriesCategory.name),
- ),
- pocketSponsoredStories = sponsoredStories,
- ),
- )
-
- val result = state.getFilteredStories().toMutableList()
-
- // Only 7 stories available: 3*2 stories from the selected categories plus one sponsored story
- assertEquals(7, result.size)
- assertEquals(sponsoredStories[0], result[1]) // second story should be a sponsored one
- // remove the sponsored story to hopefully only remain with stories from the selected category
- result.removeAt(1)
- assertNull(
- result.firstOrNull {
- (it !is PocketRecommendedStory) ||
- (it.category != otherStoriesCategory.name && it.category != anotherStoriesCategory.name)
- },
- )
- }
-
- @Test
- fun `GIVEN two categories selected and 2 sponsored stories available WHEN getFilteredStories is called THEN no more than the default stories number are returned`() {
- val sponsoredStories = getFakeSponsoredStories(2)
- val yetAnotherStoriesCategory =
- PocketRecommendedStoriesCategory("yetAnother", getFakePocketStories(40, "yetAnother"))
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketStoriesCategories = listOf(
- otherStoriesCategory,
- anotherStoriesCategory,
- yetAnotherStoriesCategory,
- defaultStoriesCategory,
- ),
- pocketStoriesCategoriesSelections = listOf(
- PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name),
- PocketRecommendedStoriesSelectedCategory(yetAnotherStoriesCategory.name),
- ),
- pocketSponsoredStories = sponsoredStories,
- ),
- )
-
- val result = state.getFilteredStories().toMutableList()
-
- assertEquals(TOTAL_CONTENT_RECOMMENDATIONS_TO_SHOW_COUNT, result.size)
- // 2nd and 9th story should be sponsored stories
- assertEquals(sponsoredStories[1], result[SPONSORED_STORIES_INDEXES[0]])
- assertEquals(sponsoredStories[0], result[SPONSORED_STORIES_INDEXES[1]])
- // remove the sponsored stories to hopefully only remain with stories from the selected categories
- result.removeAt(SPONSORED_STORIES_INDEXES[1])
- result.removeAt(SPONSORED_STORIES_INDEXES[0])
- assertNull(
- result.firstOrNull {
- (it !is PocketRecommendedStory) ||
- (it.category != otherStoriesCategory.name && it.category != yetAnotherStoriesCategory.name)
- },
- )
- }
-
@Test
fun `GIVEN a category is selected WHEN getFilteredStories is called THEN no more than the default stories number are returned from the selected category`() {
val otherStoriesCategoryWithManyStories =
@@ -649,31 +385,6 @@ class AppStateTest {
)
}
- @Test
- fun `GIVEN a content recommendations state update WHEN copying the content recommendations state THEN return the updated state`() {
- val sponsoredStories = getFakeSponsoredStories(1)
- val pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory)
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketStoriesCategories = pocketStoriesCategories,
- ),
- )
-
- assertEquals(0, state.recommendationState.pocketStories.size)
- assertEquals(pocketStoriesCategories, state.recommendationState.pocketStoriesCategories)
- assertEquals(0, state.recommendationState.pocketStoriesCategoriesSelections.size)
- assertEquals(0, state.recommendationState.pocketSponsoredStories.size)
-
- val newState = state.copyWithRecommendationsState {
- it.copy(pocketSponsoredStories = sponsoredStories)
- }
-
- assertEquals(0, newState.recommendationState.pocketStories.size)
- assertEquals(pocketStoriesCategories, newState.recommendationState.pocketStoriesCategories)
- assertEquals(0, newState.recommendationState.pocketStoriesCategoriesSelections.size)
- assertEquals(sponsoredStories, newState.recommendationState.pocketSponsoredStories)
- }
-
@Test
fun `GIVEN content recommendations with no sponsored stories WHEN getStories is called THEN return a list of content recommendations to displayed sorted by the number of impressions`() {
val recommendations = getFakeContentRecommendations(40)
@@ -693,27 +404,6 @@ class AppStateTest {
)
}
- @Test
- fun `GIVEN content recommendations and sponsored stories WHEN getStories is called THEN return a list of 30 stories with sponsored stories at position 2 and 9`() {
- val recommendations = getFakeContentRecommendations(40)
- val sponsoredStories = getFakeSponsoredStories(4)
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketSponsoredStories = sponsoredStories,
- contentRecommendations = recommendations,
- ),
- )
-
- val result = state.getStories()
-
- assertCombinedStories(
- recommendedStories = recommendations,
- sponsoredStories = sponsoredStories,
- sponsoredStoriesIndexes = listOf(1, 3),
- result = result,
- )
- }
-
@Test
fun `GIVEN content recommendations and sponsored contents WHEN getStories is called THEN return a list of 40 stories with sponsored contents at position 2 and 9`() {
val recommendations = getFakeContentRecommendations(40)
@@ -725,7 +415,7 @@ class AppStateTest {
),
)
- val result = state.getStories(useSponsoredStoriesState = false)
+ val result = state.getStories()
assertCombinedStories(
recommendedStories = recommendations,
@@ -735,27 +425,6 @@ class AppStateTest {
)
}
- @Test
- fun `GIVEN content recommendations and 1 sponsored story WHEN getStories is called THEN return a list of stories with sponsored stories at position 2`() {
- val recommendations = getFakeContentRecommendations(4)
- val sponsoredContents = getFakeSponsoredStories(1)
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketSponsoredStories = sponsoredContents,
- contentRecommendations = recommendations,
- ),
- )
-
- val result = state.getStories()
-
- assertEquals(5, result.size)
- assertEquals(recommendations[0], result[0])
- assertEquals(sponsoredContents[0], result[1])
- assertEquals(recommendations[1], result[2])
- assertEquals(recommendations[2], result[3])
- assertEquals(recommendations[3], result[4])
- }
-
@Test
fun `GIVEN content recommendations and 1 sponsored content WHEN getStories is called THEN return a list of stories with sponsored content at position 2`() {
val recommendations = getFakeContentRecommendations(4)
@@ -767,7 +436,7 @@ class AppStateTest {
),
)
- val result = state.getStories(useSponsoredStoriesState = false)
+ val result = state.getStories()
assertEquals(5, result.size)
assertEquals(recommendations[0], result[0])
@@ -777,28 +446,6 @@ class AppStateTest {
assertEquals(recommendations[3], result[4])
}
- @Test
- fun `GIVEN content recommendations and 2 sponsored story WHEN getStories is called THEN return a list of stories with sponsored stories at position 2 and 6`() {
- val recommendations = getFakeContentRecommendations(4)
- val sponsoredStories = getFakeSponsoredStories(2)
- val state = AppState(
- recommendationState = ContentRecommendationsState(
- pocketSponsoredStories = sponsoredStories,
- contentRecommendations = recommendations,
- ),
- )
-
- val result = state.getStories()
-
- assertEquals(6, result.size)
- assertEquals(recommendations[0], result[0])
- assertEquals(sponsoredStories[1], result[1])
- assertEquals(recommendations[1], result[2])
- assertEquals(recommendations[2], result[3])
- assertEquals(recommendations[3], result[4])
- assertEquals(sponsoredStories[0], result[5])
- }
-
@Test
fun `GIVEN content recommendations and 2 sponsored contents WHEN getStories is called THEN return a list of stories with sponsored contents at position 2 and 6`() {
val recommendations = getFakeContentRecommendations(4)
@@ -810,7 +457,7 @@ class AppStateTest {
),
)
- val result = state.getStories(useSponsoredStoriesState = false)
+ val result = state.getStories()
assertEquals(6, result.size)
assertEquals(recommendations[0], result[0])
@@ -919,30 +566,6 @@ private fun getFakePocketStories(
}
}
-private fun getFakeSponsoredStories(limit: Int) = mutableListOf().apply {
- for (index in 0 until limit) {
- add(
- PocketSponsoredStory(
- id = index,
- title = "Story title $index",
- url = "https://sponsored.story",
- imageUrl = "https://sponsored.image",
- sponsor = "Sponsor $index",
- shim = PocketSponsoredStoryShim(
- click = "Story title $index click shim",
- impression = "Story title $index impression shim",
- ),
- priority = 2 + index % 2,
- caps = PocketSponsoredStoryCaps(
- lifetimeCount = 1 + index * 5,
- flightCount = 1 + index * 2,
- flightPeriod = 1 + index * 3,
- ),
- ),
- )
- }
-}
-
private fun getFakeSponsoredContents(limit: Int) = mutableListOf().apply {
for (index in 0 until limit) {
add(
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt
index e0892ecf5c56c..7e6c7c4d62448 100644
--- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt
@@ -298,52 +298,6 @@ class PocketMiddlewareTest {
}
}
- /**
- * Assert that the Pocket categories with [expected] names are currently selected
- * and that this selection happened in the past 10 seconds.
- */
- private fun FakeDataStore.assertSelectedCategories(vararg expected: String) {
- val now = System.currentTimeMillis()
- val actualSelections = currentCategorySelection.valuesList
- assertEquals(expected.size, actualSelections.size)
- actualSelections.forEachIndexed { index, selection ->
- assertEquals(expected[index], selection.name)
- assertTrue(selection.selectionTimestamp in now - 10000..now)
- }
- }
-
- /**
- * Assert that the Pocket categories with [expected] names are currently selected
- * and that this selection happened in the past 10 seconds.
- */
- private fun AppStore.assertSelectedCategories(vararg expected: String) {
- val now = System.currentTimeMillis()
- val actualSelections = state.recommendationState.pocketStoriesCategoriesSelections
- assertEquals(expected.size, actualSelections.size)
- actualSelections.forEachIndexed { index, selection ->
- assertEquals(expected[index], selection.name)
- assertTrue(selection.selectionTimestamp in now - 10000..now)
- }
- }
-
- @Test
- fun `GIVEN hasPocketSponsoredStoriesProfileMigrated is true WHEN App is Started THEN don't try to delete the old Pocket profile`() = runTest {
- val pocketService: PocketStoriesService = mockk(relaxed = true)
- val pocketMiddleware = PocketMiddleware(
- lazy { pocketService },
- mockk(),
- FakePocketSettings(),
- RunWhenReadyQueue(this).also { it.ready() },
- this,
- )
-
- pocketMiddleware.invoke(mockk(), {}, AppAction.AppLifecycleAction.StartAction)
-
- verify(exactly = 0) {
- pocketService.deleteProfile()
- }
- }
-
@Test
fun `GIVEN pocket settings are true WHEN App is Started THEN start the Pocket workers`() = runTest {
val pocketService: PocketStoriesService = mockk(relaxed = true)
@@ -423,3 +377,31 @@ data class FakePocketSettings(
override val showPocketRecommendationsFeature: Boolean = true,
override val showPocketSponsoredStories: Boolean = true,
) : PocketSettings
+
+/**
+ * Assert that the Pocket categories with [expected] names are currently selected
+ * and that this selection happened in the past 10 seconds.
+ */
+private fun FakeDataStore.assertSelectedCategories(vararg expected: String) {
+ val now = System.currentTimeMillis()
+ val actualSelections = currentCategorySelection.valuesList
+ assertEquals(expected.size, actualSelections.size)
+ actualSelections.forEachIndexed { index, selection ->
+ assertEquals(expected[index], selection.name)
+ assertTrue(selection.selectionTimestamp in now - 10000..now)
+ }
+}
+
+/**
+ * Assert that the Pocket categories with [expected] names are currently selected
+ * and that this selection happened in the past 10 seconds.
+ */
+private fun AppStore.assertSelectedCategories(vararg expected: String) {
+ val now = System.currentTimeMillis()
+ val actualSelections = state.recommendationState.pocketStoriesCategoriesSelections
+ assertEquals(expected.size, actualSelections.size)
+ actualSelections.forEachIndexed { index, selection ->
+ assertEquals(expected[index], selection.name)
+ assertTrue(selection.selectionTimestamp in now - 10000..now)
+ }
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt
index 60f076f1e10d7..f34083ebd8b20 100644
--- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt
@@ -248,44 +248,6 @@ class DefaultPocketStoriesControllerTest {
}
}
- @Test
- fun `WHEN a new sponsored story is shown THEN update the State and record telemetry`() {
- val store = spyk(AppStore())
- val controller = createController(appStore = store)
- val storyShown: PocketSponsoredStory = mockk {
- every { shim.click } returns "testClickShim"
- every { shim.impression } returns "testImpressionShim"
- every { id } returns 123
- every { caps } returns storyCapsForShownTest
- }
- var wasPingSent = false
- val job = Pings.spoc.testBeforeNextSubmit { reason ->
- assertEquals(storyShown.shim.impression, Pocket.spocShim.testGetValue())
- assertEquals(Pings.spocReasonCodes.impression.name, reason?.name)
- wasPingSent = true
- }
-
- controller.handleStoryShown(storyShown, storyPosition = Triple(1, 2, 3))
- job.join()
-
- verify {
- store.dispatch(
- ContentRecommendationsAction.PocketStoriesShown(
- impressions = listOf(
- PocketImpression(story = storyShown, position = 3),
- ),
- ),
- )
- }
- assertNotNull(Pocket.homeRecsSpocShown.testGetValue())
- assertEquals(1, Pocket.homeRecsSpocShown.testGetValue()!!.size)
- val data = Pocket.homeRecsSpocShown.testGetValue()!!.single().extra
- assertEquals("123", data?.entries?.first { it.key == "spoc_id" }?.value)
- assertEquals("1x2", data?.entries?.first { it.key == "position" }?.value)
- assertEquals("4", data?.entries?.first { it.key == "times_shown" }?.value)
- assertTrue(wasPingSent)
- }
-
@Test
fun `WHEN a sponsored content is shown THEN update the State and record telemetry`() {
val store = spyk(AppStore())
@@ -425,102 +387,6 @@ class DefaultPocketStoriesControllerTest {
assertEquals(story.timesShown.inc().toString(), event.single().extra!!["times_shown"])
}
- @Test
- fun `WHEN a sponsored story is clicked THEN open that story's url using HomeActivity and record telemetry`() {
- val storyClicked = PocketSponsoredStory(
- id = 7,
- title = "",
- url = "testLink",
- imageUrl = "",
- sponsor = "",
- shim = mockk {
- every { click } returns "testClickShim"
- every { impression } returns "testImpressionShim"
- },
- priority = 3,
- caps = storyCapsForClickedTest,
- )
- val controller = createController()
- var wasPingSent = false
- assertNull(Pocket.homeRecsSpocClicked.testGetValue())
-
- // Test that the spoc ping is immediately sent with the needed data.
- val job = Pings.spoc.testBeforeNextSubmit { reason ->
- assertEquals(storyClicked.shim.click, Pocket.spocShim.testGetValue())
- assertEquals(Pings.spocReasonCodes.click.name, reason?.name)
- wasPingSent = true
- }
-
- controller.handleStoryClicked(storyClicked, storyPosition = Triple(2, 3, 4))
- job.join()
-
- verify {
- navController.navigate(R.id.browserFragment)
- fenixBrowserUseCases.loadUrlOrSearch(
- searchTermOrURL = storyClicked.url,
- newTab = true,
- private = false,
- )
- }
-
- assertNotNull(Pocket.homeRecsSpocClicked.testGetValue())
- assertEquals(1, Pocket.homeRecsSpocClicked.testGetValue()!!.size)
- val data = Pocket.homeRecsSpocClicked.testGetValue()!!.single().extra
- assertEquals("7", data?.entries?.first { it.key == "spoc_id" }?.value)
- assertEquals("2x3", data?.entries?.first { it.key == "position" }?.value)
- assertEquals("3", data?.entries?.first { it.key == "times_shown" }?.value)
- assertTrue(wasPingSent)
- }
-
- @Test
- fun `GIVEN homepage as a new tab is enabled WHEN a sponsored story is clicked THEN navigate to the sponsored story's url and record the interaction`() {
- every { settings.enableHomepageAsNewTab } returns true
-
- val storyClicked = PocketSponsoredStory(
- id = 7,
- title = "",
- url = "testLink",
- imageUrl = "",
- sponsor = "",
- shim = mockk {
- every { click } returns "testClickShim"
- every { impression } returns "testImpressionShim"
- },
- priority = 3,
- caps = storyCapsForClickedTest,
- )
- val controller = createController()
- var wasPingSent = false
- assertNull(Pocket.homeRecsSpocClicked.testGetValue())
-
- // Test that the spoc ping is immediately sent with the needed data.
- val job = Pings.spoc.testBeforeNextSubmit { reason ->
- assertEquals(storyClicked.shim.click, Pocket.spocShim.testGetValue())
- assertEquals(Pings.spocReasonCodes.click.name, reason?.name)
- wasPingSent = true
- }
-
- controller.handleStoryClicked(storyClicked, storyPosition = Triple(2, 3, 4))
- job.join()
-
- verify {
- navController.navigate(R.id.browserFragment)
- fenixBrowserUseCases.loadUrlOrSearch(
- searchTermOrURL = storyClicked.url,
- newTab = false,
- private = false,
- )
- }
-
- assertNotNull(Pocket.homeRecsSpocClicked.testGetValue())
- assertEquals(1, Pocket.homeRecsSpocClicked.testGetValue()!!.size)
- val data = Pocket.homeRecsSpocClicked.testGetValue()!!.single().extra
- assertEquals("7", data?.entries?.first { it.key == "spoc_id" }?.value)
- assertEquals("2x3", data?.entries?.first { it.key == "position" }?.value)
- assertEquals("3", data?.entries?.first { it.key == "times_shown" }?.value)
- assertTrue(wasPingSent)
- }
-
@Test
fun `WHEN a sponsored content is clicked THEN navigate to the sponsored content URL and record the interaction`() {
val sponsoredContent = SponsoredContent(
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
index 4575dfd8fb2ef..426b5d39e8866 100644
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -9581,16 +9581,6 @@
mirror: always
do_not_use_directly: true
-# Use the JS microtask queue for Promise jobs and other microtasks.
-- name: javascript.options.use_js_microtask_queue
- type: RelaxedAtomicBool
- value: true
- # Changing this mid process will break invariants and crash, however
- # making this mirror: once, and set_spidermonkey_pref: startup is
- # broken: See Bug 1989094
- mirror: always
- set_spidermonkey_pref: always
-
# Whether to use the off-thread script compilation and decoding.
- name: javascript.options.parallel_parsing
type: bool
diff --git a/mots.yaml b/mots.yaml
index 557039e0290fb..c5929a538ce15 100644
--- a/mots.yaml
+++ b/mots.yaml
@@ -8,7 +8,7 @@
# documentation and how to modify this file.
repo: mozilla-central
created_at: '2021-10-14T12:50:40.073465'
-updated_at: '2026-02-02T15:13:45.743395+00:00'
+updated_at: '2026-02-04T21:37:00.601823+00:00'
export:
path: ./docs/mots/index.rst
format: rst
@@ -201,6 +201,10 @@ people:
bmo_id: 525693
name: Nazım Can Altınova
nick: canova
+ - &cdupuis
+ bmo_id: 760375
+ name: Chris DuPuis
+ nick: cdupuis
- &charlie
bmo_id: 710471
name: Charlie Humphreys
@@ -3550,13 +3554,16 @@ modules:
includes:
- toolkit/mozapps/update/**/*
owners:
- - *bytesized
+ - *cdupuis
peers:
- - *molly
+ - *bytesized
machine_name: application_update
meta:
+ owners_emeritus:
+ - Robin Steuber
peers_emeritus:
- Adam Gashlin
+ - Molly Howell
review_group: application-update-reviewers
@@ -4011,8 +4018,10 @@ modules:
components:
- Firefox::Installer
review_group: browser-installer-reviewers
+ owners_emeritus:
+ - Molly Howell
owners:
- - *molly
+ - *cdupuis
peers:
- *agashlin
- *nalexander
@@ -4707,5 +4716,5 @@ modules:
- Ryan Tilder
group: dev-platform
hashes:
- config: 2db07f7fa261720687b12b3253afccfda3a3ebfe
- export: 8ca12e00a0c82974b57bbb2a3df9ad5ce251ca61
+ config: dca905ad82c5bb43b44a4db068dfe988b592fe13
+ export: bbece186da876a62b4777ffa92f8241e22d5630a
diff --git a/taskcluster/android_taskgraph/transforms/build_android_app.py b/taskcluster/android_taskgraph/transforms/build_android_app.py
index 034f33b6232dc..fc194c737d6e2 100644
--- a/taskcluster/android_taskgraph/transforms/build_android_app.py
+++ b/taskcluster/android_taskgraph/transforms/build_android_app.py
@@ -121,7 +121,6 @@ def _get_secrets_keys_and_target_files(task):
f"app/src/{gradle_build_type}/res/values/firebase.xml",
),
("wallpaper_url", ".wallpaper_url"),
- ("pocket_consumer_key", ".pocket_consumer_key"),
])
return secrets
diff --git a/testing/mozbase/moztest/moztest/resolve.py b/testing/mozbase/moztest/moztest/resolve.py
index 02a2a5cabd2fd..7dcd84f41a8bf 100644
--- a/testing/mozbase/moztest/moztest/resolve.py
+++ b/testing/mozbase/moztest/moztest/resolve.py
@@ -1209,8 +1209,6 @@ def add_wpt_manifest_data(self):
] # full path on disk until web-platform tests directory
for test_type, path, tests in manifest:
- full_path = mozpath.join(tests_root, path)
- src_path = mozpath.relpath(full_path, self.topsrcdir)
if test_type not in WPT_TYPES:
continue
diff --git a/third_party/rust/error-support/.cargo-checksum.json b/third_party/rust/error-support/.cargo-checksum.json
index 4200b097edfe0..0f8f944dd14bb 100644
--- a/third_party/rust/error-support/.cargo-checksum.json
+++ b/third_party/rust/error-support/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"7f193fead888139c98e8b06e03dc737b7c9a7c24702326a6abd06cd14cc557e9","README.md":"d820e387ac36a98c8e554fcaba13b76eb935413b535019444f6d448240e4d07e","metrics.yaml":"d7404186be19150cfba00c7ffb4261479fd91f3b674151ed88d28d15172283b2","pings.yaml":"80e8cae15ec4b9369d92ffc2fd038931a108cfb1edcacd6f27e529a809979043","src/error_tracing.rs":"1982a6772f3d25ad8563cf9a9328dc8b276d1d3ef280546bbd6c7630367b3181","src/handling.rs":"4b1183afe0716653918515299bc877a4d461fe8d2bb114a8c25f303203a35fdb","src/lib.rs":"f3860585d0cf613fe2974c5119f0738601c65c74aaba2c79311e440903dde254","src/macros.rs":"27366e0424e4d700605c34bd96295cd35fa41aed8b49f30e0b6e0c59b870fe73","src/redact.rs":"c9a4df1a87be68b15d583587bda941d4c60a1d0449e2d43ff99f3611a290a863","src/reporting.rs":"eda5580fabe633bd4fe7ac69ea8056874e8adfc093a74942294edcfbaa48824f","uniffi.toml":"af91bcd8e7b1fa3f475a5e556979ff23c57b338395e0b65abc1cb1a0ee823e23"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"0e170ec1223b8e519342a24e0f13b723dc9b9a6795d4388645d68aad0c4e405f","README.md":"d820e387ac36a98c8e554fcaba13b76eb935413b535019444f6d448240e4d07e","metrics.yaml":"d7404186be19150cfba00c7ffb4261479fd91f3b674151ed88d28d15172283b2","pings.yaml":"80e8cae15ec4b9369d92ffc2fd038931a108cfb1edcacd6f27e529a809979043","src/error_tracing.rs":"1982a6772f3d25ad8563cf9a9328dc8b276d1d3ef280546bbd6c7630367b3181","src/handling.rs":"4b1183afe0716653918515299bc877a4d461fe8d2bb114a8c25f303203a35fdb","src/lib.rs":"0c8e91b271dfc34c5b188dc6271843f513ddc96ecb44c38a91b6181d5b1a6b46","src/macros.rs":"27366e0424e4d700605c34bd96295cd35fa41aed8b49f30e0b6e0c59b870fe73","src/redact.rs":"c9a4df1a87be68b15d583587bda941d4c60a1d0449e2d43ff99f3611a290a863","uniffi.toml":"fa676d148b909a75dd03c4ab55b6b5df885e02efb2e7d7c445b27ccd924307c6"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/error-support/Cargo.toml b/third_party/rust/error-support/Cargo.toml
index 5e8abb52c97f8..ad3da6a1033d2 100644
--- a/third_party/rust/error-support/Cargo.toml
+++ b/third_party/rust/error-support/Cargo.toml
@@ -27,14 +27,6 @@ license = "MPL-2.0"
[features]
backtrace = ["dep:backtrace"]
testing = []
-tracing-logging = [
- "dep:tracing",
- "dep:tracing-support",
-]
-tracing-reporting = [
- "dep:tracing",
- "dep:tracing-support",
-]
[lib]
name = "error_support"
@@ -66,7 +58,6 @@ optional = true
[dependencies.tracing-support]
path = "../tracing"
-optional = true
[dependencies.uniffi]
version = "0.31"
diff --git a/third_party/rust/error-support/src/lib.rs b/third_party/rust/error-support/src/lib.rs
index 4ce236d96aa43..e51eebf89b6b5 100644
--- a/third_party/rust/error-support/src/lib.rs
+++ b/third_party/rust/error-support/src/lib.rs
@@ -3,31 +3,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// kinda abusing features here, but features "override" builtin support.
-#[cfg(not(feature = "tracing-logging"))]
-pub use log::{debug, error, info, trace, warn, Level};
-
-#[cfg(feature = "tracing-logging")]
pub use tracing_support::{debug, error, info, trace, warn, Level};
-#[cfg(all(feature = "testing", not(feature = "tracing-logging")))]
-pub fn init_for_tests() {
- let _ = env_logger::try_init();
-}
-
-#[cfg(all(feature = "testing", not(feature = "tracing-logging")))]
-pub fn init_for_tests_with_level(level: Level) {
- // There's gotta be a better way :(
- let level_name = match level {
- Level::Debug => "debug",
- Level::Trace => "trace",
- Level::Info => "info",
- Level::Warn => "warn",
- Level::Error => "error",
- };
- env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(level_name)).init();
-}
-
-#[cfg(all(feature = "testing", feature = "tracing-logging"))]
+#[cfg(feature = "testing")]
pub use tracing_support::{init_for_tests, init_for_tests_with_level};
mod macros;
@@ -67,17 +45,7 @@ pub mod backtrace {
mod redact;
pub use redact::*;
-#[cfg(not(feature = "tracing-reporting"))]
-mod reporting;
-#[cfg(not(feature = "tracing-reporting"))]
-pub use reporting::{
- report_breadcrumb, report_error_to_app, set_application_error_reporter,
- unset_application_error_reporter, ApplicationErrorReporter,
-};
-
-#[cfg(feature = "tracing-reporting")]
mod error_tracing;
-#[cfg(feature = "tracing-reporting")]
pub use error_tracing::{report_breadcrumb, report_error_to_app};
pub use error_support_macros::handle_error;
diff --git a/third_party/rust/error-support/src/reporting.rs b/third_party/rust/error-support/src/reporting.rs
deleted file mode 100644
index 2da314be41a8f..0000000000000
--- a/third_party/rust/error-support/src/reporting.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-/* 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/. */
-
-use parking_lot::RwLock;
-use std::sync::atomic::{AtomicU32, Ordering};
-
-/// Counter for breadcrumb messages
-///
-/// We are currently seeing breadcrumbs that may indicate that the reporting is unreliable. In
-/// some reports, the breadcrumbs seem like they may be duplicated and/or out of order. This
-/// counter is a temporary measure to check out that theory.
-static BREADCRUMB_COUNTER: AtomicU32 = AtomicU32::new(0);
-
-fn get_breadcrumb_counter_value() -> u32 {
- // Notes:
- // - fetch_add is specified to wrap around in case of overflow, which seems okay.
- // - By itself, this does not guarantee that breadcrumb logs will be ordered the same way as
- // the counter values. If two threads are running at the same time, it's very possible
- // that thread A gets the lower breadcrumb value, but thread B wins the race to report its
- // breadcrumb. However, if we expect operations to be synchronized, like with places DB,
- // then the breadcrumb counter values should always increase by 1.
- BREADCRUMB_COUNTER.fetch_add(1, Ordering::Relaxed)
-}
-
-/// Application error reporting trait
-///
-/// The application that's consuming application-services implements this via a UniFFI callback
-/// interface, then calls `set_application_error_reporter()` to setup a global
-/// ApplicationErrorReporter.
-#[uniffi::export(callback_interface)]
-pub trait ApplicationErrorReporter: Sync + Send {
- /// Send an error report to a Sentry-like error reporting system
- ///
- /// type_name should be used to group errors together
- fn report_error(&self, type_name: String, message: String);
- /// Send a breadcrumb to a Sentry-like error reporting system
- fn report_breadcrumb(&self, message: String, module: String, line: u32, column: u32);
-}
-
-// ApplicationErrorReporter to use if the app doesn't set one
-struct DefaultApplicationErrorReporter;
-impl ApplicationErrorReporter for DefaultApplicationErrorReporter {
- fn report_error(&self, _type_name: String, _message: String) {}
- fn report_breadcrumb(&self, _message: String, _module: String, _line: u32, _column: u32) {}
-}
-
-lazy_static::lazy_static! {
- // RwLock rather than a Mutex, since we only expect to set this once.
- pub(crate) static ref APPLICATION_ERROR_REPORTER: RwLock> = RwLock::new(Box::new(DefaultApplicationErrorReporter));
-}
-
-/// Set the global error reporter. This is typically done early in startup.
-#[uniffi::export]
-pub fn set_application_error_reporter(error_reporter: Box) {
- *APPLICATION_ERROR_REPORTER.write() = error_reporter;
-}
-
-/// Unset the global error reporter. This is typically done at shutdown for
-/// platforms that want to cleanup references like Desktop.
-#[uniffi::export]
-pub fn unset_application_error_reporter() {
- *APPLICATION_ERROR_REPORTER.write() = Box::new(DefaultApplicationErrorReporter)
-}
-
-pub fn report_error_to_app(type_name: String, message: String) {
- APPLICATION_ERROR_REPORTER
- .read()
- .report_error(type_name, message);
-}
-
-pub fn report_breadcrumb(message: String, module: String, line: u32, column: u32) {
- let message = format!("{} ({})", message, get_breadcrumb_counter_value());
- APPLICATION_ERROR_REPORTER
- .read()
- .report_breadcrumb(message, module, line, column);
-}
diff --git a/third_party/rust/error-support/uniffi.toml b/third_party/rust/error-support/uniffi.toml
index 517f14922b4f2..3229d5b941aea 100644
--- a/third_party/rust/error-support/uniffi.toml
+++ b/third_party/rust/error-support/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.errorsupport"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/init_rust_components/.cargo-checksum.json b/third_party/rust/init_rust_components/.cargo-checksum.json
index e5f89cf028286..0bc8b4ccf73be 100644
--- a/third_party/rust/init_rust_components/.cargo-checksum.json
+++ b/third_party/rust/init_rust_components/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"cf60bdba9b0536032be9ac69aa9fa5c68f6ad50c99e78d3e00c8d034156276a6","README.md":"4c4ba1a04d445dca0507eef3ff8cd79e0954474d2a75feadd98946187fd8374f","src/lib.rs":"421e93bbc63bdcc41131b2ef740be98d00c4151775ef164857671430666f7ebd","uniffi.toml":"2e98a909732dc83db0c0b41438b0935c1fd5f869cea327e3ef5501fae5fa5d01"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"cf60bdba9b0536032be9ac69aa9fa5c68f6ad50c99e78d3e00c8d034156276a6","README.md":"4c4ba1a04d445dca0507eef3ff8cd79e0954474d2a75feadd98946187fd8374f","src/lib.rs":"421e93bbc63bdcc41131b2ef740be98d00c4151775ef164857671430666f7ebd","uniffi.toml":"37bdd45b1aacec56179d643c339f7c8939c03b67caa11ac0d8b68dbde471b7cc"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/init_rust_components/uniffi.toml b/third_party/rust/init_rust_components/uniffi.toml
index 122cb430927b0..088bc5789822b 100644
--- a/third_party/rust/init_rust_components/uniffi.toml
+++ b/third_party/rust/init_rust_components/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.init_rust_components"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/logins/.cargo-checksum.json b/third_party/rust/logins/.cargo-checksum.json
index 2a72bbf044194..97b0bf340fae4 100644
--- a/third_party/rust/logins/.cargo-checksum.json
+++ b/third_party/rust/logins/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"b56d32e96d574fc21a484e35be91fd5bdf5ce15ab033d50f95216eebfd09fb36","README.md":"2c25808f8371b5d9ceb58e51f71304a270d5b33dad5a280a6e95e06e606f4492","build.rs":"63ff52215682b7d516679e7aaeaaaf5d3ac98ebdf0c08361193c34c99506bfdf","metrics.yaml":"a875db3d9d759935f43131bd5c830307f8fe5d42df0e47768deb91e4568d0f6b","src/db.rs":"3f023ab6dc16fa593af9cb6d973e8e8dd28918b65b9f4993f4db1edeca7e67e5","src/encryption.rs":"df48d5a5209743007817922eeef25278255ef6a2550d43782f7504ecff6a383f","src/error.rs":"ed3799d9ad49a7554613923313ff921b7fb71dbf7076b2b9198323d7eb2f4e68","src/lib.rs":"f9a56ad783794e23fa635ad38db62ccb8dfce5d9d44b0c58de3176c978291fe8","src/login.rs":"e427f19a5bb899a2f06eb613b3e249fd2febae84e46dba14bb1e59317ed420e6","src/logins.udl":"a6017f61fa3b28078b91bbef2fc71f28e33847468724cd4a4e59c67a2ef9e043","src/schema.rs":"384e10c123c6cc9fa5356a9718cc757eca157345047c5dc5c9df15dce8c19fe2","src/store.rs":"dcc09f3bdd162761b0c67b542ba448500be2b40a50f8ffb3dbbaa50c9b8795d5","src/sync/engine.rs":"ea96e598c3ab13b9c4d4373550580677713bc1066ab176e9e3f4666618929a1f","src/sync/merge.rs":"6fbfdd4239826098cf9c7cf6ee3d2d32251a03d92f013d7ba0be8cb459df1de4","src/sync/mod.rs":"00eb3bdd5fcac411fb314400a3d66aea874aa117db1003caf75ea43d385e372e","src/sync/payload.rs":"c116f627ffb5f9294e90988d879cc55e33a83984a090e36410e6cc40e25fbb95","src/sync/update_plan.rs":"2c0a3b926adee2fd68a6355332e7188277cd0f047342adbc83d6ce498cebe2b1","src/util.rs":"1ced78e185164640e9859b0316f4798ccacd30bad9d6c549f9f650350a5f97b6","uniffi.toml":"1b1ea1a488fc051b9fe5a289e474462f7c676bcd02ca176713d885d280414bf6"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"b56d32e96d574fc21a484e35be91fd5bdf5ce15ab033d50f95216eebfd09fb36","README.md":"2c25808f8371b5d9ceb58e51f71304a270d5b33dad5a280a6e95e06e606f4492","build.rs":"63ff52215682b7d516679e7aaeaaaf5d3ac98ebdf0c08361193c34c99506bfdf","metrics.yaml":"be52a2d042250c159890f6187f6623d6fc27fda7a5bdcdf3b80af2dab05371c4","src/db.rs":"3f023ab6dc16fa593af9cb6d973e8e8dd28918b65b9f4993f4db1edeca7e67e5","src/encryption.rs":"df48d5a5209743007817922eeef25278255ef6a2550d43782f7504ecff6a383f","src/error.rs":"ed3799d9ad49a7554613923313ff921b7fb71dbf7076b2b9198323d7eb2f4e68","src/lib.rs":"f9a56ad783794e23fa635ad38db62ccb8dfce5d9d44b0c58de3176c978291fe8","src/login.rs":"e427f19a5bb899a2f06eb613b3e249fd2febae84e46dba14bb1e59317ed420e6","src/logins.udl":"a6017f61fa3b28078b91bbef2fc71f28e33847468724cd4a4e59c67a2ef9e043","src/schema.rs":"384e10c123c6cc9fa5356a9718cc757eca157345047c5dc5c9df15dce8c19fe2","src/store.rs":"dcc09f3bdd162761b0c67b542ba448500be2b40a50f8ffb3dbbaa50c9b8795d5","src/sync/engine.rs":"ea96e598c3ab13b9c4d4373550580677713bc1066ab176e9e3f4666618929a1f","src/sync/merge.rs":"6fbfdd4239826098cf9c7cf6ee3d2d32251a03d92f013d7ba0be8cb459df1de4","src/sync/mod.rs":"00eb3bdd5fcac411fb314400a3d66aea874aa117db1003caf75ea43d385e372e","src/sync/payload.rs":"c116f627ffb5f9294e90988d879cc55e33a83984a090e36410e6cc40e25fbb95","src/sync/update_plan.rs":"2c0a3b926adee2fd68a6355332e7188277cd0f047342adbc83d6ce498cebe2b1","src/util.rs":"1ced78e185164640e9859b0316f4798ccacd30bad9d6c549f9f650350a5f97b6","uniffi.toml":"ce2300f501280648452d97e1938279e55a67ee0f599370125cd8fce6e81376f3"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/logins/metrics.yaml b/third_party/rust/logins/metrics.yaml
index 8a4299a02f5cd..088e88deb95a3 100644
--- a/third_party/rust/logins/metrics.yaml
+++ b/third_party/rust/logins/metrics.yaml
@@ -72,101 +72,6 @@ logins_store:
- bdk@mozilla.com
expires: never
- # These help us understand how much the logins store is being used, and
- # whether it's succeeding in the duties asked of it. We'll use them to
- # graph e.g. the error rate of applications trying to use the logins store,
- # and identify application or platform features that lead to unusually
- # high error rates.
- read_query_count:
- type: counter
- description: >
- The total number of read operations performed on the logins store.
- The count only includes operations triggered by the application, not
- e.g. incidental reads performed as part of a sync. It is intended to be
- used together with `read_query_error_count` to measure the overall error
- rate of read operations on the logins store.
- bugs:
- - https://github.com/mozilla/application-services/issues/2225
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1597895
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1649044
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1694316
- data_sensitivity:
- - interaction
- notification_emails:
- - mhammond@mozilla.com
- - synced-client-integrations@mozilla.com
- expires: "never"
-
- read_query_error_count:
- type: labeled_counter
- description: >
- The total number of errors encountered during read operations on the
- logins store, labeled by type.
- It is intended to be used together with `read_query_count` to measure
- the overall error rate of read operations on the logins store.
- labels:
- - interrupted
- - storage_error
- bugs:
- - https://github.com/mozilla/application-services/issues/2225
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1597895
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1649044
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1694316
- data_sensitivity:
- - interaction
- notification_emails:
- - mhammond@mozilla.com
- - synced-client-integrations@mozilla.com
- expires: "never"
-
- write_query_count:
- type: counter
- description: >
- The total number of write operations performed on the logins store.
- The count only includes operations triggered by the application, not
- e.g. incidental writes performed as part of a sync. It is intended to
- be used together with `write_query_error_count` to measure the overall
- error rate of write operations on the logins store.
- bugs:
- - https://github.com/mozilla/application-services/issues/2225
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1597895
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1649044
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1694316
- data_sensitivity:
- - interaction
- notification_emails:
- - mhammond@mozilla.com
- - synced-client-integrations@mozilla.com
- expires: "never"
-
- write_query_error_count:
- type: labeled_counter
- description: >
- The total number of errors encountered during write operations on the
- logins store, labeled by type.
- It is intended to be used together with `write_query_count` to measure
- the overall error rate of write operations on the logins store.
- labels:
- - no_such_record
- - interrupted
- - invalid_record
- - storage_error
- bugs:
- - https://github.com/mozilla/application-services/issues/2225
- data_reviews:
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1597895
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1649044
- - https://bugzilla.mozilla.org/show_bug.cgi?id=1694316
- data_sensitivity:
- - interaction
- notification_emails:
- - mhammond@mozilla.com
- - synced-client-integrations@mozilla.com
- expires: "never"
-
local_undecryptable_deleted:
type: counter
description: >
diff --git a/third_party/rust/logins/uniffi.toml b/third_party/rust/logins/uniffi.toml
index ff077a29cd2eb..12267874f3d99 100644
--- a/third_party/rust/logins/uniffi.toml
+++ b/third_party/rust/logins/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.logins"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/remote_settings/.cargo-checksum.json b/third_party/rust/remote_settings/.cargo-checksum.json
index ebd9adae0956b..411f3f76f19aa 100644
--- a/third_party/rust/remote_settings/.cargo-checksum.json
+++ b/third_party/rust/remote_settings/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"9fa186d81541e67cf3a622845782101de9e44de3a43fb27dfb905e183a182876","dumps/main/attachments/regions/world":"00b308033d44f61612b962f572765d14a3999586d92fc8b9fff2217a1ae070e8","dumps/main/attachments/regions/world-buffered":"1d3ed6954fac2a5b31302f5d3e8186c5fa08a20239afc0643ca5dfbb4d8a86fc","dumps/main/attachments/regions/world-buffered.meta.json":"914a71376a152036aceccb6877e079fbb9e3373c6219f24f00dd30e901a72cce","dumps/main/attachments/regions/world.meta.json":"2a47d77834997b98e563265d299723e7f7fd64c8c7a5731afc722862333d6fbd","dumps/main/attachments/search-config-icons/001500a9-1a6c-3f5a-ba15-a5f5a075d256":"fdadf15c6eae7933c3d254ae6311112e0bc8a422c38c758189dbe6a4d7f6b718","dumps/main/attachments/search-config-icons/001500a9-1a6c-3f5a-ba15-a5f5a075d256.meta.json":"6ed1e1c390a45360590e5a1e6d7823218e7b10860581646ddb5e368143aa72fc","dumps/main/attachments/search-config-icons/06cf7432-efd7-f244-927b-5e423005e1ea":"b75ef04a805325e303c4195833cdd077d3d406f360b25b72502fc55880b9150b","dumps/main/attachments/search-config-icons/06cf7432-efd7-f244-927b-5e423005e1ea.meta.json":"0d4cce0ed0dc6b2c46651bea32fc3cc2facfe8b341e1022b65f2cd2231f6b713","dumps/main/attachments/search-config-icons/0a57b0cf-34f0-4d09-96e4-dbd6e3355410":"a7493c6a9d70d60acccf73f62dcbc127a580469570aee60b7482cd42cdb59f69","dumps/main/attachments/search-config-icons/0a57b0cf-34f0-4d09-96e4-dbd6e3355410.meta.json":"d33a128c92b96af2e643158ed3b861d3726bd67a59907fed0795ab2210c82b96","dumps/main/attachments/search-config-icons/0d7668a8-c3f4-cfee-cbc8-536511528937":"7042293af6b04e421cb7b68dc599ac644b76939cdcf5970159e44f658dd6a0cc","dumps/main/attachments/search-config-icons/0d7668a8-c3f4-cfee-cbc8-536511528937.meta.json":"d6523508334a67b201326591606d7e225a04fc53fdce2c1b4d8afac1b41af6b0","dumps/main/attachments/search-config-icons/0eec5640-6fde-d6fe-322a-c72c6d5bd5a2":"64800e32b24b2c8c0582750e1657426d56abd74b65682e20e892f82710d120b6","dumps/main/attachments/search-config-icons/0eec5640-6fde-d6fe-322a-c72c6d5bd5a2.meta.json":"56fb61a078cc45abf7bc3b8fe89b60ef75f3b86ea61d63084749607c4662bbef","dumps/main/attachments/search-config-icons/101ce01d-2691-b729-7f16-9d389803384b":"62d2faa3a8322b1f643aab6e045837500ebe3049c5cb140cb44c4dfc7290337a","dumps/main/attachments/search-config-icons/101ce01d-2691-b729-7f16-9d389803384b.meta.json":"de134ed423a2bd92b4ad8cdf631aad6a83cc2c30f8df9ee251a435ee9f46f28f","dumps/main/attachments/search-config-icons/177aba42-9bed-4078-e36b-580e8794cd7f":"3b88f3ef3cbfaed127d679ec7e44a44fe8dcad688feb89a70a1a9447c1460d15","dumps/main/attachments/search-config-icons/177aba42-9bed-4078-e36b-580e8794cd7f.meta.json":"c35210da5afc11b3af156baf46c23fa523dafac7e8cb2738b4caef80ed48c72e","dumps/main/attachments/search-config-icons/25de0352-aabb-d31f-15f7-bf9299fb004c":"828c3ca82e9be483ae583e5a705dde57b24fd8431e192e3a2d0809871992afa5","dumps/main/attachments/search-config-icons/25de0352-aabb-d31f-15f7-bf9299fb004c.meta.json":"aa5483b5c65427c028a676b2fc13892f6fcaf602613183962744c43ca146d86a","dumps/main/attachments/search-config-icons/2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335":"723ac3228124926537d5a61284d60e198a52895195f9f69b967c578ef7a012ad","dumps/main/attachments/search-config-icons/2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335.meta.json":"d754b38d7e2f79e651d5fe110a7bb9855e0f7269e177be6d047453a36a52a4c5","dumps/main/attachments/search-config-icons/2e835b0e-9709-d1bb-9725-87f59f3445ca":"16ea89d4baa39529d7a84d5152867a4c6ed6867198c4dfa1648b1f43ce6a3f6f","dumps/main/attachments/search-config-icons/2e835b0e-9709-d1bb-9725-87f59f3445ca.meta.json":"7edb361a6610fdabb431a58bc170b7df3f179ca1fadebae6f2e98777b64b35c5","dumps/main/attachments/search-config-icons/2ecca3f8-c1ef-43cc-b053-886d1ae46c36":"774f0a7a613c6c5bea642e3628fa7436851de79e7da9713ad0c96d5db7f44300","dumps/main/attachments/search-config-icons/2ecca3f8-c1ef-43cc-b053-886d1ae46c36.meta.json":"194cb07dd29fd66121f05bbef38291e894291adcbc4c63c373b5f72f6f2e245e","dumps/main/attachments/search-config-icons/32d26d19-aeb0-5c01-32e8-f8970be9246f":"a64f553b79fbb8c45734310dac401ad253ccd05aeabfa58bb5541daa6d8caf70","dumps/main/attachments/search-config-icons/32d26d19-aeb0-5c01-32e8-f8970be9246f.meta.json":"6e56cf6a9470f575b283e40ed6a049e9dbffadcb59fa74a0f941b431c444d795","dumps/main/attachments/search-config-icons/39d0b17d-c020-4890-932f-83c0f6ed130b":"4f409c3ffc67cfa870b05e4089b6ffc3fc81448fa60afba447f0177cd1192b1e","dumps/main/attachments/search-config-icons/39d0b17d-c020-4890-932f-83c0f6ed130b.meta.json":"686c72d6cd220285d8b97af55474198027eab1b1af7ed89508c8935c9e00e7d4","dumps/main/attachments/search-config-icons/41135a88-093d-4077-873b-9de1ae133427":"c2718c5e416670426475dd8cc496f5464bf95224e8f8f0a72b695360ddc917c0","dumps/main/attachments/search-config-icons/41135a88-093d-4077-873b-9de1ae133427.meta.json":"a941fc27ca88b56eccbdec380ce2d3b911f4c62e4aba1f5399bc4498c6601d94","dumps/main/attachments/search-config-icons/41f0d805-3775-4988-8d8c-5ad8ccd86d1c":"755b8939c63b1fcc9acd05cd33ffed675397516d37b5bd8f3a03875e25d3fb43","dumps/main/attachments/search-config-icons/41f0d805-3775-4988-8d8c-5ad8ccd86d1c.meta.json":"25cafe7d629c6b006e15dee98987c26ef509f348ed0350a3ccf1f06838db86f3","dumps/main/attachments/search-config-icons/47da97b5-600f-c450-fd15-a52bb2169c11":"d7fdfd971d874f2ec6f209df6f6b8173d126cd3f7a25daacb94de4259efbcf16","dumps/main/attachments/search-config-icons/47da97b5-600f-c450-fd15-a52bb2169c11.meta.json":"e6b95ca29bf3e750819cf890ae8e879ac506d54b918c3c0ab065adc16a131188","dumps/main/attachments/search-config-icons/48c72361-cd67-412e-bd7f-f81a43c10791":"92da7ef030e1d3ed97235748156383e5d75fa6d2744bd124334ab47dc0b689a1","dumps/main/attachments/search-config-icons/48c72361-cd67-412e-bd7f-f81a43c10791.meta.json":"21d1320d60c981b9c9248b9b186fc49f9d95046a817d7a3145f48465236087e8","dumps/main/attachments/search-config-icons/4e271681-3e0f-91ac-9750-03f665efc171":"189ed3031a2cefd3150c9e5b37bee1ffbc1f7850f7ac0621e4b8d262f2c1048c","dumps/main/attachments/search-config-icons/4e271681-3e0f-91ac-9750-03f665efc171.meta.json":"0a401871b82f1c1cbca91232fd643be1cc1a76c07a48830eaa2a47cdfafb1f14","dumps/main/attachments/search-config-icons/50f6171f-8e7a-b41b-862e-f97397038fb2":"9140bd1b30953f41bc758d2c0ecc873f5163e4f51126c278991eccd38589c541","dumps/main/attachments/search-config-icons/50f6171f-8e7a-b41b-862e-f97397038fb2.meta.json":"805d8ce33bac4611ce12d1f84ef431313555d76bf996264736e401ceb6dabe98","dumps/main/attachments/search-config-icons/5203dd03-2c55-4b53-9c60-58258d587be1":"adb29f6fd95956401630d94967381ac473f57215d96a5bcf500a00e747731380","dumps/main/attachments/search-config-icons/5203dd03-2c55-4b53-9c60-58258d587be1.meta.json":"27a6f994c2c7653a33d380ac13c4cfbc00f543a92f229426aa71d85b1f968357","dumps/main/attachments/search-config-icons/5914932e-66ba-4126-8be5-d37beadd9532":"02f54211387baa59e4246356dc7344e48f39a412f2e5993d7f403aa538df7276","dumps/main/attachments/search-config-icons/5914932e-66ba-4126-8be5-d37beadd9532.meta.json":"2df77daeeef5892fdb7fdeecffb804638bd0d2f7da5b9e617f01303d786dcd09","dumps/main/attachments/search-config-icons/5ded611d-44b2-dc46-fd67-fb116888d75d":"877fb3aca13d2a7c656df1f94df3fa052afbb40b65c99ba5382392ff5499016e","dumps/main/attachments/search-config-icons/5ded611d-44b2-dc46-fd67-fb116888d75d.meta.json":"d1c75915fdc86461e755cd08e670b01da41cd3d76688afe692f841733a9b7ee0","dumps/main/attachments/search-config-icons/5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41":"9cd3da38e3938549434d1c3cba6fed249ffa7d91d9a6d7ffb5f4184f527cac76","dumps/main/attachments/search-config-icons/5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41.meta.json":"8506d9438825f2dc34875e04e0212e22a2652c5c43aa93e4d184d59ec765316f","dumps/main/attachments/search-config-icons/6644f26f-28ea-4222-929d-5d43a02dae05":"4f1bfbfec1441bd9a304ca7f3b8fd54130e94df185f7b28bb17c86ba517e13b7","dumps/main/attachments/search-config-icons/6644f26f-28ea-4222-929d-5d43a02dae05.meta.json":"f7d37f9c8b87480539cd981f463790c99ed15b2ffc5e3ff4786dd11c25228df4","dumps/main/attachments/search-config-icons/6d10d702-7bd6-1452-90a5-3df665a38f66":"f895a965b68d02e7391cc4504d9be75e1ba7f9b50a1dd59af77bb44a7769c08c","dumps/main/attachments/search-config-icons/6d10d702-7bd6-1452-90a5-3df665a38f66.meta.json":"d782d80f8187cf8051be89c6b8a1ef4700e0b84dcda006843d1fd2f266c4419b","dumps/main/attachments/search-config-icons/6e36a151-e4f4-4117-9067-1ca82c47d01a":"e9849089ffced59563896974afee0fceedac7fc8455bbeaa5bae230f54c933d9","dumps/main/attachments/search-config-icons/6e36a151-e4f4-4117-9067-1ca82c47d01a.meta.json":"0e5a64c06ea0ae875e8e0fdc850feddf5a8714b8290293dcd100d455de32ace0","dumps/main/attachments/search-config-icons/6f4da442-d31e-28f8-03af-797d16bbdd27":"dd5cab3711f778677859e86000a127ed07a6175e8e58aecb0fba71b825ce76d7","dumps/main/attachments/search-config-icons/6f4da442-d31e-28f8-03af-797d16bbdd27.meta.json":"0a947797180fdaf031a9c2d2f841b88243c90ff85f0af873f99ddd770fc59e8b","dumps/main/attachments/search-config-icons/7072564d-a573-4750-bf33-f0a07631c9eb":"0a653ea57472694ac05623d9b237e479232a0d65683d05f89661f996054e3276","dumps/main/attachments/search-config-icons/7072564d-a573-4750-bf33-f0a07631c9eb.meta.json":"bb8a256f72b37166fea2ecd4c3a59f49615854a7210e6963f2571f8dcb3d3a3f","dumps/main/attachments/search-config-icons/70fdd651-6c50-b7bb-09ec-7e85da259173":"31a793dad95b5ffd02d39ebf14fc40877596f418f5926247487265034181dc8f","dumps/main/attachments/search-config-icons/70fdd651-6c50-b7bb-09ec-7e85da259173.meta.json":"8c744e9d2f218256e63f1b1a2193f2fc4a7b980e72464e8d57fbe150446f2efc","dumps/main/attachments/search-config-icons/71f41a0c-5b70-4116-b30f-e62089083522":"5aad083bfcef256d433c1ffa571b814d16f61832bcd7565bf03909011f6a0bfc","dumps/main/attachments/search-config-icons/71f41a0c-5b70-4116-b30f-e62089083522.meta.json":"4a8f0c4e4ee643faa2f8d8cc4ddb8f87d14c740e8c9252223f826181c0117741","dumps/main/attachments/search-config-icons/74793ce1-a918-a5eb-d3c0-2aadaff3c88c":"ca8f102ac4f35189ebcb786d080843b603b234f89b8d8b1c0ef27a0ab7148182","dumps/main/attachments/search-config-icons/74793ce1-a918-a5eb-d3c0-2aadaff3c88c.meta.json":"60a7975cd79156623b0ca58be4110d152e50b6e9caaaa211dad1cd37eabb0345","dumps/main/attachments/search-config-icons/74f94dc2-caf6-4b90-b3d2-f3e2f7714d88":"3376e14529ed2e96c7dc491b3bf11914d7c8ff47a068311b2432c086c2ae0f28","dumps/main/attachments/search-config-icons/74f94dc2-caf6-4b90-b3d2-f3e2f7714d88.meta.json":"3a94a78a846f312c85c0609971b153aaba9819e2b657f25e3b0648a3956933d1","dumps/main/attachments/search-config-icons/764e3b14-fe16-4feb-8384-124c516a5afa":"71413ef23ac14ce2b7bb76f7f5d16b2df267239841a88ddab36b129481e00616","dumps/main/attachments/search-config-icons/764e3b14-fe16-4feb-8384-124c516a5afa.meta.json":"2f16dd51ade97a327d4e5f14d689f822a5c9061b9b27810bbccbf2f406a5e56f","dumps/main/attachments/search-config-icons/7bf4ca37-e2b8-4d31-a1c3-979bc0e85131":"912d20feefcba57d43bffff5e245b8c1e3865155ed686d8ad253bbab71116e83","dumps/main/attachments/search-config-icons/7bf4ca37-e2b8-4d31-a1c3-979bc0e85131.meta.json":"3ec071b0a2940cd8892a72bd28d44237aecfd20d88303661348257ddb98aee43","dumps/main/attachments/search-config-icons/7c81cf98-7c11-4afd-8279-db89118a6dfb":"e988445d87afe0d285bea251705fc23eb70ac42426ab0d7a69d9276585c5573c","dumps/main/attachments/search-config-icons/7c81cf98-7c11-4afd-8279-db89118a6dfb.meta.json":"273dd09cad2a62459cc062e3b39b835d55a06b10cb7d5149aad77dd55451821f","dumps/main/attachments/search-config-icons/7cb4d88a-d4df-45b2-87e4-f896eaf1bbdb":"8dc2e75e6792b8374b20621fa2151ac24b4626e5c1f6a1abec4f912746441859","dumps/main/attachments/search-config-icons/7cb4d88a-d4df-45b2-87e4-f896eaf1bbdb.meta.json":"21d2522ab4e47477e72da3a2d2223e50e55fef442e2ba736d56df1e09593d76f","dumps/main/attachments/search-config-icons/7edaf4fe-a8a0-432b-86d2-bf75ebe80851":"27541cb376bdda829a6cf9cefd13da112728881e3daa4ac3c1178d4ce15f1e8b","dumps/main/attachments/search-config-icons/7edaf4fe-a8a0-432b-86d2-bf75ebe80851.meta.json":"3aea5d0652172940ac33e32628dcc6a03b79fe686c6de752482c8e3ae5cd70eb","dumps/main/attachments/search-config-icons/7efbed51-813c-581d-d8d3-f8758434e451":"b0c6d1850265e3c946917232ca6c6ace3dad23347bfab4f81351eac569326d34","dumps/main/attachments/search-config-icons/7efbed51-813c-581d-d8d3-f8758434e451.meta.json":"5a723c8f04bffa33a69a7054d30a4816caf1a3924081c85e1d9770126f761a96","dumps/main/attachments/search-config-icons/84bb4962-e571-227a-9ef6-2ac5f2aac361":"a1fd5d127a5f2590ddcd439b7a2abb3456b48217ea11daf0345b26e108f520e6","dumps/main/attachments/search-config-icons/84bb4962-e571-227a-9ef6-2ac5f2aac361.meta.json":"5cb0ebdde367b9754ddf6cbd01150414d27977b2d4c5f65cd6bbb89989388f3b","dumps/main/attachments/search-config-icons/87ac4cde-f581-398b-1e32-eb4079183b36":"33ca72f1eac56793d1fd811189cedef98004a067c85b1143083b564814a4b0db","dumps/main/attachments/search-config-icons/87ac4cde-f581-398b-1e32-eb4079183b36.meta.json":"d97eca8063cc17b99c81c55d4ff121d765c793e006ed252fec5f9539bd0fc339","dumps/main/attachments/search-config-icons/8831ce10-b1e4-6eb4-4975-83c67457288e":"ca3cc8786977f6ffeb0546ff8f3bb2b7fd240d1956fbf86777dbf0e8bec9c03b","dumps/main/attachments/search-config-icons/8831ce10-b1e4-6eb4-4975-83c67457288e.meta.json":"45274d848d1f6d398563bb46a2bfa3eee40ff16a5dbf0bbc8904db442f80702c","dumps/main/attachments/search-config-icons/890de5c4-0941-a116-473a-5d240e79497a":"6ba1f0fd1d12014cab32f74daab24dfa16fb26613ace20a1e595267621038a07","dumps/main/attachments/search-config-icons/890de5c4-0941-a116-473a-5d240e79497a.meta.json":"107547059360c3658dcf187a546ffb52bb23fa385e1338151043bebe82bbf640","dumps/main/attachments/search-config-icons/8abb10a7-212f-46b5-a7b4-244f414e3810":"f8780adb4d7b28f2f881db4ca7b697d8fc916cd9fa834ccc445fe7d4b72a6cc7","dumps/main/attachments/search-config-icons/8abb10a7-212f-46b5-a7b4-244f414e3810.meta.json":"5401603c7abbc6ca5bdddb8f9cca7eee2e26e5721cc73f23d95f600d5421d431","dumps/main/attachments/search-config-icons/91a9672d-e945-8e1e-0996-aefdb0190716":"5d53ef1866a08cc29011f5f2a9ce99bbf37cf42e80de7f0e8cc30d13337e8187","dumps/main/attachments/search-config-icons/91a9672d-e945-8e1e-0996-aefdb0190716.meta.json":"4e8ea36e3d659eb22ac7c86498003ca8885bab9c40685bb8ca7796a8230201da","dumps/main/attachments/search-config-icons/94a84724-c30f-4767-ba42-01cc37fc31a4":"98dca7e24cad0a1be96ef2c323e9759beb63c72440756f887e2482d9ce8e8969","dumps/main/attachments/search-config-icons/94a84724-c30f-4767-ba42-01cc37fc31a4.meta.json":"1070966901fe9db82b71bfa74ddeedd4f23ab2ed1eddaf201634e06a604e6006","dumps/main/attachments/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b":"ca6e972004f62355c1ea97656bc2328e1643971bdecab9c6b563d45593b8122e","dumps/main/attachments/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b.meta.json":"c4e3c9e6426e6f35410c287eef95b3e2134e54409e462f02dd440876c06b1bb4","dumps/main/attachments/search-config-icons/9802e63d-05ec-48ba-93f9-746e0981ad98":"6b1b073183eb0012daea0dce351a94d395c8a0b531b610e56eac52b3d1d1da0e","dumps/main/attachments/search-config-icons/9802e63d-05ec-48ba-93f9-746e0981ad98.meta.json":"cceb55c68db8dddd23d064364a281e982069bfe2bb55eba7d282fffcec2aa89f","dumps/main/attachments/search-config-icons/9d96547d-7575-49ca-8908-1e046b8ea90e":"6a743574353de0ec7c85b49f46b2b554caa0fc1d064b90544c5dea0fea2b8901","dumps/main/attachments/search-config-icons/9d96547d-7575-49ca-8908-1e046b8ea90e.meta.json":"10897754c5aafe0b84413afee5e9948fd799262a26aa07bca85fe6fb369beac4","dumps/main/attachments/search-config-icons/a06db97d-1210-ea2e-5474-0e2f7d295bfd":"617dec5d635efb0a12d0de935c6999ef0249f4a63c62bdcb96551518bc3d1812","dumps/main/attachments/search-config-icons/a06db97d-1210-ea2e-5474-0e2f7d295bfd.meta.json":"06cf576ca882bd7c2d54c18329e21e5b1a9cb7732d4954b52cbfa52979a67765","dumps/main/attachments/search-config-icons/a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3":"d994f806b1e4225b50be5ab681b2cecf845cc216a19a432d878cea3cb815bafd","dumps/main/attachments/search-config-icons/a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3.meta.json":"67524e18799023a017c7d9db1b9ba5c9cc3090d20f8154449a8f44ba22719104","dumps/main/attachments/search-config-icons/a2c7d4e9-f770-51e1-0963-3c2c8401631d":"1bf68aca7bfc75ca8485c3dac9a1daa13c1a3eb480688c32262096af6076adfa","dumps/main/attachments/search-config-icons/a2c7d4e9-f770-51e1-0963-3c2c8401631d.meta.json":"4ab103bba0f8fde581c3950c6c08cfcf6786104d8cbcba240499308f26958d04","dumps/main/attachments/search-config-icons/a83f24e4-602c-47bd-930c-ad0947ee1adf":"66612f999921d892645c8a2b37aa5dad17b134e7fdaed375a683baec7fc10697","dumps/main/attachments/search-config-icons/a83f24e4-602c-47bd-930c-ad0947ee1adf.meta.json":"f985ffbde6cf6ef972b6798c9336922dcfa29ca9a31e3aa4fae1962e069bdb0c","dumps/main/attachments/search-config-icons/b64f09fd-52d1-c48e-af23-4ce918e7bf3b":"c3e8300801c5c585662f14fd8e819d635efd9830783dc3c631212927866e9898","dumps/main/attachments/search-config-icons/b64f09fd-52d1-c48e-af23-4ce918e7bf3b.meta.json":"f7fd846d6717131e75865f8f5ed562e88f40be3dcf1603f2660b425dedabc7d1","dumps/main/attachments/search-config-icons/b882b24d-1776-4ef9-9016-0bdbd935eda3":"076352591c7077af4af5771918f80b5da9c6bf479327cc68390abdb158f3ec03","dumps/main/attachments/search-config-icons/b882b24d-1776-4ef9-9016-0bdbd935eda3.meta.json":"229139bca53bc63bd59b8f261f36c11fbe76b2b45dfeac2580261cf290c41365","dumps/main/attachments/search-config-icons/b8ca5a94-8fff-27ad-6e00-96e244a32e21":"1474c93e49c209aca2a2df2acb61b64574805106bead6edebd67287de21920e0","dumps/main/attachments/search-config-icons/b8ca5a94-8fff-27ad-6e00-96e244a32e21.meta.json":"6fd72c11afb4249d0166d8d98c552ee02d73c0e07f0578bfd3d7e67d70bcaee3","dumps/main/attachments/search-config-icons/b9424309-f601-4a69-98ca-ca68e65633e6":"601d72e7abde5ec864b3d8ca0031896f769107670b84c66053062481a56d8665","dumps/main/attachments/search-config-icons/b9424309-f601-4a69-98ca-ca68e65633e6.meta.json":"f299a7d56c5552fc592c66073d3e1e1d16ce4a99e935dcc0cad7dafcad6b9e3b","dumps/main/attachments/search-config-icons/c411adc1-9661-4fb5-a4c1-8cfe74911943":"150765e8e9b985ba5b820ac9b8e7623023d5a0e24f94663d5e9203d8d7598059","dumps/main/attachments/search-config-icons/c411adc1-9661-4fb5-a4c1-8cfe74911943.meta.json":"c5cc89d5f24ef1fa40b1c47c2e97eaeb01439b8d3b186b9c2fe716c94ead30f2","dumps/main/attachments/search-config-icons/cbf9e891-d079-2b28-5617-283450d463dd":"5b2c34b3c4e8dd898b664dba6c3786e2ff9869eff55d673aa48361f11325ed07","dumps/main/attachments/search-config-icons/cbf9e891-d079-2b28-5617-283450d463dd.meta.json":"b757806fd1b922d81bbecab94c73d3db98cfc2aa2791a4d5137112f795a732ee","dumps/main/attachments/search-config-icons/d87f251c-3e12-a8bf-e2d0-afd43d36c5f9":"865d76c8175a8f11dedc93f0bc212242a97a8a76adac870e8249368cecc81402","dumps/main/attachments/search-config-icons/d87f251c-3e12-a8bf-e2d0-afd43d36c5f9.meta.json":"22594c8870cbabe5fc5d2637509235202502661b466e9e37c5878716f323a34f","dumps/main/attachments/search-config-icons/db0e1627-ae89-4c25-8944-a9481d8512d9":"97a68f0b948b68bbf389a9ef43e2fe6c31ff8dc7889c939fdfdea79378576c67","dumps/main/attachments/search-config-icons/db0e1627-ae89-4c25-8944-a9481d8512d9.meta.json":"1eccc999dcd377af84cf63ed60b7ef23d5d4b936a1e465d12349a5366b1b012d","dumps/main/attachments/search-config-icons/e02f23df-8d48-2b1b-3b5c-6dd27302c61c":"247aa26993083705ce99a8e5612cdf262aca98cde86ba19afc964329ba95986a","dumps/main/attachments/search-config-icons/e02f23df-8d48-2b1b-3b5c-6dd27302c61c.meta.json":"06fc893d29cf406519611da9d1993a13bd9134192940c12bf64536ea571db4f0","dumps/main/attachments/search-config-icons/e718e983-09aa-e8f6-b25f-cd4b395d4785":"809697f48848e7c3638d5f3e0b224ea60b3800504e7bd8417854d55989b85196","dumps/main/attachments/search-config-icons/e718e983-09aa-e8f6-b25f-cd4b395d4785.meta.json":"0d9baef39747776500e5b83e72cd9d901fc09ac08247368dd2117bb4ec011f54","dumps/main/attachments/search-config-icons/e7547f62-187b-b641-d462-e54a3f813d9a":"c971ee33b8c0a57349669d957bf73070b0632b128c94748e845b57d5e15221a4","dumps/main/attachments/search-config-icons/e7547f62-187b-b641-d462-e54a3f813d9a.meta.json":"8ec4f6a7826966f2ff82632ef527366db3452ebc40c90a557602111f0ea956c9","dumps/main/attachments/search-config-icons/eb62e768-151b-45d1-9fe5-9e1d2a5991c5":"aa46b3d1ed8557e5bc7e71988cc6c46b00363b890d2a781973f9dc9073f8dd31","dumps/main/attachments/search-config-icons/eb62e768-151b-45d1-9fe5-9e1d2a5991c5.meta.json":"a06682b589df8dd63b1f25c01630ead55a89217f72c0bc04b391829af3fef59f","dumps/main/attachments/search-config-icons/f312610a-ebfb-a106-ea92-fd643c5d3636":"91d17ba44192a6430ffdb447ff3a11533ef964628f67c13480cc9470212d3d65","dumps/main/attachments/search-config-icons/f312610a-ebfb-a106-ea92-fd643c5d3636.meta.json":"ce26ab2382a7a67a55688330dd74127b4a980610f4030bde8eaaa20b81306559","dumps/main/attachments/search-config-icons/f943d7bc-872e-4a81-810f-94d26465da69":"69e0131f3e85657f827eb4ad3f01c25cf17540fe2db15c7e756f4dcfc1853dd2","dumps/main/attachments/search-config-icons/f943d7bc-872e-4a81-810f-94d26465da69.meta.json":"2e29a77bc8758ac3674f3a94b42fb871171fc53ae45bcb1fec6527c787758a23","dumps/main/attachments/search-config-icons/fa0fc42c-d91d-fca7-34eb-806ff46062dc":"6da5620880159634213e197fafca1dde0272153be3e4590818533fab8d040770","dumps/main/attachments/search-config-icons/fa0fc42c-d91d-fca7-34eb-806ff46062dc.meta.json":"0c3c0eb832be884f25186395b8bf08cdbe0a6e2845b9e55e7cbea5d0f183ed7d","dumps/main/attachments/search-config-icons/fca3e3ee-56cd-f474-dc31-307fd24a891d":"c4d88cfa5262f6d2cf76b167281d25821c9e1770684b739ed6ad3cf7277a121b","dumps/main/attachments/search-config-icons/fca3e3ee-56cd-f474-dc31-307fd24a891d.meta.json":"a40c37a5150e3745849f67305fe2fe1e06ef1c3901f6dc604a8e3f6c94e7b624","dumps/main/attachments/search-config-icons/fe75ce3f-1545-400c-b28c-ad771054e69f":"3a9d06951c7c9d2c19cd00533a760b0f8755b1e2e718af81c710297d030fbe44","dumps/main/attachments/search-config-icons/fe75ce3f-1545-400c-b28c-ad771054e69f.meta.json":"3f353e083d7a885f6d59d36c9276a2d325686a533cf8c502cd41a10172e763ea","dumps/main/attachments/search-config-icons/fed4f021-ff3e-942a-010e-afa43fda2136":"d7fdfd971d874f2ec6f209df6f6b8173d126cd3f7a25daacb94de4259efbcf16","dumps/main/attachments/search-config-icons/fed4f021-ff3e-942a-010e-afa43fda2136.meta.json":"740f77dcc93ece89fd55557baf399a4464373c81154b0a9758b8622f6c458253","dumps/main/attachments/translations-wasm/4fd32605-9889-4dd9-9fc7-577ad1136746":"a3a89d9ad0a4ed8f27bf3e403701b23f5709816f6376438503f2fa5b0182c2dc","dumps/main/attachments/translations-wasm/4fd32605-9889-4dd9-9fc7-577ad1136746.meta.json":"443145b56a534c87db789181355536a2a60062b74ef6de9aa7bdb38bc33c328e","dumps/main/regions.json":"e8990158373f82d3f89fed5089cf29e4177cc85904479128728e05025e9a0c0c","dumps/main/regions.timestamp":"a0109417e42fae00eee42898223a4e4eeadc8d0567ad0aa5a379ced494aac5bf","dumps/main/search-config-icons.json":"9b83df4a7c80c7136917f4ae89624c7f5fc25bb710d77e487d53178c5543c9fb","dumps/main/search-config-icons.timestamp":"eb8bfe4d2b9fce7cde34b9f41f513664015a86ba35254a3524df327b8a29080b","dumps/main/search-config-v2.json":"4a409b410fbb5cf72443cae17c76181ef5852b75d219e90db2af8a7141aa9ed3","dumps/main/search-config-v2.timestamp":"912e3976dc7e6bfd5585322ae4375962ec220148815799fcc949982449872beb","dumps/main/search-telemetry-v2.json":"405b6a0e198f5cc2d2a4e484a5ccd2ca269bd5e08d03857cdc8e102c35f58708","dumps/main/search-telemetry-v2.timestamp":"95f877d4333a744273784096aa9a87f381c40f44bcbd539d17d3686874fae9f9","dumps/main/summarizer-models-config.json":"2785f498567aebcc2c3157b360a06970b8174815506a6e018c4cf9130d150002","dumps/main/summarizer-models-config.timestamp":"a1a521e82e18713743a7c6b7c437072a1b371019fd933ca8138938cd52f1d728","dumps/main/translations-models.json":"2445d5664eab4b3719094532cc36b1c22fa11a64a010d82158cbd9f477e16125","dumps/main/translations-models.timestamp":"d0b4624dcc17bd16572d34e64161ddcc5365d8ce8880f82e6e44e9063efba8dd","dumps/main/translations-wasm.json":"08f51644de84bb00ad059d131bef1621028ff802d5014a5c1c63c42c70e18fd3","dumps/main/translations-wasm.timestamp":"e2f810185686f4fd61c898403428ed17dafa3402272a2dcd585df732527eff89","src/cache.rs":"c6179802017b43885136e7d64004890cc13e8c2d4742e04073cf404b578f63db","src/client.rs":"259e79368110ce96a9e837f73b5f1eb294c4fd7735d837613a28d0a2e15c01dc","src/config.rs":"3e322bdab94855e17427187ffe92a59c1a64c9175d5e1f8605df2f6409a3c93f","src/context.rs":"43bab81026b6f1a2509d129e806fffd09ba80110d656798a02589985017a3c21","src/error.rs":"7f56c3ae167811c5a8413b73a7e6abcec2377c67cf48e7520929e15532678eef","src/jexl_filter.rs":"48a9d960e05dae444421f7c4ceeb45eab656f03f1e7071215c8e8d39aab56b54","src/lib.rs":"3e1cb064a01dd566b2359aaf25ede52251f033f05ea43afdd38fd62631b991c1","src/macros.rs":"a16189ed0c6de18d1c66ee9204edc56541d1b987513eb7491c2472a962263f51","src/schema.rs":"348e0d5ad1840aaae796b537d21381ef91bd75be262138bfec376d9f88d205b3","src/service.rs":"e917b3f0e9bfa53c7f8ca9c87fb8ca1d868800611955c216bf188d4cbf321bb4","src/signatures.rs":"5dc590aac827b03b144e0d98b8ed71998651aaf6bd09d29c9aef1d4c82cba23c","src/storage.rs":"c33dd92914770e96d3d44dbb9e95f512ce54261710a42c7cf4a896be348c529e","uniffi.toml":"bd7cc0e7c1981f53938f429c4f2541ac454ed4160a8a0b4670659e38acd23ee5"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"8e2dcabea3256909f5fc316efdd0da1a45c494e8288820215bb529f91923fa45","dumps/main/attachments/regions/world":"00b308033d44f61612b962f572765d14a3999586d92fc8b9fff2217a1ae070e8","dumps/main/attachments/regions/world-buffered":"1d3ed6954fac2a5b31302f5d3e8186c5fa08a20239afc0643ca5dfbb4d8a86fc","dumps/main/attachments/regions/world-buffered.meta.json":"914a71376a152036aceccb6877e079fbb9e3373c6219f24f00dd30e901a72cce","dumps/main/attachments/regions/world.meta.json":"2a47d77834997b98e563265d299723e7f7fd64c8c7a5731afc722862333d6fbd","dumps/main/attachments/search-config-icons/001500a9-1a6c-3f5a-ba15-a5f5a075d256":"fdadf15c6eae7933c3d254ae6311112e0bc8a422c38c758189dbe6a4d7f6b718","dumps/main/attachments/search-config-icons/001500a9-1a6c-3f5a-ba15-a5f5a075d256.meta.json":"6ed1e1c390a45360590e5a1e6d7823218e7b10860581646ddb5e368143aa72fc","dumps/main/attachments/search-config-icons/06cf7432-efd7-f244-927b-5e423005e1ea":"b75ef04a805325e303c4195833cdd077d3d406f360b25b72502fc55880b9150b","dumps/main/attachments/search-config-icons/06cf7432-efd7-f244-927b-5e423005e1ea.meta.json":"0d4cce0ed0dc6b2c46651bea32fc3cc2facfe8b341e1022b65f2cd2231f6b713","dumps/main/attachments/search-config-icons/0a57b0cf-34f0-4d09-96e4-dbd6e3355410":"a7493c6a9d70d60acccf73f62dcbc127a580469570aee60b7482cd42cdb59f69","dumps/main/attachments/search-config-icons/0a57b0cf-34f0-4d09-96e4-dbd6e3355410.meta.json":"d33a128c92b96af2e643158ed3b861d3726bd67a59907fed0795ab2210c82b96","dumps/main/attachments/search-config-icons/0d7668a8-c3f4-cfee-cbc8-536511528937":"7042293af6b04e421cb7b68dc599ac644b76939cdcf5970159e44f658dd6a0cc","dumps/main/attachments/search-config-icons/0d7668a8-c3f4-cfee-cbc8-536511528937.meta.json":"d6523508334a67b201326591606d7e225a04fc53fdce2c1b4d8afac1b41af6b0","dumps/main/attachments/search-config-icons/0eec5640-6fde-d6fe-322a-c72c6d5bd5a2":"64800e32b24b2c8c0582750e1657426d56abd74b65682e20e892f82710d120b6","dumps/main/attachments/search-config-icons/0eec5640-6fde-d6fe-322a-c72c6d5bd5a2.meta.json":"56fb61a078cc45abf7bc3b8fe89b60ef75f3b86ea61d63084749607c4662bbef","dumps/main/attachments/search-config-icons/101ce01d-2691-b729-7f16-9d389803384b":"62d2faa3a8322b1f643aab6e045837500ebe3049c5cb140cb44c4dfc7290337a","dumps/main/attachments/search-config-icons/101ce01d-2691-b729-7f16-9d389803384b.meta.json":"de134ed423a2bd92b4ad8cdf631aad6a83cc2c30f8df9ee251a435ee9f46f28f","dumps/main/attachments/search-config-icons/177aba42-9bed-4078-e36b-580e8794cd7f":"3b88f3ef3cbfaed127d679ec7e44a44fe8dcad688feb89a70a1a9447c1460d15","dumps/main/attachments/search-config-icons/177aba42-9bed-4078-e36b-580e8794cd7f.meta.json":"c35210da5afc11b3af156baf46c23fa523dafac7e8cb2738b4caef80ed48c72e","dumps/main/attachments/search-config-icons/25de0352-aabb-d31f-15f7-bf9299fb004c":"828c3ca82e9be483ae583e5a705dde57b24fd8431e192e3a2d0809871992afa5","dumps/main/attachments/search-config-icons/25de0352-aabb-d31f-15f7-bf9299fb004c.meta.json":"aa5483b5c65427c028a676b2fc13892f6fcaf602613183962744c43ca146d86a","dumps/main/attachments/search-config-icons/2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335":"723ac3228124926537d5a61284d60e198a52895195f9f69b967c578ef7a012ad","dumps/main/attachments/search-config-icons/2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335.meta.json":"d754b38d7e2f79e651d5fe110a7bb9855e0f7269e177be6d047453a36a52a4c5","dumps/main/attachments/search-config-icons/2e835b0e-9709-d1bb-9725-87f59f3445ca":"16ea89d4baa39529d7a84d5152867a4c6ed6867198c4dfa1648b1f43ce6a3f6f","dumps/main/attachments/search-config-icons/2e835b0e-9709-d1bb-9725-87f59f3445ca.meta.json":"7edb361a6610fdabb431a58bc170b7df3f179ca1fadebae6f2e98777b64b35c5","dumps/main/attachments/search-config-icons/2ecca3f8-c1ef-43cc-b053-886d1ae46c36":"774f0a7a613c6c5bea642e3628fa7436851de79e7da9713ad0c96d5db7f44300","dumps/main/attachments/search-config-icons/2ecca3f8-c1ef-43cc-b053-886d1ae46c36.meta.json":"194cb07dd29fd66121f05bbef38291e894291adcbc4c63c373b5f72f6f2e245e","dumps/main/attachments/search-config-icons/32d26d19-aeb0-5c01-32e8-f8970be9246f":"a64f553b79fbb8c45734310dac401ad253ccd05aeabfa58bb5541daa6d8caf70","dumps/main/attachments/search-config-icons/32d26d19-aeb0-5c01-32e8-f8970be9246f.meta.json":"6e56cf6a9470f575b283e40ed6a049e9dbffadcb59fa74a0f941b431c444d795","dumps/main/attachments/search-config-icons/39d0b17d-c020-4890-932f-83c0f6ed130b":"4f409c3ffc67cfa870b05e4089b6ffc3fc81448fa60afba447f0177cd1192b1e","dumps/main/attachments/search-config-icons/39d0b17d-c020-4890-932f-83c0f6ed130b.meta.json":"686c72d6cd220285d8b97af55474198027eab1b1af7ed89508c8935c9e00e7d4","dumps/main/attachments/search-config-icons/41135a88-093d-4077-873b-9de1ae133427":"c2718c5e416670426475dd8cc496f5464bf95224e8f8f0a72b695360ddc917c0","dumps/main/attachments/search-config-icons/41135a88-093d-4077-873b-9de1ae133427.meta.json":"a941fc27ca88b56eccbdec380ce2d3b911f4c62e4aba1f5399bc4498c6601d94","dumps/main/attachments/search-config-icons/41f0d805-3775-4988-8d8c-5ad8ccd86d1c":"755b8939c63b1fcc9acd05cd33ffed675397516d37b5bd8f3a03875e25d3fb43","dumps/main/attachments/search-config-icons/41f0d805-3775-4988-8d8c-5ad8ccd86d1c.meta.json":"25cafe7d629c6b006e15dee98987c26ef509f348ed0350a3ccf1f06838db86f3","dumps/main/attachments/search-config-icons/47da97b5-600f-c450-fd15-a52bb2169c11":"d7fdfd971d874f2ec6f209df6f6b8173d126cd3f7a25daacb94de4259efbcf16","dumps/main/attachments/search-config-icons/47da97b5-600f-c450-fd15-a52bb2169c11.meta.json":"e6b95ca29bf3e750819cf890ae8e879ac506d54b918c3c0ab065adc16a131188","dumps/main/attachments/search-config-icons/48c72361-cd67-412e-bd7f-f81a43c10791":"92da7ef030e1d3ed97235748156383e5d75fa6d2744bd124334ab47dc0b689a1","dumps/main/attachments/search-config-icons/48c72361-cd67-412e-bd7f-f81a43c10791.meta.json":"21d1320d60c981b9c9248b9b186fc49f9d95046a817d7a3145f48465236087e8","dumps/main/attachments/search-config-icons/4e271681-3e0f-91ac-9750-03f665efc171":"189ed3031a2cefd3150c9e5b37bee1ffbc1f7850f7ac0621e4b8d262f2c1048c","dumps/main/attachments/search-config-icons/4e271681-3e0f-91ac-9750-03f665efc171.meta.json":"0a401871b82f1c1cbca91232fd643be1cc1a76c07a48830eaa2a47cdfafb1f14","dumps/main/attachments/search-config-icons/50f6171f-8e7a-b41b-862e-f97397038fb2":"9140bd1b30953f41bc758d2c0ecc873f5163e4f51126c278991eccd38589c541","dumps/main/attachments/search-config-icons/50f6171f-8e7a-b41b-862e-f97397038fb2.meta.json":"805d8ce33bac4611ce12d1f84ef431313555d76bf996264736e401ceb6dabe98","dumps/main/attachments/search-config-icons/5203dd03-2c55-4b53-9c60-58258d587be1":"adb29f6fd95956401630d94967381ac473f57215d96a5bcf500a00e747731380","dumps/main/attachments/search-config-icons/5203dd03-2c55-4b53-9c60-58258d587be1.meta.json":"27a6f994c2c7653a33d380ac13c4cfbc00f543a92f229426aa71d85b1f968357","dumps/main/attachments/search-config-icons/5914932e-66ba-4126-8be5-d37beadd9532":"02f54211387baa59e4246356dc7344e48f39a412f2e5993d7f403aa538df7276","dumps/main/attachments/search-config-icons/5914932e-66ba-4126-8be5-d37beadd9532.meta.json":"2df77daeeef5892fdb7fdeecffb804638bd0d2f7da5b9e617f01303d786dcd09","dumps/main/attachments/search-config-icons/5ded611d-44b2-dc46-fd67-fb116888d75d":"877fb3aca13d2a7c656df1f94df3fa052afbb40b65c99ba5382392ff5499016e","dumps/main/attachments/search-config-icons/5ded611d-44b2-dc46-fd67-fb116888d75d.meta.json":"d1c75915fdc86461e755cd08e670b01da41cd3d76688afe692f841733a9b7ee0","dumps/main/attachments/search-config-icons/5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41":"9cd3da38e3938549434d1c3cba6fed249ffa7d91d9a6d7ffb5f4184f527cac76","dumps/main/attachments/search-config-icons/5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41.meta.json":"8506d9438825f2dc34875e04e0212e22a2652c5c43aa93e4d184d59ec765316f","dumps/main/attachments/search-config-icons/6644f26f-28ea-4222-929d-5d43a02dae05":"4f1bfbfec1441bd9a304ca7f3b8fd54130e94df185f7b28bb17c86ba517e13b7","dumps/main/attachments/search-config-icons/6644f26f-28ea-4222-929d-5d43a02dae05.meta.json":"f7d37f9c8b87480539cd981f463790c99ed15b2ffc5e3ff4786dd11c25228df4","dumps/main/attachments/search-config-icons/6d10d702-7bd6-1452-90a5-3df665a38f66":"f895a965b68d02e7391cc4504d9be75e1ba7f9b50a1dd59af77bb44a7769c08c","dumps/main/attachments/search-config-icons/6d10d702-7bd6-1452-90a5-3df665a38f66.meta.json":"d782d80f8187cf8051be89c6b8a1ef4700e0b84dcda006843d1fd2f266c4419b","dumps/main/attachments/search-config-icons/6e36a151-e4f4-4117-9067-1ca82c47d01a":"e9849089ffced59563896974afee0fceedac7fc8455bbeaa5bae230f54c933d9","dumps/main/attachments/search-config-icons/6e36a151-e4f4-4117-9067-1ca82c47d01a.meta.json":"0e5a64c06ea0ae875e8e0fdc850feddf5a8714b8290293dcd100d455de32ace0","dumps/main/attachments/search-config-icons/6f4da442-d31e-28f8-03af-797d16bbdd27":"dd5cab3711f778677859e86000a127ed07a6175e8e58aecb0fba71b825ce76d7","dumps/main/attachments/search-config-icons/6f4da442-d31e-28f8-03af-797d16bbdd27.meta.json":"0a947797180fdaf031a9c2d2f841b88243c90ff85f0af873f99ddd770fc59e8b","dumps/main/attachments/search-config-icons/7072564d-a573-4750-bf33-f0a07631c9eb":"0a653ea57472694ac05623d9b237e479232a0d65683d05f89661f996054e3276","dumps/main/attachments/search-config-icons/7072564d-a573-4750-bf33-f0a07631c9eb.meta.json":"bb8a256f72b37166fea2ecd4c3a59f49615854a7210e6963f2571f8dcb3d3a3f","dumps/main/attachments/search-config-icons/70fdd651-6c50-b7bb-09ec-7e85da259173":"31a793dad95b5ffd02d39ebf14fc40877596f418f5926247487265034181dc8f","dumps/main/attachments/search-config-icons/70fdd651-6c50-b7bb-09ec-7e85da259173.meta.json":"8c744e9d2f218256e63f1b1a2193f2fc4a7b980e72464e8d57fbe150446f2efc","dumps/main/attachments/search-config-icons/71f41a0c-5b70-4116-b30f-e62089083522":"5aad083bfcef256d433c1ffa571b814d16f61832bcd7565bf03909011f6a0bfc","dumps/main/attachments/search-config-icons/71f41a0c-5b70-4116-b30f-e62089083522.meta.json":"4a8f0c4e4ee643faa2f8d8cc4ddb8f87d14c740e8c9252223f826181c0117741","dumps/main/attachments/search-config-icons/74793ce1-a918-a5eb-d3c0-2aadaff3c88c":"ca8f102ac4f35189ebcb786d080843b603b234f89b8d8b1c0ef27a0ab7148182","dumps/main/attachments/search-config-icons/74793ce1-a918-a5eb-d3c0-2aadaff3c88c.meta.json":"60a7975cd79156623b0ca58be4110d152e50b6e9caaaa211dad1cd37eabb0345","dumps/main/attachments/search-config-icons/74f94dc2-caf6-4b90-b3d2-f3e2f7714d88":"3376e14529ed2e96c7dc491b3bf11914d7c8ff47a068311b2432c086c2ae0f28","dumps/main/attachments/search-config-icons/74f94dc2-caf6-4b90-b3d2-f3e2f7714d88.meta.json":"3a94a78a846f312c85c0609971b153aaba9819e2b657f25e3b0648a3956933d1","dumps/main/attachments/search-config-icons/764e3b14-fe16-4feb-8384-124c516a5afa":"71413ef23ac14ce2b7bb76f7f5d16b2df267239841a88ddab36b129481e00616","dumps/main/attachments/search-config-icons/764e3b14-fe16-4feb-8384-124c516a5afa.meta.json":"2f16dd51ade97a327d4e5f14d689f822a5c9061b9b27810bbccbf2f406a5e56f","dumps/main/attachments/search-config-icons/7bf4ca37-e2b8-4d31-a1c3-979bc0e85131":"912d20feefcba57d43bffff5e245b8c1e3865155ed686d8ad253bbab71116e83","dumps/main/attachments/search-config-icons/7bf4ca37-e2b8-4d31-a1c3-979bc0e85131.meta.json":"3ec071b0a2940cd8892a72bd28d44237aecfd20d88303661348257ddb98aee43","dumps/main/attachments/search-config-icons/7c81cf98-7c11-4afd-8279-db89118a6dfb":"e988445d87afe0d285bea251705fc23eb70ac42426ab0d7a69d9276585c5573c","dumps/main/attachments/search-config-icons/7c81cf98-7c11-4afd-8279-db89118a6dfb.meta.json":"273dd09cad2a62459cc062e3b39b835d55a06b10cb7d5149aad77dd55451821f","dumps/main/attachments/search-config-icons/7cb4d88a-d4df-45b2-87e4-f896eaf1bbdb":"8dc2e75e6792b8374b20621fa2151ac24b4626e5c1f6a1abec4f912746441859","dumps/main/attachments/search-config-icons/7cb4d88a-d4df-45b2-87e4-f896eaf1bbdb.meta.json":"21d2522ab4e47477e72da3a2d2223e50e55fef442e2ba736d56df1e09593d76f","dumps/main/attachments/search-config-icons/7edaf4fe-a8a0-432b-86d2-bf75ebe80851":"27541cb376bdda829a6cf9cefd13da112728881e3daa4ac3c1178d4ce15f1e8b","dumps/main/attachments/search-config-icons/7edaf4fe-a8a0-432b-86d2-bf75ebe80851.meta.json":"3aea5d0652172940ac33e32628dcc6a03b79fe686c6de752482c8e3ae5cd70eb","dumps/main/attachments/search-config-icons/7efbed51-813c-581d-d8d3-f8758434e451":"b0c6d1850265e3c946917232ca6c6ace3dad23347bfab4f81351eac569326d34","dumps/main/attachments/search-config-icons/7efbed51-813c-581d-d8d3-f8758434e451.meta.json":"5a723c8f04bffa33a69a7054d30a4816caf1a3924081c85e1d9770126f761a96","dumps/main/attachments/search-config-icons/84bb4962-e571-227a-9ef6-2ac5f2aac361":"a1fd5d127a5f2590ddcd439b7a2abb3456b48217ea11daf0345b26e108f520e6","dumps/main/attachments/search-config-icons/84bb4962-e571-227a-9ef6-2ac5f2aac361.meta.json":"5cb0ebdde367b9754ddf6cbd01150414d27977b2d4c5f65cd6bbb89989388f3b","dumps/main/attachments/search-config-icons/87ac4cde-f581-398b-1e32-eb4079183b36":"33ca72f1eac56793d1fd811189cedef98004a067c85b1143083b564814a4b0db","dumps/main/attachments/search-config-icons/87ac4cde-f581-398b-1e32-eb4079183b36.meta.json":"d97eca8063cc17b99c81c55d4ff121d765c793e006ed252fec5f9539bd0fc339","dumps/main/attachments/search-config-icons/8831ce10-b1e4-6eb4-4975-83c67457288e":"ca3cc8786977f6ffeb0546ff8f3bb2b7fd240d1956fbf86777dbf0e8bec9c03b","dumps/main/attachments/search-config-icons/8831ce10-b1e4-6eb4-4975-83c67457288e.meta.json":"45274d848d1f6d398563bb46a2bfa3eee40ff16a5dbf0bbc8904db442f80702c","dumps/main/attachments/search-config-icons/890de5c4-0941-a116-473a-5d240e79497a":"6ba1f0fd1d12014cab32f74daab24dfa16fb26613ace20a1e595267621038a07","dumps/main/attachments/search-config-icons/890de5c4-0941-a116-473a-5d240e79497a.meta.json":"107547059360c3658dcf187a546ffb52bb23fa385e1338151043bebe82bbf640","dumps/main/attachments/search-config-icons/8abb10a7-212f-46b5-a7b4-244f414e3810":"f8780adb4d7b28f2f881db4ca7b697d8fc916cd9fa834ccc445fe7d4b72a6cc7","dumps/main/attachments/search-config-icons/8abb10a7-212f-46b5-a7b4-244f414e3810.meta.json":"5401603c7abbc6ca5bdddb8f9cca7eee2e26e5721cc73f23d95f600d5421d431","dumps/main/attachments/search-config-icons/91a9672d-e945-8e1e-0996-aefdb0190716":"5d53ef1866a08cc29011f5f2a9ce99bbf37cf42e80de7f0e8cc30d13337e8187","dumps/main/attachments/search-config-icons/91a9672d-e945-8e1e-0996-aefdb0190716.meta.json":"4e8ea36e3d659eb22ac7c86498003ca8885bab9c40685bb8ca7796a8230201da","dumps/main/attachments/search-config-icons/94a84724-c30f-4767-ba42-01cc37fc31a4":"98dca7e24cad0a1be96ef2c323e9759beb63c72440756f887e2482d9ce8e8969","dumps/main/attachments/search-config-icons/94a84724-c30f-4767-ba42-01cc37fc31a4.meta.json":"1070966901fe9db82b71bfa74ddeedd4f23ab2ed1eddaf201634e06a604e6006","dumps/main/attachments/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b":"ca6e972004f62355c1ea97656bc2328e1643971bdecab9c6b563d45593b8122e","dumps/main/attachments/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b.meta.json":"c4e3c9e6426e6f35410c287eef95b3e2134e54409e462f02dd440876c06b1bb4","dumps/main/attachments/search-config-icons/9802e63d-05ec-48ba-93f9-746e0981ad98":"6b1b073183eb0012daea0dce351a94d395c8a0b531b610e56eac52b3d1d1da0e","dumps/main/attachments/search-config-icons/9802e63d-05ec-48ba-93f9-746e0981ad98.meta.json":"cceb55c68db8dddd23d064364a281e982069bfe2bb55eba7d282fffcec2aa89f","dumps/main/attachments/search-config-icons/9d96547d-7575-49ca-8908-1e046b8ea90e":"6a743574353de0ec7c85b49f46b2b554caa0fc1d064b90544c5dea0fea2b8901","dumps/main/attachments/search-config-icons/9d96547d-7575-49ca-8908-1e046b8ea90e.meta.json":"10897754c5aafe0b84413afee5e9948fd799262a26aa07bca85fe6fb369beac4","dumps/main/attachments/search-config-icons/a06db97d-1210-ea2e-5474-0e2f7d295bfd":"617dec5d635efb0a12d0de935c6999ef0249f4a63c62bdcb96551518bc3d1812","dumps/main/attachments/search-config-icons/a06db97d-1210-ea2e-5474-0e2f7d295bfd.meta.json":"06cf576ca882bd7c2d54c18329e21e5b1a9cb7732d4954b52cbfa52979a67765","dumps/main/attachments/search-config-icons/a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3":"d994f806b1e4225b50be5ab681b2cecf845cc216a19a432d878cea3cb815bafd","dumps/main/attachments/search-config-icons/a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3.meta.json":"67524e18799023a017c7d9db1b9ba5c9cc3090d20f8154449a8f44ba22719104","dumps/main/attachments/search-config-icons/a2c7d4e9-f770-51e1-0963-3c2c8401631d":"1bf68aca7bfc75ca8485c3dac9a1daa13c1a3eb480688c32262096af6076adfa","dumps/main/attachments/search-config-icons/a2c7d4e9-f770-51e1-0963-3c2c8401631d.meta.json":"4ab103bba0f8fde581c3950c6c08cfcf6786104d8cbcba240499308f26958d04","dumps/main/attachments/search-config-icons/a83f24e4-602c-47bd-930c-ad0947ee1adf":"66612f999921d892645c8a2b37aa5dad17b134e7fdaed375a683baec7fc10697","dumps/main/attachments/search-config-icons/a83f24e4-602c-47bd-930c-ad0947ee1adf.meta.json":"f985ffbde6cf6ef972b6798c9336922dcfa29ca9a31e3aa4fae1962e069bdb0c","dumps/main/attachments/search-config-icons/b64f09fd-52d1-c48e-af23-4ce918e7bf3b":"c3e8300801c5c585662f14fd8e819d635efd9830783dc3c631212927866e9898","dumps/main/attachments/search-config-icons/b64f09fd-52d1-c48e-af23-4ce918e7bf3b.meta.json":"f7fd846d6717131e75865f8f5ed562e88f40be3dcf1603f2660b425dedabc7d1","dumps/main/attachments/search-config-icons/b882b24d-1776-4ef9-9016-0bdbd935eda3":"076352591c7077af4af5771918f80b5da9c6bf479327cc68390abdb158f3ec03","dumps/main/attachments/search-config-icons/b882b24d-1776-4ef9-9016-0bdbd935eda3.meta.json":"229139bca53bc63bd59b8f261f36c11fbe76b2b45dfeac2580261cf290c41365","dumps/main/attachments/search-config-icons/b8ca5a94-8fff-27ad-6e00-96e244a32e21":"1474c93e49c209aca2a2df2acb61b64574805106bead6edebd67287de21920e0","dumps/main/attachments/search-config-icons/b8ca5a94-8fff-27ad-6e00-96e244a32e21.meta.json":"6fd72c11afb4249d0166d8d98c552ee02d73c0e07f0578bfd3d7e67d70bcaee3","dumps/main/attachments/search-config-icons/b9424309-f601-4a69-98ca-ca68e65633e6":"601d72e7abde5ec864b3d8ca0031896f769107670b84c66053062481a56d8665","dumps/main/attachments/search-config-icons/b9424309-f601-4a69-98ca-ca68e65633e6.meta.json":"f299a7d56c5552fc592c66073d3e1e1d16ce4a99e935dcc0cad7dafcad6b9e3b","dumps/main/attachments/search-config-icons/c411adc1-9661-4fb5-a4c1-8cfe74911943":"150765e8e9b985ba5b820ac9b8e7623023d5a0e24f94663d5e9203d8d7598059","dumps/main/attachments/search-config-icons/c411adc1-9661-4fb5-a4c1-8cfe74911943.meta.json":"c5cc89d5f24ef1fa40b1c47c2e97eaeb01439b8d3b186b9c2fe716c94ead30f2","dumps/main/attachments/search-config-icons/cbf9e891-d079-2b28-5617-283450d463dd":"5b2c34b3c4e8dd898b664dba6c3786e2ff9869eff55d673aa48361f11325ed07","dumps/main/attachments/search-config-icons/cbf9e891-d079-2b28-5617-283450d463dd.meta.json":"b757806fd1b922d81bbecab94c73d3db98cfc2aa2791a4d5137112f795a732ee","dumps/main/attachments/search-config-icons/d87f251c-3e12-a8bf-e2d0-afd43d36c5f9":"865d76c8175a8f11dedc93f0bc212242a97a8a76adac870e8249368cecc81402","dumps/main/attachments/search-config-icons/d87f251c-3e12-a8bf-e2d0-afd43d36c5f9.meta.json":"22594c8870cbabe5fc5d2637509235202502661b466e9e37c5878716f323a34f","dumps/main/attachments/search-config-icons/db0e1627-ae89-4c25-8944-a9481d8512d9":"97a68f0b948b68bbf389a9ef43e2fe6c31ff8dc7889c939fdfdea79378576c67","dumps/main/attachments/search-config-icons/db0e1627-ae89-4c25-8944-a9481d8512d9.meta.json":"1eccc999dcd377af84cf63ed60b7ef23d5d4b936a1e465d12349a5366b1b012d","dumps/main/attachments/search-config-icons/e02f23df-8d48-2b1b-3b5c-6dd27302c61c":"247aa26993083705ce99a8e5612cdf262aca98cde86ba19afc964329ba95986a","dumps/main/attachments/search-config-icons/e02f23df-8d48-2b1b-3b5c-6dd27302c61c.meta.json":"06fc893d29cf406519611da9d1993a13bd9134192940c12bf64536ea571db4f0","dumps/main/attachments/search-config-icons/e718e983-09aa-e8f6-b25f-cd4b395d4785":"809697f48848e7c3638d5f3e0b224ea60b3800504e7bd8417854d55989b85196","dumps/main/attachments/search-config-icons/e718e983-09aa-e8f6-b25f-cd4b395d4785.meta.json":"0d9baef39747776500e5b83e72cd9d901fc09ac08247368dd2117bb4ec011f54","dumps/main/attachments/search-config-icons/e7547f62-187b-b641-d462-e54a3f813d9a":"c971ee33b8c0a57349669d957bf73070b0632b128c94748e845b57d5e15221a4","dumps/main/attachments/search-config-icons/e7547f62-187b-b641-d462-e54a3f813d9a.meta.json":"8ec4f6a7826966f2ff82632ef527366db3452ebc40c90a557602111f0ea956c9","dumps/main/attachments/search-config-icons/eb62e768-151b-45d1-9fe5-9e1d2a5991c5":"aa46b3d1ed8557e5bc7e71988cc6c46b00363b890d2a781973f9dc9073f8dd31","dumps/main/attachments/search-config-icons/eb62e768-151b-45d1-9fe5-9e1d2a5991c5.meta.json":"a06682b589df8dd63b1f25c01630ead55a89217f72c0bc04b391829af3fef59f","dumps/main/attachments/search-config-icons/f312610a-ebfb-a106-ea92-fd643c5d3636":"91d17ba44192a6430ffdb447ff3a11533ef964628f67c13480cc9470212d3d65","dumps/main/attachments/search-config-icons/f312610a-ebfb-a106-ea92-fd643c5d3636.meta.json":"ce26ab2382a7a67a55688330dd74127b4a980610f4030bde8eaaa20b81306559","dumps/main/attachments/search-config-icons/f943d7bc-872e-4a81-810f-94d26465da69":"69e0131f3e85657f827eb4ad3f01c25cf17540fe2db15c7e756f4dcfc1853dd2","dumps/main/attachments/search-config-icons/f943d7bc-872e-4a81-810f-94d26465da69.meta.json":"2e29a77bc8758ac3674f3a94b42fb871171fc53ae45bcb1fec6527c787758a23","dumps/main/attachments/search-config-icons/fa0fc42c-d91d-fca7-34eb-806ff46062dc":"6da5620880159634213e197fafca1dde0272153be3e4590818533fab8d040770","dumps/main/attachments/search-config-icons/fa0fc42c-d91d-fca7-34eb-806ff46062dc.meta.json":"0c3c0eb832be884f25186395b8bf08cdbe0a6e2845b9e55e7cbea5d0f183ed7d","dumps/main/attachments/search-config-icons/fca3e3ee-56cd-f474-dc31-307fd24a891d":"c4d88cfa5262f6d2cf76b167281d25821c9e1770684b739ed6ad3cf7277a121b","dumps/main/attachments/search-config-icons/fca3e3ee-56cd-f474-dc31-307fd24a891d.meta.json":"a40c37a5150e3745849f67305fe2fe1e06ef1c3901f6dc604a8e3f6c94e7b624","dumps/main/attachments/search-config-icons/fe75ce3f-1545-400c-b28c-ad771054e69f":"3a9d06951c7c9d2c19cd00533a760b0f8755b1e2e718af81c710297d030fbe44","dumps/main/attachments/search-config-icons/fe75ce3f-1545-400c-b28c-ad771054e69f.meta.json":"3f353e083d7a885f6d59d36c9276a2d325686a533cf8c502cd41a10172e763ea","dumps/main/attachments/search-config-icons/fed4f021-ff3e-942a-010e-afa43fda2136":"d7fdfd971d874f2ec6f209df6f6b8173d126cd3f7a25daacb94de4259efbcf16","dumps/main/attachments/search-config-icons/fed4f021-ff3e-942a-010e-afa43fda2136.meta.json":"740f77dcc93ece89fd55557baf399a4464373c81154b0a9758b8622f6c458253","dumps/main/attachments/translations-wasm/4fd32605-9889-4dd9-9fc7-577ad1136746":"a3a89d9ad0a4ed8f27bf3e403701b23f5709816f6376438503f2fa5b0182c2dc","dumps/main/attachments/translations-wasm/4fd32605-9889-4dd9-9fc7-577ad1136746.meta.json":"443145b56a534c87db789181355536a2a60062b74ef6de9aa7bdb38bc33c328e","dumps/main/regions.json":"e8990158373f82d3f89fed5089cf29e4177cc85904479128728e05025e9a0c0c","dumps/main/regions.timestamp":"a0109417e42fae00eee42898223a4e4eeadc8d0567ad0aa5a379ced494aac5bf","dumps/main/search-config-icons.json":"9b83df4a7c80c7136917f4ae89624c7f5fc25bb710d77e487d53178c5543c9fb","dumps/main/search-config-icons.timestamp":"eb8bfe4d2b9fce7cde34b9f41f513664015a86ba35254a3524df327b8a29080b","dumps/main/search-config-v2.json":"4a409b410fbb5cf72443cae17c76181ef5852b75d219e90db2af8a7141aa9ed3","dumps/main/search-config-v2.timestamp":"912e3976dc7e6bfd5585322ae4375962ec220148815799fcc949982449872beb","dumps/main/search-telemetry-v2.json":"405b6a0e198f5cc2d2a4e484a5ccd2ca269bd5e08d03857cdc8e102c35f58708","dumps/main/search-telemetry-v2.timestamp":"95f877d4333a744273784096aa9a87f381c40f44bcbd539d17d3686874fae9f9","dumps/main/summarizer-models-config.json":"2785f498567aebcc2c3157b360a06970b8174815506a6e018c4cf9130d150002","dumps/main/summarizer-models-config.timestamp":"a1a521e82e18713743a7c6b7c437072a1b371019fd933ca8138938cd52f1d728","dumps/main/translations-models.json":"2445d5664eab4b3719094532cc36b1c22fa11a64a010d82158cbd9f477e16125","dumps/main/translations-models.timestamp":"d0b4624dcc17bd16572d34e64161ddcc5365d8ce8880f82e6e44e9063efba8dd","dumps/main/translations-wasm.json":"08f51644de84bb00ad059d131bef1621028ff802d5014a5c1c63c42c70e18fd3","dumps/main/translations-wasm.timestamp":"e2f810185686f4fd61c898403428ed17dafa3402272a2dcd585df732527eff89","src/cache.rs":"c6179802017b43885136e7d64004890cc13e8c2d4742e04073cf404b578f63db","src/client.rs":"259e79368110ce96a9e837f73b5f1eb294c4fd7735d837613a28d0a2e15c01dc","src/config.rs":"3e322bdab94855e17427187ffe92a59c1a64c9175d5e1f8605df2f6409a3c93f","src/context.rs":"43bab81026b6f1a2509d129e806fffd09ba80110d656798a02589985017a3c21","src/error.rs":"7f56c3ae167811c5a8413b73a7e6abcec2377c67cf48e7520929e15532678eef","src/jexl_filter.rs":"48a9d960e05dae444421f7c4ceeb45eab656f03f1e7071215c8e8d39aab56b54","src/lib.rs":"3e1cb064a01dd566b2359aaf25ede52251f033f05ea43afdd38fd62631b991c1","src/macros.rs":"a16189ed0c6de18d1c66ee9204edc56541d1b987513eb7491c2472a962263f51","src/schema.rs":"348e0d5ad1840aaae796b537d21381ef91bd75be262138bfec376d9f88d205b3","src/service.rs":"e917b3f0e9bfa53c7f8ca9c87fb8ca1d868800611955c216bf188d4cbf321bb4","src/signatures.rs":"5dc590aac827b03b144e0d98b8ed71998651aaf6bd09d29c9aef1d4c82cba23c","src/storage.rs":"c33dd92914770e96d3d44dbb9e95f512ce54261710a42c7cf4a896be348c529e","uniffi.toml":"82e9235a10ce83e77d1690c792aa6d68a02a6107a1ca3f172c110e097da24da9"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/remote_settings/Cargo.toml b/third_party/rust/remote_settings/Cargo.toml
index 590c12dc3a854..663332f7f03fa 100644
--- a/third_party/rust/remote_settings/Cargo.toml
+++ b/third_party/rust/remote_settings/Cargo.toml
@@ -58,7 +58,6 @@ optional = true
[dependencies.error-support]
path = "../support/error"
-features = ["tracing-logging"]
[dependencies.firefox-versioning]
path = "../support/firefox-versioning"
diff --git a/third_party/rust/remote_settings/uniffi.toml b/third_party/rust/remote_settings/uniffi.toml
index b322c379ac16d..2979bcb94424f 100644
--- a/third_party/rust/remote_settings/uniffi.toml
+++ b/third_party/rust/remote_settings/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.remotesettings"
+omit_checksums = true
[bindings.kotlin.custom_types.RsJsonObject]
# Name of the type in the Kotlin code
diff --git a/third_party/rust/search/.cargo-checksum.json b/third_party/rust/search/.cargo-checksum.json
index 948477f5ecc2a..e5bc03d3dab46 100644
--- a/third_party/rust/search/.cargo-checksum.json
+++ b/third_party/rust/search/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"6c6c4350846640098a3dc5d834a9bb98fd219a99ad465986fa4e8c7ed40f9f75","README.md":"d59a6ad6232a86a7bd3632ca62c44ba8bd466615c5d47ce0d836b270bac5562c","android/build.gradle":"e3b617d653aa0221f2229bb16c2fd635003fe82d0274c4b9a6f2d8154851985a","android/proguard-rules.pro":"1cf8c57e8f79c250b0af9c1a5a4edad71a5c348a79ab70243b6bae086c150ad2","android/src/main/AndroidManifest.xml":"0a05039a6124be0296764c2b0f41e863b5538d75e6164dd9ae945b59d983c318","src/configuration_overrides_types.rs":"220a5e12ee3deb309a1571c5820ec5132c959f56667c4c48f997bbe2be0c7eeb","src/configuration_types.rs":"f5906e5b0002365a84b0d3e4d36428f69d83ae2fe44eb5c6b66d33f058c69527","src/environment_matching.rs":"5a1ade9a900942c62e8740597528a34df6fb3fdb72c801a647a3386acd42fcc8","src/error.rs":"d3c1eda7a8da15446a321139d4d29dd9ceee99e916519690d5eb2d45ed628598","src/filter.rs":"908cc2c088cd3f7fcfc148d5d4d30f6d1355c9ee0b2980382e6abd09bb977c58","src/lib.rs":"bd2a45a5b4f247aee0c483412cd2d192914e00de96c7c30a99185b8fc73e006c","src/selector.rs":"6e48c68f3ddc9baf7352cfafc13e6a705fa6c505d7c06ce6294e831bee97c7b1","src/sort_helpers.rs":"1abd6b7affb392d5e65b2906e5924ef6a54baec5e8cc69a41e1edea862c0e87c","src/test_helpers.rs":"25ca3f3bb3625a04e395bf8ed7363b04f1b09592978dfd76cca8af770507850a","src/types.rs":"b25de9810c3023c986632b8f3d4ecd41b6702bec4c03e08d95250e27bb202455","uniffi.toml":"96f1cd569483ff59e3c73852f085a03889fa24a2ce20ff7a3003799a9f48a51e"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"6c6c4350846640098a3dc5d834a9bb98fd219a99ad465986fa4e8c7ed40f9f75","README.md":"d59a6ad6232a86a7bd3632ca62c44ba8bd466615c5d47ce0d836b270bac5562c","android/build.gradle":"e3b617d653aa0221f2229bb16c2fd635003fe82d0274c4b9a6f2d8154851985a","android/proguard-rules.pro":"1cf8c57e8f79c250b0af9c1a5a4edad71a5c348a79ab70243b6bae086c150ad2","android/src/main/AndroidManifest.xml":"0a05039a6124be0296764c2b0f41e863b5538d75e6164dd9ae945b59d983c318","src/configuration_overrides_types.rs":"220a5e12ee3deb309a1571c5820ec5132c959f56667c4c48f997bbe2be0c7eeb","src/configuration_types.rs":"f5906e5b0002365a84b0d3e4d36428f69d83ae2fe44eb5c6b66d33f058c69527","src/environment_matching.rs":"5a1ade9a900942c62e8740597528a34df6fb3fdb72c801a647a3386acd42fcc8","src/error.rs":"d3c1eda7a8da15446a321139d4d29dd9ceee99e916519690d5eb2d45ed628598","src/filter.rs":"106dcc6aaf7866df6c35500f6556f25a697a5f250493f4067188b87097178c36","src/lib.rs":"bd2a45a5b4f247aee0c483412cd2d192914e00de96c7c30a99185b8fc73e006c","src/selector.rs":"6e48c68f3ddc9baf7352cfafc13e6a705fa6c505d7c06ce6294e831bee97c7b1","src/sort_helpers.rs":"1abd6b7affb392d5e65b2906e5924ef6a54baec5e8cc69a41e1edea862c0e87c","src/test_helpers.rs":"421ed2ffe500ececdf86fc5b5c34213fbac936ae9e45cc38c005ded07427eaae","src/types.rs":"b25de9810c3023c986632b8f3d4ecd41b6702bec4c03e08d95250e27bb202455","uniffi.toml":"7d7e68d7e065d68111197c0bfd2a2eae6130ea8a43fc4f118ac4a862b471cc0a"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/search/src/filter.rs b/third_party/rust/search/src/filter.rs
index d9583c580aa4e..aa55ce4c84696 100644
--- a/third_party/rust/search/src/filter.rs
+++ b/third_party/rust/search/src/filter.rs
@@ -487,11 +487,10 @@ fn find_engine_id_with_match(
#[cfg(test)]
mod tests {
- use std::{collections::HashMap, vec};
-
use super::*;
use crate::*;
use once_cell::sync::Lazy;
+ use std::{collections::HashMap, vec};
#[test]
fn test_default_search_engine_url() {
@@ -558,1041 +557,65 @@ mod tests {
assert_eq!(
test_engine.partner_code, "override-partner-code",
"Should override the partner code"
- );
- assert_eq!(
- test_engine.click_url,
- Some("https://example.com/click-url".to_string()),
- "Should override the click url"
- );
- assert_eq!(
- test_engine.urls.search.base, "https://example.com/override-search",
- "Should override search url"
- );
- assert_eq!(
- test_engine.telemetry_suffix, "original-telemetry-suffix",
- "Should not override telemetry suffix when telemetry suffix is supplied as None"
- );
- }
-
- #[test]
- fn test_merge_override_locale_match() {
- let mut test_engine = SearchEngineDefinition {
- identifier: "test".to_string(),
- partner_code: "partner-code".to_string(),
- telemetry_suffix: "original-telemetry-suffix".to_string(),
- ..Default::default()
- };
-
- let override_record = JSONOverridesRecord {
- identifier: "test".to_string(),
- partner_code: "override-partner-code".to_string(),
- click_url: "https://example.com/click-url".to_string(),
- telemetry_suffix: None,
- urls: JSONEngineUrls {
- search: Some(JSONEngineUrl {
- base: Some("https://example.com/override-search".to_string()),
- display_name_map: Some(HashMap::from([
- // Default display name
- ("default".to_string(), "My Display Name".to_string()),
- // en-GB locale with unique display name
- ("en-GB".to_string(), "en-GB Display Name".to_string()),
- ])),
- ..Default::default()
- }),
- ..Default::default()
- },
- };
-
- test_engine.merge_override(
- &SearchUserEnvironment {
- // en-GB locale
- locale: "en-GB".into(),
- ..Default::default()
- },
- &override_record,
- );
-
- assert_eq!(
- test_engine.urls.search.display_name,
- Some("en-GB Display Name".to_string()),
- "Should override display name with en-GB version"
- );
- }
-
- #[test]
- fn test_from_configuration_details_fallsback_to_defaults() {
- // This test doesn't use `..Default::default()` as we want to
- // be explicit about `JSONEngineBase` and handling `None`
- // options/default values.
- let result = SearchEngineDefinition::from_configuration_details(
- &SearchUserEnvironment {
- locale: "fi".into(),
- ..Default::default()
- },
- "test",
- JSONEngineBase {
- aliases: None,
- charset: None,
- classification: SearchEngineClassification::General,
- name: "Test".to_string(),
- partner_code: None,
- urls: JSONEngineUrls {
- search: Some(JSONEngineUrl {
- base: Some("https://example.com".to_string()),
- ..Default::default()
- }),
- suggestions: None,
- trending: None,
- search_form: None,
- visual_search: None,
- },
- },
- &JSONEngineVariant {
- environment: JSONVariantEnvironment {
- all_regions_and_locales: true,
- ..Default::default()
- },
- is_new_until: None,
- optional: false,
- partner_code: None,
- telemetry_suffix: None,
- urls: None,
- sub_variants: vec![],
- },
- &None,
- );
-
- assert_eq!(
- result,
- SearchEngineDefinition {
- aliases: Vec::new(),
- charset: "UTF-8".to_string(),
- classification: SearchEngineClassification::General,
- identifier: "test".to_string(),
- is_new_until: None,
- partner_code: String::new(),
- name: "Test".to_string(),
- optional: false,
- order_hint: None,
- telemetry_suffix: String::new(),
- urls: SearchEngineUrls {
- search: SearchEngineUrl {
- base: "https://example.com".to_string(),
- ..Default::default()
- },
- suggestions: None,
- trending: None,
- search_form: None,
- visual_search: None,
- },
- click_url: None
- }
- )
- }
-
- static ENGINE_BASE: Lazy = Lazy::new(|| JSONEngineBase {
- aliases: Some(vec!["foo".to_string(), "bar".to_string()]),
- charset: Some("ISO-8859-15".to_string()),
- classification: SearchEngineClassification::Unknown,
- name: "Test".to_string(),
- partner_code: Some("firefox".to_string()),
- urls: JSONEngineUrls {
- search: Some(JSONEngineUrl {
- base: Some("https://example.com".to_string()),
- method: Some(crate::JSONEngineMethod::Post),
- params: Some(vec![
- SearchUrlParam {
- name: "param".to_string(),
- value: Some("test param".to_string()),
- enterprise_value: None,
- experiment_config: None,
- },
- SearchUrlParam {
- name: "enterprise-name".to_string(),
- value: None,
- enterprise_value: Some("enterprise-value".to_string()),
- experiment_config: None,
- },
- ]),
- search_term_param_name: Some("baz".to_string()),
- ..Default::default()
- }),
- suggestions: Some(JSONEngineUrl {
- base: Some("https://example.com/suggestions".to_string()),
- method: Some(crate::JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "suggest-name".to_string(),
- value: None,
- enterprise_value: None,
- experiment_config: Some("suggest-experiment-value".to_string()),
- }]),
- search_term_param_name: Some("suggest".to_string()),
- ..Default::default()
- }),
- trending: Some(JSONEngineUrl {
- base: Some("https://example.com/trending".to_string()),
- method: Some(crate::JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "trend-name".to_string(),
- value: Some("trend-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- ..Default::default()
- }),
- search_form: Some(JSONEngineUrl {
- base: Some("https://example.com/search_form".to_string()),
- method: Some(crate::JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "search-form-name".to_string(),
- value: Some("search-form-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- ..Default::default()
- }),
- visual_search: Some(JSONEngineUrl {
- base: Some("https://example.com/visual_search".to_string()),
- method: Some(crate::JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "visual-search-name".to_string(),
- value: Some("visual-search-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("url".to_string()),
- display_name_map: Some(HashMap::from([
- // Default display name
- ("default".to_string(), "Visual Search".to_string()),
- // en-GB locale with unique display name
- ("en-GB".to_string(), "Visual Search en-GB".to_string()),
- ])),
- is_new_until: Some("2095-01-01".to_string()),
- exclude_partner_code_from_telemetry: true,
- accepted_content_types: Some(vec![
- "image/gif".to_string(),
- "image/jpeg".to_string(),
- ]),
- }),
- },
- });
-
- #[test]
- fn test_from_configuration_details_uses_values() {
- let result = SearchEngineDefinition::from_configuration_details(
- &SearchUserEnvironment {
- locale: "fi".into(),
- ..Default::default()
- },
- "test",
- Lazy::force(&ENGINE_BASE).clone(),
- &JSONEngineVariant {
- environment: JSONVariantEnvironment {
- all_regions_and_locales: true,
- ..Default::default()
- },
- is_new_until: None,
- optional: false,
- partner_code: None,
- telemetry_suffix: None,
- urls: None,
- sub_variants: vec![],
- },
- &None,
- );
-
- assert_eq!(
- result,
- SearchEngineDefinition {
- aliases: vec!["foo".to_string(), "bar".to_string()],
- charset: "ISO-8859-15".to_string(),
- classification: SearchEngineClassification::Unknown,
- identifier: "test".to_string(),
- is_new_until: None,
- partner_code: "firefox".to_string(),
- name: "Test".to_string(),
- optional: false,
- order_hint: None,
- telemetry_suffix: String::new(),
- urls: SearchEngineUrls {
- search: SearchEngineUrl {
- base: "https://example.com".to_string(),
- method: "POST".to_string(),
- params: vec![
- SearchUrlParam {
- name: "param".to_string(),
- value: Some("test param".to_string()),
- enterprise_value: None,
- experiment_config: None,
- },
- SearchUrlParam {
- name: "enterprise-name".to_string(),
- value: None,
- enterprise_value: Some("enterprise-value".to_string()),
- experiment_config: None,
- },
- ],
- search_term_param_name: Some("baz".to_string()),
- ..Default::default()
- },
- suggestions: Some(SearchEngineUrl {
- base: "https://example.com/suggestions".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "suggest-name".to_string(),
- value: None,
- enterprise_value: None,
- experiment_config: Some("suggest-experiment-value".to_string()),
- }],
- search_term_param_name: Some("suggest".to_string()),
- ..Default::default()
- }),
- trending: Some(SearchEngineUrl {
- base: "https://example.com/trending".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "trend-name".to_string(),
- value: Some("trend-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- search_form: Some(SearchEngineUrl {
- base: "https://example.com/search_form".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "search-form-name".to_string(),
- value: Some("search-form-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- visual_search: Some(SearchEngineUrl {
- base: "https://example.com/visual_search".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "visual-search-name".to_string(),
- value: Some("visual-search-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("url".to_string()),
- display_name: Some("Visual Search".to_string()),
- is_new_until: Some("2095-01-01".to_string()),
- exclude_partner_code_from_telemetry: true,
- accepted_content_types: Some(vec![
- "image/gif".to_string(),
- "image/jpeg".to_string(),
- ]),
- }),
- },
- click_url: None
- }
- )
- }
-
- #[test]
- fn test_from_configuration_details_uses_values_locale_match() {
- let result = SearchEngineDefinition::from_configuration_details(
- &SearchUserEnvironment {
- // en-GB locale
- locale: "en-GB".into(),
- ..Default::default()
- },
- "test",
- Lazy::force(&ENGINE_BASE).clone(),
- &JSONEngineVariant {
- environment: JSONVariantEnvironment {
- all_regions_and_locales: true,
- ..Default::default()
- },
- is_new_until: None,
- optional: false,
- partner_code: None,
- telemetry_suffix: None,
- urls: None,
- sub_variants: vec![],
- },
- &None,
- );
-
- assert_eq!(
- result,
- SearchEngineDefinition {
- aliases: vec!["foo".to_string(), "bar".to_string()],
- charset: "ISO-8859-15".to_string(),
- classification: SearchEngineClassification::Unknown,
- identifier: "test".to_string(),
- is_new_until: None,
- partner_code: "firefox".to_string(),
- name: "Test".to_string(),
- optional: false,
- order_hint: None,
- telemetry_suffix: String::new(),
- urls: SearchEngineUrls {
- search: SearchEngineUrl {
- base: "https://example.com".to_string(),
- method: "POST".to_string(),
- params: vec![
- SearchUrlParam {
- name: "param".to_string(),
- value: Some("test param".to_string()),
- enterprise_value: None,
- experiment_config: None,
- },
- SearchUrlParam {
- name: "enterprise-name".to_string(),
- value: None,
- enterprise_value: Some("enterprise-value".to_string()),
- experiment_config: None,
- },
- ],
- search_term_param_name: Some("baz".to_string()),
- ..Default::default()
- },
- suggestions: Some(SearchEngineUrl {
- base: "https://example.com/suggestions".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "suggest-name".to_string(),
- value: None,
- enterprise_value: None,
- experiment_config: Some("suggest-experiment-value".to_string()),
- }],
- search_term_param_name: Some("suggest".to_string()),
- ..Default::default()
- }),
- trending: Some(SearchEngineUrl {
- base: "https://example.com/trending".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "trend-name".to_string(),
- value: Some("trend-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- search_form: Some(SearchEngineUrl {
- base: "https://example.com/search_form".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "search-form-name".to_string(),
- value: Some("search-form-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- visual_search: Some(SearchEngineUrl {
- base: "https://example.com/visual_search".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "visual-search-name".to_string(),
- value: Some("visual-search-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("url".to_string()),
- // Should be the en-GB display name since the "en-GB"
- // locale is present in `display_name_map`.
- display_name: Some("Visual Search en-GB".to_string()),
- is_new_until: Some("2095-01-01".to_string()),
- exclude_partner_code_from_telemetry: true,
- accepted_content_types: Some(vec![
- "image/gif".to_string(),
- "image/jpeg".to_string(),
- ]),
- }),
- },
- click_url: None
- }
- )
- }
-
- static ENGINE_VARIANT: Lazy = Lazy::new(|| JSONEngineVariant {
- environment: JSONVariantEnvironment {
- all_regions_and_locales: true,
- ..Default::default()
- },
- is_new_until: Some("2063-04-05".to_string()),
- optional: true,
- partner_code: Some("trek".to_string()),
- telemetry_suffix: Some("star".to_string()),
- urls: Some(JSONEngineUrls {
- search: Some(JSONEngineUrl {
- base: Some("https://example.com/variant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "variant".to_string(),
- value: Some("test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("ship".to_string()),
- ..Default::default()
- }),
- suggestions: Some(JSONEngineUrl {
- base: Some("https://example.com/suggestions-variant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "suggest-variant".to_string(),
- value: Some("sugg test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("variant".to_string()),
- ..Default::default()
- }),
- trending: Some(JSONEngineUrl {
- base: Some("https://example.com/trending-variant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "trend-variant".to_string(),
- value: Some("trend test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("trend".to_string()),
- exclude_partner_code_from_telemetry: true,
- ..Default::default()
- }),
- search_form: Some(JSONEngineUrl {
- base: Some("https://example.com/search_form".to_string()),
- method: Some(crate::JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "search-form-name".to_string(),
- value: Some("search-form-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- ..Default::default()
- }),
- visual_search: Some(JSONEngineUrl {
- base: Some("https://example.com/visual-search-variant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "visual-search-variant-name".to_string(),
- value: Some("visual-search-variant-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("url_variant".to_string()),
- display_name_map: Some(HashMap::from([
- ("default".to_string(), "Visual Search Variant".to_string()),
- // en-GB locale with unique display name
- (
- "en-GB".to_string(),
- "Visual Search Variant en-GB".to_string(),
- ),
- ])),
- is_new_until: Some("2096-02-02".to_string()),
- accepted_content_types: Some(vec![
- "image/png".to_string(),
- "image/jpeg".to_string(),
- ]),
- ..Default::default()
- }),
- }),
- sub_variants: vec![],
- });
-
- #[test]
- fn test_from_configuration_details_merges_variants() {
- let result = SearchEngineDefinition::from_configuration_details(
- &SearchUserEnvironment {
- locale: "fi".into(),
- ..Default::default()
- },
- "test",
- Lazy::force(&ENGINE_BASE).clone(),
- &ENGINE_VARIANT,
- &None,
- );
-
- assert_eq!(
- result,
- SearchEngineDefinition {
- aliases: vec!["foo".to_string(), "bar".to_string()],
- charset: "ISO-8859-15".to_string(),
- classification: SearchEngineClassification::Unknown,
- identifier: "test".to_string(),
- is_new_until: Some("2063-04-05".to_string()),
- partner_code: "trek".to_string(),
- name: "Test".to_string(),
- optional: true,
- order_hint: None,
- telemetry_suffix: "star".to_string(),
- urls: SearchEngineUrls {
- search: SearchEngineUrl {
- base: "https://example.com/variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "variant".to_string(),
- value: Some("test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("ship".to_string()),
- ..Default::default()
- },
- suggestions: Some(SearchEngineUrl {
- base: "https://example.com/suggestions-variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "suggest-variant".to_string(),
- value: Some("sugg test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("variant".to_string()),
- ..Default::default()
- }),
- trending: Some(SearchEngineUrl {
- base: "https://example.com/trending-variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "trend-variant".to_string(),
- value: Some("trend test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("trend".to_string()),
- exclude_partner_code_from_telemetry: true,
- ..Default::default()
- }),
- search_form: Some(SearchEngineUrl {
- base: "https://example.com/search_form".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "search-form-name".to_string(),
- value: Some("search-form-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- visual_search: Some(SearchEngineUrl {
- base: "https://example.com/visual-search-variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "visual-search-variant-name".to_string(),
- value: Some("visual-search-variant-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("url_variant".to_string()),
- // Should be the "default" display name since the "fi"
- // locale isn't present in `display_name_map`.
- display_name: Some("Visual Search Variant".to_string()),
- is_new_until: Some("2096-02-02".to_string()),
- exclude_partner_code_from_telemetry: false,
- accepted_content_types: Some(vec![
- "image/png".to_string(),
- "image/jpeg".to_string(),
- ]),
- }),
- },
- click_url: None
- }
- )
- }
-
- #[test]
- fn test_from_configuration_details_merges_variants_locale_match() {
- let result = SearchEngineDefinition::from_configuration_details(
- &SearchUserEnvironment {
- // en-GB locale
- locale: "en-GB".into(),
- ..Default::default()
- },
- "test",
- Lazy::force(&ENGINE_BASE).clone(),
- &ENGINE_VARIANT,
- &None,
- );
-
- assert_eq!(
- result,
- SearchEngineDefinition {
- aliases: vec!["foo".to_string(), "bar".to_string()],
- charset: "ISO-8859-15".to_string(),
- classification: SearchEngineClassification::Unknown,
- identifier: "test".to_string(),
- is_new_until: Some("2063-04-05".to_string()),
- partner_code: "trek".to_string(),
- name: "Test".to_string(),
- optional: true,
- order_hint: None,
- telemetry_suffix: "star".to_string(),
- urls: SearchEngineUrls {
- search: SearchEngineUrl {
- base: "https://example.com/variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "variant".to_string(),
- value: Some("test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("ship".to_string()),
- ..Default::default()
- },
- suggestions: Some(SearchEngineUrl {
- base: "https://example.com/suggestions-variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "suggest-variant".to_string(),
- value: Some("sugg test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("variant".to_string()),
- ..Default::default()
- }),
- trending: Some(SearchEngineUrl {
- base: "https://example.com/trending-variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "trend-variant".to_string(),
- value: Some("trend test variant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("trend".to_string()),
- exclude_partner_code_from_telemetry: true,
- ..Default::default()
- }),
- search_form: Some(SearchEngineUrl {
- base: "https://example.com/search_form".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "search-form-name".to_string(),
- value: Some("search-form-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- visual_search: Some(SearchEngineUrl {
- base: "https://example.com/visual-search-variant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "visual-search-variant-name".to_string(),
- value: Some("visual-search-variant-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("url_variant".to_string()),
- // Should be the en-GB display name since the "en-GB"
- // locale is present in `display_name_map`.
- display_name: Some("Visual Search Variant en-GB".to_string()),
- is_new_until: Some("2096-02-02".to_string()),
- exclude_partner_code_from_telemetry: false,
- accepted_content_types: Some(vec![
- "image/png".to_string(),
- "image/jpeg".to_string(),
- ]),
- }),
- },
- click_url: None
- }
- )
- }
-
- static ENGINE_SUBVARIANT: Lazy = Lazy::new(|| JSONEngineVariant {
- environment: JSONVariantEnvironment {
- all_regions_and_locales: true,
- ..Default::default()
- },
- is_new_until: Some("2063-04-05".to_string()),
- optional: true,
- partner_code: Some("trek2".to_string()),
- telemetry_suffix: Some("star2".to_string()),
- urls: Some(JSONEngineUrls {
- search: Some(JSONEngineUrl {
- base: Some("https://example.com/subvariant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "subvariant".to_string(),
- value: Some("test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("shuttle".to_string()),
- ..Default::default()
- }),
- suggestions: Some(JSONEngineUrl {
- base: Some("https://example.com/suggestions-subvariant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "suggest-subvariant".to_string(),
- value: Some("sugg test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("subvariant".to_string()),
- exclude_partner_code_from_telemetry: true,
- ..Default::default()
- }),
- trending: Some(JSONEngineUrl {
- base: Some("https://example.com/trending-subvariant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "trend-subvariant".to_string(),
- value: Some("trend test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("subtrend".to_string()),
- ..Default::default()
- }),
- search_form: Some(JSONEngineUrl {
- base: Some("https://example.com/search-form-subvariant".to_string()),
- method: Some(crate::JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "search-form-subvariant".to_string(),
- value: Some("search form subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- ..Default::default()
- }),
- visual_search: Some(JSONEngineUrl {
- base: Some("https://example.com/visual-search-subvariant".to_string()),
- method: Some(JSONEngineMethod::Get),
- params: Some(vec![SearchUrlParam {
- name: "visual-search-subvariant-name".to_string(),
- value: Some("visual-search-subvariant-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }]),
- search_term_param_name: Some("url_subvariant".to_string()),
- display_name_map: Some(HashMap::from([
- (
- "default".to_string(),
- "Visual Search Subvariant".to_string(),
- ),
- // en-GB locale with unique display name
- (
- "en-GB".to_string(),
- "Visual Search Subvariant en-GB".to_string(),
- ),
- ])),
- is_new_until: Some("2097-03-03".to_string()),
- accepted_content_types: Some(vec![
- "image/jpeg".to_string(),
- "image/webp".to_string(),
- ]),
- ..Default::default()
- }),
- }),
- sub_variants: vec![],
- });
-
- #[test]
- fn test_from_configuration_details_merges_sub_variants() {
- let result = SearchEngineDefinition::from_configuration_details(
- &SearchUserEnvironment {
- locale: "fi".into(),
- ..Default::default()
- },
- "test",
- Lazy::force(&ENGINE_BASE).clone(),
- &ENGINE_VARIANT,
- &Some(ENGINE_SUBVARIANT.clone()),
- );
-
- assert_eq!(
- result,
- SearchEngineDefinition {
- aliases: vec!["foo".to_string(), "bar".to_string()],
- charset: "ISO-8859-15".to_string(),
- classification: SearchEngineClassification::Unknown,
- identifier: "test".to_string(),
- is_new_until: Some("2063-04-05".to_string()),
- partner_code: "trek2".to_string(),
- name: "Test".to_string(),
- optional: true,
- order_hint: None,
- telemetry_suffix: "star2".to_string(),
- urls: SearchEngineUrls {
- search: SearchEngineUrl {
- base: "https://example.com/subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "subvariant".to_string(),
- value: Some("test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("shuttle".to_string()),
- ..Default::default()
- },
- suggestions: Some(SearchEngineUrl {
- base: "https://example.com/suggestions-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "suggest-subvariant".to_string(),
- value: Some("sugg test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("subvariant".to_string()),
- exclude_partner_code_from_telemetry: true,
- ..Default::default()
- }),
- trending: Some(SearchEngineUrl {
- base: "https://example.com/trending-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "trend-subvariant".to_string(),
- value: Some("trend test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("subtrend".to_string()),
- ..Default::default()
- }),
- search_form: Some(SearchEngineUrl {
- base: "https://example.com/search-form-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "search-form-subvariant".to_string(),
- value: Some("search form subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- visual_search: Some(SearchEngineUrl {
- base: "https://example.com/visual-search-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "visual-search-subvariant-name".to_string(),
- value: Some("visual-search-subvariant-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("url_subvariant".to_string()),
- // Should be the "default" display name since the "fi"
- // locale isn't present in `display_name_map`.
- display_name: Some("Visual Search Subvariant".to_string()),
- is_new_until: Some("2097-03-03".to_string()),
- exclude_partner_code_from_telemetry: false,
- accepted_content_types: Some(vec![
- "image/jpeg".to_string(),
- "image/webp".to_string(),
- ]),
- }),
- },
- click_url: None
- }
- )
+ );
+ assert_eq!(
+ test_engine.click_url,
+ Some("https://example.com/click-url".to_string()),
+ "Should override the click url"
+ );
+ assert_eq!(
+ test_engine.urls.search.base, "https://example.com/override-search",
+ "Should override search url"
+ );
+ assert_eq!(
+ test_engine.telemetry_suffix, "original-telemetry-suffix",
+ "Should not override telemetry suffix when telemetry suffix is supplied as None"
+ );
}
#[test]
- fn test_from_configuration_details_merges_sub_variants_locale_match() {
- let result = SearchEngineDefinition::from_configuration_details(
+ fn test_merge_override_locale_match() {
+ let mut test_engine = SearchEngineDefinition {
+ identifier: "test".to_string(),
+ partner_code: "partner-code".to_string(),
+ telemetry_suffix: "original-telemetry-suffix".to_string(),
+ ..Default::default()
+ };
+
+ let override_record = JSONOverridesRecord {
+ identifier: "test".to_string(),
+ partner_code: "override-partner-code".to_string(),
+ click_url: "https://example.com/click-url".to_string(),
+ telemetry_suffix: None,
+ urls: JSONEngineUrls {
+ search: Some(JSONEngineUrl {
+ base: Some("https://example.com/override-search".to_string()),
+ display_name_map: Some(HashMap::from([
+ // Default display name
+ ("default".to_string(), "My Display Name".to_string()),
+ // en-GB locale with unique display name
+ ("en-GB".to_string(), "en-GB Display Name".to_string()),
+ ])),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ };
+
+ test_engine.merge_override(
&SearchUserEnvironment {
// en-GB locale
locale: "en-GB".into(),
..Default::default()
},
- "test",
- Lazy::force(&ENGINE_BASE).clone(),
- &ENGINE_VARIANT,
- &Some(ENGINE_SUBVARIANT.clone()),
+ &override_record,
);
assert_eq!(
- result,
- SearchEngineDefinition {
- aliases: vec!["foo".to_string(), "bar".to_string()],
- charset: "ISO-8859-15".to_string(),
- classification: SearchEngineClassification::Unknown,
- identifier: "test".to_string(),
- is_new_until: Some("2063-04-05".to_string()),
- partner_code: "trek2".to_string(),
- name: "Test".to_string(),
- optional: true,
- order_hint: None,
- telemetry_suffix: "star2".to_string(),
- urls: SearchEngineUrls {
- search: SearchEngineUrl {
- base: "https://example.com/subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "subvariant".to_string(),
- value: Some("test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("shuttle".to_string()),
- ..Default::default()
- },
- suggestions: Some(SearchEngineUrl {
- base: "https://example.com/suggestions-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "suggest-subvariant".to_string(),
- value: Some("sugg test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("subvariant".to_string()),
- exclude_partner_code_from_telemetry: true,
- ..Default::default()
- }),
- trending: Some(SearchEngineUrl {
- base: "https://example.com/trending-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "trend-subvariant".to_string(),
- value: Some("trend test subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("subtrend".to_string()),
- ..Default::default()
- }),
- search_form: Some(SearchEngineUrl {
- base: "https://example.com/search-form-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "search-form-subvariant".to_string(),
- value: Some("search form subvariant".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- ..Default::default()
- }),
- visual_search: Some(SearchEngineUrl {
- base: "https://example.com/visual-search-subvariant".to_string(),
- method: "GET".to_string(),
- params: vec![SearchUrlParam {
- name: "visual-search-subvariant-name".to_string(),
- value: Some("visual-search-subvariant-value".to_string()),
- enterprise_value: None,
- experiment_config: None,
- }],
- search_term_param_name: Some("url_subvariant".to_string()),
- // Should be the en-GB display name since the "en-GB"
- // locale is present in `display_name_map`.
- display_name: Some("Visual Search Subvariant en-GB".to_string()),
- is_new_until: Some("2097-03-03".to_string()),
- exclude_partner_code_from_telemetry: false,
- accepted_content_types: Some(vec![
- "image/jpeg".to_string(),
- "image/webp".to_string(),
- ]),
- }),
- },
- click_url: None
- }
- )
+ test_engine.urls.search.display_name,
+ Some("en-GB Display Name".to_string()),
+ "Should override display name with en-GB version"
+ );
}
static ENGINES_LIST: Lazy> = Lazy::new(|| {
@@ -1980,3 +1003,386 @@ mod tests {
);
}
}
+
+#[cfg(test)]
+mod from_configuration_details_tests {
+ use crate::test_helpers::{
+ ExpectedEngineFromJSONBase, JSON_ENGINE_BASE, JSON_ENGINE_SUBVARIANT, JSON_ENGINE_VARIANT,
+ };
+ use crate::*;
+ use once_cell::sync::Lazy;
+
+ #[test]
+ fn test_fallsback_to_defaults() {
+ // This test doesn't use `..Default::default()` as we want to
+ // be explicit about `JSONEngineBase` and handling `None`
+ // options/default values.
+ let result = SearchEngineDefinition::from_configuration_details(
+ &SearchUserEnvironment {
+ locale: "fi".into(),
+ ..Default::default()
+ },
+ "test",
+ JSONEngineBase {
+ aliases: None,
+ charset: None,
+ classification: SearchEngineClassification::General,
+ name: "Test".to_string(),
+ partner_code: None,
+ urls: JSONEngineUrls {
+ search: Some(JSONEngineUrl {
+ base: Some("https://example.com".to_string()),
+ ..Default::default()
+ }),
+ suggestions: None,
+ trending: None,
+ search_form: None,
+ visual_search: None,
+ },
+ },
+ &JSONEngineVariant {
+ environment: JSONVariantEnvironment {
+ all_regions_and_locales: true,
+ ..Default::default()
+ },
+ is_new_until: None,
+ optional: false,
+ partner_code: None,
+ telemetry_suffix: None,
+ urls: None,
+ sub_variants: vec![],
+ },
+ &None,
+ );
+
+ assert_eq!(
+ result,
+ SearchEngineDefinition {
+ aliases: Vec::new(),
+ charset: "UTF-8".to_string(),
+ classification: SearchEngineClassification::General,
+ identifier: "test".to_string(),
+ is_new_until: None,
+ partner_code: String::new(),
+ name: "Test".to_string(),
+ optional: false,
+ order_hint: None,
+ telemetry_suffix: String::new(),
+ urls: SearchEngineUrls {
+ search: SearchEngineUrl {
+ base: "https://example.com".to_string(),
+ ..Default::default()
+ },
+ suggestions: None,
+ trending: None,
+ search_form: None,
+ visual_search: None,
+ },
+ click_url: None
+ }
+ )
+ }
+
+ #[test]
+ fn test_uses_base_values_only() {
+ let result = SearchEngineDefinition::from_configuration_details(
+ &SearchUserEnvironment {
+ locale: "fi".into(),
+ ..Default::default()
+ },
+ "test",
+ Lazy::force(&JSON_ENGINE_BASE).clone(),
+ &JSONEngineVariant {
+ environment: JSONVariantEnvironment {
+ all_regions_and_locales: true,
+ ..Default::default()
+ },
+ is_new_until: None,
+ optional: false,
+ partner_code: None,
+ telemetry_suffix: None,
+ urls: None,
+ sub_variants: vec![],
+ },
+ &None,
+ );
+ assert_eq!(
+ result,
+ ExpectedEngineFromJSONBase::new("test", "Test").build()
+ );
+ }
+
+ #[test]
+ fn test_uses_locale_specific_visual_display_name() {
+ let result = SearchEngineDefinition::from_configuration_details(
+ &SearchUserEnvironment {
+ locale: "en-GB".into(),
+ ..Default::default()
+ },
+ "test",
+ Lazy::force(&JSON_ENGINE_BASE).clone(),
+ &JSONEngineVariant {
+ environment: JSONVariantEnvironment {
+ all_regions_and_locales: true,
+ ..Default::default()
+ },
+ is_new_until: None,
+ optional: false,
+ partner_code: None,
+ telemetry_suffix: None,
+ urls: None,
+ sub_variants: vec![],
+ },
+ &None,
+ );
+
+ assert_eq!(
+ result,
+ ExpectedEngineFromJSONBase::new("test", "Test")
+ .visual_search_display_name("Visual Search en-GB")
+ .build()
+ );
+ }
+
+ #[test]
+ fn test_merges_variants() {
+ let result = SearchEngineDefinition::from_configuration_details(
+ &SearchUserEnvironment {
+ locale: "fi".into(),
+ ..Default::default()
+ },
+ "test",
+ Lazy::force(&JSON_ENGINE_BASE).clone(),
+ &JSON_ENGINE_VARIANT,
+ &None,
+ );
+
+ assert_eq!(
+ result,
+ ExpectedEngineFromJSONBase::new("test", "Test")
+ .variant_is_new_until("2063-04-05")
+ .variant_optional(true)
+ .variant_partner_code("trek")
+ .variant_telemetry_suffix("star")
+ .variant_search_url(
+ "https://example.com/variant",
+ "GET",
+ "variant",
+ "test variant",
+ "ship",
+ )
+ .variant_suggestions_url(
+ "https://example.com/suggestions-variant",
+ "GET",
+ "suggest-variant",
+ "sugg test variant",
+ "variant",
+ )
+ .variant_trending_url(
+ "https://example.com/trending-variant",
+ "GET",
+ "trend-variant",
+ "trend test variant",
+ "trend",
+ true,
+ )
+ .variant_search_form_url(
+ "https://example.com/search_form",
+ "GET",
+ "search-form-name",
+ "search-form-value",
+ )
+ .variant_visual_search_url(
+ "https://example.com/visual-search-variant",
+ "visual-search-variant-name",
+ "visual-search-variant-value",
+ "url_variant",
+ "Visual Search Variant",
+ "2096-02-02",
+ )
+ .build()
+ );
+ }
+
+ #[test]
+ fn test_merges_variant_and_uses_locale_specific_visual_search_display_name() {
+ let result = SearchEngineDefinition::from_configuration_details(
+ &SearchUserEnvironment {
+ locale: "en-GB".into(),
+ ..Default::default()
+ },
+ "test",
+ Lazy::force(&JSON_ENGINE_BASE).clone(),
+ &JSON_ENGINE_VARIANT,
+ &None,
+ );
+
+ assert_eq!(
+ result,
+ ExpectedEngineFromJSONBase::new("test", "Test")
+ .variant_is_new_until("2063-04-05")
+ .variant_optional(true)
+ .variant_partner_code("trek")
+ .variant_telemetry_suffix("star")
+ .variant_search_url(
+ "https://example.com/variant",
+ "GET",
+ "variant",
+ "test variant",
+ "ship",
+ )
+ .variant_suggestions_url(
+ "https://example.com/suggestions-variant",
+ "GET",
+ "suggest-variant",
+ "sugg test variant",
+ "variant",
+ )
+ .variant_trending_url(
+ "https://example.com/trending-variant",
+ "GET",
+ "trend-variant",
+ "trend test variant",
+ "trend",
+ true,
+ )
+ .variant_search_form_url(
+ "https://example.com/search_form",
+ "GET",
+ "search-form-name",
+ "search-form-value",
+ )
+ .variant_visual_search_url(
+ "https://example.com/visual-search-variant",
+ "visual-search-variant-name",
+ "visual-search-variant-value",
+ "url_variant",
+ // locale-specific display name is the key difference here
+ "Visual Search Variant en-GB",
+ "2096-02-02",
+ )
+ .build()
+ );
+ }
+
+ #[test]
+ fn test_merges_sub_variants() {
+ let result = SearchEngineDefinition::from_configuration_details(
+ &SearchUserEnvironment {
+ locale: "fi".into(),
+ ..Default::default()
+ },
+ "test",
+ Lazy::force(&JSON_ENGINE_BASE).clone(),
+ &JSON_ENGINE_VARIANT,
+ &Some(JSON_ENGINE_SUBVARIANT.clone()),
+ );
+
+ assert_eq!(
+ result,
+ ExpectedEngineFromJSONBase::new("test", "Test")
+ .variant_is_new_until("2063-04-05")
+ .variant_optional(true)
+ .subvariant_partner_code("trek2")
+ .subvariant_telemetry_suffix("star2")
+ .subvariant_search_url(
+ "https://example.com/subvariant",
+ "GET",
+ "subvariant",
+ "test subvariant",
+ "shuttle",
+ )
+ .subvariant_suggestions_url(
+ "https://example.com/suggestions-subvariant",
+ "GET",
+ "suggest-subvariant",
+ "sugg test subvariant",
+ "subvariant",
+ true,
+ )
+ .subvariant_trending_url(
+ "https://example.com/trending-subvariant",
+ "GET",
+ "trend-subvariant",
+ "trend test subvariant",
+ "subtrend",
+ )
+ .subvariant_search_form_url(
+ "https://example.com/search-form-subvariant",
+ "GET",
+ "search-form-subvariant",
+ "search form subvariant",
+ )
+ .subvariant_visual_search_url(
+ "https://example.com/visual-search-subvariant",
+ "visual-search-subvariant-name",
+ "visual-search-subvariant-value",
+ "url_subvariant",
+ "Visual Search Subvariant",
+ "2097-03-03",
+ )
+ .build()
+ );
+ }
+
+ #[test]
+ fn test_merges_subvariant_and_uses_locale_specific_visual_search_display_name() {
+ let result = SearchEngineDefinition::from_configuration_details(
+ &SearchUserEnvironment {
+ locale: "en-GB".into(),
+ ..Default::default()
+ },
+ "test",
+ Lazy::force(&JSON_ENGINE_BASE).clone(),
+ &JSON_ENGINE_VARIANT,
+ &Some(JSON_ENGINE_SUBVARIANT.clone()),
+ );
+
+ assert_eq!(
+ result,
+ ExpectedEngineFromJSONBase::new("test", "Test")
+ .variant_is_new_until("2063-04-05")
+ .variant_optional(true)
+ .subvariant_partner_code("trek2")
+ .subvariant_telemetry_suffix("star2")
+ .subvariant_search_url(
+ "https://example.com/subvariant",
+ "GET",
+ "subvariant",
+ "test subvariant",
+ "shuttle",
+ )
+ .subvariant_suggestions_url(
+ "https://example.com/suggestions-subvariant",
+ "GET",
+ "suggest-subvariant",
+ "sugg test subvariant",
+ "subvariant",
+ true,
+ )
+ .subvariant_trending_url(
+ "https://example.com/trending-subvariant",
+ "GET",
+ "trend-subvariant",
+ "trend test subvariant",
+ "subtrend",
+ )
+ .subvariant_search_form_url(
+ "https://example.com/search-form-subvariant",
+ "GET",
+ "search-form-subvariant",
+ "search form subvariant",
+ )
+ .subvariant_visual_search_url(
+ "https://example.com/visual-search-subvariant",
+ "visual-search-subvariant-name",
+ "visual-search-subvariant-value",
+ "url_subvariant",
+ // locale-specific display name is the key difference here
+ "Visual Search Subvariant en-GB",
+ "2097-03-03",
+ )
+ .build()
+ );
+ }
+}
diff --git a/third_party/rust/search/src/test_helpers.rs b/third_party/rust/search/src/test_helpers.rs
index 186e160be9eb2..9cb2470d0dd27 100644
--- a/third_party/rust/search/src/test_helpers.rs
+++ b/third_party/rust/search/src/test_helpers.rs
@@ -1,6 +1,7 @@
use crate::{
- SearchEngineClassification, SearchEngineDefinition, SearchEngineUrl, SearchEngineUrls,
- SearchUrlParam,
+ JSONEngineBase, JSONEngineMethod, JSONEngineUrl, JSONEngineUrls, JSONEngineVariant,
+ JSONVariantEnvironment, SearchEngineClassification, SearchEngineDefinition, SearchEngineUrl,
+ SearchEngineUrls, SearchUrlParam,
};
use serde_json::{json, Value};
@@ -500,3 +501,648 @@ impl ExpectedEngine {
}
}
}
+
+#[cfg(test)]
+use once_cell::sync::Lazy;
+
+#[cfg(test)]
+use std::collections::HashMap;
+
+pub static JSON_ENGINE_BASE: Lazy = Lazy::new(|| JSONEngineBase {
+ aliases: Some(vec!["foo".to_string(), "bar".to_string()]),
+ charset: Some("ISO-8859-15".to_string()),
+ classification: SearchEngineClassification::Unknown,
+ name: "Test".to_string(),
+ partner_code: Some("firefox".to_string()),
+ urls: JSONEngineUrls {
+ search: Some(JSONEngineUrl {
+ base: Some("https://example.com".to_string()),
+ method: Some(JSONEngineMethod::Post),
+ params: Some(vec![
+ SearchUrlParam {
+ name: "param".to_string(),
+ value: Some("test param".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ },
+ SearchUrlParam {
+ name: "enterprise-name".to_string(),
+ value: None,
+ enterprise_value: Some("enterprise-value".to_string()),
+ experiment_config: None,
+ },
+ ]),
+ search_term_param_name: Some("baz".to_string()),
+ ..Default::default()
+ }),
+ suggestions: Some(JSONEngineUrl {
+ base: Some("https://example.com/suggestions".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "suggest-name".to_string(),
+ value: None,
+ enterprise_value: None,
+ experiment_config: Some("suggest-experiment-value".to_string()),
+ }]),
+ search_term_param_name: Some("suggest".to_string()),
+ ..Default::default()
+ }),
+ trending: Some(JSONEngineUrl {
+ base: Some("https://example.com/trending".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "trend-name".to_string(),
+ value: Some("trend-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ ..Default::default()
+ }),
+ search_form: Some(JSONEngineUrl {
+ base: Some("https://example.com/search_form".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "search-form-name".to_string(),
+ value: Some("search-form-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ ..Default::default()
+ }),
+ visual_search: Some(JSONEngineUrl {
+ base: Some("https://example.com/visual_search".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "visual-search-name".to_string(),
+ value: Some("visual-search-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("url".to_string()),
+ display_name_map: Some(HashMap::from([
+ // Default display name
+ ("default".to_string(), "Visual Search".to_string()),
+ // en-GB locale with unique display name
+ ("en-GB".to_string(), "Visual Search en-GB".to_string()),
+ ])),
+ is_new_until: Some("2095-01-01".to_string()),
+ exclude_partner_code_from_telemetry: true,
+ accepted_content_types: Some(vec!["image/gif".to_string(), "image/jpeg".to_string()]),
+ }),
+ },
+});
+
+#[cfg(test)]
+pub static JSON_ENGINE_VARIANT: Lazy = Lazy::new(|| JSONEngineVariant {
+ environment: JSONVariantEnvironment {
+ all_regions_and_locales: true,
+ ..Default::default()
+ },
+ is_new_until: Some("2063-04-05".to_string()),
+ optional: true,
+ partner_code: Some("trek".to_string()),
+ telemetry_suffix: Some("star".to_string()),
+ urls: Some(JSONEngineUrls {
+ search: Some(JSONEngineUrl {
+ base: Some("https://example.com/variant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "variant".to_string(),
+ value: Some("test variant".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("ship".to_string()),
+ ..Default::default()
+ }),
+ suggestions: Some(JSONEngineUrl {
+ base: Some("https://example.com/suggestions-variant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "suggest-variant".to_string(),
+ value: Some("sugg test variant".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("variant".to_string()),
+ ..Default::default()
+ }),
+ trending: Some(JSONEngineUrl {
+ base: Some("https://example.com/trending-variant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "trend-variant".to_string(),
+ value: Some("trend test variant".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("trend".to_string()),
+ exclude_partner_code_from_telemetry: true,
+ ..Default::default()
+ }),
+ search_form: Some(JSONEngineUrl {
+ base: Some("https://example.com/search_form".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "search-form-name".to_string(),
+ value: Some("search-form-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ ..Default::default()
+ }),
+ visual_search: Some(JSONEngineUrl {
+ base: Some("https://example.com/visual-search-variant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "visual-search-variant-name".to_string(),
+ value: Some("visual-search-variant-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("url_variant".to_string()),
+ display_name_map: Some(HashMap::from([
+ ("default".to_string(), "Visual Search Variant".to_string()),
+ (
+ "en-GB".to_string(),
+ "Visual Search Variant en-GB".to_string(),
+ ),
+ ])),
+ is_new_until: Some("2096-02-02".to_string()),
+ accepted_content_types: Some(vec!["image/png".to_string(), "image/jpeg".to_string()]),
+ ..Default::default()
+ }),
+ }),
+ sub_variants: vec![],
+});
+
+#[cfg(test)]
+pub static JSON_ENGINE_SUBVARIANT: Lazy = Lazy::new(|| JSONEngineVariant {
+ environment: JSONVariantEnvironment {
+ all_regions_and_locales: true,
+ ..Default::default()
+ },
+ is_new_until: Some("2063-04-05".to_string()),
+ optional: true,
+ partner_code: Some("trek2".to_string()),
+ telemetry_suffix: Some("star2".to_string()),
+ urls: Some(JSONEngineUrls {
+ search: Some(JSONEngineUrl {
+ base: Some("https://example.com/subvariant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "subvariant".to_string(),
+ value: Some("test subvariant".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("shuttle".to_string()),
+ ..Default::default()
+ }),
+ suggestions: Some(JSONEngineUrl {
+ base: Some("https://example.com/suggestions-subvariant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "suggest-subvariant".to_string(),
+ value: Some("sugg test subvariant".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("subvariant".to_string()),
+ exclude_partner_code_from_telemetry: true,
+ ..Default::default()
+ }),
+ trending: Some(JSONEngineUrl {
+ base: Some("https://example.com/trending-subvariant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "trend-subvariant".to_string(),
+ value: Some("trend test subvariant".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("subtrend".to_string()),
+ ..Default::default()
+ }),
+ search_form: Some(JSONEngineUrl {
+ base: Some("https://example.com/search-form-subvariant".to_string()),
+ method: Some(crate::JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "search-form-subvariant".to_string(),
+ value: Some("search form subvariant".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ ..Default::default()
+ }),
+ visual_search: Some(JSONEngineUrl {
+ base: Some("https://example.com/visual-search-subvariant".to_string()),
+ method: Some(JSONEngineMethod::Get),
+ params: Some(vec![SearchUrlParam {
+ name: "visual-search-subvariant-name".to_string(),
+ value: Some("visual-search-subvariant-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }]),
+ search_term_param_name: Some("url_subvariant".to_string()),
+ display_name_map: Some(HashMap::from([
+ (
+ "default".to_string(),
+ "Visual Search Subvariant".to_string(),
+ ),
+ // en-GB locale with unique display name
+ (
+ "en-GB".to_string(),
+ "Visual Search Subvariant en-GB".to_string(),
+ ),
+ ])),
+ is_new_until: Some("2097-03-03".to_string()),
+ accepted_content_types: Some(vec!["image/jpeg".to_string(), "image/webp".to_string()]),
+ ..Default::default()
+ }),
+ }),
+ sub_variants: vec![],
+});
+
+#[cfg(test)]
+pub struct ExpectedEngineFromJSONBase {
+ engine: SearchEngineDefinition,
+}
+
+#[cfg(test)]
+impl ExpectedEngineFromJSONBase {
+ pub fn new(identifier: &str, name: &str) -> Self {
+ Self {
+ engine: SearchEngineDefinition {
+ aliases: vec!["foo".to_string(), "bar".to_string()],
+ charset: "ISO-8859-15".to_string(),
+ classification: SearchEngineClassification::Unknown,
+ identifier: identifier.to_string(),
+ is_new_until: None,
+ partner_code: "firefox".to_string(),
+ name: name.to_string(),
+ optional: false,
+ order_hint: None,
+ telemetry_suffix: String::new(),
+ urls: SearchEngineUrls {
+ search: SearchEngineUrl {
+ base: "https://example.com".to_string(),
+ method: "POST".to_string(),
+ params: vec![
+ SearchUrlParam {
+ name: "param".to_string(),
+ value: Some("test param".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ },
+ SearchUrlParam {
+ name: "enterprise-name".to_string(),
+ value: None,
+ enterprise_value: Some("enterprise-value".to_string()),
+ experiment_config: None,
+ },
+ ],
+ search_term_param_name: Some("baz".to_string()),
+ ..Default::default()
+ },
+ suggestions: Some(SearchEngineUrl {
+ base: "https://example.com/suggestions".to_string(),
+ method: "GET".to_string(),
+ params: vec![SearchUrlParam {
+ name: "suggest-name".to_string(),
+ value: None,
+ enterprise_value: None,
+ experiment_config: Some("suggest-experiment-value".to_string()),
+ }],
+ search_term_param_name: Some("suggest".to_string()),
+ ..Default::default()
+ }),
+ trending: Some(SearchEngineUrl {
+ base: "https://example.com/trending".to_string(),
+ method: "GET".to_string(),
+ params: vec![SearchUrlParam {
+ name: "trend-name".to_string(),
+ value: Some("trend-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ ..Default::default()
+ }),
+ search_form: Some(SearchEngineUrl {
+ base: "https://example.com/search_form".to_string(),
+ method: "GET".to_string(),
+ params: vec![SearchUrlParam {
+ name: "search-form-name".to_string(),
+ value: Some("search-form-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ ..Default::default()
+ }),
+ visual_search: Some(SearchEngineUrl {
+ base: "https://example.com/visual_search".to_string(),
+ method: "GET".to_string(),
+ params: vec![SearchUrlParam {
+ name: "visual-search-name".to_string(),
+ value: Some("visual-search-value".to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some("url".to_string()),
+ display_name: Some("Visual Search".to_string()),
+ is_new_until: Some("2095-01-01".to_string()),
+ exclude_partner_code_from_telemetry: true,
+ accepted_content_types: Some(vec![
+ "image/gif".to_string(),
+ "image/jpeg".to_string(),
+ ]),
+ }),
+ },
+ click_url: None,
+ },
+ }
+ }
+ pub fn variant_is_new_until(mut self, date: &str) -> Self {
+ self.engine.is_new_until = Some(date.to_string());
+ self
+ }
+
+ pub fn variant_optional(mut self, optional: bool) -> Self {
+ self.engine.optional = optional;
+ self
+ }
+
+ pub fn variant_partner_code(mut self, partner_code: &str) -> Self {
+ self.engine.partner_code = partner_code.to_string();
+ self
+ }
+
+ pub fn variant_telemetry_suffix(mut self, suffix: &str) -> Self {
+ self.engine.telemetry_suffix = suffix.to_string();
+ self
+ }
+
+ pub fn visual_search_display_name(mut self, display_name: &str) -> Self {
+ let visual = self
+ .engine
+ .urls
+ .visual_search
+ .as_mut()
+ .expect("Expected base engine to include visual_search");
+
+ visual.display_name = Some(display_name.to_string());
+ self
+ }
+
+ pub fn variant_search_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ ) -> Self {
+ self.engine.urls.search = SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ ..Default::default()
+ };
+ self
+ }
+
+ pub fn variant_suggestions_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ ) -> Self {
+ self.engine.urls.suggestions = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ ..Default::default()
+ });
+ self
+ }
+
+ pub fn variant_trending_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ exclude_partner_code_from_telemetry: bool,
+ ) -> Self {
+ self.engine.urls.trending = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ exclude_partner_code_from_telemetry,
+ ..Default::default()
+ });
+ self
+ }
+
+ pub fn variant_search_form_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ ) -> Self {
+ self.engine.urls.search_form = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ ..Default::default()
+ });
+ self
+ }
+
+ pub fn variant_visual_search_url(
+ mut self,
+ base: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ display_name: &str,
+ is_new_until: &str,
+ ) -> Self {
+ self.engine.urls.visual_search = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: "GET".to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ display_name: Some(display_name.to_string()),
+ is_new_until: Some(is_new_until.to_string()),
+ accepted_content_types: Some(vec!["image/png".to_string(), "image/jpeg".to_string()]),
+ exclude_partner_code_from_telemetry: false,
+ });
+ self
+ }
+
+ pub fn subvariant_partner_code(mut self, partner_code: &str) -> Self {
+ self.engine.partner_code = partner_code.to_string();
+ self
+ }
+
+ pub fn subvariant_telemetry_suffix(mut self, suffix: &str) -> Self {
+ self.engine.telemetry_suffix = suffix.to_string();
+ self
+ }
+
+ pub fn subvariant_search_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ ) -> Self {
+ self.engine.urls.search = SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ ..Default::default()
+ };
+ self
+ }
+
+ pub fn subvariant_suggestions_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ exclude_partner_code_from_telemetry: bool,
+ ) -> Self {
+ self.engine.urls.suggestions = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ exclude_partner_code_from_telemetry,
+ ..Default::default()
+ });
+ self
+ }
+
+ pub fn subvariant_trending_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ ) -> Self {
+ self.engine.urls.trending = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ ..Default::default()
+ });
+ self
+ }
+
+ pub fn subvariant_search_form_url(
+ mut self,
+ base: &str,
+ method: &str,
+ param_name: &str,
+ param_value: &str,
+ ) -> Self {
+ self.engine.urls.search_form = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: method.to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ ..Default::default()
+ });
+ self
+ }
+
+ pub fn subvariant_visual_search_url(
+ mut self,
+ base: &str,
+ param_name: &str,
+ param_value: &str,
+ search_term_param_name: &str,
+ display_name: &str,
+ is_new_until: &str,
+ ) -> Self {
+ self.engine.urls.visual_search = Some(SearchEngineUrl {
+ base: base.to_string(),
+ method: "GET".to_string(),
+ params: vec![SearchUrlParam {
+ name: param_name.to_string(),
+ value: Some(param_value.to_string()),
+ enterprise_value: None,
+ experiment_config: None,
+ }],
+ search_term_param_name: Some(search_term_param_name.to_string()),
+ display_name: Some(display_name.to_string()),
+ is_new_until: Some(is_new_until.to_string()),
+ exclude_partner_code_from_telemetry: false,
+ accepted_content_types: Some(vec!["image/jpeg".to_string(), "image/webp".to_string()]),
+ });
+ self
+ }
+
+ pub fn build(self) -> SearchEngineDefinition {
+ self.engine
+ }
+}
diff --git a/third_party/rust/search/uniffi.toml b/third_party/rust/search/uniffi.toml
index 06d98be36aed2..8ac17103752cf 100644
--- a/third_party/rust/search/uniffi.toml
+++ b/third_party/rust/search/uniffi.toml
@@ -1,5 +1,7 @@
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
ffi_module_filename = "searchFFI"
+
[bindings.kotlin]
package_name = "mozilla.appservices.search"
+omit_checksums = true
diff --git a/third_party/rust/suggest/.cargo-checksum.json b/third_party/rust/suggest/.cargo-checksum.json
index cec26e95fdf28..17ac969faa753 100644
--- a/third_party/rust/suggest/.cargo-checksum.json
+++ b/third_party/rust/suggest/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"f5d1ff1a9b75bfc0eeee9ac46230fa47946f1d4dd91f0539bfbeeeafffcef046","README.md":"60e4f7ca928dd739768925ff26eada1907509b3e48dfd369a00845bc7eac1653","metrics.yaml":"5df9ca1a3415d9a796ccc8dba8e3126cbcc338f7792b0053030e3b5399470149","src/benchmarks/README.md":"ccee8dbddba8762d0453fa855bd6984137b224b8c019f3dd8e86a3c303f51d71","src/benchmarks/client.rs":"e5897d4e2eda06809fa6dc6db4e780b9ef266f613fb113aa6613b83f7005dd0b","src/benchmarks/geoname.rs":"fb4e8eb48e27879fe3f35d6d2415f0237b087b51b89ee03fb50fa635e1c1a3b5","src/benchmarks/ingest.rs":"3e0dc8239452b449df79100bfe21fbfae85f6c55b6bc17336e9f86000a7b4042","src/benchmarks/mod.rs":"2c9a39b7a5144674d2475f4d7d69d77c4545f9aa5f123968cb32574e76f10b1a","src/benchmarks/query.rs":"d54946063e72cf98e7f46d94665c17c66af637774c2bb50cd5798dbe63d74f3c","src/config.rs":"0ca876e845841bb6429862c0904c82265003f53b55aea053fac60aed278586a7","src/db.rs":"9492dfe1660fad68770f7746682c164205fa0e8f4bbaadfda75d18f5d791b42d","src/error.rs":"fd26db688c01987f4f4cbd1e4f728b7f4981c24d4c13bfbc9d92a1322e99c740","src/fakespot.rs":"f501c9fe5296e7c130a9fcb532b861465717652cb5ef688230bc7a3b94df91b1","src/geoname.rs":"adbf29aab9031c2d09242dfcbf351c139b8bb792fa42ed72ef33082c920f69e8","src/lib.rs":"772dfc3625c0733dde442a5e10020d53e54fc86e39c37ce43e84dfb79a534936","src/metrics.rs":"871f0d834efbbc9e26d61f66fa31f0021dcf41444746cd7c082f93ba9628e399","src/provider.rs":"dbf7a0da37f7022c6606c06356ce152b44f799dabd6a6ee734dba426c1d96386","src/query.rs":"d4eeaf43251c8e6404ac75570ed069085c7e3f61be887ab8d127b08c106198c5","src/rs.rs":"60bc209657820050151c7025f14055f6bc56b889ac16fc827bd4697a3033f3f6","src/schema.rs":"490f7544b45361807fb7fe3d213e1827ef0b4f588c2b35fad46eb4478d6a8c5a","src/store.rs":"d1b561668a9ddb4ceabf83a69ca949879c7d8fe333d8b1bad4d1a42623629259","src/suggestion.rs":"d97050db6df628da09a8f481a4e650b17ce916f3cd88118c6571d2883d36b964","src/testing/client.rs":"47a32fd84c733001f11e8bfff94dc8c060b6b0780346dca5ddc7a5f5489c1d85","src/testing/data.rs":"4f79c37eb026b02d60fdbcfa871df4e25589d2cd8153753ae0a3f05333ce4423","src/testing/mod.rs":"7b4a507aceb159347843ba09461e0d58962027ba5dcd35aa4b993fe3666508ce","src/util.rs":"22091900a1f83babe126d86da10388f9d4d22b34dd1e67afdd868fedeb460c2b","src/weather.rs":"d7b747a32549d5200f19b2ba851cda84086b8a8f4ee7affe90e95920b22274f2","src/yelp.rs":"1fe3b7eb6b3f7462e9758b6eb62457dfa26f7549a8290cdff7637d2fb3ffea4f","uniffi.toml":"8205e4679ac26d53e70af0f85c013fd27cda1119f4322aebf5f2b9403d45a611"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"972433fcd4f63cff7052c8cb614b20571d729ecc0f5fee2d9b5a07b17f3f207f","README.md":"60e4f7ca928dd739768925ff26eada1907509b3e48dfd369a00845bc7eac1653","metrics.yaml":"5df9ca1a3415d9a796ccc8dba8e3126cbcc338f7792b0053030e3b5399470149","src/benchmarks/README.md":"ccee8dbddba8762d0453fa855bd6984137b224b8c019f3dd8e86a3c303f51d71","src/benchmarks/client.rs":"e5897d4e2eda06809fa6dc6db4e780b9ef266f613fb113aa6613b83f7005dd0b","src/benchmarks/geoname.rs":"fb4e8eb48e27879fe3f35d6d2415f0237b087b51b89ee03fb50fa635e1c1a3b5","src/benchmarks/ingest.rs":"3e0dc8239452b449df79100bfe21fbfae85f6c55b6bc17336e9f86000a7b4042","src/benchmarks/mod.rs":"2c9a39b7a5144674d2475f4d7d69d77c4545f9aa5f123968cb32574e76f10b1a","src/benchmarks/query.rs":"d54946063e72cf98e7f46d94665c17c66af637774c2bb50cd5798dbe63d74f3c","src/config.rs":"0ca876e845841bb6429862c0904c82265003f53b55aea053fac60aed278586a7","src/db.rs":"9492dfe1660fad68770f7746682c164205fa0e8f4bbaadfda75d18f5d791b42d","src/error.rs":"fd26db688c01987f4f4cbd1e4f728b7f4981c24d4c13bfbc9d92a1322e99c740","src/fakespot.rs":"f501c9fe5296e7c130a9fcb532b861465717652cb5ef688230bc7a3b94df91b1","src/geoname.rs":"adbf29aab9031c2d09242dfcbf351c139b8bb792fa42ed72ef33082c920f69e8","src/lib.rs":"772dfc3625c0733dde442a5e10020d53e54fc86e39c37ce43e84dfb79a534936","src/metrics.rs":"871f0d834efbbc9e26d61f66fa31f0021dcf41444746cd7c082f93ba9628e399","src/provider.rs":"dbf7a0da37f7022c6606c06356ce152b44f799dabd6a6ee734dba426c1d96386","src/query.rs":"d4eeaf43251c8e6404ac75570ed069085c7e3f61be887ab8d127b08c106198c5","src/rs.rs":"60bc209657820050151c7025f14055f6bc56b889ac16fc827bd4697a3033f3f6","src/schema.rs":"490f7544b45361807fb7fe3d213e1827ef0b4f588c2b35fad46eb4478d6a8c5a","src/store.rs":"d1b561668a9ddb4ceabf83a69ca949879c7d8fe333d8b1bad4d1a42623629259","src/suggestion.rs":"d97050db6df628da09a8f481a4e650b17ce916f3cd88118c6571d2883d36b964","src/testing/client.rs":"47a32fd84c733001f11e8bfff94dc8c060b6b0780346dca5ddc7a5f5489c1d85","src/testing/data.rs":"4f79c37eb026b02d60fdbcfa871df4e25589d2cd8153753ae0a3f05333ce4423","src/testing/mod.rs":"7b4a507aceb159347843ba09461e0d58962027ba5dcd35aa4b993fe3666508ce","src/util.rs":"22091900a1f83babe126d86da10388f9d4d22b34dd1e67afdd868fedeb460c2b","src/weather.rs":"d7b747a32549d5200f19b2ba851cda84086b8a8f4ee7affe90e95920b22274f2","src/yelp.rs":"1fe3b7eb6b3f7462e9758b6eb62457dfa26f7549a8290cdff7637d2fb3ffea4f","uniffi.toml":"f391781c4a79a105fdb9783b84e463d7082ae855fd356b5c64e84db2cb2b3074"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/suggest/Cargo.toml b/third_party/rust/suggest/Cargo.toml
index 9e6d736fb565d..2581257cd48e8 100644
--- a/third_party/rust/suggest/Cargo.toml
+++ b/third_party/rust/suggest/Cargo.toml
@@ -47,7 +47,6 @@ thiserror = "2"
[dependencies.error-support]
path = "../support/error"
-features = ["tracing-logging"]
[dependencies.icu_normalizer]
version = "2"
diff --git a/third_party/rust/suggest/uniffi.toml b/third_party/rust/suggest/uniffi.toml
index 9e0b0dc3dd772..f767404d7c97b 100644
--- a/third_party/rust/suggest/uniffi.toml
+++ b/third_party/rust/suggest/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.suggest"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/sync15/.cargo-checksum.json b/third_party/rust/sync15/.cargo-checksum.json
index a164c3185cd3c..c9d98d03828ae 100644
--- a/third_party/rust/sync15/.cargo-checksum.json
+++ b/third_party/rust/sync15/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"5f51c183dcf682c429756e444e543ce18ff8a1acfc11260b4954e1d74f47dca4","README.md":"6d4ff5b079ac5340d18fa127f583e7ad793c5a2328b8ecd12c3fc723939804f2","build.rs":"aa971160d67ce8626b26e15c04c34b730f594c45c817aae34cfc9f3ea14ae284","src/bso/content.rs":"c34a689d5d910bc612c5ebfe34db00d5bf6710bfb657d6770e63e62c3cfedbe4","src/bso/crypto.rs":"27602dcccb37d3a55620ee4e16b705da455d49af575de115c7c79c0178eb1d6d","src/bso/mod.rs":"28e4fd3267aff256c3f03be27e6ee38b5f98c3dfeb0852f668c618e0f2573634","src/bso/test_utils.rs":"4ec5a2df5e1c0ec14dc770681e959bdcef6ef04f6fde435999197f46a8ae4831","src/client/coll_state.rs":"db1b5a3d2a274698218b18e9c7552bf2868b8a860815ac677b91ecc4f3ea8afc","src/client/coll_update.rs":"dac04a90c29dd969f8b4250414609c9b6d61daf2dfa4ae77d1c4a165ba970b05","src/client/collection_keys.rs":"c27b2277a3a52033b58ab01490fc2ea7007494195dd5e6dc2c6931a4ca96795a","src/client/mod.rs":"8f588d4a035cf79d96f2500f06d5651c1a7c566127c456ffa5429811ddce3fd6","src/client/request.rs":"2b5d2c77279d4d922b845020ffa0c6f34f84400fffff772cd4ab3a79f23e45a0","src/client/state.rs":"6f425a20f814a9325a42e3fa7c22068b33ee0f481c8cc1825f51bfd911b9c4f6","src/client/status.rs":"f445a8765dac9789444e23b5145148413407bb1d18a15ef56682243997f591bf","src/client/storage_client.rs":"ca74d26cf177a16697627d919add5b7e33f22f5e7357a5c5c546edb6687dd8f8","src/client/sync.rs":"ffcca4a2417ce4a6404363f3c1446def32f1297c14c27750ed69e6c03e23b9c1","src/client/sync_multiple.rs":"6d91cae760d7553f19443df9dc38cbdfb66b996a9a7b3d7a15f4179233c7dff9","src/client/token.rs":"ddfeb461f1be1b775a185c9b0cb372744032cf62ddb30666c672e73f761fab5d","src/client/util.rs":"71cc70ee41f821f53078675e636e9fad9c6046fa1a989e37f5487e340a2277d6","src/client_types.rs":"3c3cac1540b92482f43660d9e43bdde8481c4cc1a98253a68c80e791231f5976","src/clients_engine/engine.rs":"b6556f16eaa83e922686d838fc8ec53498da274445762250c10b69d268f20eda","src/clients_engine/mod.rs":"461729e6f89b66b2cbd89b041a03d4d6a8ba582284ed4f3015cb13e1a0c6da97","src/clients_engine/record.rs":"736870c81dff89a05c0703eef52beda83f45fe9f8d989dfb6e13b88781f1198f","src/clients_engine/ser.rs":"be6a19c45eb8002ff8e7cf746d2f97d9cecd1740f9817a8f1d624825475fd777","src/device_type.rs":"dc2d4296d25e31471c8e68488f1043ff239b902036cd6aea8a686cf79b4ed335","src/enc_payload.rs":"aa3eea7df49b24cd59831680a47c417b73a3e36e6b0f3f4baf14ca66bd68be6b","src/engine/bridged_engine.rs":"00220c64abc42e5c37e87efd6dbfe5c4c48507b9960e9ba722ac93e038ccfd30","src/engine/mod.rs":"d0d031d80fbdd90686c443b8c44720ab2ab0aff2c1106e0fdd7d60c46361fe8b","src/engine/request.rs":"5923025fb9550178339f880a1bf8526d8e853e7a0b2bce6d9d687cc808ac0085","src/engine/sync_engine.rs":"531b35d72ce9e04c3e543c0468c1e450fba2c0dc3d33d68d9b1c0a5c1ad7dd34","src/error.rs":"eb40d5e79ffafc50d3c2998c15007e1c3fc19b11a3e8d14be1e990a6e9c5e61e","src/key_bundle.rs":"a68b484ec8702a269645e892aa0b3161943388dac6b160e87393617139fad22a","src/lib.rs":"a396aeea2b37dc9dd92f41aa0dfe79ae82ca8ee65bc586aa10803f064a40b460","src/record_types.rs":"02bb3d352fb808131d298f9b90d9c95b7e9e0138b97c5401f3b9fdacc5562f44","src/server_timestamp.rs":"63916817796e83fe31fbd598bac025dfa71ec9e1808d09073db258c78a3331cd","src/sync15.udl":"464047a67a7877bc671f9f3aca13f3039cf34beb51756bcdb86015d789a8f400","src/telemetry.rs":"7261f0241587c8533239b6f19345d5849f6242bab62fe46e9dab5b57614a52db","uniffi.toml":"d9a5a5cb0eee5218f5eee4d8d89214cc1d7fb5b49323fd17becdf4adb706a6aa"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"5f51c183dcf682c429756e444e543ce18ff8a1acfc11260b4954e1d74f47dca4","README.md":"6d4ff5b079ac5340d18fa127f583e7ad793c5a2328b8ecd12c3fc723939804f2","build.rs":"aa971160d67ce8626b26e15c04c34b730f594c45c817aae34cfc9f3ea14ae284","src/bso/content.rs":"c34a689d5d910bc612c5ebfe34db00d5bf6710bfb657d6770e63e62c3cfedbe4","src/bso/crypto.rs":"27602dcccb37d3a55620ee4e16b705da455d49af575de115c7c79c0178eb1d6d","src/bso/mod.rs":"28e4fd3267aff256c3f03be27e6ee38b5f98c3dfeb0852f668c618e0f2573634","src/bso/test_utils.rs":"4ec5a2df5e1c0ec14dc770681e959bdcef6ef04f6fde435999197f46a8ae4831","src/client/coll_state.rs":"db1b5a3d2a274698218b18e9c7552bf2868b8a860815ac677b91ecc4f3ea8afc","src/client/coll_update.rs":"dac04a90c29dd969f8b4250414609c9b6d61daf2dfa4ae77d1c4a165ba970b05","src/client/collection_keys.rs":"c27b2277a3a52033b58ab01490fc2ea7007494195dd5e6dc2c6931a4ca96795a","src/client/mod.rs":"8f588d4a035cf79d96f2500f06d5651c1a7c566127c456ffa5429811ddce3fd6","src/client/request.rs":"2b5d2c77279d4d922b845020ffa0c6f34f84400fffff772cd4ab3a79f23e45a0","src/client/state.rs":"6f425a20f814a9325a42e3fa7c22068b33ee0f481c8cc1825f51bfd911b9c4f6","src/client/status.rs":"f445a8765dac9789444e23b5145148413407bb1d18a15ef56682243997f591bf","src/client/storage_client.rs":"ca74d26cf177a16697627d919add5b7e33f22f5e7357a5c5c546edb6687dd8f8","src/client/sync.rs":"ffcca4a2417ce4a6404363f3c1446def32f1297c14c27750ed69e6c03e23b9c1","src/client/sync_multiple.rs":"6d91cae760d7553f19443df9dc38cbdfb66b996a9a7b3d7a15f4179233c7dff9","src/client/token.rs":"ddfeb461f1be1b775a185c9b0cb372744032cf62ddb30666c672e73f761fab5d","src/client/util.rs":"71cc70ee41f821f53078675e636e9fad9c6046fa1a989e37f5487e340a2277d6","src/client_types.rs":"3c3cac1540b92482f43660d9e43bdde8481c4cc1a98253a68c80e791231f5976","src/clients_engine/engine.rs":"b6556f16eaa83e922686d838fc8ec53498da274445762250c10b69d268f20eda","src/clients_engine/mod.rs":"461729e6f89b66b2cbd89b041a03d4d6a8ba582284ed4f3015cb13e1a0c6da97","src/clients_engine/record.rs":"736870c81dff89a05c0703eef52beda83f45fe9f8d989dfb6e13b88781f1198f","src/clients_engine/ser.rs":"be6a19c45eb8002ff8e7cf746d2f97d9cecd1740f9817a8f1d624825475fd777","src/device_type.rs":"dc2d4296d25e31471c8e68488f1043ff239b902036cd6aea8a686cf79b4ed335","src/enc_payload.rs":"aa3eea7df49b24cd59831680a47c417b73a3e36e6b0f3f4baf14ca66bd68be6b","src/engine/bridged_engine.rs":"00220c64abc42e5c37e87efd6dbfe5c4c48507b9960e9ba722ac93e038ccfd30","src/engine/mod.rs":"d0d031d80fbdd90686c443b8c44720ab2ab0aff2c1106e0fdd7d60c46361fe8b","src/engine/request.rs":"5923025fb9550178339f880a1bf8526d8e853e7a0b2bce6d9d687cc808ac0085","src/engine/sync_engine.rs":"531b35d72ce9e04c3e543c0468c1e450fba2c0dc3d33d68d9b1c0a5c1ad7dd34","src/error.rs":"eb40d5e79ffafc50d3c2998c15007e1c3fc19b11a3e8d14be1e990a6e9c5e61e","src/key_bundle.rs":"a68b484ec8702a269645e892aa0b3161943388dac6b160e87393617139fad22a","src/lib.rs":"a396aeea2b37dc9dd92f41aa0dfe79ae82ca8ee65bc586aa10803f064a40b460","src/record_types.rs":"02bb3d352fb808131d298f9b90d9c95b7e9e0138b97c5401f3b9fdacc5562f44","src/server_timestamp.rs":"63916817796e83fe31fbd598bac025dfa71ec9e1808d09073db258c78a3331cd","src/sync15.udl":"464047a67a7877bc671f9f3aca13f3039cf34beb51756bcdb86015d789a8f400","src/telemetry.rs":"7261f0241587c8533239b6f19345d5849f6242bab62fe46e9dab5b57614a52db","uniffi.toml":"378f3cab5fa9740ad18ef2574dee238eb3f2caa50b759bfe0d8c7d180017274d"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/sync15/uniffi.toml b/third_party/rust/sync15/uniffi.toml
index de8b20d31fa6c..a0e8a6febbb82 100644
--- a/third_party/rust/sync15/uniffi.toml
+++ b/third_party/rust/sync15/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.sync15"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/tabs/.cargo-checksum.json b/third_party/rust/tabs/.cargo-checksum.json
index 9087a53e5ca4b..a4a3c937fcec4 100644
--- a/third_party/rust/tabs/.cargo-checksum.json
+++ b/third_party/rust/tabs/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"ddc5184038709a429c8f6950453ab3d08d55876b762799e244eb42eab4dfe518","README.md":"1b0262e7b9b9c25787699a249f772a45e490135413b37f46e6fa988253f54139","build.rs":"33e61b811b19ed2b58e319cc65d5988bed258d2c4fea2d706301184c59847a0f","src/error.rs":"6e5fd48a3f228d37977881a3657f8635b1b37e3b16d91ac2d8476174172a2a74","src/lib.rs":"887beb91cdc9fcc91233c25db9f49f6636bb4d54b5becdfef87c5695d4199c72","src/schema.rs":"698118cabf04cea702dfd2201457afd4238116245438fb50738b5e0a393e3f6c","src/storage.rs":"6634976de518c9afe0807deaa59e8fa51144ab685eda542ee84ed05666d1a591","src/store.rs":"871d985e46608d502fdbbd04a9bdd1940eab026e1764661e7e5d12cd97101841","src/sync/bridge.rs":"502d35c07541261879d8144a98760e2802ad848c38092665de9f3a80220099ef","src/sync/engine.rs":"e15ef9d81619ab4f7345dbef17287131d2b233e4b8a9899a14a98df944f1b0ae","src/sync/mod.rs":"02bd15d80065f19f471cb6e063cf09c27151a42f5788b924ec75f6f9376488a5","src/sync/record.rs":"e85a77aaa2ccdb8cc7cc7fe0e55f93b355dea5d1cd5f41f26102c49d429afbe7","src/tabs.udl":"92ed71e1501ab51cd3c46eb5c67dbeb441e90d7f7219c1b078d63b436c442c7d","uniffi.toml":"70a41bac1bbbde7a571f1b023f22636337ca3bffd6891dd67596fe13ab98b2f6"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"90ed1d2aa4d5f20e7b47371e088f8109a5809633486303793530965cc712d679","README.md":"1b0262e7b9b9c25787699a249f772a45e490135413b37f46e6fa988253f54139","build.rs":"33e61b811b19ed2b58e319cc65d5988bed258d2c4fea2d706301184c59847a0f","src/error.rs":"6e5fd48a3f228d37977881a3657f8635b1b37e3b16d91ac2d8476174172a2a74","src/lib.rs":"887beb91cdc9fcc91233c25db9f49f6636bb4d54b5becdfef87c5695d4199c72","src/schema.rs":"698118cabf04cea702dfd2201457afd4238116245438fb50738b5e0a393e3f6c","src/storage.rs":"6634976de518c9afe0807deaa59e8fa51144ab685eda542ee84ed05666d1a591","src/store.rs":"871d985e46608d502fdbbd04a9bdd1940eab026e1764661e7e5d12cd97101841","src/sync/bridge.rs":"502d35c07541261879d8144a98760e2802ad848c38092665de9f3a80220099ef","src/sync/engine.rs":"e15ef9d81619ab4f7345dbef17287131d2b233e4b8a9899a14a98df944f1b0ae","src/sync/mod.rs":"02bd15d80065f19f471cb6e063cf09c27151a42f5788b924ec75f6f9376488a5","src/sync/record.rs":"e85a77aaa2ccdb8cc7cc7fe0e55f93b355dea5d1cd5f41f26102c49d429afbe7","src/tabs.udl":"92ed71e1501ab51cd3c46eb5c67dbeb441e90d7f7219c1b078d63b436c442c7d","uniffi.toml":"90216612ab8e448deaab09a5fdcc2022059e85fae7a93f00b9718b6e5b2e509d"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/tabs/Cargo.toml b/third_party/rust/tabs/Cargo.toml
index 4dacc2397fd10..1915328c0bede 100644
--- a/third_party/rust/tabs/Cargo.toml
+++ b/third_party/rust/tabs/Cargo.toml
@@ -42,7 +42,6 @@ url = "2"
[dependencies.error-support]
path = "../support/error"
-features = ["tracing-logging"]
[dependencies.interrupt-support]
path = "../support/interrupt"
diff --git a/third_party/rust/tabs/uniffi.toml b/third_party/rust/tabs/uniffi.toml
index da413b2c4b127..c1d4edc366778 100644
--- a/third_party/rust/tabs/uniffi.toml
+++ b/third_party/rust/tabs/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.remotetabs"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/tracing-support/.cargo-checksum.json b/third_party/rust/tracing-support/.cargo-checksum.json
index 9320cc69726b6..5ddef65a34caa 100644
--- a/third_party/rust/tracing-support/.cargo-checksum.json
+++ b/third_party/rust/tracing-support/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"0596c8d314e2df5d7fac06383a2cee403a123267f3ee9adbecfd1133b2fb341b","android/build.gradle":"7eb3991c22ae47043c9754db53091493192a76ca4709f040a13aa8b5cd5bc64f","android/src/main/AndroidManifest.xml":"0edcec96c2345be661da3ead2714e059418ff5154123509923c344b66b4890c1","src/filters.rs":"8d15290905b251bd713f9fdb0513b23898e2f8015d65920ffc90e02bfa784ce0","src/layer.rs":"fea35aa34452c5a9c70b4f3e65a981f57c67745ea509936909260b8417354316","src/lib.rs":"f02180d894933c96720d374b9da6414996d81e0d8092bc589e003ba793ac7b6e","uniffi.toml":"357c46607bdec389b533ea0a908d98586b3fa2b6de2fa6263ca6903d13156f30"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"0596c8d314e2df5d7fac06383a2cee403a123267f3ee9adbecfd1133b2fb341b","android/build.gradle":"7eb3991c22ae47043c9754db53091493192a76ca4709f040a13aa8b5cd5bc64f","android/src/main/AndroidManifest.xml":"0edcec96c2345be661da3ead2714e059418ff5154123509923c344b66b4890c1","src/filters.rs":"8d15290905b251bd713f9fdb0513b23898e2f8015d65920ffc90e02bfa784ce0","src/layer.rs":"fea35aa34452c5a9c70b4f3e65a981f57c67745ea509936909260b8417354316","src/lib.rs":"f02180d894933c96720d374b9da6414996d81e0d8092bc589e003ba793ac7b6e","uniffi.toml":"da6f74fbc1181880740966609c28a5009b4e2a601aaffb07d5e574d35ea3ce33"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/tracing-support/uniffi.toml b/third_party/rust/tracing-support/uniffi.toml
index 66525f278de0a..7de6d0a4310f7 100644
--- a/third_party/rust/tracing-support/uniffi.toml
+++ b/third_party/rust/tracing-support/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.tracing"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/viaduct/.cargo-checksum.json b/third_party/rust/viaduct/.cargo-checksum.json
index 082975cf2eda1..a6538b1517153 100644
--- a/third_party/rust/viaduct/.cargo-checksum.json
+++ b/third_party/rust/viaduct/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"dd526fa9f4c1ad3ed26edfb0a5650d898f411c8374c6b7e0030039679dd50485","README.md":"d73ab3e981a55c60a3c0995daa46fee6e513e8d78fb3c34691dd47320547695e","src/backend.rs":"501657c02aff05a987eb9d1a94aef8725a4db0af8a4d7c4959a49b3dc048fa68","src/backend/ffi.rs":"1c07fbfd807ee63ce32d5ac1000b8e0eef0a660ac4aa48c6ff15abf59fdadb75","src/client.rs":"9919c9178e2382967b708980a7f12dbf14cada876da53eb515eff676f216c3c2","src/error.rs":"2bf09dee6a0fbce7f0a7ffb4d5a01c59d458634336e8767399ed42ce7e9b1287","src/fetch_msg_types.proto":"de8a46a4947a140783a4d714364f18ccf02c4759d6ab5ace9da0b1c058efa6c3","src/headers.rs":"63bfa3ba4953f1900a56e3cc6e15b0eb8bd1268cdb0020a1b86391f1043dab82","src/headers/name.rs":"d7304e006278a5466b5fe09e194a9ca921b565d76e24be491ec6178f527d2d61","src/lib.rs":"9eb7c5a81feca0cf7fa22af3d1f32e8043bf7259765c4018e8cd53ee65d91933","src/mozilla.appservices.httpconfig.protobuf.rs":"9ede762489a0c07bc08a5b852b33013a410cb41b44b92a44555f85bb2db91412","src/new_backend.rs":"583be624622b3fd9b9261b8d2bc80d6204b0de08520b34eadaede3bc3deb5bf3","src/ohttp.rs":"9ae48d6574e649efab08ecc43cb024132d77eda7d7a3a512becd2ec4d3d502d2","src/ohttp_client.rs":"a530c26ff6b008428bbb68ab1bb68cb1c4e68c515a43b58d6206e8c7393f2976","src/settings.rs":"06ec24040286911581862740759c7086d5b111b91bd91f2809123e922d5cf8a0","uniffi.toml":"d73e88bf94a6b2c4a94bd0bada509542dbbadb3be1bf4ad32f52e465af6503d7"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"dd526fa9f4c1ad3ed26edfb0a5650d898f411c8374c6b7e0030039679dd50485","README.md":"d73ab3e981a55c60a3c0995daa46fee6e513e8d78fb3c34691dd47320547695e","src/backend.rs":"501657c02aff05a987eb9d1a94aef8725a4db0af8a4d7c4959a49b3dc048fa68","src/backend/ffi.rs":"1c07fbfd807ee63ce32d5ac1000b8e0eef0a660ac4aa48c6ff15abf59fdadb75","src/client.rs":"9919c9178e2382967b708980a7f12dbf14cada876da53eb515eff676f216c3c2","src/error.rs":"2bf09dee6a0fbce7f0a7ffb4d5a01c59d458634336e8767399ed42ce7e9b1287","src/fetch_msg_types.proto":"de8a46a4947a140783a4d714364f18ccf02c4759d6ab5ace9da0b1c058efa6c3","src/headers.rs":"a7d90d445eba7e52a797f164270317e5ce73676996a19aa59a5f92d7d2fb8e14","src/headers/name.rs":"d7304e006278a5466b5fe09e194a9ca921b565d76e24be491ec6178f527d2d61","src/lib.rs":"bc355e65f86cd1e0193f574bb959fcde309d936c18ff37ed1b88f72c8bcef362","src/mozilla.appservices.httpconfig.protobuf.rs":"9ede762489a0c07bc08a5b852b33013a410cb41b44b92a44555f85bb2db91412","src/new_backend.rs":"583be624622b3fd9b9261b8d2bc80d6204b0de08520b34eadaede3bc3deb5bf3","src/ohttp.rs":"78e131adad534f39cc6d2af7598c5735b581e6fe229f4567cf670a522ede5d3b","src/ohttp_client.rs":"a530c26ff6b008428bbb68ab1bb68cb1c4e68c515a43b58d6206e8c7393f2976","src/settings.rs":"06ec24040286911581862740759c7086d5b111b91bd91f2809123e922d5cf8a0","uniffi.toml":"149b5035825de0401325e674f5dc1d1a6e006958ef088541495e898be9568edd"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/viaduct/src/headers.rs b/third_party/rust/viaduct/src/headers.rs
index 3fc2d37efdbf8..510f28cac54a3 100644
--- a/third_party/rust/viaduct/src/headers.rs
+++ b/third_party/rust/viaduct/src/headers.rs
@@ -103,7 +103,7 @@ impl std::fmt::Display for Header {
}
/// A list of headers.
-#[derive(Clone, Debug, PartialEq, Eq, Default)]
+#[derive(Clone, PartialEq, Eq, Default)]
pub struct Headers {
headers: Vec,
}
@@ -336,6 +336,14 @@ impl Headers {
}
}
+impl std::fmt::Debug for Headers {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_map()
+ .entries(self.headers.iter().map(|h| (h.name().as_str(), &h.value)))
+ .finish()
+ }
+}
+
impl std::iter::IntoIterator for Headers {
type IntoIter = as IntoIterator>::IntoIter;
type Item = Header;
diff --git a/third_party/rust/viaduct/src/lib.rs b/third_party/rust/viaduct/src/lib.rs
index 39dfe21773de1..2a3ad310d7ca8 100644
--- a/third_party/rust/viaduct/src/lib.rs
+++ b/third_party/rust/viaduct/src/lib.rs
@@ -75,7 +75,7 @@ impl std::fmt::Display for Method {
}
#[must_use = "`Request`'s \"builder\" functions take by move, not by `&mut self`"]
-#[derive(Clone, Debug, uniffi::Record)]
+#[derive(Clone, uniffi::Record)]
pub struct Request {
pub method: Method,
pub url: Url,
@@ -237,8 +237,23 @@ impl Request {
}
}
+// Hand-written `Debug` impl for nicer logging
+impl std::fmt::Debug for Request {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Request")
+ .field("method", &self.method)
+ .field("url", &self.url.to_string())
+ .field("headers", &self.headers)
+ .field(
+ "body",
+ &self.body.as_ref().map(|body| String::from_utf8_lossy(body)),
+ )
+ .finish()
+ }
+}
+
/// A response from the server.
-#[derive(Clone, Debug, uniffi::Record)]
+#[derive(Clone, uniffi::Record)]
pub struct Response {
/// The method used to request this response.
pub request_method: Method,
@@ -303,6 +318,19 @@ impl Response {
}
}
+// Hand-written `Debug` impl for nicer logging
+impl std::fmt::Debug for Response {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Response")
+ .field("request_method", &self.request_method)
+ .field("url", &self.url.to_string())
+ .field("status", &self.status)
+ .field("headers", &self.headers)
+ .field("body", &String::from_utf8_lossy(&self.body))
+ .finish()
+ }
+}
+
/// A module containing constants for all HTTP status codes.
pub mod status_codes {
@@ -406,3 +434,31 @@ uniffi::custom_type!(Headers, std::collections::HashMap, {
});
uniffi::setup_scaffolding!("viaduct");
+
+/// Send a request through an OHTTP channel.
+///
+/// This encrypts the request and routes it through the configured OHTTP
+/// relay/gateway for the specified channel.
+///
+/// # Arguments
+/// * `request` - The request to send
+/// * `channel` - The name of the OHTTP channel to use (e.g., "merino")
+///
+/// # Example (Kotlin)
+/// ```kotlin
+/// val response = sendOhttpRequest(
+/// Request(
+/// method = Method.GET,
+/// url = "https://example.com/api",
+/// headers = mapOf("Accept" to "application/json"),
+/// body = null
+/// ),
+/// "merino"
+/// )
+/// ```
+#[cfg(feature = "ohttp")]
+#[uniffi::export]
+pub async fn send_ohttp_request(request: Request, channel: String) -> Result {
+ let settings = crate::ClientSettings::default();
+ crate::ohttp::process_ohttp_request(request, &channel, settings).await
+}
diff --git a/third_party/rust/viaduct/src/ohttp.rs b/third_party/rust/viaduct/src/ohttp.rs
index f4928015fd276..f6b9dc43b03d2 100644
--- a/third_party/rust/viaduct/src/ohttp.rs
+++ b/third_party/rust/viaduct/src/ohttp.rs
@@ -11,6 +11,27 @@ use url::Url;
use crate::{Headers, Method, Request, Response, Result, ViaductError};
+/// Send a request using either the new backend or the old backend.
+///
+/// This function provides compatibility with both backend systems:
+/// - If the new backend is initialized, it uses it with the provided settings
+/// - Otherwise, it falls back to the old backend (which uses global settings)
+///
+/// Note: When using the old backend, the `settings` parameter is ignored and
+/// global settings from `GLOBAL_SETTINGS` are used instead.
+async fn send_request(request: Request, settings: crate::ClientSettings) -> Result {
+ // Try to use the new backend first
+ if let Ok(backend) = crate::new_backend::get_backend() {
+ return backend.send_request(request, settings).await;
+ }
+
+ // Fall back to the old backend (synchronous, uses global settings)
+ crate::trace!(
+ "OHTTP: Using old backend (global settings will be used instead of per-request settings)"
+ );
+ crate::backend::send(request)
+}
+
/// Configuration for an OHTTP channel
#[derive(Debug, Clone, uniffi::Record)]
pub struct OhttpConfig {
@@ -198,14 +219,13 @@ async fn fetch_config_from_network(gateway_host: &str) -> Result> {
let config_url = Url::parse(&gateway_url)?.join("ohttp-configs")?;
let request = Request::get(config_url.clone());
- let backend = crate::new_backend::get_backend()?;
let settings = crate::ClientSettings {
timeout: 10000,
redirect_limit: 5,
..crate::ClientSettings::default()
};
- let response = backend.send_request(request, settings).await?;
+ let response = send_request(request, settings).await?;
if !response.is_success() {
return Err(ViaductError::OhttpConfigFetchFailed(format!(
@@ -338,8 +358,7 @@ pub async fn process_ohttp_request(
// Send the encrypted request to the relay using the backend
crate::trace!("Sending to relay with timeout: {}ms", settings.timeout);
let relay_start = std::time::Instant::now();
- let backend = crate::new_backend::get_backend()?;
- let relay_response = backend.send_request(relay_request, settings).await?;
+ let relay_response = send_request(relay_request, settings).await?;
let relay_duration = relay_start.elapsed();
crate::trace!(
diff --git a/third_party/rust/viaduct/uniffi.toml b/third_party/rust/viaduct/uniffi.toml
index b6182cf3c7a3d..6ccc4b63b1871 100644
--- a/third_party/rust/viaduct/uniffi.toml
+++ b/third_party/rust/viaduct/uniffi.toml
@@ -1,5 +1,6 @@
[bindings.kotlin]
package_name = "mozilla.appservices.viaduct"
+omit_checksums = true
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
diff --git a/third_party/rust/webext-storage/.cargo-checksum.json b/third_party/rust/webext-storage/.cargo-checksum.json
index 39e4e6d483eb0..6b982c8681cac 100644
--- a/third_party/rust/webext-storage/.cargo-checksum.json
+++ b/third_party/rust/webext-storage/.cargo-checksum.json
@@ -1 +1 @@
-{"files":{"Cargo.toml":"65686be35fbd69e80a6948b6e71918005b32520b4fd109776f997cc8c8f40667","README.md":"821cac7eb5b963fc3f3fe21dd890427ab2bbf335cb25cbae89b713b3350687c5","build.rs":"f4ff15cd54890d3e3636e77a0458ba9a8882f271ccb0056a0bbae1975cdd75d5","sql/create_schema.sql":"a17311a407ec10e033886b7125da4c8b84bc6d761f6b28edc9594de430e1d964","sql/create_sync_temp_tables.sql":"860ede362c94feb47d85522553fa2852f9bdb9f9b025d6438dd5dee3d4acd527","sql/tests/create_schema_v1.sql":"77cf0c90eaac3e1aea626537147e1b8ec349b68d6076c92fa7ae402aac613050","src/api.rs":"c4ef4f934b5fdf1e26be0752d34490f69f51d0d61ad83621986e4b4d98fcbc05","src/db.rs":"b24a68226889cf84f0020142dddfdecf84949f47e4582c2e0c7b62d4e2883a9d","src/error.rs":"4a37ff2221551ebf53a835085b3e21f9de3f86a74711ddd5903975c7110333a1","src/ffi.rs":"f66a81393bebe7a4b7e7960cb426df106ff1f02bfebcaa6e335b4b8b56c5c936","src/lib.rs":"78d0388e4a21ab39ecd8e6f653e97d181ca0fa9c57b92050c7946b543d828a05","src/migration.rs":"7f09887df071e15f34be5bd24116f43b786410e935e1f1d7e49471877629dd19","src/schema.rs":"0a668212a6db65cf5c59a786eaa342707583d580c1d374c81374335301f758bc","src/store.rs":"b1fdf330760e1271c5abcc77e7c842d941fe95a1b33095d72f677e2abf79bfb6","src/sync/bridge.rs":"14d095bc67e511297b833e279912f61dd67993a877be941cc058afe9017cb058","src/sync/incoming.rs":"8b7cd75463aae7bc08e87d5feb2b125ae68b512bcb712483457d2837c4b0086a","src/sync/mod.rs":"5e6ae17f5a0fe0e278ed1fe552cea4fb7993280811333c36ac40599cde4059d6","src/sync/outgoing.rs":"93328d141d9f1559d60c2f321bf37ed85107e0a828353e73cafe80a24f5f23b8","src/sync/sync_tests.rs":"c5490abbaed5cffc2afa397bdd8386762c30b28b7d95a30ce0825678a72e56b3","src/webext-storage.udl":"18e7d7d9e6fa7ec5655a6cf410374a3af8f8378d5e884019083b2e39747938b1","uniffi.toml":"beeec89c2f877eb89be0090dc304dbc7c74e787385e7459bad78c6165bb66791"},"package":null}
\ No newline at end of file
+{"files":{"Cargo.toml":"65686be35fbd69e80a6948b6e71918005b32520b4fd109776f997cc8c8f40667","README.md":"821cac7eb5b963fc3f3fe21dd890427ab2bbf335cb25cbae89b713b3350687c5","build.rs":"f4ff15cd54890d3e3636e77a0458ba9a8882f271ccb0056a0bbae1975cdd75d5","sql/create_schema.sql":"a17311a407ec10e033886b7125da4c8b84bc6d761f6b28edc9594de430e1d964","sql/create_sync_temp_tables.sql":"860ede362c94feb47d85522553fa2852f9bdb9f9b025d6438dd5dee3d4acd527","sql/tests/create_schema_v1.sql":"77cf0c90eaac3e1aea626537147e1b8ec349b68d6076c92fa7ae402aac613050","src/api.rs":"c4ef4f934b5fdf1e26be0752d34490f69f51d0d61ad83621986e4b4d98fcbc05","src/db.rs":"b24a68226889cf84f0020142dddfdecf84949f47e4582c2e0c7b62d4e2883a9d","src/error.rs":"4a37ff2221551ebf53a835085b3e21f9de3f86a74711ddd5903975c7110333a1","src/ffi.rs":"f66a81393bebe7a4b7e7960cb426df106ff1f02bfebcaa6e335b4b8b56c5c936","src/lib.rs":"78d0388e4a21ab39ecd8e6f653e97d181ca0fa9c57b92050c7946b543d828a05","src/migration.rs":"7f09887df071e15f34be5bd24116f43b786410e935e1f1d7e49471877629dd19","src/schema.rs":"0a668212a6db65cf5c59a786eaa342707583d580c1d374c81374335301f758bc","src/store.rs":"b1fdf330760e1271c5abcc77e7c842d941fe95a1b33095d72f677e2abf79bfb6","src/sync/bridge.rs":"14d095bc67e511297b833e279912f61dd67993a877be941cc058afe9017cb058","src/sync/incoming.rs":"8b7cd75463aae7bc08e87d5feb2b125ae68b512bcb712483457d2837c4b0086a","src/sync/mod.rs":"5e6ae17f5a0fe0e278ed1fe552cea4fb7993280811333c36ac40599cde4059d6","src/sync/outgoing.rs":"93328d141d9f1559d60c2f321bf37ed85107e0a828353e73cafe80a24f5f23b8","src/sync/sync_tests.rs":"c5490abbaed5cffc2afa397bdd8386762c30b28b7d95a30ce0825678a72e56b3","src/webext-storage.udl":"18e7d7d9e6fa7ec5655a6cf410374a3af8f8378d5e884019083b2e39747938b1","uniffi.toml":"38657e79bf1dd33a6b25c3b2d0328f89cdae6922ca48c876caffefc842cd347b"},"package":null}
\ No newline at end of file
diff --git a/third_party/rust/webext-storage/uniffi.toml b/third_party/rust/webext-storage/uniffi.toml
index e65e8d615e4e8..e8b0a5c69a915 100644
--- a/third_party/rust/webext-storage/uniffi.toml
+++ b/third_party/rust/webext-storage/uniffi.toml
@@ -1,3 +1,4 @@
[bindings.kotlin]
package_name = "mozilla.appservices.webextstorage"
cdylib_name = "megazord"
+omit_checksums = true
diff --git a/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs b/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs
index 21ede74c7df16..ee6645bad3f1b 100644
--- a/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs
+++ b/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs
@@ -1040,6 +1040,8 @@ export class UserCharacteristicsPageService {
"mathml9",
"mathml10",
"monochrome",
+ "cssSystemColors",
+ "cssSystemFonts",
"oscpu",
"pdfViewer",
"platform",
diff --git a/toolkit/components/resistfingerprinting/content/usercharacteristics.js b/toolkit/components/resistfingerprinting/content/usercharacteristics.js
index 60002c4a5b3bd..68996d748b011 100644
--- a/toolkit/components/resistfingerprinting/content/usercharacteristics.js
+++ b/toolkit/components/resistfingerprinting/content/usercharacteristics.js
@@ -774,6 +774,120 @@ async function populateCSSQueries() {
};
}
+async function populateCSSSystemColors() {
+ const systemColors = [
+ "Canvas",
+ "CanvasText",
+ "LinkText",
+ "VisitedText",
+ "ActiveText",
+ "ButtonFace",
+ "ButtonText",
+ "ButtonBorder",
+ "Field",
+ "FieldText",
+ "Highlight",
+ "HighlightText",
+ "SelectedItem",
+ "SelectedItemText",
+ "AccentColor",
+ "AccentColorText",
+ "Mark",
+ "MarkText",
+ "GrayText",
+ "ActiveBorder",
+ "ActiveCaption",
+ "AppWorkspace",
+ "Background",
+ "ButtonShadow",
+ "InactiveBorder",
+ "InactiveCaption",
+ "InactiveCaptionText",
+ "InfoBackground",
+ "InfoText",
+ "Menu",
+ "MenuText",
+ "Scrollbar",
+ "ThreeDDarkShadow",
+ "ThreeDFace",
+ "ThreeDHighlight",
+ "ThreeDLightShadow",
+ "ThreeDShadow",
+ "Window",
+ "WindowFrame",
+ "WindowText",
+ ];
+
+ const rgbToHex = rgb => {
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
+ if (!match) {
+ return rgb;
+ }
+ const [, r, g, b] = match;
+ return [r, g, b]
+ .map(x => parseInt(x, 10).toString(16).padStart(2, "0"))
+ .join("")
+ .toUpperCase();
+ };
+
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+
+ const results = [];
+ for (const colorName of systemColors) {
+ div.style.backgroundColor = colorName;
+ const computed = getComputedStyle(div).backgroundColor;
+ results.push({ [colorName]: rgbToHex(computed) });
+ }
+
+ document.body.removeChild(div);
+
+ return {
+ cssSystemColors: JSON.stringify(results),
+ };
+}
+
+async function populateCSSSystemFonts() {
+ const systemFonts = [
+ "caption",
+ "icon",
+ "menu",
+ "message-box",
+ "small-caption",
+ "status-bar",
+ "serif",
+ "sans-serif",
+ "monospace",
+ "cursive",
+ "fantasy",
+ "system-ui",
+ "Arial",
+ "Helvetica",
+ "Times New Roman",
+ "Courier New",
+ "Verdana",
+ "Georgia",
+ ];
+
+ const div = document.createElement("div");
+ div.textContent = "Test";
+ document.body.appendChild(div);
+
+ const results = [];
+ for (const fontName of systemFonts) {
+ div.style.fontFamily = fontName;
+ const computed = getComputedStyle(div);
+ const value = computed.fontSize + " " + computed.fontFamily;
+ results.push({ [fontName]: value });
+ }
+
+ document.body.removeChild(div);
+
+ return {
+ cssSystemFonts: JSON.stringify(results),
+ };
+}
+
async function populateNavigatorProperties() {
return {
oscpu: navigator.oscpu,
@@ -866,7 +980,16 @@ async function populateICEFoundations() {
})
.catch(reject);
- return promise;
+ // Add timeout to prevent hanging indefinitely
+ const timeout = setTimeout(() => {
+ pc.close();
+ resolve(result);
+ }, 5000);
+
+ return promise.then(res => {
+ clearTimeout(timeout);
+ return res;
+ });
}
// Run get candidates multiple times to see if foundation order changes
@@ -985,18 +1108,35 @@ async function populateMathML() {
async function populateAudioDeviceProperties() {
const ctx = new AudioContext();
- await ctx.resume();
+
+ try {
+ // Add a timeout to prevent hanging indefinitely if the user has no audio hardware
+ await Promise.race([
+ ctx.resume(),
+ new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error("AudioContext.resume() timeout")),
+ 5000
+ )
+ ),
+ ]);
+ } catch (e) {
+ throw new Error(
+ "AudioContext.resume error, probably a timeout, user may not have audio hardware"
+ );
+ }
// Give firefox some time to calculate latency
await new Promise(resolve => setTimeout(resolve, 2000));
// All the other properties (min/max decibels, smoothingTimeConstant,
// fftSize, frequencyBinCount, baseLatency) are hardcoded.
- return {
+ const result = {
audioFrames: ctx.outputLatency * ctx.sampleRate,
audioRate: ctx.sampleRate,
audioChannels: ctx.destination.maxChannelCount,
};
+ return result;
}
async function populateTimezoneWeb() {
@@ -1080,6 +1220,8 @@ async function startPopulating() {
populateSensorInfo,
populateMathML,
populateCSSQueries,
+ populateCSSSystemColors,
+ populateCSSSystemFonts,
populateNavigatorProperties,
populateAudioDeviceProperties,
populateTimezoneWeb,
diff --git a/toolkit/components/resistfingerprinting/metrics.yaml b/toolkit/components/resistfingerprinting/metrics.yaml
index e4cdd5ba4b571..d9e048020e6d4 100644
--- a/toolkit/components/resistfingerprinting/metrics.yaml
+++ b/toolkit/components/resistfingerprinting/metrics.yaml
@@ -4559,6 +4559,55 @@ characteristics:
data_sensitivity:
- technical
+ css_system_colors:
+ type: text
+ description: >
+ JSON array containing CSS system color keywords and their computed values as
+ uppercase hex (without # prefix). System colors (like Canvas, ButtonFace,
+ LinkText, etc.) are CSS keywords that resolve to theme/OS-specific colors and
+ can be used for fingerprinting. This metric collects the computed backgroundColor
+ for each system color keyword. Data format: [{"Canvas": "FFFFFF"}, {"ButtonFace": "F0F0F0"}, ...].
+ lifetime: application
+ send_in_pings:
+ - user-characteristics
+ notification_emails:
+ - tom@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1879151
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010671
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010671
+ expires: never
+ data_sensitivity:
+ # Text metrics are _required_ to be web_activity or highly_sensitive, so even though this
+ # is more like 'technical' (per the Data Review), I'm marking highly sensitive.
+ - highly_sensitive
+
+ css_system_fonts:
+ type: text
+ description: >
+ JSON array containing CSS system font keywords and their computed font properties.
+ System fonts (like caption, icon, menu, etc.) and common font families are tested
+ to determine their rendered fontSize and fontFamily values, which can vary by OS
+ and be used for fingerprinting. This metric collects computed fontSize + fontFamily
+ for each font keyword/name tested.
+ Data format: [{"caption": "11px system-ui"}, {"Arial": "16px Arial"}, ...].
+ lifetime: application
+ send_in_pings:
+ - user-characteristics
+ notification_emails:
+ - tom@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1879151
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010671
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=2010671
+ expires: never
+ data_sensitivity:
+ # Text metrics are _required_ to be web_activity or highly_sensitive, so even though this
+ # is more like 'technical' (per the Data Review), I'm marking highly sensitive.
+ - highly_sensitive
+
firefox_binary_arch:
type: string
description: >
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser.toml b/toolkit/components/resistfingerprinting/tests/browser/browser.toml
index c7f2f71f37036..db1736ee859b5 100644
--- a/toolkit/components/resistfingerprinting/tests/browser/browser.toml
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser.toml
@@ -85,6 +85,8 @@ skip-if = [
"os == 'win' && arch == 'x86'", # OOM on win32 due to memory constraints
]
+["browser_usercharacteristics_css.js"]
+
["browser_usercharacteristics_gamepads.js"]
["browser_usercharacteristics_linux_distro.js"]
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_usercharacteristics_css.js b/toolkit/components/resistfingerprinting/tests/browser/browser_usercharacteristics_css.js
new file mode 100644
index 0000000000000..edf6b54412dff
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_usercharacteristics_css.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+add_task(async function test_css_system_colors_and_fonts() {
+ info("Testing CSS system colors and fonts collection...");
+
+ await BrowserTestUtils.withNewTab({ gBrowser, url: emptyPage }, () =>
+ GleanPings.userCharacteristics.testSubmission(
+ () => {
+ const colorsValue =
+ Glean.characteristics.cssSystemColors.testGetValue();
+ Assert.notEqual(
+ colorsValue,
+ null,
+ "CSS system colors should be collected"
+ );
+ Assert.notEqual(
+ colorsValue,
+ "",
+ "CSS system colors should not be empty"
+ );
+
+ const colorsParsed = JSON.parse(colorsValue);
+ Assert.ok(
+ Array.isArray(colorsParsed),
+ "CSS system colors should be an array"
+ );
+ Assert.greater(
+ colorsParsed.length,
+ 0,
+ "CSS system colors should contain entries"
+ );
+
+ const firstColorEntry = colorsParsed[0];
+ Assert.strictEqual(
+ typeof firstColorEntry,
+ "object",
+ "Each color entry should be an object"
+ );
+
+ const colorKeys = Object.keys(firstColorEntry);
+ Assert.equal(
+ colorKeys.length,
+ 1,
+ "Each color entry should have exactly one key"
+ );
+
+ const colorValue = firstColorEntry[colorKeys[0]];
+ Assert.ok(
+ /^[0-9A-F]{6}$/.test(colorValue),
+ "Color value should be in uppercase HEX format (e.g., FFFFFF)"
+ );
+
+ info(`Collected ${colorsParsed.length} system colors`);
+
+ const fontsValue = Glean.characteristics.cssSystemFonts.testGetValue();
+ Assert.notEqual(
+ fontsValue,
+ null,
+ "CSS system fonts should be collected"
+ );
+ Assert.notEqual(fontsValue, "", "CSS system fonts should not be empty");
+
+ const fontsParsed = JSON.parse(fontsValue);
+ Assert.ok(
+ Array.isArray(fontsParsed),
+ "CSS system fonts should be an array"
+ );
+ Assert.greater(
+ fontsParsed.length,
+ 0,
+ "CSS system fonts should contain entries"
+ );
+
+ const firstFontEntry = fontsParsed[0];
+ Assert.strictEqual(
+ typeof firstFontEntry,
+ "object",
+ "Each font entry should be an object"
+ );
+
+ const fontKeys = Object.keys(firstFontEntry);
+ Assert.equal(
+ fontKeys.length,
+ 1,
+ "Each font entry should have exactly one key"
+ );
+
+ const fontValue = firstFontEntry[fontKeys[0]];
+ Assert.ok(
+ fontValue.includes("px"),
+ "Font value should include pixel size"
+ );
+
+ info(`Collected ${fontsParsed.length} system fonts`);
+ },
+ async () => {
+ const populated = TestUtils.topicObserved(
+ "user-characteristics-populating-data-done",
+ () => true
+ );
+ Services.obs.notifyObservers(
+ null,
+ "user-characteristics-testing-please-populate-data"
+ );
+ await populated;
+ GleanPings.userCharacteristics.submit();
+ }
+ )
+ );
+});
diff --git a/toolkit/components/uniffi-bindgen-gecko-js/components/Cargo.toml b/toolkit/components/uniffi-bindgen-gecko-js/components/Cargo.toml
index a6e84b9677fea..d9f8c0bcae1c6 100644
--- a/toolkit/components/uniffi-bindgen-gecko-js/components/Cargo.toml
+++ b/toolkit/components/uniffi-bindgen-gecko-js/components/Cargo.toml
@@ -24,7 +24,7 @@ search = "0.1"
suggest = "0.1"
relevancy = "0.1"
webext-storage = "0.1"
-error-support = { version = "0.1", features = ["tracing-logging", "tracing-reporting"] }
+error-support = { version = "0.1" }
tracing-support = "0.1"
logins = { version = "0.1", features = ["keydb"] }
init_rust_components = { version = "0.1", features = ["keydb"] }
diff --git a/xpcom/base/CycleCollectedJSContext.cpp b/xpcom/base/CycleCollectedJSContext.cpp
index da731e24c00ee..e055a7899a5e0 100644
--- a/xpcom/base/CycleCollectedJSContext.cpp
+++ b/xpcom/base/CycleCollectedJSContext.cpp
@@ -269,15 +269,6 @@ bool CycleCollectedJSContext::getHostDefinedData(
return true;
}
-bool CycleCollectedJSContext::enqueuePromiseJob(
- JSContext* aCx, JS::Handle aPromise, JS::Handle aJob,
- JS::Handle aAllocationSite,
- JS::Handle hostDefinedData) {
- MOZ_CRASH(
- "This method should never be called: Gecko no longer supports the"
- "pref javascript.options.use_js_microtask_queue being false.");
-}
-
// Used only by the SpiderMonkey Debugger API, and even then only via
// JS::AutoDebuggerJobQueueInterruption, to ensure that the debuggee's queue is
// not affected; see comments in js/public/Promise.h.
@@ -287,12 +278,6 @@ void CycleCollectedJSContext::runJobs(JSContext* aCx) {
PerformMicroTaskCheckPoint();
}
-bool CycleCollectedJSContext::empty() const {
- MOZ_CRASH(
- "This method should never be called: Gecko no longer supports the"
- "pref javascript.options.use_js_microtask_queue being false.");
-}
-
MicroTaskRunnable* MustConsumeMicroTask::MaybeUnwrapTaskToRunnable() const {
if (!IsJSMicroTask()) {
void* nonJSTask = mMicroTask.toPrivate();
@@ -664,13 +649,11 @@ JS::GenericMicroTask RunnableToMicroTask(
bool EnqueueMicroTask(JSContext* aCx,
already_AddRefed aRunnable) {
- MOZ_ASSERT(StaticPrefs::javascript_options_use_js_microtask_queue());
JS::GenericMicroTask v = RunnableToMicroTask(aRunnable);
return JS::EnqueueMicroTask(aCx, v);
}
bool EnqueueDebugMicroTask(JSContext* aCx,
already_AddRefed aRunnable) {
- MOZ_ASSERT(StaticPrefs::javascript_options_use_js_microtask_queue());
JS::GenericMicroTask v = RunnableToMicroTask(aRunnable);
return JS::EnqueueDebugMicroTask(aCx, v);
}
@@ -718,7 +701,6 @@ bool SuppressedMicroTaskList::Suppressed() {
return true;
}
- MOZ_ASSERT(StaticPrefs::javascript_options_use_js_microtask_queue());
MOZ_ASSERT(mContext->mSuppressedMicroTaskList == this);
MOZ_LOG_FMT(gLog, LogLevel::Verbose, "Prepending %zu suppressed microtasks",
diff --git a/xpcom/base/CycleCollectedJSContext.h b/xpcom/base/CycleCollectedJSContext.h
index 033683c55a0cd..ffdffb2edfee4 100644
--- a/xpcom/base/CycleCollectedJSContext.h
+++ b/xpcom/base/CycleCollectedJSContext.h
@@ -497,16 +497,12 @@ class CycleCollectedJSContext : dom::PerThreadAtomCache, public JS::JobQueue {
bool getHostDefinedGlobal(JSContext* cx,
JS::MutableHandle) const override;
- bool enqueuePromiseJob(JSContext* cx, JS::Handle promise,
- JS::Handle job,
- JS::Handle allocationSite,
- JS::Handle hostDefinedData) override;
// MOZ_CAN_RUN_SCRIPT_BOUNDARY for now so we don't have to change SpiderMonkey
// headers. The caller presumably knows this can run script (like everything
// in SpiderMonkey!) and will deal.
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void runJobs(JSContext* cx) override;
- bool empty() const override;
+
bool isDrainingStopped() const override { return false; }
// Trace hook for non-GCThing microtask values (e.g., Private values
diff --git a/xpcom/rust/gecko_tracing/Cargo.toml b/xpcom/rust/gecko_tracing/Cargo.toml
index 59b7d3022f94e..73213f9e626bc 100644
--- a/xpcom/rust/gecko_tracing/Cargo.toml
+++ b/xpcom/rust/gecko_tracing/Cargo.toml
@@ -8,5 +8,5 @@ license = "MPL-2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "std"] }
# app-services crates, overridden in the `[patch.crates-io]` section of the top-level Cargo.toml
-error-support = { version = "0.1", features = ["tracing-logging", "tracing-reporting"] }
+error-support = { version = "0.1" }
tracing-support = { version = "0.1" }