Skip to content

Implement opt-in anonymous telemetry and device identity management.#430

Merged
rainxchzed merged 5 commits intomainfrom
backend-settings-integr
Apr 18, 2026
Merged

Implement opt-in anonymous telemetry and device identity management.#430
rainxchzed merged 5 commits intomainfrom
backend-settings-integr

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented Apr 18, 2026

  • Add DeviceIdentityRepository to manage unique, anonymous device identifiers with support for ID rotation.
  • Implement TelemetryRepository to record search performance, repository views, and installation events.
  • Add telemetry batching and flushing logic with a 30-second interval and offline retry support.
  • Integrate search and repository click tracking into SearchViewModel.
  • Introduce a Privacy section in Settings (Tweaks) to toggle telemetry and reset the analytics ID.
  • Update DetailsRepositoryImpl to supplement backend repository stats with best-effort GitHub API calls for licenses and issue counts.
  • Add a lenient ISO timestamp parser in TimeFormatters.kt to handle inconsistent backend date formats.
  • Update localization strings for privacy and telemetry across all supported languages.

Summary by CodeRabbit

  • New Features

    • Privacy section in Settings with telemetry toggle and "reset anonymous analytics ID" action
    • App now records telemetry events (searches, clicks, repo views, installs, favorites, uninstalls) and flushes them in background
  • Improvements

    • Repo details enriched with live GitHub info (issues/license) when available
    • More resilient date parsing/formatting in listings
  • Localization

    • New privacy/telemetry UI text added across supported languages; placeholder “None” changed to an em dash (—)

- Add `DeviceIdentityRepository` to manage unique, anonymous device identifiers with support for ID rotation.
- Implement `TelemetryRepository` to record search performance, repository views, and installation events.
- Add telemetry batching and flushing logic with a 30-second interval and offline retry support.
- Integrate search and repository click tracking into `SearchViewModel`.
- Introduce a Privacy section in Settings (Tweaks) to toggle telemetry and reset the analytics ID.
- Update `DetailsRepositoryImpl` to supplement backend repository stats with best-effort GitHub API calls for licenses and issue counts.
- Add a lenient ISO timestamp parser in `TimeFormatters.kt` to handle inconsistent backend date formats.
- Update localization strings for privacy and telemetry across all supported languages.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46cf9cb5-9fba-420d-9d78-6d8fca586d18

📥 Commits

Reviewing files that changed from the base of the PR and between 8e25d12 and 738e9f9.

📒 Files selected for processing (9)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/system/InstallationManagerImpl.kt
  • feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SaveInstalledAppParams.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt

Walkthrough

Adds a telemetry system: device identity persistence, telemetry recording with buffered batching and backend submission, telemetry opt-in controls and UI, DI registrations for telemetry components, query hashing, and multiple integrations into Search, Details, and Tweaks features.

Changes

