Full-stack experiments and feature flags
A full-stack campaign lets your own code decide what each visitor sees. Instead of changing the page for you, Personizely assigns the visitor to a variation and returns which features are enabled for it, with their parameters. Your code reads them and renders the experience. Use it for feature flags, gradual rollouts, and A/B tests of anything you build yourself: new checkout steps, pricing displays, recommendation logic, app or headless storefront features.
Available on: all platforms, through the browser JavaScript API. A server-side SDK is in Preview (see Server-side SDK). Plan: full-stack campaigns must be included in your plan. Available as: A/B test · Personalization
Test or personalize. You can run a full-stack campaign as an A/B test, where visitors are split at random and you measure which version wins, or as a personalization, where each variation is shown to a specific audience, with an optional control group to measure the lift. The setup is the same; Personalizations covers what's different.
How it works
- You define features once per website. A feature has a name, an ID your code uses, and optional typed parameters with default values (for example buttonColor, a string, default "green").
- Each variation enables features and sets their parameters. The control enables nothing. A variation can enable several features at once.
- Your code runs the campaign. Full-stack campaigns never run on their own. Your code calls ply.runCampaign(campaignId), which checks the target and traffic allocation, assigns the visitor (or reuses their earlier assignment), and returns the variation.
- Your code reads the features. variation.getFeature(id) returns the feature, feature.isEnabled() tells you whether it's on, and feature.getParameterValue(key) returns a parameter's value.
- The view is recorded on the first call per page load. Conversions are then attributed to the variation like in any other campaign.
Set up
1. Create features
Go to Settings → Features → Create feature.
Field | What it does | Values / default |
|---|---|---|
Name | Label in the app | Required, must be unique |
ID | The key your code passes to getFeature() | Generated from the name in camelCase (Button color → buttonColor); can't be changed after creation |
Parameters | Values a variation can set | Each has a Key, a type (String, Number or Boolean) and a Default value |
2. Create the campaign
- Go to Campaigns → New campaign → Full stack, and choose Experiment or Personalization.
- For each variation, open its features, click Add feature, switch it on and set the parameter values. Parameters start at their default values.
- Set the target, goals and traffic split, and start the campaign. Note the campaign ID; your code needs it.
3. Call it from your code
function runFullStack (campaignId, callback) {
// The snippet defines window.ply when it loads
if (window.ply && window.ply.runCampaign) {
callback(window.ply.runCampaign(campaignId))
} else {
setTimeout(() => runFullStack(campaignId, callback), 50)
}
}
runFullStack(12345, (variation) => {
// undefined = control, not allocated, or not in the target: show the default experience
if (!variation) return
const feature = variation.getFeature('newCheckoutButton')
if (feature.isEnabled()) {
const color = feature.getParameterValue('buttonColor') // e.g. "red"
renderNewCheckoutButton({ color })
}
})JavaScript API reference
Call | Returns |
|---|---|
ply.runCampaign(campaignId) | The visitor's variation, or undefined when the visitor is in the control, outside the traffic allocation or not in the target. Also undefined if the campaign isn't running. |
variation.getFeature(featureId) | The feature. If the variation doesn't enable that feature, a disabled feature is returned, so you can always call isEnabled() safely. |
feature.isEnabled() | true if the variation enables the feature |
feature.getParameterValue(key) | The parameter's value for this variation, or undefined if it isn't set |
variation.id | The variation's numeric ID |
ply.runCampaign is synchronous. Pass the campaign ID as a number: ply.runCampaign('12345') finds nothing. You can call it as often as you like: the visitor's assignment doesn't change, and only the first call on each page load records a view.
Behavior details and limitations
- Your code decides when the visitor is exposed. A view is only counted when runCampaign is called. Call it at the point the experience would actually differ, not on every page, so the report only includes visitors who could see the change.
- Sticky assignment. The visitor keeps their variation on later pages and visits, in the same browser. See How visitors are assigned.
- Control has no features. A visitor in the control gets undefined, exactly like a visitor who isn't in the test, so your default code path serves both. The control is still counted in the report.
- Targets that need server data. If the campaign's target uses filters checked on Personizely's servers (for example location or order history), call runCampaign after the page has finished loading, so that data is available.
- Single-page apps. When the snippet detects a route change, the next call records a new view, as on a full page load.
- Run it from your code. A widget's Run campaign automation only lists content personalizations, so full-stack campaigns are run with ply.runCampaign (or the server-side SDK).
- No flicker handling. Personizely doesn't hide anything for full-stack campaigns. Render a neutral state until your code has the variation, and the first-activation prompt to switch to synchronous loading doesn't appear for this type.
- No deploy. Full-stack campaigns can't be deployed. To roll out a winner, make the winning behavior your code's default, or keep the campaign running with 100% of traffic in that variation.
- Feature IDs are permanent. Because your code references them, a feature's ID can't be edited. Create a new feature to rename it.
Server-side SDK (Preview)
@personizely/sdk runs full-stack campaigns in Node.js, at the edge or in the browser, deciding locally from your website's campaign data without a network call per decision. It also tracks events and goals. The SDK is in Preview: contact support to have it enabled for your account. See Server-side SDK.
Decide each campaign in one place, either in the browser with ply.runCampaign or on the server with the SDK, so each visitor gets a single, consistent decision.
Running it as a personalization
- One target per variation, first match wins. Each variation has its own target, and ply.runCampaign returns the first variation in the list whose target the visitor matches. A visitor who matches no variation gets undefined.
- Control group. 25% of matched visitors by default are control: ply.runCampaign returns undefined for them too, so your default code path serves them, and they're counted as control in the report.
- Live or Permanent. With Live, targets are checked again on every page, so the variation returned can change, for example after a visitor logs in. Use Permanent to keep the first variation.
- No deploy. Personalizations can't be deployed, so the report has no Deploy. A personalization keeps running until you stop it, and you can edit its variations and targets as needed. To stop measuring and show it to every matching visitor, set the control group to 0%. To roll a feature out to everyone, make it your code's default.
- Example audience: turn a feature flag on for logged-in B2B customers, using the Customer filter on customers tagged b2b.
Examples
- Feature flag rollout: a personalization with one variation enabling newSearch, target "All visitors", traffic allocation 10%. Raise the allocation as confidence grows.
- Free-shipping message test: a feature shippingMessage with a string parameter text, and two variations with different wording, rendered by your headless storefront.
- Recommendation algorithm test: a feature recsAlgorithm with a string parameter model set to "bestsellers" in one variation and "similar" in another, measured on revenue per visitor.
Troubleshooting
- runCampaign always returns undefined → the campaign isn't running, the visitor is outside the target or allocation, or they're in the control. Open the page with ?ply_debug=1 to see why in the console.
- ply is undefined → the snippet hasn't loaded yet when your code runs. Wait for window.ply, as in the example above.
- getParameter is not a function → the method is getParameterValue.
- The report shows no views → runCampaign is never called on pages visitors reach, or it's called before the snippet loads.