API Reference
API Reference

Subotiz A/B Test SDK

Subotiz A/B testing front-end SDK. Fetches experiment assignment data in the browser and supports on-demand conversion event reporting.

  • Browser-first, framework-agnostic (works with React / Vue / plain HTML pages)
  • First-class TypeScript support with complete typings

Loading via CDN

The SDK can be included via CDN.

CDN call syntax: abSubotiz(methodName, ...args)

<!-- 1. Synchronously define the global proxy and method queue -->
<script>
  window.abSubotizQueue = window.abSubotizQueue || [];
  window.abSubotiz = window.abSubotiz || function () {
    window.abSubotizQueue.push([].slice.call(arguments));
  };

  // 2. Start queueing calls immediately
  abSubotiz('init', {
    storeId: 'your_store_id',
  });
  abSubotiz('onReady', function (experiments) {
    var hero = experiments['homepage_hero_test'];
    if (hero && hero.variantKey === 'variant_b') {
      document.documentElement.dataset.heroVariant = 'b';
    }
  });
</script>

<!-- 3. Asynchronously load the real SDK; the queue is replayed automatically once loaded -->
<script async src="https://cdn.subotiz.com/ab-sdk-subotiz/v1/ab-sdk-subotiz.min.js"></script>

Reporting events

Business events are reported via track(eventName, properties?), commonly used for experiment conversion analysis (signup completion, button clicks, your own revenue / LTV, and other metrics the platform cannot collect automatically):

abSubotiz.track('signup_complete');                                  // count / unique-user metrics
abSubotiz.track('ltv', { value: 99.9 });                             // numeric metrics (sum / average)

Characteristics:

  • Network failure / 5xx → queued to the localStorage offline queue and automatically resent when back online
  • 4xx → silently dropped (the event itself is invalid)
  • Called before init completes → silently skipped

Do not pass user-private data (phone number, email, ID number, address, etc.) in event properties. The SDK does not filter anything; the caller is responsible for this.

Numeric metrics: properties.value

For metrics that require sum / average aggregation (e.g. revenue, LTV, time on page), put the number in properties.value:

abSubotiz.track('ltv', { value: 99.9 });
  • value must be a finite number (typeof === 'number' and not NaN / Infinity) to be included in numeric aggregation by the backend.
  • An invalid value (string '99.9', NaN, null, etc.) will not pollute numeric statistics; it is passed through as an ordinary tag and treated by the backend with "non-numeric" fallback semantics.
  • Count / unique-user metrics (e.g. signup_complete) do not need a value; just call track('signup_complete').

Debugging: metric whitelist hints

The metric keys declared for experiments in the admin console are delivered together with the assignment result. When debug: true, the SDK uses them for spell-check hints: if the eventName passed to track is not declared in any running experiment's metrics, the console emits a warn (suggesting a possible typo / case mismatch, and printing the current whitelist).

This is only a debugging hint — the event is still reported normally. In production mode (debug: false) there is zero overhead and no impact on reporting behavior.

Exposure events

When an experiment is first hit, the SDK automatically reports an exposure event — no manual call is required. Experiments already present in the sticky cache are not reported again.


Automatic pricing-link injection

When an experiment object type is price_list, the SDK appends the following query parameters to the returned pricing-table link. The link the business receives is already fully injected:

ParameterValue
ab_sbt_sidCurrent store id
ab_sbt_uidCurrent user id
ab_sbt_utype'user' or 'anonymous'
ab_sbt_schSource channel (appended only when a valid sourceChannel is passed to init; see below)
 // → https://checkout.subotiz.com/pricing-v2?ab_sbt_sid=store_123&ab_sbt_uid=user_456&ab_sbt_utype=user
const exp = experiments['pricing_redirect_test'];
if (exp && exp.objectType === 'price_list') {
  window.location.href = exp.config!.link as string;
}

Source-channel targeting (sourceChannel)

You can pass sourceChannel to init to let the backend perform experiment targeting by traffic source (e.g. "enable an experiment only for traffic coming from facebook").

abSubotiz.init({
  storeId: 'your_store_id',
  sourceChannel: 'facebook',
});

Value and normalization rules

The SDK performs no enum validation and no case conversion. The set of allowed values (e.g. facebook / google / tiktok) is agreed upon between you and your product team. Before sending, only the following processing is applied:

Input valueResult
Normal stringCarried after trim (original casing preserved)
undefined / null / non-stringTreated as not provided; the source_channel field is omitted (not an empty placeholder)
Empty string / whitespace onlyTreated as not provided; omitted
Length ≥ 50 after trimTreated as not provided; omitted (debug: true emits a console warn for over-length)

