Skip to content

Commit f730341

Browse files
Merge pull request #191 from statsig-io/diag_core_api
Diagnostics core api
2 parents 826e736 + 4e3e70f commit f730341

File tree

7 files changed

+213
-17
lines changed

7 files changed

+213
-17
lines changed

src/main/kotlin/com/statsig/sdk/Diagnostics.kt

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ const val NANO_IN_MS = 1_000_000.0
66
const val MAX_SAMPLING_RATE = 10_000
77
internal class Diagnostics(private var isDisabled: Boolean, private var logger: StatsigLogger) {
88
var diagnosticsContext: ContextType = ContextType.INITIALIZE
9-
private val samplingRates: MutableMap<String, Int> = mutableMapOf("dcs" to 0, "log" to 0, "initialize" to MAX_SAMPLING_RATE, "idlist" to 0)
10-
private var markers: DiagnosticsMarkers = mutableMapOf()
9+
private val samplingRates: MutableMap<String, Int> = mutableMapOf(
10+
"dcs" to 0,
11+
"log" to 0,
12+
"initialize" to MAX_SAMPLING_RATE,
13+
"idlist" to 0,
14+
"api_call" to 0,
15+
)
16+
internal var markers: DiagnosticsMarkers = mutableMapOf()
1117

1218
fun setSamplingRate(rates: Map<String, Int>) {
1319
rates.forEach { entry ->
@@ -23,10 +29,11 @@ internal class Diagnostics(private var isDisabled: Boolean, private var logger:
2329
}
2430
}
2531

26-
fun markStart(key: KeyType, step: StepType? = null, additionalMarker: Marker? = null) {
32+
fun markStart(key: KeyType, step: StepType? = null, context: ContextType? = null, additionalMarker: Marker? = null) {
2733
if (isDisabled) {
2834
return
2935
}
36+
val contextType = context ?: diagnosticsContext
3037
val marker = Marker(key = key, action = ActionType.START, timestamp = System.nanoTime() / NANO_IN_MS, step = step)
3138
when (key) {
3239
KeyType.GET_ID_LIST -> {
@@ -38,13 +45,20 @@ internal class Diagnostics(private var isDisabled: Boolean, private var logger:
3845
}
3946
}
4047
}
41-
this.addMarker(marker)
48+
when (contextType) {
49+
ContextType.API_CALL -> {
50+
marker.configName = additionalMarker?.configName
51+
marker.markerID = additionalMarker?.markerID
52+
}
53+
}
54+
this.addMarker(marker, contextType)
4255
}
4356

44-
fun markEnd(key: KeyType, success: Boolean, step: StepType? = null, additionalMarker: Marker? = null) {
57+
fun markEnd(key: KeyType, success: Boolean, step: StepType? = null, context: ContextType? = null, additionalMarker: Marker? = null) {
4558
if (isDisabled) {
4659
return
4760
}
61+
val contextType = context ?: diagnosticsContext
4862
val marker = Marker(key = key, action = ActionType.END, success = success, timestamp = System.nanoTime() / NANO_IN_MS, step = step)
4963
when (key) {
5064
KeyType.DOWNLOAD_CONFIG_SPECS -> {
@@ -72,24 +86,31 @@ internal class Diagnostics(private var isDisabled: Boolean, private var logger:
7286
marker.reason = additionalMarker?.reason
7387
}
7488
}
75-
this.addMarker(marker)
89+
when (contextType) {
90+
ContextType.API_CALL -> {
91+
marker.configName = additionalMarker?.configName
92+
marker.markerID = additionalMarker?.markerID
93+
}
94+
}
95+
this.addMarker(marker, contextType)
7696
}
7797

78-
private fun shouldLogDiagnostics(context: ContextType): Boolean {
98+
internal fun shouldLogDiagnostics(context: ContextType): Boolean {
7999
val samplingKey: String =
80100
when (context) {
81101
ContextType.CONFIG_SYNC -> "dcs"
82102
ContextType.INITIALIZE -> "initialize"
103+
ContextType.API_CALL -> "api_call"
83104
}
84105
val rand = Math.random() * MAX_SAMPLING_RATE
85106
return samplingRates[samplingKey] ?: 0 > rand
86107
}
87108

88-
private fun addMarker(marker: Marker) {
89-
if (this.markers[diagnosticsContext] == null) {
90-
this.markers[diagnosticsContext] = mutableListOf()
109+
private fun addMarker(marker: Marker, context: ContextType) {
110+
if (this.markers[context] == null) {
111+
this.markers[context] = mutableListOf()
91112
}
92-
this.markers[diagnosticsContext]?.add(marker)
113+
this.markers[context]?.add(marker)
93114
this.markers.values
94115
}
95116

src/main/kotlin/com/statsig/sdk/DiagnosticsData.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.statsig.sdk
33
import com.google.gson.annotations.SerializedName
44

55
data class Marker(
6+
@SerializedName("markerID") var markerID: String? = null,
67
@SerializedName("key") val key: KeyType? = null,
78
@SerializedName("action") val action: ActionType? = null,
89
@SerializedName("timestamp") val timestamp: Double? = null,
@@ -13,6 +14,7 @@ data class Marker(
1314
@SerializedName("idListCount") var idListCount: Int? = null,
1415
@SerializedName("reason") var reason: String? = null,
1516
@SerializedName("sdkRegion") var sdkRegion: String? = null,
17+
@SerializedName("configName") var configName: String? = null,
1618
)
1719

1820
enum class ContextType {
@@ -21,6 +23,9 @@ enum class ContextType {
2123

2224
@SerializedName("config_sync")
2325
CONFIG_SYNC,
26+
27+
@SerializedName("api_call")
28+
API_CALL,
2429
}
2530

2631
enum class KeyType {
@@ -38,6 +43,35 @@ enum class KeyType {
3843

3944
@SerializedName("overall")
4045
OVERALL,
46+
47+
@SerializedName("check_gate")
48+
CHECK_GATE,
49+
50+
@SerializedName("get_config")
51+
GET_CONFIG,
52+
53+
@SerializedName("get_experiment")
54+
GET_EXPERIMENT,
55+
56+
@SerializedName("get_layer")
57+
GET_LAYER, ;
58+
59+
companion object {
60+
fun convertFromString(value: String): KeyType? {
61+
return when (value) {
62+
in "checkGate", "checkGateWithExposureLoggingDisabled" ->
63+
CHECK_GATE
64+
in "getExperiment", "getExperimentWithExposureLoggingDisabled" ->
65+
GET_EXPERIMENT
66+
in "getConfig", "getConfigWithExposureLoggingDisabled" ->
67+
GET_CONFIG
68+
in "getLayer", "getLayerWithExposureLoggingDisabled" ->
69+
GET_LAYER
70+
else ->
71+
null
72+
}
73+
}
74+
}
4175
}
4276

4377
enum class StepType {

src/main/kotlin/com/statsig/sdk/ErrorBoundary.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ internal class ErrorBoundary(private val apiKey: String, private val options: St
1313
internal var uri = URI("https://statsigapi.net/v1/sdk_exception")
1414
private val seen = HashSet<String>()
1515
private val maxInfoLength = 3000
16-
1716
private val client = OkHttpClient()
17+
internal var diagnostics: Diagnostics? = null
18+
1819
private companion object {
1920
val MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
2021
}
@@ -34,10 +35,17 @@ internal class ErrorBoundary(private val apiKey: String, private val options: St
3435
}
3536

3637
suspend fun <T> capture(tag: String, task: suspend () -> T, recover: suspend () -> T, configName: String? = null): T {
38+
var markerID: String? = null
39+
var keyType: KeyType? = null
3740
return try {
38-
task()
41+
keyType = KeyType.convertFromString(tag)
42+
markerID = markStart(keyType, configName)
43+
val result = task()
44+
markEnd(keyType, true, configName, markerID)
45+
return result
3946
} catch (ex: Throwable) {
4047
onException(tag, ex, configName)
48+
markEnd(keyType, false, configName, markerID)
4149
recover()
4250
}
4351
}
@@ -99,4 +107,21 @@ internal class ErrorBoundary(private val apiKey: String, private val options: St
99107

100108
logException(tag, ex, configName)
101109
}
110+
111+
private fun markStart(keyType: KeyType?, configName: String?): String? {
112+
if (diagnostics == null || keyType == null) {
113+
return null
114+
}
115+
val markerID = keyType.name + "_" + (diagnostics?.markers?.get(ContextType.API_CALL)?.count() ?: 0)
116+
117+
diagnostics?.markStart(keyType, context = ContextType.API_CALL, additionalMarker = Marker(markerID = markerID, configName = configName))
118+
return markerID
119+
}
120+
121+
private fun markEnd(keyType: KeyType?, success: Boolean, configName: String?, markerID: String?) {
122+
if (diagnostics == null || keyType == null) {
123+
return
124+
}
125+
diagnostics?.markEnd(keyType, success, context = ContextType.API_CALL, additionalMarker = Marker(markerID = markerID, configName = configName))
126+
}
102127
}

src/main/kotlin/com/statsig/sdk/SpecStore.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ internal class SpecStore constructor(
141141
}
142142
val body = response.body ?: return
143143
val jsonResponse = gson.fromJson<Map<String, IDList>>(body.string()) ?: return
144-
diagnostics.markStart(KeyType.GET_ID_LIST_SOURCES, StepType.PROCESS, Marker(idListCount = jsonResponse.size))
144+
diagnostics.markStart(KeyType.GET_ID_LIST_SOURCES, StepType.PROCESS, additionalMarker = Marker(idListCount = jsonResponse.size))
145145
val tasks = mutableListOf<Job>()
146146

147147
for ((name, serverList) in jsonResponse) {

src/main/kotlin/com/statsig/sdk/StatsigLogger.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ internal class StatsigLogger(
4747
}
4848
}
4949
private val gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
50-
50+
internal var diagnostics: Diagnostics? = null
5151
fun log(event: StatsigEvent) {
5252
events.add(event)
5353

@@ -131,6 +131,9 @@ internal class StatsigLogger(
131131
}
132132

133133
fun logDiagnostics(context: ContextType, markers: Collection<Marker>) {
134+
if (markers.isEmpty()) {
135+
return
136+
}
134137
val event = StatsigEvent(DIAGNOSTICS_EVENT)
135138
event.eventMetadata = mapOf(
136139
"context" to context.toString().lowercase(),
@@ -140,8 +143,25 @@ internal class StatsigLogger(
140143
log(event)
141144
}
142145

146+
fun addAPICallDiagnostics() {
147+
val markers = diagnostics?.markers?.get(ContextType.API_CALL)
148+
if (markers.isNullOrEmpty() ||
149+
diagnostics?.shouldLogDiagnostics(ContextType.API_CALL) != true
150+
) {
151+
return
152+
}
153+
val event = StatsigEvent(DIAGNOSTICS_EVENT)
154+
event.eventMetadata = mapOf(
155+
"context" to "api_call",
156+
"markers" to gson.toJson(markers),
157+
"setupOptions" to gson.toJson(statsigOptions.getLoggingCopy()),
158+
)
159+
events.add(event)
160+
}
161+
143162
suspend fun flush() {
144163
withContext(Dispatchers.IO) {
164+
addAPICallDiagnostics()
145165
if (events.size() == 0) {
146166
return@withContext
147167
}

src/main/kotlin/com/statsig/sdk/StatsigServer.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,10 @@ private class StatsigServerImpl() :
327327
return DynamicConfig.empty(experimentName)
328328
}
329329
return this.errorBoundary.capture("getExperiment", {
330-
return@capture getConfig(user, experimentName)
330+
val normalizedUser = normalizeUser(user)
331+
val result = getConfigImpl(user, experimentName)
332+
logConfigImpl(normalizedUser, experimentName, result)
333+
return@capture getDynamicConfigFromEvalResult(result, user, experimentName)
331334
}, {
332335
return@capture DynamicConfig.empty(experimentName)
333336
}, configName = experimentName)
@@ -784,6 +787,8 @@ private class StatsigServerImpl() :
784787

785788
private fun setupAndStartDiagnostics() {
786789
diagnostics = Diagnostics(options.disableDiagnostics, logger)
790+
errorBoundary.diagnostics = diagnostics
791+
logger.diagnostics = diagnostics
787792
diagnostics.markStart(KeyType.OVERALL)
788793
}
789794

src/test/java/com/statsig/sdk/DiagnosticsTest.kt

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,98 @@ class DiagnosticsTest {
5959
assertEquals(Gson().toJson(options.getLoggingCopy()), diagnosticsEvent!!.eventMetadata!!["statsigOptions"])
6060
}
6161

62+
@Test
63+
fun testCoreAPI() = runBlocking {
64+
val downloadConfigSpecsResponseWithSampling = StringBuilder(downloadConfigSpecsResponse).insert(downloadConfigSpecsResponse.length - 2, ",\n \"diagnostics\": {\"initialize\": \"0\", \"api_call\": \"10000\"}").toString()
65+
setupWebServer(downloadConfigSpecsResponseWithSampling)
66+
driver.initializeAsync("secret-testcase", options).get()
67+
val user = StatsigUser("testUser")
68+
driver.checkGate(user, "always_on_gate")
69+
driver.getConfig(user, "test_config")
70+
driver.getExperiment(user, "sample_experiment")
71+
driver.getLayer(user, "a_layer")
72+
driver.shutdown()
73+
val events = TestUtil.captureEvents(eventLogInputCompletable)
74+
val diagnosticsEvent = events.find { it.eventName == "statsig::diagnostics" }
75+
val markers: List<Marker> = gson.fromJson(diagnosticsEvent!!.eventMetadata!!["markers"], object : TypeToken<List<Marker>>() {}.type)
76+
assertEquals(8, markers.size)
77+
verifyMarker(
78+
markers[0],
79+
Marker(
80+
markerID = "CHECK_GATE_0",
81+
action = ActionType.START,
82+
configName = "always_on_gate",
83+
key = KeyType.CHECK_GATE,
84+
),
85+
)
86+
verifyMarker(
87+
markers[1],
88+
Marker(
89+
markerID = "CHECK_GATE_0",
90+
action = ActionType.END,
91+
configName = "always_on_gate",
92+
key = KeyType.CHECK_GATE,
93+
),
94+
)
95+
verifyMarker(
96+
markers[2],
97+
Marker(
98+
markerID = "GET_CONFIG_2",
99+
action = ActionType.START,
100+
configName = "test_config",
101+
key = KeyType.GET_CONFIG,
102+
),
103+
)
104+
verifyMarker(
105+
markers[3],
106+
Marker(
107+
markerID = "GET_CONFIG_2",
108+
action = ActionType.END,
109+
configName = "test_config",
110+
key = KeyType.GET_CONFIG,
111+
),
112+
)
113+
verifyMarker(
114+
markers[4],
115+
Marker(
116+
markerID = "GET_EXPERIMENT_4",
117+
action = ActionType.START,
118+
configName = "sample_experiment",
119+
key = KeyType.GET_EXPERIMENT,
120+
),
121+
)
122+
verifyMarker(
123+
markers[5],
124+
Marker(
125+
markerID = "GET_EXPERIMENT_4",
126+
action = ActionType.END,
127+
configName = "sample_experiment",
128+
key = KeyType.GET_EXPERIMENT,
129+
),
130+
)
131+
verifyMarker(
132+
markers[6],
133+
Marker(
134+
markerID = "GET_LAYER_6",
135+
action = ActionType.START,
136+
configName = "a_layer",
137+
key = KeyType.GET_LAYER,
138+
),
139+
)
140+
verifyMarker(
141+
markers[7],
142+
Marker(
143+
markerID = "GET_LAYER_6",
144+
action = ActionType.END,
145+
configName = "a_layer",
146+
key = KeyType.GET_LAYER,
147+
),
148+
)
149+
}
150+
62151
@Test
63152
fun testSamping() = runBlocking {
64-
val downloadConfigSpecsResponseWithSampling = StringBuilder(downloadConfigSpecsResponse).insert(downloadConfigSpecsResponse.length - 2, ",\n \"diagnostics\": {\"initialize\": \"0\"}").toString()
153+
val downloadConfigSpecsResponseWithSampling = StringBuilder(downloadConfigSpecsResponse).insert(downloadConfigSpecsResponse.length - 2, ",\n \"diagnostics\": {\"initialize\": \"0\", \"api_call\": \"0\"}").toString()
65154
setupWebServer(downloadConfigSpecsResponseWithSampling)
66155
driver.initializeAsync("secret-testcase", options).get()
67156
driver.shutdown()
@@ -99,5 +188,7 @@ class DiagnosticsTest {
99188
Assert.assertEquals(expected.key, actual.key)
100189
Assert.assertEquals(expected.action, actual.action)
101190
Assert.assertEquals(expected.step, actual.step)
191+
Assert.assertEquals(expected.configName, actual.configName)
192+
Assert.assertEquals(expected.markerID, actual.markerID)
102193
}
103194
}

0 commit comments

Comments
 (0)