Cohort / File(s) Summary
DI Module & ViewModel wiring
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt, core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
Switched DetailsViewModel Koin registration to params-based viewModel { params -> ... }. Registered DeviceIdentityRepository and TelemetryRepository singletons in core DI module.
Telemetry domain interfaces
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/DeviceIdentityRepository.kt, core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt
Added DeviceIdentityRepository and TelemetryRepository interfaces describing device ID lifecycle and telemetry recording/flush APIs.
Telemetry data layer
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt, core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/EventRequest.kt, core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt, core/data/src/commonMain/kotlin/zed/rainxch/core/data/utils/QueryHash.kt
Implemented TelemetryRepositoryImpl with in-memory buffered events, mutex protection, periodic 30s flush, batch posting to backend, retry/re-enqueue semantics, EventRequest DTO, backend postEvents() with status handling (including 429->RateLimitedException), and hashQuery() utility.
Device identity & tweaks storage
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt, core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt, core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt
Added DeviceIdentityRepositoryImpl backed by DataStore (generate/reset UUID). Extended TweaksRepository interface and impl to persist telemetry opt-in (getTelemetryEnabled() / setTelemetryEnabled(enabled)).
Presentation: localization & utilities
core/presentation/src/commonMain/composeResources/values*/strings*.xml (default + 15 locales), core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt
Replaced license_none text with em dash across locales and added six privacy/telemetry string keys. Added lenient ISO instant parser and updated time-format helpers to use it.
Tweaks feature: UI, VM, events
feature/tweaks/presentation/src/commonMain/.../TweaksViewModel.kt, .../TweaksAction.kt, .../TweaksEvent.kt, .../TweaksState.kt, .../components/sections/Others.kt, .../TweaksRoot.kt
Added telemetry toggle and reset-analytics UI; new actions/events for toggling telemetry and resetting analytics ID; TweaksViewModel observes/persists telemetry opt-in and calls device ID reset; snackbar shown on reset.
Search feature integration
feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt
Injected TelemetryRepository and recorded recordSearchPerformed() for initial searches and recordSearchResultClicked() on repo clicks.
Details feature integration & install handling
feature/details/presentation/.../DetailsViewModel.kt, feature/details/data/.../DetailsRepositoryImpl.kt, feature/details/data/.../InstallationManagerImpl.kt, feature/details/domain/.../SaveInstalledAppParams.kt
Added telemetry calls across install/uninstall/open/favorite flows in DetailsViewModel. DetailsRepositoryImpl enriches repo stats with GitHub data via a GitHubClientProvider. Installation saving now persists asset variant metadata; SaveInstalledAppParams extended with siblingAssetCount and pickedAssetIndex.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI (Search / Tweaks / Details)
    participant VM as ViewModel
    participant TR as TelemetryRepositoryImpl
    participant DI as DeviceIdentityRepository
    participant Buffer as In-Memory Buffer
    participant BE as BackendApiClient
    participant Server as Backend Server

    UI->>VM: user action (search / click / install / reset)
    VM->>TR: recordXxx(...) call
    TR->>DI: getDeviceId()
    alt no telemetry consent or no device id
        TR-->>VM: drop event (silent)
    else telemetry enabled & device id present
        TR->>Buffer: enqueue EventRequest (with appVersion, queryHash)
    end

    rect rgba(150,200,100,0.5)
    Note over TR,Buffer: periodic flush (every 30s) on appScope
    end

    TR->>Buffer: dequeue up to MAX_BATCH_SIZE
    TR->>BE: postEvents(batch)
    alt success (2xx or 204)
        BE-->>TR: success
    else 429
        BE-->>TR: RateLimitedException
        TR->>Buffer: re-enqueue batch at front (bounded)
    else other error
        BE-->>TR: BackendException
        TR->>Buffer: re-enqueue batch at front (bounded)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #246: Modifies DetailsViewModel constructor and Koin DI registration to params-based factories — directly related to the ViewModel wiring changes here.
  • PR #352: Changes to tweaks persistence and preference-backed tweak keys; strongly related to added telemetry opt-in persistence.
  • PR #360: Edits Koin viewModelsModule and ViewModel registrations — overlaps with the DI registration changes for DetailsViewModel.

Poem

🐰 I burrowed bytes into a tidy queue,

