A comprehensive collection of plugins for the ABsmartly JavaScript SDK including DOM manipulation, experiment overrides, cookie management, and web vitals tracking.
- Complete DOM Manipulation: All change types including styleRules with pseudo-states
- Smart Exposure Tracking: Cross-variant tracking prevents sample ratio mismatch
- Dynamic Content Support: Pending changes with
waitForElementfor SPAs - React/Vue Compatibility: StyleRules survive re-renders
- Anti-Flicker Support: Hide content until experiments load to prevent visual flash
- Query String Overrides: Force experiments via URL parameters
- Cookie Persistence: Server-compatible experiment overrides
- API Integration: Fetch non-running experiments (Full version)
- Development Support: Test experiments in any environment
- Unit ID Management: Generate and persist user identifiers
- UTM Tracking: Capture and store UTM parameters
- Landing Page Tracking: Track first visits and referrers
- Storage Fallbacks: Cookie → localStorage → memory
- Core Web Vitals: Track CLS, LCP, FCP, INP, TTFB
- Page Metrics: Network timing, DOM processing, resource counts
- Performance Ratings: Automatic good/needs-improvement/poor classification
- Compression Metrics: Track page size and compression ratios
npm install @absmartly/sdk-pluginsimport { 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:
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');
});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
});Use individual query parameters with configurable prefix:
# Single experiment
https://example.com?_exp_button_color=1
# Multiple experiments
https://example.com?_exp_hero_title=0&_exp_nav_style=2
# With environment
https://example.com?env=staging&_exp_dev_feature=1,1
# With experiment ID
https://example.com?_exp_archived_test=1,2,12345
Cookies use comma as separator (no encoding needed):
// 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
- Query String Priority: Query parameters take precedence over cookies
- Environment Support: Use
envparameter for dev/staging experiments - API Fetching: Non-running experiments are fetched from ABsmartly API
- Context Injection: Experiments are transparently injected into context.data()
- DOM Application: DOMChangesPlugin applies changes from all experiments
The plugin supports comprehensive DOM manipulation with advanced features:
{
selector: '.headline',
type: 'text',
value: 'New Headline Text'
}{
selector: '.content',
type: 'html',
value: '<p>New <strong>HTML</strong> content</p>'
}{
selector: '.button',
type: 'style',
value: {
backgroundColor: 'red', // Use camelCase for CSS properties
color: '#ffffff',
fontSize: '18px'
},
trigger_on_view: false // Control exposure timing
}{
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
{
selector: '.card',
type: 'class',
add: ['highlighted', 'featured'],
remove: ['default']
}{
selector: 'input[name="email"]',
type: 'attribute',
value: {
'placeholder': 'Enter your email',
'required': 'true'
}
}{
selector: '.dynamic-element',
type: 'javascript',
value: 'element.addEventListener("click", () => console.log("Clicked!"))'
}{
selector: '.sidebar',
type: 'move',
targetSelector: '.main-content',
position: 'before' // 'before', 'after', 'firstChild', 'lastChild'
}{
selector: '.new-banner', // For identification
type: 'create',
element: '<div class="banner">Special Offer!</div>',
targetSelector: 'body',
position: 'firstChild',
trigger_on_view: false
}{
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
The trigger_on_view property prevents sample ratio mismatch by controlling when A/B test exposures are recorded:
{
selector: '.below-fold-element',
type: 'style',
value: { backgroundColor: 'blue' },
trigger_on_view: true // Only trigger when element becomes visible
}false(default): Exposure triggers immediatelytrue: Exposure triggers when element enters viewport- Cross-variant tracking: Tracks elements from ALL variants for unbiased exposure
// 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();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
Prevent content flash before experiments load with two modes:
Hide entire page with smooth fade-in:
const domPlugin = new DOMChangesPlugin({
context: context,
hideUntilReady: 'body',
hideTransition: '0.3s ease-in' // Smooth fade-in
});Hide only experiment elements (recommended):
<!-- Mark elements that need anti-flicker -->
<div data-absmartly-hide>
Hero section with experiment
</div>
<div data-absmartly-hide>
CTA button being tested
</div>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:
const domPlugin = new DOMChangesPlugin({
context: context,
hideUntilReady: '[data-absmartly-hide], [data-custom-hide], .test-element',
hideTransition: '0.3s ease-in'
});-
During Load (Before Experiments Applied):
/* No transition: */ body { visibility: hidden !important; } /* With transition: */ body { visibility: hidden !important; opacity: 0 !important; }
-
After Experiments Applied:
- No transition: Instant reveal (removes
visibility:hidden) - With transition: Smooth 4-step fade-in:
- Remove
visibility:hidden - Add CSS transition
- Animate opacity 0 → 1
- Clean up after transition completes
- Remove
- No transition: Instant reveal (removes
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)
})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)
✅ Use hideUntilReady: '[data-absmartly-hide]' (Recommended)
- Mark only elements being tested
- Faster perceived load time
- Better user experience
- No CLS (Cumulative Layout Shift)
<nav>Normal navigation</nav>
<div data-absmartly-hide>
<!-- Only this hero is hidden -->
<h1>Hero headline being tested</h1>
</div>
<main>Rest of content visible immediately</main>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
{
hideTimeout: 2000 // Faster for good connections
hideTimeout: 3000 // Balanced (default)
hideTimeout: 5000 // Safer for slow connections
}hideTransition: '0.2s ease-in' // ✅ Subtle, professional
hideTransition: '0.3s ease-out' // ✅ Smooth
hideTransition: '1s linear' // ❌ Too slow, feels broken| 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 transition
Automatically reapply experiment styles when frameworks (React/Vue/Angular) overwrite them:
// 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! ❌{
selector: '.button',
type: 'style',
value: { backgroundColor: 'red' },
persistStyle: true // ✅ Reapply when React overwrites it
}- MutationObserver watches for style attribute changes
- Detects when framework overwrites experiment styles
- Automatically reapplies experiment styles
- Throttled logging (max once per 5 seconds to avoid spam)
Automatically in SPA mode:
new DOMChangesPlugin({
context: context,
spa: true // ✅ Style persistence auto-enabled for all style changes
})Opt-in per change:
{
selector: '.button',
type: 'style',
value: { backgroundColor: 'red' },
persistStyle: true // ✅ Explicit opt-in (works even if spa: false)
}✅ React components with hover states:
// Button has React onClick that changes style
{
selector: '.cta-button',
type: 'style',
value: { backgroundColor: '#ff6b6b' },
persistStyle: true // Survives React re-renders
}✅ Vue reactive styles:
{
selector: '.dynamic-price',
type: 'style',
value: { color: 'green', fontWeight: 'bold' },
persistStyle: true // Persists through Vue updates
}✅ Angular material components:
{
selector: 'mat-button',
type: 'style',
value: { backgroundColor: '#1976d2' },
persistStyle: true // Works with Angular's style binding
}❌ Static HTML (persistence not needed):
{
selector: '.static-banner',
type: 'style',
value: { display: 'none' },
persistStyle: false // Not needed, saves performance
}Target experiments to specific pages or URL patterns using the urlFilter property. This enables precise control over where experiments run without code changes.
Wrap your DOM changes in a configuration object with urlFilter:
{
urlFilter: '/products', // Simple path match
changes: [
{
selector: '.product-title',
type: 'style',
value: { color: 'red' }
}
]
}{
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: [ /* ... */ ]
}path (default) - Match against pathname + hash:
// 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:
// 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:
// URL: https://shop.example.com/products
// Matches: "shop.example.com"
{
urlFilter: { include: ['shop.example.com'], matchType: 'domain' },
changes: [ /* ... */ ]
}query - Match only query parameters:
// URL: https://example.com/products?category=shoes
// Matches: "?category=shoes"
{
urlFilter: { include: ['*category=shoes*'], matchType: 'query' },
changes: [ /* ... */ ]
}hash - Match only hash fragment:
// URL: https://example.com/page#section-1
// Matches: "#section-1"
{
urlFilter: { include: ['#section-*'], matchType: 'hash' },
changes: [ /* ... */ ]
}Simple Mode (default) - Glob-style wildcards:
{
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 match
Regex Mode - Full regex power:
{
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: [ /* ... */ ]
}Include only:
{
urlFilter: { include: ['/products/*'] },
changes: [ /* ... */ ]
}
// ✅ Runs on: /products/shoes, /products/bags
// ❌ Skips: /about, /checkoutInclude with exclusions:
{
urlFilter: {
include: ['/products/*'],
exclude: ['/products/admin', '/products/*/edit']
},
changes: [ /* ... */ ]
}
// ✅ Runs on: /products/shoes, /products/bags
// ❌ Skips: /products/admin, /products/shoes/editExclude only (match all except):
{
urlFilter: { exclude: ['/admin/*', '/api/*'] },
changes: [ /* ... */ ]
}
// ✅ Runs on: /products, /checkout, /about
// ❌ Skips: /admin/users, /api/dataNo filter (match all):
{
urlFilter: {}, // or omit urlFilter entirely
changes: [ /* ... */ ]
}
// ✅ Runs on all pagesMatch nothing (disable experiment):
{
urlFilter: { include: [] }, // Empty array = explicit "match nothing"
changes: [ /* ... */ ]
}
// ❌ Never runs (useful for temporary disabling)Critical: When using urlFilter, the plugin implements Sample Ratio Mismatch (SRM) prevention by tracking ALL variants, not just the current variant:
// 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.
In SPA mode (spa: true), the plugin automatically detects URL changes and re-evaluates experiments:
new DOMChangesPlugin({
context: context,
spa: true, // Enable URL change detection
autoApply: true
});What it tracks:
- ✅
history.pushState()- Client-side navigation - ✅
history.replaceState()- URL updates - ✅
popstateevents - Browser back/forward buttons
What happens on URL change:
- Remove all currently applied DOM changes
- Re-evaluate URL filters with the new URL
- Apply changes for experiments matching the new URL
- 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: falseonly for static sites without client-side routing
E-commerce experiment on specific pages:
{
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:
{
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:
{
urlFilter: {
include: ['*utm_campaign=summer*'],
matchType: 'query'
},
changes: [
{
selector: '.hero-banner',
type: 'html',
value: '<h1>Summer Sale - 50% Off!</h1>'
}
]
}Homepage only (exact match):
{
urlFilter: '/', // Shorthand for { include: ['/'] }
changes: [
{
selector: '.hero-title',
type: 'text',
value: 'Limited Time Offer!'
}
]
}Multiple pages (array shorthand):
{
urlFilter: ['/pricing', '/features'], // Shorthand for { include: [...] }
changes: [ /* ... */ ]
}Enable spa: true for Single Page Applications. This automatically activates three critical features:
Automatically detects client-side navigation and re-evaluates experiments:
- Intercepts
history.pushState()andhistory.replaceState() - Listens to
popstateevents (browser back/forward) - Re-applies appropriate experiments when URL changes
See URL Change Detection above for details.
Automatically waits for elements that don't exist yet in the DOM:
{
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
Automatically re-applies styles when frameworks overwrite them:
{
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
✅ 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
You can still use flags explicitly if you need granular control:
{
selector: '.button',
type: 'style',
value: { backgroundColor: 'red' },
persistStyle: true, // Force enable (even if spa: false)
waitForElement: true // Force enable (even if spa: false)
}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