Skip to content

Run an Experiment

RevTurbine can allocate a native experiment or consume the assignment made by the experimentation SDK already running in your application. For an external assignment, your application owns and initializes the vendor client. A small RevTurbine domain provider maps the vendor’s keys to canonical RevTurbine experiment and variant handles.

The normalized assignment becomes part of an ephemeral effective user context. Segments, entitlements, placements, direct variant reads, and telemetry all use that same resolved assignment. RevTurbine never persists the vendor client or its credentials and never mutates the user object supplied by your app.

  1. In the experiment editor, choose the assignment source: native (RevTurbine assigns) or Customer SDK provider (your vendor SDK assigns).
  2. Native: register the built-in bucketer (next section). Vendor: initialize the vendor SDK as usual and create a RevTurbine adapter with explicit experiment and variant mappings.
  3. Pass the provider in domainProviders when initializing RevTurbine.
  4. Use getExperimentVariant() when application code needs the selected variant directly; RevTurbine decisions and telemetry already use the same assignment.

No vendor SDK needed. Register the built-in bucketer as a domain provider. It assigns deterministically per subject, so the same user lands in the same arm on every load, and everything downstream — targeting, placements, getExperimentVariant(), telemetry — reads that one assignment.

TypeScript
import { initRevTurbine, createBasicExperimentProvider } from '@revturbine/sdk';
const rt = initRevTurbine({
tenantId: 'tenant_abc',
apiKey: 'rtk_…', // publishable ingest key
endpoint: 'https://revturbine.com/app',
mode: 'snippet',
domainProviders: [
createBasicExperimentProvider({
subject: () => currentUserId, // re-read on every resolve
experiments: {
pricing_test: { variants: { control: 1, treatment: 1 } }, // relative weights
onboarding_copy: { variants: ['a', 'b'], exposure: 0.2 }, // 20% enrolled
},
}),
],
});

A subject outside exposure is not enrolled — the experiment is simply absent from their assignment, which is different from being in control. No stable subject, no assignment: bucketing an anonymous id would hand the same person a different arm on the next load.

An allocation authored in the dashboard — an Experiment version with its handle, traffic allocation and weighted variants — drives createNativeExperimentAssignmentProvider instead, which also emits the assignment fact that ties results back to that version. See the API reference for its options.

The GrowthBook adapter accepts an initialized customer-owned client. It is a separate optional entry, so GrowthBook is not included when your application imports only the base SDK.

TypeScript
import { GrowthBook } from '@growthbook/growthbook';
import { initRevTurbine } from '@revturbine/sdk';
import { createGrowthBookExperimentAssignmentProvider } from '@revturbine/sdk/growthbook';
const userId = 'user_123';
const growthbook = new GrowthBook({
attributes: { id: userId },
});
await growthbook.init({
apiHost: 'https://cdn.growthbook.io',
clientKey: 'sdk-...',
});
const experimentProvider = createGrowthBookExperimentAssignmentProvider({
client: growthbook,
bindings: [{
experimentHandle: 'pricing_test',
featureKey: 'pricing-variant',
allowedVariants: ['control', 'treatment'],
}],
});
const rt = initRevTurbine({
tenantId: 'tenant_abc',
publicKey: 'rtk_…', // public key — safe in the browser
user: { id: userId, plan_handle: 'free' },
domainProviders: [experimentProvider],
});

Keep the GrowthBook attributes current using the GrowthBook client. When the user changes in RevTurbine, identify() invalidates the effective context and resolves the provider for the new identity.

The Optimizely adapter also accepts an initialized client and derives the Optimizely user id from the current RevTurbine context by default.

TypeScript
import { createInstance } from '@optimizely/optimizely-sdk';
import { initRevTurbine } from '@revturbine/sdk';
import { createOptimizelyExperimentAssignmentProvider } from '@revturbine/sdk/optimizely';
const optimizely = createInstance({ sdkKey: 'your-optimizely-sdk-key' });
await optimizely.onReady();
const experimentProvider = createOptimizelyExperimentAssignmentProvider({
client: optimizely,
bindings: [{
experimentHandle: 'pricing_test',
experimentKey: 'pricing-experiment',
allowedVariants: ['control', 'treatment'],
}],
attributes: ({ userContext }) => ({
plan: userContext.plan_handle,
}),
});
const rt = initRevTurbine({
tenantId: 'tenant_abc',
publicKey: 'rtk_…', // public key — safe in the browser
user: { id: 'user_123', plan_handle: 'free' },
domainProviders: [experimentProvider],
});

Mappings are explicit: vendor keys do not leak into Playbooks. An allowed variant list prevents a misspelled or newly introduced vendor value from being silently treated as a valid RevTurbine variant.

Use the structured selection when your application must render variant-specific behavior itself:

TypeScript
const selection = await rt.getExperimentVariant('pricing_test');
if (selection.status === 'assigned') {
renderPricingVariant(selection.variantHandle);
} else {
// Missing, unavailable, conflicting, and unsupported assignments are never
// fabricated as "control". Keep the existing safe experience.
renderDefaultPricing();
}

The result distinguishes assigned, not_assigned, unavailable, and unsupported. This lets you keep non-enrollment separate from vendor failure and diagnose ownership or assignment conflicts without guessing.

For diagnostics, the same immutable context used by local evaluation is available from getEffectiveUserContext():

TypeScript
const effectiveContext = await rt.getEffectiveUserContext();
console.log(effectiveContext.experiments?.pricing_test);

An adapter is optional when your code has already normalized the assignment. Use the canonical experiments map, not an arbitrary custom trait:

TypeScript
const rt = initRevTurbine({
tenantId: 'tenant_abc',
publicKey: 'rtk_…', // public key — safe in the browser
user: {
id: 'user_123',
plan_handle: 'free',
experiments: { pricing_test: 'treatment' },
},
});

Direct and provider assignments coalesce when they agree. If they disagree, RevTurbine reports an assignment conflict and does not invent a winner. Two providers also cannot own the same experiment handle.

Create a Segment for the experiment in the RevTurbine dashboard (Targeting Studio) and reference it from an entitlement rule or placement payload. The SDK resolves the provider before evaluating the Playbook, so experiment-backed Segment membership and the resulting placement or entitlement use the effective assignment.

Track the conversion normally:

TypeScript
await rt.track('upgrade_completed', {
plan: 'professional',
source: 'pricing_experiment',
});

The SDK stamps the effective experiment assignment on telemetry from the same resolution used by direct selection and local decisions. It does not call the vendor separately for each consumer.

If an existing integration stores a variant under custom.pricing_experiment, move it to either a client provider binding or experiments.pricing_test. Update the Segment to use the canonical experiment handle, validate the vendor’s variant vocabulary, and then remove the old custom-trait predicate. Do not configure an Experiment Data Connection for client assignment.