-
Notifications
You must be signed in to change notification settings - Fork 59
[WIP]Client event reporting 2 #1702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
10661de
cf5c34c
9aa4811
47c2b79
30d130d
5de5a92
713fc0a
b56d3e0
086d285
7f5fe03
c90823b
2e9f641
ec68870
801da73
5cd656d
9125e8e
43c875e
7b3db27
d4dbc92
36af925
ef46a33
ae2e77e
144ff57
b00efd0
1198356
3254302
22f85c8
5dfce23
3a5a07e
01776cf
f08af11
690b9ab
e787c69
a5dd29a
5c125e6
83ee729
47f6d42
f1a9f7e
3cd0811
ad42270
3cdbce1
5d57cc5
bd2a2ba
4c428a9
e0169e9
291671f
699e6c5
732b71b
af82b29
2700890
b9a57fb
5fb286e
e3dc60d
b5a3a01
1c89a96
1f6d324
4b9a26b
a16cccc
5e31613
93d60cd
489816f
927166c
2f81177
7edfd08
bdf34cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /* | ||
| * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Stream License; | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://github.com/GetStream/stream-video-android/blob/main/LICENSE | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package io.getstream.video.android.ui | ||
|
|
||
| import io.getstream.result.Error | ||
| import io.getstream.video.android.core.faultinjector.FailureInjector | ||
| import io.getstream.video.android.core.faultinjector.FailureKey | ||
| import retrofit2.HttpException | ||
| import retrofit2.Response | ||
|
|
||
| internal class FailureInjectorImpl : FailureInjector { | ||
| private val faultCounts = mutableMapOf<FailureKey, Int>() | ||
|
|
||
| override fun enable(key: FailureKey) { | ||
| if ((faultCounts[key] ?: 0) == 0) faultCounts[key] = 1 | ||
| } | ||
|
|
||
| override fun disable(key: FailureKey) { | ||
| faultCounts[key] = 0 | ||
| } | ||
|
|
||
| override fun setEnabled(key: FailureKey, enabled: Boolean) { | ||
| if (enabled) enable(key) else disable(key) | ||
| } | ||
|
|
||
| override fun isEnabled(key: FailureKey): Boolean { | ||
| return (faultCounts[key] ?: 0) > 0 | ||
| } | ||
|
|
||
| override fun setCount(key: FailureKey, count: Int) { | ||
| faultCounts[key] = count | ||
| } | ||
|
|
||
| override fun getCount(key: FailureKey): Int { | ||
| return faultCounts[key] ?: 0 | ||
| } | ||
|
|
||
| override fun clear() { | ||
| faultCounts.clear() | ||
| } | ||
|
|
||
| override fun throwDebugFault(key: FailureKey) { | ||
| val count = faultCounts[key] ?: 0 | ||
| if (count > 0) { | ||
| faultCounts[key] = count - 1 | ||
| throw when (key) { | ||
| FailureKey.FAIL_LOCATION -> HttpException( | ||
| Response.error<String>( | ||
| 100, | ||
| okhttp3.ResponseBody.create(null, ""), | ||
| ), | ||
| ) | ||
| else -> RuntimeException("Failure injected: $key") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun sendFailResult(key: FailureKey): io.getstream.result.Result.Failure { | ||
| val count = faultCounts[key] ?: 0 | ||
| if (count > 0) { | ||
| faultCounts[key] = count - 1 | ||
| } | ||
|
Comment on lines
+26
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) Show FailureInjectorImpl implementation fully (bounded)
FILE="demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorImpl.kt"
echo "== $FILE (relevant excerpt) =="
nl -ba "$FILE" | sed -n '1,220p'
# 2) Find all usages of FailureInjectorImpl / FailureInjector interface
echo "== Searching for FailureInjectorImpl / FailureInjector usage =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|FailureInjector" demo-app/src/main/kotlin || true
# 3) Specifically find calls to throwDebugFault / sendFailResult and show call sites
echo "== Call sites for throwDebugFault / sendFailResult =="
rg -n --hidden --glob '!*build/*' "throwDebugFault\(|sendFailResult\(" demo-app/src/main/kotlin || true
# 4) Also check non-demo modules if FailureInjectorImpl is referenced elsewhere
echo "== Call sites across repository (optional) =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|throwDebugFault\(|sendFailResult\(" || true
# 5) Check for coroutine/thread contexts around join/socket/RTC flows mentioning these fault methods
echo "== Looking for join/socket/RTC fault injection references =="
rg -n --hidden --glob '!*build/*' "join|socket|rtc|RT|WebSocket" demo-app/src/main/kotlin/io/getstream/video/android/ui demo-app/src/main/kotlin/io/getstream/video/android || trueRepository: GetStream/stream-video-android Length of output: 238 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorImpl.kt"
echo "== File length =="
wc -l "$FILE" || true
echo "== $FILE (lines 1-220) =="
cat -n "$FILE" | sed -n '1,220p'
echo "== Call sites for throwDebugFault / sendFailResult (demo-app) =="
rg -n --hidden --glob '!*build/*' "throwDebugFault\(|sendFailResult\(" demo-app/src/main/kotlin || true
echo "== References to FailureInjector / FailureInjectorImpl (demo-app) =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|FailureInjector" demo-app/src/main/kotlin || true
echo "== References across repository (only these symbols) =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|FailureInjector|throwDebugFault\(|sendFailResult\(" || trueRepository: GetStream/stream-video-android Length of output: 10317 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show relevant call-site code around the referenced line numbers
sed -n '200,280p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt | nl -ba | sed -n '1,120p' || true
sed -n '420,520p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt | nl -ba | sed -n '1,140p' || true
sed -n '760,880p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt | nl -ba | sed -n '1,160p' || true
sed -n '1,160p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt | nl -ba | sed -n '1,200p' || true
# Demo UI wiring: how FailureInjectorImpl is created and used
sed -n '60,120p' demo-app/src/main/kotlin/io/getstream/video/android/App.kt | nl -ba | sed -n '1,140p' || true
sed -n '180,280p' demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorUi.kt | nl -ba | sed -n '1,200p' || true
sed -n '80,260p' demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt | nl -ba | sed -n '1,220p' || trueRepository: GetStream/stream-video-android Length of output: 364 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== SfuSocket.kt excerpt around throwDebugFault calls =="
# show a bit around the previously reported line numbers
sed -n '200,270p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt
echo
echo "== StreamVideoClient.kt excerpt around sendFailResult calls =="
sed -n '430,490p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
echo
sed -n '800,860p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
echo
echo "== ClientState.kt excerpt about failureInjector =="
sed -n '1,160p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt
echo
echo "== demo-app App.kt excerpt where FailureInjectorImpl is created =="
sed -n '60,120p' demo-app/src/main/kotlin/io/getstream/video/android/App.kt
echo
echo "== demo-app FailureInjectorUi.kt excerpt where a FailureInjector is provided =="
sed -n '1,320p' demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorUi.kt | head -n 260
echo
echo "== demo-app CallJoinScreen.kt excerpt wiring FailureInjectorUi =="
sed -n '180,260p' demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.ktRepository: GetStream/stream-video-android Length of output: 29260 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt"
echo "== Occurrences of debugFaultInjectors( =="
rg -n "debugFaultInjectors\(" "$FILE"
# show around each occurrence
echo "== Context around debugFaultInjectors call sites =="
# use rg line numbers then sed ranges
python3 - <<'PY'
import re, subprocess, os, sys, textwrap, json
file_path = "stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt"
out = subprocess.check_output(["rg","-n","debugFaultInjectors\\(",file_path], text=True)
for line in out.strip().splitlines():
m = re.match(r".*:(\d+):", line)
if not m:
continue
ln = int(m.group(1))
start = max(1, ln-40)
end = ln+60
print(f"\n--- {file_path}:{ln} (showing {start}-{end}) ---")
# use sed to print range
subprocess.run(["sed","-n",f"{start},{end}p",file_path], check=False)
PYRepository: GetStream/stream-video-android Length of output: 320 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt"
echo "== SfuSocket.kt first 120 lines =="
sed -n '1,140p' "$FILE"
echo
echo "== SfuSocket.kt lines 140-280 =="
sed -n '140,280p' "$FILE"
echo
echo "== Search for userScope / coroutine scope usage in SfuSocket.kt =="
rg -n "userScope|CoroutineScope|launch\(|withContext\(|Dispatchers\." "$FILE"Repository: GetStream/stream-video-android Length of output: 13748 Make
🤖 Prompt for AI Agents |
||
| val message = when (key) { | ||
| FailureKey.FAIL_JOIN_CALL -> "Unable to resolve host" | ||
| else -> "Failure injected: $key" | ||
| } | ||
| return io.getstream.result.Result.Failure( | ||
| Error.ThrowableError( | ||
| message, | ||
| RuntimeException(message), | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| /* | ||
| * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Stream License; | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://github.com/GetStream/stream-video-android/blob/main/LICENSE | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package io.getstream.video.android.ui | ||
|
|
||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.clickable | ||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.lazy.LazyColumn | ||
| import androidx.compose.foundation.lazy.items | ||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||
| import androidx.compose.material.Button | ||
| import androidx.compose.material.ButtonDefaults | ||
| import androidx.compose.material.DropdownMenu | ||
| import androidx.compose.material.DropdownMenuItem | ||
| import androidx.compose.material.Icon | ||
| import androidx.compose.material.Text | ||
| import androidx.compose.material.icons.Icons | ||
| import androidx.compose.material.icons.filled.ArrowDropDown | ||
| import androidx.compose.material.icons.filled.Close | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateMapOf | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import io.getstream.result.Error | ||
| import io.getstream.video.android.compose.theme.VideoTheme | ||
| import io.getstream.video.android.core.faultinjector.FailureInjector | ||
| import io.getstream.video.android.core.faultinjector.FailureKey | ||
| import io.getstream.video.android.core.internal.InternalStreamVideoApi | ||
|
|
||
| private val countOptions = listOf(0, 1, 2, 3, 5, 10) | ||
|
|
||
| @OptIn(InternalStreamVideoApi::class) | ||
| @Composable | ||
| fun FailureInjectorUi( | ||
| modifier: Modifier = Modifier, | ||
| failureInjector: FailureInjector, | ||
| onClose: () -> Unit, | ||
| ) { | ||
| val countState = remember { | ||
| mutableStateMapOf<FailureKey, Int>().apply { | ||
| FailureKey.entries.forEach { key -> put(key, failureInjector.getCount(key)) } | ||
| } | ||
| } | ||
|
|
||
| Column( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .background(VideoTheme.colors.baseSheetPrimary), | ||
| ) { | ||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .background(VideoTheme.colors.baseSheetSecondary) | ||
| .padding(horizontal = 16.dp, vertical = 14.dp), | ||
| horizontalArrangement = Arrangement.SpaceBetween, | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| Text( | ||
| text = "Failure Injection", | ||
| style = VideoTheme.typography.subtitleM, | ||
| color = VideoTheme.colors.basePrimary, | ||
| ) | ||
|
|
||
| Row( | ||
| horizontalArrangement = Arrangement.spacedBy(12.dp), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| Text( | ||
| text = "Clear all", | ||
| modifier = Modifier.clickable { | ||
| failureInjector.clear() | ||
| FailureKey.entries.forEach { key -> countState[key] = 0 } | ||
| }, | ||
| style = VideoTheme.typography.bodyS, | ||
| color = VideoTheme.colors.brandPrimary, | ||
| ) | ||
|
|
||
| Box( | ||
| modifier = Modifier | ||
| .background( | ||
| color = VideoTheme.colors.baseSheetTertiary, | ||
| shape = RoundedCornerShape(999.dp), | ||
| ) | ||
| .clickable(onClick = onClose) | ||
| .padding(8.dp), | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Default.Close, | ||
| contentDescription = "Close", | ||
| tint = VideoTheme.colors.basePrimary, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| LazyColumn( | ||
| modifier = Modifier | ||
| .weight(1f) | ||
| .fillMaxWidth() | ||
| .padding(horizontal = 16.dp, vertical = 8.dp), | ||
| verticalArrangement = Arrangement.spacedBy(4.dp), | ||
| ) { | ||
| items(FailureKey.entries) { key -> | ||
| val count = countState[key] ?: 0 | ||
| var expanded by remember { mutableStateOf(false) } | ||
|
|
||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(horizontal = 8.dp, vertical = 4.dp), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| horizontalArrangement = Arrangement.SpaceBetween, | ||
| ) { | ||
| Text( | ||
| text = key.name, | ||
| style = VideoTheme.typography.bodyM, | ||
| color = VideoTheme.colors.basePrimary, | ||
| modifier = Modifier.weight(1f), | ||
| ) | ||
|
|
||
| Box { | ||
| Row( | ||
| modifier = Modifier | ||
| .background( | ||
| color = VideoTheme.colors.baseSheetTertiary, | ||
| shape = RoundedCornerShape(6.dp), | ||
| ) | ||
| .clickable { expanded = true } | ||
| .padding(horizontal = 12.dp, vertical = 6.dp), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| horizontalArrangement = Arrangement.spacedBy(4.dp), | ||
| ) { | ||
| Text( | ||
| text = "$count", | ||
| style = VideoTheme.typography.bodyM, | ||
| color = if (count > 0) VideoTheme.colors.brandPrimary else VideoTheme.colors.baseSecondary, | ||
| ) | ||
| Icon( | ||
| imageVector = Icons.Default.ArrowDropDown, | ||
| contentDescription = null, | ||
| tint = VideoTheme.colors.baseSecondary, | ||
| ) | ||
| } | ||
|
|
||
| DropdownMenu( | ||
| expanded = expanded, | ||
| onDismissRequest = { expanded = false }, | ||
| ) { | ||
| countOptions.forEach { option -> | ||
| DropdownMenuItem( | ||
| onClick = { | ||
| countState[key] = option | ||
| failureInjector.setCount(key, option) | ||
| expanded = false | ||
| }, | ||
| ) { | ||
| Text( | ||
| text = "$option", | ||
| style = VideoTheme.typography.bodyM, | ||
| color = if (option > 0) VideoTheme.colors.brandPrimary else VideoTheme.colors.basePrimary, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Box( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .background(VideoTheme.colors.baseSheetSecondary) | ||
| .padding(horizontal = 16.dp, vertical = 12.dp), | ||
| ) { | ||
| Button( | ||
| onClick = onClose, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| shape = RoundedCornerShape(8.dp), | ||
| colors = ButtonDefaults.buttonColors( | ||
| backgroundColor = VideoTheme.colors.brandPrimary, | ||
| contentColor = VideoTheme.colors.baseSheetPrimary, | ||
| ), | ||
| ) { | ||
| Text( | ||
| text = "OK", | ||
| style = VideoTheme.typography.labelL, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun FaultInjectorUiDemo() { | ||
| VideoTheme { | ||
| FailureInjectorUi( | ||
| Modifier, | ||
| object : FailureInjector { | ||
| override fun enable(key: FailureKey) {} | ||
|
|
||
| override fun disable(key: FailureKey) {} | ||
|
|
||
| override fun setEnabled(key: FailureKey, enabled: Boolean) {} | ||
|
|
||
| override fun isEnabled(key: FailureKey): Boolean = false | ||
|
|
||
| override fun setCount(key: FailureKey, count: Int) {} | ||
|
|
||
| override fun getCount(key: FailureKey): Int = 0 | ||
|
|
||
| override fun clear() {} | ||
|
|
||
| override fun throwDebugFault(key: FailureKey) {} | ||
|
|
||
| override fun sendFailResult( | ||
| key: FailureKey, | ||
| ): io.getstream.result.Result.Failure { | ||
| return io.getstream.result.Result.Failure( | ||
| Error.GenericError("Failure injected: $key"), | ||
| ) | ||
| } | ||
| }, | ||
| ) {} | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Install the injector for SDK instances created later.
StreamVideo.instanceOrNull()can still benullhere on cold starts without a persisted user, and this file already documents that deferred SDK init is a supported path. In that case the injector is never attached, so the new failure-injection menu silently stops working after login. Please hook this into SDK creation, or observeStreamVideo.instanceStateand reapply it whenever a new instance appears.🤖 Prompt for AI Agents