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 tags—getTag(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 site→Serialized 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.