Product

What Pharen does, exactly.

Shipping an iOS app is one loop: cut a build, get it onto devices, watch what happens, fix what breaks. Most stacks split that loop across three or four vendors — the crash lives in one tool, the session that led to it in another, and the build it came from belongs to nobody. Pharen keeps the loop whole: every event shares one envelope, one consent model, and one release registry, so a crash arrives already tied to its build, its session, and its user. The sections below are the loop, in exact behavior — including the connection that lets your agent ask about all of it — and where a detail matters, the docs carry the full account.

01 · Crash reporting

Captured at crash time. Delivered next launch. Counted once.

A crashing process can’t be trusted to make a network request on its way down. Pharen writes a compact report to disk at crash time and uploads it the next time your app launches. The report file is the durable queue — deleted only when the server confirms a terminal outcome.

Every report gets a deterministic id derived from the report itself, so retries across launches re-send the same id and the server answers duplicate. A crash is counted once, no matter how many launches it takes to get through. Nothing to configure, nothing to clean up.

  • Uncaught exceptions and the fatal POSIX signals, with call stack, binary images, and the identity of the build that died — read at crash time, not upload time.
  • Handlers chain: existing handlers still run, signals re-raise, the OS crash report is still produced.
  • Grouping fingerprints the symbolicated stack and ignores volatile parts. A resolved bug that turns up on a build the crash was never seen on reopens as a regression. Pharen decides that from the build it happened on, never from one build number being higher than another.
  • Late dSYMs retroactively re-group issues you already collected, with real function names.
swifthandled errors, with context
Pharen.shared?.crashReporting.recordError(
    "Checkout failed: payment gateway timeout",
    stack: Thread.callStackSymbols,
    severity: .error,
    breadcrumbs: ["tapped_pay", "network_slow"]
)
The delivery guarantee, plainly

Reports keep their original id and re-send until acknowledged. The server deduplicates by id. Re-running, relaunching, or retrying can never double-count a crash.

02 · Distribution

The whole release in one command.

One step archives your app, uploads the build, mints an over-the-air install link, registers the release, and uploads every dSYM the archive produced — so crashes from that build are readable from the moment it’s installed. Send the link; a tester taps it in Safari and the app installs. No App Store, no cable, no login.

  • Install links are served from a locked, CDN-fronted edge host at unguessable paths — shareable, not enumerable.
  • Builds and dSYMs upload straight to edge object storage over presigned URLs — multi-megabyte artifacts never squeeze through an app server.
  • Version, build number, and bundle id are read from the archived Info.plist, so uploaded metadata can never drift from what was built.
  • The one secret is an auth token in your CI environment. Org and app live in a committed .pharen.yml that grants nothing on its own.
  • Prefer fastlane? The plugin is MIT-licensed and published to RubyGems — one pharen_release call after build_app.

The underlying CLI is provided with your credentials when your application is approved.

bashthe release lane
$ PHAREN_AUTH_TOKEN=… bash release/release-lane.sh
→ archiving (Release) · exporting .ipa
→ symbols upload · 3 dSYMs
→ registering release 1.2.0 (42) · commit 3f9c2e1
→ builds upload
itms-services://?action=download-manifest
  &url=https://install.pharen.ai/install/…/manifest.plist
Environments are decided at build time
Configuration
Ingest key
Lands in
Debug
phi_test_…
development
Release
phi_live_…
production

The environment derives on the server from the key compiled into the build — a Debug build physically cannot write to production data.

03 · Sessions & analytics

One envelope. One timeline. The whole experience.

Sessions manage themselves: start at launch, continue across a short break, end after 15 minutes without activity — no code. Two calls cover your own product analytics: track() for named events, screen() for screen views.

Because every event shares one strict envelope, crashes, screens, and custom events sit in the same timeline in the event explorer, carry the same session id, roll up into session summaries, and filter the same way. A crash isn’t a separate universe from the behavior that led to it.

  • Batched and sent off the main thread; persisted to disk as queued, so a killed app delivers on next launch.
  • Every event has an id the server deduplicates on — a retry never double-counts.
  • Properties are for dimensions and metrics, not people: the SDK drops obvious identifying keys before sending, and the server enforces the rule regardless.
swifttwo calls, that's the API
Pharen.shared?.track("checkout_completed", properties: [
    "order_value": 49.99,
    "items": 3
])

Pharen.shared?.screen("Cart")
In the console

Events arrive as a live stream — type, properties, session, device context — and sessions roll up into summaries: duration and the events they contain. Segment views by release, environment, and event dimensions; acquisition-channel segmentation is on the roadmap, stated plainly as such.

05 · Identity & consent

Know your users without carrying their PII.

Every event carries a stable, pseudonymous install id — no login required. Call identify() and events are also linked to your own user id, connecting one user’s activity across their sessions and installs.

Traits route to a restricted attribute store, separate from events. They are never inlined into event properties and never written to logs. The event stream carries only the pseudonymous id.

The full consent architecture →
swiftidentity, separated from events
Pharen.shared?.identify("cus_482", traits: ["plan": "pro"])

// events carry:      cus_482  (pseudonymous id)
// restricted store:  plan=pro (never on the stream,
//                    never in logs)
06 · Your agent

Ask it, exactly. It answers about your app.

A query key is read-only, scoped to the apps and the one environment you name, rate-limited, and revocable — tenant and environment derive from the key itself, never from the request. Point any MCP client at mcp.pharen.ai with that key in the header and the tools it lists are the ones the key allows. The host is a thin transport: it keeps no session, adds no privilege, and refuses a missing key before any tool runs.

What is askable is a registry, not a query language. A request names a metric, the dimensions to group by, and the filters — each of which must exist in the registry, or the request is refused. Grouped cells under five are withheld for builds that reached customers; your own development and tester builds answer exactly when the query names its build provenance. Nothing about a person crosses without a grant you chose. On this path Pharen runs no model of its own.

  • Aggregates from the SDK: counts and sums grouped by version, build, platform, device model, OS, and build provenance — sessions, crashes, reach, new and retained installs, adoption, thermal load, SDK health, config rollout, funnel volume and reach.
  • Your crashes, grouped: each issue with its title, exception, counts, when it was first and last seen, and the representative symbolicated frames — section 07.
  • What the store shows: ratings per territory, review text as published, search and chart rank, downloads and engagement, the store’s own launch and hang percentiles — section 08.
  • Your configuration, on a connection that presents your control credential: what each build actually enforces, and the revision history behind it.
  • Users and installs, only under a grant: A user or an install, what your app said about a user, and the address a reporter left to be reached at cross only on a key you minted with the grant that covers them and a stated purpose, and every such read is recorded: in your identity reads log, or on the issue's history for a reporter's address.
  • Never, under any grant: a raw event, a location a device shared, anything from the special-category store, anything under the health purpose. Those are structurally absent from the registry, not filtered out at request time.
Connect your agent →
jsonFIXTURE DATAone tool call, and what comes back
// → pharen_query
{ "metric": "crashes.count",
  "app_id": "sample-app",
  "environment": "production",
  "group_by": ["app_version", "device_model"] }

// ← rows (released builds; small cells withheld)
9.9.9   iPhone14,5   14
9.9.9   iPhone15,3    6
9.9.8   iPhone14,5    5
// meta: suppressed_rows 1 · small_cell_threshold 5
The connection, plainly

Mint: pharen query-keys create --environment production --app your-app (omit --app for a key that reads every app you own) . Connect: any MCP client that sends a bearer header, at https://mcp.pharen.ai/mcp. Revoke: pharen query-keys revoke. Every call is re-authenticated by the platform; the host keeps no key.

07 · Your crashes

The crash your agent can read.

An issue is every crash that shares a cause, grouped: its title and exception, how many crashes were counted into it, when it was first and last seen, the newest build it was seen on, and the frames of one representative crash — symbolicated when that build’s symbols were uploaded. The agent that writes the fix can read exactly that, over the same read-only key, newest activity first.

  • Never a single crash row, a device or user identifier, or an install — an issue carries code identifiers, not people. The per-crash handle and the fingerprint the console shows are withheld from the connection.
  • No small-cell floor here: a count of crashes grouped by nothing but its cause fingerprints nobody.
  • Every build appears, developer builds beside customer builds — a two-part build number such as 731.1 is a developer build — because issues carry no provenance filter. The customer-facing crash count stays crashes.count, which defaults to released builds.
  • The most recently active issues, up to a hundred; the console holds the full history.
FIXTURE DATAone call, the crashes grouped
// → pharen_issues · sample-app · production
NSInvalidArgumentException · DemoCart.total()
  open · 6 crashes · last seen on 9.9.9 (999)
  DemoCart.total() → CheckoutView.body → main

SIGSEGV · SampleStore.load(fixture:)
  open · 14 crashes · last seen on 9.9.9 (999)
  SampleStore.load(fixture:) → SampleStore.init → main
08 · The store

What the store sees, on the same connection.

The SDK measures your app from inside. The store measures it from outside — whether anyone finds it, takes it, pays for it, and rates it — across users who never opted into anything. Connect your App Store Connect key once and that view arrives beside the other: ratings per territory, customer reviews as published, search and chart placement, downloads and engagement, the store’s own launch and hang percentiles.

  • Two kinds of fact, kept apart. Public listing facts — ratings, ranks, listing metadata — are public information, not Customer Data. What your own credential retrieves — reviews, sales, performance — is retrieved at your instruction, kept to your account, and is Customer Data under the Terms’ Store Data clause. Proceeds are never served over a query key: read them in the console, or with your control credential.
  • Read-only, always. Nothing is written back to the store; a reply to a review is written by you, in App Store Connect.
  • Freshness follows the import, not the question: every number carries when it was last synced, and absence is null, never zero.
  • Search rank comes from the store’s own search ordering — a proxy for on-device placement, said as such. Apple today; the plane is built for more than one store.
FIXTURE DATAone call, what the store measures
// → pharen_store_overview · sample-app · production
rating        4.8 (n=126) · US · v9.9.9
chart_ranks   top-free/6008  #38 · US
reviews       3 this week · 1 unanswered
perf          launch p50 412 ms · hang rate 0.3%
sync          ok · 2h ago
// commerce and engagement: from your own key

Want to see it against your app?

Access is by application — your keys are yours to mint the moment it’s approved. Bringing a larger team? [email protected]

Start with Pharen