XY Xinyi Ye
Browser · iOS · Android

One signal language, three runtimes.

An interactive guide to the diagnostics client contract, real production signals, and the storage-to-upload machinery beneath each SDK.

Audience boundary. The methods are public enough for SDK modules to share, but they are not normal customer analytics APIs. Kotlin marks the interface @RestrictedAmplitudeFeature; Swift exposes it through @_spi(Internal); TypeScript exports the core type while prefixing control methods such as _flush.
01 / Contract

The common vocabulary

All three platform implementations speak in four signal types—tags, counters, histograms, and events—and expose a flush operation. The shape is intentionally small; persistence and concurrency are runtime-native.

PersistenceIndexedDB5 object stores
Memory → disk1 secondafter first mutation
Disk → server5 minutesafter reportable data
Event cap10memory and IndexedDB
Method / settingBrowser / TSiOS / Swift coreAndroid / Kotlin core
setTag✓ shared✓ shared✓ shared
increment✓ shared✓ shared✓ shared
recordHistogram✓ shared✓ shared✓ shared
recordEvent✓ shared✓ shared✓ shared
flush_flush()flush()flush()
setTags
Read tagsgetTag(s)
Lifecycle extra_setSampleRatedidLastRunCrashclose
Customer settingenableDiagnostics?enableDiagnosticsenableDiagnostics
02 / API explorer

Every operation, with a real SDK example

Search or expand a card. These are production call sites in the SDKs, not invented examples.

setTag(name, value)Attach low-cardinality context to every payload.

Meaning

A last-write-wins string label. Tags identify the SDK/runtime rather than count an occurrence. They survive flushes so the next payload keeps its context.

all platformspersistentlast write wins

Real example

// Browser initialization
diagnosticsClient.setTag('library', LIBPREFIX + '/' + VERSION);
diagnosticsClient.setTag('platform', 'Web');
diagnosticsClient.setTag('web_environment', getRuntimeEnvironment());
Production call site ↗
setTags(tags)Bulk form on iOS and Android.

Meaning

Merges app version, device, OS, platform, and SDK version in one operation. Browser performs the equivalent through repeated setTag calls.

iOSAndroidnot in TS contract

Real example

val staticContext = buildMap {
  put("version_name", contextInfo?.appVersion ?: "")
  put("device_model", contextInfo?.model ?: "")
  put("platform", contextInfo?.platform ?: "")
}
setTags(staticContext)
Production call site ↗
increment(name, size = 1)Accumulate how often something happened.

Meaning

Counters add deltas across calls and storage ticks. This powers sent/dropped totals. Passing a batch size avoids one call per analytics event.

all platformsadditive

Real example

if (status in 200..299) {
  increment("analytics.events.sent", events.size.toLong())
} else {
  increment("analytics.events.dropped", events.size.toLong())
}
Production call site ↗
recordHistogram(name, value)Measure a distribution without retaining raw samples.

Meaning

Each value updates count, min, max, and sum. Upload computes avg = sum / count; raw observations are not included. There are no percentiles.

all platformsmin/max/avg/count

Real example

const startTime = performance.now();
// Build autocapture hierarchy…
this.diagnosticsClient?.recordHistogram(
  'autocapturePlugin.getHierarchy',
  performance.now() - startTime,
);
Production call site ↗
recordEvent(name, properties?)Keep a few structured debugging examples.

Meaning

Events include a timestamp and JSON-compatible properties. A counter answers “how many?”; events explain “what happened?”. Only ten survive in a reporting window.

all platforms10-event cap

Real example

recordEvent(
  name = "analytics.events.dropped",
  properties = mapOf(
    "events" to events.map { it.eventType },
    "count" to events.size,
    "code" to status,
    "message" to message,
  ),
)
Production call site ↗
flush() / _flush()Snapshot, clear, and POST the current window.

Meaning

Forces the normal five-minute operation. Counters, histograms, and events clear before or as upload starts; tags remain. Browser’s underscore signals SDK-internal ownership.

same semanticsat-most-once attempt

Concrete implementation

public func flush() async {
  guard shouldTrack,
        let snapshot = await storage.dumpAndClearCurrentSession()
  else { return }
  await uploadSnapshot(snapshot)
}
iOS implementation ↗
Platform-only controlsAdditions outside the three-way intersection.

iOS

getTag/getTags wait for startup tags. didLastRunCrash reads crash-marker state. Callback conveniences serve non-async callers.

Swift protocol ↗

Android & Browser

Android adds close() for client/storage channels and registers a weak JVM shutdown hook. Browser adds _setSampleRate, recomputing lifecycle sampling from the original timestamp.

Kotlin lifecycle API ↗
enableDiagnosticsCustomer-visible switch; sampling still gates collection.

Meaning

Defaults to true on all three platforms. That does not guarantee collection: default sample rate is zero and remote config must select the client. The sample decision is stable for the client lifecycle.

all platformsdefault truesample rate default 0

Remote config keys

