[docx] split commit for file 6600

Signed-off-by: Ari Archer <ari.web.xyz@gmail.com>
This commit is contained in:
Ari Archer 2024-01-23 19:22:09 +02:00
parent ac0fb2a2f2
commit 2c9fa892a8
No known key found for this signature in database
GPG Key ID: A50D5B4B599AF8A2
400 changed files with 0 additions and 20635 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)
)
}
}
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)
))
}
}
}

View File

@ -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
}

View File

@ -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",
],
)

View File

@ -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",
],
)

View File

@ -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
}
}

View File

@ -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),
)
}

View File

@ -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",
],
)

View File

@ -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,
)
}

View File

@ -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",
],
)

View File

@ -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,
)
}

View File

@ -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",
],
)

View File

@ -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)
))
)
)
}
}

View File

@ -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,
)
}
}

View File

@ -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
)
}

View File

@ -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)
}
}

View File

@ -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",
],
)

View File

@ -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 (users 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))
}
}

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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)
)
)
}

View File

@ -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)
)
)
}

View File

@ -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)
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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",
],
)

View File

@ -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")
}
}
}

View File

@ -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,
)
}

View File

@ -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",
],
)

View File

@ -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.

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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))
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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",
],
)

View File

@ -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)
}
}
}
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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