Skip to content

Commit 5ca36bf

Browse files
authored
[analytics] introduce Analytics (consent) Data initialization (#112)
1 parent 911b6e7 commit 5ca36bf

File tree

3 files changed

+201
-5
lines changed

3 files changed

+201
-5
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2025 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
7+
package com.jetbrains.lang.dart.analytics
8+
9+
import com.google.gson.JsonElement
10+
import com.google.gson.JsonObject
11+
import com.intellij.openapi.application.ApplicationInfo
12+
import com.intellij.openapi.diagnostic.Logger
13+
import com.jetbrains.lang.dart.dtd.DTDProcess
14+
import com.jetbrains.lang.dart.dtd.DTDProcessListener
15+
import com.jetbrains.lang.dart.sdk.DartSdk
16+
import com.jetbrains.lang.dart.util.PrintingLogger
17+
import de.roderick.weberknecht.WebSocketException
18+
import java.util.concurrent.CountDownLatch
19+
import java.util.concurrent.TimeUnit
20+
import java.util.concurrent.TimeoutException
21+
import kotlin.time.Duration
22+
import kotlin.time.Duration.Companion.seconds
23+
24+
25+
/// Sends logging to the console.
26+
private const val DEBUGGING_LOCALLY = true
27+
28+
private val DEFAULT_RESPONSE_TIMEOUT = 1.seconds
29+
30+
private object UnifiedAnalytics {
31+
private val logger: Logger =
32+
if (DEBUGGING_LOCALLY) PrintingLogger.SYSTEM_OUT else Logger.getInstance(UnifiedAnalytics::class.java)
33+
34+
/// Service name for the DTD-hosted unified analytics service.
35+
const val SERVICE_NAME = "UnifiedAnalytics"
36+
37+
/// Service method name for the method that determines whether the unified
38+
/// analytics client should display the consent message.
39+
const val SHOULD_SHOW_MESSAGE = "shouldShowMessage"
40+
41+
/// Service method name for the method that returns the unified analytics
42+
/// consent message to prompt users with.
43+
const val GET_CONSENT_MESSAGE = "getConsentMessage"
44+
45+
/// Service method name for the method that sends an event to unified
46+
/// analytics.
47+
const val SEND = "send"
48+
49+
/// Service method name for the method that returns whether unified analytics
50+
/// telemetry is enabled.
51+
const val TELEMETRY_ENABLED = "telemetryEnabled"
52+
53+
fun callServiceWithJsonResponse(dtdProcess: DTDProcess, name: String, timeout: Duration = DEFAULT_RESPONSE_TIMEOUT): JsonElement? {
54+
val params = JsonObject()
55+
params.addProperty("tool", getToolName())
56+
var value: JsonElement? = null
57+
try {
58+
val latch = CountDownLatch(1)
59+
dtdProcess.sendRequest(
60+
"$SERVICE_NAME.$name",
61+
params,
62+
true
63+
) { response ->
64+
logger.debug("$SERVICE_NAME.$name.received: ")
65+
val result = response["result"]
66+
if (result is JsonObject) {
67+
value = result["value"]
68+
}
69+
logger.debug("\t$response")
70+
latch.countDown()
71+
}
72+
73+
val completed = latch.await(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS)
74+
if (!completed) {
75+
throw TimeoutException("Call to $SERVICE_NAME.$name timed out after $DEFAULT_RESPONSE_TIMEOUT.")
76+
}
77+
78+
} catch (e: WebSocketException) {
79+
logger.error(e)
80+
}
81+
return value
82+
}
83+
84+
fun callServiceWithStringResponse(dtdProcess: DTDProcess, name: String): String? =
85+
callServiceWithJsonResponse(dtdProcess, name)?.asString
86+
87+
fun callServiceWithBoolResponse(dtdProcess: DTDProcess, name: String): Boolean =
88+
callServiceWithJsonResponse(dtdProcess, name)?.asBoolean ?: false
89+
}
90+
91+
class UnifiedAnalyticsData {
92+
var shouldShowMessage: Boolean = false
93+
internal set
94+
var consentMessage: String? = null
95+
internal set
96+
var telemetryEnabled: Boolean = false
97+
internal set
98+
}
99+
100+
object Analytics {
101+
private val logger: Logger =
102+
if (DEBUGGING_LOCALLY) PrintingLogger.SYSTEM_OUT else Logger.getInstance(Analytics::class.java)
103+
104+
private var data: UnifiedAnalyticsData? = null
105+
106+
@JvmStatic
107+
fun initialize(sdk: DartSdk): UnifiedAnalyticsData {
108+
logger.debug("Analytics.initialize")
109+
110+
data?.let { return it }
111+
112+
data = UnifiedAnalyticsData()
113+
114+
val dtdProcess = DTDProcess()
115+
dtdProcess.listener = object : DTDProcessListener {
116+
override fun onProcessStarted(uri: String?) {
117+
logger.debug("DartAnalysisServerService.onProcessStarted")
118+
119+
val params = JsonObject()
120+
params.addProperty("tool", getToolName())
121+
122+
try {
123+
data!!.shouldShowMessage =
124+
UnifiedAnalytics.callServiceWithBoolResponse(dtdProcess, UnifiedAnalytics.SHOULD_SHOW_MESSAGE)
125+
if (data!!.shouldShowMessage) {
126+
data!!.consentMessage =
127+
UnifiedAnalytics.callServiceWithStringResponse(dtdProcess, UnifiedAnalytics.GET_CONSENT_MESSAGE)
128+
data!!.telemetryEnabled = false
129+
} else {
130+
data!!.telemetryEnabled =
131+
UnifiedAnalytics.callServiceWithBoolResponse(dtdProcess, UnifiedAnalytics.TELEMETRY_ENABLED)
132+
}
133+
} catch (t: Throwable) {
134+
logger.error(t)
135+
} finally {
136+
dtdProcess.terminate()
137+
}
138+
}
139+
}
140+
dtdProcess.start(sdk)
141+
142+
// TODO: fix race condition if we get here before the first countdown latch is started
143+
144+
return data!!
145+
}
146+
}
147+
148+
private fun getToolName(): String = when (ApplicationInfo.getInstance().build.productCode) {
149+
"AI" -> "android-studio-plugins"
150+
else -> "intellij-plugins"
151+
}

third_party/src/main/java/com/jetbrains/lang/dart/dtd/DTDProcess.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class DTDProcess {
5858
EventDispatcher.create(DartToolingDaemonListener::class.java)
5959

6060
var listener: DTDProcessListener? = null
61-
set
61+
6262
private fun connectToWebSocket(uri: String) {
6363
try {
6464
webSocket = WebSocket(URI(uri))
@@ -233,9 +233,9 @@ class DTDProcess {
233233
}
234234

235235
interface DTDProcessListener {
236-
fun onWebSocketMessage(text: String)
237-
fun onWebSocketClose()
238-
fun onWebSocketOpen()
239-
fun onProcessStarted(uri: String?)
236+
fun onWebSocketMessage(text: String) {}
237+
fun onWebSocketClose() {}
238+
fun onWebSocketOpen() {}
239+
fun onProcessStarted(uri: String?) {}
240240
}
241241

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
7+
package com.jetbrains.lang.dart.util
8+
9+
import com.intellij.openapi.diagnostic.Logger
10+
import org.jetbrains.annotations.NonNls
11+
import java.io.PrintStream
12+
13+
/* A riff on org.jetbrains.kotlin.utils.PrintingLogger */
14+
class PrintingLogger(private val out: PrintStream) : Logger() {
15+
override fun isDebugEnabled() = true
16+
17+
override fun debug(@NonNls message: String?) = out.println(message)
18+
19+
override fun debug(t: Throwable?) {
20+
t?.printStackTrace(this.out)
21+
}
22+
23+
override fun debug(@NonNls message: String?, t: Throwable?) {
24+
debug(message)
25+
debug(t)
26+
}
27+
28+
override fun info(@NonNls message: String?) = debug(message)
29+
30+
override fun info(@NonNls message: String?, t: Throwable?) = debug(message, t)
31+
32+
override fun warn(@NonNls message: String?, t: Throwable?) = debug(message, t)
33+
34+
override fun error(@NonNls message: String?, t: Throwable?, @NonNls vararg details: String) {
35+
debug(message, t)
36+
37+
for (detail in details) {
38+
debug(detail)
39+
}
40+
}
41+
42+
companion object {
43+
val SYSTEM_OUT: Logger = PrintingLogger(System.out)
44+
}
45+
}

0 commit comments

Comments
 (0)