Browser  configs.diagnostics.browserSDK
iOS      diagnostics.iosSDK
Android  diagnostics.androidSDK

fields: { enabled, sampleRate }
Browser config gate ↗
03 / Interactive lifecycle

Move a signal through the client

A simplified but faithful model: calls update memory, the one-second tick persists a snapshot, and flush builds the upload payload while retaining tags.

Generate a real signal

Memory hot path

No signals yet

Persistent IndexedDB

Waiting for +1s

POST payload /v1/capture

Waiting for flush
t = 0 · record
t = +1s · persist
t = +5m · upload
Ready. Choose a signal and record it.
04 / Under the hood

Same timing, runtime-native machinery

The client is local-first: compact in memory, persist after one second, upload after five minutes. Terminated mobile-session data is recovered and attempted on next launch.

SDK call siteSerialized memory→ 1sPersistent snapshot→ 5mCapture API

◉ Browser · TypeScript

  • Synchronous calls update plain maps/array.
  • IndexedDB: AMP_diagnostics_{first 10 API-key chars}, schema version 1.
  • Stores: tags, counters, histograms, events, internal.
  • One read-write transaction reads and clears counters/histograms/events; tags remain.
  • No IndexedDB means diagnostics calls no-op.
IndexedDB schema ↗

⌘ iOS · Swift

  • Client and storage are Swift actors.
  • Application Support / com.amplitude.diagnostics / hashed instance / session timestamp.
  • Atomic JSON for tags/counters/histograms; NDJSON events.log.
  • Event logs rotate at 256 KiB; current window caps at 10 events.
  • Previous session directories load, delete, then upload at initialization.
Filesystem persistence ↗

◆ Android · Kotlin

  • Unlimited client update channel; separate 8,192-item storage channel.
  • Same directory/file layout as Swift under SDK storage.
  • FNV-1a hashed instance; timestamped session folders.
  • JSON uses temp-file rename/copy; events append and rotate at 256 KiB.
  • close() closes channels; JVM shutdown hook is best-effort.
Storage actor/layout ↗

Aggregation

Tags overwrite. Counters add. Histograms merge count/sum and min/max, then calculate average at payload time. Events keep full structured examples up to the cap.

Timer semantics

Demand-driven, not permanent polling. The first mutation schedules persistence and flush. Browser also stores its last-flush timestamp and can flush overdue data after restart.

Transport

All POST JSON to the regional US/EU /v1/capture endpoint with X-ApiKey. Swift/Kotlin also send X-Client-Sample-Rate; current TS does not.

Crash and uncaught errors

iOS consumes a previous crash on next run. Browser adds global error/rejection listeners only when sampled and only records errors matching a registered SDK script URL.

05 / Observable outcomes

How calls become dashboard questions

Panel names below were read from the supplied live dashboards.

Browser SDK diagnostics

  • Ingestion health: sent/dropped counters and drop logs.
  • Uncaught SDK errors: rate, counts, and stack-bearing events.
  • Autocapture: block-time histograms and counts.
  • Cookies: duplicates, enabled checks, top-level-domain failures.
  • Cost & sanity: indexed volume and sampled-in occurrences.
Open dashboard ↗

Mobile SDK diagnostics

  • Crash: count/rate by app and SDK version plus logs.
  • Diagnostics: sampled-in and enabled occurrences.
  • Autocapture: session, lifecycle, screen, element, network, frustration.
  • Runtime distribution: Compose, Kotlin, OS, device, platform, SDK versions.
  • Ingestion & Session Replay: health, time, size, memory, shape, churn.
Open dashboard ↗
06 / Engineering notes

Failure, privacy, and operational edges

Best-effort delivery

All three clear/delete active counters, histograms, and events before the network result is known. Failed requests are logged but not re-queued: at-most-once attempt, not guaranteed telemetry.

Potentially sensitive events

Properties may contain event types, server messages, stack traces, filenames, and crash reports. Avoid user identifiers and raw customer payloads; assume fields leave the device.

Lifecycle-stable sampling

Clients hash their initialization timestamp against a clamped 0…1 rate. Remote-config updates reuse the seed, avoiding a new random decision on every call.

Disabling collection

enabled && sampledIn gates timers/storage. Swift deletes stored files when disabled. Kotlin stops deadlines. Browser returns early when tracking or IndexedDB is absent.

Caps

Every platform caps current-window structured events at 10. Browser also caps unique in-memory tag/counter/histogram keys at 10,000 each. Mobile logs rotate at 256 KiB.

React Native rollout

An AsyncStorage adapter exists, but IS_DIAGNOSTICS_CAPTURED is currently false and the default sample rate is zero. If enabled later, one serialized blob is used; without AsyncStorage it degrades to memory-only.

Rollout gate ↗
07 / Permalinks

Primary source index

Commit-pinned links keep the guide auditable after main moves. The iOS implementation lives in AmplitudeCore-Swift, which Amplitude-Swift consumes.