diff --git a/common/app/experiments/Experiments.scala b/common/app/experiments/Experiments.scala index dcad0f1b1d1..a70890d856c 100644 --- a/common/app/experiments/Experiments.scala +++ b/common/app/experiments/Experiments.scala @@ -16,6 +16,7 @@ object ActiveExperiments extends ExperimentsDefinition { DCRVideoPages, UpdatedHeaderDesign, MastheadWithHighlights, + AffiliateLinksDCR, ) implicit val canCheckExperiment: CanCheckExperiment = new CanCheckExperiment(this) } @@ -48,6 +49,15 @@ object DarkModeWeb participationGroup = Perc0D, ) +object AffiliateLinksDCR + extends Experiment( + name = "affiliate-links-dcr", + description = "Display affiliate links on all eligible DCR articles", + owners = Seq(Owner.withGithub("commercial.dev@theguardian.com")), + sellByDate = LocalDate.of(2024, 7, 30), + participationGroup = Perc0E, + ) + object DCRVideoPages extends Experiment( name = "dcr-video-pages", diff --git a/common/app/model/dotcomrendering/DotcomBlocksRenderingDataModel.scala b/common/app/model/dotcomrendering/DotcomBlocksRenderingDataModel.scala index 6edc08e8aa5..65c90d5e409 100644 --- a/common/app/model/dotcomrendering/DotcomBlocksRenderingDataModel.scala +++ b/common/app/model/dotcomrendering/DotcomBlocksRenderingDataModel.scala @@ -69,7 +69,7 @@ object DotcomBlocksRenderingDataModel { bodyBlocks: Seq[APIBlock], ): DotcomBlocksRenderingDataModel = { val content = page.item - val shouldAddAffiliateLinks = DotcomRenderingUtils.shouldAddAffiliateLinks(content) + val shouldAddAffiliateLinks = DotcomRenderingUtils.shouldAddAffiliateLinks(content)(request) val contentDateTimes = DotcomRenderingUtils.contentDateTimes(content) val edition = Edition(request) diff --git a/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala b/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala index f1f280ab99c..fe97e8df2d9 100644 --- a/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala +++ b/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala @@ -487,7 +487,7 @@ object DotcomRenderingDataModel { blocks.exists(block => DotcomRenderingUtils.stringContainsAffiliateableLinks(block.bodyHtml)) } - val shouldAddAffiliateLinks = DotcomRenderingUtils.shouldAddAffiliateLinks(content) + val shouldAddAffiliateLinks = DotcomRenderingUtils.shouldAddAffiliateLinks(content)(request) val shouldAddDisclaimer = hasAffiliateLinks(bodyBlocks) val contentDateTimes: ArticleDateTimes = ArticleDateTimes( diff --git a/common/app/model/dotcomrendering/DotcomRenderingUtils.scala b/common/app/model/dotcomrendering/DotcomRenderingUtils.scala index f5f29f1092e..996ef732729 100644 --- a/common/app/model/dotcomrendering/DotcomRenderingUtils.scala +++ b/common/app/model/dotcomrendering/DotcomRenderingUtils.scala @@ -22,6 +22,7 @@ import model.{ Pillar, } import org.joda.time.format.DateTimeFormat +import org.jsoup.Jsoup import play.api.libs.json._ import play.api.mvc.RequestHeader import views.support.AffiliateLinksCleaner @@ -241,19 +242,27 @@ object DotcomRenderingUtils { } } - def shouldAddAffiliateLinks(content: ContentType): Boolean = { - AffiliateLinksCleaner.shouldAddAffiliateLinks( - switchedOn = Switches.AffiliateLinks.isSwitchedOn, - section = content.metadata.sectionId, - showAffiliateLinks = content.content.fields.showAffiliateLinks, - supportedSections = Configuration.affiliateLinks.affiliateLinkSections, - defaultOffTags = Configuration.affiliateLinks.defaultOffTags, - alwaysOffTags = Configuration.affiliateLinks.alwaysOffTags, - tagPaths = content.content.tags.tags.map(_.id), - firstPublishedDate = content.content.fields.firstPublicationDate, - pageUrl = content.metadata.id, - contentType = "article", - ) + def shouldAddAffiliateLinks(content: ContentType)(implicit request: RequestHeader): Boolean = { + val contentHtml = Jsoup.parse(content.fields.body) + val bodyElements = contentHtml.select("body").first().children() + if (bodyElements.size >= 2) { + val firstEl = bodyElements.get(0) + val secondEl = bodyElements.get(1) + if (firstEl.tagName == "p" && secondEl.tagName == "p" && secondEl.text().length >= 250) { + AffiliateLinksCleaner.shouldAddAffiliateLinks( + switchedOn = Switches.AffiliateLinks.isSwitchedOn, + section = content.metadata.sectionId, + showAffiliateLinks = content.content.fields.showAffiliateLinks, + supportedSections = Configuration.affiliateLinks.affiliateLinkSections, + defaultOffTags = Configuration.affiliateLinks.defaultOffTags, + alwaysOffTags = Configuration.affiliateLinks.alwaysOffTags, + tagPaths = content.content.tags.tags.map(_.id), + firstPublishedDate = content.content.fields.firstPublicationDate, + pageUrl = content.metadata.id, + contentType = "article", + ) + } else false + } else false } def contentDateTimes(content: ContentType): ArticleDateTimes = { diff --git a/common/app/views/support/HtmlCleaner.scala b/common/app/views/support/HtmlCleaner.scala index 9756c01c857..9c543d4c310 100644 --- a/common/app/views/support/HtmlCleaner.scala +++ b/common/app/views/support/HtmlCleaner.scala @@ -6,6 +6,7 @@ import common.{Edition, GuLogging, LinkTo} import conf.Configuration.affiliateLinks._ import conf.Configuration.site.host import conf.switches.Switches._ +import experiments.{ActiveExperiments, AffiliateLinksDCR} import layout.ContentWidths import layout.ContentWidths._ import model._ @@ -873,7 +874,8 @@ case class AffiliateLinksCleaner( tags: List[String], publishedDate: Option[DateTime], contentType: String, -) extends HtmlCleaner +)(implicit request: RequestHeader) + extends HtmlCleaner with GuLogging { override def clean(document: Document): Document = { @@ -946,7 +948,7 @@ object AffiliateLinksCleaner { firstPublishedDate: Option[DateTime], pageUrl: String, contentType: String, - ): Boolean = { + )(implicit request: RequestHeader): Boolean = { val publishedCutOffDate = new DateTime(2020, 8, 14, 0, 0) val cleanedPageUrl = if (pageUrl.charAt(0) == '/') { @@ -973,11 +975,11 @@ object AffiliateLinksCleaner { // Never include affiliate links if it is tagged with an always off tag, or if it was published before our cut off date. // The cut off date is temporary while we are working on improving the compliance of affiliate links. - // The cut off date does not apply to any URL on the allow list + // The cut off date does not apply to any URL on the allow list or to galleries if ( !contentHasAlwaysOffTag(tagPaths, alwaysOffTags) && (firstPublishedDate.exists( _.isBefore(publishedCutOffDate), - ) || urlIsInAllowList || contentType == "gallery") + ) || urlIsInAllowList || contentType == "gallery" || ActiveExperiments.isParticipating(AffiliateLinksDCR)) ) { if (showAffiliateLinks.isDefined) { showAffiliateLinks.contains(true) diff --git a/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala b/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala index e0133ebaf90..28f4b9288e5 100644 --- a/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala +++ b/common/test/views/support/cleaner/AffiliateLinksCleanerTest.scala @@ -1,8 +1,10 @@ package views.support.cleaner import conf.Configuration +import conf.switches.Switches.ServerSideExperiments import org.joda.time.DateTime import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import play.api.test.FakeRequest import views.support.AffiliateLinksCleaner._ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { @@ -16,6 +18,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { } "shouldAddAffiliateLinks" should "correctly determine when to add affiliate links" in { + val fakeTestControlRequest = FakeRequest().withHeaders("X-GU-Experiment-0perc-E" -> "control") val supportedSections = Set("film", "books", "fashion") val oldPublishedDate = Some(new DateTime(2020, 8, 13, 0, 0)) val newPublishedDate = Some(new DateTime(2020, 8, 15, 0, 0)) @@ -32,7 +35,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(false) + )(fakeTestControlRequest) should be(false) shouldAddAffiliateLinks( switchedOn = true, "film", @@ -44,7 +47,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(true) + )(fakeTestControlRequest) should be(true) shouldAddAffiliateLinks( switchedOn = true, "film", @@ -56,7 +59,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(false) + )(fakeTestControlRequest) should be(false) shouldAddAffiliateLinks( switchedOn = true, "news", @@ -68,7 +71,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(true) + )(fakeTestControlRequest) should be(true) shouldAddAffiliateLinks( switchedOn = true, "news", @@ -80,7 +83,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(false) + )(fakeTestControlRequest) should be(false) shouldAddAffiliateLinks( switchedOn = true, "news", @@ -92,7 +95,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(false) + )(fakeTestControlRequest) should be(false) shouldAddAffiliateLinks( switchedOn = true, "fashion", @@ -104,7 +107,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(true) + )(fakeTestControlRequest) should be(true) shouldAddAffiliateLinks( switchedOn = true, "fashion", @@ -116,7 +119,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(false) + )(fakeTestControlRequest) should be(false) shouldAddAffiliateLinks( switchedOn = true, "fashion", @@ -128,7 +131,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { oldPublishedDate, deniedPageUrl, "article", - ) should be(true) + )(fakeTestControlRequest) should be(true) shouldAddAffiliateLinks( switchedOn = true, "film", @@ -140,7 +143,7 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { newPublishedDate, deniedPageUrl, "article", - ) should be(false) + )(fakeTestControlRequest) should be(false) shouldAddAffiliateLinks( switchedOn = true, "film", @@ -152,6 +155,6 @@ class AffiliateLinksCleanerTest extends AnyFlatSpec with Matchers { newPublishedDate, deniedPageUrl, "gallery", - ) should be(true) + )(fakeTestControlRequest) should be(true) } }