- 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)
Reporting events
Business events are reported viatrack(eventName, properties?), commonly used for experiment conversion analysis (signup completion, button clicks, your own revenue / LTV, and other metrics the platform cannot collect automatically):
- Network failure / 5xx → queued to the
localStorageoffline queue and automatically resent when back online - 4xx → silently dropped (the event itself is invalid)
- Called before
initcompletes → silently skipped
Numeric metrics: properties.value
For metrics that require sum / average aggregation (e.g. revenue, LTV, time on page), put the number in properties.value:
valuemust be a finite number (typeof === 'number'and notNaN/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 avalue; just calltrack('signup_complete').
Debugging: metric whitelist hints
The metric keys declared for experiments in the admin console are delivered together with the assignment result. Whendebug: 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 isprice_list, the SDK appends the following query parameters to the returned pricing-table link. The link the business receives is already fully injected:
Source-channel targeting (sourceChannel)
You can passsourceChannel to init to let the backend perform experiment targeting by traffic source (e.g. “enable an experiment only for traffic coming from 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:
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:
onReady(onSuccess, onError?)
Registers a ready callback. Triggered synchronously if the SDK is already ready, asynchronously otherwise. Multiple callbacks can be registered.
- Parameters
onSuccess: (experiments: SubotizExperimentMap) => voidonError?: (error: ABTestError) => void
- Returns
void
waitReady()
The Promise version of onReady.
- Returns
Promise<SubotizExperimentMap> - On failure: throws
ABTestError(equivalent toonReady’sonError)
isReady()
Synchronously checks whether the SDK is ready.
- Returns
boolean
getExperiments()
Synchronously gets all currently-hit experiments.
- Returns
SubotizExperimentMap | nullnull— not initialized, not ready, or there are currently no hit experimentsRecord<string, SubotizExperiment>— experiment map keyed byexperimentId
track(eventName, properties?)
Reports a custom business event. Any event is reported; whether it counts toward an experiment is attributed by the backend.
- Parameters
eventName: string— event nameproperties?: 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: trueandeventNamenot in the metric whitelist declared in the admin console → consolewarnspell-check hint (hint only; does not affect reporting)- Called before
initcompletes → 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.
- Parameters
experimentId: string,variantKey: string - Returns
void
Error handling
onReady(_, onError) and waitReady() return an ABTestError on failure:
Fallback strategy
The SDK has built-in fallback behavior; the business only needs to care about three things:- Render the control group first —
await waitReady()can wait up totimeoutseconds on a network error, so do not use it to block the first paint - Switch variants inside
onReady/waitReady— minimizes visual disruption for users - Keep the control group on
onError— never show blank or broken UI
Debugging
Withdebug: true enabled, the SDK logs requests and queue status to the console:
Resetting SDK state
Local storage keys
The SDK uses the following keys inlocalStorage:
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 insideonReady / 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:
- Is
abSubotiz.isReady()true?trackis silently skipped when not ready. - Enable
debug: trueand check the console for atrack: sendinglog. - Check the browser network panel for a
POST /sdk/v1/ab/eventsrequest. - Is the response 4xx or 5xx? 4xx is dropped; 5xx is queued for resend.
- Check whether
localStorage.ab_event_queue_*_trackhas events piling up.
I passed sourceChannel, but experiment targeting didn’t take effect
Check in order:
- Enable
debug: trueand verify the assignment request body’sattributes.source_channelcarries your value. - Confirm the value is valid: non-empty after
trimand length < 50. Over-length values are omitted (with a consolewarn). - Confirm the value matches the admin targeting rule exactly — the SDK does not convert case, so
Facebook≠facebook. - 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:localStoragebeing 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:- Is
storeIdcorrect? (A wrongstoreIddoes not error; you simply get no experiments.) - Is the experiment in the “Running” state in the admin console? (Paused / draft states are not delivered.)
- Does the user match the experiment’s targeting rules? (Device / country are parsed by the backend from the HTTP request headers.)
- 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 readingexperiments 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:
Object.keys(all).length === 0.
Can it be used in React Server Components / Edge Runtime?
No. The SDK depends on browser APIs such aslocalStorage / document.cookie / window.
Browser compatibility
Relies on nativefetch / Promise / localStorage. Supports:
- Chrome / Edge ≥ latest 2 major versions
- Safari ≥ latest 2 major versions
- Firefox ≥ latest 2 major versions