From e40a4bfee258162c1922b7e88fb0e3c9073ddc82 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Wed, 15 Jul 2026 23:30:13 +0200 Subject: [PATCH] fix(app): standardize branding and disable telemetry --- androidApp/src/main/res/values/strings.xml | 2 +- .../src/main/kotlin/com/vnidrop/app/main.kt | 6 ++-- gradle.properties | 5 +-- iosApp/Configuration/Config.xcconfig | 4 +-- iosApp/iosApp.xcodeproj/project.pbxproj | 6 ++-- shared/build.gradle.kts | 8 ++--- .../app/diagnostics/DiagnosticsCoordinator.kt | 8 +++-- .../diagnostics/HttpDiagnosticsTransport.kt | 3 ++ .../app/diagnostics/TelemetryRecorder.kt | 25 ++++++++------ .../app/feature/settings/SettingsViewModel.kt | 3 +- .../app/diagnostics/DiagnosticsTest.kt | 34 +++++++++++++++++++ .../com/vnidrop/app/feature/ViewModelsTest.kt | 18 +++++++++- 12 files changed, 92 insertions(+), 30 deletions(-) diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index 6a46d21..2125c4f 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - vnidrop + VniDrop diff --git a/desktopApp/src/main/kotlin/com/vnidrop/app/main.kt b/desktopApp/src/main/kotlin/com/vnidrop/app/main.kt index 585b8d0..b9a56b6 100644 --- a/desktopApp/src/main/kotlin/com/vnidrop/app/main.kt +++ b/desktopApp/src/main/kotlin/com/vnidrop/app/main.kt @@ -77,7 +77,7 @@ fun main(args: Array) { Window( onCloseRequest = ::exitApplication, state = windowState, - title = "vnidrop", + title = "VniDrop", // Compose keeps edge resizers active for this client-decorated Linux window. undecorated = linux, ) { @@ -166,7 +166,7 @@ private fun WindowScope.MacOsTitleBar() { ) { Box(modifier = Modifier.fillMaxSize().padding(end = MacOsTrafficLightsWidth)) { BasicText( - text = "vnidrop", + text = "VniDrop", modifier = Modifier.align(Alignment.Center), style = TextStyle( color = colors.foregroundDefault, @@ -195,7 +195,7 @@ private fun WindowScope.LinuxTitleBar( .background(colors.backgroundSurface200), ) { BasicText( - text = "vnidrop", + text = "VniDrop", modifier = Modifier.align(Alignment.Center), style = TextStyle( color = colors.foregroundDefault, diff --git a/gradle.properties b/gradle.properties index 6b35196..34d6c2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,8 +16,9 @@ android.useAndroidX=true # VniDrop: compile-time diagnostics/telemetry product surface. # false → no Share-diagnostics toggle, no telemetry or crash auto-upload stack. # Bug report UI remains available (user-initiated). -# Override per build: ./gradlew … -Pvnidrop.diagnostics.included=false -vnidrop.diagnostics.included=true +# Enable per build only when endpoint and ingest key are configured: +# ./gradlew … -Pvnidrop.diagnostics.included=true +vnidrop.diagnostics.included=false # Cloudflare Worker base URL (no trailing slash). Both endpoint/key empty → NoOp transport. # Example: https://vnidrop-diagnostics..workers.dev vnidrop.diagnostics.endpoint= diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig index b2f475d..92bef3e 100644 --- a/iosApp/Configuration/Config.xcconfig +++ b/iosApp/Configuration/Config.xcconfig @@ -1,7 +1,7 @@ TEAM_ID= -PRODUCT_NAME=vnidrop +PRODUCT_NAME=VniDrop PRODUCT_BUNDLE_IDENTIFIER=com.vnidrop.app.vnidrop$(TEAM_ID) CURRENT_PROJECT_VERSION=1 -MARKETING_VERSION=1.0 \ No newline at end of file +MARKETING_VERSION=1.0 diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 02dd757..f4eaddb 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXFileReference section */ - FA325F1B4E7D8FFDF19A5C4A /* vnidrop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = vnidrop.app; sourceTree = BUILT_PRODUCTS_DIR; }; + FA325F1B4E7D8FFDF19A5C4A /* VniDrop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VniDrop.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -59,7 +59,7 @@ C13A067056BA9F38FD87A539 /* Products */ = { isa = PBXGroup; children = ( - FA325F1B4E7D8FFDF19A5C4A /* vnidrop.app */, + FA325F1B4E7D8FFDF19A5C4A /* VniDrop.app */, ); name = Products; sourceTree = ""; @@ -87,7 +87,7 @@ packageProductDependencies = ( ); productName = iosApp; - productReference = FA325F1B4E7D8FFDF19A5C4A /* vnidrop.app */; + productReference = FA325F1B4E7D8FFDF19A5C4A /* VniDrop.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 7b799cd..04cd882 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -41,12 +41,12 @@ plugins { // included=false: no Share-diagnostics toggle, no telemetry/crash auto-upload stack. // endpoint/key both empty: transport is NoOp (safe default until Cloudflare is deployed). val diagnosticsIncluded: Boolean = - (findProperty("vnidrop.diagnostics.included") as String?)?.toBooleanStrictOrNull() ?: true + (findProperty("vnidrop.diagnostics.included") as String?)?.toBooleanStrictOrNull() ?: false val diagnosticsEndpoint: String = (findProperty("vnidrop.diagnostics.endpoint") as String?)?.trim().orEmpty() val diagnosticsIngestKey: String = (findProperty("vnidrop.diagnostics.ingestKey") as String?)?.trim().orEmpty() -check(diagnosticsEndpoint.isEmpty() == diagnosticsIngestKey.isEmpty()) { +check(!diagnosticsIncluded || diagnosticsEndpoint.isEmpty() == diagnosticsIngestKey.isEmpty()) { "vnidrop.diagnostics.endpoint and vnidrop.diagnostics.ingestKey must be configured together" } @@ -56,8 +56,8 @@ val generateDiagnosticsBuildConfig by tasks.registering { description = "Generates DiagnosticsBuildConfig from vnidrop.diagnostics.* properties" val outputDir = diagnosticsBuildConfigDir val included = diagnosticsIncluded - val endpoint = diagnosticsEndpoint - val ingestKey = diagnosticsIngestKey + val endpoint = if (included) diagnosticsEndpoint else "" + val ingestKey = if (included) diagnosticsIngestKey else "" inputs.property("vnidrop.diagnostics.included", included) inputs.property("vnidrop.diagnostics.endpoint", endpoint) inputs.property("vnidrop.diagnostics.ingestKey", ingestKey) diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/DiagnosticsCoordinator.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/DiagnosticsCoordinator.kt index 0173f63..4b3cd8c 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/DiagnosticsCoordinator.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/DiagnosticsCoordinator.kt @@ -18,13 +18,14 @@ class DiagnosticsCoordinator( val crashReporter: CrashReporter, val bugReports: BugReportService, private val scope: CoroutineScope, + private val included: Boolean = DiagnosticsBuildConfig.INCLUDED, ) { fun start() { // Install id is useful for bug-report correlation even without telemetry. scope.launch { preferencesRepository.ensureDiagnosticsInstallId() } - if (!DiagnosticsBuildConfig.INCLUDED) return + if (!included) return crashReporter.startObservingPreferences() crashReporter.installUnhandledExceptionHandler() scope.launch { @@ -33,7 +34,7 @@ class DiagnosticsCoordinator( } fun record(name: String, properties: Map = emptyMap()) { - if (!DiagnosticsBuildConfig.INCLUDED) return + if (!included) return telemetry.record(name, properties) } @@ -45,6 +46,7 @@ class DiagnosticsCoordinator( preferencesRepository: PreferencesRepository, scope: CoroutineScope, transport: DiagnosticsTransport = NoOpDiagnosticsTransport(), + included: Boolean = DiagnosticsBuildConfig.INCLUDED, ): DiagnosticsCoordinator { val breadcrumbs = BreadcrumbBuffer() val crashStore = createPendingCrashStore(appDataDir) @@ -53,6 +55,7 @@ class DiagnosticsCoordinator( transport = transport, breadcrumbs = breadcrumbs, scope = scope, + included = included, ) val crashReporter = CrashReporter( store = crashStore, @@ -78,6 +81,7 @@ class DiagnosticsCoordinator( crashReporter = crashReporter, bugReports = bugReports, scope = scope, + included = included, ) } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/HttpDiagnosticsTransport.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/HttpDiagnosticsTransport.kt index 193da04..14e68e4 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/HttpDiagnosticsTransport.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/HttpDiagnosticsTransport.kt @@ -119,6 +119,7 @@ fun createDiagnosticsTransport( platform: String, installIdProvider: suspend () -> String, ): DiagnosticsTransport = buildDiagnosticsTransport( + included = DiagnosticsBuildConfig.INCLUDED, endpoint = DiagnosticsBuildConfig.ENDPOINT, ingestKey = DiagnosticsBuildConfig.INGEST_KEY, appVersion = appVersion, @@ -127,12 +128,14 @@ fun createDiagnosticsTransport( ) internal fun buildDiagnosticsTransport( + included: Boolean = true, endpoint: String, ingestKey: String, appVersion: String, platform: String, installIdProvider: suspend () -> String, ): DiagnosticsTransport { + if (!included) return NoOpDiagnosticsTransport() val normalizedEndpoint = endpoint.trim() val normalizedIngestKey = ingestKey.trim() if (normalizedEndpoint.isEmpty() && normalizedIngestKey.isEmpty()) return NoOpDiagnosticsTransport() diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/TelemetryRecorder.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/TelemetryRecorder.kt index 8e04ad7..66b0e06 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/TelemetryRecorder.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/diagnostics/TelemetryRecorder.kt @@ -31,6 +31,7 @@ class TelemetryRecorder( private val flushIntervalMillis: Long = DefaultFlushIntervalMillis, private val retryBackoffMillis: Long = DefaultRetryBackoffMillis, private val automaticRetryCount: Int = DefaultAutomaticRetryCount, + private val included: Boolean = true, ) { private val bufferMutex = Mutex() private val state = AtomicReference(TelemetryState()) @@ -42,22 +43,24 @@ class TelemetryRecorder( require(flushIntervalMillis > 0) { "flushIntervalMillis must be positive" } require(retryBackoffMillis > 0) { "retryBackoffMillis must be positive" } require(automaticRetryCount >= 0) { "automaticRetryCount must not be negative" } - scope.launch { - preferencesRepository.preferences - .map { it.diagnosticsEnabled } - .distinctUntilChanged() - .collect { isEnabled -> - updateState { current -> - if (isEnabled) current.copy(enabled = true) else TelemetryState(enabled = false) + if (included) { + scope.launch { + preferencesRepository.preferences + .map { it.diagnosticsEnabled } + .distinctUntilChanged() + .collect { isEnabled -> + updateState { current -> + if (isEnabled) current.copy(enabled = true) else TelemetryState(enabled = false) + } + flushSignals.trySend(Unit) } - flushSignals.trySend(Unit) - } + } + scope.launch { runAutomaticFlushes() } } - scope.launch { runAutomaticFlushes() } } fun record(name: String, properties: Map = emptyMap()) { - if (!DiagnosticsBuildConfig.INCLUDED) return + if (!included) return val sanitizedName = sanitizeDiagnosticName(name) if (sanitizedName.isBlank()) return val sanitizedProperties = sanitizeDiagnosticProperties(properties) diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt index 7f450e2..1cfc999 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt @@ -88,6 +88,7 @@ class SettingsViewModel( private val messages: UiMessageController, private val bugReports: BugReportService, private val diagnostics: DiagnosticsCoordinator? = null, + private val diagnosticsIncluded: Boolean = DiagnosticsBuildConfig.INCLUDED, ) : ViewModel() { private val _state = MutableStateFlow( SettingsState( @@ -200,7 +201,7 @@ class SettingsViewModel( } fun setDiagnosticsEnabled(enabled: Boolean) { - if (!DiagnosticsBuildConfig.INCLUDED) return + if (!diagnosticsIncluded) return viewModelScope.launch { preferencesRepository.setDiagnosticsEnabled(enabled) diagnostics?.record( diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/diagnostics/DiagnosticsTest.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/diagnostics/DiagnosticsTest.kt index c83dad3..0c9bf90 100644 --- a/shared/src/commonTest/kotlin/com/vnidrop/app/diagnostics/DiagnosticsTest.kt +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/diagnostics/DiagnosticsTest.kt @@ -273,6 +273,19 @@ class DiagnosticsTest { assertIs(transport) } + @Test + fun excludedTransportIgnoresIncompleteConfiguration() { + val transport = buildDiagnosticsTransport( + included = false, + endpoint = "https://diag.example", + ingestKey = "", + appVersion = "1.0", + platform = "Test", + installIdProvider = { "id" }, + ) + assertIs(transport) + } + @Test fun noOpTransportReportsUnavailableDelivery() = runTest { val result = NoOpDiagnosticsTransport().sendEvents( @@ -329,6 +342,27 @@ class DiagnosticsTest { assertEquals(1, breadcrumbs.snapshot().size) } + @Test + fun excludedTelemetryDoesNotStartOrRecordEvents() = runTest { + val transport = RecordingDiagnosticsTransport() + val breadcrumbs = BreadcrumbBuffer() + val recorder = TelemetryRecorder( + preferencesRepository = fakePrefs(diagnosticsEnabled = true), + transport = transport, + breadcrumbs = breadcrumbs, + scope = TestScope(UnconfinedTestDispatcher(testScheduler)), + flushThreshold = 1, + included = false, + ) + + recorder.record("app_open") + advanceUntilIdle() + + assertEquals(0, recorder.pendingCount()) + assertTrue(transport.events.isEmpty()) + assertTrue(breadcrumbs.snapshot().isEmpty()) + } + @Test fun telemetryRetainsColdStartEventsUntilConsentLoads() = runTest { val backing = fakePrefs(diagnosticsEnabled = true) diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt index c026019..c2843d7 100644 --- a/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt @@ -158,7 +158,7 @@ class ViewModelsTest { fun settingsTogglesDiagnosticsPreference() = runTest { Dispatchers.setMain(StandardTestDispatcher(testScheduler)) val preferences = preferences() - val viewModel = settingsViewModel(preferences) + val viewModel = settingsViewModel(preferences, diagnosticsIncluded = true) advanceUntilIdle() assertFalse(viewModel.state.value.diagnosticsEnabled) viewModel.setDiagnosticsEnabled(true) @@ -167,6 +167,20 @@ class ViewModelsTest { assertTrue(viewModel.state.value.diagnosticsEnabled) } + @Test + fun settingsIgnoresDiagnosticsOptInWhenExcluded() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + val preferences = preferences() + val viewModel = settingsViewModel(preferences) + advanceUntilIdle() + + viewModel.setDiagnosticsEnabled(true) + advanceUntilIdle() + + assertFalse(preferences.mutablePreferences.value.diagnosticsEnabled) + assertFalse(viewModel.state.value.diagnosticsEnabled) + } + @Test fun settingsSubmitsBugReportAndClearsForm() = runTest { Dispatchers.setMain(StandardTestDispatcher(testScheduler)) @@ -647,6 +661,7 @@ class ViewModelsTest { notifications: FakeNotificationService = FakeNotificationService(), transport: DiagnosticsTransport = RecordingDiagnosticsTransport(), fileSystem: FakeFileSystemService = FakeFileSystemService(folder), + diagnosticsIncluded: Boolean = false, ) = SettingsViewModel( environment(), { DeviceInfo("Device", "Model", "OS", "Wi-Fi", "80%") }, @@ -662,6 +677,7 @@ class ViewModelsTest { platform = "Test", logReader = { "sample log line" }, ), + diagnosticsIncluded = diagnosticsIncluded, ) private fun receivedTransfer(id: ULong, status: TransferStatus) = Transfer(