Send exposures to your analytics tool
Controlvar records an exposure each time it enrols a user in an experiment. The JavaScript, Python, Go, React, React Native, Java and .NET SDKs can also call a function of yours at that moment, the exposure hook, so you can send the exposure to Segment, Google Analytics 4, Amplitude, Mixpanel, RudderStack or your own warehouse.
Which SDKs have it
| SDK | Set it with | Details |
|---|---|---|
| JavaScript | onExposure in new ExperimentationClient({ ... }) |
Exposure hook |
| Python | on_exposure= in ExperimentationClient(...) |
Exposure hook |
| Go | exp.WithOnExposure(fn) in exp.New(...) |
Exposure hook |
| React | onExposure in the config of ExperimentationProvider, and of ServerClient |
Exposure hook |
| React Native | onExposure in new ExperimentationClient({ ... }) |
Exposure hook |
| Java | exposureListener(...) on SdkConfig.builder(...), or an ExposureListener bean with the Spring Boot starter |
Exposure hook |
| .NET | OnExposure on SdkConfig |
Exposure hook |
Each SDK's page describes its hook in full, and says whether that SDK is published yet.
When it is called
The server records one exposure for each assignment request it answers with assigned: true. The
SDK calls the hook at most once for each such answer, when it stores the answer for a user and
experiment that had no unexpired cached answer in that client. Within one cache lifetime (5
minutes by default), one client therefore calls it once per user and experiment, however often the
answer is asked for.
It is not called for:
- a cached answer;
- the second of two calls for the same new user and experiment made at the same time;
- a call that fails;
- an answer with
assigned: false: a user in the global holdout, in another experiment of the same mutual exclusion group or outside the experiment's targeting, or an experiment that is not running, for which the server records no exposure; - a feature flag.
React's server render makes no request and calls nothing; hydration in the browser then makes the
one request and the one call. React's ServerClient.bootstrap() reads and fills no cache, so a
ServerClient shared across requests calls its own hook on every bootstrap() call, once for each
experiment the server enrols the user in (What the bootstrap records).
It is called again, and the server records another exposure, whenever the SDK asks the server
again: after the cache lifetime, after clearCache() (Java's invalidateCache too; Go has
neither), after the answer is evicted from a full cache, and in every new client, such as each page
load in a browser or each process or worker on a server; except that React Native, with its
default offlineFallback, keeps the answer in AsyncStorage for the cache lifetime, so on one
device neither clearCache() nor a new client calls it again until that stored answer expires,
unless you set offlineFallback: false or call clearStorage().
So your tool can receive several exposures for one user and experiment: count users, not events. It never receives an exposure Controlvar did not record. It can receive fewer: two calls for the same new user and experiment at the same time in Python, Go, Java or .NET each record one and call the hook once, and a request that times out after the server recorded it calls nothing.
It is called when your code asks for the assignment, whether or not you then show the variant, so ask for it where the variant is shown.
Keep it quick
It runs inside the call that asked for the assignment, after the answer is cached and before the call returns; in JavaScript, React and React Native, before that call's Promise resolves. Whatever your tool, a call that sends before it returns adds that request to the assignment, once per user and experiment per cache lifetime; put the exposure on your own queue, as the Python, Go, Java and .NET pages show.
If it throws (in Go, panics), the answer is unchanged, in every SDK.
Examples
Each example is a JavaScript function for the browser. It receives the Exposure of the
JavaScript, React and React Native SDKs: experimentKey, userId, variantId (null when the
server sends none) and variantName.
Pass it as onExposure when you create the client: in the options of ExperimentationClient in
the JavaScript and React Native SDKs, or in the config of React's ExperimentationProvider. Each
example assumes the tool's library is already loaded and set up with your account, as the tool's
own documentation describes.
The event and property names are examples: use the ones your team reports on. If your tool identifies people, identify each one with the user id you give Controlvar, so you can join the exposures to their other events.
Segment
function onExposure(exposure) {
analytics.track('Experiment Viewed', {
experiment_key: exposure.experimentKey,
variant_name: exposure.variantName,
variant_id: exposure.variantId,
});
}
Google Analytics 4
function onExposure(exposure) {
gtag('event', 'experiment_exposure', {
experiment_key: exposure.experimentKey,
variant_name: exposure.variantName,
});
}
Amplitude
function onExposure(exposure) {
amplitude.track('Experiment Viewed', {
experiment_key: exposure.experimentKey,
variant_name: exposure.variantName,
variant_id: exposure.variantId,
});
}
Mixpanel
function onExposure(exposure) {
mixpanel.track('Experiment Viewed', {
experiment_key: exposure.experimentKey,
variant_name: exposure.variantName,
variant_id: exposure.variantId,
});
}
RudderStack
function onExposure(exposure) {
rudderanalytics.track('Experiment Viewed', {
experiment_key: exposure.experimentKey,
variant_name: exposure.variantName,
variant_id: exposure.variantId,
});
}
When nothing arrives
- Is the experiment running? A draft, paused, completed or archived experiment enrols nobody.
- Is the user enrolled? A user in the global holdout, in another experiment of the same mutual exclusion group, or outside the experiment's targeting is not.
- Has this client already reported it? A cached answer, for 5 minutes by default, is not reported
again. In React Native, with the default
offlineFallback, neitherclearCache()nor a new client reports it again until the answer stored in AsyncStorage expires. - Is your function failing? In React, Go, Java and .NET nothing reports it: catch and log inside
it. In JavaScript and React Native, look at
onError, with the operation'exposure'; in Python, aton_errorand thecontrolvar.clientlogger. - In a test, does your faked
/assignanswer include"assigned": true? Without it, the hook is not called. - Is your SDK new enough? Use the JavaScript or Python SDK 0.3.0 or later, or the Go SDK v0.1.0 or later; for React, React Native, Java and .NET, see that SDK's page.