diff --git a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/EnrichmentConf.scala b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/EnrichmentConf.scala index fd8cd0cb6..528968cbd 100644 --- a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/EnrichmentConf.scala +++ b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/EnrichmentConf.scala @@ -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 @@ -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( diff --git a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/JavascriptScriptEnrichment.scala b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/JavascriptScriptEnrichment.scala index 99eea9060..4578b047a 100644 --- a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/JavascriptScriptEnrichment.scala +++ b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/JavascriptScriptEnrichment.scala @@ -15,6 +15,7 @@ import cats.implicits._ import io.circe._ import io.circe.parser._ +import io.circe.syntax._ import javax.script._ @@ -52,11 +53,16 @@ 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 @@ -64,15 +70,18 @@ final case class JavascriptScriptEnrichment(schemaKey: SchemaKey, rawFunction: S .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 = diff --git a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/EnrichmentRegistrySpec.scala b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/EnrichmentRegistrySpec.scala index c71401dfb..d997c5668 100644 --- a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/EnrichmentRegistrySpec.scala +++ b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/EnrichmentRegistrySpec.scala @@ -29,6 +29,7 @@ class EnrichmentRegistrySpec extends Specification with CatsEffect { EnrichmentRegistry should parse array of enrichments without any JS enrichment correctly $noJSEnrichment EnrichmentRegistry should parse array of enrichments with single JS enrichment correctly $singleJSEnrichment EnrichmentRegistry should parse array of enrichments with multiple JS enrichments correctly $multipleJSEnrichments + EnrichmentRegistry should parse JS enrichment with config field correctly $jsEnrichmentWithConfig """ def noJSEnrichment = @@ -67,7 +68,7 @@ class EnrichmentRegistrySpec extends Specification with CatsEffect { def multipleJSEnrichments = { val jsReturns = List("return1", "return2") - val jsEnrichments = jsReturns.map(jsEnrichment) + val jsEnrichments = jsReturns.map(jsEnrichment(_)) EnrichmentRegistry .parse[IO]( enrichmentConfig(jsEnrichments), @@ -85,18 +86,50 @@ class EnrichmentRegistrySpec extends Specification with CatsEffect { } } } + + def jsEnrichmentWithConfig = { + val jsEnrichments = List(jsEnrichment(addConfig = true)) + EnrichmentRegistry + .parse[IO]( + enrichmentConfig(jsEnrichments), + SpecHelpers.client, + localMode = false, + SpecHelpers.registryLookup + ) + .map { res => + val jsConfs = res.getOrElse(List.empty).filter { + case _: EnrichmentConf.JavascriptScriptConf => true + case _ => false + } + jsConfs.size must beEqualTo(1) + } + } } object EnrichmentRegistrySpec { - def jsEnrichment(jsReturn: String = "defaultReturn"): Json = { + def jsEnrichment(jsReturn: String = "defaultReturn", addConfig: Boolean = false): Json = { val script = s""" function process(event) { return $jsReturn; } """ - json"""{ + val config = json"""{ + "schema": "iglu:com.snowplowanalytics.snowplow/javascript_script_config/jsonschema/1-0-1", + "data": { + "parameters": { + "config": { + "foo": 3, + "nested": { + "bar": 42 + } + } + } + } + }""" + + val jsEnrichment = json"""{ "schema": "iglu:com.snowplowanalytics.snowplow/javascript_script_config/jsonschema/1-0-0", "data": { "vendor": "com.snowplowanalytics.snowplow", @@ -107,6 +140,7 @@ object EnrichmentRegistrySpec { } } }""" + if (addConfig) jsEnrichment.deepMerge(config) else jsEnrichment } // Vendor and name are intentionally tweaked in the first enrichment diff --git a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/EnrichmentConfigsSpec.scala b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/EnrichmentConfigsSpec.scala index 56b591c9d..01c97895a 100644 --- a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/EnrichmentConfigsSpec.scala +++ b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/EnrichmentConfigsSpec.scala @@ -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 { diff --git a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/JavascriptScriptEnrichmentSpec.scala b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/JavascriptScriptEnrichmentSpec.scala index b7a424028..d596b8c86 100644 --- a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/JavascriptScriptEnrichmentSpec.scala +++ b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/JavascriptScriptEnrichmentSpec.scala @@ -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 = @@ -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" diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 7001f53cf..ee55dfbb7 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -187,7 +187,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 = {