Official GrowthBook SDK for Roku/BrightScript applications. Add feature flags and A/B testing to your Roku channels with a simple, lightweight SDK.
Production-ready release with critical fixes:
- ✅ Fixed: Weighted Experiments - 70/30 splits and custom weight distributions now work correctly
- ✅ Fixed: Consistent Hashing - Users get same variation across sessions (cross-platform compatible)
- ✅ New: Version Targeting - Six new operators (
$vgt,$vgte,$vlt,$vlte,$veq,$vne) for semantic version comparisons - ✅ Enterprise Documentation - Complete integration guide, quick start, and working examples
View Changelog | Integration Guide | Quick Start
- 🚀 Lightweight - Core SDK is ~50KB, minimal memory footprint
- ⚡ Fast - Feature evaluation in <1ms, optimized for Roku devices
- 🎯 Powerful Targeting - Target users by app version, attributes, and segments
- 🧪 A/B Testing - Run experiments with accurate traffic splits (70/30, 50/25/25, etc.)
- 🔄 Consistent Bucketing - Same user always sees same variation (cross-platform)
- 📊 Analytics Ready - Built-in experiment tracking callbacks
- 🔒 Secure - Support for encrypted feature payloads
- 🎨 No Dependencies - Pure BrightScript, works on all Roku devices (Roku 3+, OS 9.0+)
Copy GrowthBook.brs to your Roku channel's source/ directory:
your-roku-channel/
├── source/
│ ├── main.brs
│ └── GrowthBook.brs ← Add this file
└── manifest' In your main.brs or scene component
function initGrowthBook() as object
config = {
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123", ' Your GrowthBook client key
attributes: {
id: "user-123",
deviceType: "roku",
premium: true
}
}
gb = GrowthBook(config)
' Load features from GrowthBook API
if gb.init()
print "GrowthBook ready!"
end if
return gb
end function' Boolean feature flag
if gb.isOn("new-player-ui") then
showNewPlayer()
else
showLegacyPlayer()
end if
' Get feature value with fallback
buttonColor = gb.getFeatureValue("cta-color", "#0000FF")
maxVideos = gb.getFeatureValue("videos-per-page", 12)
' JSON feature configuration
playerConfig = gb.getFeatureValue("player-settings", {
autoplay: false,
quality: "HD"
})Create one instance and resuse it.
' Initialize ONCE when app starts
m.global.addFields({ gb: invalid })
function InitApp()
m.global.gb = GrowthBook({
clientKey: "sdk_YOUR_KEY",
attributes: { id: GetUserId() }
})
m.global.gb.init()
end function
' Reuse the same instance throughout your app
function ShowFeature()
if m.global.gb.isOn("feature-key") then
' Feature logic
end if
end function
' Update attributes when needed (e.g., user login)
function OnUserLogin(newUserId)
m.global.gb.setAttributes({ id: newUserId })
' Features are now re-evaluated with new user ID
end functionCreating a new instance for each feature check causes:
| Problem | Impact |
|---|---|
| Redundant API calls | Every init() call fetches features again (expensive) |
| Memory waste | Each instance consumes ~150KB of memory |
| Inconsistent variations | Different hash seeds mean same user gets different variations |
| Poor performance | Network latency on every feature evaluation |
| Broken experiments | Inconsistent variation assignment for A/B tests |
' In your app's global initialization
function Main()
' Create ONCE
globalNode = CreateObject("roSGNode", "GlobalNode")
' Initialize GrowthBook once
gb = GrowthBook({
clientKey: "sdk_YOUR_KEY",
attributes: { id: "user123" }
})
if gb.init()
' Store globally to reuse everywhere
globalNode.addFields({ growthBook: gb })
end if
' Now use it throughout your app
' Access via: globalNode.growthBook
end function' ✅ DO THIS: Update attributes without recreating instance
m.global.gb.setAttributes({
id: newUserId,
subscription: "premium",
country: "US"
})
' ❌ DON'T DO THIS: Creating new instance to change attributes
m.global.gb = GrowthBook({ ... }) ' Wrong!
m.global.gb.init() ' Wrong!'
' Complete example showing SDK configuration and initialization
'
function InitializeGrowthBook() as object
' Step 1: Create configuration object
config = {
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk_YOUR_CLIENT_KEY",
' Set user attributes for targeting
attributes: {
id: "user-" + GetUserId(), ' Unique user ID
email: GetUserEmail(), ' User email
subscription: GetSubscriptionTier(), ' free, basic, premium, enterprise
country: "US", ' User location
isPremium: (GetSubscriptionTier() = "premium")
},
' Enable debug logging during development
enableDevMode: false
}
' Step 2: Create GrowthBook instance
gb = GrowthBook(config)
' Step 3: Initialize - load features from API
if gb.init()
print "✓ GrowthBook initialized successfully"
print " Features loaded: " + Str(gb.features.Count())
else
print "✗ Failed to initialize GrowthBook"
' Optionally continue with defaults or cached features
return invalid
end if
return gb
end function
function GetUserId() as string
' Get from your app's user data
return "12345"
end function
function GetUserEmail() as string
return "[email protected]"
end function
function GetSubscriptionTier() as string
' Return: "free", "basic", "premium", or "enterprise"
return "premium"
end function
' Usage in your main application
sub Main()
gb = InitializeGrowthBook()
if gb <> invalid
' Now ready to use feature flags
if gb.isOn("app-feature")
print "Feature is enabled!"
end if
end if
end sub'
' Comprehensive example showing attribute management and feature evaluation
'
sub ManageUserAttributesAndFeatures()
' Initialize with basic attributes
gb = GrowthBook({
clientKey: "sdk_YOUR_KEY",
attributes: {
id: "user-123",
country: "US"
}
})
gb.init()
' =========================================================
' SECTION 1: Initial Feature Evaluation
' =========================================================
print "--- Initial State ---"
if gb.isOn("enable-new-ui")
print "✓ New UI is enabled"
LoadNewUserInterface()
else
print "✗ New UI is disabled, using legacy"
LoadLegacyUserInterface()
end if
' =========================================================
' SECTION 2: User Logs In - Update Attributes
' =========================================================
' When user logs in, update their attributes
gb.setAttributes({
id: "user-456", ' Now we know their real ID
email: "[email protected]", ' Email address
subscription: "premium", ' Premium subscriber
country: "US",
accountAgeInDays: 365, ' Account created a year ago
watchTimeMinutes: 50000 ' User has watched a lot
})
print ""
print "--- After User Login ---"
' =========================================================
' SECTION 3: Evaluate Features with New Attributes
' =========================================================
' Feature 1: Premium feature (only for premium users)
if gb.isOn("premium-features")
print "✓ Premium features available"
ShowPremiumContent()
else
print "✗ Premium features not available"
end if
' Feature 2: Get specific value with fallback
maxQuality = gb.getFeatureValue("max-video-quality", "1080p")
print " Max quality: " + maxQuality
' Feature 3: Complex object configuration
playerSettings = gb.getFeatureValue("player-config", {
autoplay: false,
quality: "HD",
subtitles: "on",
dolbyVision: false
})
print " Player autoplay: " + Str(playerSettings.autoplay)
print " Player quality: " + playerSettings.quality
' =========================================================
' SECTION 4: Detailed Feature Evaluation
' =========================================================
' Get detailed evaluation including source and targeting info
result = gb.evalFeature("advanced-search")
print ""
print "--- Feature Evaluation Details ---"
print "Feature: " + result.key
print "Enabled: " + Str(result.on)
print "Value: " + Str(result.value)
print "Source: " + result.source ' defaultValue, force, experiment, or unknownFeature
if result.source = "experiment"
print "Experiment: " + result.experimentId
print "Variation: " + Str(result.variationId)
end if
' =========================================================
' SECTION 5: Attribute Updates Over Time
' =========================================================
' User upgrades subscription
print ""
print "--- User Upgrades Subscription ---"
gb.setAttributes({
id: "user-456",
email: "[email protected]",
subscription: "enterprise", ' Now enterprise user!
country: "US",
accountAgeInDays: 365,
watchTimeMinutes: 50000
})
' Re-evaluate premium features
if gb.isOn("premium-features")
print "✓ Enterprise features now available"
end if
' Get enterprise-specific feature
apiLimit = gb.getFeatureValue("api-rate-limit", 1000)
print " API rate limit: " + Str(apiLimit) + " requests/day"
end sub
function LoadNewUserInterface()
print "Loading new UI..."
end function
function LoadLegacyUserInterface()
print "Loading legacy UI..."
end function
function ShowPremiumContent()
print "Showing premium content..."
end function'
' Advanced example with complex targeting conditions and experiment tracking
'
sub AdvancedTargetingAndExperiments()
' Initialize with tracking callback
config = {
clientKey: "sdk_YOUR_KEY",
attributes: {
id: "user-789",
subscription: "premium",
country: "US",
accountAge: 200,
isTestUser: false
},
' Callback fired when user enters an experiment
trackingCallback: sub(experiment, result)
print "[EXPERIMENT TRACKED]"
print " Experiment ID: " + experiment.key
print " Variation ID: " + Str(result.variationId)
print " Value: " + Str(result.value)
' Send to your analytics platform
SendAnalyticsEvent({
event: "experiment_exposed",
experimentId: experiment.key,
variationId: result.variationId,
userId: experiment.userId
})
end sub
}
gb = GrowthBook(config)
gb.init()
' =========================================================
' TARGETING BY SUBSCRIPTION LEVEL
' =========================================================
print "--- Premium Features (Subscription Targeting) ---"
' This feature only shows for premium/enterprise users
if gb.isOn("advanced-analytics")
print "✓ Advanced analytics available for premium users"
ShowAdvancedAnalytics()
end if
' =========================================================
' TARGETING BY COUNTRY
' =========================================================
print ""
print "--- Regional Features (Country Targeting) ---"
' Feature targeted to US & Canada only
if gb.isOn("hdr-streaming")
print "✓ HDR streaming enabled for North America"
else
print "✗ HDR streaming not available in your region"
end if
' =========================================================
' A/B TESTING - BUTTON COLOR EXPERIMENT
' =========================================================
print ""
print "--- A/B Testing: Button Color ---"
' Get button color from experiment
' User will be consistently assigned to same variation
buttonColorResult = gb.evalFeature("button-color-test")
' buttonColorResult will contain:
' - value: the assigned color
' - variationId: 0 or 1 (which variation they're in)
' - source: "experiment" if in test, "defaultValue" if not
' - experimentId: ID of the experiment
buttonColor = buttonColorResult.value
if buttonColorResult.source = "experiment"
print "User in A/B test (Variation " + Str(buttonColorResult.variationId) + ")"
print "Button color: " + Str(buttonColor)
else
print "Using default button color: " + Str(buttonColor)
end if
' Store for later conversion tracking
m.global.addFields({
currentButtonColor: buttonColor,
buttonExperimentId: buttonColorResult.experimentId
})
' =========================================================
' A/B TESTING - PROGRESSIVE ROLLOUT
' =========================================================
print ""
print "--- Progressive Rollout: New Video Player ---"
' This feature gradually rolls out to percentage of users
' based on consistent hash of user ID
useNewPlayer = gb.isOn("new-video-player-rollout")
if useNewPlayer
print "✓ Using new video player (progressive rollout)"
m.player = CreateObject("roSGNode", "NewVideoPlayer")
else
print "✗ Using legacy video player"
m.player = CreateObject("roSGNode", "LegacyVideoPlayer")
end if
' =========================================================
' COMPLEX TARGETING - MULTI-CONDITION
' =========================================================
print ""
print "--- Complex Targeting: Premium US Users Only ---"
' This feature requires multiple conditions:
' - subscription = "premium" OR "enterprise"
' - country = "US"
' - accountAge > 30 days
if gb.isOn("vip-support-exclusive")
print "✓ VIP support available"
ShowVIPSupport()
else
print "✗ VIP support not available for this user"
end if
' =========================================================
' FEATURE VALUE WITH COMPLEX OBJECT
' =========================================================
print ""
print "--- Complex Feature Configuration ---"
playerConfig = gb.getFeatureValue("player-configuration", {
autoplay: false,
quality: "1080p",
bitrateLimit: 10000,
enableSubtitles: true,
enableDolbyVision: false,
cacheSize: 500
})
print "Player settings:"
print " Autoplay: " + Str(playerConfig.autoplay)
print " Quality: " + playerConfig.quality
print " Bitrate limit: " + Str(playerConfig.bitrateLimit) + " Kbps"
print " Subtitles: " + Str(playerConfig.enableSubtitles)
print " Dolby Vision: " + Str(playerConfig.enableDolbyVision)
print " Cache size: " + Str(playerConfig.cacheSize) + " MB"
' Apply to player
m.player.autoplay = playerConfig.autoplay
m.player.quality = playerConfig.quality
m.player.bitrateLimit = playerConfig.bitrateLimit
' =========================================================
' CONVERSION TRACKING (Manual)
' =========================================================
' When user completes an action (like purchase):
' You track it manually with the experiment context
end sub
function ShowAdvancedAnalytics()
print "Showing advanced analytics dashboard..."
end function
function ShowVIPSupport()
print "Showing VIP support options..."
end function
sub SendAnalyticsEvent(event as object)
' Send event to your analytics service
' Example: send to Roku analytics, Firebase, Mixpanel, etc.
print "[ANALYTICS] Event: " + event.event
end sub- Download
GrowthBook.brs - Copy to your channel's
source/directory - Initialize in your main scene or component
cd your-roku-channel
git submodule add https://github.com/growthbook/growthbook-roku.git lib/growthbookThen reference the SDK:
' In your main.brs
' Roku will automatically include all .brs files from source/| Option | Type | Description |
|---|---|---|
apiHost |
string | GrowthBook API host (default: https://cdn.growthbook.io) |
clientKey |
string | Required - Your SDK client key from GrowthBook |
decryptionKey |
string | Optional - Decrypt encrypted feature payloads |
attributes |
object | User attributes for targeting (e.g., {id: "user-123", premium: true}) |
trackingCallback |
function | Callback fired when user is placed in an experiment |
enableDevMode |
boolean | Enable verbose logging for debugging |
Load features from GrowthBook API. Returns true on success.
if gb.init()
print "Features loaded successfully"
end ifCheck if a boolean feature flag is enabled.
if gb.isOn("dark-mode") then
applyDarkTheme()
end ifGet the value of a feature flag with a fallback.
theme = gb.getFeatureValue("theme-color", "#0000FF")
maxItems = gb.getFeatureValue("max-items", 20)Update user attributes for targeting.
gb.setAttributes({
id: userId,
subscription: "premium",
country: "US"
})Get detailed feature evaluation result.
result = gb.evalFeature("pricing-test")
print result.value ' The feature value
print result.source ' Where it came from: "defaultValue", "force", "experiment"
print result.on ' Boolean: is feature "on"See the examples/ directory for complete working examples:
simple_flag.brs- Basic feature flag usageexperiments.brs- A/B testing with trackingtargeting.brs- Advanced audience targeting
Use the JavaScript mock to validate core logic:
npm testnpm install -g @rokucommunity/bsc
npm run lintexport ROKU_DEV_TARGET=192.168.1.100
export ROKU_DEV_PASSWORD=your-password
./scripts/deploy.shSee TESTING.md for comprehensive testing guide.
Benchmarks on Roku Ultra (2023):
| Operation | Time | Notes |
|---|---|---|
| SDK Initialization | ~50ms | Without network call |
| Feature Load (API) | 500-2000ms | First load from network |
| Feature Evaluation | <1ms | Cached, in-memory |
| Experiment Evaluation | <2ms | With hashing & targeting |
Memory Usage: ~150KB total (SDK + cached features)
Works on all Roku devices:
- ✅ Roku Ultra
- ✅ Roku Streaming Stick
- ✅ Roku Express
- ✅ Roku TV
- ✅ Roku Premiere
- ✅ Legacy Roku devices (Roku 2/3)
Minimum Roku OS: 9.0+
- No Server-Sent Events (SSE) streaming support (Roku limitation)
- No Visual Editor experiments (SceneGraph only)
- AES decryption requires
roEVPCiphercomponent (Roku OS 9.2+) - Network requests are asynchronous only
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
- Fork and clone the repo
- Make changes to
source/GrowthBook.brs - Test with
npm test - Deploy to Roku device for integration testing
- Submit a pull request
# Install dependencies
npm install
# Run unit tests
npm test
# Validate syntax
npm run lint
# Deploy to test device
npm run deployQ: Does this work with SceneGraph?
A: Yes! Initialize GrowthBook in your Scene component and access it throughout your SceneGraph tree.
Q: Can I use this offline?
A: Yes, features are cached after the first load. Pass features directly to skip network calls:
gb = GrowthBook({features: myFeaturesObject})Q: How do I target by device type?
A: Set device info as attributes:
deviceInfo = CreateObject("roDeviceInfo")
gb.setAttributes({
id: userId,
deviceType: deviceInfo.GetModel(),
osVersion: deviceInfo.GetVersion()
})Q: Does this support remote evaluation?
A: No, Roku SDK uses local evaluation only. Features are evaluated on the device.
MIT License - see LICENSE file for details.
- 📧 Email: [email protected]
- 💬 Slack: Join our community
- 🐛 Issues: GitHub Issues
Made with ❤️ by the GrowthBook team