Skip to content

Latest commit

 

History

History
 
 

Campaign

Campaign

Campaign is a simple tool for creation and management of banner campaigns on your web. It allows you to create banner campaigns without knowledge of HTML/CSS/JS, A/B test the banners and in combination with REMP Beam (for providing statistics) display and evaluate performance of your campaigns.

Admin (Laravel)

Campaign Admin serves as a tool for configuration of banners and campaigns. It's the place for UI generation of banners and definition of how and to whom display Campaigns.

When the backend is ready, don't forget to create .env file (use .env.example as boilerplate), install dependencies and run DB migrations:

# 1. Download PHP dependencies
composer install

# 2. Download JS/HTML dependencies
yarn install

# !. use extra switch if your system doesn't support symlinks (Windows; can be enabled)
yarn install --no-bin-links

# 3. Generate assets
yarn run all-dev // or any other alternative defined within package.json

# 4. Run migrations
php artisan migrate

# 5. Generate app key
php artisan key:generate

# 6. Create elastic structure for campaign stats
curl -XPUT -H "Content-Type: application/json" elastic:9200/commerce -d '{"mappings": {"_doc": {"properties": {"revenue": {"type": "double"}}}}}' 

# 7. Run seeders (optional)
php artisan db:seed

Dependencies

  • PHP ^7.1.3
  • MySQL ^5.7.8
  • Redis ^3.2

Admin integration with CMS/CRM

Javascript snippet

Include following snippet into the page to process campaigns and display banners. Update rempConfig object as needed.

Note: To automatically track banner events to BEAM Tracker, add also tracker property to rempConfig object. See BEAM README for details. The two snippets complement each other and can be combined into one big JS snippet including Campaign and Beam functionallity.

(function(win, doc) {
    function mock(fn) {
        return function() {
            this._.push([fn, arguments])
        }
    }
    function load(url) {
        var script = doc.createElement("script");
        script.type = "text/javascript";
        script.async = true;
        script.src = url;
        doc.getElementsByTagName("head")[0].appendChild(script);
    }
    win.remplib = win.remplib || {};
    var mockFuncs = {
        "campaign": "init",
        "tracker": "init trackEvent trackPageview trackCommerce",
        "iota": "init"
    };

    Object.keys(mockFuncs).forEach(function (key) {
        if (!win.remplib[key]) {
            var fn, i, funcs = mockFuncs[key].split(" ");
            win.remplib[key] = {_: []};

            for (i = 0; i < funcs.length; i++) {
                fn = funcs[i];
                win.remplib[key][fn] = mock(fn);
            }
        }
    });
    // change URL to location of CAMPAIGN remplib.js
    load("http://campaign.remp.press/assets/lib/js/remplib.js");
})(window, document);

var rempConfig = {
    // UUIDv4 based REMP BEAM token of appropriate property
    // (see BEAM Admin -> Properties)
    // required if you're using REMP segments
    token: String,
    
    // optional, identification of logged user
    userId: String,
    
    // optional, flag whether user is currently subscribed to the displayed content 
    userSubscribed: Boolean,
    
    // optional, this is by default generated by remplib.js library and you don't need to override it
    browserId: String,
    
    // optional, controls where cookies (UTM parameters of visit) are stored
    cookieDomain: ".remp.press",

    // required, Campaign specific options
    campaign: {
        // required, URL host of REMP Campaign
        url: "http://campaign.remp.press",
        
        // Additional params that will be appended links within displayed banner
        //
        // Key represents variable name, value should be defined as callback returning string response.
        // Following example will be appended as "&foo=bar&baz=XXX".
        // If the value is not function, remplib validation will throw an error and won't proceed further.
        bannerUrlParams:  {
            "foo": function() { return "bar" },
            "baz": function() { return "XXX" }
        },
        
        variables: {
            // variables replace template placeholders in banners,
            // e.g. {{ email }} -> [email protected]
            //
            // the callback doesn't pass any parameters, it's required for convenience and just-in-time evaluation
            //
            // missing variable is translated to empty string
            email: {
                value: function() {
                    return "[email protected]"
                }
            },
        }
    }
    
    // if you want to automatically track banner events to BEAM Tracker,
    // add also rempConfig.tracker property
    //
    // see REMP BEAM README.md
    
};
remplib.campaign.init(rempConfig);
Segment integration

