Skip to content

Commit

Permalink
Integrate new substrata (#9)
Browse files Browse the repository at this point in the history
* update environment

* replace with new substrata

* Downgrade gradle to allow ./gradlew publishToSonatype to work.

* upgrade to the latest analytics library

* bug fix

* use week reference to hold application context

* update substrata dependency to use snapshot

* replace foreach call that cause lint issues

* notify dependent when live plugin is already loaded

---------

Co-authored-by: Wenxi Zeng <[email protected]>
Co-authored-by: Didier Garcia <[email protected]>
  • Loading branch information
3 people authored Apr 29, 2024
1 parent fc213fd commit f15e5b6
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 148 deletions.
32 changes: 15 additions & 17 deletions analytics-kotlin-live/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ plugins {
}

android {
compileSdk 31
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 31
targetSdk 34

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All @@ -26,27 +26,25 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
namespace 'com.segment.analytics.liveplugins.kotlin'
}

dependencies {
// Segment
api 'com.segment.analytics.kotlin:substrata:0.0.6'
implementation 'com.segment.analytics.kotlin:android:1.9.0'

implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.segment.analytics.kotlin:substrata:0.0.8-SNAPSHOT'
implementation 'com.segment.analytics.kotlin:android:1.15.0'

implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.20"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1'
implementation 'androidx.core:core-ktx:1.13.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

// V8
implementation 'com.eclipsesource.j2v8:j2v8:6.2.0@aar'
// implementation 'io.alicorn.v8:v8-adapter:1.59'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

apply from: rootProject.file('gradle/artifacts.gradle')
Expand Down
3 changes: 1 addition & 2 deletions analytics-kotlin-live/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.segment.analytics.liveplugins.kotlin">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -1,88 +1,98 @@
package com.segment.analytics.liveplugins.kotlin

import android.content.Context
import com.eclipsesource.v8.V8Object
import com.segment.analytics.kotlin.android.Analytics
import com.segment.analytics.kotlin.core.Analytics
import com.segment.analytics.kotlin.core.platform.Plugin
import com.segment.analytics.kotlin.core.utilities.getInt
import com.segment.analytics.kotlin.core.utilities.getString
import com.segment.analytics.substrata.kotlin.JSValue
import com.segment.analytics.substrata.kotlin.j2v8.J2V8Engine
import com.segment.analytics.substrata.kotlin.j2v8.fromV8Object
import com.segment.analytics.substrata.kotlin.j2v8.toV8Object
import com.segment.analytics.substrata.kotlin.JSObject
import com.segment.analytics.substrata.kotlin.JSScope
import com.segment.analytics.substrata.kotlin.JsonElementConverter
import java.lang.ref.WeakReference

object LivePluginsHolder {
var plugin: WeakReference<LivePlugins>? = null
}

class JSAnalytics {

internal var analytics: Analytics
internal var engine: J2V8Engine
internal val mainAnalytics: Boolean
internal lateinit var analytics: Analytics
internal lateinit var engine: JSScope
internal var mainAnalytics: Boolean = false

val anonymousId: String
get() = analytics.anonymousId()

val userId: String?
get() = analytics.userId()

val traits: V8Object?
get() {
var res: V8Object? = null
analytics.traits()?.let {
res = engine.underlying.toV8Object(it)
val traits: JSObject?
get() = analytics.traits()?.let {
engine.await {
JsonElementConverter.write(it, context) as JSObject
}
return res
}

private val context: Any?
val context: Any?
get() = analytics.configuration.application

// JSEngine requires an empty constructor to be able to export this class
constructor() {}

// This is the constructor used by the native to create the injected instance
constructor(analytics: Analytics, engine: J2V8Engine) {
constructor(analytics: Analytics, engine: JSScope) {
this.analytics = analytics
this.engine = engine
mainAnalytics = true
}

// This is the constructor used when JS creates a new one
constructor(writeKey: String, baseAnalytics: JSAnalytics) {
require(baseAnalytics.context is Context) {
constructor(writeKey: String) {
val application = LivePluginsHolder.plugin?.get()?.analytics?.configuration?.application
val engine = LivePluginsHolder.plugin?.get()?.engine
require(application != null) {
"Application Context Not Found!"
}
require(engine != null) {
"JS Engine is not initialized!"
}
require(application is Context) {
"Incompatible Android Context!"
}
this.analytics = Analytics(writeKey, baseAnalytics.context as Context)
this.engine = baseAnalytics.engine
this.analytics = Analytics(writeKey, application)
this.engine = engine
mainAnalytics = false
}

fun track(event: String) {
analytics.track(event)
}

fun track(event: String, properties: V8Object) {
analytics.track(event, fromV8Object(properties)!!)
fun track(event: String, properties: JSObject) {
analytics.track(event, JsonElementConverter.read(properties))
}

fun identify(userId: String) {
analytics.identify(userId)
}

fun identify(userId: String, traits: V8Object) {
analytics.identify(userId, fromV8Object(traits)!!)
fun identify(userId: String, traits: JSObject) {
analytics.identify(userId, JsonElementConverter.read(traits))
}

fun screen(title: String, category: String) {
analytics.screen(title, category)
}

fun screen(title: String, category: String, properties: V8Object) {
analytics.screen(title, fromV8Object(properties)!!, category)
fun screen(title: String, category: String, properties: JSObject) {
analytics.screen(title, JsonElementConverter.read(properties), category)
}

fun group(groupId: String) {
analytics.group(groupId)
}

fun group(groupId: String, traits: V8Object) {
analytics.group(groupId, fromV8Object(traits)!!)
fun group(groupId: String, traits: JSObject) {
analytics.group(groupId, JsonElementConverter.read(traits))
}

fun alias(newId: String) {
Expand All @@ -97,22 +107,15 @@ class JSAnalytics {
analytics.reset()
}

fun add(plugin: V8Object): Boolean {
fun add(plugin: JSObject): Boolean {
if (!mainAnalytics) return false // Only allow adding plugins to injected analytics

val jsPlugin = JSValue.JSObjectReference(plugin)
val jsPluginData = jsPlugin.content ?: return false

val type: Plugin.Type = jsPluginData.getInt("type")?.let {
pluginTypeFromInt(it)
} ?: return false
val destination: String? = jsPluginData.getString("destination")

val type: Plugin.Type = pluginTypeFromInt(plugin.getInt("type")) ?: return false
var result = false
val livePlugin = LivePlugin(jsPlugin, type, engine)
if (destination != null) {
val found = analytics.find(destination)
found?.let {
val livePlugin = LivePlugin(plugin, type, engine)
val destination = plugin["destination"]
if (destination is String) {
analytics.find(destination)?.let {
it.add(livePlugin)
result = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import com.segment.analytics.kotlin.core.Settings
import com.segment.analytics.kotlin.core.TrackEvent
import com.segment.analytics.kotlin.core.platform.EventPlugin
import com.segment.analytics.kotlin.core.platform.Plugin
import com.segment.analytics.substrata.kotlin.JSValue
import com.segment.analytics.substrata.kotlin.j2v8.J2V8Engine
import com.segment.analytics.substrata.kotlin.j2v8.toBaseEvent
import com.segment.analytics.substrata.kotlin.j2v8.toJSObject
import com.segment.analytics.kotlin.core.utilities.EncodeDefaultsJson
import com.segment.analytics.kotlin.core.utilities.LenientJson
import com.segment.analytics.kotlin.core.utilities.getString
import com.segment.analytics.substrata.kotlin.JSObject
import com.segment.analytics.substrata.kotlin.JSScope
import com.segment.analytics.substrata.kotlin.JsonElementConverter
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject

Expand All @@ -24,9 +27,9 @@ import kotlinx.serialization.json.jsonObject
* LivePlugin is responsible for ensuring all data being passed is understandable by JS
*/
internal class LivePlugin(
private val jsPlugin: JSValue.JSObjectReference,
private val jsPlugin: JSObject,
override val type: Plugin.Type,
private val engine: J2V8Engine
private val engine: JSScope
) : EventPlugin {

override lateinit var analytics: Analytics
Expand All @@ -35,11 +38,12 @@ internal class LivePlugin(
super.update(settings, type)

val settingsJson = Json.encodeToJsonElement(settings).jsonObject
engine.call(
jsPlugin,
"update",
listOf(JSValue.JSObject(settingsJson), JSValue.JSString(type.toString()))
)
engine.sync {
call(jsPlugin,
"update",
JsonElementConverter.write(settingsJson, context),
type.toString())
}
}

override fun alias(payload: AliasEvent): BaseEvent? {
Expand All @@ -63,12 +67,33 @@ internal class LivePlugin(
}

override fun execute(event: BaseEvent): BaseEvent? {
val modified = engine.call(jsPlugin, "execute", listOf(event.toJSObject()))
val payload = EncodeDefaultsJson.encodeToJsonElement(event)
val ret = engine.await {
val modified = call(
jsPlugin,
"execute",
JsonElementConverter.write(payload, context)
)

return@await if (modified is JSObject) {
JsonElementConverter.read(modified).jsonObject.toBaseEvent()
} else {
null
}
}
return ret
}

private fun JsonObject.toBaseEvent(): BaseEvent? {
val type = getString("type")

return if (modified !is JSValue.JSObject) {
null
} else {
modified.toBaseEvent()
return when (type) {
"identify" -> LenientJson.decodeFromJsonElement(IdentifyEvent.serializer(), this)
"track" -> LenientJson.decodeFromJsonElement(TrackEvent.serializer(), this)
"screen" -> LenientJson.decodeFromJsonElement(ScreenEvent.serializer(), this)
"group" -> LenientJson.decodeFromJsonElement(GroupEvent.serializer(), this)
"alias" -> LenientJson.decodeFromJsonElement(AliasEvent.serializer(), this)
else -> null
}
}
}
Loading

0 comments on commit f15e5b6

Please sign in to comment.