This sample collects the following sensitive user data and sends it to Firebase Analytics using the logEvent method:
User ID (Data type: User IDs, Category: Personal info)
Blood type (Data type: Health info, Category: Health and fitness)
For the sake of this demo, we pretend that the app is published on Google Play and that the data types collected are not disclosed in the Data safety section.
packageorg.owasp.mastestappimportandroid.os.Bundleimportandroidx.activity.ComponentActivityimportandroidx.activity.compose.setContentimportandroidx.activity.enableEdgeToEdgeimportandroidx.compose.foundation.layout.Columnimportandroidx.compose.foundation.layout.paddingimportandroidx.compose.material3.Buttonimportandroidx.compose.material3.DropdownMenuimportandroidx.compose.material3.DropdownMenuItemimportandroidx.compose.material3.Textimportandroidx.compose.runtime.Composableimportandroidx.compose.runtime.getValueimportandroidx.compose.runtime.mutableStateOfimportandroidx.compose.runtime.rememberimportandroidx.compose.runtime.setValueimportandroidx.compose.ui.Modifierimportandroidx.compose.ui.graphics.Colorimportandroidx.compose.ui.platform.LocalContextimportandroidx.compose.ui.platform.testTagimportandroidx.compose.ui.text.AnnotatedStringimportandroidx.compose.ui.text.SpanStyleimportandroidx.compose.ui.text.buildAnnotatedStringimportandroidx.compose.ui.text.font.FontFamilyimportandroidx.compose.ui.text.withStyleimportandroidx.compose.ui.tooling.preview.Previewimportandroidx.compose.ui.unit.dpimportandroidx.compose.ui.unit.spimportkotlinx.serialization.json.Jsonimportkotlinx.serialization.json.JsonArrayimportkotlinx.serialization.json.decodeFromJsonElementconstvalMASTG_TEXT_TAG="mastgTestText"classMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)enableEdgeToEdge()setContent{MainScreen()}}}funUpdateDisplayString(defaultMessage:String,result:String):AnnotatedString{returnbuildAnnotatedString{append(defaultMessage)try{valjsonArrayFromString=Json.parseToJsonElement(result)asJsonArrayvaldemoResults=jsonArrayFromString.map{Json.decodeFromJsonElement<DemoResult>(it)}for(demoResultindemoResults){when(demoResult.status){Status.PASS->{withStyle(style=SpanStyle(color=Color.Green)){append("MASTG-DEMO-${demoResult.demoId} demonstrated a successful test:\n${demoResult.message}\n\n")}}Status.FAIL->{withStyle(style=SpanStyle(color=Color(0xFFFF9800))){append("MASTG-DEMO-${demoResult.demoId} demonstrated a failed test:\n${demoResult.message}\n\n")}}Status.ERROR->{withStyle(style=SpanStyle(color=Color.Red)){append("MASTG-DEMO-${demoResult.demoId} failed:\n${demoResult.message}\n\n")}}}}}catch(_:Exception){// not a valid set of DemoResult, so print the result without any parsingappend(result)}}}@Preview@ComposablefunMainScreen(){valdefaultMessage="Click \"Start\" to send the data.\n\n"vardisplayStringbyremember{mutableStateOf(buildAnnotatedString{append(defaultMessage)})}varselectedBloodTypebyremember{mutableStateOf("")}valcontext=LocalContext.currentvalmastgTestClass=MastgTest(context)// By default run the test in a separate thread, this ensures that network tests such as those using SSLSocket work properly.// However, some tests which interact with UI elements need to run on the main thread.// You can set shouldRunInMainThread = true in MastgTest.kt for those tests.valrunInMainThread=MastgTest::class.members.find{it.name=="shouldRunInMainThread"}?.call(mastgTestClass)as?Boolean?:falseBaseScreen(onStartClick={if(runInMainThread){valresult=mastgTestClass.mastgTest(selectedBloodType)displayString=UpdateDisplayString(defaultMessage,result)}else{Thread{valresult=mastgTestClass.mastgTest(selectedBloodType)android.os.Handler(android.os.Looper.getMainLooper()).post{displayString=UpdateDisplayString(defaultMessage,result)}}.start()}}){Column(modifier=Modifier.padding(16.dp)){// Normal visible selection UI: list of radio buttons for blood typesvalbloodTypes=listOf("A+","A-","B+","B-","AB+","AB-","O+","O-")varexpandedbyremember{mutableStateOf(false)}Button(onClick={expanded=!expanded}){Text("Select Blood Type")}DropdownMenu(expanded=expanded,onDismissRequest={expanded=false}){bloodTypes.forEach{DropdownMenuItem(text={Text(it)},onClick={selectedBloodType=itexpanded=false})}}if(selectedBloodType.isNotEmpty()){Text(modifier=Modifier.padding(vertical=16.dp),color=Color.White,text="Selected Blood Type: $selectedBloodType")}Text(modifier=Modifier.padding(top=8.dp).testTag(MASTG_TEXT_TAG),text=displayString,color=Color.White,fontSize=16.sp,fontFamily=FontFamily.Monospace)}}}
packageorg.owasp.mastestappimportandroid.content.Contextimportcom.google.firebase.analytics.FirebaseAnalyticsimportcom.google.firebase.analytics.logEventimportkotlin.random.RandomclassMastgTest(context:Context){valanalytics=FirebaseAnalytics.getInstance(context)// Random arbitrary number for the sake of the demovaluserId:String=(1..8).map{Random.nextInt(0,10)}.joinToString("")funmastgTest(bloodType:String):String{analytics.logEvent("user_blood_type"){param("user_id",userId)param("blood_type",bloodType)}return""" 'user_blood_type' event was sent to Firebase Analytics. User id: $userId Blood type: $bloodType """.trimIndent()}}
The output shows all instances of logEvent calls to the Firebase Analytics SDK found at runtime, along with the parameters sent. A backtrace is also provided to help identify the location in the code.
This test fails because sensitive data (blood_type parameter) is being sent to Firebase Analytics via the logEvent method for a particular user (user_id parameter) and this data collection is not disclosed in the Data safety section on Google Play (as we indicated in the sample description).