events hop in while consent is true.
Thirty seconds, then off they dart,
to backend fields we send with heart.
Press reset—new ID, fresh start. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the PR's primary objective: implementing opt-in anonymous telemetry and device identity management.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend-settings-integr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt`:
- Around line 22-27: The viewModel provider in ViewModelsModule.kt uses
params.get() for multiple parameters of the same type which is ambiguous; switch
to indexed Koin parameters so each argument to DetailsViewModel is unambiguous:
use params.get(0) for repositoryId, params.get(1) for ownerParam, params.get(2)
for repoParam and params.get(3) for isComingFromUpdate when constructing
DetailsViewModel in the viewModel { params -> ... } lambda.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt`:
- Around line 17-29: getDeviceId() has a race between read and write leading to
inconsistent persisted ID; to fix, add a shared coroutine Mutex (e.g., private
val deviceIdMutex = Mutex()) in DeviceIdentityRepositoryImpl and wrap the bodies
of both getDeviceId() and resetDeviceId() in deviceIdMutex.withLock { ... } so
the read-check/generate/write sequence is atomic; use
kotlinx.coroutines.sync.Mutex and withLock to serialize access to DEVICE_ID_KEY
in preferences.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt`:
- Around line 100-120: flushPending must re-check the user's telemetry consent
(the same check used by enqueue) before sending buffered events: at the start of
flushPending, call the current consent-check (the same method/flag used by
enqueue) and if consent is false, acquire bufferMutex, clear buffer, and return
early so nothing is sent; additionally, before re-adding pending events on
failure (inside the result.isFailure branch) check consent again and skip
re-adding if consent is now false to avoid restoring dropped events. Ensure you
reference buffer, bufferMutex, flushPending, enqueue,
backendApiClient.postEvents, and the failure re-add loop when making the
changes.

