Skip to content

Commit ebdca3c

Browse files
DzmitryFomchyngithub-actions[bot]
authored andcommitted
Copilot errors telemetry (#10714)
* Standalone telemetry * Report copilot errors * Address code review comments GitOrigin-RevId: fda2888e3911de26ff1accde25e8ded805aa2f0b
1 parent f9e2790 commit ebdca3c

File tree

6 files changed

+337
-0
lines changed

6 files changed

+337
-0
lines changed

copilot/src/main/java/com/mapbox/navigation/copilot/MapboxCopilotImpl.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import com.mapbox.navigation.core.internal.lifecycle.CarAppLifecycleOwner
3535
import com.mapbox.navigation.core.internal.telemetry.ExtendedUserFeedback
3636
import com.mapbox.navigation.core.internal.telemetry.UserFeedbackObserver
3737
import com.mapbox.navigation.core.internal.telemetry.registerUserFeedbackObserver
38+
import com.mapbox.navigation.core.internal.telemetry.standalone.StandaloneCustomEvent
39+
import com.mapbox.navigation.core.internal.telemetry.standalone.StandaloneNavigationTelemetry
3840
import com.mapbox.navigation.core.internal.telemetry.unregisterUserFeedbackObserver
3941
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
4042
import com.mapbox.navigation.utils.internal.InternalJobControlFactory
@@ -242,6 +244,10 @@ internal class MapboxCopilotImpl(
242244
}
243245

244246
val recording = copilotHistoryRecorder.startRecording().firstOrNull() ?: ""
247+
if (recording.isEmpty()) {
248+
reportCopilotError("History recording files are empty")
249+
}
250+
245251
currentHistoryRecordingSessionState = historyRecordingSessionState
246252
startSessionTime = SystemClock.elapsedRealtime()
247253
activeSession = CopilotSession.create(
@@ -284,6 +290,10 @@ internal class MapboxCopilotImpl(
284290
limitTotalHistoryFilesSize(finishedSessions)
285291
}
286292
val recording = copilotHistoryRecorder.startRecording().firstOrNull() ?: ""
293+
if (recording.isEmpty()) {
294+
reportCopilotError("History recording files are empty")
295+
}
296+
287297
// when restarting recording we inherit previous session info and just update start time
288298
activeSession = activeSession.copy(
289299
recording = recording,
@@ -436,5 +446,12 @@ internal class MapboxCopilotImpl(
436446
internal const val PROD_BASE_URL = "https://events.mapbox.com"
437447

438448
internal fun logD(msg: String) = logD(msg, LOG_CATEGORY)
449+
450+
internal fun reportCopilotError(description: String) {
451+
logD("reportCopilotError($description)", LOG_CATEGORY)
452+
StandaloneNavigationTelemetry.getOrCreate().sendEvent(
453+
StandaloneCustomEvent(type = "copilot-error", payload = description),
454+
)
455+
}
439456
}
440457
}

copilot/src/main/java/com/mapbox/navigation/copilot/work/HistoryUploadWorker.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.mapbox.navigation.copilot.MapboxCopilotImpl.Companion.MEDIA_TYPE_ZIP
3030
import com.mapbox.navigation.copilot.MapboxCopilotImpl.Companion.PROD_BASE_URL
3131
import com.mapbox.navigation.copilot.MapboxCopilotImpl.Companion.ZIP
3232
import com.mapbox.navigation.copilot.MapboxCopilotImpl.Companion.gson
33+
import com.mapbox.navigation.copilot.MapboxCopilotImpl.Companion.reportCopilotError
3334
import com.mapbox.navigation.copilot.internal.CopilotSession
3435
import com.mapbox.navigation.copilot.internal.PushStatus
3536
import com.mapbox.navigation.copilot.internal.saveFilename
@@ -65,6 +66,23 @@ internal class HistoryUploadWorker(
6566
val recordingFile =
6667
rename(File(copilotSession.recording), attachmentFilename(copilotSession))
6768
val sessionFile = File(recordingFile.parent, copilotSession.saveFilename())
69+
70+
if (!recordingFile.exists()) {
71+
reportCopilotError(
72+
"History file does not exist: ${recordingFile.name}. " +
73+
"Copilot session: $copilotSession. " +
74+
"Copilot dir files: ${recordingFile.parentFile?.listFiles()?.map { it.name }}",
75+
)
76+
}
77+
78+
if (!sessionFile.exists()) {
79+
reportCopilotError(
80+
"Session file does not exist: ${sessionFile.name}. " +
81+
"Copilot session: $copilotSession. " +
82+
"Copilot dir files: ${recordingFile.parentFile?.listFiles()?.map { it.name }}",
83+
)
84+
}
85+
6886
val uploadSessionId = workerParams.inputData.getString(UPLOAD_SESSION_ID)!!
6987

7088
val metadataList = arrayListOf(
@@ -138,6 +156,21 @@ internal class HistoryUploadWorker(
138156
TransferState.FINISHED -> {
139157
val httpResultCode = uploadStatus.httpResult?.value?.code
140158
logD("uploadStatus state = FINISHED httpResultCode = $httpResultCode")
159+
160+
uploadStatus.httpResult?.onError { error ->
161+
reportCopilotError(
162+
"History upload failed for ${uploadOptions.filePath}. " +
163+
"Response error = $error",
164+
)
165+
}?.onValue { value ->
166+
if (!value.code.isSuccessful()) {
167+
reportCopilotError(
168+
"History upload failed for ${uploadOptions.filePath}. " +
169+
"Response data = $value",
170+
)
171+
}
172+
}
173+
141174
cont.resume(httpResultCode.isSuccessful())
142175
}
143176

@@ -146,6 +179,13 @@ internal class HistoryUploadWorker(
146179
"uploadStatus state = FAILED error = ${uploadStatus.error}; " +
147180
"HttpResponseData = ${uploadStatus.httpResult?.value}",
148181
)
182+
183+
reportCopilotError(
184+
"History upload failed for ${uploadOptions.filePath}. " +
185+
"Error = ${uploadStatus.error}, " +
186+
"HttpResponseData = ${uploadStatus.httpResult?.value}",
187+
)
188+
149189
cont.resume(false)
150190
}
151191
}

copilot/src/test/java/com/mapbox/navigation/copilot/HistoryUploadWorkerTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.mapbox.navigation.copilot.internal.PushStatus
2525
import com.mapbox.navigation.copilot.internal.PushStatusObserver
2626
import com.mapbox.navigation.copilot.work.HistoryUploadWorker
2727
import com.mapbox.navigation.copilot.work.HistoryUploadWorker.Companion.putCopilotSession
28+
import com.mapbox.navigation.core.internal.telemetry.standalone.StandaloneNavigationTelemetry
2829
import com.mapbox.navigation.testing.LoggingFrontendTestRule
2930
import io.mockk.CapturingSlot
3031
import io.mockk.coEvery
@@ -94,6 +95,9 @@ class HistoryUploadWorkerTest {
9495
mockedUploadServiceInterface = mockHttpService()
9596

9697
MapboxCopilot.pushStatusObservers.add(mockedPushStatusObserver)
98+
99+
mockkObject(StandaloneNavigationTelemetry.Companion)
100+
every { StandaloneNavigationTelemetry.getOrCreate() } returns mockk(relaxed = true)
97101
}
98102

99103
@After
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.mapbox.navigation.core.internal.telemetry.standalone
2+
3+
import androidx.annotation.RestrictTo
4+
import androidx.annotation.VisibleForTesting
5+
import com.mapbox.common.Event
6+
import com.mapbox.common.EventPriority
7+
import com.mapbox.common.EventsServerOptions
8+
import com.mapbox.common.EventsService
9+
import com.mapbox.common.EventsServiceInterface
10+
import com.mapbox.navigation.core.MapboxNavigation
11+
import com.mapbox.navigation.core.internal.SdkInfoProvider
12+
import com.mapbox.navigation.metrics.internal.TelemetryUtilsDelegate
13+
import com.mapbox.navigation.utils.internal.logD
14+
import com.mapbox.navigation.utils.internal.logE
15+
16+
internal object EventsServiceProvider {
17+
fun provideEventsService(eventsServerOptions: EventsServerOptions): EventsServiceInterface =
18+
EventsService.getOrCreate(eventsServerOptions)
19+
}
20+
21+
/**
22+
* Telemetry implementation that does not require [MapboxNavigation] object.
23+
* Should be replaced by native telemetry when NN decouples it from the Navigator
24+
* https://mapbox.atlassian.net/browse/NN-4149
25+
*/
26+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
27+
interface StandaloneNavigationTelemetry {
28+
29+
fun sendEvent(event: StandaloneTelemetryEvent)
30+
31+
companion object {
32+
33+
@Volatile
34+
private lateinit var INSTANCE: StandaloneNavigationTelemetry
35+
36+
@Synchronized
37+
fun getOrCreate(): StandaloneNavigationTelemetry {
38+
if (!::INSTANCE.isInitialized) {
39+
val eventsServerOptions = EventsServerOptions(
40+
SdkInfoProvider.sdkInformation(),
41+
null,
42+
)
43+
44+
INSTANCE = StandaloneNavigationTelemetryImpl(
45+
EventsServiceProvider.provideEventsService(eventsServerOptions),
46+
)
47+
}
48+
return INSTANCE
49+
}
50+
51+
@VisibleForTesting
52+
@JvmSynthetic
53+
@Synchronized
54+
internal fun reinitializeForTests() {
55+
if (::INSTANCE.isInitialized) {
56+
val eventsServerOptions = EventsServerOptions(
57+
SdkInfoProvider.sdkInformation(),
58+
null,
59+
)
60+
61+
INSTANCE = StandaloneNavigationTelemetryImpl(
62+
EventsServiceProvider.provideEventsService(eventsServerOptions),
63+
)
64+
}
65+
}
66+
}
67+
}
68+
69+
internal class StandaloneNavigationTelemetryImpl(
70+
private val eventsService: EventsServiceInterface,
71+
) : StandaloneNavigationTelemetry {
72+
73+
override fun sendEvent(event: StandaloneTelemetryEvent) {
74+
if (!TelemetryUtilsDelegate.getEventsCollectionState()) {
75+
logD(LOG_CATEGORY) {
76+
"Skipped event send, events collection disabled"
77+
}
78+
return
79+
}
80+
81+
eventsService.sendEvent(
82+
Event(EventPriority.IMMEDIATE, event.toValue(), null),
83+
) { result ->
84+
result.onValue {
85+
logD(LOG_CATEGORY) {
86+
"Event has been sent: ${event.metricName}"
87+
}
88+
}.onError { error ->
89+
logE(LOG_CATEGORY) {
90+
"EventsService failure: $error. Event: ${event.metricName}"
91+
}
92+
}
93+
}
94+
}
95+
96+
private companion object {
97+
const val LOG_CATEGORY = "StandaloneNavigationTelemetry"
98+
}
99+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.mapbox.navigation.core.internal.telemetry.standalone
2+
3+
import android.os.Build
4+
import androidx.annotation.RestrictTo
5+
import com.google.gson.Gson
6+
import com.mapbox.bindgen.Value
7+
import com.mapbox.common.TelemetrySystemUtils
8+
import com.mapbox.common.toValue
9+
import com.mapbox.navigation.base.metrics.MetricEvent
10+
import com.mapbox.navigation.base.metrics.NavigationMetrics
11+
import com.mapbox.navigation.core.internal.SdkInfoProvider
12+
13+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
14+
interface StandaloneTelemetryEvent : MetricEvent {
15+
fun toValue(): Value
16+
}
17+
18+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
19+
class StandaloneCustomEvent(
20+
val type: String,
21+
val payload: String,
22+
val customEventVersion: String = "1.0",
23+
) : StandaloneTelemetryEvent {
24+
25+
private val created: String = TelemetrySystemUtils.obtainCurrentDate()
26+
27+
override val metricName: String = EVENT_NAME
28+
29+
override fun toValue(): Value {
30+
return hashMapOf(
31+
"type" to type.toValue(),
32+
"payload" to payload.toValue(),
33+
"customEventVersion" to customEventVersion.toValue(),
34+
"created" to created.toValue(),
35+
).run {
36+
putAll(CONSTANT_FIELDS)
37+
Value.valueOf(this)
38+
}
39+
}
40+
41+
override fun toJson(gson: Gson): String {
42+
return gson.toJson(this)
43+
}
44+
45+
private companion object {
46+
47+
const val EVENT_NAME = NavigationMetrics.CUSTOM_EVENT
48+
49+
// Required fields that can't be absent.
50+
// Most of them don't make sense in a standalone mode without navigation state.
51+
val CONSTANT_FIELDS = mapOf(
52+
"event" to EVENT_NAME.toValue(),
53+
"version" to "2.4".toValue(),
54+
"sdkIdentifier" to SdkInfoProvider.sdkInformation().name.toValue(),
55+
"operatingSystem" to "Android - ${Build.VERSION.RELEASE}".toValue(),
56+
"device" to (Build.MODEL ?: "Unknown").toValue(),
57+
"createdMonotime" to 0.toValue(),
58+
"driverMode" to "freeDrive".toValue(),
59+
"driverModeId" to "00000000-0000-0000-0000-000000000000".toValue(),
60+
"driverModeStartTimestamp" to "non_valid".toValue(),
61+
"driverModeStartTimestampMonotime" to 0.toValue(),
62+
"eventVersion" to 0.toValue(),
63+
"simulation" to false.toValue(),
64+
"locationEngine" to "".toValue(),
65+
"lat" to 0.0.toValue(),
66+
"lng" to 0.0.toValue(),
67+
)
68+
}
69+
}

0 commit comments

Comments
 (0)