Visitor Context & Properties

Create visitor contexts, manage properties, segments, and entity lookup

A Context ties all SDK actions to a specific visitor. A unique visitor ID is required for deterministic bucketing -- as long as the ID stays the same, bucketing is consistent. On new experiments, ramping total traffic up or down keeps everyone in place too (see Bucketing Algorithm); a persistent DataStore preserves assignments across the config changes that do move visitors, such as re-weighting variations.

Visitor ID Format

A visitor ID must be a non-empty string that uses only these characters:

  • letters a-z and A-Z
  • digits 0-9
  • dot ., hyphen - and underscore _

As a regular expression, the rule is ^[0-9a-zA-Z._-]+$.

Keep the ID to 64 characters or fewer. One longer ID can stop reports from using visitor-level revenue statistics for its whole variation. An ID over 256 characters may not be tracked at all.

The Convert tracking server enforces this rule. It drops every event for a visitor whose ID breaks it. It still answers 200 OK, so the request looks successful. The events never reach your reports or Live Logs. In a request that carries several visitors, only the visitor with the invalid ID is dropped.

Bucketing still works on an invalid ID, so the variation renders as normal. Only the tracking is lost, which makes the problem easy to miss.

IDs from identity providers and user tables break the rule most often. An Auth0 user ID such as auth0|64f1c2… contains |, and an email address contains @. URL-encoding does not help, because % is not allowed either. Pass the SHA-256 hex digest of such an ID instead, which is exactly 64 characters. Do not use a longer digest such as SHA-512. A hex string uses only allowed characters, and the same input always gives the same ID.

Creating a User Context

import type {ContextInterface} from '@convertcom/js-sdk';

const userContext = convertSDK.createContext(
  'user-unique-id',
  {
    country: 'US',
    language: 'en'
  }
);
use ConvertSdk\Interfaces\ContextInterface;

$context = $sdk->createContext(
    'visitor-unique-id',
    [
        'country' => 'US',
        'language' => 'en',
    ]
);
val context = sdk.createContext(
    visitorId = "user-unique-id",
    attributes = mapOf("country" to "US", "language" to "en"),
)
context = CONVERT_SDK.create_context(
  "user-unique-id",
  { country: "US", language: "en" }
)
// Anonymous visitor → auto-generated UUID persisted in the Keychain.
let anon = sdk.createContext()

// Explicit visitor ID with initial attributes (scalars only):
let context = sdk.createContext(
    visitorId: "user-unique-id",
    attributes: ["country": "US", "language": "en"]
)
# visitor_attributes are copied defensively — later caller mutations don't leak in:
context = core.create_context(
    "user-unique-id",
    visitor_attributes={"country": "US", "language": "en"},
)

The first argument is the visitor's unique ID. The second is an optional object of initial visitor properties used for audience and segment evaluation.

Bucketing Attributes

Every context method that runs experiences or features accepts an optional attributes object. This table lists all supported properties:

PropertyTypeDescription
locationPropertiesobject / arrayKey-value pairs used for evaluating experience locations
visitorPropertiesobject / arrayKey-value pairs used for evaluating experience audiences (overwrites same keys from context creation)
updateVisitorPropertiesbooleanWhether to permanently update in-memory visitor properties
enableTrackingbooleanWhether to track bucketing events immediately (default: true)
environmentstringOverride environment for this call
typeCastingbooleanAuto-convert feature variable values to the variable's defined type (default: true)
experienceKeysstring[]Limit feature evaluation to specific experiences only
ignoreLocationPropertiesbooleanSkip location matching for this call -- no location properties need be supplied and none are consulted (default: false); audience and environment gating are unaffected; only the boolean true bypasses -- any other value is ignored
forceVariationIdstringForce a specific variation instead of bucketing the visitor -- NOT preview: environment, location, and audience gating still apply. Preview bypasses every gate and resolves before any bucketing; this steers selection inside normal gating

forceVariationId is scoped to the experience it names: it binds only if the id resolves to a variation on that experience, and is otherwise ignored so normal selection proceeds -- so on a run-all call, a single id binds on at most one experience and is inert on the rest. It also overrides a conflicting stored decision: a previously persisted variation does not win over an explicit force. See QA & Preview for preview, the mechanism this contrasts against.

const variation = userContext.runExperience('experience-key', {
  locationProperties: { url: '/pricing' },
  visitorProperties: { plan: 'pro' },
  updateVisitorProperties: true,
  enableTracking: true
});
use OpenAPI\Client\BucketingAttributes;

