the-algorithm/home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/side_effect/ClientEventsBuilder.scala

186 lines
7.6 KiB
Scala

package com.twitter.home_mixer.functional_component.side_effect
import com.twitter.conversions.DurationOps._
import com.twitter.home_mixer.functional_component.decorator.HomeQueryTypePredicates
import com.twitter.home_mixer.functional_component.decorator.builder.HomeTweetTypePredicates
import com.twitter.home_mixer.model.HomeFeatures.AccountAgeFeature
import com.twitter.home_mixer.model.HomeFeatures.SuggestTypeFeature
import com.twitter.home_mixer.model.HomeFeatures.VideoDurationMsFeature
import com.twitter.home_mixer.model.request.FollowingProduct
import com.twitter.home_mixer.model.request.ForYouProduct
import com.twitter.home_mixer.model.request.ListTweetsProduct
import com.twitter.home_mixer.model.request.SubscribedProduct
import com.twitter.product_mixer.component_library.side_effect.ScribeClientEventSideEffect.ClientEvent
import com.twitter.product_mixer.component_library.side_effect.ScribeClientEventSideEffect.EventNamespace
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
import com.twitter.product_mixer.core.model.common.presentation.CandidateWithDetails
import com.twitter.product_mixer.core.model.common.presentation.ItemCandidateWithDetails
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.timelines.injection.scribe.InjectionScribeUtil
private[side_effect] sealed trait ClientEventsBuilder {
private val FollowingSection = Some("latest")
private val ForYouSection = Some("home")
private val ListTweetsSection = Some("list")
private val SubscribedSection = Some("subscribed")
protected def section(query: PipelineQuery): Option[String] = {
query.product match {
case FollowingProduct => FollowingSection
case ForYouProduct => ForYouSection
case ListTweetsProduct => ListTweetsSection
case SubscribedProduct => SubscribedSection
case other => throw new UnsupportedOperationException(s"Unknown product: $other")
}
}
protected def count(
candidates: Seq[CandidateWithDetails],
predicate: FeatureMap => Boolean = _ => true,
queryFeatures: FeatureMap = FeatureMap.empty
): Option[Long] = Some(candidates.view.count(item => predicate(item.features ++ queryFeatures)))
protected def sum(
candidates: Seq[CandidateWithDetails],
predicate: FeatureMap => Option[Int],
queryFeatures: FeatureMap = FeatureMap.empty
): Option[Long] =
Some(candidates.view.flatMap(item => predicate(item.features ++ queryFeatures)).sum)
}
private[side_effect] object ServedEventsBuilder extends ClientEventsBuilder {
private val ServedTweetsAction = Some("served_tweets")
private val ServedUsersAction = Some("served_users")
private val InjectedComponent = Some("injected")
private val PromotedComponent = Some("promoted")
private val WhoToFollowComponent = Some("who_to_follow")
private val WhoToSubscribeComponent = Some("who_to_subscribe")
private val WithVideoDurationComponent = Some("with_video_duration")
private val VideoDurationSumElement = Some("video_duration_sum")
private val NumVideosElement = Some("num_videos")
def build(
query: PipelineQuery,
injectedTweets: Seq[ItemCandidateWithDetails],
promotedTweets: Seq[ItemCandidateWithDetails],
whoToFollowUsers: Seq[ItemCandidateWithDetails],
whoToSubscribeUsers: Seq[ItemCandidateWithDetails]
): Seq[ClientEvent] = {
val baseEventNamespace = EventNamespace(
section = section(query),
action = ServedTweetsAction
)
val overallServedEvents = Seq(
ClientEvent(baseEventNamespace, eventValue = count(injectedTweets ++ promotedTweets)),
ClientEvent(
baseEventNamespace.copy(component = InjectedComponent),
eventValue = count(injectedTweets)),
ClientEvent(
baseEventNamespace.copy(component = PromotedComponent),
eventValue = count(promotedTweets)),
ClientEvent(
baseEventNamespace.copy(component = WhoToFollowComponent, action = ServedUsersAction),
eventValue = count(whoToFollowUsers)),
ClientEvent(
baseEventNamespace.copy(component = WhoToSubscribeComponent, action = ServedUsersAction),
eventValue = count(whoToSubscribeUsers)),
)
val tweetTypeServedEvents = HomeTweetTypePredicates.PredicateMap.map {
case (tweetType, predicate) =>
ClientEvent(
baseEventNamespace.copy(component = InjectedComponent, element = Some(tweetType)),
eventValue = count(injectedTweets, predicate, query.features.getOrElse(FeatureMap.empty))
)
}.toSeq
val suggestTypeServedEvents = injectedTweets
.flatMap(_.features.getOrElse(SuggestTypeFeature, None))
.map {
InjectionScribeUtil.scribeComponent
}
.groupBy(identity).map {
case (suggestType, group) =>
ClientEvent(
baseEventNamespace.copy(component = suggestType),
eventValue = Some(group.size.toLong))
}.toSeq
// Video duration events
val numVideosEvent = ClientEvent(
baseEventNamespace.copy(component = WithVideoDurationComponent, element = NumVideosElement),
eventValue = count(injectedTweets, _.getOrElse(VideoDurationMsFeature, None).nonEmpty)
)
val videoDurationSumEvent = ClientEvent(
baseEventNamespace
.copy(component = WithVideoDurationComponent, element = VideoDurationSumElement),
eventValue = sum(injectedTweets, _.getOrElse(VideoDurationMsFeature, None))
)
val videoEvents = Seq(numVideosEvent, videoDurationSumEvent)
overallServedEvents ++ tweetTypeServedEvents ++ suggestTypeServedEvents ++ videoEvents
}
}
private[side_effect] object EmptyTimelineEventsBuilder extends ClientEventsBuilder {
private val EmptyAction = Some("empty")
private val AccountAgeLessThan30MinutesComponent = Some("account_age_less_than_30_minutes")
private val ServedNonPromotedTweetElement = Some("served_non_promoted_tweet")
def build(
query: PipelineQuery,
injectedTweets: Seq[ItemCandidateWithDetails]
): Seq[ClientEvent] = {
val baseEventNamespace = EventNamespace(
section = section(query),
action = EmptyAction
)
// Empty timeline events
val accountAgeLessThan30Minutes = query.features
.flatMap(_.getOrElse(AccountAgeFeature, None))
.exists(_.untilNow < 30.minutes)
val isEmptyTimeline = count(injectedTweets).contains(0L)
val predicates = Seq(
None -> isEmptyTimeline,
AccountAgeLessThan30MinutesComponent -> (isEmptyTimeline && accountAgeLessThan30Minutes)
)
for {
(component, predicate) <- predicates
if predicate
} yield ClientEvent(
baseEventNamespace.copy(component = component, element = ServedNonPromotedTweetElement))
}
}
private[side_effect] object QueryEventsBuilder extends ClientEventsBuilder {
private val ServedSizePredicateMap: Map[String, Int => Boolean] = Map(
("size_is_empty", _ <= 0),
("size_at_most_5", _ <= 5),
("size_at_most_10", _ <= 10),
("size_at_most_35", _ <= 35)
)
def build(
query: PipelineQuery,
injectedTweets: Seq[ItemCandidateWithDetails]
): Seq[ClientEvent] = {
val baseEventNamespace = EventNamespace(
section = section(query)
)
val queryFeatureMap = query.features.getOrElse(FeatureMap.empty)
val servedSizeQueryEvents =
for {
(queryPredicateName, queryPredicate) <- HomeQueryTypePredicates.PredicateMap
if queryPredicate(queryFeatureMap)
(servedSizePredicateName, servedSizePredicate) <- ServedSizePredicateMap
if servedSizePredicate(injectedTweets.size)
} yield ClientEvent(
baseEventNamespace
.copy(component = Some(servedSizePredicateName), action = Some(queryPredicateName)))
servedSizeQueryEvents.toSeq
}
}