diff --git a/CHANGELOG.md b/CHANGELOG.md index 3751eee441b..61b98a79609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ - Add `LoginStore::list_candidates()` and `LoginStore::get_many()`, a pair of read APIs for consumers which filter logins on their unencrypted fields. `list_candidates()` returns a `LoginCandidate` per stored login - everything `Login` has except the secure fields (`username`/`password`), so searching by `origin`, `httpRealm` or `formActionOrigin` no longer forces a primary password prompt. `get_many()` then decrypts just the logins which matched. `list()` is unchanged, for callers who really do want every login in cleartext. +## ⚠️ Breaking Changes ⚠️ + +### Suggest + +- `Suggestion.Amp` gained a new `suggestionId` field: a unique identifier for the sponsored suggestion assigned by the ingestion pipeline, deserialized from the `suggestion_id` field of the remote settings AMP data. ([#7554](https://github.com/mozilla/application-services/pull/7554)) + # v155.0 (_2026-08-13_) [Full Changelog](https://github.com/mozilla/application-services/compare/v154.0...v155.0) diff --git a/components/suggest/src/db.rs b/components/suggest/src/db.rs index 4ce90fd08aa..e87476d887b 100644 --- a/components/suggest/src/db.rs +++ b/components/suggest/src/db.rs @@ -333,6 +333,7 @@ impl<'a> SuggestDao<'a> { amp.iab_category, amp.impression_url, amp.click_url, + amp.moz_suggestion_id, i.data AS icon, i.mimetype AS icon_mimetype FROM @@ -368,6 +369,7 @@ impl<'a> SuggestDao<'a> { raw_click_url, score, fts_match_info: None, + suggestion_id: row.get("moz_suggestion_id")?, }) }, ) @@ -422,6 +424,7 @@ impl<'a> SuggestDao<'a> { amp.iab_category, amp.impression_url, amp.click_url, + amp.moz_suggestion_id, i.data AS icon, i.mimetype AS icon_mimetype FROM @@ -463,6 +466,7 @@ impl<'a> SuggestDao<'a> { raw_click_url, score, fts_match_info: Some(match_info), + suggestion_id: row.get("moz_suggestion_id")?, }) }, ) @@ -1436,9 +1440,10 @@ impl<'conn> AmpInsertStatement<'conn> { iab_category, impression_url, click_url, - icon_id + icon_id, + moz_suggestion_id ) - VALUES(?, ?, ?, ?, ?, ?, ?) + VALUES(?, ?, ?, ?, ?, ?, ?, ?) ", )?)) } @@ -1453,6 +1458,7 @@ impl<'conn> AmpInsertStatement<'conn> { &.impression_url, &.click_url, &.icon_id, + &.suggestion_id, )) .with_context("amp insert")?; Ok(()) diff --git a/components/suggest/src/rs.rs b/components/suggest/src/rs.rs index 401fb4c1bbf..23eb4d2ef94 100644 --- a/components/suggest/src/rs.rs +++ b/components/suggest/src/rs.rs @@ -364,6 +364,7 @@ pub(crate) struct DownloadedAmpSuggestion { pub impression_url: String, #[serde(rename = "icon")] pub icon_id: String, + pub suggestion_id: String, } /// A Wikipedia suggestion to ingest from a Wikipedia attachment. diff --git a/components/suggest/src/schema.rs b/components/suggest/src/schema.rs index 8c4f39eade4..47fc802c736 100644 --- a/components/suggest/src/schema.rs +++ b/components/suggest/src/schema.rs @@ -23,7 +23,7 @@ use sql_support::{ /// `clear_database()` by adding their names to `conditional_tables`, unless /// they are cleared via a deletion trigger or there's some other good /// reason not to do so. -pub const VERSION: u32 = 45; +pub const VERSION: u32 = 46; /// The current Suggest database schema. pub const SQL: &str = " @@ -108,6 +108,7 @@ CREATE TABLE amp_custom_details( impression_url TEXT NOT NULL, click_url TEXT NOT NULL, icon_id TEXT NOT NULL, + moz_suggestion_id TEXT NOT NULL, FOREIGN KEY(suggestion_id) REFERENCES suggestions(id) ON DELETE CASCADE ); @@ -835,6 +836,26 @@ impl ConnectionInitializer for SuggestConnectionInitializer<'_> { )?; Ok(()) } + 45 => { + clear_database(tx)?; + tx.execute_batch( + " + DROP TABLE amp_custom_details; + CREATE TABLE amp_custom_details( + suggestion_id INTEGER PRIMARY KEY, + advertiser TEXT NOT NULL, + block_id INTEGER NOT NULL, + iab_category TEXT NOT NULL, + impression_url TEXT NOT NULL, + click_url TEXT NOT NULL, + icon_id TEXT NOT NULL, + moz_suggestion_id TEXT NOT NULL, + FOREIGN KEY(suggestion_id) REFERENCES suggestions(id) ON DELETE CASCADE + ); + ", + )?; + Ok(()) + } _ => Err(open_database::Error::IncompatibleVersion(version)), } diff --git a/components/suggest/src/store.rs b/components/suggest/src/store.rs index ce09fcc26c9..e4901616b70 100644 --- a/components/suggest/src/store.rs +++ b/components/suggest/src/store.rs @@ -808,10 +808,18 @@ where context.measure_download(|| self.settings_client.download_attachment(record))?; match serde_json::from_slice::>(&attachment_data) { Ok(attachment) => ingestion_handler(dao, &record.id, attachment.suggestions()), - // If the attachment doesn't match our expected schema, just skip it. It's possible - // that we're using an older version. If so, we'll get the data when we re-ingest - // after updating the schema. - Err(_) => Ok(()), + // If the attachment doesn't match our expected schema, just skip it and emit an error. + // It's possible that we're using an older version. If so, we'll get the data when we + // re-ingest after updating the schema. + Err(e) => { + error_support::report_error!( + "suggest-attachment-deserialize", + "Failed to deserialize attachment for record {}: {}", + record.id, + e + ); + Ok(()) + } } } diff --git a/components/suggest/src/suggestion.rs b/components/suggest/src/suggestion.rs index 692544efc59..b2dc832de4e 100644 --- a/components/suggest/src/suggestion.rs +++ b/components/suggest/src/suggestion.rs @@ -45,6 +45,7 @@ pub enum Suggestion { raw_click_url: String, score: f64, fts_match_info: Option, + suggestion_id: String, }, Wikipedia { title: String, diff --git a/components/suggest/src/testing/data.rs b/components/suggest/src/testing/data.rs index 9fde6658fa0..d142d6ea9dc 100644 --- a/components/suggest/src/testing/data.rs +++ b/components/suggest/src/testing/data.rs @@ -21,7 +21,8 @@ pub fn los_pollos_amp() -> JsonValue { "icon": "los-pollos-favicon", "impression_url": "https://example.com/impression_url", "click_url": "https://example.com/click_url", - "score": 0.3 + "score": 0.3, + "suggestion_id": "11111111-1111-1111-1111-111111111111" }) } @@ -53,6 +54,7 @@ pub fn los_pollos_suggestion( score: 0.3, full_keyword: full_keyword.to_string(), fts_match_info, + suggestion_id: "11111111-1111-1111-1111-111111111111".into(), } } @@ -67,7 +69,8 @@ pub fn good_place_eats_amp() -> JsonValue { "url": "https://www.lasagna.restaurant", "icon": "good-place-eats-favicon", "impression_url": "https://example.com/impression_url", - "click_url": "https://example.com/click_url" + "click_url": "https://example.com/click_url", + "suggestion_id": "22222222-2222-2222-2222-222222222222" }) } @@ -99,6 +102,7 @@ pub fn good_place_eats_suggestion( raw_click_url: "https://example.com/click_url".into(), score: 0.2, fts_match_info, + suggestion_id: "22222222-2222-2222-2222-222222222222".into(), } }