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(