Stability: sourceChannel is treated as a stable value within a single session and is determined only once at init. The SDK does not watch for changes afterward, nor does it re-fetch assignments when the channel changes.

Omitting beats truncating: over-length / invalid values are always omitted, to avoid the backend receiving a silently-rewritten value that diverges from your analytics configuration.


API reference

init(config)

Initializes the SDK. Can only be called once per SDK instance; repeated calls are ignored. Returns immediately and does not block the main thread.

  • Parameter config: SubotizSDKConfig
  • Returns void

Configuration options

init(config) accepts a SubotizSDKConfig object:

FieldTypeDefaultDescription
storeIdstringRequired. Store ID (the Subotiz admin store access no)
sourceChannelstringOptional. Source channel, used for backend traffic targeting.
timeoutnumber30000Assignment request timeout in milliseconds
debugbooleanfalseEnables debug logging and allows forceVariant to take effect. Keep disabled in production.

onReady(onSuccess, onError?)

Registers a ready callback. Triggered synchronously if the SDK is already ready, asynchronously otherwise. Multiple callbacks can be registered.

abSubotiz.onReady(
  (experiments) => { /* experiment data is ready */ },
  (error) => { /* network error and no cache */ },
);
  • Parameters
    • onSuccess: (experiments: SubotizExperimentMap) => void
    • onError?: (error: ABTestError) => void
  • Returns void

waitReady()

The Promise version of onReady.

