Tally ships two SDKs. Both are designed to work inside other people's apps, so the #1 rule is: never cause problems in the host app.
- Zero dependencies — uses only Net::HTTP from Ruby stdlib. Analytics gems run inside Rails apps, Django apps, Sinatra apps. A dependency conflict between your gem and the host app is a production incident.
- Fire and forget — network errors are silently swallowed. A broken analytics service must never crash the host app.
- Thread-safe — single global client instance, safe for multi-threaded servers (Puma).
# config/initializers/tally.rb
TallyAnalytics.configure do |config|
config.api_key = ENV["TALLY_API_KEY"]
config.endpoint = "https://tally.example.com"
end
# Anywhere in your app
TallyAnalytics.track("user@email.com", "subscription-update", plan: "premium", mrr: 99)
TallyAnalytics.identify("user@email.com", name: "Jane Doe", plan: "premium")
TallyAnalytics.alias("anon_abc123", "user@email.com")| Before (Mixpanel) | After (Tally) |
|---|---|
tracker.track(email, "new-user") |
TallyAnalytics.track(email, "new-user") |
tracker.track(email, "subscription-update") |
TallyAnalytics.track(email, "subscription-update") |
tracker.people.set(email, {...}) |
TallyAnalytics.identify(email, {...}) |
~5 call sites on the backend. 10-minute swap.
- ~2KB minified — loaded on every page, must be tiny
- Client-side batching — accumulate events in memory, flush every 5 seconds. Reduces HTTP requests from N to 1.
- sendBeacon on unload —
navigator.sendBeaconfires reliably on page close. Without this, 10-30% of events are lost on navigation. - localStorage for identity — generates
anon_xxxxUUID, stored in localStorage (no cookies = simpler privacy/GDPR) - No dependencies — vanilla JS, no build step required for the host app
<!-- Drop this in your app's <head> -->
<script src="https://tally.example.com/sdk/tally.js"></script>
<script>
tally('init', 'pk_abc123');
tally('track', 'page-view', { page: '/quotes' });
tally('identify', 'user@email.com', { plan: 'premium' });
</script>Event fired → pushed to in-memory queue
↓
5-second timer fires (or page unload)
↓
POST /api/v1/batch with all queued events
↓
Queue cleared on success, re-queued on failure
| Before (Intercom) | After (Tally) |
|---|---|
intercom.trackEvent("quote") |
tally('track', 'quote') |
intercom.trackEvent("project") |
tally('track', 'project') |
~5 call sites on the frontend. Another 10-minute swap.
-
Why zero dependencies? — Error/analytics gems run inside other apps. A version conflict between your
faradayand the host app'sfaradayis a production incident that has nothing to do with analytics. -
Why swallow errors silently? — Analytics is never on the critical path. If Tally is down, the host app must keep working. Log a warning, move on.
-
Why sendBeacon? — Regular XHR/fetch requests get cancelled on page navigation.
sendBeaconis specifically designed to survive page unloads. Without it, you lose every event that happens right before a user navigates away. -
Why localStorage over cookies? — Simpler privacy story. No cookie banners needed for a UUID that never leaves the analytics context. Cookies also get sent on every HTTP request to the domain, which is wasteful.