Unified analytics client for App Store Connect and Google Play. Works with live APIs or CSV exports.
pnpm add @drift/app-store-analyticsimport { AppAnalyticsClient, CsvAdapter } from '@drift/app-store-analytics'
const adapter = await CsvAdapter.fromFile('./fixit-active_last_30_days.csv', 'ios', 'app-id', 'FIXiT')
console.log(adapter.getAvailableMetrics()) // ['active_devices']
const client = new AppAnalyticsClient()
client.registerAdapter(adapter)
const series = await client.getMetrics('ios', 'app-id', 'active_devices', {
start: new Date('2026-01-01'),
end: new Date('2026-06-30'),
})Get credentials from App Store Connect → Users & Access → Integrations → App Store Connect API.
import { AppAnalyticsClient } from '@drift/app-store-analytics'
import { readFile } from 'node:fs/promises'
const client = new AppAnalyticsClient({
appStoreConnect: {
keyId: process.env.ASC_KEY_ID!,
issuerId: process.env.ASC_ISSUER_ID!,
privateKey: await readFile('./AuthKey_XXXXXX.p8', 'utf-8'),
},
})
const apps = await client.getApps('ios')
const reviews = await client.getReviews('ios', apps[0].id, { limit: 50 })Create a service account in Google Cloud Console, grant it access in Play Console → Users & Permissions.
const client = new AppAnalyticsClient({
googlePlay: {
clientEmail: process.env.GOOGLE_CLIENT_EMAIL!,
privateKey: process.env.GOOGLE_PRIVATE_KEY!,
},
})
const reviews = await client.getReviews('android', 'nz.govt.wellington.fixit')App Store Connect rate limits are strict. Opt into read-through caching with
cache — an in-memory store with sensible per-method TTLs by default:
const client = new AppAnalyticsClient({
appStoreConnect: { /* ... */ },
cache: {}, // enables an in-memory cache (apps 24h, metrics 6h, reviews 30m, ratings 1h)
})
// Override TTLs, or plug in your own store (Redis, etc.)
const client2 = new AppAnalyticsClient({
appStoreConnect: { /* ... */ },
cache: { ttls: { metrics: 60 * 60_000 }, store: myRedisStore },
})Errors are never cached, metric date-ranges are keyed by the day (so a moving
"last 30 days" window still hits), and concurrent identical calls are coalesced
into a single request. Implement the CacheStore interface to back it with
anything — note that a serialising store loses Date types on revive.
const summary = await client.getCrossPlatformSummary(
{ ios: 'ios-app-id', android: 'com.example.app' },
['installs', 'active_devices'],
{ start: new Date('2026-01-01'), end: new Date('2026-06-30') },
)Quick terminal queries — works against CSV exports immediately, or the live
APIs when credentials are set (env vars, or a .env in the working directory).
# Build once, then use the bin (or `pnpm cli -- <args>` in dev)
pnpm build
# Metric series from a CSV export
app-store-analytics metrics --csv ./active.csv --metric active_devices
# Reviews from the live App Store Connect API, one-star only
app-store-analytics reviews --app 1234567890 --rating 1
# Rating summary as JSON
app-store-analytics ratings --csv ./reviews.csv --jsonFIXiT — active_devices (ios)
2026-03-01 → 2026-06-30 · 4 points
▁▃▅█
first 1100 (2026-03-01)
latest 1500 (2026-06-30)
peak 1500
total 5100
change +36.4%
Run app-store-analytics help for the full flag list.
The api/ directory holds Vercel serverless functions that wrap the library,
and public/index.html is a self-contained dashboard. Deploy with zero config:
vercel --prodSet ASC_KEY_ID / ASC_ISSUER_ID / ASC_PRIVATE_KEY and
GOOGLE_CLIENT_EMAIL / GOOGLE_PRIVATE_KEY as project env vars for live data.
| Endpoint | Query |
|---|---|
GET /api/metrics |
platform, app, metric, days (or start/end) |
GET /api/reviews |
platform, app, limit?, rating? |
GET /api/summary |
ios?, android?, metrics (csv), days |
The dashboard calls these for live data and falls back to parsing CSV exports entirely in the browser when no credentials are configured — so it's useful even before you have API keys.
| Key | App Store Connect | Google Play |
|---|---|---|
installs |
✓ live (report-based) | ✓ (Cloud Storage) |
active_devices |
✓ live (report-based) | ✓ (Cloud Storage) |
sessions |
✓ live (report-based) | ✓ (Cloud Storage) |
crashes |
no data yet¹ | via Vitals API |
page_views |
✓ live (report-based) | — |
paying_users |
— | — |
App Store Connect metrics are live via the report-based pipeline.
getMetricsdiscovers the app's ONGOING analytics report request, fetches DAILY instances, downloads gzipped TSV segments from S3, and aggregates rows by date. Report data has a ~24–48h provisioning delay per app — usepnpm asc:enable <appId>to create the ONGOING request if you haven't already. CSV exports remain the fallback for any metric without live instances yet.Column → MetricKey mapping (verified from live data 2026-06-11):
Metric Report Column Filter installsApp Downloads Standard CountsDownload Type = First-time downloadsessionsApp Sessions Standard Sessions— active_devicesApp Sessions Standard Unique Devices— page_viewsApp Store Discovery and Engagement Standard CountsEvent = Page viewcrashesApp Crashes Crashes(assumed)— ¹
crashes: the App Crashes report has no DAILY instances yet as of 2026-06-11 (report request created 2026-06-08). Will auto-enable once Apple provisions it.active_devicessumsUnique Devicesper row across dimensional combinations (source type, territory, etc.) — may slightly overcount the true unique total; this is the best available approximation from the report API.Google Play rich metrics (installs, sessions, etc.) require a Cloud Storage linked account. Reviews and ratings work without it.
- Go to App Store Connect → Users and Access → Integrations → App Store Connect API
- Generate a new key (Admin role for full analytics access)
- Download the
.p8file — you can only download it once - Note your Key ID and Issuer ID
import type { AnalyticsAdapter } from '@drift/app-store-analytics'
class MyAdapter implements AnalyticsAdapter {
readonly platform = 'ios' as const
// implement getApps, getMetrics, getReviews, getRatingSummary
}
client.registerAdapter(new MyAdapter())