mirror of
https://github.com/twitter/the-algorithm.git
synced 2024-11-16 08:29:21 +01:00
[docx] split commit for file 6600
Signed-off-by: Ari Archer <ari.web.xyz@gmail.com>
This commit is contained in:
parent
ac0fb2a2f2
commit
2c9fa892a8
Binary file not shown.
@ -1,40 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.client_event
|
||||
|
||||
import com.twitter.clientapp.thriftscala.ItemType
|
||||
|
||||
object ItemTypeFilterPredicates {
|
||||
private val TweetItemTypes = Set[ItemType](ItemType.Tweet, ItemType.QuotedTweet)
|
||||
private val TopicItemTypes = Set[ItemType](ItemType.Tweet, ItemType.QuotedTweet, ItemType.Topic)
|
||||
private val ProfileItemTypes = Set[ItemType](ItemType.User)
|
||||
private val TypeaheadResultItemTypes = Set[ItemType](ItemType.Search, ItemType.User)
|
||||
private val SearchResultsPageFeedbackSubmitItemTypes =
|
||||
Set[ItemType](ItemType.Tweet, ItemType.RelevancePrompt)
|
||||
|
||||
/**
|
||||
* DDG lambda metrics count Tweets based on the `itemType`
|
||||
* Reference code - https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/src/scala/com/twitter/experiments/lambda/shared/Timelines.scala?L156
|
||||
* Since enums `PROMOTED_TWEET` and `POPULAR_TWEET` are deprecated in the following thrift
|
||||
* https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/src/thrift/com/twitter/clientapp/gen/client_app.thrift?L131
|
||||
* UUA filters two types of Tweets only: `TWEET` and `QUOTED_TWEET`
|
||||
*/
|
||||
def isItemTypeTweet(itemTypeOpt: Option[ItemType]): Boolean =
|
||||
itemTypeOpt.exists(itemType => TweetItemTypes.contains(itemType))
|
||||
|
||||
def isItemTypeTopic(itemTypeOpt: Option[ItemType]): Boolean =
|
||||
itemTypeOpt.exists(itemType => TopicItemTypes.contains(itemType))
|
||||
|
||||
def isItemTypeProfile(itemTypeOpt: Option[ItemType]): Boolean =
|
||||
itemTypeOpt.exists(itemType => ProfileItemTypes.contains(itemType))
|
||||
|
||||
def isItemTypeTypeaheadResult(itemTypeOpt: Option[ItemType]): Boolean =
|
||||
itemTypeOpt.exists(itemType => TypeaheadResultItemTypes.contains(itemType))
|
||||
|
||||
def isItemTypeForSearchResultsPageFeedbackSubmit(itemTypeOpt: Option[ItemType]): Boolean =
|
||||
itemTypeOpt.exists(itemType => SearchResultsPageFeedbackSubmitItemTypes.contains(itemType))
|
||||
|
||||
/**
|
||||
* Always return true. Use this when there is no need to filter based on `item_type` and all
|
||||
* values of `item_type` are acceptable.
|
||||
*/
|
||||
def ignoreItemType(itemTypeOpt: Option[ItemType]): Boolean = true
|
||||
}
|
Binary file not shown.
@ -1,26 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.client_event
|
||||
|
||||
import com.twitter.clientapp.thriftscala.LogEvent
|
||||
import com.twitter.clientapp.thriftscala.{Item => LogEventItem}
|
||||
|
||||
object NotificationClientEventUtils {
|
||||
|
||||
// Notification id for notification in the Notification Tab
|
||||
def getNotificationIdForNotificationTab(
|
||||
ceItem: LogEventItem
|
||||
): Option[String] = {
|
||||
for {
|
||||
notificationTabDetails <- ceItem.notificationTabDetails
|
||||
clientEventMetaData <- notificationTabDetails.clientEventMetadata
|
||||
notificationId <- clientEventMetaData.upstreamId
|
||||
} yield {
|
||||
notificationId
|
||||
}
|
||||
}
|
||||
|
||||
// Notification id for Push Notification
|
||||
def getNotificationIdForPushNotification(logEvent: LogEvent): Option[String] = for {
|
||||
pushNotificationDetails <- logEvent.notificationDetails
|
||||
notificationId <- pushNotificationDetails.impressionId
|
||||
} yield notificationId
|
||||
}
|
Binary file not shown.
@ -1,109 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.client_event
|
||||
|
||||
import com.twitter.clientapp.thriftscala.EventNamespace
|
||||
import com.twitter.clientapp.thriftscala.LogEvent
|
||||
import com.twitter.clientapp.thriftscala.{Item => LogEventItem}
|
||||
import com.twitter.suggests.controller_data.home_tweets.thriftscala.HomeTweetsControllerDataAliases.V1Alias
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
|
||||
object ProductSurfaceUtils {
|
||||
|
||||
def getProductSurface(eventNamespace: Option[EventNamespace]): Option[ProductSurface] = {
|
||||
(
|
||||
eventNamespace.flatMap(_.page),
|
||||
eventNamespace.flatMap(_.section),
|
||||
eventNamespace.flatMap(_.element)) match {
|
||||
case (Some("home") | Some("home_latest"), _, _) => Some(ProductSurface.HomeTimeline)
|
||||
case (Some("ntab"), _, _) => Some(ProductSurface.NotificationTab)
|
||||
case (Some(page), Some(section), _) if isPushNotification(page, section) =>
|
||||
Some(ProductSurface.PushNotification)
|
||||
case (Some("search"), _, _) => Some(ProductSurface.SearchResultsPage)
|
||||
case (_, _, Some("typeahead")) => Some(ProductSurface.SearchTypeahead)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
private def isPushNotification(page: String, section: String): Boolean = {
|
||||
Seq[String]("notification", "toasts").contains(page) ||
|
||||
(page == "app" && section == "push")
|
||||
}
|
||||
|
||||
def getProductSurfaceInfo(
|
||||
productSurface: Option[ProductSurface],
|
||||
ceItem: LogEventItem,
|
||||
logEvent: LogEvent
|
||||
): Option[ProductSurfaceInfo] = {
|
||||
productSurface match {
|
||||
case Some(ProductSurface.HomeTimeline) => createHomeTimelineInfo(ceItem)
|
||||
case Some(ProductSurface.NotificationTab) => createNotificationTabInfo(ceItem)
|
||||
case Some(ProductSurface.PushNotification) => createPushNotificationInfo(logEvent)
|
||||
case Some(ProductSurface.SearchResultsPage) => createSearchResultPageInfo(ceItem, logEvent)
|
||||
case Some(ProductSurface.SearchTypeahead) => createSearchTypeaheadInfo(ceItem, logEvent)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
private def createPushNotificationInfo(logEvent: LogEvent): Option[ProductSurfaceInfo] =
|
||||
NotificationClientEventUtils.getNotificationIdForPushNotification(logEvent) match {
|
||||
case Some(notificationId) =>
|
||||
Some(
|
||||
ProductSurfaceInfo.PushNotificationInfo(
|
||||
PushNotificationInfo(notificationId = notificationId)))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
private def createNotificationTabInfo(ceItem: LogEventItem): Option[ProductSurfaceInfo] =
|
||||
NotificationClientEventUtils.getNotificationIdForNotificationTab(ceItem) match {
|
||||
case Some(notificationId) =>
|
||||
Some(
|
||||
ProductSurfaceInfo.NotificationTabInfo(
|
||||
NotificationTabInfo(notificationId = notificationId)))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
private def createHomeTimelineInfo(ceItem: LogEventItem): Option[ProductSurfaceInfo] = {
|
||||
def suggestType: Option[String] = HomeInfoUtils.getSuggestType(ceItem)
|
||||
def controllerData: Option[V1Alias] = HomeInfoUtils.getHomeTweetControllerDataV1(ceItem)
|
||||
|
||||
if (suggestType.isDefined || controllerData.isDefined) {
|
||||
Some(
|
||||
ProductSurfaceInfo.HomeTimelineInfo(
|
||||
HomeTimelineInfo(
|
||||
suggestionType = suggestType,
|
||||
injectedPosition = controllerData.flatMap(_.injectedPosition)
|
||||
)))
|
||||
} else None
|
||||
}
|
||||
|
||||
private def createSearchResultPageInfo(
|
||||
ceItem: LogEventItem,
|
||||
logEvent: LogEvent
|
||||
): Option[ProductSurfaceInfo] = {
|
||||
val searchInfoUtil = new SearchInfoUtils(ceItem)
|
||||
searchInfoUtil.getQueryOptFromItem(logEvent).map { query =>
|
||||
ProductSurfaceInfo.SearchResultsPageInfo(
|
||||
SearchResultsPageInfo(
|
||||
query = query,
|
||||
querySource = searchInfoUtil.getQuerySourceOptFromControllerDataFromItem,
|
||||
itemPosition = ceItem.position,
|
||||
tweetResultSources = searchInfoUtil.getTweetResultSources,
|
||||
userResultSources = searchInfoUtil.getUserResultSources,
|
||||
queryFilterType = searchInfoUtil.getQueryFilterType(logEvent)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private def createSearchTypeaheadInfo(
|
||||
ceItem: LogEventItem,
|
||||
logEvent: LogEvent
|
||||
): Option[ProductSurfaceInfo] = {
|
||||
logEvent.searchDetails.flatMap(_.query).map { query =>
|
||||
ProductSurfaceInfo.SearchTypeaheadInfo(
|
||||
SearchTypeaheadInfo(
|
||||
query = query,
|
||||
itemPosition = ceItem.position
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,129 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.client_event
|
||||
|
||||
import com.twitter.clientapp.thriftscala.LogEvent
|
||||
import com.twitter.clientapp.thriftscala.{Item => LogEventItem}
|
||||
import com.twitter.search.common.constants.thriftscala.ThriftQuerySource
|
||||
import com.twitter.search.common.constants.thriftscala.TweetResultSource
|
||||
import com.twitter.search.common.constants.thriftscala.UserResultSource
|
||||
import com.twitter.suggests.controller_data.search_response.item_types.thriftscala.ItemTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.item_types.thriftscala.ItemTypesControllerData.TweetTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.item_types.thriftscala.ItemTypesControllerData.UserTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.request.thriftscala.RequestControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.thriftscala.SearchResponseControllerData.V1
|
||||
import com.twitter.suggests.controller_data.search_response.thriftscala.SearchResponseControllerDataAliases.V1Alias
|
||||
import com.twitter.suggests.controller_data.thriftscala.ControllerData.V2
|
||||
import com.twitter.suggests.controller_data.v2.thriftscala.ControllerData.SearchResponse
|
||||
import com.twitter.unified_user_actions.thriftscala.SearchQueryFilterType
|
||||
import com.twitter.unified_user_actions.thriftscala.SearchQueryFilterType._
|
||||
|
||||
class SearchInfoUtils(item: LogEventItem) {
|
||||
private val searchControllerDataOpt: Option[V1Alias] = item.suggestionDetails.flatMap { sd =>
|
||||
sd.decodedControllerData.flatMap { decodedControllerData =>
|
||||
decodedControllerData match {
|
||||
case V2(v2ControllerData) =>
|
||||
v2ControllerData match {
|
||||
case SearchResponse(searchResponseControllerData) =>
|
||||
searchResponseControllerData match {
|
||||
case V1(searchResponseControllerDataV1) =>
|
||||
Some(searchResponseControllerDataV1)
|
||||
case _ => None
|
||||
}
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val requestControllerDataOptFromItem: Option[RequestControllerData] =
|
||||
searchControllerDataOpt.flatMap { searchControllerData =>
|
||||
searchControllerData.requestControllerData
|
||||
}
|
||||
private val itemTypesControllerDataOptFromItem: Option[ItemTypesControllerData] =
|
||||
searchControllerDataOpt.flatMap { searchControllerData =>
|
||||
searchControllerData.itemTypesControllerData
|
||||
}
|
||||
|
||||
def checkBit(bitmap: Long, idx: Int): Boolean = {
|
||||
(bitmap / Math.pow(2, idx)).toInt % 2 == 1
|
||||
}
|
||||
|
||||
def getQueryOptFromSearchDetails(logEvent: LogEvent): Option[String] = {
|
||||
logEvent.searchDetails.flatMap { sd => sd.query }
|
||||
}
|
||||
|
||||
def getQueryOptFromControllerDataFromItem: Option[String] = {
|
||||
requestControllerDataOptFromItem.flatMap { rd => rd.rawQuery }
|
||||
}
|
||||
|
||||
def getQueryOptFromItem(logEvent: LogEvent): Option[String] = {
|
||||
// First we try to get the query from controller data, and if that's not available, we fall
|
||||
// back to query in search details. If both are None, queryOpt is None.
|
||||
getQueryOptFromControllerDataFromItem.orElse(getQueryOptFromSearchDetails(logEvent))
|
||||
}
|
||||
|
||||
def getTweetTypesOptFromControllerDataFromItem: Option[TweetTypesControllerData] = {
|
||||
itemTypesControllerDataOptFromItem.flatMap { itemTypes =>
|
||||
itemTypes match {
|
||||
case TweetTypesControllerData(tweetTypesControllerData) =>
|
||||
Some(TweetTypesControllerData(tweetTypesControllerData))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getUserTypesOptFromControllerDataFromItem: Option[UserTypesControllerData] = {
|
||||
itemTypesControllerDataOptFromItem.flatMap { itemTypes =>
|
||||
itemTypes match {
|
||||
case UserTypesControllerData(userTypesControllerData) =>
|
||||
Some(UserTypesControllerData(userTypesControllerData))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getQuerySourceOptFromControllerDataFromItem: Option[ThriftQuerySource] = {
|
||||
requestControllerDataOptFromItem
|
||||
.flatMap { rd => rd.querySource }
|
||||
.flatMap { querySourceVal => ThriftQuerySource.get(querySourceVal) }
|
||||
}
|
||||
|
||||
def getTweetResultSources: Option[Set[TweetResultSource]] = {
|
||||
getTweetTypesOptFromControllerDataFromItem
|
||||
.flatMap { cd => cd.tweetTypesControllerData.tweetTypesBitmap }
|
||||
.map { tweetTypesBitmap =>
|
||||
TweetResultSource.list.filter { t => checkBit(tweetTypesBitmap, t.value) }.toSet
|
||||
}
|
||||
}
|
||||
|
||||
def getUserResultSources: Option[Set[UserResultSource]] = {
|
||||
getUserTypesOptFromControllerDataFromItem
|
||||
.flatMap { cd => cd.userTypesControllerData.userTypesBitmap }
|
||||
.map { userTypesBitmap =>
|
||||
UserResultSource.list.filter { t => checkBit(userTypesBitmap, t.value) }.toSet
|
||||
}
|
||||
}
|
||||
|
||||
def getQueryFilterType(logEvent: LogEvent): Option[SearchQueryFilterType] = {
|
||||
val searchTab = logEvent.eventNamespace.map(_.client).flatMap {
|
||||
case Some("m5") | Some("android") => logEvent.eventNamespace.flatMap(_.element)
|
||||
case _ => logEvent.eventNamespace.flatMap(_.section)
|
||||
}
|
||||
searchTab.flatMap {
|
||||
case "search_filter_top" => Some(Top)
|
||||
case "search_filter_live" => Some(Latest)
|
||||
// android uses search_filter_tweets instead of search_filter_live
|
||||
case "search_filter_tweets" => Some(Latest)
|
||||
case "search_filter_user" => Some(People)
|
||||
case "search_filter_image" => Some(Photos)
|
||||
case "search_filter_video" => Some(Videos)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
def getRequestJoinId: Option[Long] = requestControllerDataOptFromItem.flatMap(_.requestJoinId)
|
||||
|
||||
def getTraceId: Option[Long] = requestControllerDataOptFromItem.flatMap(_.traceId)
|
||||
|
||||
}
|
Binary file not shown.
@ -1,157 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.client_event
|
||||
|
||||
import com.twitter.clientapp.thriftscala.EventNamespace
|
||||
import com.twitter.clientapp.thriftscala.Item
|
||||
import com.twitter.clientapp.thriftscala.ItemType.Topic
|
||||
import com.twitter.guide.scribing.thriftscala.TopicModuleMetadata
|
||||
import com.twitter.guide.scribing.thriftscala.TransparentGuideDetails
|
||||
import com.twitter.suggests.controller_data.home_hitl_topic_annotation_prompt.thriftscala.HomeHitlTopicAnnotationPromptControllerData
|
||||
import com.twitter.suggests.controller_data.home_hitl_topic_annotation_prompt.v1.thriftscala.{
|
||||
HomeHitlTopicAnnotationPromptControllerData => HomeHitlTopicAnnotationPromptControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.home_topic_annotation_prompt.thriftscala.HomeTopicAnnotationPromptControllerData
|
||||
import com.twitter.suggests.controller_data.home_topic_annotation_prompt.v1.thriftscala.{
|
||||
HomeTopicAnnotationPromptControllerData => HomeTopicAnnotationPromptControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.home_topic_follow_prompt.thriftscala.HomeTopicFollowPromptControllerData
|
||||
import com.twitter.suggests.controller_data.home_topic_follow_prompt.v1.thriftscala.{
|
||||
HomeTopicFollowPromptControllerData => HomeTopicFollowPromptControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.home_tweets.thriftscala.HomeTweetsControllerData
|
||||
import com.twitter.suggests.controller_data.home_tweets.v1.thriftscala.{
|
||||
HomeTweetsControllerData => HomeTweetsControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.search_response.item_types.thriftscala.ItemTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.thriftscala.SearchResponseControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.topic_follow_prompt.thriftscala.SearchTopicFollowPromptControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.tweet_types.thriftscala.TweetTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.v1.thriftscala.{
|
||||
SearchResponseControllerData => SearchResponseControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.thriftscala.ControllerData
|
||||
import com.twitter.suggests.controller_data.timelines_topic.thriftscala.TimelinesTopicControllerData
|
||||
import com.twitter.suggests.controller_data.timelines_topic.v1.thriftscala.{
|
||||
TimelinesTopicControllerData => TimelinesTopicControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.v2.thriftscala.{ControllerData => ControllerDataV2}
|
||||
import com.twitter.util.Try
|
||||
|
||||
object TopicIdUtils {
|
||||
val DomainId: Long = 131 // Topical Domain
|
||||
|
||||
def getTopicId(
|
||||
item: Item,
|
||||
namespace: EventNamespace
|
||||
): Option[Long] =
|
||||
getTopicIdFromHomeSearch(item)
|
||||
.orElse(getTopicFromGuide(item))
|
||||
.orElse(getTopicFromOnboarding(item, namespace))
|
||||
.orElse(getTopicIdFromItem(item))
|
||||
|
||||
def getTopicIdFromItem(item: Item): Option[Long] =
|
||||
if (item.itemType.contains(Topic))
|
||||
item.id
|
||||
else None
|
||||
|
||||
def getTopicIdFromHomeSearch(
|
||||
item: Item
|
||||
): Option[Long] = {
|
||||
val decodedControllerData = item.suggestionDetails.flatMap(_.decodedControllerData)
|
||||
decodedControllerData match {
|
||||
case Some(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTweets(
|
||||
HomeTweetsControllerData.V1(homeTweets: HomeTweetsControllerDataV1)))
|
||||
) =>
|
||||
homeTweets.topicId
|
||||
case Some(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTopicFollowPrompt(
|
||||
HomeTopicFollowPromptControllerData.V1(
|
||||
homeTopicFollowPrompt: HomeTopicFollowPromptControllerDataV1)))
|
||||
) =>
|
||||
homeTopicFollowPrompt.topicId
|
||||
case Some(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.TimelinesTopic(
|
||||
TimelinesTopicControllerData.V1(
|
||||
timelinesTopic: TimelinesTopicControllerDataV1
|
||||
)))
|
||||
) =>
|
||||
Some(timelinesTopic.topicId)
|
||||
case Some(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(s: SearchResponseControllerDataV1)))
|
||||
) =>
|
||||
s.itemTypesControllerData match {
|
||||
case Some(
|
||||
ItemTypesControllerData.TopicFollowControllerData(
|
||||
topicFollowControllerData: SearchTopicFollowPromptControllerData)) =>
|
||||
topicFollowControllerData.topicId
|
||||
case Some(
|
||||
ItemTypesControllerData.TweetTypesControllerData(
|
||||
tweetTypesControllerData: TweetTypesControllerData)) =>
|
||||
tweetTypesControllerData.topicId
|
||||
case _ => None
|
||||
}
|
||||
case Some(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTopicAnnotationPrompt(
|
||||
HomeTopicAnnotationPromptControllerData.V1(
|
||||
homeTopicAnnotationPrompt: HomeTopicAnnotationPromptControllerDataV1
|
||||
)))
|
||||
) =>
|
||||
Some(homeTopicAnnotationPrompt.topicId)
|
||||
case Some(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeHitlTopicAnnotationPrompt(
|
||||
HomeHitlTopicAnnotationPromptControllerData.V1(
|
||||
homeHitlTopicAnnotationPrompt: HomeHitlTopicAnnotationPromptControllerDataV1
|
||||
)))
|
||||
) =>
|
||||
Some(homeHitlTopicAnnotationPrompt.topicId)
|
||||
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
def getTopicFromOnboarding(
|
||||
item: Item,
|
||||
namespace: EventNamespace
|
||||
): Option[Long] =
|
||||
if (namespace.page.contains("onboarding") &&
|
||||
(namespace.section.exists(_.contains("topic")) ||
|
||||
namespace.component.exists(_.contains("topic")) ||
|
||||
namespace.element.exists(_.contains("topic")))) {
|
||||
item.description.flatMap { description =>
|
||||
// description: "id=123,main=xyz,row=1"
|
||||
val tokens = description.split(",").headOption.map(_.split("="))
|
||||
tokens match {
|
||||
case Some(Array("id", token, _*)) => Try(token.toLong).toOption
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
} else None
|
||||
|
||||
def getTopicFromGuide(
|
||||
item: Item
|
||||
): Option[Long] =
|
||||
item.guideItemDetails.flatMap {
|
||||
_.transparentGuideDetails match {
|
||||
case Some(TransparentGuideDetails.TopicMetadata(topicMetadata)) =>
|
||||
topicMetadata match {
|
||||
case TopicModuleMetadata.TttInterest(_) =>
|
||||
None
|
||||
case TopicModuleMetadata.SemanticCoreInterest(semanticCoreInterest) =>
|
||||
if (semanticCoreInterest.domainId == DomainId.toString)
|
||||
Try(semanticCoreInterest.entityId.toLong).toOption
|
||||
else None
|
||||
case TopicModuleMetadata.SimClusterInterest(_) =>
|
||||
None
|
||||
case TopicModuleMetadata.UnknownUnionField(_) => None
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,42 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.client_event
|
||||
|
||||
import com.twitter.clientapp.thriftscala.AmplifyDetails
|
||||
import com.twitter.clientapp.thriftscala.MediaDetails
|
||||
import com.twitter.unified_user_actions.thriftscala.TweetVideoWatch
|
||||
import com.twitter.unified_user_actions.thriftscala.TweetActionInfo
|
||||
import com.twitter.video.analytics.thriftscala.MediaIdentifier
|
||||
|
||||
object VideoClientEventUtils {
|
||||
|
||||
/**
|
||||
* For Tweets with multiple videos, find the id of the video that generated the client-event
|
||||
*/
|
||||
def videoIdFromMediaIdentifier(mediaIdentifier: MediaIdentifier): Option[String] =
|
||||
mediaIdentifier match {
|
||||
case MediaIdentifier.MediaPlatformIdentifier(mediaPlatformIdentifier) =>
|
||||
mediaPlatformIdentifier.mediaId.map(_.toString)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
/**
|
||||
* Given:
|
||||
* 1. the id of the video (`mediaId`)
|
||||
* 2. details about all the media items in the Tweet (`mediaItems`),
|
||||
* iterate over the `mediaItems` to lookup the metadata about the video with id `mediaId`.
|
||||
*/
|
||||
def getVideoMetadata(
|
||||
mediaId: String,
|
||||
mediaItems: Seq[MediaDetails],
|
||||
amplifyDetails: Option[AmplifyDetails]
|
||||
): Option[TweetActionInfo] = {
|
||||
mediaItems.collectFirst {
|
||||
case media if media.contentId.contains(mediaId) =>
|
||||
TweetActionInfo.TweetVideoWatch(
|
||||
TweetVideoWatch(
|
||||
mediaType = media.mediaType,
|
||||
isMonetizable = media.dynamicAds,
|
||||
videoType = amplifyDetails.flatMap(_.videoType)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,15 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.common
|
||||
|
||||
import com.twitter.snowflake.id.SnowflakeId
|
||||
import com.twitter.util.Time
|
||||
|
||||
object AdapterUtils {
|
||||
def currentTimestampMs: Long = Time.now.inMilliseconds
|
||||
def getTimestampMsFromTweetId(tweetId: Long): Long = SnowflakeId.unixTimeMillisFromId(tweetId)
|
||||
|
||||
// For now just make sure both language code and country code are in upper cases for consistency
|
||||
// For language code, there are mixed lower and upper cases
|
||||
// For country code, there are mixed lower and upper cases
|
||||
def normalizeLanguageCode(inputLanguageCode: String): String = inputLanguageCode.toUpperCase
|
||||
def normalizeCountryCode(inputCountryCode: String): String = inputCountryCode.toUpperCase
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"snowflake/src/main/scala/com/twitter/snowflake/id",
|
||||
"util/util-core:util-core-util",
|
||||
],
|
||||
)
|
Binary file not shown.
@ -1,14 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"src/thrift/com/twitter/ibis:logging-scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
@ -1,55 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.email_notification_event
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finatra.kafka.serde.UnKeyed
|
||||
import com.twitter.ibis.thriftscala.NotificationScribe
|
||||
import com.twitter.ibis.thriftscala.NotificationScribeType
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.EmailNotificationInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.ProductSurface
|
||||
import com.twitter.unified_user_actions.thriftscala.ProductSurfaceInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.TweetInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
import com.twitter.unified_user_actions.thriftscala.UserIdentifier
|
||||
|
||||
class EmailNotificationEventAdapter
|
||||
extends AbstractAdapter[NotificationScribe, UnKeyed, UnifiedUserAction] {
|
||||
import EmailNotificationEventAdapter._
|
||||
override def adaptOneToKeyedMany(
|
||||
input: NotificationScribe,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(UnKeyed, UnifiedUserAction)] =
|
||||
adaptEvent(input).map { e => (UnKeyed, e) }
|
||||
}
|
||||
|
||||
object EmailNotificationEventAdapter {
|
||||
|
||||
def adaptEvent(scribe: NotificationScribe): Seq[UnifiedUserAction] = {
|
||||
Option(scribe).flatMap { e =>
|
||||
e.`type` match {
|
||||
case NotificationScribeType.Click =>
|
||||
val tweetIdOpt = e.logBase.flatMap(EmailNotificationEventUtils.extractTweetId)
|
||||
(tweetIdOpt, e.impressionId) match {
|
||||
case (Some(tweetId), Some(impressionId)) =>
|
||||
Some(
|
||||
UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = e.userId),
|
||||
item = Item.TweetInfo(TweetInfo(actionTweetId = tweetId)),
|
||||
actionType = ActionType.ClientTweetEmailClick,
|
||||
eventMetadata = EmailNotificationEventUtils.extractEventMetaData(e),
|
||||
productSurface = Some(ProductSurface.EmailNotification),
|
||||
productSurfaceInfo = Some(
|
||||
ProductSurfaceInfo.EmailNotificationInfo(
|
||||
EmailNotificationInfo(notificationId = impressionId)))
|
||||
)
|
||||
)
|
||||
case _ => None
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}.toSeq
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,39 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.email_notification_event
|
||||
|
||||
import com.twitter.ibis.thriftscala.NotificationScribe
|
||||
import com.twitter.logbase.thriftscala.LogBase
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.SourceLineage
|
||||
|
||||
object EmailNotificationEventUtils {
|
||||
|
||||
/*
|
||||
* Extract TweetId from Logbase.page, here is a sample page below
|
||||
* https://twitter.com/i/events/1580827044245544962?cn=ZmxleGlibGVfcmVjcw%3D%3D&refsrc=email
|
||||
* */
|
||||
def extractTweetId(path: String): Option[Long] = {
|
||||
val ptn = raw".*/([0-9]+)\\??.*".r
|
||||
path match {
|
||||
case ptn(tweetId) =>
|
||||
Some(tweetId.toLong)
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def extractTweetId(logBase: LogBase): Option[Long] = logBase.page match {
|
||||
case Some(path) => extractTweetId(path)
|
||||
case None => None
|
||||
}
|
||||
|
||||
def extractEventMetaData(scribe: NotificationScribe): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = scribe.timestamp,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.EmailNotificationEvents,
|
||||
language = scribe.logBase.flatMap(_.language),
|
||||
countryCode = scribe.logBase.flatMap(_.country),
|
||||
clientAppId = scribe.logBase.flatMap(_.clientAppId),
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"fanoutservice/thrift/src/main/thrift:thrift-scala",
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
@ -1,52 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.favorite_archival_events
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finatra.kafka.serde.UnKeyed
|
||||
import com.twitter.timelineservice.fanout.thriftscala.FavoriteArchivalEvent
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
|
||||
class FavoriteArchivalEventsAdapter
|
||||
extends AbstractAdapter[FavoriteArchivalEvent, UnKeyed, UnifiedUserAction] {
|
||||
|
||||
import FavoriteArchivalEventsAdapter._
|
||||
override def adaptOneToKeyedMany(
|
||||
input: FavoriteArchivalEvent,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(UnKeyed, UnifiedUserAction)] =
|
||||
adaptEvent(input).map { e => (UnKeyed, e) }
|
||||
}
|
||||
|
||||
object FavoriteArchivalEventsAdapter {
|
||||
|
||||
def adaptEvent(e: FavoriteArchivalEvent): Seq[UnifiedUserAction] =
|
||||
Option(e).map { e =>
|
||||
UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(e.favoriterId)),
|
||||
item = getItem(e),
|
||||
actionType =
|
||||
if (e.isArchivingAction.getOrElse(true)) ActionType.ServerTweetArchiveFavorite
|
||||
else ActionType.ServerTweetUnarchiveFavorite,
|
||||
eventMetadata = getEventMetadata(e)
|
||||
)
|
||||
}.toSeq
|
||||
|
||||
def getItem(e: FavoriteArchivalEvent): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
// Please note that here we always use TweetId (not sourceTweetId)!!!
|
||||
actionTweetId = e.tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = e.tweetUserId)),
|
||||
retweetedTweetId = e.sourceTweetId
|
||||
)
|
||||
)
|
||||
|
||||
def getEventMetadata(e: FavoriteArchivalEvent): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = e.timestampMs,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerFavoriteArchivalEvents,
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"fanoutservice/thrift/src/main/thrift:thrift-scala",
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
@ -1,51 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.retweet_archival_events
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finatra.kafka.serde.UnKeyed
|
||||
import com.twitter.tweetypie.thriftscala.RetweetArchivalEvent
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
|
||||
class RetweetArchivalEventsAdapter
|
||||
extends AbstractAdapter[RetweetArchivalEvent, UnKeyed, UnifiedUserAction] {
|
||||
|
||||
import RetweetArchivalEventsAdapter._
|
||||
override def adaptOneToKeyedMany(
|
||||
input: RetweetArchivalEvent,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(UnKeyed, UnifiedUserAction)] =
|
||||
adaptEvent(input).map { e => (UnKeyed, e) }
|
||||
}
|
||||
|
||||
object RetweetArchivalEventsAdapter {
|
||||
|
||||
def adaptEvent(e: RetweetArchivalEvent): Seq[UnifiedUserAction] =
|
||||
Option(e).map { e =>
|
||||
UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(e.retweetUserId)),
|
||||
item = getItem(e),
|
||||
actionType =
|
||||
if (e.isArchivingAction.getOrElse(true)) ActionType.ServerTweetArchiveRetweet
|
||||
else ActionType.ServerTweetUnarchiveRetweet,
|
||||
eventMetadata = getEventMetadata(e)
|
||||
)
|
||||
}.toSeq
|
||||
|
||||
def getItem(e: RetweetArchivalEvent): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = e.srcTweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(e.srcTweetUserId))),
|
||||
retweetingTweetId = Some(e.retweetId)
|
||||
)
|
||||
)
|
||||
|
||||
def getEventMetadata(e: RetweetArchivalEvent): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = e.timestampMs,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerRetweetArchivalEvents,
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"src/thrift/com/twitter/socialgraph:thrift-scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
@ -1,24 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.social_graph_event
|
||||
|
||||
import com.twitter.socialgraph.thriftscala.Action
|
||||
import com.twitter.socialgraph.thriftscala.SrcTargetRequest
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.ProfileActionInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.ProfileInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.ServerProfileReport
|
||||
|
||||
abstract class BaseReportSocialGraphWriteEvent[T] extends BaseSocialGraphWriteEvent[T] {
|
||||
def socialGraphAction: Action
|
||||
|
||||
override def getSocialGraphItem(socialGraphSrcTargetRequest: SrcTargetRequest): Item = {
|
||||
Item.ProfileInfo(
|
||||
ProfileInfo(
|
||||
actionProfileId = socialGraphSrcTargetRequest.target,
|
||||
profileActionInfo = Some(
|
||||
ProfileActionInfo.ServerProfileReport(
|
||||
ServerProfileReport(reportType = socialGraphAction)
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,60 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.social_graph_event
|
||||
|
||||
import com.twitter.socialgraph.thriftscala.LogEventContext
|
||||
import com.twitter.socialgraph.thriftscala.SrcTargetRequest
|
||||
import com.twitter.socialgraph.thriftscala.WriteEvent
|
||||
import com.twitter.socialgraph.thriftscala.WriteRequestResult
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.ProfileInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.SourceLineage
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
import com.twitter.unified_user_actions.thriftscala.UserIdentifier
|
||||
|
||||
trait BaseSocialGraphWriteEvent[T] {
|
||||
def uuaActionType: ActionType
|
||||
|
||||
def getSrcTargetRequest(
|
||||
e: WriteEvent
|
||||
): Seq[SrcTargetRequest] = getSubType(e) match {
|
||||
case Some(subType: Seq[T]) =>
|
||||
getWriteRequestResultFromSubType(subType).collect {
|
||||
case r if r.validationError.isEmpty => r.request
|
||||
}
|
||||
case _ => Nil
|
||||
}
|
||||
|
||||
def getSubType(e: WriteEvent): Option[Seq[T]]
|
||||
def getWriteRequestResultFromSubType(subType: Seq[T]): Seq[WriteRequestResult]
|
||||
|
||||
def toUnifiedUserAction(
|
||||
writeEvent: WriteEvent,
|
||||
uuaAction: BaseSocialGraphWriteEvent[_]
|
||||
): Seq[UnifiedUserAction] =
|
||||
uuaAction.getSrcTargetRequest(writeEvent).map { srcTargetRequest =>
|
||||
UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = writeEvent.context.loggedInUserId),
|
||||
item = getSocialGraphItem(srcTargetRequest),
|
||||
actionType = uuaAction.uuaActionType,
|
||||
eventMetadata = getEventMetadata(writeEvent.context)
|
||||
)
|
||||
}
|
||||
|
||||
def getSocialGraphItem(socialGraphSrcTargetRequest: SrcTargetRequest): Item = {
|
||||
Item.ProfileInfo(
|
||||
ProfileInfo(
|
||||
actionProfileId = socialGraphSrcTargetRequest.target
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def getEventMetadata(context: LogEventContext): EventMetadata = {
|
||||
EventMetadata(
|
||||
sourceTimestampMs = context.timestamp,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerSocialGraphEvents,
|
||||
)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,48 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.social_graph_event
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finatra.kafka.serde.UnKeyed
|
||||
import com.twitter.socialgraph.thriftscala.Action._
|
||||
import com.twitter.socialgraph.thriftscala.WriteEvent
|
||||
import com.twitter.socialgraph.thriftscala.{Action => SocialGraphAction}
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.unified_user_actions.adapter.social_graph_event.SocialGraphEngagement._
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
|
||||
class SocialGraphAdapter extends AbstractAdapter[WriteEvent, UnKeyed, UnifiedUserAction] {
|
||||
|
||||
import SocialGraphAdapter._
|
||||
|
||||
override def adaptOneToKeyedMany(
|
||||
input: WriteEvent,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(UnKeyed, UnifiedUserAction)] =
|
||||
adaptEvent(input).map { e => (UnKeyed, e) }
|
||||
}
|
||||
|
||||
object SocialGraphAdapter {
|
||||
|
||||
def adaptEvent(writeEvent: WriteEvent): Seq[UnifiedUserAction] =
|
||||
Option(writeEvent).flatMap { e =>
|
||||
socialGraphWriteEventTypeToUuaEngagementType.get(e.action)
|
||||
} match {
|
||||
case Some(uuaAction) => uuaAction.toUnifiedUserAction(writeEvent, uuaAction)
|
||||
case None => Nil
|
||||
}
|
||||
|
||||
private val socialGraphWriteEventTypeToUuaEngagementType: Map[
|
||||
SocialGraphAction,
|
||||
BaseSocialGraphWriteEvent[_]
|
||||
] =
|
||||
Map[SocialGraphAction, BaseSocialGraphWriteEvent[_]](
|
||||
Follow -> ProfileFollow,
|
||||
Unfollow -> ProfileUnfollow,
|
||||
Block -> ProfileBlock,
|
||||
Unblock -> ProfileUnblock,
|
||||
Mute -> ProfileMute,
|
||||
Unmute -> ProfileUnmute,
|
||||
ReportAsSpam -> ProfileReportAsSpam,
|
||||
ReportAsAbuse -> ProfileReportAsAbuse
|
||||
)
|
||||
}
|
Binary file not shown.
@ -1,157 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.social_graph_event
|
||||
|
||||
import com.twitter.socialgraph.thriftscala.Action
|
||||
import com.twitter.socialgraph.thriftscala.BlockGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.FollowGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.MuteGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.ReportAsAbuseGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.ReportAsSpamGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.WriteEvent
|
||||
import com.twitter.socialgraph.thriftscala.WriteRequestResult
|
||||
import com.twitter.unified_user_actions.thriftscala.{ActionType => UuaActionType}
|
||||
|
||||
object SocialGraphEngagement {
|
||||
|
||||
/**
|
||||
* This is "Follow" event to indicate user1 follows user2 captured in ServerProfileFollow
|
||||
*/
|
||||
object ProfileFollow extends BaseSocialGraphWriteEvent[FollowGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileFollow
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[FollowGraphEvent]] =
|
||||
e.follow
|
||||
|
||||
override def getWriteRequestResultFromSubType(
|
||||
e: Seq[FollowGraphEvent]
|
||||
): Seq[WriteRequestResult] = {
|
||||
// Remove all redundant operations (FollowGraphEvent.redundantOperation == Some(true))
|
||||
e.collect {
|
||||
case fe if !fe.redundantOperation.getOrElse(false) => fe.result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is "Unfollow" event to indicate user1 unfollows user2 captured in ServerProfileUnfollow
|
||||
*
|
||||
* Both Unfollow and Follow use the struct FollowGraphEvent, but are treated in its individual case
|
||||
* class.
|
||||
*/
|
||||
object ProfileUnfollow extends BaseSocialGraphWriteEvent[FollowGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileUnfollow
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[FollowGraphEvent]] =
|
||||
e.follow
|
||||
|
||||
override def getWriteRequestResultFromSubType(
|
||||
e: Seq[FollowGraphEvent]
|
||||
): Seq[WriteRequestResult] =
|
||||
e.collect {
|
||||
case fe if !fe.redundantOperation.getOrElse(false) => fe.result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is "Block" event to indicate user1 blocks user2 captured in ServerProfileBlock
|
||||
*/
|
||||
object ProfileBlock extends BaseSocialGraphWriteEvent[BlockGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileBlock
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[BlockGraphEvent]] =
|
||||
e.block
|
||||
|
||||
override def getWriteRequestResultFromSubType(
|
||||
e: Seq[BlockGraphEvent]
|
||||
): Seq[WriteRequestResult] =
|
||||
e.map(_.result)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is "Unblock" event to indicate user1 unblocks user2 captured in ServerProfileUnblock
|
||||
*
|
||||
* Both Unblock and Block use struct BlockGraphEvent, but are treated in its individual case
|
||||
* class.
|
||||
*/
|
||||
object ProfileUnblock extends BaseSocialGraphWriteEvent[BlockGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileUnblock
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[BlockGraphEvent]] =
|
||||
e.block
|
||||
|
||||
override def getWriteRequestResultFromSubType(
|
||||
e: Seq[BlockGraphEvent]
|
||||
): Seq[WriteRequestResult] =
|
||||
e.map(_.result)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is "Mute" event to indicate user1 mutes user2 captured in ServerProfileMute
|
||||
*/
|
||||
object ProfileMute extends BaseSocialGraphWriteEvent[MuteGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileMute
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[MuteGraphEvent]] =
|
||||
e.mute
|
||||
|
||||
override def getWriteRequestResultFromSubType(e: Seq[MuteGraphEvent]): Seq[WriteRequestResult] =
|
||||
e.map(_.result)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is "Unmute" event to indicate user1 unmutes user2 captured in ServerProfileUnmute
|
||||
*
|
||||
* Both Unmute and Mute use the struct MuteGraphEvent, but are treated in its individual case
|
||||
* class.
|
||||
*/
|
||||
object ProfileUnmute extends BaseSocialGraphWriteEvent[MuteGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileUnmute
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[MuteGraphEvent]] =
|
||||
e.mute
|
||||
|
||||
override def getWriteRequestResultFromSubType(e: Seq[MuteGraphEvent]): Seq[WriteRequestResult] =
|
||||
e.map(_.result)
|
||||
}
|
||||
|
||||
object ProfileReportAsSpam extends BaseReportSocialGraphWriteEvent[ReportAsSpamGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileReport
|
||||
override def socialGraphAction: Action = Action.ReportAsSpam
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[ReportAsSpamGraphEvent]] =
|
||||
e.reportAsSpam
|
||||
|
||||
override def getWriteRequestResultFromSubType(
|
||||
e: Seq[ReportAsSpamGraphEvent]
|
||||
): Seq[WriteRequestResult] =
|
||||
e.map(_.result)
|
||||
}
|
||||
|
||||
object ProfileReportAsAbuse extends BaseReportSocialGraphWriteEvent[ReportAsAbuseGraphEvent] {
|
||||
override def uuaActionType: UuaActionType = UuaActionType.ServerProfileReport
|
||||
override def socialGraphAction: Action = Action.ReportAsAbuse
|
||||
|
||||
override def getSubType(
|
||||
e: WriteEvent
|
||||
): Option[Seq[ReportAsAbuseGraphEvent]] =
|
||||
e.reportAsAbuse
|
||||
|
||||
override def getWriteRequestResultFromSubType(
|
||||
e: Seq[ReportAsAbuseGraphEvent]
|
||||
): Seq[WriteRequestResult] =
|
||||
e.map(_.result)
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"src/thrift/com/twitter/timelineservice/server/internal:thrift-scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
@ -1,109 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.tls_favs_event
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finatra.kafka.serde.UnKeyed
|
||||
import com.twitter.timelineservice.thriftscala._
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
|
||||
class TlsFavsAdapter
|
||||
extends AbstractAdapter[ContextualizedFavoriteEvent, UnKeyed, UnifiedUserAction] {
|
||||
|
||||
import TlsFavsAdapter._
|
||||
|
||||
override def adaptOneToKeyedMany(
|
||||
input: ContextualizedFavoriteEvent,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(UnKeyed, UnifiedUserAction)] =
|
||||
adaptEvent(input).map { e => (UnKeyed, e) }
|
||||
}
|
||||
|
||||
object TlsFavsAdapter {
|
||||
|
||||
def adaptEvent(e: ContextualizedFavoriteEvent): Seq[UnifiedUserAction] =
|
||||
Option(e).flatMap { e =>
|
||||
e.event match {
|
||||
case FavoriteEventUnion.Favorite(favoriteEvent) =>
|
||||
Some(
|
||||
UnifiedUserAction(
|
||||
userIdentifier = getUserIdentifier(Left(favoriteEvent)),
|
||||
item = getFavItem(favoriteEvent),
|
||||
actionType = ActionType.ServerTweetFav,
|
||||
eventMetadata = getEventMetadata(Left(favoriteEvent), e.context),
|
||||
productSurface = None,
|
||||
productSurfaceInfo = None
|
||||
))
|
||||
|
||||
case FavoriteEventUnion.Unfavorite(unfavoriteEvent) =>
|
||||
Some(
|
||||
UnifiedUserAction(
|
||||
userIdentifier = getUserIdentifier(Right(unfavoriteEvent)),
|
||||
item = getUnfavItem(unfavoriteEvent),
|
||||
actionType = ActionType.ServerTweetUnfav,
|
||||
eventMetadata = getEventMetadata(Right(unfavoriteEvent), e.context),
|
||||
productSurface = None,
|
||||
productSurfaceInfo = None
|
||||
))
|
||||
|
||||
case _ => None
|
||||
}
|
||||
}.toSeq
|
||||
|
||||
def getFavItem(favoriteEvent: FavoriteEvent): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = favoriteEvent.tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(favoriteEvent.tweetUserId))),
|
||||
retweetingTweetId = favoriteEvent.retweetId
|
||||
)
|
||||
)
|
||||
|
||||
def getUnfavItem(unfavoriteEvent: UnfavoriteEvent): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = unfavoriteEvent.tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(unfavoriteEvent.tweetUserId))),
|
||||
retweetingTweetId = unfavoriteEvent.retweetId
|
||||
)
|
||||
)
|
||||
|
||||
def getEventMetadata(
|
||||
event: Either[FavoriteEvent, UnfavoriteEvent],
|
||||
context: LogEventContext
|
||||
): EventMetadata = {
|
||||
val sourceTimestampMs = event match {
|
||||
case Left(favoriteEvent) => favoriteEvent.eventTimeMs
|
||||
case Right(unfavoriteEvent) => unfavoriteEvent.eventTimeMs
|
||||
}
|
||||
// Client UI language, see more at http://go/languagepriority. The format should be ISO 639-1.
|
||||
val language = event match {
|
||||
case Left(favoriteEvent) => favoriteEvent.viewerContext.flatMap(_.requestLanguageCode)
|
||||
case Right(unfavoriteEvent) => unfavoriteEvent.viewerContext.flatMap(_.requestLanguageCode)
|
||||
}
|
||||
// From the request (user’s current location),
|
||||
// see https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/src/thrift/com/twitter/context/viewer.thrift?L54
|
||||
// The format should be ISO_3166-1_alpha-2.
|
||||
val countryCode = event match {
|
||||
case Left(favoriteEvent) => favoriteEvent.viewerContext.flatMap(_.requestCountryCode)
|
||||
case Right(unfavoriteEvent) => unfavoriteEvent.viewerContext.flatMap(_.requestCountryCode)
|
||||
}
|
||||
EventMetadata(
|
||||
sourceTimestampMs = sourceTimestampMs,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerTlsFavs,
|
||||
language = language.map(AdapterUtils.normalizeLanguageCode),
|
||||
countryCode = countryCode.map(AdapterUtils.normalizeCountryCode),
|
||||
traceId = Some(context.traceId),
|
||||
clientAppId = context.clientApplicationId,
|
||||
)
|
||||
}
|
||||
|
||||
// Get id of the user that took the action
|
||||
def getUserIdentifier(event: Either[FavoriteEvent, UnfavoriteEvent]): UserIdentifier =
|
||||
event match {
|
||||
case Left(favoriteEvent) => UserIdentifier(userId = Some(favoriteEvent.userId))
|
||||
case Right(unfavoriteEvent) => UserIdentifier(userId = Some(unfavoriteEvent.userId))
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"src/thrift/com/twitter/tweetypie:events-scala",
|
||||
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
@ -1,51 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.tweetypie_event
|
||||
|
||||
import com.twitter.tweetypie.thriftscala.TweetEventFlags
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
import com.twitter.unified_user_actions.thriftscala.UserIdentifier
|
||||
|
||||
/**
|
||||
* Base class for Tweetypie Tweet Event.
|
||||
* Extends this class if you need to implement the parser for a new Tweetypie Tweet Event Type.
|
||||
* @see https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/src/thrift/com/twitter/tweetypie/tweet_events.thrift?L225
|
||||
*/
|
||||
trait BaseTweetypieTweetEvent[T] {
|
||||
|
||||
/**
|
||||
* Returns an Optional UnifiedUserAction from the event.
|
||||
*/
|
||||
def getUnifiedUserAction(event: T, flags: TweetEventFlags): Option[UnifiedUserAction]
|
||||
|
||||
/**
|
||||
* Returns UnifiedUserAction.ActionType for each type of event.
|
||||
*/
|
||||
protected def actionType: ActionType
|
||||
|
||||
/**
|
||||
* Output type of the predicate. Could be an input of getItem.
|
||||
*/
|
||||
type ExtractedEvent
|
||||
|
||||
/**
|
||||
* Returns Some(ExtractedEvent) if the event is valid and None otherwise.
|
||||
*/
|
||||
protected def extract(event: T): Option[ExtractedEvent]
|
||||
|
||||
/**
|
||||
* Get the UnifiedUserAction.Item from the event.
|
||||
*/
|
||||
protected def getItem(extractedEvent: ExtractedEvent, event: T): Item
|
||||
|
||||
/**
|
||||
* Get the UnifiedUserAction.UserIdentifier from the event.
|
||||
*/
|
||||
protected def getUserIdentifier(event: T): UserIdentifier
|
||||
|
||||
/**
|
||||
* Get UnifiedUserAction.EventMetadata from the event.
|
||||
*/
|
||||
protected def getEventMetadata(event: T, flags: TweetEventFlags): EventMetadata
|
||||
}
|
Binary file not shown.
@ -1,200 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.tweetypie_event
|
||||
|
||||
import com.twitter.tweetypie.thriftscala.QuotedTweet
|
||||
import com.twitter.tweetypie.thriftscala.Share
|
||||
import com.twitter.tweetypie.thriftscala.TweetCreateEvent
|
||||
import com.twitter.tweetypie.thriftscala.TweetEventFlags
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.AuthorInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.SourceLineage
|
||||
import com.twitter.unified_user_actions.thriftscala.TweetInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
import com.twitter.unified_user_actions.thriftscala.UserIdentifier
|
||||
|
||||
/**
|
||||
* Base class for Tweetypie TweetCreateEvent including Quote, Reply, Retweet, and Create.
|
||||
*/
|
||||
trait BaseTweetypieTweetEventCreate extends BaseTweetypieTweetEvent[TweetCreateEvent] {
|
||||
type ExtractedEvent
|
||||
protected def actionType: ActionType
|
||||
|
||||
/**
|
||||
* This is the country code where actionTweetId is sent from. For the definitions,
|
||||
* check https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/src/thrift/com/twitter/tweetypie/tweet.thrift?L1001.
|
||||
*
|
||||
* UUA sets this to be consistent with IESource to meet existing use requirement.
|
||||
*
|
||||
* For ServerTweetReply/Retweet/Quote, the geo-tagging country code is not available in TweetCreatEvent.
|
||||
* Thus, user signup country is picked to meet a customer use case.
|
||||
*
|
||||
* The definition here conflicts with the intention of UUA to log the request country code
|
||||
* rather than the signup / geo-tagging country.
|
||||
*
|
||||
*/
|
||||
protected def getCountryCode(tce: TweetCreateEvent): Option[String] = {
|
||||
tce.tweet.place match {
|
||||
case Some(p) => p.countryCode
|
||||
case _ => tce.user.safety.flatMap(_.signupCountryCode)
|
||||
}
|
||||
}
|
||||
|
||||
protected def getItem(
|
||||
extractedEvent: ExtractedEvent,
|
||||
tweetCreateEvent: TweetCreateEvent
|
||||
): Item
|
||||
protected def extract(tweetCreateEvent: TweetCreateEvent): Option[ExtractedEvent]
|
||||
|
||||
def getUnifiedUserAction(
|
||||
tweetCreateEvent: TweetCreateEvent,
|
||||
tweetEventFlags: TweetEventFlags
|
||||
): Option[UnifiedUserAction] = {
|
||||
extract(tweetCreateEvent).map { extractedEvent =>
|
||||
UnifiedUserAction(
|
||||
userIdentifier = getUserIdentifier(tweetCreateEvent),
|
||||
item = getItem(extractedEvent, tweetCreateEvent),
|
||||
actionType = actionType,
|
||||
eventMetadata = getEventMetadata(tweetCreateEvent, tweetEventFlags),
|
||||
productSurface = None,
|
||||
productSurfaceInfo = None
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected def getUserIdentifier(tweetCreateEvent: TweetCreateEvent): UserIdentifier =
|
||||
UserIdentifier(userId = Some(tweetCreateEvent.user.id))
|
||||
|
||||
protected def getEventMetadata(
|
||||
tweetCreateEvent: TweetCreateEvent,
|
||||
flags: TweetEventFlags
|
||||
): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = flags.timestampMs,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerTweetypieEvents,
|
||||
traceId = None, // Currently traceId is not stored in TweetCreateEvent
|
||||
// UUA sets this to None since there is no request level language info.
|
||||
language = None,
|
||||
countryCode = getCountryCode(tweetCreateEvent),
|
||||
clientAppId = tweetCreateEvent.tweet.deviceSource.flatMap(_.clientAppId),
|
||||
clientVersion = None // Currently clientVersion is not stored in TweetCreateEvent
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UnifiedUserAction from a tweet Create.
|
||||
* Note the Create is generated when the tweet is not a Quote/Retweet/Reply.
|
||||
*/
|
||||
object TweetypieCreateEvent extends BaseTweetypieTweetEventCreate {
|
||||
type ExtractedEvent = Long
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetCreate
|
||||
override protected def extract(tweetCreateEvent: TweetCreateEvent): Option[Long] =
|
||||
Option(tweetCreateEvent.tweet.id)
|
||||
|
||||
protected def getItem(
|
||||
tweetId: Long,
|
||||
tweetCreateEvent: TweetCreateEvent
|
||||
): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(tweetCreateEvent.user.id)))
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UnifiedUserAction from a Reply.
|
||||
* Note the Reply is generated when someone is replying to a tweet.
|
||||
*/
|
||||
object TweetypieReplyEvent extends BaseTweetypieTweetEventCreate {
|
||||
case class PredicateOutput(tweetId: Long, userId: Long)
|
||||
override type ExtractedEvent = PredicateOutput
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetReply
|
||||
override protected def extract(tweetCreateEvent: TweetCreateEvent): Option[PredicateOutput] =
|
||||
tweetCreateEvent.tweet.coreData
|
||||
.flatMap(_.reply).flatMap(r =>
|
||||
r.inReplyToStatusId.map(tweetId => PredicateOutput(tweetId, r.inReplyToUserId)))
|
||||
|
||||
override protected def getItem(
|
||||
repliedTweet: PredicateOutput,
|
||||
tweetCreateEvent: TweetCreateEvent
|
||||
): Item = {
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = repliedTweet.tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(repliedTweet.userId))),
|
||||
replyingTweetId = Some(tweetCreateEvent.tweet.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UnifiedUserAction from a Quote.
|
||||
* Note the Quote is generated when someone is quoting (retweeting with comment) a tweet.
|
||||
*/
|
||||
object TweetypieQuoteEvent extends BaseTweetypieTweetEventCreate {
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetQuote
|
||||
type ExtractedEvent = QuotedTweet
|
||||
override protected def extract(tweetCreateEvent: TweetCreateEvent): Option[QuotedTweet] =
|
||||
tweetCreateEvent.tweet.quotedTweet
|
||||
|
||||
override protected def getItem(
|
||||
quotedTweet: QuotedTweet,
|
||||
tweetCreateEvent: TweetCreateEvent
|
||||
): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = quotedTweet.tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(quotedTweet.userId))),
|
||||
quotingTweetId = Some(tweetCreateEvent.tweet.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UnifiedUserAction from a Retweet.
|
||||
* Note the Retweet is generated when someone is retweeting (without comment) a tweet.
|
||||
*/
|
||||
object TweetypieRetweetEvent extends BaseTweetypieTweetEventCreate {
|
||||
override type ExtractedEvent = Share
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetRetweet
|
||||
override protected def extract(tweetCreateEvent: TweetCreateEvent): Option[Share] =
|
||||
tweetCreateEvent.tweet.coreData.flatMap(_.share)
|
||||
|
||||
override protected def getItem(share: Share, tweetCreateEvent: TweetCreateEvent): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = share.sourceStatusId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(share.sourceUserId))),
|
||||
retweetingTweetId = Some(tweetCreateEvent.tweet.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UnifiedUserAction from a TweetEdit.
|
||||
* Note the Edit is generated when someone is editing their quote or default tweet. The edit will
|
||||
* generate a new Tweet.
|
||||
*/
|
||||
object TweetypieEditEvent extends BaseTweetypieTweetEventCreate {
|
||||
override type ExtractedEvent = Long
|
||||
override protected def actionType: ActionType = ActionType.ServerTweetEdit
|
||||
override protected def extract(tweetCreateEvent: TweetCreateEvent): Option[Long] =
|
||||
TweetypieEventUtils.editedTweetIdFromTweet(tweetCreateEvent.tweet)
|
||||
|
||||
override protected def getItem(
|
||||
editedTweetId: Long,
|
||||
tweetCreateEvent: TweetCreateEvent
|
||||
): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = tweetCreateEvent.tweet.id,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(tweetCreateEvent.user.id))),
|
||||
editedTweetId = Some(editedTweetId),
|
||||
quotedTweetId = tweetCreateEvent.tweet.quotedTweet.map(_.tweetId)
|
||||
)
|
||||
)
|
||||
}
|
Binary file not shown.
@ -1,146 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.tweetypie_event
|
||||
|
||||
import com.twitter.tweetypie.thriftscala.QuotedTweet
|
||||
import com.twitter.tweetypie.thriftscala.Share
|
||||
import com.twitter.tweetypie.thriftscala.TweetDeleteEvent
|
||||
import com.twitter.tweetypie.thriftscala.TweetEventFlags
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.AuthorInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.SourceLineage
|
||||
import com.twitter.unified_user_actions.thriftscala.TweetInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
import com.twitter.unified_user_actions.thriftscala.UserIdentifier
|
||||
|
||||
trait BaseTweetypieTweetEventDelete extends BaseTweetypieTweetEvent[TweetDeleteEvent] {
|
||||
type ExtractedEvent
|
||||
protected def actionType: ActionType
|
||||
|
||||
def getUnifiedUserAction(
|
||||
tweetDeleteEvent: TweetDeleteEvent,
|
||||
tweetEventFlags: TweetEventFlags
|
||||
): Option[UnifiedUserAction] =
|
||||
extract(tweetDeleteEvent).map { extractedEvent =>
|
||||
UnifiedUserAction(
|
||||
userIdentifier = getUserIdentifier(tweetDeleteEvent),
|
||||
item = getItem(extractedEvent, tweetDeleteEvent),
|
||||
actionType = actionType,
|
||||
eventMetadata = getEventMetadata(tweetDeleteEvent, tweetEventFlags)
|
||||
)
|
||||
}
|
||||
|
||||
protected def extract(tweetDeleteEvent: TweetDeleteEvent): Option[ExtractedEvent]
|
||||
|
||||
protected def getItem(extractedEvent: ExtractedEvent, tweetDeleteEvent: TweetDeleteEvent): Item
|
||||
|
||||
protected def getUserIdentifier(tweetDeleteEvent: TweetDeleteEvent): UserIdentifier =
|
||||
UserIdentifier(userId = tweetDeleteEvent.user.map(_.id))
|
||||
|
||||
protected def getEventMetadata(
|
||||
tweetDeleteEvent: TweetDeleteEvent,
|
||||
flags: TweetEventFlags
|
||||
): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = flags.timestampMs,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerTweetypieEvents,
|
||||
traceId = None, // Currently traceId is not stored in TweetDeleteEvent.
|
||||
// UUA sets this to None since there is no request level language info.
|
||||
language = None,
|
||||
// UUA sets this to be consistent with IESource. For the definition,
|
||||
// see https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/src/thrift/com/twitter/tweetypie/tweet.thrift?L1001.
|
||||
// The definition here conflicts with the intention of UUA to log the request country code
|
||||
// rather than the signup / geo-tagging country.
|
||||
countryCode = tweetDeleteEvent.tweet.place.flatMap(_.countryCode),
|
||||
/* clientApplicationId is user's app id if the delete is initiated by a user,
|
||||
* or auditor's app id if the delete is initiated by an auditor */
|
||||
clientAppId = tweetDeleteEvent.audit.flatMap(_.clientApplicationId),
|
||||
clientVersion = None // Currently clientVersion is not stored in TweetDeleteEvent.
|
||||
)
|
||||
}
|
||||
|
||||
object TweetypieDeleteEvent extends BaseTweetypieTweetEventDelete {
|
||||
type ExtractedEvent = Long
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetDelete
|
||||
|
||||
override protected def extract(tweetDeleteEvent: TweetDeleteEvent): Option[Long] = Some(
|
||||
tweetDeleteEvent.tweet.id)
|
||||
|
||||
protected def getItem(
|
||||
tweetId: Long,
|
||||
tweetDeleteEvent: TweetDeleteEvent
|
||||
): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = tweetId,
|
||||
actionTweetAuthorInfo =
|
||||
Some(AuthorInfo(authorId = tweetDeleteEvent.tweet.coreData.map(_.userId)))
|
||||
))
|
||||
}
|
||||
|
||||
object TweetypieUnretweetEvent extends BaseTweetypieTweetEventDelete {
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetUnretweet
|
||||
|
||||
override type ExtractedEvent = Share
|
||||
|
||||
override protected def extract(tweetDeleteEvent: TweetDeleteEvent): Option[Share] =
|
||||
tweetDeleteEvent.tweet.coreData.flatMap(_.share)
|
||||
|
||||
override protected def getItem(share: Share, tweetDeleteEvent: TweetDeleteEvent): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = share.sourceStatusId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(share.sourceUserId))),
|
||||
retweetingTweetId = Some(tweetDeleteEvent.tweet.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
object TweetypieUnreplyEvent extends BaseTweetypieTweetEventDelete {
|
||||
case class PredicateOutput(tweetId: Long, userId: Long)
|
||||
|
||||
override type ExtractedEvent = PredicateOutput
|
||||
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetUnreply
|
||||
|
||||
override protected def extract(tweetDeleteEvent: TweetDeleteEvent): Option[PredicateOutput] =
|
||||
tweetDeleteEvent.tweet.coreData
|
||||
.flatMap(_.reply).flatMap(r =>
|
||||
r.inReplyToStatusId.map(tweetId => PredicateOutput(tweetId, r.inReplyToUserId)))
|
||||
|
||||
override protected def getItem(
|
||||
repliedTweet: PredicateOutput,
|
||||
tweetDeleteEvent: TweetDeleteEvent
|
||||
): Item = {
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = repliedTweet.tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(repliedTweet.userId))),
|
||||
replyingTweetId = Some(tweetDeleteEvent.tweet.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object TweetypieUnquoteEvent extends BaseTweetypieTweetEventDelete {
|
||||
override protected val actionType: ActionType = ActionType.ServerTweetUnquote
|
||||
|
||||
type ExtractedEvent = QuotedTweet
|
||||
|
||||
override protected def extract(tweetDeleteEvent: TweetDeleteEvent): Option[QuotedTweet] =
|
||||
tweetDeleteEvent.tweet.quotedTweet
|
||||
|
||||
override protected def getItem(
|
||||
quotedTweet: QuotedTweet,
|
||||
tweetDeleteEvent: TweetDeleteEvent
|
||||
): Item =
|
||||
Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = quotedTweet.tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(quotedTweet.userId))),
|
||||
quotingTweetId = Some(tweetDeleteEvent.tweet.id)
|
||||
)
|
||||
)
|
||||
}
|
Binary file not shown.
@ -1,78 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.tweetypie_event
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finatra.kafka.serde.UnKeyed
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.tweetypie.thriftscala.TweetEvent
|
||||
import com.twitter.tweetypie.thriftscala.TweetEventData
|
||||
import com.twitter.tweetypie.thriftscala.TweetCreateEvent
|
||||
import com.twitter.tweetypie.thriftscala.TweetDeleteEvent
|
||||
import com.twitter.tweetypie.thriftscala.TweetEventFlags
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
|
||||
class TweetypieEventAdapter extends AbstractAdapter[TweetEvent, UnKeyed, UnifiedUserAction] {
|
||||
import TweetypieEventAdapter._
|
||||
override def adaptOneToKeyedMany(
|
||||
tweetEvent: TweetEvent,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(UnKeyed, UnifiedUserAction)] =
|
||||
adaptEvent(tweetEvent).map(e => (UnKeyed, e))
|
||||
}
|
||||
|
||||
object TweetypieEventAdapter {
|
||||
def adaptEvent(tweetEvent: TweetEvent): Seq[UnifiedUserAction] = {
|
||||
Option(tweetEvent).flatMap { e =>
|
||||
e.data match {
|
||||
case TweetEventData.TweetCreateEvent(tweetCreateEvent: TweetCreateEvent) =>
|
||||
getUUAFromTweetCreateEvent(tweetCreateEvent, e.flags)
|
||||
case TweetEventData.TweetDeleteEvent(tweetDeleteEvent: TweetDeleteEvent) =>
|
||||
getUUAFromTweetDeleteEvent(tweetDeleteEvent, e.flags)
|
||||
case _ => None
|
||||
}
|
||||
}.toSeq
|
||||
}
|
||||
|
||||
def getUUAFromTweetCreateEvent(
|
||||
tweetCreateEvent: TweetCreateEvent,
|
||||
tweetEventFlags: TweetEventFlags
|
||||
): Option[UnifiedUserAction] = {
|
||||
val tweetTypeOpt = TweetypieEventUtils.tweetTypeFromTweet(tweetCreateEvent.tweet)
|
||||
|
||||
tweetTypeOpt.flatMap { tweetType =>
|
||||
tweetType match {
|
||||
case TweetTypeReply =>
|
||||
TweetypieReplyEvent.getUnifiedUserAction(tweetCreateEvent, tweetEventFlags)
|
||||
case TweetTypeRetweet =>
|
||||
TweetypieRetweetEvent.getUnifiedUserAction(tweetCreateEvent, tweetEventFlags)
|
||||
case TweetTypeQuote =>
|
||||
TweetypieQuoteEvent.getUnifiedUserAction(tweetCreateEvent, tweetEventFlags)
|
||||
case TweetTypeDefault =>
|
||||
TweetypieCreateEvent.getUnifiedUserAction(tweetCreateEvent, tweetEventFlags)
|
||||
case TweetTypeEdit =>
|
||||
TweetypieEditEvent.getUnifiedUserAction(tweetCreateEvent, tweetEventFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getUUAFromTweetDeleteEvent(
|
||||
tweetDeleteEvent: TweetDeleteEvent,
|
||||
tweetEventFlags: TweetEventFlags
|
||||
): Option[UnifiedUserAction] = {
|
||||
val tweetTypeOpt = TweetypieEventUtils.tweetTypeFromTweet(tweetDeleteEvent.tweet)
|
||||
|
||||
tweetTypeOpt.flatMap { tweetType =>
|
||||
tweetType match {
|
||||
case TweetTypeRetweet =>
|
||||
TweetypieUnretweetEvent.getUnifiedUserAction(tweetDeleteEvent, tweetEventFlags)
|
||||
case TweetTypeReply =>
|
||||
TweetypieUnreplyEvent.getUnifiedUserAction(tweetDeleteEvent, tweetEventFlags)
|
||||
case TweetTypeQuote =>
|
||||
TweetypieUnquoteEvent.getUnifiedUserAction(tweetDeleteEvent, tweetEventFlags)
|
||||
case TweetTypeDefault | TweetTypeEdit =>
|
||||
TweetypieDeleteEvent.getUnifiedUserAction(tweetDeleteEvent, tweetEventFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
@ -1,54 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.tweetypie_event
|
||||
|
||||
import com.twitter.tweetypie.thriftscala.EditControl
|
||||
import com.twitter.tweetypie.thriftscala.EditControlEdit
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
|
||||
sealed trait TweetypieTweetType
|
||||
object TweetTypeDefault extends TweetypieTweetType
|
||||
object TweetTypeReply extends TweetypieTweetType
|
||||
object TweetTypeRetweet extends TweetypieTweetType
|
||||
object TweetTypeQuote extends TweetypieTweetType
|
||||
object TweetTypeEdit extends TweetypieTweetType
|
||||
|
||||
object TweetypieEventUtils {
|
||||
def editedTweetIdFromTweet(tweet: Tweet): Option[Long] = tweet.editControl.flatMap {
|
||||
case EditControl.Edit(EditControlEdit(initialTweetId, _)) => Some(initialTweetId)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
def tweetTypeFromTweet(tweet: Tweet): Option[TweetypieTweetType] = {
|
||||
val data = tweet.coreData
|
||||
val inReplyingToStatusIdOpt = data.flatMap(_.reply).flatMap(_.inReplyToStatusId)
|
||||
val shareOpt = data.flatMap(_.share)
|
||||
val quotedTweetOpt = tweet.quotedTweet
|
||||
val editedTweetIdOpt = editedTweetIdFromTweet(tweet)
|
||||
|
||||
(inReplyingToStatusIdOpt, shareOpt, quotedTweetOpt, editedTweetIdOpt) match {
|
||||
// Reply
|
||||
case (Some(_), None, _, None) =>
|
||||
Some(TweetTypeReply)
|
||||
// For any kind of retweet (be it retweet of quote tweet or retweet of a regular tweet)
|
||||
// we only need to look at the `share` field
|
||||
// https://confluence.twitter.biz/pages/viewpage.action?spaceKey=CSVC&title=TweetyPie+FAQ#TweetypieFAQ-HowdoItellifaTweetisaRetweet
|
||||
case (None, Some(_), _, None) =>
|
||||
Some(TweetTypeRetweet)
|
||||
// quote
|
||||
case (None, None, Some(_), None) =>
|
||||
Some(TweetTypeQuote)
|
||||
// create
|
||||
case (None, None, None, None) =>
|
||||
Some(TweetTypeDefault)
|
||||
// edit
|
||||
case (None, None, _, Some(_)) =>
|
||||
Some(TweetTypeEdit)
|
||||
// reply and retweet shouldn't be present at the same time
|
||||
case (Some(_), Some(_), _, _) =>
|
||||
None
|
||||
// reply and edit / retweet and edit shouldn't be present at the same time
|
||||
case (Some(_), None, _, Some(_)) | (None, Some(_), _, Some(_)) =>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = [
|
||||
"bazel-compatible",
|
||||
"bazel-only",
|
||||
],
|
||||
dependencies = [
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"src/thrift/com/twitter/gizmoduck:thrift-scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
@ -1,41 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.user_modification
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finatra.kafka.serde.UnKeyed
|
||||
import com.twitter.gizmoduck.thriftscala.UserModification
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.unified_user_actions.adapter.user_modification_event.UserCreate
|
||||
import com.twitter.unified_user_actions.adapter.user_modification_event.UserUpdate
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
|
||||
class UserModificationAdapter
|
||||
extends AbstractAdapter[UserModification, UnKeyed, UnifiedUserAction] {
|
||||
|
||||
import UserModificationAdapter._
|
||||
|
||||
override def adaptOneToKeyedMany(
|
||||
input: UserModification,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(UnKeyed, UnifiedUserAction)] =
|
||||
adaptEvent(input).map { e => (UnKeyed, e) }
|
||||
}
|
||||
|
||||
object UserModificationAdapter {
|
||||
|
||||
def adaptEvent(input: UserModification): Seq[UnifiedUserAction] =
|
||||
Option(input).toSeq.flatMap { e =>
|
||||
if (e.create.isDefined) { // User create
|
||||
Some(UserCreate.getUUA(input))
|
||||
} else if (e.update.isDefined) { // User updates
|
||||
Some(UserUpdate.getUUA(input))
|
||||
} else if (e.destroy.isDefined) {
|
||||
None
|
||||
} else if (e.erase.isDefined) {
|
||||
None
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"None of the possible events is defined, there must be something with the source")
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,97 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.user_modification_event
|
||||
|
||||
import com.twitter.gizmoduck.thriftscala.UserModification
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.ProfileActionInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.ServerUserUpdate
|
||||
import com.twitter.unified_user_actions.thriftscala.ProfileInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.SourceLineage
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
import com.twitter.unified_user_actions.thriftscala.UserIdentifier
|
||||
|
||||
abstract class BaseUserModificationEvent(actionType: ActionType) {
|
||||
|
||||
def getUUA(input: UserModification): UnifiedUserAction = {
|
||||
val userIdentifier: UserIdentifier = UserIdentifier(userId = input.userId)
|
||||
|
||||
UnifiedUserAction(
|
||||
userIdentifier = userIdentifier,
|
||||
item = getItem(input),
|
||||
actionType = actionType,
|
||||
eventMetadata = getEventMetadata(input),
|
||||
)
|
||||
}
|
||||
|
||||
protected def getItem(input: UserModification): Item =
|
||||
Item.ProfileInfo(
|
||||
ProfileInfo(
|
||||
actionProfileId = input.userId
|
||||
.getOrElse(throw new IllegalArgumentException("target user_id is missing"))
|
||||
)
|
||||
)
|
||||
|
||||
protected def getEventMetadata(input: UserModification): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = input.updatedAtMsec
|
||||
.getOrElse(throw new IllegalArgumentException("timestamp is required")),
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerGizmoduckUserModificationEvents,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* When there is a new user creation event in Gizmoduck
|
||||
*/
|
||||
object UserCreate extends BaseUserModificationEvent(ActionType.ServerUserCreate) {
|
||||
override protected def getItem(input: UserModification): Item =
|
||||
Item.ProfileInfo(
|
||||
ProfileInfo(
|
||||
actionProfileId = input.create
|
||||
.map { user =>
|
||||
user.id
|
||||
}.getOrElse(throw new IllegalArgumentException("target user_id is missing")),
|
||||
name = input.create.flatMap { user =>
|
||||
user.profile.map(_.name)
|
||||
},
|
||||
handle = input.create.flatMap { user =>
|
||||
user.profile.map(_.screenName)
|
||||
},
|
||||
description = input.create.flatMap { user =>
|
||||
user.profile.map(_.description)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
override protected def getEventMetadata(input: UserModification): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = input.create
|
||||
.map { user =>
|
||||
user.updatedAtMsec
|
||||
}.getOrElse(throw new IllegalArgumentException("timestamp is required")),
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerGizmoduckUserModificationEvents,
|
||||
)
|
||||
}
|
||||
|
||||
object UserUpdate extends BaseUserModificationEvent(ActionType.ServerUserUpdate) {
|
||||
override protected def getItem(input: UserModification): Item =
|
||||
Item.ProfileInfo(
|
||||
ProfileInfo(
|
||||
actionProfileId =
|
||||
input.userId.getOrElse(throw new IllegalArgumentException("userId is required")),
|
||||
profileActionInfo = Some(
|
||||
ProfileActionInfo.ServerUserUpdate(
|
||||
ServerUserUpdate(updates = input.update.getOrElse(Nil), success = input.success)))
|
||||
)
|
||||
)
|
||||
|
||||
override protected def getEventMetadata(input: UserModification): EventMetadata =
|
||||
EventMetadata(
|
||||
sourceTimestampMs = input.updatedAtMsec.getOrElse(AdapterUtils.currentTimestampMs),
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = SourceLineage.ServerGizmoduckUserModificationEvents,
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
scala_library(
|
||||
sources = [
|
||||
"*.scala",
|
||||
],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"iesource/thrift/src/main/thrift:thrift-scala",
|
||||
"kafka/finagle-kafka/finatra-kafka/src/main/scala",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter:base",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/thrift/src/main/thrift/com/twitter/unified_user_actions:unified_user_actions-scala",
|
||||
],
|
||||
)
|
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
Currently this dir contains multiple adapters.
|
||||
The goal is similar: to generate Rekeyed (key by TweetId) `KeyedUuaTweet` events that can be
|
||||
used for View Counts (aggregation).
|
||||
|
||||
The 2 adapters:
|
||||
1. Reads from UUA-all topic
|
||||
2. Reads from InteractionEvents
|
||||
We have 2 adapters mainly because currently InteractionEvents have 10% more TweetRenderImpressions
|
||||
than what UUA has. Details can be found at https://docs.google.com/document/d/1UcEzAZ7rFrsU_6kl20R3YZ6u_Jt8PH_4-mVHWe216eM/edit#
|
||||
|
||||
It is still unclear which source should be used, but at a time there should be only one service running.
|
Binary file not shown.
Binary file not shown.
@ -1,33 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.uua_aggregates
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
|
||||
/**
|
||||
* The main purpose of the rekey adapter and the rekey service is to not break the existing
|
||||
* customers with the existing Unkeyed and also making the value as a super light-weight schema.
|
||||
* After we rekey from Unkeyed to Long (tweetId), downstream KafkaStreams can directly consume
|
||||
* without repartitioning.
|
||||
*/
|
||||
class RekeyUuaAdapter extends AbstractAdapter[UnifiedUserAction, Long, KeyedUuaTweet] {
|
||||
|
||||
import RekeyUuaAdapter._
|
||||
override def adaptOneToKeyedMany(
|
||||
input: UnifiedUserAction,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(Long, KeyedUuaTweet)] =
|
||||
adaptEvent(input).map { e => (e.tweetId, e) }
|
||||
}
|
||||
|
||||
object RekeyUuaAdapter {
|
||||
def adaptEvent(e: UnifiedUserAction): Seq[KeyedUuaTweet] =
|
||||
Option(e).flatMap { e =>
|
||||
e.actionType match {
|
||||
case ActionType.ClientTweetRenderImpression =>
|
||||
ClientTweetRenderImpressionUua.getRekeyedUUA(e)
|
||||
case _ => None
|
||||
}
|
||||
}.toSeq
|
||||
}
|
Binary file not shown.
@ -1,86 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.uua_aggregates
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.iesource.thriftscala.ClientEventContext
|
||||
import com.twitter.iesource.thriftscala.EngagingContext
|
||||
import com.twitter.unified_user_actions.adapter.AbstractAdapter
|
||||
import com.twitter.iesource.thriftscala.InteractionType
|
||||
import com.twitter.iesource.thriftscala.InteractionEvent
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.KeyedUuaTweet
|
||||
import com.twitter.unified_user_actions.thriftscala.SourceLineage
|
||||
import com.twitter.unified_user_actions.thriftscala.UserIdentifier
|
||||
|
||||
/**
|
||||
* This is to read directly from InteractionEvents
|
||||
*/
|
||||
class RekeyUuaFromInteractionEventsAdapter
|
||||
extends AbstractAdapter[InteractionEvent, Long, KeyedUuaTweet] {
|
||||
|
||||
import RekeyUuaFromInteractionEventsAdapter._
|
||||
override def adaptOneToKeyedMany(
|
||||
input: InteractionEvent,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[(Long, KeyedUuaTweet)] =
|
||||
adaptEvent(input, statsReceiver).map { e => (e.tweetId, e) }
|
||||
}
|
||||
|
||||
object RekeyUuaFromInteractionEventsAdapter {
|
||||
|
||||
def adaptEvent(
|
||||
e: InteractionEvent,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Seq[KeyedUuaTweet] =
|
||||
Option(e).flatMap { e =>
|
||||
e.interactionType.flatMap {
|
||||
case InteractionType.TweetRenderImpression if !isDetailImpression(e.engagingContext) =>
|
||||
getRekeyedUUA(
|
||||
input = e,
|
||||
actionType = ActionType.ClientTweetRenderImpression,
|
||||
sourceLineage = SourceLineage.ClientEvents,
|
||||
statsReceiver = statsReceiver)
|
||||
case _ => None
|
||||
}
|
||||
}.toSeq
|
||||
|
||||
def getRekeyedUUA(
|
||||
input: InteractionEvent,
|
||||
actionType: ActionType,
|
||||
sourceLineage: SourceLineage,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Option[KeyedUuaTweet] =
|
||||
input.engagingUserId match {
|
||||
// please see https://docs.google.com/document/d/1-fy2S-8-YMRQgEN0Sco0OLTmeOIUdqgiZ5G1KwTHt2g/edit#
|
||||
// in order to withstand of potential attacks, we filter out the logged-out users.
|
||||
// Checking user id is 0 is the reverse engineering of
|
||||
// https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/iesource/thrift/src/main/thrift/com/twitter/iesource/interaction_event.thrift?L220
|
||||
// https://sourcegraph.twitter.biz/git.twitter.biz/source/-/blob/iesource/common/src/main/scala/com/twitter/iesource/common/converters/client/LogEventConverter.scala?L198
|
||||
case 0L =>
|
||||
statsReceiver.counter("loggedOutEvents").incr()
|
||||
None
|
||||
case _ =>
|
||||
Some(
|
||||
KeyedUuaTweet(
|
||||
tweetId = input.targetId,
|
||||
actionType = actionType,
|
||||
userIdentifier = UserIdentifier(userId = Some(input.engagingUserId)),
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = input.triggeredTimestampMillis.getOrElse(input.timestampMillis),
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = sourceLineage
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
def isDetailImpression(engagingContext: EngagingContext): Boolean =
|
||||
engagingContext match {
|
||||
case EngagingContext.ClientEventContext(
|
||||
ClientEventContext(_, _, _, _, _, _, _, Some(isDetailsImpression), _)
|
||||
) if isDetailsImpression =>
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,36 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter.uua_aggregates
|
||||
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.EventMetadata
|
||||
import com.twitter.unified_user_actions.thriftscala.Item
|
||||
import com.twitter.unified_user_actions.thriftscala.KeyedUuaTweet
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
|
||||
abstract class BaseUuaAction(actionType: ActionType) {
|
||||
def getRekeyedUUA(input: UnifiedUserAction): Option[KeyedUuaTweet] =
|
||||
getTweetIdFromItem(input.item).map { tweetId =>
|
||||
KeyedUuaTweet(
|
||||
tweetId = tweetId,
|
||||
actionType = input.actionType,
|
||||
userIdentifier = input.userIdentifier,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = input.eventMetadata.sourceTimestampMs,
|
||||
receivedTimestampMs = AdapterUtils.currentTimestampMs,
|
||||
sourceLineage = input.eventMetadata.sourceLineage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
protected def getTweetIdFromItem(item: Item): Option[Long] = {
|
||||
item match {
|
||||
case Item.TweetInfo(tweetInfo) => Some(tweetInfo.actionTweetId)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When there is a new user creation event in Gizmoduck
|
||||
*/
|
||||
object ClientTweetRenderImpressionUua extends BaseUuaAction(ActionType.ClientTweetRenderImpression)
|
Binary file not shown.
@ -1,29 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.unified_user_actions.adapter.common.AdapterUtils
|
||||
import com.twitter.util.Time
|
||||
|
||||
class AdapterUtilsSpec extends Test {
|
||||
trait Fixture {
|
||||
|
||||
val frozenTime: Time = Time.fromMilliseconds(1658949273000L)
|
||||
val languageCode = "en"
|
||||
val countryCode = "us"
|
||||
}
|
||||
|
||||
test("tests") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val actual = Time.fromMilliseconds(AdapterUtils.currentTimestampMs)
|
||||
assert(frozenTime === actual)
|
||||
}
|
||||
|
||||
val actionedTweetId = 1554576940756246272L
|
||||
assert(AdapterUtils.getTimestampMsFromTweetId(actionedTweetId) === 1659474999976L)
|
||||
|
||||
assert(languageCode.toUpperCase === AdapterUtils.normalizeLanguageCode(languageCode))
|
||||
assert(countryCode.toUpperCase === AdapterUtils.normalizeCountryCode(countryCode))
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,282 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.ads.spendserver.thriftscala.SpendServerEvent
|
||||
import com.twitter.adserver.thriftscala.EngagementType
|
||||
import com.twitter.clientapp.thriftscala.AmplifyDetails
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.unified_user_actions.adapter.TestFixtures.AdsCallbackEngagementsFixture
|
||||
import com.twitter.unified_user_actions.adapter.ads_callback_engagements.AdsCallbackEngagementsAdapter
|
||||
import com.twitter.unified_user_actions.thriftscala.ActionType
|
||||
import com.twitter.unified_user_actions.thriftscala.TweetActionInfo
|
||||
import com.twitter.unified_user_actions.thriftscala.UnifiedUserAction
|
||||
import com.twitter.util.Time
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class AdsCallbackEngagementsAdapterSpec extends Test with TableDrivenPropertyChecks {
|
||||
|
||||
test("Test basic conversion for ads callback engagement type fav") {
|
||||
|
||||
new AdsCallbackEngagementsFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val events = Table(
|
||||
("inputEvent", "expectedUuaOutput"),
|
||||
( // Test with authorId
|
||||
createSpendServerEvent(EngagementType.Fav),
|
||||
Seq(
|
||||
createExpectedUua(
|
||||
ActionType.ServerPromotedTweetFav,
|
||||
createTweetInfoItem(authorInfo = Some(authorInfo)))))
|
||||
)
|
||||
forEvery(events) { (event: SpendServerEvent, expected: Seq[UnifiedUserAction]) =>
|
||||
val actual = AdsCallbackEngagementsAdapter.adaptEvent(event)
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Test basic conversion for different engagement types") {
|
||||
new AdsCallbackEngagementsFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val mappings = Table(
|
||||
("engagementType", "actionType"),
|
||||
(EngagementType.Unfav, ActionType.ServerPromotedTweetUnfav),
|
||||
(EngagementType.Reply, ActionType.ServerPromotedTweetReply),
|
||||
(EngagementType.Retweet, ActionType.ServerPromotedTweetRetweet),
|
||||
(EngagementType.Block, ActionType.ServerPromotedTweetBlockAuthor),
|
||||
(EngagementType.Unblock, ActionType.ServerPromotedTweetUnblockAuthor),
|
||||
(EngagementType.Send, ActionType.ServerPromotedTweetComposeTweet),
|
||||
(EngagementType.Detail, ActionType.ServerPromotedTweetClick),
|
||||
(EngagementType.Report, ActionType.ServerPromotedTweetReport),
|
||||
(EngagementType.Mute, ActionType.ServerPromotedTweetMuteAuthor),
|
||||
(EngagementType.ProfilePic, ActionType.ServerPromotedTweetClickProfile),
|
||||
(EngagementType.ScreenName, ActionType.ServerPromotedTweetClickProfile),
|
||||
(EngagementType.UserName, ActionType.ServerPromotedTweetClickProfile),
|
||||
(EngagementType.Hashtag, ActionType.ServerPromotedTweetClickHashtag),
|
||||
(EngagementType.CarouselSwipeNext, ActionType.ServerPromotedTweetCarouselSwipeNext),
|
||||
(
|
||||
EngagementType.CarouselSwipePrevious,
|
||||
ActionType.ServerPromotedTweetCarouselSwipePrevious),
|
||||
(EngagementType.DwellShort, ActionType.ServerPromotedTweetLingerImpressionShort),
|
||||
(EngagementType.DwellMedium, ActionType.ServerPromotedTweetLingerImpressionMedium),
|
||||
(EngagementType.DwellLong, ActionType.ServerPromotedTweetLingerImpressionLong),
|
||||
(EngagementType.DismissSpam, ActionType.ServerPromotedTweetDismissSpam),
|
||||
(EngagementType.DismissWithoutReason, ActionType.ServerPromotedTweetDismissWithoutReason),
|
||||
(EngagementType.DismissUninteresting, ActionType.ServerPromotedTweetDismissUninteresting),
|
||||
(EngagementType.DismissRepetitive, ActionType.ServerPromotedTweetDismissRepetitive),
|
||||
)
|
||||
|
||||
forEvery(mappings) { (engagementType: EngagementType, actionType: ActionType) =>
|
||||
val event = createSpendServerEvent(engagementType)
|
||||
val actual = AdsCallbackEngagementsAdapter.adaptEvent(event)
|
||||
val expected =
|
||||
Seq(createExpectedUua(actionType, createTweetInfoItem(authorInfo = Some(authorInfo))))
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Test conversion for ads callback engagement type spotlight view and click") {
|
||||
new AdsCallbackEngagementsFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val input = Table(
|
||||
("adsEngagement", "uuaAction"),
|
||||
(EngagementType.SpotlightClick, ActionType.ServerPromotedTweetClickSpotlight),
|
||||
(EngagementType.SpotlightView, ActionType.ServerPromotedTweetViewSpotlight),
|
||||
(EngagementType.TrendView, ActionType.ServerPromotedTrendView),
|
||||
(EngagementType.TrendClick, ActionType.ServerPromotedTrendClick),
|
||||
)
|
||||
forEvery(input) { (engagementType: EngagementType, actionType: ActionType) =>
|
||||
val adsEvent = createSpendServerEvent(engagementType)
|
||||
val expected = Seq(createExpectedUua(actionType, trendInfoItem))
|
||||
val actual = AdsCallbackEngagementsAdapter.adaptEvent(adsEvent)
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Test basic conversion for ads callback engagement open link with or without url") {
|
||||
new AdsCallbackEngagementsFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val input = Table(
|
||||
("url", "tweetActionInfo"),
|
||||
(Some("go/url"), openLinkWithUrl),
|
||||
(None, openLinkWithoutUrl)
|
||||
)
|
||||
|
||||
forEvery(input) { (url: Option[String], tweetActionInfo: TweetActionInfo) =>
|
||||
val event = createSpendServerEvent(engagementType = EngagementType.Url, url = url)
|
||||
val actual = AdsCallbackEngagementsAdapter.adaptEvent(event)
|
||||
val expected = Seq(createExpectedUua(
|
||||
ActionType.ServerPromotedTweetOpenLink,
|
||||
createTweetInfoItem(authorInfo = Some(authorInfo), actionInfo = Some(tweetActionInfo))))
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Test basic conversion for different engagement types with profile info") {
|
||||
new AdsCallbackEngagementsFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val mappings = Table(
|
||||
("engagementType", "actionType"),
|
||||
(EngagementType.Follow, ActionType.ServerPromotedProfileFollow),
|
||||
(EngagementType.Unfollow, ActionType.ServerPromotedProfileUnfollow)
|
||||
)
|
||||
forEvery(mappings) { (engagementType: EngagementType, actionType: ActionType) =>
|
||||
val event = createSpendServerEvent(engagementType)
|
||||
val actual = AdsCallbackEngagementsAdapter.adaptEvent(event)
|
||||
val expected = Seq(createExpectedUuaWithProfileInfo(actionType))
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Test basic conversion for ads callback engagement type video_content_*") {
|
||||
new AdsCallbackEngagementsFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val events = Table(
|
||||
("engagementType", "amplifyDetails", "actionType", "tweetActionInfo"),
|
||||
//For video_content_* events on promoted tweets when there is no preroll ad played
|
||||
(
|
||||
EngagementType.VideoContentPlayback25,
|
||||
amplifyDetailsPromotedTweetWithoutAd,
|
||||
ActionType.ServerPromotedTweetVideoPlayback25,
|
||||
tweetActionInfoPromotedTweetWithoutAd),
|
||||
(
|
||||
EngagementType.VideoContentPlayback50,
|
||||
amplifyDetailsPromotedTweetWithoutAd,
|
||||
ActionType.ServerPromotedTweetVideoPlayback50,
|
||||
tweetActionInfoPromotedTweetWithoutAd),
|
||||
(
|
||||
EngagementType.VideoContentPlayback75,
|
||||
amplifyDetailsPromotedTweetWithoutAd,
|
||||
ActionType.ServerPromotedTweetVideoPlayback75,
|
||||
tweetActionInfoPromotedTweetWithoutAd),
|
||||
//For video_content_* events on promoted tweets when there is a preroll ad
|
||||
(
|
||||
EngagementType.VideoContentPlayback25,
|
||||
amplifyDetailsPromotedTweetWithAd,
|
||||
ActionType.ServerPromotedTweetVideoPlayback25,
|
||||
tweetActionInfoPromotedTweetWithAd),
|
||||
(
|
||||
EngagementType.VideoContentPlayback50,
|
||||
amplifyDetailsPromotedTweetWithAd,
|
||||
ActionType.ServerPromotedTweetVideoPlayback50,
|
||||
tweetActionInfoPromotedTweetWithAd),
|
||||
(
|
||||
EngagementType.VideoContentPlayback75,
|
||||
amplifyDetailsPromotedTweetWithAd,
|
||||
ActionType.ServerPromotedTweetVideoPlayback75,
|
||||
tweetActionInfoPromotedTweetWithAd),
|
||||
)
|
||||
forEvery(events) {
|
||||
(
|
||||
engagementType: EngagementType,
|
||||
amplifyDetails: Option[AmplifyDetails],
|
||||
actionType: ActionType,
|
||||
actionInfo: Option[TweetActionInfo]
|
||||
) =>
|
||||
val spendEvent =
|
||||
createVideoSpendServerEvent(engagementType, amplifyDetails, promotedTweetId, None)
|
||||
val expected = Seq(createExpectedVideoUua(actionType, actionInfo, promotedTweetId))
|
||||
|
||||
val actual = AdsCallbackEngagementsAdapter.adaptEvent(spendEvent)
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Test basic conversion for ads callback engagement type video_ad_*") {
|
||||
|
||||
new AdsCallbackEngagementsFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val events = Table(
|
||||
(
|
||||
"engagementType",
|
||||
"amplifyDetails",
|
||||
"actionType",
|
||||
"tweetActionInfo",
|
||||
"promotedTweetId",
|
||||
"organicTweetId"),
|
||||
//For video_ad_* events when the preroll ad is on a promoted tweet.
|
||||
(
|
||||
EngagementType.VideoAdPlayback25,
|
||||
amplifyDetailsPrerollAd,
|
||||
ActionType.ServerPromotedTweetVideoAdPlayback25,
|
||||
tweetActionInfoPrerollAd,
|
||||
promotedTweetId,
|
||||
None
|
||||
),
|
||||
(
|
||||
EngagementType.VideoAdPlayback50,
|
||||
amplifyDetailsPrerollAd,
|
||||
ActionType.ServerPromotedTweetVideoAdPlayback50,
|
||||
tweetActionInfoPrerollAd,
|
||||
promotedTweetId,
|
||||
None
|
||||
),
|
||||
(
|
||||
EngagementType.VideoAdPlayback75,
|
||||
amplifyDetailsPrerollAd,
|
||||
ActionType.ServerPromotedTweetVideoAdPlayback75,
|
||||
tweetActionInfoPrerollAd,
|
||||
promotedTweetId,
|
||||
None
|
||||
),
|
||||
// For video_ad_* events when the preroll ad is on an organic tweet.
|
||||
(
|
||||
EngagementType.VideoAdPlayback25,
|
||||
amplifyDetailsPrerollAd,
|
||||
ActionType.ServerTweetVideoAdPlayback25,
|
||||
tweetActionInfoPrerollAd,
|
||||
None,
|
||||
organicTweetId
|
||||
),
|
||||
(
|
||||
EngagementType.VideoAdPlayback50,
|
||||
amplifyDetailsPrerollAd,
|
||||
ActionType.ServerTweetVideoAdPlayback50,
|
||||
tweetActionInfoPrerollAd,
|
||||
None,
|
||||
organicTweetId
|
||||
),
|
||||
(
|
||||
EngagementType.VideoAdPlayback75,
|
||||
amplifyDetailsPrerollAd,
|
||||
ActionType.ServerTweetVideoAdPlayback75,
|
||||
tweetActionInfoPrerollAd,
|
||||
None,
|
||||
organicTweetId
|
||||
),
|
||||
)
|
||||
forEvery(events) {
|
||||
(
|
||||
engagementType: EngagementType,
|
||||
amplifyDetails: Option[AmplifyDetails],
|
||||
actionType: ActionType,
|
||||
actionInfo: Option[TweetActionInfo],
|
||||
promotedTweetId: Option[Long],
|
||||
organicTweetId: Option[Long],
|
||||
) =>
|
||||
val spendEvent =
|
||||
createVideoSpendServerEvent(
|
||||
engagementType,
|
||||
amplifyDetails,
|
||||
promotedTweetId,
|
||||
organicTweetId)
|
||||
val actionTweetId = if (organicTweetId.isDefined) organicTweetId else promotedTweetId
|
||||
val expected = Seq(createExpectedVideoUua(actionType, actionInfo, actionTweetId))
|
||||
|
||||
val actual = AdsCallbackEngagementsAdapter.adaptEvent(spendEvent)
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
junit_tests(
|
||||
sources = ["**/*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/junit",
|
||||
"3rdparty/jvm/org/scalatest",
|
||||
"3rdparty/jvm/org/scalatestplus:junit",
|
||||
"finatra/inject/inject-core/src/test/scala:test-deps",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/ads_callback_engagements",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/client_event",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/common",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/email_notification_event",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/favorite_archival_events",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/retweet_archival_events",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/social_graph_event",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/tls_favs_event",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/tweetypie_event",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/user_modification_event",
|
||||
"unified_user_actions/adapter/src/main/scala/com/twitter/unified_user_actions/adapter/uua_aggregates",
|
||||
"util/util-mock/src/main/scala/com/twitter/util/mock",
|
||||
],
|
||||
)
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,20 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.unified_user_actions.adapter.TestFixtures.EmailNotificationEventFixture
|
||||
import com.twitter.unified_user_actions.adapter.email_notification_event.EmailNotificationEventAdapter
|
||||
import com.twitter.util.Time
|
||||
|
||||
class EmailNotificationEventAdapterSpec extends Test {
|
||||
|
||||
test("Notifications with click event") {
|
||||
new EmailNotificationEventFixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val actual = EmailNotificationEventAdapter.adaptEvent(notificationEvent)
|
||||
assert(expectedUua == actual.head)
|
||||
assert(EmailNotificationEventAdapter.adaptEvent(notificationEventWOTweetId).isEmpty)
|
||||
assert(EmailNotificationEventAdapter.adaptEvent(notificationEventWOImpressionId).isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,32 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.unified_user_actions.adapter.TestFixtures.EmailNotificationEventFixture
|
||||
import com.twitter.unified_user_actions.adapter.email_notification_event.EmailNotificationEventUtils
|
||||
|
||||
class EmailNotificationEventUtilsSpec extends Test {
|
||||
|
||||
test("Extract TweetId from pageUrl") {
|
||||
new EmailNotificationEventFixture {
|
||||
|
||||
val invalidUrls: Seq[String] =
|
||||
List("", "abc.com/what/not?x=y", "?abc=def", "12345/", "12345/?")
|
||||
val invalidDomain = "https://twitter.app.link/addressbook/"
|
||||
val numericHandle =
|
||||
"https://twitter.com/1234/status/12345?cxt=HBwWgsDTgY3tp&cn=ZmxleGl&refsrc=email)"
|
||||
|
||||
assert(EmailNotificationEventUtils.extractTweetId(pageUrlStatus).contains(tweetIdStatus))
|
||||
assert(EmailNotificationEventUtils.extractTweetId(pageUrlEvent).contains(tweetIdEvent))
|
||||
assert(EmailNotificationEventUtils.extractTweetId(pageUrlNoArgs).contains(tweetIdNoArgs))
|
||||
assert(EmailNotificationEventUtils.extractTweetId(invalidDomain).isEmpty)
|
||||
assert(EmailNotificationEventUtils.extractTweetId(numericHandle).contains(12345L))
|
||||
invalidUrls.foreach(url => assert(EmailNotificationEventUtils.extractTweetId(url).isEmpty))
|
||||
}
|
||||
}
|
||||
|
||||
test("Extract TweetId from LogBase") {
|
||||
new EmailNotificationEventFixture {
|
||||
assert(EmailNotificationEventUtils.extractTweetId(logBase1).contains(tweetIdStatus))
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,132 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.timelineservice.fanout.thriftscala.FavoriteArchivalEvent
|
||||
import com.twitter.unified_user_actions.adapter.favorite_archival_events.FavoriteArchivalEventsAdapter
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
import com.twitter.util.Time
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class FavoriteArchivalEventsAdapterSpec extends Test with TableDrivenPropertyChecks {
|
||||
trait Fixture {
|
||||
|
||||
val frozenTime = Time.fromMilliseconds(1658949273000L)
|
||||
|
||||
val userId = 1L
|
||||
val authorId = 2L
|
||||
val tweetId = 101L
|
||||
val retweetId = 102L
|
||||
|
||||
val favArchivalEventNoRetweet = FavoriteArchivalEvent(
|
||||
favoriterId = userId,
|
||||
tweetId = tweetId,
|
||||
timestampMs = 0L,
|
||||
isArchivingAction = Some(true),
|
||||
tweetUserId = Some(authorId)
|
||||
)
|
||||
val favArchivalEventRetweet = FavoriteArchivalEvent(
|
||||
favoriterId = userId,
|
||||
tweetId = retweetId,
|
||||
timestampMs = 0L,
|
||||
isArchivingAction = Some(true),
|
||||
tweetUserId = Some(authorId),
|
||||
sourceTweetId = Some(tweetId)
|
||||
)
|
||||
val favUnarchivalEventNoRetweet = FavoriteArchivalEvent(
|
||||
favoriterId = userId,
|
||||
tweetId = tweetId,
|
||||
timestampMs = 0L,
|
||||
isArchivingAction = Some(false),
|
||||
tweetUserId = Some(authorId)
|
||||
)
|
||||
val favUnarchivalEventRetweet = FavoriteArchivalEvent(
|
||||
favoriterId = userId,
|
||||
tweetId = retweetId,
|
||||
timestampMs = 0L,
|
||||
isArchivingAction = Some(false),
|
||||
tweetUserId = Some(authorId),
|
||||
sourceTweetId = Some(tweetId)
|
||||
)
|
||||
|
||||
val expectedUua1 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(userId)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(authorId))),
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetArchiveFavorite,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 0L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerFavoriteArchivalEvents,
|
||||
)
|
||||
)
|
||||
val expectedUua2 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(userId)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = retweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(authorId))),
|
||||
retweetedTweetId = Some(tweetId)
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetArchiveFavorite,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 0L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerFavoriteArchivalEvents,
|
||||
)
|
||||
)
|
||||
val expectedUua3 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(userId)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(authorId))),
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetUnarchiveFavorite,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 0L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerFavoriteArchivalEvents,
|
||||
)
|
||||
)
|
||||
val expectedUua4 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(userId)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = retweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(authorId))),
|
||||
retweetedTweetId = Some(tweetId)
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetUnarchiveFavorite,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 0L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerFavoriteArchivalEvents,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
test("all tests") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val table = Table(
|
||||
("event", "expected"),
|
||||
(favArchivalEventNoRetweet, expectedUua1),
|
||||
(favArchivalEventRetweet, expectedUua2),
|
||||
(favUnarchivalEventNoRetweet, expectedUua3),
|
||||
(favUnarchivalEventRetweet, expectedUua4)
|
||||
)
|
||||
forEvery(table) { (event: FavoriteArchivalEvent, expected: UnifiedUserAction) =>
|
||||
val actual = FavoriteArchivalEventsAdapter.adaptEvent(event)
|
||||
assert(Seq(expected) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,36 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.unified_user_actions.adapter.TestFixtures.InteractionEventsFixtures
|
||||
import com.twitter.unified_user_actions.adapter.uua_aggregates.RekeyUuaFromInteractionEventsAdapter
|
||||
import com.twitter.util.Time
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class RekeyUuaFromInteractionEventsAdapterSpec extends Test with TableDrivenPropertyChecks {
|
||||
test("ClientTweetRenderImpressions") {
|
||||
new InteractionEventsFixtures {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
assert(
|
||||
RekeyUuaFromInteractionEventsAdapter.adaptEvent(baseInteractionEvent) === Seq(
|
||||
expectedBaseKeyedUuaTweet))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Filter out logged out users") {
|
||||
new InteractionEventsFixtures {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
assert(RekeyUuaFromInteractionEventsAdapter.adaptEvent(loggedOutInteractionEvent) === Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Filter out detail impressions") {
|
||||
new InteractionEventsFixtures {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
assert(
|
||||
RekeyUuaFromInteractionEventsAdapter.adaptEvent(detailImpressionInteractionEvent) === Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,86 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.tweetypie.thriftscala.RetweetArchivalEvent
|
||||
import com.twitter.unified_user_actions.adapter.retweet_archival_events.RetweetArchivalEventsAdapter
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
import com.twitter.util.Time
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class RetweetArchivalEventsAdapterSpec extends Test with TableDrivenPropertyChecks {
|
||||
trait Fixture {
|
||||
|
||||
val frozenTime = Time.fromMilliseconds(1658949273000L)
|
||||
|
||||
val authorId = 1L
|
||||
val tweetId = 101L
|
||||
val retweetId = 102L
|
||||
val retweetAuthorId = 2L
|
||||
|
||||
val retweetArchivalEvent = RetweetArchivalEvent(
|
||||
retweetId = retweetId,
|
||||
srcTweetId = tweetId,
|
||||
retweetUserId = retweetAuthorId,
|
||||
srcTweetUserId = authorId,
|
||||
timestampMs = 0L,
|
||||
isArchivingAction = Some(true),
|
||||
)
|
||||
val retweetUnarchivalEvent = RetweetArchivalEvent(
|
||||
retweetId = retweetId,
|
||||
srcTweetId = tweetId,
|
||||
retweetUserId = retweetAuthorId,
|
||||
srcTweetUserId = authorId,
|
||||
timestampMs = 0L,
|
||||
isArchivingAction = Some(false),
|
||||
)
|
||||
|
||||
val expectedUua1 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(retweetAuthorId)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(authorId))),
|
||||
retweetingTweetId = Some(retweetId)
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetArchiveRetweet,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 0L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerRetweetArchivalEvents,
|
||||
)
|
||||
)
|
||||
val expectedUua2 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(retweetAuthorId)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = tweetId,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(authorId))),
|
||||
retweetingTweetId = Some(retweetId)
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetUnarchiveRetweet,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 0L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerRetweetArchivalEvents,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
test("all tests") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val table = Table(
|
||||
("event", "expected"),
|
||||
(retweetArchivalEvent, expectedUua1),
|
||||
(retweetUnarchivalEvent, expectedUua2),
|
||||
)
|
||||
forEvery(table) { (event: RetweetArchivalEvent, expected: UnifiedUserAction) =>
|
||||
val actual = RetweetArchivalEventsAdapter.adaptEvent(event)
|
||||
assert(Seq(expected) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,355 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.clientapp.thriftscala.SuggestionDetails
|
||||
import com.twitter.clientapp.thriftscala._
|
||||
import com.twitter.search.common.constants.thriftscala.ThriftQuerySource
|
||||
import com.twitter.search.common.constants.thriftscala.TweetResultSource
|
||||
import com.twitter.search.common.constants.thriftscala.UserResultSource
|
||||
import com.twitter.suggests.controller_data.search_response.item_types.thriftscala.ItemTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.request.thriftscala.RequestControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.thriftscala.SearchResponseControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.tweet_types.thriftscala.TweetTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.user_types.thriftscala.UserTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.v1.thriftscala.{
|
||||
SearchResponseControllerData => SearchResponseControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.thriftscala.ControllerData
|
||||
import com.twitter.suggests.controller_data.v2.thriftscala.{ControllerData => ControllerDataV2}
|
||||
import com.twitter.util.mock.Mockito
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
import org.scalatestplus.junit.JUnitRunner
|
||||
import com.twitter.unified_user_actions.adapter.client_event.SearchInfoUtils
|
||||
import com.twitter.unified_user_actions.thriftscala.SearchQueryFilterType
|
||||
import com.twitter.unified_user_actions.thriftscala.SearchQueryFilterType._
|
||||
import org.scalatest.prop.TableFor2
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class SearchInfoUtilsSpec
|
||||
extends AnyFunSuite
|
||||
with Matchers
|
||||
with Mockito
|
||||
with TableDrivenPropertyChecks {
|
||||
|
||||
trait Fixture {
|
||||
def mkControllerData(
|
||||
queryOpt: Option[String],
|
||||
querySourceOpt: Option[Int] = None,
|
||||
traceId: Option[Long] = None,
|
||||
requestJoinId: Option[Long] = None
|
||||
): ControllerData = {
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(
|
||||
SearchResponseControllerDataV1(requestControllerData = Some(
|
||||
RequestControllerData(
|
||||
rawQuery = queryOpt,
|
||||
querySource = querySourceOpt,
|
||||
traceId = traceId,
|
||||
requestJoinId = requestJoinId
|
||||
)))
|
||||
)))
|
||||
}
|
||||
|
||||
def mkTweetTypeControllerData(bitmap: Long, topicId: Option[Long] = None): ControllerData.V2 = {
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(
|
||||
SearchResponseControllerDataV1(itemTypesControllerData = Some(
|
||||
ItemTypesControllerData.TweetTypesControllerData(
|
||||
TweetTypesControllerData(
|
||||
tweetTypesBitmap = Some(bitmap),
|
||||
topicId = topicId
|
||||
))
|
||||
))
|
||||
)))
|
||||
}
|
||||
|
||||
def mkUserTypeControllerData(bitmap: Long): ControllerData.V2 = {
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(
|
||||
SearchResponseControllerDataV1(itemTypesControllerData = Some(
|
||||
ItemTypesControllerData.UserTypesControllerData(UserTypesControllerData(
|
||||
userTypesBitmap = Some(bitmap)
|
||||
))
|
||||
))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
test("getQueryOptFromControllerDataFromItem should return query if present in controller data") {
|
||||
new Fixture {
|
||||
|
||||
val controllerData: ControllerData = mkControllerData(Some("twitter"))
|
||||
val suggestionDetails: SuggestionDetails =
|
||||
SuggestionDetails(decodedControllerData = Some(controllerData))
|
||||
val item: Item = Item(suggestionDetails = Some(suggestionDetails))
|
||||
val result: Option[String] = new SearchInfoUtils(item).getQueryOptFromControllerDataFromItem
|
||||
result shouldEqual Option("twitter")
|
||||
}
|
||||
}
|
||||
|
||||
test("getRequestJoinId should return requestJoinId if present in controller data") {
|
||||
new Fixture {
|
||||
|
||||
val controllerData: ControllerData = mkControllerData(
|
||||
Some("twitter"),
|
||||
traceId = Some(11L),
|
||||
requestJoinId = Some(12L)
|
||||
)
|
||||
val suggestionDetails: SuggestionDetails =
|
||||
SuggestionDetails(decodedControllerData = Some(controllerData))
|
||||
val item: Item = Item(suggestionDetails = Some(suggestionDetails))
|
||||
val infoUtils = new SearchInfoUtils(item)
|
||||
infoUtils.getTraceId shouldEqual Some(11L)
|
||||
infoUtils.getRequestJoinId shouldEqual Some(12L)
|
||||
}
|
||||
}
|
||||
|
||||
test("getQueryOptFromControllerDataFromItem should return None if no suggestion details") {
|
||||
new Fixture {
|
||||
|
||||
val suggestionDetails: SuggestionDetails = SuggestionDetails()
|
||||
val item: Item = Item(suggestionDetails = Some(suggestionDetails))
|
||||
val result: Option[String] = new SearchInfoUtils(item).getQueryOptFromControllerDataFromItem
|
||||
result shouldEqual None
|
||||
}
|
||||
}
|
||||
|
||||
test("getQueryOptFromSearchDetails should return query if present") {
|
||||
new Fixture {
|
||||
|
||||
val searchDetails: SearchDetails = SearchDetails(query = Some("twitter"))
|
||||
val result: Option[String] = new SearchInfoUtils(Item()).getQueryOptFromSearchDetails(
|
||||
LogEvent(eventName = "", searchDetails = Some(searchDetails))
|
||||
)
|
||||
result shouldEqual Option("twitter")
|
||||
}
|
||||
}
|
||||
|
||||
test("getQueryOptFromSearchDetails should return None if not present") {
|
||||
new Fixture {
|
||||
|
||||
val searchDetails: SearchDetails = SearchDetails()
|
||||
val result: Option[String] = new SearchInfoUtils(Item()).getQueryOptFromSearchDetails(
|
||||
LogEvent(eventName = "", searchDetails = Some(searchDetails))
|
||||
)
|
||||
result shouldEqual None
|
||||
}
|
||||
}
|
||||
|
||||
test("getQuerySourceOptFromControllerDataFromItem should return QuerySource if present") {
|
||||
new Fixture {
|
||||
|
||||
// 1 is Typed Query
|
||||
val controllerData: ControllerData = mkControllerData(Some("twitter"), Some(1))
|
||||
|
||||
val item: Item = Item(
|
||||
suggestionDetails = Some(
|
||||
SuggestionDetails(
|
||||
decodedControllerData = Some(controllerData)
|
||||
))
|
||||
)
|
||||
new SearchInfoUtils(item).getQuerySourceOptFromControllerDataFromItem shouldEqual Some(
|
||||
ThriftQuerySource.TypedQuery)
|
||||
}
|
||||
}
|
||||
|
||||
test("getQuerySourceOptFromControllerDataFromItem should return None if not present") {
|
||||
new Fixture {
|
||||
|
||||
val controllerData: ControllerData = mkControllerData(Some("twitter"), None)
|
||||
|
||||
val item: Item = Item(
|
||||
suggestionDetails = Some(
|
||||
SuggestionDetails(
|
||||
decodedControllerData = Some(controllerData)
|
||||
))
|
||||
)
|
||||
new SearchInfoUtils(item).getQuerySourceOptFromControllerDataFromItem shouldEqual None
|
||||
}
|
||||
}
|
||||
|
||||
test("Decoding Tweet Result Sources bitmap") {
|
||||
new Fixture {
|
||||
|
||||
TweetResultSource.list
|
||||
.foreach { tweetResultSource =>
|
||||
val bitmap = (1 << tweetResultSource.getValue()).toLong
|
||||
val controllerData = mkTweetTypeControllerData(bitmap)
|
||||
|
||||
val item = Item(
|
||||
suggestionDetails = Some(
|
||||
SuggestionDetails(
|
||||
decodedControllerData = Some(controllerData)
|
||||
))
|
||||
)
|
||||
|
||||
val result = new SearchInfoUtils(item).getTweetResultSources
|
||||
result shouldEqual Some(Set(tweetResultSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Decoding multiple Tweet Result Sources") {
|
||||
new Fixture {
|
||||
|
||||
val tweetResultSources: Set[TweetResultSource] =
|
||||
Set(TweetResultSource.QueryInteractionGraph, TweetResultSource.QueryExpansion)
|
||||
val bitmap: Long = tweetResultSources.foldLeft(0L) {
|
||||
case (acc, source) => acc + (1 << source.getValue())
|
||||
}
|
||||
|
||||
val controllerData: ControllerData.V2 = mkTweetTypeControllerData(bitmap)
|
||||
|
||||
val item: Item = Item(
|
||||
suggestionDetails = Some(
|
||||
SuggestionDetails(
|
||||
decodedControllerData = Some(controllerData)
|
||||
))
|
||||
)
|
||||
|
||||
val result: Option[Set[TweetResultSource]] = new SearchInfoUtils(item).getTweetResultSources
|
||||
result shouldEqual Some(tweetResultSources)
|
||||
}
|
||||
}
|
||||
|
||||
test("Decoding User Result Sources bitmap") {
|
||||
new Fixture {
|
||||
|
||||
UserResultSource.list
|
||||
.foreach { userResultSource =>
|
||||
val bitmap = (1 << userResultSource.getValue()).toLong
|
||||
val controllerData = mkUserTypeControllerData(bitmap)
|
||||
|
||||
val item = Item(
|
||||
suggestionDetails = Some(
|
||||
SuggestionDetails(
|
||||
decodedControllerData = Some(controllerData)
|
||||
))
|
||||
)
|
||||
|
||||
val result = new SearchInfoUtils(item).getUserResultSources
|
||||
result shouldEqual Some(Set(userResultSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Decoding multiple User Result Sources") {
|
||||
new Fixture {
|
||||
|
||||
val userResultSources: Set[UserResultSource] =
|
||||
Set(UserResultSource.QueryInteractionGraph, UserResultSource.ExpertSearch)
|
||||
val bitmap: Long = userResultSources.foldLeft(0L) {
|
||||
case (acc, source) => acc + (1 << source.getValue())
|
||||
}
|
||||
|
||||
val controllerData: ControllerData.V2 = mkUserTypeControllerData(bitmap)
|
||||
|
||||
val item: Item = Item(
|
||||
suggestionDetails = Some(
|
||||
SuggestionDetails(
|
||||
decodedControllerData = Some(controllerData)
|
||||
))
|
||||
)
|
||||
|
||||
val result: Option[Set[UserResultSource]] = new SearchInfoUtils(item).getUserResultSources
|
||||
result shouldEqual Some(userResultSources)
|
||||
}
|
||||
}
|
||||
|
||||
test("getQueryFilterTabType should return correct query filter type") {
|
||||
new Fixture {
|
||||
val infoUtils = new SearchInfoUtils(Item())
|
||||
val eventsToBeChecked: TableFor2[Option[EventNamespace], Option[SearchQueryFilterType]] =
|
||||
Table(
|
||||
("eventNamespace", "queryFilterType"),
|
||||
(
|
||||
Some(EventNamespace(client = Some("m5"), element = Some("search_filter_top"))),
|
||||
Some(Top)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("m5"), element = Some("search_filter_live"))),
|
||||
Some(Latest)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("m5"), element = Some("search_filter_user"))),
|
||||
Some(People)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("m5"), element = Some("search_filter_image"))),
|
||||
Some(Photos)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("m5"), element = Some("search_filter_video"))),
|
||||
Some(Videos)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("m5"), section = Some("search_filter_top"))),
|
||||
None
|
||||
), // if client is web, element determines the query filter hence None if element is None
|
||||
(
|
||||
Some(EventNamespace(client = Some("android"), element = Some("search_filter_top"))),
|
||||
Some(Top)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("android"), element = Some("search_filter_tweets"))),
|
||||
Some(Latest)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("android"), element = Some("search_filter_user"))),
|
||||
Some(People)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("android"), element = Some("search_filter_image"))),
|
||||
Some(Photos)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("android"), element = Some("search_filter_video"))),
|
||||
Some(Videos)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("m5"), section = Some("search_filter_top"))),
|
||||
None
|
||||
), // if client is android, element determines the query filter hence None if element is None
|
||||
(
|
||||
Some(EventNamespace(client = Some("iphone"), section = Some("search_filter_top"))),
|
||||
Some(Top)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("iphone"), section = Some("search_filter_live"))),
|
||||
Some(Latest)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("iphone"), section = Some("search_filter_user"))),
|
||||
Some(People)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("iphone"), section = Some("search_filter_image"))),
|
||||
Some(Photos)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("iphone"), section = Some("search_filter_video"))),
|
||||
Some(Videos)),
|
||||
(
|
||||
Some(EventNamespace(client = Some("iphone"), element = Some("search_filter_top"))),
|
||||
None
|
||||
), // if client is iphone, section determines the query filter hence None if section is None
|
||||
(
|
||||
Some(EventNamespace(client = None, section = Some("search_filter_top"))),
|
||||
Some(Top)
|
||||
), // if client is missing, use section by default
|
||||
(
|
||||
Some(EventNamespace(client = None, element = Some("search_filter_top"))),
|
||||
None
|
||||
), // if client is missing, section is used by default hence None since section is missing
|
||||
(
|
||||
Some(EventNamespace(client = Some("iphone"))),
|
||||
None
|
||||
), // if both element and section missing, expect None
|
||||
(None, None), // if namespace is missing from LogEvent, expect None
|
||||
)
|
||||
|
||||
forEvery(eventsToBeChecked) {
|
||||
(
|
||||
eventNamespace: Option[EventNamespace],
|
||||
searchQueryFilterType: Option[SearchQueryFilterType]
|
||||
) =>
|
||||
infoUtils.getQueryFilterType(
|
||||
LogEvent(
|
||||
eventName = "srp_event",
|
||||
eventNamespace = eventNamespace)) shouldEqual searchQueryFilterType
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,359 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.socialgraph.thriftscala.Action
|
||||
import com.twitter.socialgraph.thriftscala.BlockGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.FollowGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.FollowRequestGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.FollowRetweetsGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.LogEventContext
|
||||
import com.twitter.socialgraph.thriftscala.MuteGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.ReportAsAbuseGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.ReportAsSpamGraphEvent
|
||||
import com.twitter.socialgraph.thriftscala.SrcTargetRequest
|
||||
import com.twitter.socialgraph.thriftscala.WriteEvent
|
||||
import com.twitter.socialgraph.thriftscala.WriteRequestResult
|
||||
import com.twitter.unified_user_actions.adapter.social_graph_event.SocialGraphAdapter
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
import com.twitter.util.Time
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
import org.scalatest.prop.TableFor1
|
||||
import org.scalatest.prop.TableFor3
|
||||
|
||||
class SocialGraphAdapterSpec extends Test with TableDrivenPropertyChecks {
|
||||
trait Fixture {
|
||||
|
||||
val frozenTime: Time = Time.fromMilliseconds(1658949273000L)
|
||||
|
||||
val testLogEventContext: LogEventContext = LogEventContext(
|
||||
timestamp = 1001L,
|
||||
hostname = "",
|
||||
transactionId = "",
|
||||
socialGraphClientId = "",
|
||||
loggedInUserId = Some(1111L),
|
||||
)
|
||||
|
||||
val testWriteRequestResult: WriteRequestResult = WriteRequestResult(
|
||||
request = SrcTargetRequest(
|
||||
source = 1111L,
|
||||
target = 2222L
|
||||
)
|
||||
)
|
||||
|
||||
val testWriteRequestResultWithValidationError: WriteRequestResult = WriteRequestResult(
|
||||
request = SrcTargetRequest(
|
||||
source = 1111L,
|
||||
target = 2222L
|
||||
),
|
||||
validationError = Some("action unsuccessful")
|
||||
)
|
||||
|
||||
val baseEvent: WriteEvent = WriteEvent(
|
||||
context = testLogEventContext,
|
||||
action = Action.AcceptFollowRequest
|
||||
)
|
||||
|
||||
val sgFollowEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Follow,
|
||||
follow = Some(List(FollowGraphEvent(testWriteRequestResult))))
|
||||
|
||||
val sgUnfollowEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unfollow,
|
||||
follow = Some(List(FollowGraphEvent(testWriteRequestResult))))
|
||||
|
||||
val sgFollowRedundantEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Follow,
|
||||
follow = Some(
|
||||
List(
|
||||
FollowGraphEvent(
|
||||
result = testWriteRequestResult,
|
||||
redundantOperation = Some(true)
|
||||
))))
|
||||
|
||||
val sgFollowRedundantIsFalseEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Follow,
|
||||
follow = Some(
|
||||
List(
|
||||
FollowGraphEvent(
|
||||
result = testWriteRequestResult,
|
||||
redundantOperation = Some(false)
|
||||
))))
|
||||
|
||||
val sgUnfollowRedundantEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unfollow,
|
||||
follow = Some(
|
||||
List(
|
||||
FollowGraphEvent(
|
||||
result = testWriteRequestResult,
|
||||
redundantOperation = Some(true)
|
||||
))))
|
||||
|
||||
val sgUnfollowRedundantIsFalseEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unfollow,
|
||||
follow = Some(
|
||||
List(
|
||||
FollowGraphEvent(
|
||||
result = testWriteRequestResult,
|
||||
redundantOperation = Some(false)
|
||||
))))
|
||||
|
||||
val sgUnsuccessfulFollowEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Follow,
|
||||
follow = Some(List(FollowGraphEvent(testWriteRequestResultWithValidationError))))
|
||||
|
||||
val sgUnsuccessfulUnfollowEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unfollow,
|
||||
follow = Some(List(FollowGraphEvent(testWriteRequestResultWithValidationError))))
|
||||
|
||||
val sgBlockEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Block,
|
||||
block = Some(List(BlockGraphEvent(testWriteRequestResult))))
|
||||
|
||||
val sgUnsuccessfulBlockEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Block,
|
||||
block = Some(List(BlockGraphEvent(testWriteRequestResultWithValidationError))))
|
||||
|
||||
val sgUnblockEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unblock,
|
||||
block = Some(List(BlockGraphEvent(testWriteRequestResult))))
|
||||
|
||||
val sgUnsuccessfulUnblockEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unblock,
|
||||
block = Some(List(BlockGraphEvent(testWriteRequestResultWithValidationError))))
|
||||
|
||||
val sgMuteEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Mute,
|
||||
mute = Some(List(MuteGraphEvent(testWriteRequestResult))))
|
||||
|
||||
val sgUnsuccessfulMuteEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Mute,
|
||||
mute = Some(List(MuteGraphEvent(testWriteRequestResultWithValidationError))))
|
||||
|
||||
val sgUnmuteEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unmute,
|
||||
mute = Some(List(MuteGraphEvent(testWriteRequestResult))))
|
||||
|
||||
val sgUnsuccessfulUnmuteEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.Unmute,
|
||||
mute = Some(List(MuteGraphEvent(testWriteRequestResultWithValidationError))))
|
||||
|
||||
val sgCreateFollowRequestEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.CreateFollowRequest,
|
||||
followRequest = Some(List(FollowRequestGraphEvent(testWriteRequestResult)))
|
||||
)
|
||||
|
||||
val sgCancelFollowRequestEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.CancelFollowRequest,
|
||||
followRequest = Some(List(FollowRequestGraphEvent(testWriteRequestResult)))
|
||||
)
|
||||
|
||||
val sgAcceptFollowRequestEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.AcceptFollowRequest,
|
||||
followRequest = Some(List(FollowRequestGraphEvent(testWriteRequestResult)))
|
||||
)
|
||||
|
||||
val sgAcceptFollowRetweetEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.FollowRetweets,
|
||||
followRetweets = Some(List(FollowRetweetsGraphEvent(testWriteRequestResult)))
|
||||
)
|
||||
|
||||
val sgAcceptUnfollowRetweetEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.UnfollowRetweets,
|
||||
followRetweets = Some(List(FollowRetweetsGraphEvent(testWriteRequestResult)))
|
||||
)
|
||||
|
||||
val sgReportAsSpamEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.ReportAsSpam,
|
||||
reportAsSpam = Some(
|
||||
List(
|
||||
ReportAsSpamGraphEvent(
|
||||
result = testWriteRequestResult
|
||||
))))
|
||||
|
||||
val sgReportAsAbuseEvent: WriteEvent = baseEvent.copy(
|
||||
action = Action.ReportAsAbuse,
|
||||
reportAsAbuse = Some(
|
||||
List(
|
||||
ReportAsAbuseGraphEvent(
|
||||
result = testWriteRequestResult
|
||||
))))
|
||||
|
||||
def getExpectedUUA(
|
||||
userId: Long,
|
||||
actionProfileId: Long,
|
||||
sourceTimestampMs: Long,
|
||||
actionType: ActionType,
|
||||
socialGraphAction: Option[Action] = None
|
||||
): UnifiedUserAction = {
|
||||
val actionItem = socialGraphAction match {
|
||||
case Some(sgAction) =>
|
||||
Item.ProfileInfo(
|
||||
ProfileInfo(
|
||||
actionProfileId = actionProfileId,
|
||||
profileActionInfo = Some(
|
||||
ProfileActionInfo.ServerProfileReport(
|
||||
ServerProfileReport(reportType = sgAction)
|
||||
))
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
Item.ProfileInfo(
|
||||
ProfileInfo(
|
||||
actionProfileId = actionProfileId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(userId)),
|
||||
item = actionItem,
|
||||
actionType = actionType,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = sourceTimestampMs,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerSocialGraphEvents
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val expectedUuaFollow: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileFollow
|
||||
)
|
||||
|
||||
val expectedUuaUnfollow: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileUnfollow
|
||||
)
|
||||
|
||||
val expectedUuaMute: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileMute
|
||||
)
|
||||
|
||||
val expectedUuaUnmute: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileUnmute
|
||||
)
|
||||
|
||||
val expectedUuaBlock: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileBlock
|
||||
)
|
||||
|
||||
val expectedUuaUnblock: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileUnblock
|
||||
)
|
||||
|
||||
val expectedUuaReportAsSpam: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileReport,
|
||||
socialGraphAction = Some(Action.ReportAsSpam)
|
||||
)
|
||||
|
||||
val expectedUuaReportAsAbuse: UnifiedUserAction = getExpectedUUA(
|
||||
userId = 1111L,
|
||||
actionProfileId = 2222L,
|
||||
sourceTimestampMs = 1001L,
|
||||
actionType = ActionType.ServerProfileReport,
|
||||
socialGraphAction = Some(Action.ReportAsAbuse)
|
||||
)
|
||||
}
|
||||
|
||||
test("SocialGraphAdapter ignore events not in the list") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val ignoredSocialGraphEvents: TableFor1[WriteEvent] = Table(
|
||||
"ignoredSocialGraphEvents",
|
||||
sgAcceptUnfollowRetweetEvent,
|
||||
sgAcceptFollowRequestEvent,
|
||||
sgAcceptFollowRetweetEvent,
|
||||
sgCreateFollowRequestEvent,
|
||||
sgCancelFollowRequestEvent,
|
||||
)
|
||||
forEvery(ignoredSocialGraphEvents) { writeEvent: WriteEvent =>
|
||||
val actual = SocialGraphAdapter.adaptEvent(writeEvent)
|
||||
assert(actual.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Test SocialGraphAdapter consuming Write events") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val socialProfileActions: TableFor3[String, WriteEvent, UnifiedUserAction] = Table(
|
||||
("actionType", "event", "expectedUnifiedUserAction"),
|
||||
("ProfileFollow", sgFollowEvent, expectedUuaFollow),
|
||||
("ProfileUnfollow", sgUnfollowEvent, expectedUuaUnfollow),
|
||||
("ProfileBlock", sgBlockEvent, expectedUuaBlock),
|
||||
("ProfileUnBlock", sgUnblockEvent, expectedUuaUnblock),
|
||||
("ProfileMute", sgMuteEvent, expectedUuaMute),
|
||||
("ProfileUnmute", sgUnmuteEvent, expectedUuaUnmute),
|
||||
("ProfileReportAsSpam", sgReportAsSpamEvent, expectedUuaReportAsSpam),
|
||||
("ProfileReportAsAbuse", sgReportAsAbuseEvent, expectedUuaReportAsAbuse),
|
||||
)
|
||||
forEvery(socialProfileActions) {
|
||||
(_: String, event: WriteEvent, expected: UnifiedUserAction) =>
|
||||
val actual = SocialGraphAdapter.adaptEvent(event)
|
||||
assert(Seq(expected) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("SocialGraphAdapter ignore redundant follow/unfollow events") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val socialGraphActions: TableFor3[String, WriteEvent, Seq[UnifiedUserAction]] = Table(
|
||||
("actionType", "ignoredRedundantFollowUnfollowEvents", "expectedUnifiedUserAction"),
|
||||
("ProfileFollow", sgFollowRedundantEvent, Nil),
|
||||
("ProfileFollow", sgFollowRedundantIsFalseEvent, Seq(expectedUuaFollow)),
|
||||
("ProfileUnfollow", sgUnfollowRedundantEvent, Nil),
|
||||
("ProfileUnfollow", sgUnfollowRedundantIsFalseEvent, Seq(expectedUuaUnfollow))
|
||||
)
|
||||
forEvery(socialGraphActions) {
|
||||
(_: String, event: WriteEvent, expected: Seq[UnifiedUserAction]) =>
|
||||
val actual = SocialGraphAdapter.adaptEvent(event)
|
||||
assert(expected === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("SocialGraphAdapter ignore Unsuccessful SocialGraph events") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val unsuccessfulSocialGraphEvents: TableFor1[WriteEvent] = Table(
|
||||
"ignoredSocialGraphEvents",
|
||||
sgUnsuccessfulFollowEvent,
|
||||
sgUnsuccessfulUnfollowEvent,
|
||||
sgUnsuccessfulBlockEvent,
|
||||
sgUnsuccessfulUnblockEvent,
|
||||
sgUnsuccessfulMuteEvent,
|
||||
sgUnsuccessfulUnmuteEvent
|
||||
)
|
||||
|
||||
forEvery(unsuccessfulSocialGraphEvents) { writeEvent: WriteEvent =>
|
||||
val actual = SocialGraphAdapter.adaptEvent(writeEvent)
|
||||
assert(actual.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,205 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.context.thriftscala.Viewer
|
||||
import com.twitter.inject.Test
|
||||
import com.twitter.timelineservice.thriftscala._
|
||||
import com.twitter.unified_user_actions.adapter.tls_favs_event.TlsFavsAdapter
|
||||
import com.twitter.unified_user_actions.thriftscala._
|
||||
import com.twitter.util.Time
|
||||
|
||||
class TlsFavsAdapterSpec extends Test {
|
||||
trait Fixture {
|
||||
|
||||
val frozenTime = Time.fromMilliseconds(1658949273000L)
|
||||
|
||||
val favEventNoRetweet = ContextualizedFavoriteEvent(
|
||||
event = FavoriteEventUnion.Favorite(
|
||||
FavoriteEvent(
|
||||
userId = 91L,
|
||||
tweetId = 1L,
|
||||
tweetUserId = 101L,
|
||||
eventTimeMs = 1001L
|
||||
)
|
||||
),
|
||||
context = LogEventContext(hostname = "", traceId = 31L)
|
||||
)
|
||||
val favEventRetweet = ContextualizedFavoriteEvent(
|
||||
event = FavoriteEventUnion.Favorite(
|
||||
FavoriteEvent(
|
||||
userId = 92L,
|
||||
tweetId = 2L,
|
||||
tweetUserId = 102L,
|
||||
eventTimeMs = 1002L,
|
||||
retweetId = Some(22L)
|
||||
)
|
||||
),
|
||||
context = LogEventContext(hostname = "", traceId = 32L)
|
||||
)
|
||||
val unfavEventNoRetweet = ContextualizedFavoriteEvent(
|
||||
event = FavoriteEventUnion.Unfavorite(
|
||||
UnfavoriteEvent(
|
||||
userId = 93L,
|
||||
tweetId = 3L,
|
||||
tweetUserId = 103L,
|
||||
eventTimeMs = 1003L
|
||||
)
|
||||
),
|
||||
context = LogEventContext(hostname = "", traceId = 33L)
|
||||
)
|
||||
val unfavEventRetweet = ContextualizedFavoriteEvent(
|
||||
event = FavoriteEventUnion.Unfavorite(
|
||||
UnfavoriteEvent(
|
||||
userId = 94L,
|
||||
tweetId = 4L,
|
||||
tweetUserId = 104L,
|
||||
eventTimeMs = 1004L,
|
||||
retweetId = Some(44L)
|
||||
)
|
||||
),
|
||||
context = LogEventContext(hostname = "", traceId = 34L)
|
||||
)
|
||||
val favEventWithLangAndCountry = ContextualizedFavoriteEvent(
|
||||
event = FavoriteEventUnion.Favorite(
|
||||
FavoriteEvent(
|
||||
userId = 91L,
|
||||
tweetId = 1L,
|
||||
tweetUserId = 101L,
|
||||
eventTimeMs = 1001L,
|
||||
viewerContext =
|
||||
Some(Viewer(requestCountryCode = Some("us"), requestLanguageCode = Some("en")))
|
||||
)
|
||||
),
|
||||
context = LogEventContext(hostname = "", traceId = 31L)
|
||||
)
|
||||
|
||||
val expectedUua1 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(91L)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = 1L,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(101L))),
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetFav,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 1001L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerTlsFavs,
|
||||
traceId = Some(31L)
|
||||
)
|
||||
)
|
||||
val expectedUua2 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(92L)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = 2L,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(102L))),
|
||||
retweetingTweetId = Some(22L)
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetFav,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 1002L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerTlsFavs,
|
||||
traceId = Some(32L)
|
||||
)
|
||||
)
|
||||
val expectedUua3 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(93L)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = 3L,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(103L))),
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetUnfav,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 1003L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerTlsFavs,
|
||||
traceId = Some(33L)
|
||||
)
|
||||
)
|
||||
val expectedUua4 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(94L)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = 4L,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(104L))),
|
||||
retweetingTweetId = Some(44L)
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetUnfav,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 1004L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerTlsFavs,
|
||||
traceId = Some(34L)
|
||||
)
|
||||
)
|
||||
val expectedUua5 = UnifiedUserAction(
|
||||
userIdentifier = UserIdentifier(userId = Some(91L)),
|
||||
item = Item.TweetInfo(
|
||||
TweetInfo(
|
||||
actionTweetId = 1L,
|
||||
actionTweetAuthorInfo = Some(AuthorInfo(authorId = Some(101L))),
|
||||
)
|
||||
),
|
||||
actionType = ActionType.ServerTweetFav,
|
||||
eventMetadata = EventMetadata(
|
||||
sourceTimestampMs = 1001L,
|
||||
receivedTimestampMs = frozenTime.inMilliseconds,
|
||||
sourceLineage = SourceLineage.ServerTlsFavs,
|
||||
language = Some("EN"),
|
||||
countryCode = Some("US"),
|
||||
traceId = Some(31L)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
test("fav event with no retweet") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val actual = TlsFavsAdapter.adaptEvent(favEventNoRetweet)
|
||||
assert(Seq(expectedUua1) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("fav event with a retweet") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val actual = TlsFavsAdapter.adaptEvent(favEventRetweet)
|
||||
assert(Seq(expectedUua2) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("unfav event with no retweet") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val actual = TlsFavsAdapter.adaptEvent(unfavEventNoRetweet)
|
||||
assert(Seq(expectedUua3) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("unfav event with a retweet") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val actual = TlsFavsAdapter.adaptEvent(unfavEventRetweet)
|
||||
assert(Seq(expectedUua4) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("fav event with language and country") {
|
||||
new Fixture {
|
||||
Time.withTimeAt(frozenTime) { _ =>
|
||||
val actual = TlsFavsAdapter.adaptEvent(favEventWithLangAndCountry)
|
||||
assert(Seq(expectedUua5) === actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,545 +0,0 @@
|
||||
package com.twitter.unified_user_actions.adapter
|
||||
|
||||
import com.twitter.clientapp.thriftscala._
|
||||
import com.twitter.clientapp.thriftscala.SuggestionDetails
|
||||
import com.twitter.guide.scribing.thriftscala._
|
||||
import com.twitter.guide.scribing.thriftscala.{SemanticCoreInterest => SemanticCoreInterestV1}
|
||||
import com.twitter.guide.scribing.thriftscala.{SimClusterInterest => SimClusterInterestV1}
|
||||
import com.twitter.guide.scribing.thriftscala.TopicModuleMetadata.SemanticCoreInterest
|
||||
import com.twitter.guide.scribing.thriftscala.TopicModuleMetadata.SimClusterInterest
|
||||
import com.twitter.guide.scribing.thriftscala.TransparentGuideDetails.TopicMetadata
|
||||
import com.twitter.logbase.thriftscala.LogBase
|
||||
import com.twitter.scrooge.TFieldBlob
|
||||
import com.twitter.suggests.controller_data.home_hitl_topic_annotation_prompt.thriftscala.HomeHitlTopicAnnotationPromptControllerData
|
||||
import com.twitter.suggests.controller_data.home_hitl_topic_annotation_prompt.v1.thriftscala.{
|
||||
HomeHitlTopicAnnotationPromptControllerData => HomeHitlTopicAnnotationPromptControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.home_topic_annotation_prompt.thriftscala.HomeTopicAnnotationPromptControllerData
|
||||
import com.twitter.suggests.controller_data.home_topic_annotation_prompt.v1.thriftscala.{
|
||||
HomeTopicAnnotationPromptControllerData => HomeTopicAnnotationPromptControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.home_topic_follow_prompt.thriftscala.HomeTopicFollowPromptControllerData
|
||||
import com.twitter.suggests.controller_data.home_topic_follow_prompt.v1.thriftscala.{
|
||||
HomeTopicFollowPromptControllerData => HomeTopicFollowPromptControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.home_tweets.thriftscala.HomeTweetsControllerData
|
||||
import com.twitter.suggests.controller_data.home_tweets.v1.thriftscala.{
|
||||
HomeTweetsControllerData => HomeTweetsControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.search_response.item_types.thriftscala.ItemTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.thriftscala.SearchResponseControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.topic_follow_prompt.thriftscala.SearchTopicFollowPromptControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.tweet_types.thriftscala.TweetTypesControllerData
|
||||
import com.twitter.suggests.controller_data.search_response.v1.thriftscala.{
|
||||
SearchResponseControllerData => SearchResponseControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.thriftscala.ControllerData
|
||||
import com.twitter.suggests.controller_data.timelines_topic.thriftscala.TimelinesTopicControllerData
|
||||
import com.twitter.suggests.controller_data.timelines_topic.v1.thriftscala.{
|
||||
TimelinesTopicControllerData => TimelinesTopicControllerDataV1
|
||||
}
|
||||
import com.twitter.suggests.controller_data.v2.thriftscala.{ControllerData => ControllerDataV2}
|
||||
import org.apache.thrift.protocol.TField
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatestplus.junit.JUnitRunner
|
||||
import com.twitter.util.mock.Mockito
|
||||
import org.mockito.Mockito.when
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class TopicsIdUtilsSpec
|
||||
extends AnyFunSuite
|
||||
with Matchers
|
||||
with Mockito
|
||||
with TableDrivenPropertyChecks {
|
||||
import com.twitter.unified_user_actions.adapter.client_event.TopicIdUtils._
|
||||
|
||||
trait Fixture {
|
||||
def buildLogBase(userId: Long): LogBase = {
|
||||
val logBase = mock[LogBase]
|
||||
when(logBase.country).thenReturn(Some("US"))
|
||||
when(logBase.userId).thenReturn(Some(userId))
|
||||
when(logBase.timestamp).thenReturn(100L)
|
||||
when(logBase.guestId).thenReturn(Some(1L))
|
||||
when(logBase.userAgent).thenReturn(None)
|
||||
when(logBase.language).thenReturn(Some("en"))
|
||||
logBase
|
||||
}
|
||||
|
||||
def buildItemForTimeline(
|
||||
itemId: Long,
|
||||
itemType: ItemType,
|
||||
topicId: Long,
|
||||
fn: Long => ControllerData.V2
|
||||
): Item = {
|
||||
val item = Item(
|
||||
id = Some(itemId),
|
||||
itemType = Some(itemType),
|
||||
suggestionDetails = Some(SuggestionDetails(decodedControllerData = Some(fn(topicId))))
|
||||
)
|
||||
item
|
||||
}
|
||||
|
||||
def buildClientEventForHomeSearchTimeline(
|
||||
itemId: Long,
|
||||
itemType: ItemType,
|
||||
topicId: Long,
|
||||
fn: Long => ControllerData.V2,
|
||||
userId: Long = 1L,
|
||||
eventNamespaceOpt: Option[EventNamespace] = None,
|
||||
): LogEvent = {
|
||||
val logEvent = mock[LogEvent]
|
||||
when(logEvent.eventNamespace).thenReturn(eventNamespaceOpt)
|
||||
val eventsDetails = mock[EventDetails]
|
||||
when(eventsDetails.items)
|
||||
.thenReturn(Some(Seq(buildItemForTimeline(itemId, itemType, topicId, fn))))
|
||||
val logbase = buildLogBase(userId)
|
||||
when(logEvent.logBase).thenReturn(Some(logbase))
|
||||
when(logEvent.eventDetails).thenReturn(Some(eventsDetails))
|
||||
logEvent
|
||||
}
|
||||
|
||||
def buildClientEventForHomeTweetsTimeline(
|
||||
itemId: Long,
|
||||
itemType: ItemType,
|
||||
topicId: Long,
|
||||
topicIds: Set[Long],
|
||||
fn: (Long, Set[Long]) => ControllerData.V2,
|
||||
userId: Long = 1L,
|
||||
eventNamespaceOpt: Option[EventNamespace] = None,
|
||||
): LogEvent = {
|
||||
val logEvent = mock[LogEvent]
|
||||
when(logEvent.eventNamespace).thenReturn(eventNamespaceOpt)
|
||||
val eventsDetails = mock[EventDetails]
|
||||
when(eventsDetails.items)
|
||||
.thenReturn(Some(Seq(buildItemForHomeTimeline(itemId, itemType, topicId, topicIds, fn))))
|
||||
val logbase = buildLogBase(userId)
|
||||
when(logEvent.logBase).thenReturn(Some(logbase))
|
||||
when(logEvent.eventDetails).thenReturn(Some(eventsDetails))
|
||||
logEvent
|
||||
}
|
||||
|
||||
def buildClientEventForGuide(
|
||||
itemId: Long,
|
||||
itemType: ItemType,
|
||||
topicId: Long,
|
||||
fn: Long => TopicMetadata,
|
||||
userId: Long = 1L,
|
||||
eventNamespaceOpt: Option[EventNamespace] = None,
|
||||
): LogEvent = {
|
||||
val logEvent = mock[LogEvent]
|
||||
when(logEvent.eventNamespace).thenReturn(eventNamespaceOpt)
|
||||
val logbase = buildLogBase(userId)
|
||||
when(logEvent.logBase).thenReturn(Some(logbase))
|
||||
val eventDetails = mock[EventDetails]
|
||||
val item = buildItemForGuide(itemId, itemType, topicId, fn)
|
||||
when(eventDetails.items).thenReturn(Some(Seq(item)))
|
||||
when(logEvent.eventDetails).thenReturn(Some(eventDetails))
|
||||
logEvent
|
||||
}
|
||||
|
||||
def buildClientEventForOnboarding(
|
||||
itemId: Long,
|
||||
topicId: Long,
|
||||
userId: Long = 1L
|
||||
): LogEvent = {
|
||||
val logEvent = mock[LogEvent]
|
||||
val logbase = buildLogBase(userId)
|
||||
when(logEvent.logBase).thenReturn(Some(logbase))
|
||||
when(logEvent.eventNamespace).thenReturn(Some(buildNamespaceForOnboarding))
|
||||
val eventDetails = mock[EventDetails]
|
||||
val item = buildItemForOnboarding(itemId, topicId)
|
||||
when(eventDetails.items)
|
||||
.thenReturn(Some(Seq(item)))
|
||||
when(logEvent.eventDetails).thenReturn(Some(eventDetails))
|
||||
logEvent
|
||||
}
|
||||
|
||||
def buildClientEventForOnboardingBackend(
|
||||
topicId: Long,
|
||||
userId: Long = 1L
|
||||
): LogEvent = {
|
||||
val logEvent = mock[LogEvent]
|
||||
val logbase = buildLogBase(userId)
|
||||
when(logEvent.logBase).thenReturn(Some(logbase))
|
||||
when(logEvent.eventNamespace).thenReturn(Some(buildNamespaceForOnboardingBackend))
|
||||
val eventDetails = buildEventDetailsForOnboardingBackend(topicId)
|
||||
when(logEvent.eventDetails).thenReturn(Some(eventDetails))
|
||||
logEvent
|
||||
}
|
||||
|
||||
def defaultNamespace: EventNamespace = {
|
||||
EventNamespace(Some("iphone"), None, None, None, None, Some("favorite"))
|
||||
}
|
||||
|
||||
def buildNamespaceForOnboardingBackend: EventNamespace = {
|
||||
EventNamespace(
|
||||
Some("iphone"),
|
||||
Some("onboarding_backend"),
|
||||
Some("subtasks"),
|
||||
Some("topics_selector"),
|
||||
Some("removed"),
|
||||
Some("selected"))
|
||||
}
|
||||
|
||||
def buildNamespaceForOnboarding: EventNamespace = {
|
||||
EventNamespace(
|
||||
Some("iphone"),
|
||||
Some("onboarding"),
|
||||
Some("topics_selector"),
|
||||
None,
|
||||
Some("topic"),
|
||||
Some("follow")
|
||||
)
|
||||
}
|
||||
|
||||
def buildItemForHomeTimeline(
|
||||
itemId: Long,
|
||||
itemType: ItemType,
|
||||
topicId: Long,
|
||||
topicIds: Set[Long],
|
||||
fn: (Long, Set[Long]) => ControllerData.V2
|
||||
): Item = {
|
||||
val item = Item(
|
||||
id = Some(itemId),
|
||||
itemType = Some(itemType),
|
||||
suggestionDetails =
|
||||
Some(SuggestionDetails(decodedControllerData = Some(fn(topicId, topicIds))))
|
||||
)
|
||||
item
|
||||
}
|
||||
|
||||
def buildItemForGuide(
|
||||
itemId: Long,
|
||||
itemType: ItemType,
|
||||
topicId: Long,
|
||||
fn: Long => TopicMetadata
|
||||
): Item = {
|
||||
val item = mock[Item]
|
||||
when(item.id).thenReturn(Some(itemId))
|
||||
when(item.itemType).thenReturn(Some(itemType))
|
||||
when(item.suggestionDetails)
|
||||
.thenReturn(Some(SuggestionDetails(suggestionType = Some("ErgTweet"))))
|
||||
val guideItemDetails = mock[GuideItemDetails]
|
||||
when(guideItemDetails.transparentGuideDetails).thenReturn(Some(fn(topicId)))
|
||||
when(item.guideItemDetails).thenReturn(Some(guideItemDetails))
|
||||
item
|
||||
}
|
||||
|
||||
def buildItemForOnboarding(
|
||||
itemId: Long,
|
||||
topicId: Long
|
||||
): Item = {
|
||||
val item = Item(
|
||||
id = Some(itemId),
|
||||
itemType = None,
|
||||
description = Some(s"id=$topicId,row=1")
|
||||
)
|
||||
item
|
||||
}
|
||||
|
||||
def buildEventDetailsForOnboardingBackend(
|
||||
topicId: Long
|
||||
): EventDetails = {
|
||||
val eventDetails = mock[EventDetails]
|
||||
val item = Item(
|
||||
id = Some(topicId)
|
||||
)
|
||||
val itemTmp = buildItemForOnboarding(10, topicId)
|
||||
when(eventDetails.items).thenReturn(Some(Seq(itemTmp)))
|
||||
when(eventDetails.targets).thenReturn(Some(Seq(item)))
|
||||
eventDetails
|
||||
}
|
||||
|
||||
def topicMetadataInGuide(topicId: Long): TopicMetadata =
|
||||
TopicMetadata(
|
||||
SemanticCoreInterest(
|
||||
SemanticCoreInterestV1(domainId = "131", entityId = topicId.toString)
|
||||
)
|
||||
)
|
||||
|
||||
def simClusterMetadataInGuide(simclusterId: Long = 1L): TopicMetadata =
|
||||
TopicMetadata(
|
||||
SimClusterInterest(
|
||||
SimClusterInterestV1(simclusterId.toString)
|
||||
)
|
||||
)
|
||||
|
||||
def timelineTopicControllerData(topicId: Long): ControllerData.V2 =
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.TimelinesTopic(
|
||||
TimelinesTopicControllerData.V1(
|
||||
TimelinesTopicControllerDataV1(
|
||||
topicId = topicId,
|
||||
topicTypesBitmap = 1
|
||||
)
|
||||
)))
|
||||
|
||||
def homeTweetControllerData(topicId: Long): ControllerData.V2 =
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTweets(
|
||||
HomeTweetsControllerData.V1(
|
||||
HomeTweetsControllerDataV1(
|
||||
topicId = Some(topicId)
|
||||
))))
|
||||
|
||||
def homeTopicFollowPromptControllerData(topicId: Long): ControllerData.V2 =
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTopicFollowPrompt(HomeTopicFollowPromptControllerData.V1(
|
||||
HomeTopicFollowPromptControllerDataV1(Some(topicId)))))
|
||||
|
||||
def homeTopicAnnotationPromptControllerData(topicId: Long): ControllerData.V2 =
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTopicAnnotationPrompt(HomeTopicAnnotationPromptControllerData.V1(
|
||||
HomeTopicAnnotationPromptControllerDataV1(tweetId = 1L, topicId = topicId))))
|
||||
|
||||
def homeHitlTopicAnnotationPromptControllerData(topicId: Long): ControllerData.V2 =
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeHitlTopicAnnotationPrompt(
|
||||
HomeHitlTopicAnnotationPromptControllerData.V1(
|
||||
HomeHitlTopicAnnotationPromptControllerDataV1(tweetId = 2L, topicId = topicId))))
|
||||
|
||||
def searchTopicFollowPromptControllerData(topicId: Long): ControllerData.V2 =
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(
|
||||
SearchResponseControllerDataV1(
|
||||
Some(ItemTypesControllerData.TopicFollowControllerData(
|
||||
SearchTopicFollowPromptControllerData(Some(topicId))
|
||||
)),
|
||||
None
|
||||
))))
|
||||
|
||||
def searchTweetTypesControllerData(topicId: Long): ControllerData.V2 =
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(
|
||||
SearchResponseControllerDataV1(
|
||||
Some(ItemTypesControllerData.TweetTypesControllerData(
|
||||
TweetTypesControllerData(None, Some(topicId))
|
||||
)),
|
||||
None
|
||||
)
|
||||
)))
|
||||
|
||||
//used for creating logged out user client events
|
||||
def buildLogBaseWithoutUserId(guestId: Long): LogBase =
|
||||
LogBase(
|
||||
ipAddress = "120.10.10.20",
|
||||
guestId = Some(guestId),
|
||||
userAgent = None,
|
||||
transactionId = "",
|
||||
country = Some("US"),
|
||||
timestamp = 100L,
|
||||
language = Some("en")
|
||||
)
|
||||
}
|
||||
|
||||
test("getTopicId should correctly find topic id from item for home timeline and search") {
|
||||
new Fixture {
|
||||
|
||||
val testData = Table(
|
||||
("ItemType", "topicId", "controllerData"),
|
||||
(ItemType.Tweet, 1L, timelineTopicControllerData(1L)),
|
||||
(ItemType.User, 2L, timelineTopicControllerData(2L)),
|
||||
(ItemType.Topic, 3L, homeTweetControllerData(3L)),
|
||||
(ItemType.Topic, 4L, homeTopicFollowPromptControllerData(4L)),
|
||||
(ItemType.Topic, 5L, searchTopicFollowPromptControllerData(5L)),
|
||||
(ItemType.Topic, 6L, homeHitlTopicAnnotationPromptControllerData(6L))
|
||||
)
|
||||
|
||||
forEvery(testData) {
|
||||
(itemType: ItemType, topicId: Long, controllerDataV2: ControllerData.V2) =>
|
||||
getTopicId(
|
||||
buildItemForTimeline(1, itemType, topicId, _ => controllerDataV2),
|
||||
defaultNamespace) shouldEqual Some(topicId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("getTopicId should correctly find topic id from item for guide events") {
|
||||
new Fixture {
|
||||
getTopicId(
|
||||
buildItemForGuide(1, ItemType.Tweet, 100, topicMetadataInGuide),
|
||||
defaultNamespace
|
||||
) shouldEqual Some(100)
|
||||
}
|
||||
}
|
||||
|
||||
test("getTopicId should correctly find topic id for onboarding events") {
|
||||
new Fixture {
|
||||
getTopicId(
|
||||
buildItemForOnboarding(1, 100),
|
||||
buildNamespaceForOnboarding
|
||||
) shouldEqual Some(100)
|
||||
}
|
||||
}
|
||||
|
||||
test("should return TopicId From HomeSearch") {
|
||||
val testData = Table(
|
||||
("controllerData", "topicId"),
|
||||
(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTweets(
|
||||
HomeTweetsControllerData.V1(HomeTweetsControllerDataV1(topicId = Some(1L))))
|
||||
),
|
||||
Some(1L)),
|
||||
(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTopicFollowPrompt(HomeTopicFollowPromptControllerData
|
||||
.V1(HomeTopicFollowPromptControllerDataV1(topicId = Some(2L))))),
|
||||
Some(2L)),
|
||||
(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.TimelinesTopic(
|
||||
TimelinesTopicControllerData.V1(
|
||||
TimelinesTopicControllerDataV1(topicId = 3L, topicTypesBitmap = 100)
|
||||
))),
|
||||
Some(3L)),
|
||||
(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(SearchResponseControllerDataV1(itemTypesControllerData =
|
||||
Some(ItemTypesControllerData.TopicFollowControllerData(
|
||||
SearchTopicFollowPromptControllerData(topicId = Some(4L)))))))),
|
||||
Some(4L)),
|
||||
(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.SearchResponse(
|
||||
SearchResponseControllerData.V1(
|
||||
SearchResponseControllerDataV1(itemTypesControllerData = Some(ItemTypesControllerData
|
||||
.TweetTypesControllerData(TweetTypesControllerData(topicId = Some(5L)))))))),
|
||||
Some(5L)),
|
||||
(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2
|
||||
.SearchResponse(SearchResponseControllerData.V1(SearchResponseControllerDataV1()))),
|
||||
None)
|
||||
)
|
||||
|
||||
forEvery(testData) { (controllerDataV2: ControllerData.V2, topicId: Option[Long]) =>
|
||||
getTopicIdFromHomeSearch(
|
||||
Item(suggestionDetails = Some(
|
||||
SuggestionDetails(decodedControllerData = Some(controllerDataV2))))) shouldEqual topicId
|
||||
}
|
||||
}
|
||||
|
||||
test("test TopicId From Onboarding") {
|
||||
val testData = Table(
|
||||
("Item", "EventNamespace", "topicId"),
|
||||
(
|
||||
Item(description = Some("id=11,key=value")),
|
||||
EventNamespace(
|
||||
page = Some("onboarding"),
|
||||
section = Some("section has topic"),
|
||||
component = Some("component has topic"),
|
||||
element = Some("element has topic")
|
||||
),
|
||||
Some(11L)),
|
||||
(
|
||||
Item(description = Some("id=22,key=value")),
|
||||
EventNamespace(
|
||||
page = Some("onboarding"),
|
||||
section = Some("section has topic")
|
||||
),
|
||||
Some(22L)),
|
||||
(
|
||||
Item(description = Some("id=33,key=value")),
|
||||
EventNamespace(
|
||||
page = Some("onboarding"),
|
||||
component = Some("component has topic")
|
||||
),
|
||||
Some(33L)),
|
||||
(
|
||||
Item(description = Some("id=44,key=value")),
|
||||
EventNamespace(
|
||||
page = Some("onboarding"),
|
||||
element = Some("element has topic")
|
||||
),
|
||||
Some(44L)),
|
||||
(
|
||||
Item(description = Some("id=678,key=value")),
|
||||
EventNamespace(
|
||||
page = Some("onXYZboarding"),
|
||||
section = Some("section has topic"),
|
||||
component = Some("component has topic"),
|
||||
element = Some("element has topic")
|
||||
),
|
||||
None),
|
||||
(
|
||||
Item(description = Some("id=678,key=value")),
|
||||
EventNamespace(
|
||||
page = Some("page has onboarding"),
|
||||
section = Some("section has topPic"),
|
||||
component = Some("component has topPic"),
|
||||
element = Some("element has topPic")
|
||||
),
|
||||
None),
|
||||
(
|
||||
Item(description = Some("key=value,id=678")),
|
||||
EventNamespace(
|
||||
page = Some("page has onboarding"),
|
||||
section = Some("section has topic"),
|
||||
component = Some("component has topic"),
|
||||
element = Some("element has topic")
|
||||
),
|
||||
None)
|
||||
)
|
||||
|
||||
forEvery(testData) { (item: Item, eventNamespace: EventNamespace, topicId: Option[Long]) =>
|
||||
getTopicFromOnboarding(item, eventNamespace) shouldEqual topicId
|
||||
}
|
||||
}
|
||||
|
||||
test("test from Guide") {
|
||||
val testData = Table(
|
||||
("guideItemDetails", "topicId"),
|
||||
(
|
||||
GuideItemDetails(transparentGuideDetails = Some(
|
||||
TransparentGuideDetails.TopicMetadata(
|
||||
TopicModuleMetadata.TttInterest(tttInterest = TttInterest.unsafeEmpty)))),
|
||||
None),
|
||||
(
|
||||
GuideItemDetails(transparentGuideDetails = Some(
|
||||
TransparentGuideDetails.TopicMetadata(
|
||||
TopicModuleMetadata.SimClusterInterest(simClusterInterest =
|
||||
com.twitter.guide.scribing.thriftscala.SimClusterInterest.unsafeEmpty)))),
|
||||
None),
|
||||
(
|
||||
GuideItemDetails(transparentGuideDetails = Some(
|
||||
TransparentGuideDetails.TopicMetadata(TopicModuleMetadata.UnknownUnionField(field =
|
||||
TFieldBlob(new TField(), Array.empty[Byte]))))),
|
||||
None),
|
||||
(
|
||||
GuideItemDetails(transparentGuideDetails = Some(
|
||||
TransparentGuideDetails.TopicMetadata(
|
||||
TopicModuleMetadata.SemanticCoreInterest(
|
||||
com.twitter.guide.scribing.thriftscala.SemanticCoreInterest.unsafeEmpty
|
||||
.copy(domainId = "131", entityId = "1"))))),
|
||||
Some(1L)),
|
||||
)
|
||||
|
||||
forEvery(testData) { (guideItemDetails: GuideItemDetails, topicId: Option[Long]) =>
|
||||
getTopicFromGuide(Item(guideItemDetails = Some(guideItemDetails))) shouldEqual topicId
|
||||
}
|
||||
}
|
||||
|
||||
test("getTopicId should return topicIds") {
|
||||
getTopicId(
|
||||
item = Item(suggestionDetails = Some(
|
||||
SuggestionDetails(decodedControllerData = Some(
|
||||
ControllerData.V2(
|
||||
ControllerDataV2.HomeTweets(
|
||||
HomeTweetsControllerData.V1(HomeTweetsControllerDataV1(topicId = Some(1L))))
|
||||
))))),
|
||||
namespace = EventNamespace(
|
||||
page = Some("onboarding"),
|
||||
section = Some("section has topic"),
|
||||
component = Some("component has topic"),
|
||||
element = Some("element has topic")
|
||||
)
|
||||
) shouldEqual Some(1L)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user