In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/utils/QueryHash.kt`:
- Around line 5-16: hashQuery currently returns a truncated deterministic
SHA-256 of the normalized query, which can be used to correlate queries across
users; change it to produce a non-linkable value by using a keyed HMAC with a
per-installation random secret (or stop deriving identifiers from queries
entirely). Update the function hashQuery to accept a required key/salt parameter
(or read a securely-stored per-install random secret) and compute
HMAC-SHA256(normalized, key) instead of plain SHA-256, and ensure you do not
truncate the result to a predictable short value; alternatively remove usages
that send query-derived identifiers for telemetry. Locate and update the
function hashQuery and any callers to provide the key/secret or switch to a
non-query-derived metric. Ensure the secret is generated once per installation
and stored securely so the HMAC is pseudonymous rather than deterministic across
users.
- Around line 3-16: hashQuery currently uses the JVM-only
java.security.MessageDigest in commonMain; move the platform-specific SHA-256
implementation behind an expect/actual (or existing multiplatform crypto) API
and call that from hashQuery. Specifically, keep fun hashQuery(query: String) in
commonMain but replace the MessageDigest logic with a call like
sha256Hex(normalized) (or the existing utility function if found), create expect
fun sha256Hex(input: ByteArray): String in commonMain and provide actual
implementations in JVM/Android (using MessageDigest) and other targets
(JS/IOS/Native) returning hex; preserve the normalization, empty-string
shortcut, and the hex.take(16) return behavior.

In `@core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml`:
- Line 588: The Polish telemetry string (string name "telemetry_description" in
strings-pl.xml) should not assert an absolute “no personal data is collected”;
update the value to describe the data as collected in an anonymous or
pseudonymous form (e.g., "telemetria anonimowa/pseudonimizowana") and remove the
categorical "Żadne dane osobowe nie są zbierane" claim so it accurately reflects
that searches/installs are recorded under a persistent anonymous analytics ID;
locate the "telemetry_description" entry and replace its text with a phrasing
that conveys pseudonymous/anonymous usage telemetry and consent-friendly
language.

In `@core/presentation/src/commonMain/composeResources/values/strings.xml`:
- Around line 641-647: The string resource telemetry_description currently
states "No personal information is collected" which is inaccurate; update the
string named "telemetry_description" to a neutral, accurate phrase that explains
we collect anonymized and pseudonymized usage data tied to a persistent,
resettable analytics ID (e.g., "We collect anonymized usage data (searches,
installs, interactions) tied to a resettable analytics ID; no direct identifiers
are shared with third parties"), then submit that change for legal/privacy
review and apply the same corrected wording across all 12 localized resource
files (values-ar, values-bn, values-es, values-fr, values-hi, values-it,
values-ja, values-ko, values-pl, values-ru, values-tr, values-zh-rCN) ensuring
translators/localization owners confirm appropriate translations.

In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt`:
- Around line 16-35: The function parseIsoInstantLenient currently treats
non-blank strings with surrounding whitespace as valid but fails to parse and
falls back to a normalized transform; fix by trimming the input up front: call
trim() on the isoInstant parameter (use a local val trimmed = isoInstant.trim()
and use trimmed everywhere instead of isoInstant) before the blank guard and all
parsing/normalization logic in parseIsoInstantLenient so Instant.parse and the
tz normalization (tzStart, head, tail, colonCount, normalized) operate on a
trimmed string; apply the same trimming approach to the other similar utility
usage referenced at the noted lines.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt`:
- Around line 334-349: Replace the runCatching block with an explicit try-catch
around the httpClient.executeRequest call (the githubInfo enrichment) that
rethrows CancellationException, and on any other exception logs/ignores the
error and leaves githubInfo as null so we don’t overwrite existing values with
zeros; when constructing RepoStats (the RepoStats assignment using backendRepo,
githubInfo, cacheManager.put and REPO_STATS), prefer stale cached stats if
present (query cacheManager for cacheKey before enrichment and use its
openIssues/license/totalDownloads when githubInfo is null or enrichment fails)
so transient GitHub failures fall back to stale values rather than zero/null.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 534-537: When handling TweaksAction.OnResetAnalyticsId in
TweaksViewModel, after calling deviceIdentityRepository.resetDeviceId() also
call a new TelemetryRepository.clearPending() to drop any buffered telemetry so
old deviceId isn't sent; add clearPending() to the TelemetryRepository interface
and implement it in TelemetryRepositoryImpl to lock its mutex and clear the
in-memory buffer used for offline/retry batching. Ensure the call is made inside
the same viewModelScope.launch block (after resetDeviceId()) and that
TelemetryRepositoryImpl's clearPending() acquires the same mutex used for
enqueueing to avoid races.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9078efe3-082b-407f-84c7-a271fb4776da

📥 Commits

Reviewing files that changed from the base of the PR and between 2104772 and 8e25d12.

📒 Files selected for processing (33)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/EventRequest.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/utils/QueryHash.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/DeviceIdentityRepository.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt
  • core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml
  • core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
  • core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
  • core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
  • core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
  • core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
  • core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt

Comment on lines +3 to +16
import java.security.MessageDigest

fun hashQuery(query: String): String {
val normalized = query.trim().lowercase()
if (normalized.isEmpty()) return ""
val digest = MessageDigest.getInstance("SHA-256").digest(normalized.encodeToByteArray())
val hex = buildString(digest.size * 2) {
for (byte in digest) {
val v = byte.toInt() and 0xff
if (v < 0x10) append('0')
append(v.toString(16))
}
}
return hex.take(16)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Move SHA-256 behind a multiplatform-safe API.

java.security.MessageDigest is JVM-only, so importing it from commonMain can break non-JVM targets. Keep hashQuery common, but delegate digest generation through expect/actual or an existing multiplatform crypto utility. As per coding guidelines, shared code must be in commonMain source set and use expect/actual for platform-specific APIs.

🔧 Proposed direction
 package zed.rainxch.core.data.utils
 
-import java.security.MessageDigest
+internal expect fun sha256Hex(input: String): String
 
 fun hashQuery(query: String): String {
     val normalized = query.trim().lowercase()
     if (normalized.isEmpty()) return ""
-    val digest = MessageDigest.getInstance("SHA-256").digest(normalized.encodeToByteArray())
-    val hex = buildString(digest.size * 2) {
-        for (byte in digest) {
-            val v = byte.toInt() and 0xff
-            if (v < 0x10) append('0')
-            append(v.toString(16))
-        }
-    }
-    return hex.take(16)
+    return sha256Hex(normalized).take(16)
 }

Add platform-specific actual fun sha256Hex(...) implementations in the relevant source sets.

Run this read-only check to confirm whether this module has non-JVM targets and whether an existing crypto abstraction already exists:

#!/bin/bash
# Verify commonMain JVM API usage and look for existing multiplatform hash utilities.
rg -n -C3 'java\.security\.MessageDigest|expect fun .*sha|actual fun .*sha|SHA-256|sha256' --type=kt
rg -n -C4 'kotlin\s*\(|kotlin\s*\{|androidTarget|jvm\(|ios|js\(|wasm|commonMain' --iglob '*.gradle.kts'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/utils/QueryHash.kt`
around lines 3 - 16, hashQuery currently uses the JVM-only
java.security.MessageDigest in commonMain; move the platform-specific SHA-256
implementation behind an expect/actual (or existing multiplatform crypto) API
and call that from hashQuery. Specifically, keep fun hashQuery(query: String) in
commonMain but replace the MessageDigest logic with a call like
sha256Hex(normalized) (or the existing utility function if found), create expect
fun sha256Hex(input: ByteArray): String in commonMain and provide actual
implementations in JVM/Android (using MessageDigest) and other targets
(JS/IOS/Native) returning hex; preserve the normalization, empty-string
shortcut, and the hex.take(16) return behavior.

