Skip to content

Commit

Permalink
Allow multiple javascript enrichments
Browse files Browse the repository at this point in the history
  • Loading branch information
istreeter authored and spenes committed Feb 8, 2024
1 parent 2a7cbe5 commit 865e8dc
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ object EnrichmentManager {
_ <- getHttpHeaderContexts // Execute header extractor enrichment
_ <- getYauaaContext[F](registry.yauaa, raw.context.headers) // Runs YAUAA enrichment (gets info thanks to user agent)
_ <- extractSchemaFields[F](unstructEvent) // Extract the event vendor/name/format/version
_ <- getJsScript[F](registry.javascriptScript) // Execute the JavaScript scripting enrichment
_ <- registry.javascriptScript.traverse(getJsScript[F](_)) // Execute the JavaScript scripting enrichment
_ <- getCurrency[F](raw.context.timestamp, registry.currencyConversion) // Finalize the currency conversion
_ <- getWeatherContext[F](registry.weather) // Fetch weather context
_ <- geoLocation[F](registry.ipLookups) // Execute IP lookup enrichment
Expand Down Expand Up @@ -240,7 +240,7 @@ object EnrichmentManager {
_ <- getYauaaContext[F](registry.yauaa, raw.context.headers) // Runs YAUAA enrichment (gets info thanks to user agent)
_ <- extractSchemaFields[F](unstructEvent) // Extract the event vendor/name/format/version
_ <- geoLocation[F](registry.ipLookups) // Execute IP lookup enrichment
_ <- getJsScript[F](registry.javascriptScript) // Execute the JavaScript scripting enrichment
_ <- registry.javascriptScript.traverse(getJsScript[F](_)) // Execute the JavaScript scripting enrichment
_ <- sqlContexts // Derive some contexts with custom SQL Query enrichment
_ <- apiContexts // Derive some contexts with custom API Request enrichment
// format: on
Expand Down Expand Up @@ -671,16 +671,12 @@ object EnrichmentManager {

// Execute the JavaScript scripting enrichment
def getJsScript[F[_]: Applicative](
javascriptScript: Option[JavascriptScriptEnrichment]
javascriptScript: JavascriptScriptEnrichment
): EStateT[F, Unit] =
EStateT.fromEither {
case (event, derivedContexts) =>
javascriptScript match {
case Some(jse) =>
ME.formatContexts(derivedContexts).foreach(c => event.derived_contexts = c)
jse.process(event).leftMap(NonEmptyList.one)
case None => Nil.asRight
}
ME.formatContexts(derivedContexts).foreach(c => event.derived_contexts = c)
javascriptScript.process(event).leftMap(NonEmptyList.one)
}

def headerContexts[F[_]: Applicative, A](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ object EnrichmentRegistry {
enrichment <- EitherT.right(c.enrichment[F](blockingEC))
registry <- er
} yield registry.copy(ipLookups = enrichment.some)
case c: JavascriptScriptConf => er.map(_.copy(javascriptScript = c.enrichment.some))
case c: JavascriptScriptConf => er.map(v => v.copy(javascriptScript = v.javascriptScript :+ c.enrichment))
case c: RefererParserConf =>
for {
enrichment <- c.enrichment[F]
Expand Down Expand Up @@ -246,7 +246,7 @@ final case class EnrichmentRegistry[F[_]](
httpHeaderExtractor: Option[HttpHeaderExtractorEnrichment] = None,
iab: Option[IabEnrichment] = None,
ipLookups: Option[IpLookupsEnrichment[F]] = None,
javascriptScript: Option[JavascriptScriptEnrichment] = None,
javascriptScript: List[JavascriptScriptEnrichment] = Nil,
refererParser: Option[RefererParserEnrichment] = None,
uaParser: Option[UaParserEnrichment[F]] = None,
userAgentUtils: Option[UserAgentUtilsEnrichment] = None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import cats.data.EitherT

import cats.effect.kernel.{Async, Sync}

import io.circe.JsonObject

import org.joda.money.CurrencyUnit

import com.snowplowanalytics.iglu.core.SchemaKey
Expand Down Expand Up @@ -192,8 +194,12 @@ object EnrichmentConf {
)
}

final case class JavascriptScriptConf(schemaKey: SchemaKey, rawFunction: String) extends EnrichmentConf {
def enrichment: JavascriptScriptEnrichment = JavascriptScriptEnrichment(schemaKey, rawFunction)
final case class JavascriptScriptConf(
schemaKey: SchemaKey,
rawFunction: String,
params: JsonObject
) extends EnrichmentConf {
def enrichment: JavascriptScriptEnrichment = JavascriptScriptEnrichment(schemaKey, rawFunction, params)
}

final case class RefererParserConf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import cats.implicits._

import io.circe._
import io.circe.parser._
import io.circe.syntax._

import javax.script._

Expand Down Expand Up @@ -52,27 +53,35 @@ object JavascriptScriptEnrichment extends ParseableEnrichment {
_ <- isParseable(c, schemaKey)
encoded <- CirceUtils.extract[String](c, "parameters", "script").toEither
script <- ConversionUtils.decodeBase64Url(encoded)
params <- CirceUtils.extract[Option[JsonObject]](c, "parameters", "config").toEither
_ <- if (script.isEmpty) Left("Provided script for JS enrichment is empty") else Right(())
} yield JavascriptScriptConf(schemaKey, script)).toValidatedNel
} yield JavascriptScriptConf(schemaKey, script, params.getOrElse(JsonObject.empty))).toValidatedNel
}

final case class JavascriptScriptEnrichment(schemaKey: SchemaKey, rawFunction: String) extends Enrichment {
final case class JavascriptScriptEnrichment(
schemaKey: SchemaKey,
rawFunction: String,
params: JsonObject = JsonObject.empty
) extends Enrichment {
private val enrichmentInfo =
FailureDetails.EnrichmentInformation(schemaKey, "Javascript enrichment").some

private val engine = new ScriptEngineManager(null)
.getEngineByMimeType("text/javascript")
.asInstanceOf[ScriptEngine with Invocable with Compilable]

private val stringified = rawFunction + """
function getJavascriptContexts(event) {
var result = process(event);
if (result == null) {
return "[]"
} else {
return JSON.stringify(result);
private val stringified = rawFunction + s"""
var getJavascriptContexts = function() {
const params = ${params.asJson.noSpaces};
return function(event) {
const result = process(event, params);
if (result == null) {
return "[]"
} else {
return JSON.stringify(result);
}
}
}
}()
"""

private val invocable =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class EnrichmentManagerSpec extends Specification with EitherMatchers with CatsE
val jsEnrichConf =
JavascriptScriptEnrichment.parse(config, schemaKey).toOption.get
val jsEnrich = JavascriptScriptEnrichment(jsEnrichConf.schemaKey, jsEnrichConf.rawFunction)
val enrichmentReg = EnrichmentRegistry[IO](javascriptScript = Some(jsEnrich))
val enrichmentReg = EnrichmentRegistry[IO](javascriptScript = List(jsEnrich))

val parameters = Map(
"e" -> "pp",
Expand Down Expand Up @@ -230,7 +230,7 @@ class EnrichmentManagerSpec extends Specification with EitherMatchers with CatsE
val jsEnrichConf =
JavascriptScriptEnrichment.parse(config, schemaKey).toOption.get
val jsEnrich = JavascriptScriptEnrichment(jsEnrichConf.schemaKey, jsEnrichConf.rawFunction)
val enrichmentReg = EnrichmentRegistry[IO](javascriptScript = Some(jsEnrich))
val enrichmentReg = EnrichmentRegistry[IO](javascriptScript = List(jsEnrich))

val parameters = Map(
"e" -> "pp",
Expand Down Expand Up @@ -876,7 +876,7 @@ class EnrichmentManagerSpec extends Specification with EitherMatchers with CatsE
SchemaVer.Full(1, 0, 0)
)
val enrichmentReg = EnrichmentRegistry[IO](
javascriptScript = Some(JavascriptScriptEnrichment(schemaKey, script)),
javascriptScript = List(JavascriptScriptEnrichment(schemaKey, script)),
httpHeaderExtractor = Some(HttpHeaderExtractorEnrichment(".*"))
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,39 @@ class EnrichmentConfigsSpec extends Specification with ValidatedMatchers with Da
val result = JavascriptScriptEnrichment.parse(javascriptScriptEnrichmentJson, schemaKey)
result must beValid // TODO: check the result's contents by evaluating some JavaScript
}
"parse the additional arguments" in {
val params = json"""{"foo": 3, "nested": {"bar": 42}}""".asObject.get
val script =
s"""|function process(event, params) {
| return [];
|}
|""".stripMargin
val javascriptScriptEnrichmentJson = {
val encoder = new Base64(true)
val encoded = new String(encoder.encode(script.getBytes)).trim // Newline being appended by some Base64 versions
parse(s"""{
"enabled": true,
"parameters": {
"script": "$encoded",
"config": {
"foo": 3,
"nested": {
"bar": 42
}
}
}
}""").toOption.get
}
val schemaKey = SchemaKey(
"com.snowplowanalytics.snowplow",
"javascript_script_config",
"jsonschema",
SchemaVer.Full(1, 0, 0)
)
val result = JavascriptScriptEnrichment.parse(javascriptScriptEnrichmentJson, schemaKey)
result must beValid
result.map(_.params).toOption mustEqual Some(params)
}
}

"Parsing a valid event_fingerprint_config enrichment JSON" should {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class JavascriptScriptEnrichmentSpec extends Specification {
Javascript enrichment should be able to proceed without return statement $e9
Javascript enrichment should be able to proceed with return null $e10
Javascript enrichment should be able to update the fields without return statement $e11
Javascript enrichment should be able to utilize the passed parameters $e12
"""

val schemaKey =
Expand Down Expand Up @@ -168,6 +169,19 @@ class JavascriptScriptEnrichmentSpec extends Specification {
enriched.app_id must beEqualTo(newAppId)
}

def e12 = {
val appId = "greatApp"
val enriched = buildEnriched(appId)
val params = json"""{"foo": "bar", "nested": {"foo": "newId"}}""".asObject.get
val function =
s"""
function process(event, params) {
event.setApp_id(params.nested.foo)
}"""
JavascriptScriptEnrichment(schemaKey, function, params).process(enriched)
enriched.app_id must beEqualTo("newId")
}

def buildEnriched(appId: String = "my super app"): EnrichedEvent = {
val e = new EnrichedEvent()
e.platform = "server"
Expand Down
5 changes: 4 additions & 1 deletion project/BuildSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ object BuildSettings {
// Build and publish
publishSettings ++
// Tests
scoverageSettings ++ noParallelTestExecution
scoverageSettings ++ noParallelTestExecution ++ Seq(
Test / fork := true,
Test / javaOptions := Seq("-Dnashorn.args=--language=es6")
)
}

lazy val commonFs2BuildSettings = {
Expand Down

0 comments on commit 865e8dc

Please sign in to comment.