To determine who to display a campaign to, Campaign is dependent on user segments - effectively lists of user/browser IDs which should see a banner. You can register as many segment providers as you want, the only condition is that the providers should work with the same user-base (one user ID has to always point to the) same user.

The implementation is required to implement App\Contracts\SegmentContract interface.

All registered implementations are hidden behind facade of SegmentAggregator. This facade is then used to display available segments in configuration listings and to evaluate actual members of segments during campaign runtime.

If you want to link the Campaign to your own system, these are the methods to implement:

  • provider(): string: Uniquely identifies segment provider among other segment providers. This is internally required to namespace segment names in case of same segment name being used in multiple segment sources.

    return "my-provider"; 
  • list(): Collection: Returns collection of all segments available for this provider. The structure of response is:

    return [
        [
            'name' => String, // user friendly label
            'provider' => String, // should be same as result of provider()
            'code' => String, // machine friendly name, slug
            'group' => [
                'id' => Integer, // ID of segment group
                'name' => String, // user friendly label of group
                'sorting' => Integer // sorting index; lower the number, sooner the group appears in the list 
            ]
        ],
    ];
  • users(CampaignSegment $segment): Collection: Returns list of user IDs belonging to the segment.

    • $segment: Instance of CampaignSegment holding the reference to segment used in a campaign.

    The response is than expected to be collection of integers/strings representing user IDs:

    return collect([
        String,
        String,
        // ...
    ])
  • checkUser(CampaignSegment $campaignSegment, string $userId): bool: Checks whether given userId belongs to the provided campaignSegment. This can either be done against external API (if it's prepared for realtime usage) or against cached list of user IDs - it's internal to the implementation. If the implementation doesn't support user-based segments, return false.

  • checkBrowser(CampaignSegment $campaignSegment, string $browserId): bool: Checks whether given browserId belongs to the provided campaignSegment. This can either be done against external API (if it's prepared for realtime usage) or against cached list of browser IDs - it's internal to the implementation. If the implementation doesn't support browser-based segments, return false.

  • cacheEnabled(): bool: Flag whether the segments of this provider used in active campaigns should be cached by the application or not. If true, Campaign will every hour request users() for each active Campaign and stores the collection of user IDs to the application cache.

  • getProviderData(): Provider can pass arbitrary data back to the user's browser. This can be leveraged to cache browser/user related information without affecting the backend system. This data can be then altered by JS in the frontend application (if needed). All provided data is passed back to the server on each campaign/showtime request - on each banner display attempt.

    Note: We use this kind of caching to count number of trackable events directly in browser. Segment rules based on these events are then evaluated based on the data provided in cache instead of hitting database. This method dramatically improved performance of REMP Beam event database (Elasticsearch).

  • setProviderData($providerData): void: Complementary providerData method to store back the data provided by frontend application in incoming request.

When implemented, create a segment provider providing the instance of your implementation. The provider should set \App\Contracts\SegmentAggregator::TAG to the class so your implementation would get also registered to the SegmentAggregator.

Schedule

For application to function properly you need to add Laravel's schedule running into your crontab:

* * * * * php artisan schedule:run >> storage/logs/schedule.log 2>&1

Laravel's scheduler currently includes:

CacheSegmentJob:

  • Triggered hourly and forced to refresh cache segments.

Queue

For application to function properly, you also need to have Laravel's queue worker running as a daemon. Please follow the official documentation's guidelines.

php artisan queue:work

Laravel's queue currently includes

CacheSegmentJob:

  • Triggered when campaign is activated.
  • Trigerred when cached data got invalidated and need to be fetched again.

If the data are still valid, job doesn't refresh them.

MaxMind - GeoIP2 Lite

This product includes GeoLite2 data created by MaxMind, available from http://www.maxmind.com.