Theme
SDK support matrix
What each SDK can actually do today. Every cell was read out of the SDK source or out of a sentence in that SDK's page — nothing here is aspirational, and where the evidence did not settle a cell it says unknown rather than guessing.
| Meaning | |
|---|---|
| ✅ | Supported. Source implements it and the page documents it. |
| ⚠️ | Partial. Works, with a stated limitation — read the note. |
| ❌ | Not supported, and the SDK's page says so explicitly. |
| ❔ | Unknown. Neither the source nor the page settles it. |
Capture
| Capability | Browser | React | Node | Python | Java | Go | Swift | Android |
|---|---|---|---|---|---|---|---|---|
| Autocapture | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Session replay | ✅ | ⚠️ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ |
| Manual screen / page views | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
Autocapture. The browser plugin captures clicks, scroll depth, JS exceptions, and rage, dead and error clicks; SPA page views come from a second plugin. It does not capture form submissions. It is tag-build only, and gated on tenant entitlement. React reuses the same browser plugins, but React render errors are not autocaptured. The server SDKs have no DOM, so the question does not arise — Node's page states this outright. Swift and Android are manual: you call screen() yourself, and the Android page says "No automatic screen tracking."
Session replay. One browser plugin, built on rrweb, loaded from a public CDN by default. React inherits it unchanged. Node's page states it explicitly as unsupported. Python, Java, Go, Swift and Android are marked unknown: their source contains no replay code and their pages never mention it, so "no replay code today" is established but a deliberate product statement is not.
Delivery and durability
| Capability | Browser | React | Node | Python | Java | Go | Swift | Android |
|---|---|---|---|---|---|---|---|---|
| Batching | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Retry with exponential backoff | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Honours Retry-After | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❔ | ❔ |
| Durable queue across restart | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ⚠️ |
| Flush on shutdown | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Network reachability awareness | ❔ | ❔ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Durability. The browser queue lives in localStorage and is restored on the next load. Swift persists to an atomically written JSON file under Application Support. Android is ⚠️ because it ships a pluggable Storage interface with a file-backed implementation, but the default is in-memory and loses everything — you must pass storage yourself. Node, Python, Java and Go hold a bounded in-memory queue; Node's page puts it plainly: a hard kill loses it.
Retry-After. Python honours it, clamped to 60 seconds. Node, Java and Go explicitly ignore it, each saying so on their page. Swift and Android are unknown — their retry sections describe backoff without addressing the header either way.
Product features
| Capability | Browser | React | Node | Python | Java | Go | Swift | Android |
|---|---|---|---|---|---|---|---|---|
Experiments (variant() + $exposure) | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Standalone feature flags | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
identify / reset / auto device id | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| User properties | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Groups and entities | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Revenue | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Plugin pipeline (three stages) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Experiments are evaluated locally against fetched configuration in every SDK, with an $exposure event on first read. Every page carries a "what this does not do" subsection saying the same thing: there are no standalone feature flags. An experiment is the only construct.
React now has a real package. @tangent/analytics-react wraps the browser client rather than reimplementing it: a provider that owns one client and shuts it down on unmount, hooks for tracking, identity, entities and experiment variants, and automatic page views that work with React Router and Next.js's app router without depending on either. It is correct under React 18 StrictMode — where the doubled initial render would otherwise leave an orphaned client holding a flush timer — and inert during server-side rendering. Any remaining ⚠️ for React means the underlying browser capability is itself partial, not that you have to write the hook yourself.
Server identity is per call. In Node, Python, Java and Go, identify() sets a process-wide subject. That is right for a CLI or a worker and wrong inside a request handler, where concurrent requests overwrite each other. Pass the user id per event instead.
Privacy and consent
| Capability | Browser | React | Node | Python | Java | Go | Swift | Android |
|---|---|---|---|---|---|---|---|---|
| Consent gate (blocks delivery) | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| Consent as a recorded signal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Opt-out | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Per-purpose consent filtering | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Do Not Track respected | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Anonymous mode | ✅ | ✅ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ |
Read this row by row before you build a consent banner on top of it.
In Python and Java, consent is a signal, not a gate: setting it records the state and stamps events, but it does not stop delivery. If you need blocking behaviour in those two, do not send the event. Go is the one server SDK with a real gate, and its page says so. Node's gate is three-state: granted, pending, denied.
Do Not Track is a browser signal, read from the browser context and honoured there. No server SDK reads it. Neither mobile SDK does either, for the same reason both pages give: neither Apple platforms nor Android emit one.
Per-purpose filtering — dropping analytics but keeping strictly-necessary events, say — is a separate browser plugin. No other SDK has an equivalent.
Framework integration
| SDK | What ships | Gaps stated on the page |
|---|---|---|
| Browser | Page-view, autocapture, attribution, replay, feedback and consent plugins | Packages not yet published; tag build is the supported path |
| React | react-router v6/v7, Next.js App Router, error boundaries | Package exists but is not published to npm; experiment definitions are polled, not pushed |
| Node | requestTracker (Express/Connect/http), koaRequestTracker | No Fastify, Hapi or NestJS adapter; the Koa adapter has no ignore option |
| Python | WSGI (Flask, Django) and ASGI (FastAPI, Starlette) middleware, Celery/RQ | A documented Flask route-fallback defect |
| Java | RequestTrackingFilter, WebFlux, Jakarta EE / Tomcat, Spring Boot | — |
| Go | net/http middleware, gin | — |
| Swift | SwiftUI, UIKit, Combine, async/await, app extensions and widgets | Network reachability not implemented |
| Android | Compose, Navigation, Coroutines, OkHttp, Hilt — as documented patterns | Not integrated: no Compose helper, no WorkManager upload worker |
Publication status
No SDK is on a public registry yet. This is the single biggest practical constraint on adoption, so it is stated here rather than buried.
| SDK | Registry | Supported path today |
|---|---|---|
| Browser, React | npm — not published | The tag served by the collector |
| Node | npm — not published | file: path dependency on the checkout |
| Python | PyPI — not published | pip install ./sdk/python, or a pinned git URL |
| Java | Maven Central — not published | mvn install to ~/.m2, or your own Nexus/Artifactory |
| Go | Go module proxy — not fetchable | A replace directive, or vendoring |
| Swift | SPI / CocoaPods — not published | A local path dependency (identity is the directory name) |
| Android | Maven Central — not published | includeBuild, or publishToMavenLocal |
One further known defect worth knowing before you pick Node: sdk/node/src/index.js resolves the tracker core from a path that no longer exists after the TypeScript workspace rebuild, so require() throws until that path is fixed. The Node page carries the detail and the fix.
How this page was derived
Each cell was established from the SDK source first — exported symbols, configuration option names, class and method names — and from the SDK's own page second, which by house style must state its gaps explicitly. A cell reads ❔ only where neither settled it. If you find a cell that disagrees with the code, the code wins and the page is the bug.