A/B Smartly SDK for Shopify Liquid templating language with server-side rendering support.
This SDK enables A/B testing in Shopify stores using Liquid templates. It provides Liquid filters, tags, and drops (objects) that integrate with ABsmartly's experimentation platform.
Architecture:
- Ruby backend SDK (handles HTTP, variant assignment, hashing)
- Liquid filters and tags for template integration
- Server-side context initialization and caching
- Shopify theme integration
- Shopify Liquid templates
- Ruby 2.7+ (Shopify backend)
- Works with Shopify themes and apps
- Add the ABsmartly Liquid SDK files to your theme:
assets/
absmartly.js # Client-side tracking (optional)
snippets/
absmartly-init.liquid # Initialization snippet
absmartly-tracking.liquid # Tracking snippet
- Add initialization to your
theme.liquid:
{% render 'absmartly-init',
session_id: customer.id | default: request.cookie.session_id,
customer_id: customer.id
%}Add to your Gemfile:
gem 'absmartly-liquid-sdk'Then run:
bundle installIn your Shopify app controller or theme:
# Initialize SDK
sdk = ABSmartly::SDK.new(
endpoint: 'https://your-endpoint.absmartly.io/v1',
api_key: ENV['ABSMARTLY_API_KEY'],
application: 'shopify-store',
environment: 'production'
)
# Create context
@absmartly_context = sdk.create_context(
units: {
session_id: session[:id],
customer_id: current_customer&.id
}
)
# Wait for ready
@absmartly_context.ready
# Make context available to Liquid
@absmartly_data = {
'context_data' => @absmartly_context.data.to_json,
'units' => @absmartly_context.get_units
}{% assign variant = 'exp_button_color' | absmartly_treatment %}
{% if variant == 0 %}
<button class="btn-blue">Buy Now</button>
{% elsif variant == 1 %}
<button class="btn-red">Buy Now</button>
{% endif %}{% assign button_text = 'button_text' | absmartly_variable: 'Buy Now' %}
<button>{{ button_text }}</button>{% assign variant = 'exp_button_color' | absmartly_peek %}{% absmartly_track 'add_to_cart', amount: product.price, quantity: 1 %}Or use the filter:
{{ 'purchase' | absmartly_track: amount: order.total_price }}For tracking after page load:
<script>
// ABsmartly context initialized server-side
window.absmartly.track('button_click', {
variant: {{ variant }},
product_id: {{ product.id }}
});
</script>Get variant and track exposure.
{% assign variant = 'exp_test' | absmartly_treatment %}Returns: Variant number (0, 1, 2, ...)
Side effect: Tracks exposure event
Get variant without tracking exposure.
{% assign variant = 'exp_test' | absmartly_peek %}Returns: Variant number (0, 1, 2, ...)
Side effect: None
Get variable value and track exposure.
{% assign value = 'button_color' | absmartly_variable: 'blue' %}Parameters:
- First argument (pipe input): Variable key
- Second argument: Default value
Returns: Variable value or default
Side effect: Tracks exposure event
Get variable value without tracking exposure.
{% assign value = 'button_color' | absmartly_peek_variable: 'blue' %}Returns: Variable value or default
Side effect: None
Track goal achievement.
{{ 'purchase' | absmartly_track: amount: order.total_price, items: order.line_items.size }}Parameters:
- First argument (pipe input): Goal name
- Named parameters: Goal properties (must be numeric)
Returns: Empty string
Side effect: Queues goal event for publishing
Get custom field value.
{% assign metadata = 'exp_test' | absmartly_custom_field: 'metadata' %}Returns: Parsed field value based on type
Block tag for treatment assignment.
{% absmartly_treatment 'exp_button_color' %}
{% if variant == 0 %}
<button class="btn-blue">Control</button>
{% elsif variant == 1 %}
<button class="btn-red">Treatment</button>
{% endif %}
{% endabsmartly_treatment %}Inside the block, variant variable is available.
Track goal with properties.
{% absmartly_track 'purchase', amount: order.total_price, items: order.line_items.size %}ABsmartly context is available as absmartly object in Liquid:
{{ absmartly.experiments }} <!-- Array of experiment names -->
{{ absmartly.pending }} <!-- Number of pending events -->
{{ absmartly.ready }} <!-- true/false -->ready(boolean) - Whether context is readyfailed(boolean) - Whether context failed to loadfinalized(boolean) - Whether context is finalizedexperiments(array) - List of experiment namespending(number) - Count of pending events
{{ absmartly.treatment('exp_test') }}
{{ absmartly.peek('exp_test') }}
{{ absmartly.variable('button_color', 'blue') }}
{{ absmartly.peek_variable('button_color', 'blue') }}
{{ absmartly.custom_field('exp_test', 'metadata') }}
{{ absmartly.track('goal_name', properties) }}class ProductsController < ApplicationController
before_action :init_absmartly
def show
@product = Product.find(params[:id])
# Get variant for this user
variant = @absmartly.treatment('exp_product_layout')
# Pass to view
@layout_variant = variant
# Data for Liquid
@absmartly_liquid = ABSmartly::LiquidDrop.new(@absmartly)
end
private
def init_absmartly
@absmartly = $absmartly_sdk.create_context(
units: {
session_id: session[:id],
customer_id: current_customer&.id
}
)
@absmartly.ready
end
end<!-- theme.liquid -->
{% capture session_id %}{{ request.cookie['_shopify_s'] }}{% endcapture %}
{% render 'absmartly-init',
endpoint: 'https://your-endpoint.absmartly.io/v1',
api_key: 'your-api-key',
session_id: session_id,
customer_id: customer.id
%}
<!-- product.liquid -->
{% assign layout_variant = 'exp_product_layout' | absmartly_treatment %}
{% if layout_variant == 0 %}
{% render 'product-layout-control' %}
{% elsif layout_variant == 1 %}
{% render 'product-layout-treatment' %}
{% endif %}For better performance, pre-fetch context data server-side:
# In controller
data = sdk.get_client.get_context(units: { session_id: session[:id] })
# Create context with data (no HTTP call)
@absmartly = sdk.create_context_with(
{ units: { session_id: session[:id] } },
data
)
# Pass to Liquid
assigns['absmartly'] = ABSmartly::LiquidDrop.new(@absmartly)# Cache context data for session
cache_key = "absmartly:#{session[:id]}"
data = Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
sdk.get_client.get_context(units: { session_id: session[:id] })
end
@absmartly = sdk.create_context_with({ units: { session_id: session[:id] } }, data)class ShopifyEventLogger
def handle_event(context, event_name, data)
case event_name
when 'exposure'
# Log to Shopify analytics
Analytics.track('absmartly_exposure', data)
when 'goal'
# Log to your tracking system
Tracking.event('absmartly_goal', data)
when 'error'
# Log errors
Rails.logger.error("ABsmartly error: #{data}")
end
end
end
sdk = ABSmartly::SDK.new(
endpoint: ENV['ABSMARTLY_ENDPOINT'],
api_key: ENV['ABSMARTLY_API_KEY'],
application: 'shopify-store',
environment: Rails.env,
event_logger: ShopifyEventLogger.new
)Events are automatically published, but you can manually publish:
# In controller after action
@absmartly.publishOr finalize on session end:
# In ApplicationController
after_action :finalize_absmartly
def finalize_absmartly
@absmartly&.finalize
end<!-- product.liquid -->
{% assign variant = 'exp_product_images' | absmartly_treatment %}
{% if variant == 0 %}
<!-- Control: Single image -->
<img src="{{ product.featured_image | img_url: 'large' }}" alt="{{ product.title }}">
{% elsif variant == 1 %}
<!-- Treatment: Image gallery -->
<div class="product-gallery">
{% for image in product.images %}
<img src="{{ image | img_url: 'medium' }}" alt="{{ product.title }}">
{% endfor %}
</div>
{% endif %}
<!-- Track add to cart -->
<form action="/cart/add" method="post">
<button type="submit" onclick="absmartly.track('add_to_cart', { product_id: {{ product.id }}, price: {{ product.price }} })">
Add to Cart
</button>
</form>{% assign button_text = 'checkout_button_text' | absmartly_variable: 'Checkout' %}
{% assign button_color = 'checkout_button_color' | absmartly_variable: 'blue' %}
<a href="/checkout" class="btn-{{ button_color }}">
{{ button_text }}
</a>{% assign free_shipping_threshold = 'free_shipping_threshold' | absmartly_variable: 50 %}
{% if cart.total_price >= free_shipping_threshold %}
<div class="free-shipping-banner">
๐ You qualify for free shipping!
</div>
{% else %}
{% assign remaining = free_shipping_threshold | minus: cart.total_price %}
<div class="free-shipping-progress">
Add ${{ remaining }} more for free shipping
</div>
{% endif %}<!-- Thank you page (checkout complete) -->
{% if first_time_accessed %}
{% absmartly_track 'purchase',
amount: order.total_price,
items: order.line_items.size,
revenue: order.subtotal_price
%}
{% endif %}{% assign show_new_feature = 'feature_new_search' | absmartly_treatment %}
{% if show_new_feature == 1 %}
{% render 'search-v2' %}
{% else %}
{% render 'search-v1' %}
{% endif %}require 'minitest/autorun'
require 'absmartly/liquid'
class ABSmartlyLiquidTest < Minitest::Test
def setup
@sdk = ABSmartly::SDK.new(
endpoint: 'https://sandbox.absmartly.io/v1',
api_key: 'test-key',
application: 'test',
environment: 'test'
)
@context = @sdk.create_context(units: { session_id: 'test123' })
@context.ready
end
def test_treatment_filter
template = Liquid::Template.parse("{{ 'exp_test' | absmartly_treatment }}")
output = template.render('absmartly' => ABSmartly::LiquidDrop.new(@context))
assert_match /^[0-9]+$/, output
end
def test_variable_filter
template = Liquid::Template.parse("{{ 'button_color' | absmartly_variable: 'blue' }}")
output = template.render('absmartly' => ABSmartly::LiquidDrop.new(@context))
assert_includes ['blue', 'red', 'green'], output
end
endSee test/integration_test.rb for full integration tests.
- Cache context data per session (5-10 minutes)
- Use Redis for distributed caching
- Pre-fetch data server-side to avoid blocking Liquid rendering
- Events are batched and published asynchronously
- Configure
publish_delayto batch more events - Use background jobs for publishing on high-traffic stores
- Minimize A/B test logic in templates
- Pre-calculate variants server-side when possible
- Use fragment caching for expensive A/B test blocks
If absmartly.ready is false:
{% if absmartly.ready %}
{% assign variant = 'exp_test' | absmartly_treatment %}
{% else %}
<!-- Fallback UI -->
{% endif %}Check event logger configuration and network connectivity:
# Enable debug logging
sdk = ABSmartly::SDK.new(
endpoint: ENV['ABSMARTLY_ENDPOINT'],
api_key: ENV['ABSMARTLY_API_KEY'],
application: 'shopify-store',
environment: Rails.env,
event_logger: ->(ctx, event, data) { Rails.logger.debug "ABsmartly: #{event} - #{data}" }
)Ensure units are consistent:
# Use same session ID everywhere
session_id = session[:id] || SecureRandom.uuid
session[:id] = session_id
@absmartly = sdk.create_context(units: { session_id: session_id })See API.md for complete API documentation.
- Initialize once per request - Create context in controller/before_action
- Use consistent units - Same session_id/customer_id throughout request
- Cache context data - Reduce HTTP calls to ABsmartly
- Track conversions - Use
absmartly_trackon important actions - Test fallbacks - Handle context not ready gracefully
- Monitor performance - Log slow requests, optimize caching
See examples/ for complete Shopify theme examples:
- Product page A/B tests
- Checkout flow optimization
- Homepage layout variations
- Feature flags
- Conversion tracking
See CONTRIBUTING.md
Apache License 2.0
ABsmartly is the leading provider of state-of-the-art, on-premises, full-stack experimentation platforms for engineering and product teams that want to confidently deploy features as fast as they can develop them.