$variation = $context->runExperience('experience-key', new BucketingAttributes([
    'locationProperties' => ['url' => '/pricing'],
    'visitorProperties' => ['plan' => 'pro'],
    'updateVisitorProperties' => true,
    'enableTracking' => true,
]));
// Targeting input is set on the context, then the experience is run:
ctx.setLocationProperties(mapOf("url" to "/pricing"))
ctx.setAttributes(mapOf("plan" to "pro"))
val variation = ctx.runExperience("experience-key")
# Visitor properties are flat top-level keys; location_properties, environment,
# and enable_tracking are the reserved keys here — experience_keys and type_casting
# are reserved too, but honoured only on run_feature/run_features. (There is no
# inline updateVisitorProperties flag — use update_visitor_properties below to persist.)
variation = context.run_experience("experience-key", {
  plan: "pro",
  location_properties: { url: "/pricing" },
  enable_tracking: true
})
// iOS has no per-call attributes argument (runExperience takes only `enableTracking`),
// and attributes are immutable for a context's lifetime. Set them once at creation:
let context = sdk.createContext(visitorId: "user-unique-id", attributes: ["plan": "pro"])
let variation = await context.runExperience("experience-key")
# Per-call overlays are keyword-only and ephemeral. Visitor audiences via
# attributes=, location rules via location_attributes=. enable_tracking (default
# True) is also keyword-only — set it False to bucket without reporting the
# bucketing event. There is no inline updateVisitorProperties flag — use
# set_attributes() to persist visitor properties:
variation = context.run_experience(
    "experience-key",
    attributes={"plan": "pro"},
    location_attributes={"url": "/pricing"},
    enable_tracking=True,
)

The Android SDK has no per-call attributes object. Targeting input -- visitor and location properties -- is context state, set via setAttributes / setLocationProperties, and environment is a builder option rather than a per-call override. enableTracking is a Kotlin parameter on all four entry points, experience and feature alike -- see Running Experiences and Running Features. experienceKeys is feature-scoped: it is a Kotlin parameter on the feature entry points only, described in Running Features.

The iOS SDK has no per-call attributes object: visitor and location properties are set once at createContext and are immutable for the context's lifetime, with no setAttributes / setLocationProperties equivalent to update them afterward. enableTracking is a native Swift parameter on all four entry points, experience and feature alike -- see Running Experiences and Running Features. experienceKeys is feature-scoped: it is a Swift parameter on the feature entry points only, described in Running Features. This SDK has no typeCasting parameter at all.

Visitor Properties

Visitor properties are key-value pairs used in audience evaluation and segment matching. They can be set at context creation, updated later, or passed inline with any experience/feature call.

Update Visitor Properties

Permanently merges new properties into the visitor's existing properties.

userContext.updateVisitorProperties({
  weather: 'rainy',
  plan: 'enterprise'
});
// Set a single attribute
$context->setAttribute('weather', 'rainy');

// Set multiple attributes (merges with existing)
$context->setAttributes([
    'weather' => 'rainy',
    'plan' => 'enterprise',
]);

// Update via the data store (persists across requests when using a persistent cache)
$context->updateVisitorProperties('visitor-unique-id', [
    'weather' => 'rainy',
    'plan' => 'enterprise',
]);
// setAttributes replaces the visitor's attributes; combine maps to merge.
ctx.setAttributes(mapOf("weather" to "rainy", "plan" to "enterprise"))
# Merges into the visitor's stored properties (persists via the configured store)
# and the in-memory context attributes.
context.update_visitor_properties(
  weather: "rainy",
  plan: "enterprise"
)

Inline Update

Visitor properties can also be updated inline when calling any experience or feature method by setting updateVisitorProperties to true in the attributes:

const variation = userContext.runExperience('experience-key', {
  visitorProperties: { weather: 'rainy' },
  updateVisitorProperties: true
});
use OpenAPI\Client\BucketingAttributes;

$variation = $context->runExperience('experience-key', new BucketingAttributes([
    'visitorProperties' => ['weather' => 'rainy'],
    'updateVisitorProperties' => true,
]));

The Ruby SDK has no inline updateVisitorProperties flag — pass per-call visitor properties for matching only, and call update_visitor_properties (above) when you want them persisted.

The iOS SDK has no mutable visitor-property API — attributes are fixed when the context is created (immutable for its lifetime). To change them, create a new context with the new attributes.

Custom Segments

Custom segments represent audiences of type segmentation. Use runCustomSegments to evaluate segment rules against the current visitor context.