Comment on lines +5 to +16
fun hashQuery(query: String): String {
val normalized = query.trim().lowercase()
if (normalized.isEmpty()) return ""
val digest = MessageDigest.getInstance("SHA-256").digest(normalized.encodeToByteArray())
val hex = buildString(digest.size * 2) {
for (byte in digest) {
val v = byte.toInt() and 0xff
if (v < 0x10) append('0')
append(v.toString(16))
}
}
return hex.take(16)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid treating deterministic query hashes as anonymous.

A truncated SHA-256 of normalized search text is still dictionary-attackable and correlates the same query across users. For anonymous telemetry, avoid sending query-derived identifiers unless the UI/privacy copy explicitly treats this as pseudonymous analytics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/utils/QueryHash.kt`
around lines 5 - 16, hashQuery currently returns a truncated deterministic
SHA-256 of the normalized query, which can be used to correlate queries across
users; change it to produce a non-linkable value by using a keyed HMAC with a
per-installation random secret (or stop deriving identifiers from queries
entirely). Update the function hashQuery to accept a required key/salt parameter
(or read a securely-stored per-install random secret) and compute
HMAC-SHA256(normalized, key) instead of plain SHA-256, and ensure you do not
truncate the result to a predictable short value; alternatively remove usages
that send query-derived identifiers for telemetry. Locate and update the
function hashQuery and any callers to provide the key/secret or switch to a
non-query-derived metric. Ensure the secret is generated once per installation
and stored securely so the HMAC is pseudonymous rather than deterministic across
users.

<string name="seen_badge">Przeglądane</string>
<string name="privacy_section">Prywatność</string>
<string name="telemetry_title">Pomóż ulepszyć wyszukiwanie</string>
<string name="telemetry_description">Udostępniaj anonimowe dane o użyciu (wyszukiwania, instalacje), aby poprawić rekomendacje. Żadne dane osobowe nie są zbierane.</string>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid promising that no personal data is collected.

Line 588 says no personal data is collected, but this feature records searches/installations under a persistent anonymous analytics ID. That is safer to describe as pseudonymous/anonymous usage telemetry and should avoid an absolute “no personal data” claim across all locales.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml`
at line 588, The Polish telemetry string (string name "telemetry_description" in
strings-pl.xml) should not assert an absolute “no personal data is collected”;
update the value to describe the data as collected in an anonymous or
pseudonymous form (e.g., "telemetria anonimowa/pseudonimizowana") and remove the
categorical "Żadne dane osobowe nie są zbierane" claim so it accurately reflects
that searches/installs are recorded under a persistent anonymous analytics ID;
locate the "telemetry_description" entry and replace its text with a phrasing
that conveys pseudonymous/anonymous usage telemetry and consent-friendly
language.

Comment on lines +641 to +647
<!-- Privacy / telemetry -->
<string name="privacy_section">Privacy</string>
<string name="telemetry_title">Help improve search</string>
<string name="telemetry_description">Share anonymous usage data (searches, installs) so we can improve recommendations. No personal information is collected.</string>
<string name="reset_analytics_id_title">Reset analytics ID</string>
<string name="reset_analytics_id_description">Generate a new anonymous ID, severing the link to past telemetry.</string>
<string name="analytics_id_reset">Analytics ID reset</string>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the strings.xml file and understand its context
find . -name "strings.xml" -type f | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 192


🏁 Script executed:

# Search for analytics/telemetry implementation in the codebase
rg "analytics|telemetry" --type java --type kt -i -l | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 96


🏁 Script executed:

# Look for any privacy policy or legal documentation
find . -type f \( -name "*privacy*" -o -name "*legal*" -o -name "*terms*" \) | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 52


🏁 Script executed:

# Search for analytics ID implementation details
rg "analytics.*id|reset.*analytics" -i -A 3 -B 3 | head -50

Repository: OpenHub-Store/GitHub-Store

Length of output: 6433


🏁 Script executed:

# Check if there are localized copies of strings.xml
find . -path "*/values*/strings.xml" -type f

Repository: OpenHub-Store/GitHub-Store

Length of output: 192


🏁 Script executed:

# Read the target strings.xml file to confirm the exact content
cat -n core/presentation/src/commonMain/composeResources/values/strings.xml | sed -n '640,650p'

Repository: OpenHub-Store/GitHub-Store

Length of output: 819


🏁 Script executed:

# Search for where telemetry/analytics data is actually collected
rg "telemetry|analytics" --type xml -A 2 -B 2

Repository: OpenHub-Store/GitHub-Store

Length of output: 17522


🏁 Script executed:

# Search for analytics data collection in Kotlin files
rg "analytics" -A 5 -B 2 | grep -E "(class|fun|data|collect|send)" | head -40

Repository: OpenHub-Store/GitHub-Store

Length of output: 592


🏁 Script executed:

# Look for localized string resources (values-* directories)
find . -type d -name "values-*" | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 817


🏁 Script executed:

# Check for any privacy-related documentation or comments in source files
rg "personal.*information|privacy.*data" -i | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 309


🏁 Script executed:

# Search for analytics data collection/sending logic
rg "analytics|telemetry" -i --type java -A 5 -B 2 | head -100

Repository: OpenHub-Store/GitHub-Store

Length of output: 52


🏁 Script executed:

# Look for any analytics or telemetry initialization/configuration
find . -type f -name "*.kt" -o -name "*.java" | xargs grep -l "analytics\|telemetry" -i | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 1316


🏁 Script executed:

# Search for HTTP requests or API calls that might send analytics data
rg "POST|GET|send|track" -i -A 3 -B 3 | grep -E "(analytics|telemetry|event)" -i | head -50

Repository: OpenHub-Store/GitHub-Store

Length of output: 8083


🌐 Web query:

California Civil Code 1798.140 personal information definition identifiers

💡 Result:

California Civil Code Section 1798.140 defines "personal information" in subdivision (v) as information that identifies, relates to, describes, is reasonably capable of being associated with, or could reasonably be linked, directly or indirectly, with a particular consumer or household. It includes, but is not limited to, the following categories if they meet the above criteria: (A) Identifiers such as a real name, alias, postal address, unique personal identifier, online identifier, Internet Protocol address, email address, account name, social security number, driver's license number, passport number, or other similar identifiers. (B) Any personal information described in subdivision (e) of Section 1798.80 (which includes name, signature, address, telephone number, education, employment, medical history, etc.). (C) Characteristics of protected classifications under California or federal law (e.g., race, religion, age, etc.). (D) Commercial information, including records of personal property, products or services purchased, obtained, or considered, or other purchasing or consuming histories or tendencies. (E) Biometric information. (F) Internet or other electronic network activity information, including, but not limited to, browsing history, search history, and information regarding a consumer’s interaction with an internet website, application, or advertisement. (G) Geolocation data. (H) Audio, electronic, visual, thermal, olfactory, or similar information. (I) Professional or employment-related information. (J) Education information not publicly available (as defined in the Family Educational Rights and Privacy Act). (K) Inferences drawn from any of the information identified to create a profile about a consumer reflecting the consumer’s preferences, characteristics, psychological trends, predispositions, behavior, attitudes, intelligence, abilities, and aptitudes. (L) Sensitive personal information. Personal information does not include publicly available information or lawfully obtained, truthful information that is a matter of public concern. This definition is from the California Consumer Privacy Act (CCPA), as amended by the California Privacy Rights Act (CPRA), and is current as of 2025-2026 per official legislative sources.

Citations:


🌐 Web query:

GDPR pseudonymised data personal data re-identification European Commission

💡 Result:

Under the GDPR, pseudonymised data remains personal data if it can be re-identified using additional information. Article 4(5) defines pseudonymisation as processing personal data so it cannot be attributed to a specific data subject without additional information kept separately and subject to technical/organisational measures. Recital 26 states that pseudonymised data which could be attributed to a natural person by additional information is information on an identifiable natural person, thus personal data within GDPR scope. The European Commission confirms that personal data de-identified, encrypted, or pseudonymised but re-identifiable remains personal data under GDPR. EDPB Guidelines 01/2025 (adopted Jan 2025) reiterate pseudonymised data remains personal data if attributable via additional information, even if held by others, and serves as a safeguard for GDPR compliance (e.g., data protection by design, security). Recent CJEU ruling (C-413/23 P EDPS v SRB, Sep 2025) clarifies: pseudonymised data is personal data for the controller holding the key, but for a recipient without reasonable means to access the key or re-identify (due to technical, organisational, legal barriers), it may not be personal data from their perspective. Re-identification risk must be assessed contextually, considering all reasonably likely means (time, cost, technology). Pseudonymisation reduces but does not eliminate GDPR obligations; unauthorised re-identification may constitute a data breach.

Citations:


🏁 Script executed:

# Read the TelemetryRepositoryImpl to see what data is collected
cat core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 5813


🏁 Script executed:

# Read the TelemetryRepository interface to understand the contract
cat core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 703


Update telemetry description to accurately reflect privacy obligations and data collection scope.

Line 644 makes an overstated "No personal information" claim that conflicts with both the implementation and privacy law. The app uses a persistent, resettable analytics ID and collects extensive event data (searches, installs, repo interactions, favorites, etc.), which under California CCPA §1798.140 and GDPR Article 4(5) constitutes personal information—identifiers combined with electronic network activity are explicitly categorized as personal data, and pseudonymised data with re-identifiable means remains personal data.

This claim requires alignment with legal/privacy review and retranslation in all 12 localized copies (values-ar, values-bn, values-es, values-fr, values-hi, values-it, values-ja, values-ko, values-pl, values-ru, values-tr, values-zh-rCN).

Suggested safer wording
-    <string name="telemetry_description">Share anonymous usage data (searches, installs) so we can improve recommendations. No personal information is collected.</string>
+    <string name="telemetry_description">Share optional usage data (searches, installs) linked to a resettable anonymous ID so we can improve recommendations. We don't collect your GitHub credentials.</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/presentation/src/commonMain/composeResources/values/strings.xml` around
lines 641 - 647, The string resource telemetry_description currently states "No
personal information is collected" which is inaccurate; update the string named
"telemetry_description" to a neutral, accurate phrase that explains we collect
anonymized and pseudonymized usage data tied to a persistent, resettable
analytics ID (e.g., "We collect anonymized usage data (searches, installs,
interactions) tied to a resettable analytics ID; no direct identifiers are
shared with third parties"), then submit that change for legal/privacy review
and apply the same corrected wording across all 12 localized resource files
(values-ar, values-bn, values-es, values-fr, values-hi, values-it, values-ja,
values-ko, values-pl, values-ru, values-tr, values-zh-rCN) ensuring
translators/localization owners confirm appropriate translations.

Comment on lines +534 to +537
TweaksAction.OnResetAnalyticsId -> {
viewModelScope.launch {
deviceIdentityRepository.resetDeviceId()
_events.send(TweaksEvent.OnAnalyticsIdReset)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Drop pending telemetry when resetting the analytics ID.

resetDeviceId() only rotates the persisted ID. Any events already buffered for offline/30-second retry still contain the old deviceId and can be posted after the reset, which undermines the reset action’s privacy expectation.

🛡️ Suggested direction
 TweaksAction.OnResetAnalyticsId -> {
     viewModelScope.launch {
+        telemetryRepository.clearPending()
         deviceIdentityRepository.resetDeviceId()
         _events.send(TweaksEvent.OnAnalyticsIdReset)
     }
 }

This requires adding a clearPending() operation to TelemetryRepository and implementing it by clearing TelemetryRepositoryImpl’s mutex-protected buffer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`
around lines 534 - 537, When handling TweaksAction.OnResetAnalyticsId in
TweaksViewModel, after calling deviceIdentityRepository.resetDeviceId() also
call a new TelemetryRepository.clearPending() to drop any buffered telemetry so
old deviceId isn't sent; add clearPending() to the TelemetryRepository interface
and implement it in TelemetryRepositoryImpl to lock its mutex and clear the
in-memory buffer used for offline/retry batching. Ensure the call is made inside
the same viewModelScope.launch block (after resetDeviceId()) and that
TelemetryRepositoryImpl's clearPending() acquires the same mutex used for
enqueueing to avoid races.

…he integration of telemetry tracking.

- **Variant Pinning**: Update `InstallationManagerImpl` to capture and store an `AssetVariant` fingerprint when a user picks an APK. This allows future updates to resolve to the same flavor (e.g., architecture or ABI) by persisting preferred asset tokens and glob patterns.
- **Telemetry Integration**: Integrate `TelemetryRepository` into `DetailsViewModel` to record user actions, including repository views, favoriting/unfavoriting, app uninstalls, and the full lifecycle of downloads and installations (start, success, failure).
- **Silent Install Persistence**: Ensure that silent installations (e.g., via Shizuku) are correctly persisted to the database in `DetailsViewModel` by validating the APK and saving the installed app state upon completion.
- **Data Model Updates**: Expand `SaveInstalledAppParams` and the internal `InstalledApp` model to include metadata about sibling asset counts and the index of the picked asset to support variant resolution.
…obustness

- **Telemetry**: Add checks to `flushPending` to ensure events are cleared and not sent if user consent is withdrawn. Implement `clearPending` to drop buffered events when the analytics ID is reset.
- **Device Identity**: Introduce a `Mutex` in `DeviceIdentityRepositoryImpl` to prevent race conditions during device ID generation and reset.
- **Time Formatting**: Improve `parseIsoInstantLenient` to handle surrounding whitespace and stabilize parsing when seconds are missing from backend timestamps.
- **Repository Improvements**: In `DetailsRepositoryImpl`, transition from `runCatching` to explicit try-catch to properly propagate `CancellationException`. Add stale cache fallback for GitHub repository stats to prevent clobbering data during transient API failures.
- **Dependency Injection**: Use indexed parameter access for `DetailsViewModel` in Koin to ensure type-safe resolution of multiple string parameters.
@rainxchzed rainxchzed merged commit 39ccd4f into main Apr 18, 2026
1 check was pending
@rainxchzed rainxchzed deleted the backend-settings-integr branch April 18, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant