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 five sections below are the loop, in exact behavior — and where a detail matters, the docs carry the full account.
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. Issues track per release — a resolved bug that reappears is flagged as a regression, not silently re-counted.
- Late dSYMs retroactively re-group issues you already collected, with real function names.
Pharen.shared?.crashes.recordError(
"Checkout failed: payment gateway timeout",
stack: Thread.callStackSymbols,
severity: "error",
breadcrumbs: ["tapped_pay", "network_slow"]
)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.
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 when we onboard your team, together with your credentials.
$ PHAREN_AUTH_TOKEN=… bash release/release-lane.sh
→ archiving (Release) · exporting .ipa
→ upload-build · registering release 1.2.0 (42)
→ upload-symbols · 3 dSYMs
itms-services://?action=download-manifest
&url=https://dl.pharen.ai/…/manifest.plistThe environment derives on the server from the key compiled into the build — a Debug build physically cannot write to production data.
One envelope. One timeline. The whole experience.
Sessions manage themselves: start at launch, end on background, resume within 30 minutes — the standard mobile rule, 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.
Pharen.shared?.track("checkout_completed", properties: [
"order_value": 49.99,
"items": 3
])
Pharen.shared?.screen("Cart")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.
Routing is yours. Telemetry is minimal.
Point your universal links at the SDK — it accepts links on pharen.link and any custom domains you add — and route every recognized link through one onLink hook. Routing always happens for a recognized link: consent gates the telemetry, never your navigation.
The open event is deliberately small. It carries the route path and a deferred flag — never the query string, which can hide magic-link tokens and PII. Your app still gets the full parse (route, params, source host) for routing; the params just never leave the device on the event stream.
- One handler covers universal links and custom-scheme opens; unrecognized hosts are simply not consumed.
- Create campaign-tagged short links on pharen.link to share — creation and opens land in the same event timeline as everything else.
- Deferred opens are flagged as such. Full post-install attribution is roadmap, stated plainly as such.
// Route recognized links (fires regardless of consent):
Pharen.shared?.deepLinks.onLink = { link in
router.open(link.targetPath, params: link.params)
}
// From .onContinueUserActivity / your scene delegate:
Pharen.shared?.deepLinks.handle(userActivity)Pharen.shared?.deepLinks.createLink(
destination: "/p/abc",
campaign: "spring_launch",
metadata: [:]
) { result in
// share result.url — https://pharen.link/…
}Know your users without carrying their PII.
Every event carries a stable, pseudonymous install id — no login required. Call identify() and events also carry your customer id, connecting a person’s activity across sessions and devices.
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 →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)Want to see it against your app?
We’ll set up keys, your first release, and your console. hello@pharen.ai