Conversation
Wraps the site-ID-to-host map that build tooling bakes into the app asset, and owns the partitioning of a flush batch by host. There is deliberately no default host: a site ID missing from the map does not resolve, so its events can be dropped rather than sent to a region that may be wrong. Grouping is by resolved host rather than by site ID so that several site IDs in the same region still share a single request, as they do today.
ROOT_URL is gone. RestClient takes the URL per call instead of capturing it at construction, so one connection serves every host, and FlushQueue partitions the stored queue by resolved host and sends one request per host. Two bugs go with it. Events are now removed per group on that group's own success, where a single remove() previously dropped events belonging to a request that had failed. And the debug line logged the ROOT_URL constant rather than the URL actually used. Events whose site ID is not in the baked map are removed from storage after being logged: the map cannot change while the app runs, so retaining them would grow the disk-backed queue forever without them ever being sent.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #126 +/- ##
==========================================
+ Coverage 73.46% 74.94% +1.47%
==========================================
Files 23 24 +1
Lines 441 487 +46
Branches 52 61 +9
==========================================
+ Hits 324 365 +41
- Misses 99 104 +5
Partials 18 18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Resolves each declared site ID against Parse.ly at the publisher's build time and writes parsely-hosts.json into the variant's generated assets, which the SDK reads at init. Nothing is ever written into src/. The build fails on a missing or empty parsely-apikeys.json, on a site ID Parse.ly does not recognise, and on any other HTTP error. A network failure is the one deliberate exception: if the generated asset already covers every declared site ID the build warns and continues, so a Parse.ly outage does not break every customer's CI while still failing closed whenever data is actually missing. The plugin reacts to the Android plugin via pluginManager.withPlugin, so the order of the publisher's plugins block does not matter. It is not applied to :example. readme.yml builds :example:assembleDebug on every push to every branch, and a fresh CI checkout has no generated asset to fall back on, so an unreachable Parse.ly would turn a push red. It was verified by applying it to :example locally instead.
The endpoint shipped at /api/<site id>/jess/sdk_config/, not the flat path this plugin was written against. Verified end to end against production on :example: the declaration resolves, the asset is written, and it lands in the APK at assets/parsely-hosts.json. A missing declaration, an empty one, an unknown site ID and an unreachable host all behave as intended. The parsely.sdkConfigEndpoint property becomes parsely.apiBase, since the override is now the API root rather than a full endpoint path.
dhruvkb
force-pushed
the
build-time-pixel-host
branch
from
September 24, 2026 19:36
63a8a65 to
57b43dc
Compare
fromAssets had no test for the path that actually runs in a publisher's app: only the missing-asset failure was covered, so nothing verified that a baked asset is found and read. A Robolectric test now reads a fixture from the unit-test asset source set. The blank-host filter was untested too. An artifact entry with an empty host must not resolve, so its events are dropped rather than sent to "https:///mobileproxy".
Member
Author
|
Covered the two lines Codecov flagged in |
The functional tests reached MockWebServer by reflectively overwriting ROOT_URL, which this branch deletes, so all six failed in setup with NoSuchFieldException. They now learn the server's address the way a real app does, from a baked asset in androidTest/assets that maps the test site ID to localhost on a fixed port. Because the SDK sends only to https, MockWebServer serves TLS with a certificate generated per run and trusted on both sides; nothing outside the test process trusts it, and no test-shaped seam is added to the shipped SDK. Verified as far as is possible without an emulator: the sources compile and the fixture lands in the test APK at assets/parsely-hosts.json. The six tests themselves run on CI.
customSiteIdIsAppliedToConcurrentEventsInEngagementSession tracks under a SiteIdSource.Custom site ID, which the baked asset did not cover, so the SDK dropped those events exactly as designed and the test saw no request. Declaring it on the same host as the default site ID also gives the coalescing rule an end-to-end check: two site IDs, one host, one request, which is what the test already asserted.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parse.ly decides which collection endpoint a site's data goes to, and that decision can differ per site. The SDK hardcoded
https://p1.parsely.com/mobileproxyinROOT_URL, so an app could only ever send to one endpoint regardless of which one its sites actually belong to.The endpoint is now resolved per site ID at the publisher's build time and baked into the app, the same way the web tracker resolves it when it builds a site's
p.js.How it works
com.parsely.hostsGradle plugin and commitsparsely-apikeys.jsondeclaring every site ID the app can track — including any passed asSiteIdSource.Custom, not just the one given toParselyTracker.init.parsely-hosts.jsoninto the variant's generated assets. Nothing is ever written intosrc/, and the generated asset is not committed.FlushQueuegroups queued events by resolved endpoint and sends one request per endpoint.Grouping is by endpoint rather than by site ID deliberately: the payload already carries
idsiteper event, so several site IDs in one region still share a single request, exactly as they did before. A multi-site app in one region sends one request, not N.There is no default endpoint left in the binary. An event whose site ID is not in the baked map is dropped and logged rather than sent somewhere that might be the wrong region — and removed from storage, since the baked map cannot change while the app runs and retaining them would grow the disk-backed queue forever. Nothing at runtime throws: a missing or malformed asset logs at error level and sends nothing.
Once the plugin is applied it cannot be forgotten, and it fails the build rather than guessing: a missing or empty declaration, a site ID Parse.ly does not recognise, or any other HTTP error stops the build. The one deliberate exception is an unreachable Parse.ly — if the generated asset already covers every declared site ID the build warns and proceeds, so an outage does not break every publisher's CI, while still failing closed whenever data is actually missing.
Two bugs fixed along the way
FlushQueueremoved the whole batch with a singlerepository.remove(eventsToSend). Events now come out of storage per group, on that group's own success, so a failed request no longer drops the events it failed to send.FlushQueue.kt:41logged theROOT_URLconstant rather than the URL actually used.ROOT_URLis gone and the line logs the real URL.RestClient.sendtakes the URL per call instead of capturing it at construction, so oneParselyAPIConnectionserves every endpoint.Breaking change
An app that upgrades without applying the plugin sends no analytics at all. That is intentional — the alternative is silently sending a site's data to an endpoint it does not belong to — but it means this cannot ship as a minor version. The README says so up front.
apiDumpshows no public API movement, so the break is behavioural, not source-level.Testing
./gradlew :parsely:testDebugUnitTest→ 75 tests, 0 failures. New coverage: no baked asset sends nothing and does not crash; malformed and wrong-version assets fail closed; one request per endpoint; two site IDs sharing an endpoint coalesce into one request; an undeclared site ID is dropped from storage while known ones still send; a failing request for one endpoint does not remove the other endpoint's events../gradlew apiCheck→ clean,parsely/api/parsely.apiunchanged../gradlew lintDebug→ clean../gradlew :parsely-gradle-plugin:build→ clean, includingvalidatePlugins.:example: the declaration resolves, the asset is written, andassets/parsely-hosts.jsonis confirmed present in the built APK. A missing declaration, an empty one, an unknown site ID, and an unreachable Parse.ly (both with and without covering artifact) all behave as described above.The plugin is not applied to
:exampleon this branch.readme.ymlbuilds:example:assembleDebugon every push, and a fresh CI checkout has no committed artifact to fall back on, so an outage would turn every push red. Happy to wire it up if you would rather have the demo app dogfood it.Companion changes
The equivalent iOS change is Parsely/AnalyticsSDK-iOS#98. Both depend on the Parse.ly endpoint that serves the mapping, which is already live.
🤖 Generated with Claude Code