JSPM

growthbook-roku

1.4.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2
  • Score
    100M100P100Q63002F
  • License MIT

Official GrowthBook SDK for Roku/BrightScript applications

Package Exports

  • growthbook-roku
  • growthbook-roku/source/GrowthBook.brs

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (growthbook-roku) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

GrowthBook Roku SDK

License: MIT BrightScript Roku Version Production Ready

Official GrowthBook SDK for Roku/BrightScript applications. Add feature flags and A/B testing to your Roku channels with a simple, lightweight SDK.

Current Version: v1.4.1 โ€” View Changelog | Integration Guide | Quick Start

Features

  • ๐Ÿš€ Lightweight - Core SDK is ~50KB, minimal memory footprint
  • โšก Fast - Feature evaluation in <1ms, optimized for Roku devices
  • ๐ŸŽฏ Full Spec Alignment - 401/401 native BrightScript tests passing (all 8 spec categories + smoke tests)
  • ๐Ÿงช A/B Testing - Run experiments with accurate traffic splits and holdout groups
  • ๐Ÿ”„ Consistent Bucketing - Same user always sees same variation (cross-platform)
  • ๐Ÿ“Š Analytics Ready - Tracking callbacks, feature usage events, and plugin system
  • ๐Ÿ”’ Encrypted Features - AES-128-CBC decryption for secure feature payloads (Roku OS 9.2+)
  • ๐Ÿ“Œ Sticky Bucketing - Persistent experiment assignments with in-memory and registry-based storage
  • ๐Ÿ”Œ Tracking Plugins - Extensible plugin architecture with built-in data warehouse integration
  • ๐Ÿ”ƒ On-Demand Refresh - Manual feature refresh via refreshFeatures()
  • ๐Ÿ“ฆ Ropm Support - Easy dependency management via ropm
  • ๐ŸŽจ No Dependencies - Pure BrightScript, works on all Roku devices (Roku 3+, OS 9.0+)

Quick Start

1. Installation

Manual Installation

Copy GrowthBook.brs from the source/ directory to your channel's source/ folder:

your-roku-channel/
โ”œโ”€โ”€ source/
โ”‚   โ”œโ”€โ”€ main.brs
โ”‚   โ””โ”€โ”€ GrowthBook.brs  โ† Add this file
โ””โ”€โ”€ manifest

If you are using ropm for dependency management:

ropm install growthbook-roku

2. Initialize

' 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

3. Use Feature Flags

' 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"
})

Important: Instance Management (SINGLETON PATTERN IS SUGGESTED)

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 function

Why This Matters

Creating 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

Best Practice: Singleton at App Level

' 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

Updating Attributes vs. Recreating Instance

' โœ… 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!

Detailed Examples

Example 1: Basic Configuration & Instantiation

'
' 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 "user@example.com"
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

Example 2: Setting Attributes & Evaluating Features

'
' 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: "john@example.com",         ' 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: "john@example.com",
        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

Example 3: Advanced Targeting & Experiment Tracking

'
' 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 (assigned to a variation)
        ' Use this to send data to your analytics platform (Firebase, Mixpanel, etc.)
        trackingCallback: sub(experiment, result)
            ' experiment: { key: string, variations: [] }
            ' result: { variationId: integer, value: dynamic, source: string, ruleId: string, experimentId: string }
            
            print "[EXPERIMENT TRACKED]"
            print "  Experiment ID: " + experiment.key
            print "  Variation ID: " + Str(result.variationId)
            
            ' Send to your analytics platform
            SendAnalyticsEvent({
                event: "experiment_exposed",
                experimentId: result.experimentId,
                variationId: result.variationId,
                ruleId: result.ruleId,
                value: result.value
            })
        end sub,

        ' Optional: Fired on every feature evaluation (not just experiments)
        onFeatureUsage: sub(featureKey, result)
            ' Use this for high-level usage tracking or debugging
            ' result: { value: dynamic, on: boolean, off: boolean, source: string, ... }
        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

Installation

  1. Download GrowthBook.brs
  2. Copy to your channel's source/ directory
  3. Initialize in your main scene or component

Option 2: Git Submodule

cd your-roku-channel
git submodule add https://github.com/growthbook/growthbook-roku.git lib/growthbook

Then reference the SDK:

' In your main.brs
' Roku will automatically include all .brs files from source/

Documentation

Configuration Options