userContext.runCustomSegments(['segment-key-1', 'segment-key-2'], {
  enabled: true
});
$context->runCustomSegments(['segment-key-1', 'segment-key-2'], [
    'ruleData' => ['enabled' => true],
]);
context.run_custom_segments(["segment-key-1", "segment-key-2"], {
  ruleData: { enabled: true }
})

Parameters:

ParameterTypeRequiredDescription
segmentKeysstring[]YesList of segment keys to evaluate
attributesobject / arrayNoKey-value pairs used for segment matching (PHP wraps these under a ruleData key)

In Fullstack projects, segments do not persist unless you provide a DataStore. See the segments concept for more detail on how segmentation works.

Default Segments

Default segments are used for Convert reporting. Use setDefaultSegments to permanently update the visitor's default segments. Only the following properties are included in Convert Reports:

  • browser
  • devices
  • source
  • campaign
  • visitorType
  • country
userContext.setDefaultSegments({
  country: 'US',
  browser: 'chrome',
  devices: 'desktop'
});
$context->setDefaultSegments([
    'country' => 'US',
    'browser' => 'chrome',
    'devices' => 'desktop',
]);
ctx.setDefaultSegments(mapOf(
    "country" to "US",
    "browser" to "chrome",
    "devices" to "desktop",
))
context.set_default_segments(
  country: "US",
  browser: "chrome",
  devices: "desktop"
)
// Six recognised keys: browser, devices, source, campaign, visitorType, country.
// Each call merges over existing segments; unknown keys are WARN-logged and ignored.
await context.setDefaultSegments([
    "country": "US",
    "browser": "chrome",
    "devices": "desktop",
])
# The method is set_segments(); each call shallow-merges into the stored default
# segments, kept strictly separate from visitor attributes:
context.set_segments({
    "country": "US",
    "browser": "chrome",
    "devices": "desktop",
})

Parameters:

ParameterTypeRequiredDescription
segmentsobject / arrayYesKey-value pairs merged with the initial visitor properties

Config Entity Lookup

You can look up any entity in the project configuration by its key or numeric ID.

Get Entity by Key

import ConvertSDK, {EntityType} from '@convertcom/js-sdk';
import type {Experience} from '@convertcom/js-sdk';

const experience = userContext.getConfigEntity(
  'experience-key',
  EntityType.EXPERIENCE
);
use ConvertSdk\Enums\EntityType;

$experience = $context->getConfigEntity('experience-key', EntityType::Experience->value);
# entity_type accepts :experience, :feature, or :goal (symbol or string).
experience = context.get_config_entity("experience-key", :experience)
# entity_type comes FIRST (opposite of the JS/PHP/Ruby order) and is a plain
# string — one of "experiences", "features", "goals", "audiences", "segments".
# Returns the entity mapping, or None on a normal miss (never raises):
experience = context.get_config_entity("experiences", "experience-key")

Parameters:

ParameterTypeRequiredDescription
keystringYesEntity key
entityTypeEntityType / stringYesOne of: AUDIENCE, LOCATION, SEGMENT, FEATURE, GOAL, EXPERIENCE, VARIATION

Get Entity by ID

import ConvertSDK, {EntityType, VariationChangeType} from '@convertcom/js-sdk';
import type {BucketedVariation, Feature, VariationChange} from '@convertcom/js-sdk';

const variation = userContext.runExperience('experience-key');
const changesData = variation.changes.find(
  ({type}) => type === VariationChangeType.FULLSTACK_FEATURE
);
const feature = userContext.getConfigEntityById(
  changesData.data.feature_id,
  EntityType.FEATURE
);
use ConvertSdk\Enums\EntityType;

$variation = $context->runExperience('experience-key');
if ($variation !== null) {
    $changes = $variation->changes;
    foreach ($changes as $change) {
        if (isset($change['data']['feature_id'])) {
            $feature = $context->getConfigEntityById(
                (string) $change['data']['feature_id'],
                EntityType::Feature->value
            );
        }
    }
}
# get_config_entity_by_id also takes entity_type first, then the id. Returns the
# entity mapping, or None on a normal miss (never raises):
feature = context.get_config_entity_by_id("features", "feature-id")

Parameters:

ParameterTypeRequiredDescription
idnumber / stringYesEntity numeric ID (PHP passes it as a string)
entityTypeEntityType / stringYesSame values as getConfigEntity above

Both methods return the matching entity object (or array in PHP), or null if not found.

The Ruby SDK resolves config entities by key only (get_config_entity(key, entity_type) with entity_type one of :experience, :feature, :goal); it has no by-ID variant.


Did this page help you?