Collection of plugins for ABsmartly JavaScript SDK
npm install @absmartly/sdk-pluginsA comprehensive collection of plugins for the ABsmartly JavaScript SDK including DOM manipulation, experiment overrides, cookie management, and web vitals tracking.
waitForElement for SPAs``bash`
npm install @absmartly/sdk-plugins
`javascript
import { DOMChangesPlugin, CookiePlugin, WebVitalsPlugin } from '@absmartly/sdk-plugins';
import absmartly from '@absmartly/javascript-sdk';
// Initialize ABsmartly SDK
const sdk = new absmartly.SDK({
endpoint: 'https://your-endpoint.absmartly.io/v1',
apiKey: 'YOUR_API_KEY',
environment: 'production',
application: 'your-app'
});
// Create context
const context = sdk.createContext(request);
// Initialize plugins
const domPlugin = new DOMChangesPlugin({
context: context,
autoApply: true, // Automatically apply changes on init
spa: true, // Enable SPA support
visibilityTracking: true, // Track when changes become visible
variableName: '__dom_changes', // Variable name for DOM changes
debug: true // Enable debug logging
});
// Initialize without blocking
domPlugin.ready().then(() => {
console.log('DOMChangesPlugin ready');
});
// Initialize cookie management
const cookiePlugin = new CookiePlugin({
context: context,
cookieDomain: '.yourdomain.com',
autoUpdateExpiry: true
});
cookiePlugin.ready().then(() => {
console.log('CookiePlugin ready');
});
// Initialize web vitals tracking
const vitalsPlugin = new WebVitalsPlugin({
context: context,
trackWebVitals: true,
trackPageMetrics: true
});
vitalsPlugin.ready().then(() => {
console.log('WebVitalsPlugin ready');
});
`
The OverridesPlugin enables experiment overrides for internal testing and development. Simply load it before SDK initialization and it will automatically check for and apply any overrides:
`javascript
import {
DOMChangesPlugin,
OverridesPlugin
} from '@absmartly/dom-changes-plugin';
import absmartly from '@absmartly/javascript-sdk';
// Initialize SDK and create context
const sdk = new absmartly.SDK({ / ... / });
const context = sdk.createContext({ / ... / });
// Initialize OverridesPlugin - it will automatically check for overrides
const overridesPlugin = new OverridesPlugin({
context: context,
cookieName: 'absmartly_overrides',
useQueryString: true,
queryPrefix: '_exp_',
envParam: 'env',
persistQueryToCookie: true, // Save query overrides to cookie
sdkEndpoint: 'https://your-endpoint.absmartly.io',
debug: true
});
overridesPlugin.ready().then(() => {
console.log('OverridesPlugin ready - overrides applied if present');
});
// Initialize DOMChangesPlugin for all experiments
const domPlugin = new DOMChangesPlugin({
context: context,
autoApply: true,
variableName: '__dom_changes',
debug: true
});
domPlugin.ready().then(() => {
console.log('DOMChangesPlugin ready');
});
`
#### Override Configuration Options
`javascript
const overridesPlugin = new OverridesPlugin({
context: context, // Required: ABsmartly context
// Cookie configuration
cookieName: 'absmartly_overrides', // Cookie name (omit to disable cookies)
cookieOptions: {
path: '/',
secure: true,
sameSite: 'Lax'
},
// Query string configuration
useQueryString: true, // Enable query string parsing (default: true on client)
queryPrefix: '_exp_', // Prefix for experiment params (default: '_exp_')
envParam: 'env', // Environment parameter name (default: 'env')
persistQueryToCookie: false, // Save query overrides to cookie (default: false)
// Endpoints
sdkEndpoint: 'https://...', // SDK endpoint (required if not in context)
absmartlyEndpoint: 'https://...', // API endpoint for fetching experiments
// Server-side configuration
url: req.url, // URL for server-side (Node.js)
cookieAdapter: customAdapter, // Custom cookie adapter for server-side
debug: true // Enable debug logging
});
`
#### Query String Format (New)
Use individual query parameters with configurable prefix:
`Single experiment
https://example.com?_exp_button_color=1
#### Cookie Format
Cookies use comma as separator (no encoding needed):
`javascript
// Simple overrides (comma-separated experiments)
document.cookie = 'absmartly_overrides=exp1:1,exp2:0';// With environment flags (dot-separated values)
document.cookie = 'absmartly_overrides=exp1:1.0,exp2:0.1';
// With experiment ID
document.cookie = 'absmartly_overrides=exp1:1.2.12345';
// With dev environment
document.cookie = 'absmartly_overrides=devEnv=staging|exp1:1.1,exp2:0.1';
`Format:
name:variant[.env][.id] where:
- Experiments are separated by , (comma)
- Values within an experiment are separated by . (dot)
- Environment prefix uses | (pipe) separator#### How Overrides Work
1. Query String Priority: Query parameters take precedence over cookies
2. Environment Support: Use
env parameter for dev/staging experiments
3. API Fetching: Non-running experiments are fetched from ABsmartly API
4. Context Injection: Experiments are transparently injected into context.data()
5. DOM Application: DOMChangesPlugin applies changes from all experimentsDOM Change Types
The plugin supports comprehensive DOM manipulation with advanced features:
$3
$3
`javascript
{
selector: '.headline',
type: 'text',
value: 'New Headline Text'
}
`$3
`javascript
{
selector: '.content',
type: 'html',
value: 'New HTML content
'
}
`$3
`javascript
{
selector: '.button',
type: 'style',
value: {
backgroundColor: 'red', // Use camelCase for CSS properties
color: '#ffffff',
fontSize: '18px'
},
trigger_on_view: false // Control exposure timing
}
`$3
`javascript
{
selector: '.button',
type: 'styleRules',
states: {
normal: {
backgroundColor: '#007bff',
color: 'white',
padding: '10px 20px',
borderRadius: '4px',
transition: 'all 0.2s ease'
},
hover: {
backgroundColor: '#0056b3',
transform: 'translateY(-2px)',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)'
},
active: {
backgroundColor: '#004085',
transform: 'translateY(0)'
},
focus: {
outline: '2px solid #007bff',
outlineOffset: '2px'
}
},
important: true, // default is true
trigger_on_view: true
}
`Benefits of styleRules:
- Handles CSS pseudo-states properly (hover, active, focus)
- Survives React re-renders through stylesheet injection
- More performant than inline styles for complex interactions
$3
`javascript
{
selector: '.card',
type: 'class',
add: ['highlighted', 'featured'],
remove: ['default']
}
`$3
`javascript
{
selector: 'input[name="email"]',
type: 'attribute',
value: {
'placeholder': 'Enter your email',
'required': 'true'
}
}
`$3
`javascript
{
selector: '.dynamic-element',
type: 'javascript',
value: 'element.addEventListener("click", () => console.log("Clicked!"))'
}
`$3
`javascript
{
selector: '.sidebar',
type: 'move',
targetSelector: '.main-content',
position: 'before' // 'before', 'after', 'firstChild', 'lastChild'
}
`$3
`javascript
{
selector: '.new-banner', // For identification
type: 'create',
element: '',
targetSelector: 'body',
position: 'firstChild',
trigger_on_view: false
}
`$3
`javascript
{
selector: '.lazy-loaded-button',
type: 'style',
value: {
backgroundColor: 'red',
color: 'white'
},
waitForElement: true, // Wait for element to appear
observerRoot: '.main-content', // Optional: specific container to watch
trigger_on_view: true
}
`Perfect for:
- Lazy-loaded content
- React components that mount/unmount
- Modal dialogs
- API-loaded content
- Infinite scroll items
Key Features
$3
The
trigger_on_view property prevents sample ratio mismatch by controlling when A/B test exposures are recorded:`javascript
{
selector: '.below-fold-element',
type: 'style',
value: { backgroundColor: 'blue' },
trigger_on_view: true // Only trigger when element becomes visible
}
`-
false (default): Exposure triggers immediately
- true: Exposure triggers when element enters viewport
- Cross-variant tracking: Tracks elements from ALL variants for unbiased exposure$3
`javascript
// Apply changes from ABsmartly context
await plugin.applyChanges('experiment-name');// Apply individual change
const success = plugin.applyChange(change, 'experiment-name');
// Remove changes
plugin.removeChanges('experiment-name');
// Get applied changes
const changes = plugin.getAppliedChanges('experiment-name');
// Clean up resources
plugin.destroy();
`Documentation
For detailed documentation:
- Optimized Loading Guide - Best practices for loading the SDK and plugins with minimal performance impact
- Exposure Tracking Guide - Understanding trigger_on_view and preventing sample ratio mismatch
Anti-Flicker Support
Prevent content flash before experiments load with two modes:
$3
Hide entire page with smooth fade-in:
`javascript
const domPlugin = new DOMChangesPlugin({
context: context,
hideUntilReady: 'body',
hideTransition: '0.3s ease-in' // Smooth fade-in
});
`Hide only experiment elements (recommended):
`html
Hero section with experiment
CTA button being tested
``javascript
const domPlugin = new DOMChangesPlugin({
context: context,
hideUntilReady: '[data-absmartly-hide]', // CSS selector for elements to hide
hideTransition: '0.4s ease-out'
});
`Hide multiple types of elements:
`javascript
const domPlugin = new DOMChangesPlugin({
context: context,
hideUntilReady: '[data-absmartly-hide], [data-custom-hide], .test-element',
hideTransition: '0.3s ease-in'
});
`$3
1. During Load (Before Experiments Applied):
`css
/ No transition: /
body { visibility: hidden !important; } / With transition: /
body {
visibility: hidden !important;
opacity: 0 !important;
}
`2. After Experiments Applied:
- No transition: Instant reveal (removes
visibility:hidden)
- With transition: Smooth 4-step fade-in:
1. Remove visibility:hidden
2. Add CSS transition
3. Animate opacity 0 → 1
4. Clean up after transition completes$3
`javascript
new DOMChangesPlugin({
// Anti-flicker configuration
hideUntilReady: 'body', // CSS selector for elements to hide
// Examples:
// 'body' - hide entire page
// '[data-absmartly-hide]' - hide only marked elements
// '[data-absmartly-hide], .test' - hide multiple selectors
// false: disabled (default) hideTimeout: 3000, // Max wait time in ms (default: 3000)
// Content auto-shows after timeout
// even if experiments fail to load
hideTransition: '0.3s ease-in', // CSS transition for fade-in
// Examples: '0.3s ease-in', '0.5s linear'
// false: instant reveal (default)
})
`$3
Without anti-flicker:
`
User sees: Original → FLASH → Experiment Version
^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^
(bad UX) (jarring)
`With anti-flicker:
`
User sees: [Hidden] → Experiment Version (smooth fade-in)
^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(no flash) (professional)
`$3
✅ Use
hideUntilReady: '[data-absmartly-hide]' (Recommended)
- Mark only elements being tested
- Faster perceived load time
- Better user experience
- No CLS (Cumulative Layout Shift)`html
Hero headline being tested
Rest of content visible immediately
``javascript
const domPlugin = new DOMChangesPlugin({
context: context,
hideUntilReady: '[data-absmartly-hide]',
hideTransition: '0.3s ease-in'
});
`✅ Use
hideUntilReady: 'body' (For whole-page experiments)
- Complete redesigns
- Multiple changes across page
- Ensures zero flicker✅ Set appropriate timeout
`javascript
{
hideTimeout: 2000 // Faster for good connections
hideTimeout: 3000 // Balanced (default)
hideTimeout: 5000 // Safer for slow connections
}
`⚠️ Avoid long transitions
`javascript
hideTransition: '0.2s ease-in' // ✅ Subtle, professional
hideTransition: '0.3s ease-out' // ✅ Smooth
hideTransition: '1s linear' // ❌ Too slow, feels broken
`$3
| Tool | Method | Our Implementation |
|------|--------|-------------------|
| Google Optimize |
opacity: 0 | ✅ visibility:hidden (better) |
| Optimizely | opacity: 0 | ✅ visibility:hidden (better) |
| VWO | opacity: 0 on body | ✅ Both modes supported |
| ABsmartly | - | ✅ visibility:hidden + optional smooth fade |Why
visibility:hidden is better:
- ✅ Hides from screen readers
- ✅ Prevents interaction
- ✅ No CLS (Cumulative Layout Shift)
- ✅ Can add smooth opacity transitionStyle Persistence
Automatically reapply experiment styles when frameworks (React/Vue/Angular) overwrite them:
$3
`javascript
// Experiment changes button to red
{ selector: '.button', type: 'style', value: { backgroundColor: 'red' } }// User hovers → React's hover handler changes it back to blue
// Experiment style is lost! ❌
`$3
`javascript
{
selector: '.button',
type: 'style',
value: { backgroundColor: 'red' },
persistStyle: true // ✅ Reapply when React overwrites it
}
`$3
1. MutationObserver watches for style attribute changes
2. Detects when framework overwrites experiment styles
3. Automatically reapplies experiment styles
4. Throttled logging (max once per 5 seconds to avoid spam)
$3
Automatically in SPA mode:
`javascript
new DOMChangesPlugin({
context: context,
spa: true // ✅ Style persistence auto-enabled for all style changes
})
`Opt-in per change:
`javascript
{
selector: '.button',
type: 'style',
value: { backgroundColor: 'red' },
persistStyle: true // ✅ Explicit opt-in (works even if spa: false)
}
`$3
✅ React components with hover states:
`javascript
// Button has React onClick that changes style
{
selector: '.cta-button',
type: 'style',
value: { backgroundColor: '#ff6b6b' },
persistStyle: true // Survives React re-renders
}
`✅ Vue reactive styles:
`javascript
{
selector: '.dynamic-price',
type: 'style',
value: { color: 'green', fontWeight: 'bold' },
persistStyle: true // Persists through Vue updates
}
`✅ Angular material components:
`javascript
{
selector: 'mat-button',
type: 'style',
value: { backgroundColor: '#1976d2' },
persistStyle: true // Works with Angular's style binding
}
`❌ Static HTML (persistence not needed):
`javascript
{
selector: '.static-banner',
type: 'style',
value: { display: 'none' },
persistStyle: false // Not needed, saves performance
}
`URL Filtering
Target experiments to specific pages or URL patterns using the
urlFilter property. This enables precise control over where experiments run without code changes.$3
Wrap your DOM changes in a configuration object with
urlFilter:`javascript
{
urlFilter: '/products', // Simple path match
changes: [
{
selector: '.product-title',
type: 'style',
value: { color: 'red' }
}
]
}
`$3
`javascript
{
urlFilter: {
include: ['/checkout', '/cart'], // Run on these URLs
exclude: ['/checkout/success'], // But NOT on these
mode: 'simple', // 'simple' or 'regex'
matchType: 'path' // 'full-url', 'path', 'domain', 'query', 'hash'
},
changes: [ / ... / ]
}
`$3
path (default) - Match against pathname + hash:
`javascript
// URL: https://example.com/products/shoes#new
// Matches: "/products/shoes#new"
{
urlFilter: { include: ['/products/*'], matchType: 'path' },
changes: [ / ... / ]
}
`
full-url - Match complete URL including protocol and domain:
`javascript
// URL: https://example.com/products
// Matches: "https://example.com/products"
{
urlFilter: { include: ['https://example.com/products'], matchType: 'full-url' },
changes: [ / ... / ]
}
`
domain - Match only the hostname:
`javascript
// URL: https://shop.example.com/products
// Matches: "shop.example.com"
{
urlFilter: { include: ['shop.example.com'], matchType: 'domain' },
changes: [ / ... / ]
}
`
query - Match only query parameters:
`javascript
// URL: https://example.com/products?category=shoes
// Matches: "?category=shoes"
{
urlFilter: { include: ['category=shoes'], matchType: 'query' },
changes: [ / ... / ]
}
`
hash - Match only hash fragment:
`javascript
// URL: https://example.com/page#section-1
// Matches: "#section-1"
{
urlFilter: { include: ['#section-*'], matchType: 'hash' },
changes: [ / ... / ]
}
`$3
Simple Mode (default) - Glob-style wildcards:
`javascript
{
urlFilter: {
include: [
'/products/*', // All product pages
'/checkout', // Checkout and checkout/
'/about', // Exact match
'*/special-offer' // Any path ending with /special-offer
],
mode: 'simple'
},
changes: [ / ... / ]
}
`Simple Mode Wildcards:
-
* = Match any characters (0 or more)
- ? = Match single character
- No wildcards = Exact matchRegex Mode - Full regex power:
`javascript
{
urlFilter: {
include: [
'^/products/(shoes|bags)', // Products in specific categories
'/checkout/(step-[1-3])', // Checkout steps 1-3 only
'\\?utm_source=email' // URLs with email traffic
],
mode: 'regex'
},
changes: [ / ... / ]
}
`$3
Include only:
`javascript
{
urlFilter: { include: ['/products/*'] },
changes: [ / ... / ]
}
// ✅ Runs on: /products/shoes, /products/bags
// ❌ Skips: /about, /checkout
`Include with exclusions:
`javascript
{
urlFilter: {
include: ['/products/*'],
exclude: ['/products/admin', '/products/*/edit']
},
changes: [ / ... / ]
}
// ✅ Runs on: /products/shoes, /products/bags
// ❌ Skips: /products/admin, /products/shoes/edit
`Exclude only (match all except):
`javascript
{
urlFilter: { exclude: ['/admin/', '/api/'] },
changes: [ / ... / ]
}
// ✅ Runs on: /products, /checkout, /about
// ❌ Skips: /admin/users, /api/data
`No filter (match all):
`javascript
{
urlFilter: {}, // or omit urlFilter entirely
changes: [ / ... / ]
}
// ✅ Runs on all pages
`Match nothing (disable experiment):
`javascript
{
urlFilter: { include: [] }, // Empty array = explicit "match nothing"
changes: [ / ... / ]
}
// ❌ Never runs (useful for temporary disabling)
`$3
Critical: When using
urlFilter, the plugin implements Sample Ratio Mismatch (SRM) prevention by tracking ALL variants, not just the current variant:`javascript
// Variant 0 (Control): No URL filter
{
changes: [
{ selector: '.hero', type: 'text', value: 'Welcome' }
]
}// Variant 1: Only runs on /products
{
urlFilter: '/products',
changes: [
{ selector: '.hero', type: 'text', value: 'Shop Now' }
]
}
`Without SRM prevention:
- Users on
/products → Tracked in both variants ✅
- Users on /about → Only tracked in variant 0 ❌
- Result: Biased sample sizes!With SRM prevention (automatic):
- Users on
/products → Tracked in both variants ✅
- Users on /about → Tracked in both variants ✅
- Result: Unbiased sample sizes! ✅The plugin checks if ANY variant matches the current URL. If ANY variant matches, ALL variants are tracked for that user, ensuring fair sample distribution.
$3
In SPA mode (
spa: true), the plugin automatically detects URL changes and re-evaluates experiments:`javascript
new DOMChangesPlugin({
context: context,
spa: true, // Enable URL change detection
autoApply: true
});
`What it tracks:
- ✅
history.pushState() - Client-side navigation
- ✅ history.replaceState() - URL updates
- ✅ popstate events - Browser back/forward buttonsWhat happens on URL change:
1. Remove all currently applied DOM changes
2. Re-evaluate URL filters with the new URL
3. Apply changes for experiments matching the new URL
4. Re-track exposure for newly applied experiments
Example flow:
`
User lands on: /products → Apply product page experiments
User navigates to: /checkout → Remove product experiments, apply checkout experiments
User goes back: /products → Remove checkout experiments, re-apply product experiments
`Without SPA mode:
- URL changes are NOT detected
- Experiments applied on initial page load remain active
- New experiments on the new URL are NOT applied
- Use
spa: false only for static sites without client-side routing$3
E-commerce experiment on specific pages:
`javascript
{
urlFilter: {
include: ['/products/', '/collections/'],
exclude: ['/products/admin', '*/edit'],
matchType: 'path',
mode: 'simple'
},
changes: [
{
selector: '.add-to-cart-button',
type: 'style',
value: { backgroundColor: '#ff6b6b', color: 'white' },
trigger_on_view: true
}
]
}
`Blog post experiment with regex:
`javascript
{
urlFilter: {
include: ['^/blog/20(24|25)'], // Only 2024-2025 blog posts
mode: 'regex',
matchType: 'path'
},
changes: [
{
selector: '.blog-cta',
type: 'text',
value: 'Subscribe to our newsletter!'
}
]
}
`Landing page experiment with UTM parameters:
`javascript
{
urlFilter: {
include: ['utm_campaign=summer'],
matchType: 'query'
},
changes: [
{
selector: '.hero-banner',
type: 'html',
value: 'Summer Sale - 50% Off!
'
}
]
}
`Homepage only (exact match):
`javascript
{
urlFilter: '/', // Shorthand for { include: ['/'] }
changes: [
{
selector: '.hero-title',
type: 'text',
value: 'Limited Time Offer!'
}
]
}
`Multiple pages (array shorthand):
`javascript
{
urlFilter: ['/pricing', '/features'], // Shorthand for { include: [...] }
changes: [ / ... / ]
}
`SPA Mode (React/Vue/Angular Support)
Enable
spa: true for Single Page Applications. This automatically activates three critical features:$3
Automatically detects client-side navigation and re-evaluates experiments:
- Intercepts
history.pushState() and history.replaceState()
- Listens to popstate events (browser back/forward)
- Re-applies appropriate experiments when URL changesSee URL Change Detection above for details.
$3
Automatically waits for elements that don't exist yet in the DOM:
`javascript
{
selector: '.lazy-loaded-button',
type: 'style',
value: { backgroundColor: 'red' }
// No waitForElement needed - SPA mode handles it automatically!
}
`Without SPA mode: Change skipped if element doesn't exist
With SPA mode: Observes DOM and applies when element appears
$3
Automatically re-applies styles when frameworks overwrite them:
`javascript
{
selector: '.button',
type: 'style',
value: { backgroundColor: 'red' }
// No persistStyle needed - SPA mode handles it automatically!
}
`Without SPA mode: React hover states can overwrite experiment styles
With SPA mode: Detects and re-applies styles automatically
$3
✅ Enable
spa: true for:
- React applications
- Vue.js applications
- Angular applications
- Any app with client-side routing
- Apps with lazy-loaded components
- Apps with dynamic DOM updates❌ Keep
spa: false for:
- Static HTML sites
- Server-rendered pages without client-side routing
- Sites where performance is critical and you don't need dynamic features$3
You can still use flags explicitly if you need granular control:
`javascript
{
selector: '.button',
type: 'style',
value: { backgroundColor: 'red' },
persistStyle: true, // Force enable (even if spa: false)
waitForElement: true // Force enable (even if spa: false)
}
`Configuration Options
`javascript
new DOMChangesPlugin({
// Required
context: absmartlyContext, // ABsmartly context // Core options
autoApply: true, // Auto-apply changes from SDK
spa: true, // Enable SPA support
// ✅ Auto-enables: waitForElement + persistStyle
visibilityTracking: true, // Track viewport visibility
variableName: '__dom_changes', // Variable name for DOM changes
debug: false, // Enable debug logging
// Anti-flicker options
hideUntilReady: false, // CSS selector (e.g., 'body', '[data-absmartly-hide]') or false
hideTimeout: 3000, // Max wait time (ms)
hideTransition: false, // CSS transition (e.g., '0.3s ease-in') or false
})
``- Modern browsers (Chrome, Firefox, Safari, Edge)
- Internet Explorer 11+ (with polyfills)
- Mobile browsers (iOS Safari, Chrome Mobile)
MIT