const experiments = await abSubotiz.waitReady();
  • Returns Promise<SubotizExperimentMap>
  • On failure: throws ABTestError (equivalent to onReady's onError)

isReady()

Synchronously checks whether the SDK is ready.

  • Returns boolean

getExperiments()

Synchronously gets all currently-hit experiments.

  • Returns SubotizExperimentMap | null
    • null — not initialized, not ready, or there are currently no hit experiments
    • Record<string, SubotizExperiment> — experiment map keyed by experimentId

track(eventName, properties?)

Reports a custom business event. Any event is reported; whether it counts toward an experiment is attributed by the backend.

abSubotiz.track('signup_complete');      // count / unique users
abSubotiz.track('ltv', { value: 99.9 }); // sum / average; value must be a finite number
  • Parameters
    • eventName: string — event name
    • properties?: Record<string, unknown> — additional event properties. Among them, value (a finite number) is used for sum / average metrics; all other fields are passed through as additional tags
  • Returns void (never throws)
  • Behavior
    • Always reports; no client-side whitelist dropping
    • debug: true and eventName not in the metric whitelist declared in the admin console → console warn spell-check hint (hint only; does not affect reporting)
    • Called before init completes → silently skipped

forceVariant(experimentId, variantKey)

Only effective when debug: true. Locally forces a given experiment's variant, convenient for verifying different variant UIs during development.

abSubotiz.forceVariant('homepage_hero_test', 'variant_b');
  • Parameters experimentId: string, variantKey: string
  • Returns void

Error handling

onReady(_, onError) and waitReady() return an ABTestError on failure:

interface ABTestError {
  code: ABTestErrorCode;
  message: string;
  cause?: unknown;
}

Error codes:

CodeMeaning
NETWORK_TIMEOUTRequest timed out (weak network)
NETWORK_ERRORNetwork request failed (offline, blocked by CORS)
HTTP_ERRORBackend returned a non-2xx status
INVALID_RESPONSEMalformed response format
NOT_INITIALIZEDinit validation failed (e.g. missing storeId)
abSubotiz.onReady(
  (experiments) => render(experiments),
  (error) => {
    if (error.code === "NOT_INITIALIZED") {
      console.error('invalid init config', error.message);
    }
   // do something
  },
);

Fallback strategy

The SDK has built-in fallback behavior; the business only needs to care about three things:

  1. Render the control group firstawait waitReady() can wait up to timeout seconds on a network error, so do not use it to block the first paint
  2. Switch variants inside onReady / waitReady — minimizes visual disruption for users
  3. Keep the control group on onError — never show blank or broken UI
ScenarioSDK behavior
Repeat visit + network errorAutomatically becomes ready using the localStorage cache
First visit + network errorTriggers onError
track with 5xx / offlineQueued offline and resent automatically on window.online
track with 4xxSilently dropped


Debugging

With debug: true enabled, the SDK logs requests and queue status to the console:

abSubotiz.init({ storeId: 'store_123', debug: true });
LogMeaning
[ab-sdk] reportExposure: sending N event(s)Reporting exposures
[ab-sdk] track: sending N event(s)Reporting custom events
[ab-sdk] track event queued (offline)Event added to the offline queue
[ab-sdk] 4xx error, track event dropped4xx silently dropped
[ab-sdk] MetricKeyRegistry rebuilt: N keysMetric whitelist rebuilt after a successful assignment (with key preview)
[ab-sdk] track('xxx') did not match the metric_keys ...track event name did not match the whitelist (still reported)
[ab-sdk-subotiz] sourceChannel length N >= 50, omitted ...sourceChannel over-length, omitted
[ab-sdk] Forced variant: xxx → yyyforceVariant called

Resetting SDK state

Object.keys(localStorage)
  .filter((k) => k.startsWith('_ab_sdk_') || k.startsWith('ab_event_queue_'))
  .forEach((k) => localStorage.removeItem(k));

Local storage keys

The SDK uses the following keys in localStorage:

KeyContent
_ab_sdk_uuidAnonymous user UUID (shared across tenants)
_ab_sdk_exp_{storeId}_USERIDExperiment assignment snapshot (60-day TTL)
ab_event_queue_{storeId}_USERID_trackOffline event-reporting queue
ab_event_queue_{storeId}_USERID_exposureOffline exposure-event queue

FAQ

The business code already rendered before the SDK loaded — won't it miss the experiment data?

Yes. Let the business code render the control group first, then switch to the variant inside onReady / waitReady. The sticky cache guarantees that users see their variant consistently after refresh (the first visit has a momentary "control → variant" switch).

I called track but the backend did not receive it — how do I troubleshoot?

Check in order:

  1. Is abSubotiz.isReady() true? track is silently skipped when not ready.
  2. Enable debug: true and check the console for a track: sending log.
  3. Check the browser network panel for a POST /sdk/v1/ab/events request.
  4. Is the response 4xx or 5xx? 4xx is dropped; 5xx is queued for resend.
  5. Check whether localStorage.ab_event_queue_*_track has events piling up.

I passed sourceChannel, but experiment targeting didn't take effect

Check in order:

  1. Enable debug: true and verify the assignment request body's attributes.source_channel carries your value.
  2. Confirm the value is valid: non-empty after trim and length < 50. Over-length values are omitted (with a console warn).
  3. Confirm the value matches the admin targeting rule exactly — the SDK does not convert case, so Facebookfacebook.
  4. Targeting-rule matching happens on the backend; use getExperiments() to check whether the experiment is returned.

My track value didn't make it into the sum / average statistics

value must be a finite number (typeof === 'number' and not NaN / Infinity) to be included in numeric aggregation. A common pitfall is passing a string: track('ltv', { value: '99.9' }) (a string) is treated as an ordinary tag and not included in sum / average. Use { value: 99.9 } instead.

The same experiment shows different variants before and after refresh

The sticky cache guarantees variant stability for 60 days. Inconsistency is usually caused by:

  • localStorage being cleared (private mode / the user clearing it manually)
  • Forced user assignment being enabled in the admin console, which overrides the sticky cache

An experiment isn't working in production — how do I troubleshoot?

Check in order:

  1. Is storeId correct? (A wrong storeId does not error; you simply get no experiments.)
  2. Is the experiment in the "Running" state in the admin console? (Paused / draft states are not delivered.)
  3. Does the user match the experiment's targeting rules? (Device / country are parsed by the backend from the HTTP request headers.)
  4. Use getExperiments() to inspect the result and confirm whether the variant decision itself is the control group.

I want a specific user to be completely excluded from experiments

The SDK does not provide an "opt-out" API. Workaround: have your business code decide (e.g. by login state / role) and, when the condition is met, skip reading experiments and render the default directly.

Why does getExperiments() return null instead of an empty object?

null means "not ready" or "no hit experiments", making existence checks easy:

const all = ab.getExperiments();
if (!all) return renderDefault();

This avoids writing Object.keys(all).length === 0.

Can it be used in React Server Components / Edge Runtime?

No. The SDK depends on browser APIs such as localStorage / document.cookie / window.


Browser compatibility

Relies on native fetch / Promise / localStorage. Supports:

  • Chrome / Edge ≥ latest 2 major versions
  • Safari ≥ latest 2 major versions
  • Firefox ≥ latest 2 major versions

IE is not supported. The SDK does not bundle any compatibility shims.