update guru_analytics

Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
3.0.0
Haoyi 2024-05-09 16:21:50 +08:00
parent 5927987ff5
commit 3803d53afe
23 changed files with 491 additions and 96 deletions

View File

@ -1,3 +1,39 @@
## v1.0.3
##### BugFix
- bugfix snapshotAnalyticsAudit uploaded > total
## v1.0.2
##### Feature
- init step callback
- snapshotAnalyticsAudit()
##### BugFix
- bugfix http exception
## v1.0.1
##### Feature
- init add uploadIpAddress
- CronetHelper init try catch
## v1.0.0
##### Feature
- CustomDns add cache
- init add isEnableCronet
- UserProperty guru_anm cronet/default
- PendingEvent 初始化成功前添加的埋点时间戳校正
- initialize uploadEventBaseUrl fromat check
## v0.3.2
##### Feature
- add fun peakUserProperties
- add fun getUserProperties
- add fun setEnableUpload
## v0.3.1
##### Feature

View File

@ -7,9 +7,11 @@ import android.widget.TextView
import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.db.model.EventPriority
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.handler.AnalyticsCode
class MainActivity : AppCompatActivity() {
private var enableUpload = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@ -36,6 +38,8 @@ class MainActivity : AppCompatActivity() {
.setXAppId("test_x_app_id")
.setXDeviceInfo("test_x_device_info")
.setMainProcess("com.example.guruanalytics")
.isEnableCronet(true)
.setUploadIpAddress(listOf("3.210.96.186", "34.196.69.199"))
.build()
findViewById<TextView>(R.id.tvLogEvent).setOnClickListener {
@ -76,9 +80,31 @@ class MainActivity : AppCompatActivity() {
GuruAnalytics.INSTANCE.setUploadEventBaseUrl(this, "https://www.castbox.fm/")
}
val tvEnable = findViewById<TextView>(R.id.tvEnable)
tvEnable?.setOnClickListener {
val enable = !enableUpload
GuruAnalytics.INSTANCE.setEnableUpload(enable)
enableUpload = enable
tvEnable.text = if (enableUpload) "Enable Upload" else "Disable Upload"
}
GuruAnalytics.INSTANCE.setScreen("main")
GuruAnalytics.INSTANCE.setAdId("AD_ID_01")
GuruAnalytics.INSTANCE.setUserProperty("uid", "110051")
GuruAnalytics.INSTANCE.setUserProperty("age", "12")
GuruAnalytics.INSTANCE.setUserProperty("sex", "male")
val properties = GuruAnalytics.INSTANCE.peakUserProperties()
Log.i("peakUserProperties", "properties:$properties")
GuruAnalytics.INSTANCE.getUserProperties {
Log.i("getUserProperties", "properties:$it")
}
tvEnable?.postDelayed({
val snapshot = GuruAnalytics.INSTANCE.snapshotAnalyticsAudit()
Log.i("snapshotAnalyticsAudit", "snapshot:$snapshot")
}, 10_000)
}
private val eventHandler: (Int, String?) -> Unit = { code, ext ->

View File

@ -70,4 +70,13 @@
android:text="Update BaseUrl"
android:layout_gravity="center_horizontal" />
<TextView
android:id="@+id/tvEnable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:padding="10dp"
android:text="Enable Upload"
android:layout_gravity="center_horizontal" />
</LinearLayout>

View File

@ -53,7 +53,9 @@ dependencies {
implementation okhttpDependencies
implementation process
implementation processDependencies
implementation workerDependencies
implementation cronetDependencies
}

View File

@ -21,6 +21,8 @@ ext {
preferenceVersion = '1.2.0'
processVersion = '2.4.0'
workVersion = '2.7.1'
cronetOkhttpVersion = '0.1.0'
playServicesCronetVersion = '18.0.1'
kaptDependencies = [
"androidx.room:room-compiler:$roomVersion",
@ -54,7 +56,12 @@ ext {
"androidx.work:work-rxjava2:$workVersion"
]
process = [
processDependencies = [
"androidx.lifecycle:lifecycle-process:$processVersion"
]
cronetDependencies = [
"com.google.net.cronet:cronet-okhttp:$cronetOkhttpVersion",
"com.google.android.gms:play-services-cronet:$playServicesCronetVersion"
]
}

View File

@ -18,7 +18,7 @@ publishing { // Repositories *to* which Gradle can publish artifacts
maven(MavenPublication) {
groupId 'guru.core.analytics'
artifactId 'guru_analytics'
version '0.3.1' // Your package version
version '1.0.3' // Your package version
// artifact publishArtifact //Example: *./target/myJavaClasses.jar*
// artifact "build/outputs/aar/aar-test-release.aar"//aar
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }

View File

@ -56,6 +56,12 @@ object Constants {
const val COIN = "coin"
const val EXP = "exp"
const val HP = "hp"
const val GURU_ANM = "guru_anm"
}
object AnmState {
const val CRONET = "cronet"
const val DEFAULT = "default"
}
object DeviceType {

View File

@ -4,7 +4,6 @@ import android.content.Context
import guru.core.analytics.data.db.model.EventStatistic
import guru.core.analytics.data.model.AnalyticsInfo
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.impl.GuruAnalyticsImpl
import java.io.File
@ -21,13 +20,15 @@ abstract class GuruAnalytics {
eventExpiredInDays: Int? = 7,
debug: Boolean = false,
persistableLog: Boolean = true,
listener: ((Int, String?) -> Unit)? = null,
eventHandlerCallback: ((Int, String?) -> Unit)? = null,
isInitPeriodicWork: Boolean = false,
uploadEventBaseUrl: String? = null,
fgEventPeriodInSeconds: Long? = null,
xAppId: String? = null,
xDeviceInfo: String? = null,
mainProcess: String? = null,
isEnableCronet: Boolean? = null,
uploadIpAddress: List<String>? = null,
)
abstract fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String)
@ -71,6 +72,17 @@ abstract class GuruAnalytics {
abstract fun removeUserProperties(keys: Set<String>)
abstract fun getUserProperties(callback: (Map<String, String>) -> Unit)
/**
* setUserProperty后立即获取, 可能无法获取到最新设置的值
*/
abstract fun peakUserProperties(): Map<String, String>
abstract fun setEnableUpload(enable: Boolean)
abstract fun snapshotAnalyticsAudit(): String
companion object {
val INSTANCE: GuruAnalytics by lazy() {
GuruAnalyticsImpl()
@ -116,6 +128,12 @@ abstract class GuruAnalytics {
fun setMainProcess(process: String) =
apply { analyticsInfo.mainProcess = process }
fun isEnableCronet(isEnableCronet: Boolean) =
apply { analyticsInfo.isEnableCronet = isEnableCronet }
fun setUploadIpAddress(uploadIpAddress: List<String>) =
apply { analyticsInfo.uploadIpAddress = uploadIpAddress }
fun build(): GuruAnalytics {
analyticsInfo.run {
INSTANCE.initialize(
@ -133,6 +151,8 @@ abstract class GuruAnalytics {
xAppId,
xDeviceInfo,
mainProcess,
isEnableCronet,
uploadIpAddress,
)
}
return INSTANCE

View File

@ -1,20 +1,28 @@
package guru.core.analytics.data.api
import android.content.Context
import android.net.Uri
import android.os.SystemClock
import guru.core.analytics.data.api.logging.LoggingInterceptor
import guru.core.analytics.data.api.cronet.CastboxCronetInterceptor
import guru.core.analytics.data.api.dns.CustomDns
import guru.core.analytics.data.api.dns.GoogleDnsApi
import guru.core.analytics.data.api.dns.GoogleDnsApiHost
import guru.core.analytics.data.api.logging.Level
import guru.core.analytics.data.api.logging.LoggingInterceptor
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.AndroidUtils
import guru.core.analytics.utils.DateTimeUtils
import guru.core.analytics.utils.GsonUtil
import okhttp3.*
import okhttp3.Dispatcher
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.internal.platform.Platform
import retrofit2.Converter
@ -35,6 +43,8 @@ object ServiceLocator {
private val headerParams = mutableMapOf<String, String>()
private var uploadIpAddress: List<String>? = null
fun addHeaderParam(key: String, value: String?) {
if (value.isNullOrBlank()) return
headerParams[key] = value
@ -44,20 +54,24 @@ object ServiceLocator {
this.debug = debug
}
fun provideGuruRepository(context: Context, baseUrl: String? = null): GuruRepository {
fun setUploadIpAddress(ipList: List<String>?) {
uploadIpAddress = ipList
}
fun provideGuruRepository(context: Context, baseUri: Uri? = null): GuruRepository {
synchronized(this) {
return guruRepository
?: createQuotesRepository(context, baseUrl).apply { guruRepository = this }
?: createQuotesRepository(context, baseUri).apply { guruRepository = this }
}
}
private fun createQuotesRepository(context: Context, baseUrl: String? = null): GuruRepository {
return DefaultGuruRepository(provideAnalyticsApi(context, baseUrl))
private fun createQuotesRepository(context: Context, baseUri: Uri? = null): GuruRepository {
return DefaultGuruRepository(provideAnalyticsApi(context, baseUri))
}
private fun provideAnalyticsApi(context: Context, baseUrl: String? = null): AnalyticsApi {
val finalBaseUrl = if (!baseUrl.isNullOrEmpty()) {
baseUrl
private fun provideAnalyticsApi(context: Context, baseUri: Uri? = null): AnalyticsApi {
val finalBaseUrl = if (baseUri != null) {
baseUri.toString()
} else {
val cacheBaseUrl = PreferencesManager.getInstance(context).uploadEventBaseUrl
if (cacheBaseUrl.isNullOrEmpty()) AnalyticsApiHost.BASE_URL else cacheBaseUrl
@ -114,20 +128,22 @@ object ServiceLocator {
maxRequests = 128
maxRequestsPerHost = 10
})
.dns(CustomDns(context))
.dns(CustomDns(context, uploadIpAddress))
.connectTimeout(20L, TimeUnit.SECONDS)
.readTimeout(readTimeOut, TimeUnit.SECONDS)
.writeTimeout(writeTimeOut, TimeUnit.SECONDS)
.addInterceptor(createCacheControlInterceptor(context))
.addInterceptor(createAnalyticsApiInterceptor())
.addInterceptor(createLoggingInterceptor())
.addInterceptor(createCronetInterceptor())
return builder.build()
}
private fun createDnsOkHttpClient(context: Context): OkHttpClient {
val builder = OkHttpClient.Builder()
.addInterceptor(createCacheControlInterceptor(context))
builder.addInterceptor(createLoggingInterceptor())
.addInterceptor(createLoggingInterceptor())
.addInterceptor(createCronetInterceptor())
return builder.build()
}
@ -140,6 +156,14 @@ object ServiceLocator {
.build()
}
/**
* Add the Cronet interceptor last, otherwise the subsequent interceptors will be skipped.
* https://github.com/google/cronet-transport-for-okhttp
*/
private fun createCronetInterceptor(): Interceptor {
return CastboxCronetInterceptor()
}
private fun createCacheControlInterceptor(context: Context) = Interceptor { chain ->
try {
val originalResponse = chain.proceed(chain.request())
@ -162,10 +186,7 @@ object ServiceLocator {
}
} catch (e: Exception) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_CACHE_CONTROL, e.message)
return@Interceptor exceptionResponse(
e,
chain.request()
)
throw e
}
}
@ -174,7 +195,6 @@ object ServiceLocator {
val request = chain.request()
val startTime = SystemClock.elapsedRealtime()
val builder = request.newBuilder()
.addHeader(CONTENT_TYPE, "application/json")
.addHeader(CONTENT_ENCODING, "gzip")
.addHeader(X_EVENT_TYPE, "event")
headerParams.forEach {
@ -182,17 +202,13 @@ object ServiceLocator {
}
val newRequest = builder.build()
try {
val response = chain.proceed(newRequest)
val responseTime = (SystemClock.elapsedRealtime() - startTime) / 2
calibrationTime(responseTime, response.headers)
if (response.isSuccessful) {
successResponse(response)
} else {
httpErrorResponse(response)
chain.proceed(newRequest).also { response ->
val responseTime = (SystemClock.elapsedRealtime() - startTime) / 2
calibrationTime(responseTime, response.headers)
}
} catch (e: Exception) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_API, e.message)
return@Interceptor exceptionResponse(e, newRequest)
throw e
}
}
}
@ -205,40 +221,6 @@ object ServiceLocator {
DateTimeUtils.initServerTime(date.time + responseTime)
minResponseTime = responseTime
}
private fun successResponse(response: Response): Response {
val body: ResponseBody?
var bodyString: String?
response.body.let {
bodyString = it?.string()
body = bodyString?.toResponseBody(it!!.contentType())
}
return response.newBuilder()
.body(body)
.build()
}
private fun httpErrorResponse(response: Response): Response {
val body =
"{\"code\": ${response.code}, \"msg\": \"${response.message}\", \"data\": null}".toResponseBody()
response.close()
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_RESPONSE, response.code.toString())
return response.newBuilder()
.code(1)
.body(body)
.build()
}
private fun exceptionResponse(e: Exception, request: Request): Response {
val content = "{\"code\": -1, \"msg\": \"${e.message}\", \"data\": null}"
return Response.Builder()
.code(1)
.request(request)
.protocol(Protocol.HTTP_1_1)
.message("${e.message}")
.body(content.toResponseBody())
.build()
}
}
const val CONTENT_TYPE = "Content-Type"

View File

@ -0,0 +1,27 @@
package guru.core.analytics.data.api.cronet
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import okhttp3.Interceptor
import okhttp3.Response
class CastboxCronetInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
return if (CronetHelper.cronetInterceptor != null) {
return try {
CronetHelper.cronetInterceptor!!.intercept(chain)
} catch (e: Exception) {
EventHandler.INSTANCE.notifyEventHandler(
AnalyticsCode.ERROR_CRONET_INTERCEPTOR,
e.message
)
throw e
}
} else {
chain.proceed(originalRequest)
}
}
}

View File

@ -0,0 +1,37 @@
package guru.core.analytics.data.api.cronet
import android.content.Context
import com.google.android.gms.net.CronetProviderInstaller
import com.google.net.cronet.okhttptransport.CronetInterceptor
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import org.chromium.net.CronetEngine
object CronetHelper {
var cronetInterceptor: CronetInterceptor? = null
private set
fun init(context: Context, isEnable: Boolean?, callback:((Boolean) -> Unit)? = null) {
if (isEnable == true) {
try {
CronetProviderInstaller.installProvider(context).addOnSuccessListener {
val cronetEngine = CronetEngine.Builder(context).build()
cronetInterceptor = CronetInterceptor.newBuilder(cronetEngine).build()
callback?.invoke(true)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.CRONET_INIT_SUCCESS)
}.addOnFailureListener {
cronetInterceptor = null
callback?.invoke(false)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.CRONET_INIT_FAIL, it.message)
}
} catch (e: Exception) {
callback?.invoke(false)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.CRONET_INIT_EXCEPTION, e.message)
}
} else {
cronetInterceptor = null
}
}
}

View File

@ -1,25 +1,44 @@
package guru.core.analytics.data.api.dns
import android.content.Context
import com.google.gson.reflect.TypeToken
import guru.core.analytics.data.api.ServiceLocator
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import guru.core.analytics.utils.GsonUtil
import kotlinx.coroutines.runBlocking
import okhttp3.Dns
import java.net.InetAddress
import java.net.UnknownHostException
class CustomDns(private val context: Context) : Dns {
class CustomDns(private val context: Context, private val uploadIpAddress: List<String>? = null) : Dns {
private val cachedHostAddress by lazy {
val hostAddressJson = PreferencesManager.getInstance(context).hostAddressJson
return@lazy runCatching {
if (!hostAddressJson.isNullOrBlank()) {
val mapType = object: TypeToken<Map<String, List<String>>>() {}.type
GsonUtil.gson.fromJson(hostAddressJson, mapType) as? MutableMap<String, List<String>>
} else null
}.getOrNull() ?: mutableMapOf()
}
override fun lookup(hostname: String): List<InetAddress> {
return try {
Dns.SYSTEM.lookup(hostname)
Dns.SYSTEM.lookup(hostname).also { list ->
cacheHostAddress(hostname, list.map { it.hostAddress })
}
} catch (e: UnknownHostException) {
try {
lookupByGoogleDns(hostname)
} catch (e: UnknownHostException) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DNS, e.message)
lookupByRemoteConfig(hostname)
try {
lookupByCacheAndRemote(hostname)
} catch (e: UnknownHostException) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DNS, e.message)
lookupByRemoteConfig(hostname)
}
}
}
}
@ -32,7 +51,11 @@ class CustomDns(private val context: Context) : Dns {
?.mapNotNull { it.data }
}
if (!ipList.isNullOrEmpty()) {
return ipList.map { InetAddress.getByAddress(convert(it)) }
cacheHostAddress(hostname, ipList)
val resultIpList = ipList.mapNotNull { convert(it) }
if (resultIpList.isNotEmpty()) {
return resultIpList
}
}
throw UnknownHostException("Broken Google dns lookup of $hostname")
}
@ -41,13 +64,44 @@ class CustomDns(private val context: Context) : Dns {
// val dnsConfig = RemoteConfig.getDnsConfig()
// val ipList = dnsConfig?.get(hostname)
// if (ipList != null && ipList.isNotEmpty()) {
// return ipList.map { InetAddress.getByAddress(convert(it)) }
// return ipList.mapNotNull { convert(it) }
// }
throw UnknownHostException("Broken remote config lookup of $hostname")
}
private fun convert(ip: String): ByteArray {
val ipArray = ip.split(".").map { Integer.parseInt(it).toByte() }
return ipArray.toByteArray()
private fun convert(ip: String): InetAddress? {
return runCatching {
val byteArr = ip.split(".").map { Integer.parseInt(it).toByte() }.toByteArray()
InetAddress.getByAddress(byteArr)
}.getOrNull()
}
private fun cacheHostAddress(hostname: String, hostAddressList: List<String?>) {
val addressList = hostAddressList.filterNotNull()
if (addressList.isEmpty()) return
try {
val cacheHostAddressList = cachedHostAddress[hostname]?.sorted()
if (addressList.sorted() != cacheHostAddressList) {
cachedHostAddress[hostname] = addressList
PreferencesManager.getInstance(context).hostAddressJson = GsonUtil.gson.toJson(cachedHostAddress)
}
} catch (e: Exception) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DNS_CACHE, e.message)
}
}
private fun lookupByCacheAndRemote(hostname: String): List<InetAddress> {
val ipList = uploadIpAddress?.toMutableList() ?: mutableListOf()
val cacheIpList = cachedHostAddress[hostname]
if (!cacheIpList.isNullOrEmpty()) {
ipList.addAll(cacheIpList)
}
if (ipList.isNotEmpty()) {
val resultIpList = ipList.mapNotNull { convert(it) }
if (resultIpList.isNotEmpty()) {
return resultIpList
}
}
throw UnknownHostException("Broken cache dns lookup of $hostname")
}
}

View File

@ -48,4 +48,5 @@ class PreferencesManager private constructor(
var eventCountUploaded: Int? by bind("event_count_uploaded", 0)
var uploadEventBaseUrl: String? by bind("update_event_base_url", "")
var totalDurationFgEvent: Long? by bind("total_duration_fg_event", 0L)
var hostAddressJson: String? by bind("host_address", "")
}

View File

@ -14,4 +14,6 @@ internal data class AnalyticsInfo(
var xAppId: String? = null,
var xDeviceInfo: String? = null,
var mainProcess: String? = null,
var isEnableCronet: Boolean? = null,
var uploadIpAddress: List<String>? = null,
)

View File

@ -0,0 +1,59 @@
package guru.core.analytics.data.model
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GuruAnalyticsAuditSnapshot(
@SerializedName("initialized") val initialized: Boolean = false,
@SerializedName("engineInitialized") val engineInitialized: Boolean = false,
@SerializedName("useCronet") val useCronet: Boolean = false,
@SerializedName("eventDispatcherStarted") val eventDispatcherStarted: Boolean = false,
@SerializedName("fgHelperInitialized") val fgHelperInitialized: Boolean = false,
@SerializedName("connectionState") val connectionState: Boolean = false,
@SerializedName("total") val total: Int = 0,
@SerializedName("deleted") val deleted: Int = 0,
@SerializedName("uploaded") val uploaded: Int = 0,
@SerializedName("sessionUploaded") val sessionUploaded: Int = 0,
@SerializedName("sessionDeleted") val sessionDeleted: Int = 0,
@SerializedName("sessionTotal") val sessionTotal: Int = 0,
@SerializedName("uploadReady") val uploadReady: Boolean = false,
)
object GuruAnalyticsAudit {
var initialized: Boolean = false
var engineInitialized: Boolean = false
var useCronet: Boolean = false
var eventDispatcherStarted: Boolean = false
var fgHelperInitialized: Boolean = false
var connectionState: Boolean = false
// 整体的
var total: Int = 0
var deleted: Int = 0
var uploaded: Int = 0
// 当前这一次
var sessionUploaded: Int = 0
var sessionDeleted: Int = 0
var sessionTotal: Int = 0
var uploadReady: Boolean = false
fun snapshot() = GuruAnalyticsAuditSnapshot(
initialized = initialized,
engineInitialized = engineInitialized,
useCronet = useCronet,
eventDispatcherStarted = eventDispatcherStarted,
fgHelperInitialized = fgHelperInitialized,
connectionState = connectionState,
total = total,
deleted = deleted,
uploaded = uploaded,
sessionUploaded = sessionUploaded,
sessionDeleted = sessionDeleted,
sessionTotal = sessionTotal,
uploadReady = uploadReady
)
}

View File

@ -10,6 +10,7 @@ import guru.core.analytics.utils.DateTimeUtils
import guru.core.analytics.utils.GsonUtil
import io.reactivex.subjects.BehaviorSubject
import java.util.*
import java.util.concurrent.ConcurrentHashMap
object EventInfoStore {
@ -18,9 +19,11 @@ object EventInfoStore {
UUID.randomUUID().toString()
}
private val propertiesSubject: BehaviorSubject<LinkedHashMap<String, String>> =
private val propertiesSubject: BehaviorSubject<ConcurrentHashMap<String, String>> =
BehaviorSubject.createDefault(
linkedMapOf()
ConcurrentHashMap<String, String>().apply {
this[Constants.Properties.GURU_ANM] = Constants.AnmState.DEFAULT
}
)
private val supplementEventParamsSubject: BehaviorSubject<Map<String, Any>> =
@ -39,8 +42,8 @@ object EventInfoStore {
idsSubject.onNext(value)
}
private var properties: LinkedHashMap<String, String>
get() = propertiesSubject.value ?: linkedMapOf()
var properties: ConcurrentHashMap<String, String>
get() = propertiesSubject.value ?: ConcurrentHashMap()
set(value) {
propertiesSubject.onNext(value)
}
@ -104,7 +107,12 @@ object EventInfoStore {
}
}
fun deriveEvent(event: EventItem, priority: Int = EventPriority.DEFAULT, uploading: Boolean = false): EventEntity {
fun deriveEvent(
event: EventItem,
priority: Int = EventPriority.DEFAULT,
uploading: Boolean = false,
elapsed: Long = 0
): EventEntity {
val eventMap = mutableMapOf<String, ParamValue>()
if (!event.itemCategory.isNullOrBlank()) {
eventMap[Constants.Event.ITEM_CATEGORY] = ParamValue(s = event.itemCategory)
@ -124,8 +132,9 @@ object EventInfoStore {
eventMap[entry.key] = createParamValue(entry.value)
}
val eventId = UUID.randomUUID().toString()
val eventTs = DateTimeUtils.eventAt() - elapsed
val eventData = Event(
timestamp = DateTimeUtils.eventAt(),
timestamp = eventTs,
info = ids,
event = event.eventName,
param = eventMap,
@ -139,7 +148,7 @@ object EventInfoStore {
json = eventJson,
ext = "",
status = if (uploading) 1 else 0,
at = DateTimeUtils.eventAt(),
at = eventTs,
version = Constants.VERSION,
event = event.eventName,
priority = priority

View File

@ -9,11 +9,15 @@ enum class AnalyticsCode(val code: Int) {
UPLOAD_SUCCESS(13), // 上报事件成功
UPLOAD_FAIL(14), // 上报事件失败
PERIODIC_WORK_ENQUEUE(15), // 开启PeriodicWork
ENABLE_UPLOAD(16), // 修改是否允许上传埋点状态
NETWORK_AVAILABLE(21), // 网络状态可用
NETWORK_LOST(22), // 网络状态不可用
LIFECYCLE_START(23), // app可见
LIFECYCLE_PAUSE(24), // app不可见
CRONET_INIT_SUCCESS(25), // 开启Cronet成功
CRONET_INIT_FAIL(26), // 开启Cronet失败
CRONET_INIT_EXCEPTION(27), // 开启Cronet报错
ERROR_API(101), // 调用api出错
ERROR_RESPONSE(102), // api返回结果错误
@ -22,7 +26,20 @@ enum class AnalyticsCode(val code: Int) {
ERROR_LOAD_MARK(105), // 从数据库取事件以及更改事件状态为正在上报出错
ERROR_DNS(106), // dns 错误
ERROR_ZIP(107), // zip 错误
ERROR_DNS_CACHE(108), // zip 错误
ERROR_CRONET_INTERCEPTOR(109),// cronet拦截器
EVENT_FIRST_OPEN(1001), // first_open 事件
EVENT_FG(1002), // fg 事件
// 初始化进度
INIT_STEP_1(100001),
INIT_STEP_2(100002),
INIT_STEP_3(100003),
INIT_STEP_4(100004),
INIT_STEP_5(100005),
INIT_STEP_6(100006),
INIT_STEP_7(100007),
INIT_STEP_8(100008),
INIT_STEP_9(100009),
}

View File

@ -1,16 +1,26 @@
package guru.core.analytics.impl
import android.os.SystemClock
import guru.core.analytics.data.db.GuruAnalyticsDatabase
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.data.model.EventItem
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.data.store.EventInfoStore
import timber.log.Timber
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
class PendingEvent(
val item: EventItem,
val options: AnalyticsOptions
) {
val at = SystemClock.elapsedRealtime()
}
object EventDispatcher : EventDeliver {
private val pendingEvents = ConcurrentLinkedQueue<Pair<EventItem, AnalyticsOptions>>()
private val pendingEvents = ConcurrentLinkedQueue<PendingEvent>()
private val started = AtomicBoolean(false)
@ -18,8 +28,12 @@ object EventDispatcher : EventDeliver {
Timber.d("EventDispatcher dispatchPendingEvent ${pendingEvents.size}!")
if (pendingEvents.isNotEmpty()) {
while (true) {
val pair = pendingEvents.poll() ?: return
val event = EventInfoStore.deriveEvent(pair.first, priority = pair.second.priority)
val pendingEvent = pendingEvents.poll() ?: return
val event = EventInfoStore.deriveEvent(
pendingEvent.item,
priority = pendingEvent.options.priority,
elapsed = SystemClock.elapsedRealtime() - pendingEvent.at
)
GuruAnalyticsDatabase.getInstance().eventDao().addEvent(event)
}
}
@ -29,6 +43,7 @@ object EventDispatcher : EventDeliver {
if (started.compareAndSet(false, true)) {
Timber.d("EventDispatcher started!")
dispatchPendingEvent()
GuruAnalyticsAudit.eventDispatcherStarted = true
}
}
@ -40,7 +55,7 @@ object EventDispatcher : EventDeliver {
GuruAnalyticsDatabase.getInstance().eventDao().addEvent(event)
} else {
Timber.d("EventDispatcher deliverEvent pending!")
pendingEvents.offer(item to options)
pendingEvents.offer(PendingEvent(item, options))
}
}

View File

@ -1,6 +1,7 @@
package guru.core.analytics.impl
import android.content.Context
import android.net.Uri
import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.api.GuruRepository
@ -12,6 +13,7 @@ import guru.core.analytics.data.db.model.EventStatistic
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.data.model.EventItem
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.data.store.EventInfoStore
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
@ -39,10 +41,10 @@ internal class EventEngine internal constructor(
private val batchLimit: Int = DEFAULT_BATCH_LIMIT,
private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS,
private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS,
private val uploadEventBaseUrl: String? = null,
private val uploadEventBaseUri: Uri? = null,
) : EventDeliver {
private val guruRepository: GuruRepository by lazy {
ServiceLocator.provideGuruRepository(context.applicationContext, baseUrl = uploadEventBaseUrl)
ServiceLocator.provideGuruRepository(context.applicationContext, baseUri = uploadEventBaseUri)
}
private val preferencesManager by lazy {
@ -83,6 +85,13 @@ internal class EventEngine internal constructor(
private val forceUploadSubject: PublishSubject<Boolean> = PublishSubject.create()
private val started = AtomicBoolean(false)
private var enableUpload = true
fun setEnableUpload(enable: Boolean) {
enableUpload = enable
val extMap = mapOf("enable" to enable)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ENABLE_UPLOAD, extMap)
}
fun start(startUploadDelay: Long?) {
if (started.compareAndSet(false, true)) {
@ -96,6 +105,7 @@ internal class EventEngine internal constructor(
val extMap = mapOf("startUploadDelayInSecond" to startUploadDelay)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.STATE_START_WORK, extMap)
}, startUploadDelay ?: 0, TimeUnit.SECONDS)
GuruAnalyticsAudit.engineInitialized = true
}
}
@ -146,6 +156,7 @@ internal class EventEngine internal constructor(
logDebug("pendingEvent ${it.size}")
}
val networkFlowable = ConnectionStateMonitor.connectStateFlowable.doOnNext {
GuruAnalyticsAudit.connectionState = it
logDebug("network $it")
}
val forceFlowable = forceUploadSubject.toFlowable(BackpressureStrategy.DROP)
@ -157,6 +168,7 @@ internal class EventEngine internal constructor(
logDebug("pollEvent filter $it")
return@filter it
}
.filter { enableUpload }
.flatMap { uploadEvents(500) }
.subscribe()
)
@ -174,7 +186,10 @@ internal class EventEngine internal constructor(
// 记录删除的事件数量
if (pair.first > 0) {
val eventCountDeleted = preferencesManager.eventCountDeleted ?: 0
preferencesManager.eventCountDeleted = eventCountDeleted + pair.first
val deleted = eventCountDeleted + pair.first
preferencesManager.eventCountDeleted = deleted
GuruAnalyticsAudit.deleted = deleted
GuruAnalyticsAudit.sessionDeleted += pair.first
val extMap = mapOf(
"expiredCount" to pair.first,
@ -192,6 +207,7 @@ internal class EventEngine internal constructor(
internal fun uploadEvents(count: Int): Flowable<List<EventEntity>> {
val eventDao = GuruAnalyticsDatabase.getInstance().eventDao()
GuruAnalyticsAudit.uploadReady = true
logDebug("uploadEvents: $count")
return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) }
.onErrorReturn {
@ -232,7 +248,10 @@ internal class EventEngine internal constructor(
.doOnSuccess {
// 记录上传成功的数量
val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0
preferencesManager.eventCountUploaded = eventCountUploaded + entities.size
val uploaded = eventCountUploaded + entities.size
preferencesManager.eventCountUploaded = uploaded
GuruAnalyticsAudit.uploaded = uploaded
GuruAnalyticsAudit.sessionUploaded += entities.size
val extMap = mapOf(
"count" to entities.size,
@ -288,7 +307,10 @@ internal class EventEngine internal constructor(
private fun increaseEventCount() {
val eventCountAll = preferencesManager.eventCountAll ?: 0
preferencesManager.eventCountAll = eventCountAll + 1
val total = eventCountAll + 1
preferencesManager.eventCountAll = total
GuruAnalyticsAudit.total = total
GuruAnalyticsAudit.sessionTotal ++
}
override fun deliverProperty(name: String, value: String) {

View File

@ -5,6 +5,7 @@ import android.os.SystemClock
import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.handler.AnalyticsCode
import guru.core.analytics.handler.EventHandler
import io.reactivex.Flowable
@ -75,6 +76,7 @@ internal class FgHelper(context: Context) {
}, {
Timber.tag("FgHelper").e(it)
})
GuruAnalyticsAudit.fgHelperInitialized = true
}
fun stop() {

View File

@ -1,16 +1,19 @@
package guru.core.analytics.impl
import android.content.Context
import android.net.Uri
import androidx.annotation.RequiresPermission
import androidx.work.*
import guru.core.analytics.Constants
import guru.core.analytics.GuruAnalytics
import guru.core.analytics.data.api.ServiceLocator
import guru.core.analytics.data.api.cronet.CronetHelper
import guru.core.analytics.data.db.GuruAnalyticsDatabase
import guru.core.analytics.data.db.model.EventStatistic
import guru.core.analytics.data.local.PreferencesManager
import guru.core.analytics.data.model.AnalyticsOptions
import guru.core.analytics.data.model.EventItem
import guru.core.analytics.data.model.GuruAnalyticsAudit
import guru.core.analytics.data.store.DeviceInfoStore
import guru.core.analytics.data.store.EventInfoStore
import guru.core.analytics.handler.AnalyticsCode
@ -18,6 +21,7 @@ import guru.core.analytics.handler.EventHandler
import guru.core.analytics.log.PersistentTree
import guru.core.analytics.utils.AndroidUtils
import guru.core.analytics.utils.EventChecker
import guru.core.analytics.utils.GsonUtil
import guru.core.analytics.utils.SystemProperties
import timber.log.Timber
import java.io.File
@ -75,24 +79,55 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
fgEventPeriodInSeconds: Long?,
xAppId: String?,
xDeviceInfo: String?,
mainProcess: String?
mainProcess: String?,
isEnableCronet: Boolean?,
uploadIpAddress: List<String>?,
) {
if (initialized.compareAndSet(false, true)) {
delivers.add(EventDispatcher)
eventHandlerCallback?.let { EventHandler.INSTANCE.addEventHandler(it) }
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_1)
val debugApp = SystemProperties.read("debug.guru.analytics.app")
val forceDebug = debugApp == context.packageName
if (forceDebug || persistableLog) {
Timber.plant(PersistentTree(context, debug = debug))
}
debugMode = debug
Timber.d("[$internalVersion]initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debug")
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_2)
val debugUrl = SystemProperties.read("debug.guru.analytics.url")
debugMode = forceDebug || debug
Timber.d("[$internalVersion]initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debugMode debugUrl:$debugUrl")
GuruAnalyticsDatabase.initialize(context.applicationContext)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_3)
val baseUrl = if (forceDebug && debugUrl.isNotEmpty()) debugUrl else uploadEventBaseUrl
DeviceInfoStore.setDeviceInfo(context)
ServiceLocator.setDebug(debug)
ServiceLocator.addHeaderParam("X-APP-ID", xAppId)
ServiceLocator.addHeaderParam("X-DEVICE-INFO", xDeviceInfo)
ServiceLocator.setUploadIpAddress(uploadIpAddress)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_4)
CronetHelper.init(context, isEnableCronet) {
if (it) {
setUserProperty(Constants.Properties.GURU_ANM, Constants.AnmState.CRONET)
GuruAnalyticsAudit.useCronet = true
}
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_5)
var uploadEventBaseUri: Uri? = null
if (!baseUrl.isNullOrBlank()) {
uploadEventBaseUri =
kotlin.runCatching { Uri.parse(baseUrl) }.getOrNull()
if (uploadEventBaseUri?.scheme?.startsWith("http") != true) {
throw IllegalArgumentException("initialize updateEventBaseUrl:${baseUrl} incorrect format")
}
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_6)
engine = EventEngine(
context,
@ -102,10 +137,11 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
eventExpiredInDays = EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS.coerceAtLeast(
eventExpiredInDays ?: EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS
),
uploadEventBaseUrl = uploadEventBaseUrl,
uploadEventBaseUri = uploadEventBaseUri
).apply {
start(startUploadDelayInSecond)
delivers.add(this)
start(startUploadDelayInSecond)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_7)
}
if (isInitPeriodicWork) {
val process = AndroidUtils.getProcessName(context)
@ -120,6 +156,7 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
}
}
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_8)
val extMap = mapOf(
"version_code" to internalVersion,
@ -127,12 +164,17 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
"uploadPeriodInSecond" to uploadPeriodInSeconds,
"startUploadDelayInSecond" to startUploadDelayInSecond,
"eventExpiredInDays" to eventExpiredInDays,
"uploadEventBaseUri" to uploadEventBaseUri.toString(),
"enabledCronet" to (isEnableCronet ?: false),
"unloadIpAddress" to (uploadIpAddress?.joinToString("|") ?: ""),
"debug" to debug,
)
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.STATE_INITIALIZED, extMap)
if (!uploadEventBaseUrl.isNullOrEmpty()) {
PreferencesManager.getInstance(context).uploadEventBaseUrl = uploadEventBaseUrl
PreferencesManager.getInstance(context).uploadEventBaseUrl = baseUrl
}
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_STEP_9)
GuruAnalyticsAudit.initialized = true
}
}
@ -265,6 +307,25 @@ internal class GuruAnalyticsImpl : GuruAnalytics() {
removeProperties(keys)
}
override fun getUserProperties(callback: (Map<String, String>) -> Unit) {
deliverExecutor.execute {
callback.invoke(EventInfoStore.properties)
}
}
override fun peakUserProperties(): Map<String, String> {
return EventInfoStore.properties
}
override fun setEnableUpload(enable: Boolean) {
engine?.setEnableUpload(enable)
}
override fun snapshotAnalyticsAudit(): String {
val snapshot = GuruAnalyticsAudit.snapshot()
return GsonUtil.gson.toJson(snapshot)
}
private fun deliverEvent(item: EventItem, options: AnalyticsOptions) {
Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!")
deliverExecutor.execute {

View File

@ -11,6 +11,7 @@ object ApiParamUtils {
* 组装接口上传需要的json参数
*/
fun generateApiParam(events: List<EventEntity>): String {
/// todo: 版本需要根据当前的更新
val deviceInfoJson = GsonUtil.gson.toJson(DeviceInfoStore.deviceInfo)
return "{\"version\":${Constants.VERSION},\"events\":[${events.joinToString(",") { it.json }}],\"deviceInfo\":$deviceInfoJson}"
}

View File

@ -19,7 +19,7 @@ object GZipUtils {
out.toByteArray()
} catch (e: IOException) {
EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_ZIP, e.message)
null
throw e
}
}