the-algorithm/tweetypie/common/src/scala/com/twitter/tweetypie/jiminy/tweetypie/NudgeBuilder.scala

166 lines
5.5 KiB
Scala

package com.twitter.tweetypie.jiminy.tweetypie
import com.twitter.finagle.stats.CategorizingExceptionStatsHandler
import com.twitter.finagle.stats.Stat
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.incentives.jiminy.thriftscala._
import com.twitter.servo.util.FutureArrow
import com.twitter.servo.util.Gate
import com.twitter.stitch.Stitch
import com.twitter.strato.thrift.ScroogeConvImplicits._
import com.twitter.strato.client.{Client => StratoClient}
import com.twitter.tweetypie.core.TweetCreateFailure
import com.twitter.util.Future
import com.twitter.util.Return
import com.twitter.util.Throw
case class NudgeBuilderRequest(
text: String,
inReplyToTweetId: Option[NudgeBuilder.TweetId],
conversationId: Option[NudgeBuilder.TweetId],
hasQuotedTweet: Boolean,
nudgeOptions: Option[CreateTweetNudgeOptions],
tweetId: Option[NudgeBuilder.TweetId])
trait NudgeBuilder extends FutureArrow[NudgeBuilderRequest, Unit] {
/**
* Check whether the user should receive a nudge instead of creating
* the Tweet. If nudgeOptions is None, then no nudge check will be
* performed.
*
* @return a Future.exception containing a [[TweetCreateFailure]] if the
* user should be nudged, or Future.Unit if the user should not be
* nudged.
*/
def apply(
request: NudgeBuilderRequest
): Future[Unit]
}
object NudgeBuilder {
type Type = FutureArrow[NudgeBuilderRequest, Unit]
type TweetId = Long
// darkTrafficCreateNudgeOptions ensure that our dark traffic sends a request that will
// accurately test the Jiminy backend. in this case, we specify that we want checks for all
// possible nudge types
private[this] val darkTrafficCreateNudgeOptions = Some(
CreateTweetNudgeOptions(
requestedNudgeTypes = Some(
Set(
TweetNudgeType.PotentiallyToxicTweet,
TweetNudgeType.ReviseOrMute,
TweetNudgeType.ReviseOrHideThenBlock,
TweetNudgeType.ReviseOrBlock
)
)
)
)
private[this] def mkJiminyRequest(
request: NudgeBuilderRequest,
isDarkRequest: Boolean = false
): CreateTweetNudgeRequest = {
val tweetType =
if (request.inReplyToTweetId.nonEmpty) TweetType.Reply
else if (request.hasQuotedTweet) TweetType.QuoteTweet
else TweetType.OriginalTweet
CreateTweetNudgeRequest(
tweetText = request.text,
tweetType = tweetType,
inReplyToTweetId = request.inReplyToTweetId,
conversationId = request.conversationId,
createTweetNudgeOptions =
if (isDarkRequest) darkTrafficCreateNudgeOptions else request.nudgeOptions,
tweetId = request.tweetId
)
}
/**
* NudgeBuilder implemented by calling the strato column `incentives/createNudge`.
*
* Stats recorded:
* - latency_ms: Latency histogram (also implicitly number of
* invocations). This is counted only in the case that a nudge
* check was requested (`nudgeOptions` is non-empty)
*
* - nudge: The nudge check succeeded and a nudge was created.
*
* - no_nudge: The nudge check succeeded, but no nudge was created.
*
* - failures: Calling strato to create a nudge failed. Broken out
* by exception.
*/
def apply(
nudgeArrow: FutureArrow[CreateTweetNudgeRequest, CreateTweetNudgeResponse],
enableDarkTraffic: Gate[Unit],
stats: StatsReceiver
): NudgeBuilder = {
new NudgeBuilder {
private[this] val nudgeLatencyStat = stats.stat("latency_ms")
private[this] val nudgeCounter = stats.counter("nudge")
private[this] val noNudgeCounter = stats.counter("no_nudge")
private[this] val darkRequestCounter = stats.counter("dark_request")
private[this] val nudgeExceptionHandler = new CategorizingExceptionStatsHandler
override def apply(
request: NudgeBuilderRequest
): Future[Unit] =
request.nudgeOptions match {
case None =>
if (enableDarkTraffic()) {
darkRequestCounter.incr()
Stat
.timeFuture(nudgeLatencyStat) {
nudgeArrow(mkJiminyRequest(request, isDarkRequest = true))
}
.transform { _ =>
// ignore the response since it is a dark request
Future.Done
}
} else {
Future.Done
}
case Some(_) =>
Stat
.timeFuture(nudgeLatencyStat) {
nudgeArrow(mkJiminyRequest(request))
}
.transform {
case Throw(e) =>
nudgeExceptionHandler.record(stats, e)
// If we failed to invoke the nudge column, then
// just continue on with the Tweet creation.
Future.Done
case Return(CreateTweetNudgeResponse(Some(nudge))) =>
nudgeCounter.incr()
Future.exception(TweetCreateFailure.Nudged(nudge = nudge))
case Return(CreateTweetNudgeResponse(None)) =>
noNudgeCounter.incr()
Future.Done
}
}
}
}
def apply(
strato: StratoClient,
enableDarkTraffic: Gate[Unit],
stats: StatsReceiver
): NudgeBuilder = {
val executer =
strato.executer[CreateTweetNudgeRequest, CreateTweetNudgeResponse](
"incentives/createTweetNudge")
val nudgeArrow: FutureArrow[CreateTweetNudgeRequest, CreateTweetNudgeResponse] = { req =>
Stitch.run(executer.execute(req))
}
apply(nudgeArrow, enableDarkTraffic, stats)
}
}