Option Type Description
apiHost string GrowthBook API host (default: https://cdn.growthbook.io)
clientKey string Required - Your SDK client key from GrowthBook
decryptionKey string Decrypt encrypted feature payloads (Roku OS 9.2+)
attributes object User attributes for targeting (e.g., {id: "user-123", premium: true})
trackingCallback function Callback fired when user is placed in an experiment
onFeatureUsage function Callback fired on every feature evaluation
stickyBucketService object Sticky bucket storage service (in-memory or registry-based)
enableDevMode boolean Enable verbose logging for debugging

Core Methods

init() as boolean

Load features from GrowthBook API. Returns true on success.

if gb.init()
    print "Features loaded successfully"
end if

isOn(featureKey as string) as boolean

Check if a boolean feature flag is enabled.

if gb.isOn("dark-mode") then
    applyDarkTheme()
end if

getFeatureValue(featureKey as string, fallback as dynamic) as dynamic

Get the value of a feature flag with a fallback.

theme = gb.getFeatureValue("theme-color", "#0000FF")
maxItems = gb.getFeatureValue("max-items", 20)

setAttributes(attributes as object)

Update user attributes for targeting.

gb.setAttributes({
    id: userId,
    subscription: "premium",
    country: "US"
})

refreshFeatures() as boolean

Re-fetch features from the GrowthBook API. Useful for long-running apps.

if gb.refreshFeatures()
    print "Features refreshed"
end if

registerTrackingPlugin(plugin as object)

Register a tracking plugin to receive experiment and feature usage events.

plugin = GrowthBookTrackingPlugin({
    ingestorHost: "https://analytics.example.com",
    clientKey: "sdk_YOUR_KEY"
})
gb.registerTrackingPlugin(plugin)

evalFeature(featureKey as string) as object

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"
print result.ruleId      ' ID of the matching rule

Advanced Targeting

The SDK supports all GrowthBook advanced targeting rules:

  • Filters: Complex user segmentation using hash-based filtering.
  • Ranges (Intervals): Precise traffic control for experiments and rollouts.
  • Namespaces: Mutually exclusive experiments to prevent overlap.
  • Prerequisites: Dependent feature flags (flag A requires flag B).
  • Forced Variations: Global variation overrides for testing.
  • Sticky Bucketing: Persistent variation assignments across sessions.

These are handled automatically by the SDK based on your GrowthBook configuration.

Encrypted Features

Decrypt encrypted feature payloads from the GrowthBook API. Requires Roku OS 9.2+.

gb = GrowthBook({
    clientKey: "sdk_YOUR_KEY",
    decryptionKey: "your-base64-decryption-key",
    attributes: { id: "user-123" }
})
gb.init()  ' Encrypted features are decrypted automatically

Sticky Bucketing

Persist experiment assignments so users always see the same variation, even across sessions or after attribute changes.

' In-memory (for testing or single-session use)
sbs = GrowthBookInMemoryStickyBucketService()

' Registry-based (persists across app restarts)
sbs = GrowthBookRegistryStickyBucketService()

gb = GrowthBook({
    clientKey: "sdk_YOUR_KEY",
    attributes: { id: "user-123" },
    stickyBucketService: sbs
})
gb.init()

Tracking Plugins

Register plugins to receive experiment and feature usage events. The built-in GrowthBookTrackingPlugin sends batched events to a configurable HTTP endpoint.

plugin = GrowthBookTrackingPlugin({
    ingestorHost: "https://analytics.example.com",
    clientKey: "sdk_YOUR_KEY",
    batchSize: 10
})

gb = GrowthBook({
    clientKey: "sdk_YOUR_KEY",
    attributes: { id: "user-123" }
})
gb.init()
gb.registerTrackingPlugin(plugin)

' Events are batched and sent automatically
if gb.isOn("new-feature") then doSomething()

Examples

See the examples/ directory for complete working examples:

  • simple_flag.brs - Basic feature flag usage
  • experiments.brs - A/B testing with tracking
  • targeting.brs - Advanced audience targeting
  • coverage_rollout.brs - Progressive feature rollouts
  • array_targeting.brs - Tag-based targeting with arrays
  • version_targeting.brs - Semantic version targeting
  • weighted_experiments.brs - Custom traffic splits
  • consistent_hashing.brs - User bucketing consistency

Testing

Run All Tests

npm test

This runs both the JS validator (327 spec tests) and native BrightScript runner (401 tests) in sequence.

Individual Test Commands

Command What It Does
npm run test:validator JS logic validator against cases.json (fast, no device needed)
npm run test:native Native BrightScript tests via brs-engine (runs actual .brs code)
npm run test:channel Build test channel for brs-desktop on-device validation
npm run lint BrightScript syntax and lint checks

On-Device Testing (brs-desktop)

For testing in a full Roku-like runtime with SceneGraph:

npm run test:channel

Then open test-channel/pkg.zip in brs-desktop. See test-channel/README.md for details.

See docs/TESTING.md for the comprehensive testing guide.

Performance

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)

Browser Support

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+

Limitations

  • No Server-Sent Events (SSE) streaming support (Roku limitation)
  • No Visual Editor experiments (SceneGraph only)
  • Encrypted features require roEVPCipher component (Roku OS 9.2+)
  • Network requests are synchronous (no background polling)

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

Development Setup

  1. Fork and clone the repo
  2. Make changes to source/GrowthBook.brs
  3. Test with npm test
  4. Deploy to Roku device for integration testing
  5. Submit a pull request

Running Tests

npm install
npm test

FAQ

Q: 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.

Resources

License

MIT License - see LICENSE file for details.

Support


Made with โค๏ธ by the GrowthBook team