diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml index 9dc3bde22827..c5206be5a047 100644 --- a/browser/components/newtab/metrics.yaml +++ b/browser/components/newtab/metrics.yaml @@ -906,6 +906,102 @@ newtab: expires: never telemetry_mirror: FX_ABOUTHOME_CACHE_CONSTRUCTION + report_content_open: + type: event + description: > + Recorded when content reporting is opened from context menu + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1954656 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1954656 + data_sensitivity: + - interaction + notification_emails: + - rhamoui@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + send_in_pings: + - newtab + + report_content_submit: + type: event + description: > + Recorded when content reporting has been submitted + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1954656 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1954656 + data_sensitivity: + - interaction + notification_emails: + - rhamoui@mozilla.com + expires: never + extra_keys: + card_type: + description: > + The type of the content card (e.g., "spoc", "organic") + type: string + corpus_item_id: + description: > + A content identifier. + For organic Newtab recommendations it is an opaque id produced by + Newtab's recommendation systems that corresponds uniquely to the URL. + This is the replacement for tile_id and scheduled_corpus_item_id. + type: string + is_section_followed: + description: > + If click belongs in a section, if that section is followed + type: boolean + newtab_visit_id: + description: > + The id of this newtab visit. + Allows you to separate multiple simultaneous newtabs and + build an event timeline of actions taken from this newtab. + type: string + received_rank: + description: > + The rank or order of the recommendation at the time it was sent to the client. + type: quantity + recommended_at: + description: > + The time in milliseconds the recommendation was recommended at. + type: quantity + report_reason: + description: > + The reason selected by the user when reporting the content + type: string + scheduled_corpus_item_id: + description: > + A content identifier. + For organic Newtab recommendations it is an opaque id produced by + Newtab's recommendation systems that corresponds uniquely to + a piece of content scheduled for a specific day on a specific surface. + This is the replacement for tile_id. + type: string + section: + description: > + If click belongs in a section, the name of the section + type: string + section_position: + description: > + If click belongs in a section, the numeric position of the section + type: string + title: + description: > + Title of the recommendation. + type: string + topic: + description: > + The topic of the recommendation. Like "entertainment". + type: string + url: + description: > + URL of the recommendation. + type: string + send_in_pings: + - newtab + newtab.search: enabled: lifetime: application diff --git a/browser/extensions/newtab/common/Actions.mjs b/browser/extensions/newtab/common/Actions.mjs index a4bb6e72284d..69c7a2d16d9d 100644 --- a/browser/extensions/newtab/common/Actions.mjs +++ b/browser/extensions/newtab/common/Actions.mjs @@ -137,8 +137,11 @@ for (const type of [ "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", + "REPORT_AD_OPEN", + "REPORT_AD_SUBMIT", "REPORT_CLOSE", - "REPORT_OPEN", + "REPORT_CONTENT_OPEN", + "REPORT_CONTENT_SUBMIT", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", diff --git a/browser/extensions/newtab/common/Reducers.sys.mjs b/browser/extensions/newtab/common/Reducers.sys.mjs index 5ad2dab7096b..39798bfca97f 100644 --- a/browser/extensions/newtab/common/Reducers.sys.mjs +++ b/browser/extensions/newtab/common/Reducers.sys.mjs @@ -896,15 +896,41 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) { showBlockSectionConfirmation: true, sectionData: action.data, }; - case at.REPORT_OPEN: + case at.REPORT_AD_OPEN: return { ...prevState, report: { ...prevState.report, + card_type: action.data?.card_type, + position: action.data?.position, + placement_id: action.data?.placement_id, + reporting_url: action.data?.reporting_url, + url: action.data?.url, + visible: true, + }, + }; + case at.REPORT_CONTENT_OPEN: + return { + ...prevState, + report: { + ...prevState.report, + card_type: action.data?.card_type, + corpus_item_id: action.data?.corpus_item_id, + is_section_followed: action.data?.is_section_followed, + received_rank: action.data?.received_rank, + recommended_at: action.data?.recommended_at, + scheduled_corpus_item_id: action.data?.scheduled_corpus_item_id, + section_position: action.data?.section_position, + section: action.data?.section, + title: action.data?.title, + topic: action.data?.topic, + url: action.data?.url, visible: true, }, }; case at.REPORT_CLOSE: + case at.REPORT_AD_SUBMIT: + case at.REPORT_CONTENT_SUBMIT: return { ...prevState, report: { diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx index 68ca014ceedc..cfea19640b13 100644 --- a/browser/extensions/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx +++ b/browser/extensions/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx @@ -320,14 +320,15 @@ export class _DiscoveryStreamBase extends React.PureComponent { } // Render a DS-style TopSites then the rest if any in a collapsible section + const { DiscoveryStream } = this.props; return ( {this.props.DiscoveryStream.isPrivacyInfoModalVisible && ( )} - - {reportContentEnabled && } - + {reportContentEnabled && ( + + )} {topSites && this.renderLayout([ { diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx index 32b38df83dff..531aea544f62 100644 --- a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx +++ b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -923,6 +923,8 @@ export class _DSCard extends React.PureComponent { is_section_followed={this.props.sectionFollowed} format={format} isSectionsCard={this.props.mayHaveSectionsCards} + topic={this.props.topic} + selected_topics={this.props.selected_topics} /> )} diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx index a42a3d8dbeb9..c1d13e862c79 100644 --- a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx +++ b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx @@ -31,7 +31,7 @@ export class _DSLinkMenu extends React.PureComponent { TOP_STORIES_CONTEXT_MENU_OPTIONS = [ "CheckBookmark", - ...(showReporting ? ["ReportContent"] : []), + ...(showReporting && this.props.section ? ["ReportContent"] : []), ...saveToPocketOptions, "Separator", "OpenInNewWindow", @@ -41,6 +41,9 @@ export class _DSLinkMenu extends React.PureComponent { ]; } + // eslint-disable-next-line no-console + console.log("dslinkmenu prop", this.props); + const type = this.props.type || "DISCOVERY_STREAM"; const title = this.props.title || this.props.source; @@ -76,7 +79,9 @@ export class _DSLinkMenu extends React.PureComponent { scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, recommended_at: this.props.recommended_at, received_rank: this.props.received_rank, + topic: this.props.topic, is_list_card: this.props.is_list_card, + position: index, ...(this.props.format ? { format: this.props.format } : {}), ...(this.props.section ? { diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ReportContent/ReportContent.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ReportContent/ReportContent.jsx index 340af0ed3263..c4261eeb257e 100644 --- a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ReportContent/ReportContent.jsx +++ b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ReportContent/ReportContent.jsx @@ -5,26 +5,102 @@ import React, { useRef, useEffect, useCallback, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { actionTypes as at, actionCreators as ac } from "common/Actions.mjs"; -export const ReportContent = () => { +export const ReportContent = spocs => { const dispatch = useDispatch(); const modal = useRef(null); const radioGroupRef = useRef(null); const submitButtonRef = useRef(null); const report = useSelector(state => state.DiscoveryStream.report); const [valueSelected, setValueSelected] = useState(false); + const [selectedReason, setSelectedReason] = useState(null); + const spocData = spocs.spocs.data; // Sends a dispatch to update the redux store when modal is cancelled - const handleCancel = useCallback(() => { + const handleCancel = () => { dispatch( - ac.BroadcastToContent({ + ac.AlsoToMain({ type: at.REPORT_CLOSE, }) ); - }, [dispatch]); + }; - const handleSubmit = useCallback(e => { - e.preventDefault(); - }, []); + const handleSubmit = useCallback(() => { + const { + card_type, + corpus_item_id, + is_section_followed, + position, + received_rank, + recommended_at, + reporting_url, + scheduled_corpus_item_id, + section_position, + section, + title, + topic, + url, + } = report; + + if (card_type === "organic") { + dispatch( + ac.AlsoToMain({ + type: at.REPORT_CONTENT_SUBMIT, + data: { + card_type, + corpus_item_id, + is_section_followed, + received_rank, + recommended_at, + report_reason: selectedReason, + scheduled_corpus_item_id, + section_position, + section, + title, + topic, + url, + }, + }) + ); + } else if (card_type === "spoc") { + // Retrieve placement_id by comparing spocData with the ad that was reported + const getPlacementId = () => { + if (!spocData || !report.url) { + return null; + } + + for (const [placementId, spocList] of Object.entries(spocData)) { + for (const spoc of Object.values(spocList)) { + if (spoc?.url === report.url) { + return placementId; + } + } + } + return null; + }; + + const placement_id = getPlacementId(); + + dispatch( + ac.AlsoToMain({ + type: at.REPORT_AD_SUBMIT, + data: { + report_reason: selectedReason, + placement_id, + position, + reporting_url, + url, + }, + }) + ); + } + + dispatch( + ac.AlsoToMain({ + type: at.BLOCK_URL, + data: [{ ...report }], + }) + ); + }, [dispatch, selectedReason, report, spocData]); // Opens and closes the modal based on user interaction useEffect(() => { @@ -40,7 +116,14 @@ export const ReportContent = () => { const radioGroup = radioGroupRef.current; const submitButton = submitButtonRef.current; - const handleRadioChange = () => setValueSelected(true); + const handleRadioChange = e => { + const reasonValue = e?.target?.value; + + if (reasonValue) { + setValueSelected(true); + setSelectedReason(reasonValue); + } + }; if (radioGroup) { radioGroup.addEventListener("change", handleRadioChange); @@ -62,7 +145,7 @@ export const ReportContent = () => { radioGroup.removeEventListener("change", handleRadioChange); } }; - }, [valueSelected]); + }, [valueSelected, selectedReason]); return ( { onClose={() => dispatch({ type: at.REPORT_CLOSE })} >
- - - - - + {/* spocs and stories are going to have different reporting + options, so placed a conditional to render the different reasons */} + {report.card_type === "spoc" ? ( + <> + + + + + + + ) : ( + <> + + + + + + + )} { return LinkMenuOptions.BlockUrls([site], index, eventSource); }, - // Same as BlockUrl, cept can work on an array of sites. + // Same as BlockUrl, except can work on an array of sites. BlockUrls: (tiles, pos, eventSource) => ({ id: "newtab-menu-dismiss", icon: "dismiss", @@ -525,12 +525,40 @@ export const LinkMenuOptions = { }, }), }), - ReportAd: () => ({ - id: "newtab-menu-report-this-ad", - action: ac.BroadcastToContent({ type: at.REPORT_OPEN }), - }), - ReportContent: () => ({ - id: "newtab-menu-report-content", - action: ac.BroadcastToContent({ type: at.REPORT_OPEN }), - }), + ReportAd: site => { + return { + id: "newtab-menu-report-this-ad", + action: ac.AlsoToMain({ + type: at.REPORT_AD_OPEN, + data: { + card_type: site.card_type, + position: site.position, + reporting_url: site.shim.report, + url: site.url, + }, + }), + }; + }, + + ReportContent: site => { + return { + id: "newtab-menu-report-content", + action: ac.AlsoToMain({ + type: at.REPORT_CONTENT_OPEN, + data: { + card_type: site.card_type, + corpus_item_id: site.corpus_item_id, + is_section_followed: site.is_section_followed, + received_rank: site.received_rank, + recommended_at: site.recommended_at, + scheduled_corpus_item_id: site.scheduled_corpus_item_id, + section_position: site.section_position, + section: site.section, + title: site.title, + topic: site.topic, + url: site.url, + }, + }), + }; + }, }; diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js index bf13ce35143c..98876b69db22 100644 --- a/browser/extensions/newtab/data/content/activity-stream.bundle.js +++ b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -210,8 +210,11 @@ for (const type of [ "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", + "REPORT_AD_OPEN", + "REPORT_AD_SUBMIT", "REPORT_CLOSE", - "REPORT_OPEN", + "REPORT_CONTENT_OPEN", + "REPORT_CONTENT_SUBMIT", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", @@ -1777,7 +1780,7 @@ const LinkMenuOptions = { BlockUrl: (site, index, eventSource) => { return LinkMenuOptions.BlockUrls([site], index, eventSource); }, - // Same as BlockUrl, cept can work on an array of sites. + // Same as BlockUrl, except can work on an array of sites. BlockUrls: (tiles, pos, eventSource) => ({ id: "newtab-menu-dismiss", icon: "dismiss", @@ -2209,14 +2212,42 @@ const LinkMenuOptions = { }, }), }), - ReportAd: () => ({ - id: "newtab-menu-report-this-ad", - action: actionCreators.BroadcastToContent({ type: actionTypes.REPORT_OPEN }), - }), - ReportContent: () => ({ - id: "newtab-menu-report-content", - action: actionCreators.BroadcastToContent({ type: actionTypes.REPORT_OPEN }), - }), + ReportAd: site => { + return { + id: "newtab-menu-report-this-ad", + action: actionCreators.AlsoToMain({ + type: actionTypes.REPORT_AD_OPEN, + data: { + card_type: site.card_type, + position: site.position, + reporting_url: site.shim.report, + url: site.url, + }, + }), + }; + }, + + ReportContent: site => { + return { + id: "newtab-menu-report-content", + action: actionCreators.AlsoToMain({ + type: actionTypes.REPORT_CONTENT_OPEN, + data: { + card_type: site.card_type, + corpus_item_id: site.corpus_item_id, + is_section_followed: site.is_section_followed, + received_rank: site.received_rank, + recommended_at: site.recommended_at, + scheduled_corpus_item_id: site.scheduled_corpus_item_id, + section_position: site.section_position, + section: site.section, + title: site.title, + topic: site.topic, + url: site.url, + }, + }), + }; + }, }; ;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx @@ -2414,8 +2445,11 @@ class _DSLinkMenu extends (external_React_default()).PureComponent { TOP_STORIES_CONTEXT_MENU_OPTIONS = ["BlockUrl", ...(showReporting ? ["ReportAd"] : []), "ManageSponsoredContent", "OurSponsorsAndYourPrivacy"]; } else { const saveToPocketOptions = this.props.pocket_button_enabled ? ["CheckArchiveFromPocket", "CheckSavedToPocket"] : []; - TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", ...(showReporting ? ["ReportContent"] : []), ...saveToPocketOptions, "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]; + TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", ...(showReporting && this.props.section ? ["ReportContent"] : []), ...saveToPocketOptions, "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]; } + + // eslint-disable-next-line no-console + console.log("dslinkmenu prop", this.props); const type = this.props.type || "DISCOVERY_STREAM"; const title = this.props.title || this.props.source; return /*#__PURE__*/external_React_default().createElement("div", { @@ -2451,7 +2485,9 @@ class _DSLinkMenu extends (external_React_default()).PureComponent { scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, recommended_at: this.props.recommended_at, received_rank: this.props.received_rank, + topic: this.props.topic, is_list_card: this.props.is_list_card, + position: index, ...(this.props.format ? { format: this.props.format } : {}), @@ -4047,7 +4083,9 @@ class _DSCard extends (external_React_default()).PureComponent { section_position: this.props.sectionPosition, is_section_followed: this.props.sectionFollowed, format: format, - isSectionsCard: this.props.mayHaveSectionsCards + isSectionsCard: this.props.mayHaveSectionsCards, + topic: this.props.topic, + selected_topics: this.props.selected_topics })))); } } @@ -5770,23 +5808,90 @@ class DSPrivacyModal extends (external_React_default()).PureComponent { -const ReportContent = () => { +const ReportContent = spocs => { const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)(); const modal = (0,external_React_namespaceObject.useRef)(null); const radioGroupRef = (0,external_React_namespaceObject.useRef)(null); const submitButtonRef = (0,external_React_namespaceObject.useRef)(null); const report = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream.report); const [valueSelected, setValueSelected] = (0,external_React_namespaceObject.useState)(false); + const [selectedReason, setSelectedReason] = (0,external_React_namespaceObject.useState)(null); + const spocData = spocs.spocs.data; // Sends a dispatch to update the redux store when modal is cancelled - const handleCancel = (0,external_React_namespaceObject.useCallback)(() => { - dispatch(actionCreators.BroadcastToContent({ + const handleCancel = () => { + dispatch(actionCreators.AlsoToMain({ type: actionTypes.REPORT_CLOSE })); - }, [dispatch]); - const handleSubmit = (0,external_React_namespaceObject.useCallback)(e => { - e.preventDefault(); - }, []); + }; + const handleSubmit = (0,external_React_namespaceObject.useCallback)(() => { + const { + card_type, + corpus_item_id, + is_section_followed, + position, + received_rank, + recommended_at, + reporting_url, + scheduled_corpus_item_id, + section_position, + section, + title, + topic, + url + } = report; + if (card_type === "organic") { + dispatch(actionCreators.AlsoToMain({ + type: actionTypes.REPORT_CONTENT_SUBMIT, + data: { + card_type, + corpus_item_id, + is_section_followed, + received_rank, + recommended_at, + report_reason: selectedReason, + scheduled_corpus_item_id, + section_position, + section, + title, + topic, + url + } + })); + } else if (card_type === "spoc") { + // Retrieve placement_id by comparing spocData with the ad that was reported + const getPlacementId = () => { + if (!spocData || !report.url) { + return null; + } + for (const [placementId, spocList] of Object.entries(spocData)) { + for (const spoc of Object.values(spocList)) { + if (spoc?.url === report.url) { + return placementId; + } + } + } + return null; + }; + const placement_id = getPlacementId(); + dispatch(actionCreators.AlsoToMain({ + type: actionTypes.REPORT_AD_SUBMIT, + data: { + report_reason: selectedReason, + placement_id, + position, + reporting_url, + url + } + })); + } + dispatch(actionCreators.AlsoToMain({ + type: actionTypes.BLOCK_URL, + data: [{ + ...report + }] + })); + }, [dispatch, selectedReason, report, spocData]); // Opens and closes the modal based on user interaction (0,external_React_namespaceObject.useEffect)(() => { @@ -5801,7 +5906,13 @@ const ReportContent = () => { (0,external_React_namespaceObject.useEffect)(() => { const radioGroup = radioGroupRef.current; const submitButton = submitButtonRef.current; - const handleRadioChange = () => setValueSelected(true); + const handleRadioChange = e => { + const reasonValue = e?.target?.value; + if (reasonValue) { + setValueSelected(true); + setSelectedReason(reasonValue); + } + }; if (radioGroup) { radioGroup.addEventListener("change", handleRadioChange); } @@ -5820,7 +5931,7 @@ const ReportContent = () => { radioGroup.removeEventListener("change", handleRadioChange); } }; - }, [valueSelected]); + }, [valueSelected, selectedReason]); return /*#__PURE__*/external_React_default().createElement("dialog", { className: "report-content-form", id: "dialog-report", @@ -5830,21 +5941,35 @@ const ReportContent = () => { }) }, /*#__PURE__*/external_React_default().createElement("form", { action: "" - }, /*#__PURE__*/external_React_default().createElement("moz-radio-group", { + }, report.card_type === "spoc" ? /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("moz-radio-group", { name: "report", ref: radioGroupRef, id: "report-group", "data-l10n-id": "newtab-report-ads-why-reporting" }, /*#__PURE__*/external_React_default().createElement("moz-radio", { - value: "unsafe", - "data-l10n-id": "newtab-report-ads-reason-unsafe" + "data-l10n-id": "newtab-report-ads-reason-not-interested", + value: "not_interested" }), /*#__PURE__*/external_React_default().createElement("moz-radio", { "data-l10n-id": "newtab-report-ads-reason-inappropriate", value: "inappropriate" }), /*#__PURE__*/external_React_default().createElement("moz-radio", { "data-l10n-id": "newtab-report-ads-reason-seen-it-too-many-times", - value: "too-many" - })), /*#__PURE__*/external_React_default().createElement("moz-button-group", null, /*#__PURE__*/external_React_default().createElement("moz-button", { + value: "seen_too_many_times" + }))) : /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("moz-radio-group", { + name: "report", + ref: radioGroupRef, + id: "report-group", + "data-l10n-id": "newtab-report-content-why-reporting" + }, /*#__PURE__*/external_React_default().createElement("moz-radio", { + value: "Unsafe content", + "data-l10n-id": "newtab-report-ads-reason-unsafe" + }), /*#__PURE__*/external_React_default().createElement("moz-radio", { + "data-l10n-id": "newtab-report-ads-reason-inappropriate", + value: "Inappropriate content" + }), /*#__PURE__*/external_React_default().createElement("moz-radio", { + "data-l10n-id": "newtab-report-ads-reason-seen-it-too-many-times", + value: "Seen too many times" + }))), /*#__PURE__*/external_React_default().createElement("moz-button-group", null, /*#__PURE__*/external_React_default().createElement("moz-button", { "data-l10n-id": "newtab-topic-selection-cancel-button", onClick: handleCancel }), /*#__PURE__*/external_React_default().createElement("moz-button", { @@ -7941,15 +8066,41 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) { showBlockSectionConfirmation: true, sectionData: action.data, }; - case actionTypes.REPORT_OPEN: + case actionTypes.REPORT_AD_OPEN: return { ...prevState, report: { ...prevState.report, + card_type: action.data?.card_type, + position: action.data?.position, + placement_id: action.data?.placement_id, + reporting_url: action.data?.reporting_url, + url: action.data?.url, + visible: true, + }, + }; + case actionTypes.REPORT_CONTENT_OPEN: + return { + ...prevState, + report: { + ...prevState.report, + card_type: action.data?.card_type, + corpus_item_id: action.data?.corpus_item_id, + is_section_followed: action.data?.is_section_followed, + received_rank: action.data?.received_rank, + recommended_at: action.data?.recommended_at, + scheduled_corpus_item_id: action.data?.scheduled_corpus_item_id, + section_position: action.data?.section_position, + section: action.data?.section, + title: action.data?.title, + topic: action.data?.topic, + url: action.data?.url, visible: true, }, }; case actionTypes.REPORT_CLOSE: + case actionTypes.REPORT_AD_SUBMIT: + case actionTypes.REPORT_CONTENT_SUBMIT: return { ...prevState, report: { @@ -11325,9 +11476,14 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent { } // Render a DS-style TopSites then the rest if any in a collapsible section + const { + DiscoveryStream + } = this.props; return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, this.props.DiscoveryStream.isPrivacyInfoModalVisible && /*#__PURE__*/external_React_default().createElement(DSPrivacyModal, { dispatch: this.props.dispatch - }), reportContentEnabled && /*#__PURE__*/external_React_default().createElement(ReportContent, null), topSites && this.renderLayout([{ + }), reportContentEnabled && /*#__PURE__*/external_React_default().createElement(ReportContent, { + spocs: DiscoveryStream.spocs + }), topSites && this.renderLayout([{ width: 12, components: [topSites], sectionType: "topsites" diff --git a/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs b/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs index ff5b4eb1df42..a7e3827fc43b 100644 --- a/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs +++ b/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs @@ -1,3 +1,6 @@ +// We're using console.error() to debug, so we'll be keeping this rule handy +/* eslint no-console: ["error", { allow: ["error"] }] */ + /* 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/. */ @@ -553,7 +556,6 @@ export class TelemetryFeed { }, }); const session = this.sessions.get(au.getPortIdOfSender(action)); - switch (action.data?.event) { case "CLICK": { const { @@ -984,6 +986,89 @@ export class TelemetryFeed { case at.INLINE_SELECTION_IMPRESSION: this.handleInlineSelectionUserEvent(action); break; + case at.REPORT_AD_OPEN: + case at.REPORT_AD_SUBMIT: + this.handleReportAdUserEvent(action); + break; + case at.REPORT_CONTENT_OPEN: + case at.REPORT_CONTENT_SUBMIT: + this.handleReportContentUserEvent(action); + break; + } + } + + async handleReportAdUserEvent(action) { + const { placement_id, position, report_reason, reporting_url } = + action.data || {}; + + const url = new URL(reporting_url); + url.searchParams.append("placement_id", placement_id); + url.searchParams.append("reason", report_reason); + url.searchParams.append("position", position); + const adResponse = url.toString(); + + const allowed = + this._prefs + .get(PREF_ENDPOINTS) + .split(",") + .map(item => item.trim()) + .filter(item => item) || []; + + if (!allowed.some(prefix => adResponse.startsWith(prefix))) { + throw new Error( + `[Unified ads callback] Not one of allowed prefixes (${allowed})` + ); + } + + try { + await fetch(adResponse); + } catch (error) { + console.error("Error:", error); + } + } + + handleReportContentUserEvent(action) { + const session = this.sessions.get(au.getPortIdOfSender(action)); + const { + card_type, + corpus_item_id, + is_section_followed, + received_rank, + recommended_at, + report_reason, + scheduled_corpus_item_id, + section_position, + section, + title, + topic, + url, + } = action.data || {}; + + if (session) { + switch (action.type) { + case "REPORT_CONTENT_OPEN": + Glean.newtab.reportContentOpen.record({ + newtab_visit_id: session.session_id, + }); + break; + case "REPORT_CONTENT_SUBMIT": + Glean.newtab.reportContentSubmit.record({ + card_type, + corpus_item_id, + is_section_followed, + newtab_visit_id: session.session_id, + received_rank, + recommended_at, + report_reason, + scheduled_corpus_item_id, + section_position, + section, + title, + topic, + url, + }); + break; + } } } diff --git a/browser/locales-preview/reportContentTemporary.ftl b/browser/locales-preview/reportContentTemporary.ftl index 9dc9db06b5ea..ceb548aa7add 100644 --- a/browser/locales-preview/reportContentTemporary.ftl +++ b/browser/locales-preview/reportContentTemporary.ftl @@ -6,10 +6,14 @@ newtab-report-ads-why-reporting = .label = Why are you reporting this ad? +newtab-report-content-why-reporting = + .label = Why are you reporting this story? newtab-report-ads-reason-unsafe = .label = It’s unsafe newtab-report-ads-reason-inappropriate = .label = It’s inappropriate newtab-report-ads-reason-seen-it-too-many-times = .label = I’ve seen it too many times +newtab-report-ads-reason-not-interested = + .label = Not interested newtab-report-submit = Submit