[PM-37255] feat: Add fill-assist targeting rules data layer#6991
[PM-37255] feat: Add fill-assist targeting rules data layer#6991aj-rosado wants to merge 15 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## PM-37255/fill-assist-network-layer #6991 +/- ##
======================================================================
- Coverage 86.47% 86.34% -0.14%
======================================================================
Files 873 878 +5
Lines 63670 63892 +222
Branches 9234 9272 +38
======================================================================
+ Hits 55059 55166 +107
- Misses 5436 5527 +91
- Partials 3175 3199 +24
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…assist-data-layer
…emoved unnecessary deserialization tests
…assist-data-layer
…ow updates Updating code to schema with required values
| serverConfigRepository.serverConfigStateFlow | ||
| .onEach { config -> | ||
| environmentDiskSource.fillAssistRulesUrl = | ||
| config?.serverData?.environment?.fillAssistRulesUrl |
There was a problem hiding this comment.
Why store the same data in two places?
There was a problem hiding this comment.
We cannot use ServerConfigRepository on the BaseUrlInterceptor as it would cause a circular dependency. Stored on environmentDiskSource to avoid it.
I think that despite storing same data in two places, it is cleaner and more consistent with our code than the other approach of passing the url
| ) | ||
| }.also { result -> | ||
| result.onFailure { Timber.w(it, "Fill-assist sync failed") } | ||
| } |
There was a problem hiding this comment.
The also is redundant, you can just do this:
...
fillAssistDiskSource.storeLastFetchTimestamp(
serverUrl = serverUrl,
timestamp = clock.millis(),
)
}
.onFailure { Timber.w(it, "Fill-assist sync failed") }| val parsedFields = form.fields | ||
| .mapValues { (_, elem) -> parseCompositeSelectorArray(elem) } | ||
| .filterValues { it.isNotEmpty() } | ||
| .takeIf { it.isNotEmpty() } ?: return@forEach |
There was a problem hiding this comment.
The ?: return@forEach should be on it's own line
There was a problem hiding this comment.
Also, should the return be a continue?
There was a problem hiding this comment.
Had to confirm it but it acts like a continue on this scenario, continue here returns an error
'break' and 'continue' are only allowed inside loops.
| private fun parseCompositeSelectorArray(element: JsonElement): List<SelectorClause> { | ||
| if (element !is JsonArray) return emptyList() | ||
| val result = mutableListOf<SelectorClause>() | ||
| for (item in element) { |
There was a problem hiding this comment.
Can we make this more functional?
private fun parseCompositeSelectorArray(element: JsonElement): List<SelectorClause> {
if (element !is JsonArray) return emptyList()
return element.flatMap { item ->
when (item) {
is JsonPrimitive -> listOfNotNull(parseSingleSelector(item.content))
is JsonArray -> {
item
.filterIsInstance<JsonPrimitive>()
.mapNotNull { parseSingleSelector(it.content) }
}
else -> emptyList()
}
}
}| forms: List<FillAssistFormsJson.FormJson>, | ||
| ): Map<String, MutableMap<String, MutableList<SelectorClause>>> { | ||
| val result = mutableMapOf<String, MutableMap<String, MutableList<SelectorClause>>>() | ||
| forms.forEach { form -> |
There was a problem hiding this comment.
This one looks a bit more complicated but could this also be made more functional... Using groupBy might do the trick
| "id" -> id = attrValue | ||
| "name" -> name = attrValue | ||
| "type" -> type = attrValue | ||
| "role" -> role = attrValue |
There was a problem hiding this comment.
Can we make these constants?
| val allForms = buildList { | ||
| addAll(hostEntry.forms.orEmpty()) | ||
| hostEntry.pathnames?.values?.filterNotNull()?.forEach { addAll(it.forms) } | ||
| }.distinct() |
There was a problem hiding this comment.
What do you think of this?
val allForms = buildList {
addAll(hostEntry.forms.orEmpty())
addAll(hostEntry.pathnames?.values?.filterNotNull()?.flatMap { it.forms }.orEmpty())
}
.distinct()|
|
||
| ATTRIBUTE_REGEX.findAll(effective).forEach { match -> | ||
| val attrName = match.groupValues[1] | ||
| val attrValue = match.groupValues[2] |
There was a problem hiding this comment.
Can we add some additional comments here to explain what is actually happening.
This all seems rather odd (Autofill usually is that way lol)
There was a problem hiding this comment.
We are still processing the data returned from the server forms. Basically this PR is processing the server data into something that can be useful to use when Autofilling.
Added a comment with an example that should make it easier to understand.
Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the data layer for fill-assist targeting rules: per-server disk source with cache-version migration, a self-managing sync manager driven by Code Review DetailsNo new findings beyond what has already been raised in the existing review threads. The code is consistent with the codebase, the per-server scoping is correctly implemented (the |
|
|
||
| val forms = fillAssistService | ||
| .getForms(filename = versionEntry.filename) | ||
| .getOrThrow() |
There was a problem hiding this comment.
Can we not throw here.
val forms = fillAssistService
.getForms(filename = versionEntry.filename)
.getOrNull()
?: return@runCatching| } | ||
|
|
||
| private suspend fun sync(serverUrl: String) = runCatching { | ||
| val manifest = fillAssistService.getManifest().getOrThrow() |
There was a problem hiding this comment.
Same, let's handle all these error cases more gracefully, we shouldn't be throwing everything
| val parsedFields = form.fields | ||
| .mapValues { (_, elem) -> parseCompositeSelectorArray(elem) } | ||
| .filterValues { it.isNotEmpty() } | ||
| .takeIf { it.isNotEmpty() } ?: return@mapNotNull null |
There was a problem hiding this comment.
Formatting:
.takeIf { it.isNotEmpty() }
?: return@mapNotNull null…assist-data-layer
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-37255
📔 Objective
Adds the data layer for caching and managing fill-assist targeting rules, including CSS selector parsing and the self-managing sync manager.
Changes:
FillAssistRules— domain model withHostRuleandSelectorClause(tag, id, name, type, role); CSS selectors are parsed at sync time, not at autofill timeFillAssistDiskSource/FillAssistDiskSourceImpl— per-server SharedPreferences storage (keyed byfillAssistRulesUrl); includesCURRENT_CACHE_VERSIONmigration to invalidate cached data when parsing logic changesFillAssistManager/FillAssistManagerImpl— self-managing singletonthat syncs onserverConfigStateFlowchanges; uses a 6-hour timestamp throttle before checking the manifest CID; CSS forms parsed from all pathnames, pooled and merged by category (oneHostRuleper form type per host); shadow DOM selectors (>>>) use the last segment for matchingFillAssistModule— Hilt DI bindings