mirror of
https://github.com/twitter/the-algorithm.git
synced 2024-06-01 08:48:46 +02:00
617c8c787d
Unified User Action (UUA) is a centralized, real-time stream of user actions on Twitter, consumed by various product, ML, and marketing teams. UUA makes sure all internal teams consume the uniformed user actions data in an accurate and fast way.
360 lines
12 KiB
Scala
360 lines
12 KiB
Scala
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|