Sitelet https://github.com/Parsely/AnalyticsSDK-iOS/pull/98
Skip to content

Resolve the collection endpoint per site ID at build time - #98

Draft
dhruvkb wants to merge 5 commits into
masterfrom
build-time-pixel-host
Draft

dhruvkb wants to merge 5 commits into
masterfrom
build-time-pixel-host

Conversation

@dhruvkb

@dhruvkb dhruvkb commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

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/mobileproxy, 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

  1. The publisher commits parsely-apikeys.json declaring every site ID the app can track — including any passed as a siteId: argument at runtime, not just the one given to configure(siteId:).
  2. A Run Script Build Phase (Scripts/parsely-bake-hosts.sh) resolves each one against Parse.ly and writes ParselyHosts.json into the app bundle. CocoaPods users get the phase wired by the podspec; everyone else adds it manually.
  3. configure loads the map. flush() groups queued events by resolved endpoint and issues one request per endpoint.

Grouping is by endpoint rather than by site ID deliberately: the payload already carries idsite per 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, never sent somewhere that might be the wrong region. Nothing at runtime throws or crashes: a missing or malformed map logs at error level and sends nothing.

Breaking change

An app that upgrades without adding the build phase 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, and CHANGES.rst marks it breaking.

Testing

  • make test → 106 tests, 0 failures. New coverage: no baked map sends nothing and does not crash; malformed and wrong-version artifacts fail closed; one request per endpoint; two site IDs sharing an endpoint coalesce into one request; an unknown site ID is dropped while known ones still send; a partially-offline flush requeues only the failed endpoint's events.
  • SwiftLint 0.57.0 (the pinned version) → clean.
  • Scripts/parsely-bake-hosts.sh verified against production for: a known site ID, an unknown one (build fails), a missing declaration, an empty declaration, malformed JSON, and an unreachable Parse.ly — which warns and proceeds when the committed artifact already covers every declared site ID, and fails when it does not.

Two local details worth knowing for anyone running the suite: make test pipes through xcpretty, which is not in the Gemfile, and the project's deployment target (13.0) is below what Xcode 27 accepts, so a local run needs IPHONEOS_DEPLOYMENT_TARGET=15.0 on the command line. Neither is changed here — the podspec's and Package.swift's iOS 13 floor is the SDK's public compatibility promise.

Companion changes

The equivalent Android change is Parsely/parsely-android#126. Both depend on the Parse.ly endpoint that serves the mapping, which is already live.

🤖 Generated with Claude Code

Wraps the apikey-to-host map that build tooling bakes into the app, and
owns the partitioning of a flush batch by host. There is deliberately no
default host: an apikey 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 apikey so that several apikeys
in the same region still share a single request, as they do today.
The collection endpoint is no longer hardcoded. buildRequest takes the host
it should use, configure loads the baked map from the app bundle, and flush
partitions the queue by resolved host and issues one request per host.

Events whose site ID is not in the map are dropped and logged rather than
requeued: the map cannot change while the app runs, so they would never
resolve on a later flush. An offline host returns only its own events to
the queue, so one failing region no longer strands another's events.

Also removes RequestBuilder._baseURL, which was written and never read.
Resolves each declared site ID against Parse.ly at the publisher's build
time and writes ParselyHosts.json, which the SDK reads at configure.

The build fails on a missing or empty parsely-apikeys.json, on a site ID
Parse.ly does not recognise, and on a write failure. A network failure is
the one deliberate exception: if the generated file 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 podspec wires the phase automatically for CocoaPods users; everyone
else adds a Run Script Phase, which the README now documents along with
what is lost by skipping it.
The endpoint shipped at /api/<site id>/jess/sdk_config/, not the flat path
this script was written against. Verified against production for a known
site ID, an unknown one, a malformed declaration and an unreachable host.

PARSELY_SDK_CONFIG_ENDPOINT becomes PARSELY_API_BASE, since the override is
now the API root rather than a full endpoint path.

Parse failures also stop dumping a Python traceback into the Xcode build
log, where the one-line error is what the reader needs.
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