diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java index a358c4453b5..a4852cb6740 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java @@ -18,7 +18,6 @@ import static android.content.DialogInterface.BUTTON_POSITIVE; import static android.view.animation.AnimationUtils.loadAnimation; import static org.javarosa.form.api.FormEntryController.EVENT_PROMPT_NEW_REPEAT; -import static org.odk.collect.android.analytics.AnalyticsEvents.OPEN_MAP_KIT_RESPONSE; import static org.odk.collect.android.formentry.FormIndexAnimationHandler.Direction.BACKWARDS; import static org.odk.collect.android.formentry.FormIndexAnimationHandler.Direction.FORWARDS; import static org.odk.collect.android.utilities.AnimationUtils.areAnimationsEnabled; @@ -80,7 +79,6 @@ import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; import org.joda.time.LocalDateTime; -import org.odk.collect.analytics.Analytics; import org.odk.collect.android.R; import org.odk.collect.android.analytics.AnalyticsUtils; import org.odk.collect.android.application.Collect; @@ -865,7 +863,6 @@ protected void onActivityResult(int requestCode, int resultCode, final Intent in switch (requestCode) { case RequestCodes.OSM_CAPTURE: - Analytics.log(OPEN_MAP_KIT_RESPONSE, "form"); setWidgetData(intent.getStringExtra("OSM_FILE_NAME")); break; case RequestCodes.EX_ARBITRARY_FILE_CHOOSER: diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 703eff2ac40..e103f7b8432 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -7,19 +7,6 @@ object AnalyticsEvents { */ const val SET_SERVER = "SetServer" - /** - * Track video requests with high resolution setting turned off. The action should be a hash of - * the form definition. - */ - const val REQUEST_VIDEO_NOT_HIGH_RES = "RequestVideoNotHighRes" - - /** - * Track video requests with high resolution setting turned on. This is tracked to contextualize - * the counts with the high resolution setting turned off since we expect that video is not very - * common overall. The action should be a hash of the form definition. - */ - const val REQUEST_HIGH_RES_VIDEO = "RequestHighResVideo" - /** * Track submission encryption. The action should be a hash of the form definition. */ @@ -31,28 +18,6 @@ object AnalyticsEvents { */ const val SUBMISSION = "Submission" - /** - * Tracks if any forms are being used as part of a workflow where instances are imported - * from disk - */ - const val IMPORT_INSTANCE = "ImportInstance" - - /** - * Tracks if any forms are being used as part of a workflow where instances are imported - * from disk and then encrypted - */ - const val IMPORT_AND_ENCRYPT_INSTANCE = "ImportAndEncryptInstance" - - /** - * Tracks responses from OpenMapKit to the OSMWidget - */ - const val OPEN_MAP_KIT_RESPONSE = "OpenMapKitResponse" - - /** - * Tracks how often users create shortcuts to forms - */ - const val CREATE_SHORTCUT = "CreateShortcut" - /** * Tracks how often instances that have been deleted on disk are opened for editing/viewing */ @@ -124,36 +89,16 @@ object AnalyticsEvents { const val INSTANCE_PROVIDER_DELETE = "InstanceProviderDelete" - /** - * Tracks how often form-level auto-delete setting is used - */ - const val FORM_LEVEL_AUTO_DELETE = "FormLevelAutoDelete" - - /** - * Tracks how often form-level auto-send setting is used - */ - const val FORM_LEVEL_AUTO_SEND = "FormLevelAutoSend" - - /** - * Tracks how often a form is finalized using a `ref` attribute on the `submission` element - */ - const val PARTIAL_FORM_FINALIZED = "PartialFormFinalized" - /** * Tracks how often drafts that can't be bulk finalized are attempted to be */ const val BULK_FINALIZE_ENCRYPTED_FORM = "BulkFinalizeEncryptedForm" const val BULK_FINALIZE_SAVE_POINT = "BulkFinalizeSavePoint" - /** - * Tracks how often printing with the old ExPrinterWidget is triggered - */ - const val ZEBRA_PRINTER_STARTED = "ZebraPrinterStarted" - /** * Tracks how often saved forms are manually deleted and in what number */ - const val DELETE_SAVED_FORM_FEW = "DeleteSavedFormFew" - const val DELETE_SAVED_FORM_TENS = "DeleteSavedFormTens" - const val DELETE_SAVED_FORM_HUNDREDS = "DeleteSavedFormHundreds" + const val DELETE_SAVED_FORM_FEW = "DeleteSavedFormFew" // < 10 + const val DELETE_SAVED_FORM_TENS = "DeleteSavedFormTens" // >= 10 + const val DELETE_SAVED_FORM_HUNDREDS = "DeleteSavedFormHundreds" // >= 100 } diff --git a/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt b/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt index fe2d5394beb..e0c5d059422 100644 --- a/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt @@ -21,8 +21,6 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R -import org.odk.collect.android.analytics.AnalyticsEvents -import org.odk.collect.android.analytics.AnalyticsUtils import org.odk.collect.android.formlists.blankformlist.BlankFormListItem import org.odk.collect.android.formlists.blankformlist.BlankFormListViewModel import org.odk.collect.android.injection.DaggerUtils @@ -59,10 +57,6 @@ class AndroidShortcutsActivity : AppCompatActivity() { .map { it.formName } .toTypedArray() ) { _: DialogInterface?, item: Int -> - AnalyticsUtils.logServerEvent( - AnalyticsEvents.CREATE_SHORTCUT, - settingsProvider.getUnprotectedSettings() - ) val intent = getShortcutIntent(blankFormListItems, item) setResult(RESULT_OK, intent) finish() diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java index 79b06569cfc..ea8dd695898 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java @@ -14,11 +14,11 @@ package org.odk.collect.android.instancemanagement; +import static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString; + import android.net.Uri; import org.apache.commons.io.FileUtils; -import org.odk.collect.android.analytics.AnalyticsEvents; -import org.odk.collect.android.analytics.AnalyticsUtils; import org.odk.collect.android.application.Collect; import org.odk.collect.android.exception.EncryptionException; import org.odk.collect.android.external.InstancesContract; @@ -50,8 +50,6 @@ import timber.log.Timber; -import static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString; - public class InstanceDiskSynchronizer { private static int counter; @@ -187,22 +185,11 @@ private String getInstanceIdFromInstance(final String instancePath) { private void encryptInstanceIfNeeded(Form form, Instance instance) throws EncryptionException, IOException { if (instance != null) { if (shouldInstanceBeEncrypted(form)) { - logImportAndEncrypt(form); encryptInstance(instance); - } else { - logImport(form); } } } - private void logImport(Form form) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.IMPORT_INSTANCE, form.getFormId(), form.getDisplayName()); - } - - private void logImportAndEncrypt(Form form) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.IMPORT_AND_ENCRYPT_INSTANCE, form.getFormId(), form.getDisplayName()); - } - private void encryptInstance(Instance instance) throws EncryptionException, IOException { String instancePath = instance.getInstanceFilePath(); File instanceXml = new File(instancePath); diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt index 0731c50ecce..8b88a59c852 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt @@ -1,14 +1,8 @@ package org.odk.collect.android.instancemanagement.autosend -import org.odk.collect.android.analytics.AnalyticsEvents -import org.odk.collect.android.analytics.AnalyticsUtils import org.odk.collect.forms.Form fun Form.shouldFormBeSentAutomatically(isAutoSendEnabledInSettings: Boolean): Boolean { - if (!autoSend.isNullOrEmpty()) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.FORM_LEVEL_AUTO_SEND, formId, displayName) - } - return if (isAutoSendEnabledInSettings) { getAutoSendMode() != FormAutoSendMode.OPT_OUT } else { diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java index 600a2679198..c40fd464a0a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java @@ -39,7 +39,6 @@ import org.json.JSONException; import org.json.JSONObject; import org.odk.collect.analytics.Analytics; -import org.odk.collect.android.analytics.AnalyticsEvents; import org.odk.collect.android.application.Collect; import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.exception.EncryptionException; @@ -357,10 +356,6 @@ private Instance exportData(boolean markCompleted, FormSaver.ProgressListener pr // now see if the packaging of the data for the server would make it // non-reopenable (e.g., encryption or other fraction of the form). boolean canEditAfterCompleted = formController.isSubmissionEntireForm(); - if (!canEditAfterCompleted) { - Analytics.log(AnalyticsEvents.PARTIAL_FORM_FINALIZED, "form"); - } - boolean isEncrypted = false; // build a submission.xml to hold the data being submitted diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt index 0b05398d560..2202958c87b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt @@ -1,7 +1,5 @@ package org.odk.collect.android.utilities -import org.odk.collect.android.analytics.AnalyticsEvents -import org.odk.collect.android.analytics.AnalyticsUtils import org.odk.collect.forms.FormsRepository import org.odk.collect.forms.instances.Instance import java.util.Locale @@ -21,10 +19,6 @@ object InstanceAutoDeleteChecker { instance: Instance ): Boolean { formsRepository.getLatestByFormIdAndVersion(instance.formId, instance.formVersion)?.let { form -> - if (!form.autoDelete.isNullOrEmpty()) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.FORM_LEVEL_AUTO_DELETE, form.formId, form.displayName) - } - return if (isAutoDeleteEnabledInProjectSettings) { form.autoDelete == null || form.autoDelete.trim().lowercase(Locale.US) != "false" } else { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java index 4d777e4724d..af2c2cadb2a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java @@ -27,8 +27,6 @@ import androidx.annotation.NonNull; import org.javarosa.core.model.data.IAnswerData; -import org.odk.collect.analytics.Analytics; -import org.odk.collect.android.analytics.AnalyticsEvents; import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.databinding.ExPrinterWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; @@ -187,7 +185,6 @@ private void onButtonClick() { try { waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); firePrintingActivity(intentName); - Analytics.log(AnalyticsEvents.ZEBRA_PRINTER_STARTED, "form"); } catch (ActivityNotFoundException e) { waitingForDataRegistry.cancelWaitingForData(); Toast.makeText(getContext(), @@ -240,4 +237,4 @@ private void firePrintingActivity(String intentName) throws ActivityNotFoundExce bcastIntent.putExtra("DATA", printDataBundle); getContext().sendBroadcast(bcastIntent); } -} \ No newline at end of file +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java index eb553578119..eca5256f0e8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java @@ -14,8 +14,6 @@ package org.odk.collect.android.widgets; -import static org.odk.collect.android.analytics.AnalyticsEvents.REQUEST_HIGH_RES_VIDEO; -import static org.odk.collect.android.analytics.AnalyticsEvents.REQUEST_VIDEO_NOT_HIGH_RES; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import android.annotation.SuppressLint; @@ -32,7 +30,6 @@ import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.StringData; import org.javarosa.form.api.FormEntryPrompt; -import org.odk.collect.analytics.Analytics; import org.odk.collect.android.databinding.VideoWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.Appearances; @@ -163,10 +160,8 @@ private void captureVideo() { boolean highResolution = settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_HIGH_RESOLUTION); if (highResolution) { i.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); - Analytics.log(REQUEST_HIGH_RES_VIDEO, "form"); - } else { - Analytics.log(REQUEST_VIDEO_NOT_HIGH_RES, "form"); } + try { waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); ((Activity) getContext()).startActivityForResult(i, requestCode); diff --git a/docs/ANALYTICS-QUESTIONS.md b/docs/ANALYTICS-QUESTIONS.md new file mode 100644 index 00000000000..00a44b5fcd7 --- /dev/null +++ b/docs/ANALYTICS-QUESTIONS.md @@ -0,0 +1,15 @@ +# Analytics questions + +A list of questions asked and answered via analytics already sectioned by the date (with a 90 day recording window). + +## June 2024 + +- How often is high-res video disabled? 2% of users using video widget. +- How often is `OSMWidget` used? Once. +- How many users create shortcuts? 126. +- How many user are using ADB to add forms/instances? 904 for Forms, 2728 for instances (500k events). +- How many users are importing encrypted instances? 10. +- How often is partial form finalization used? Never. +- How many forms use auto send attribute? 200+ (max recordable) with 1.5k users. +- How many forms use auto delete attribute? 200+ (max recordable) with 1k users. +- How often is old printer widget (`ExPrinterWidget`) used? Never. diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 627fa4906f7..81b6d7ab8b1 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(project(":settings")) implementation(project(":strings")) implementation(project(":web-page")) + implementation(project(":analytics")) implementation(Dependencies.android_material) implementation(Dependencies.kotlin_stdlib) implementation(Dependencies.androidx_fragment_ktx) diff --git a/maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt b/maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt new file mode 100644 index 00000000000..cfcf7bfed56 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt @@ -0,0 +1,11 @@ +package org.odk.collect.maps + +object AnalyticsEvents { + + /** + * Tracks how many offline layers people are importing at once + */ + const val IMPORT_LAYER_SINGLE = "ImportLayerSingle" // One + const val IMPORT_LAYER_FEW = "ImportLayerFew" // <= 5 + const val IMPORT_LAYER_MANY = "ImportLayerMany" // > 5 +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 1674d8f503e..1b507b51df5 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -5,10 +5,12 @@ import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import org.odk.collect.analytics.Analytics import org.odk.collect.androidshared.system.copyToFile import org.odk.collect.androidshared.system.getFileExtension import org.odk.collect.androidshared.system.getFileName import org.odk.collect.async.Scheduler +import org.odk.collect.maps.AnalyticsEvents import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.TempFiles @@ -75,7 +77,10 @@ class OfflineMapLayersViewModel( _isLoading.value = true scheduler.immediate( background = { - tempLayersDir.listFiles()?.forEach { + val layers = tempLayersDir.listFiles() + logImport(layers) + + layers?.forEach { referenceLayerRepository.addLayer(it, shared) } tempLayersDir.delete() @@ -99,4 +104,15 @@ class OfflineMapLayersViewModel( _isLoading.postValue(false) } } + + private fun logImport(layers: Array?) { + val count = layers?.size ?: return + val event = when { + count == 1 -> AnalyticsEvents.IMPORT_LAYER_SINGLE + count <= 5 -> AnalyticsEvents.IMPORT_LAYER_FEW + else -> AnalyticsEvents.IMPORT_LAYER_MANY + } + + Analytics.log(event) + } }