0.19.1 • Published 16 days ago

@optable/web-sdk v0.19.1

Weekly downloads
1
License
SEE LICENSE IN LI...
Repository
github
Last release
16 days ago

Optable Web SDK CircleCI

JavaScript SDK for integrating with an Optable Data Connectivity Node (DCN) from a web site or web application.

Contents

Installing

The Optable web SDK can be installed as a ES6 compatible npm module paired with module bundlers such as webpack or browserify, or can be loaded on a webpage directly by referencing a release build from the page HTML via a <script> tag.

:warning: CORS Configuration: Regardless of how you install the SDK, make sure that the Allowed HTTP Origins setting in the Optable DCN that you are integrating with contains the URL(s) of any web site(s) where the SDK is being used, otherwise your browser may block communication with the DCN.

npm module

If you're building a web application or want to bundle the SDK functionality with your own JavaScript, then npm is the recommended installation method. It pairs nicely with module bundlers such as webpack or browserify and exports types for applications using the typescript language and type checker. To use it simply install the package:

# latest stable release:
$ npm install @optable/web-sdk

And then simply import and use the OptableSDK class as shown in the Usage section below.

script tag

For simple integrations from your web site, you can load the SDK built for the browser from Optable's CDN via a HTML script tag. In production it's advised to lock your SDK bundle to a specific major version identified by vX or a specific minor version with vX.Y, while in development you may want to experiment with latest.

E.g. in development use the following in the <head> block of your HTML page:

<!-- Latest version for development -->
<script async src="https://cdn.optable.co/web-sdk/latest/sdk.js"></script>

Or in production:

<!-- v0 in production -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>

Note the presence of the async attribute, which instructs browsers to load the library asynchronously and not block the page from rendering.

Versioning

The SDK follows Semantic Versioning conventions. You can therefore expect that there will not be any breaking API changes if you are tracking a particular major version.

Domains and Cookies

By default, the Optable SDK makes use of a secure HTTP-only first-party browser cookie in order to anonymously identify browsers via a visitor ID, within the context of any web sites sharing an effective top-level domain plus one (eTLD+1) with the configured DCN host.

For example, if your website runs at www.customer.com or customer.com, then ideally your DCN will be configured to run at dcn.customer.com, and will read/write a first-party cookie at customer.com. The contents of the cookie will not be accessible to any third-party scripts. Finally, the cookie will have the SameSite=Laxattribute so that it is available on the first visit.

:warning: Optable Visitor ID Scope: The visitor ID configured by the Optable DCN will be unique to a browser only within the top-level domain that the DCN shares with the calling web site.

LocalStorage

In cases where it is not practical or possible to configure your DCN to run on the same effective top-level domain plus one (eTLD+1) as your website(s), then the default cookie-based transport that the SDK depends on will not work. Instead, you can configure the SDK to use browser LocalStorage. To switch to the LocalStorage based configuration, simply set the optional cookies parameter to false when creating your SDK instance. For example:

import OptableSDK from "@optable/web-sdk";

const sdk = new OptableSDK({ host: "dcn.customer.com", site: "my-site", cookies: false });

Note that the default is cookies: true and will be inferred if you do not specify the cookies parameter at all.

Using (npm module)

To configure an instance of OptableSDK integrating with an Optable DCN running at hostname dcn.customer.com, from a configured web site origin identified by slug my-site, you simply create an instance of the OptableSDK class exported by the @optable/web-sdk module:

import OptableSDK from "@optable/web-sdk";

const sdk = new OptableSDK({ host: "dcn.customer.com", site: "my-site" });

You can then call various SDK APIs on the instance as shown in the examples below. It's also possible to configure multiple instances of OptableSDK in order to connect to other (e.g., partner) DCNs and/or reference other configured web site slug IDs.

Note that all SDK communication with Optable DCNs is done over TLS. The only exception to this is if you instantiate the OptableSDK class with the insecure optional boolean parameter set to true. For example:

const sdk = new OptableSDK({ host: "dcn.customer.com", site: "my-site", insecure: true });

Note that production DCNs only listen to TLS traffic. The insecure: true option is meant to be used by Optable developers running the DCN locally for testing. See developer docs for other developer notes.

Identify API

To associate a user's browser with an authenticated identifier such as an Email address, optionally linked with other identifiers, such as your own vendor, publisher, or site-level PPID, you can call the identify API as follows:

const onSuccess = () => console.log("Identify API success!");
const onFailure = (err) => console.warn("Identify API error: ${err.message}");

const emailID = OptableSDK.eid("some.email@address.com");

// Identify with Email ID (eid):
sdk.identify(emailID).then(onSuccess).catch(onFailure);

// You can optionally link it with your own PPID in the same DCN identification call,
// simply pass a second argument to identify(). A custom PPID value can be sent to identify()
// after it is prepared with the OptableSDK.cid() helper:
const ppid = OptableSDK.cid("some.ppid");
sdk.identify(emailID, ppid).then(onSuccess).catch(onFailure);

The identify() method will asynchronously connect to the configured DCN and send IDs for resolution.

:warning: Client-Side Email Hashing: The OptableSDK.eid() helper will compute the SHA-256 hash of the Email address on the client-side and send the hashed value to the DCN. The Email address is not sent by the browser in plain text.

The frequency of invocation of identify is up to you, however for optimal identity resolution we recommended to call the identify() method on your OptableSDK instance on each page load while the user is authenticated, or periodically such as for example once every 15 to 60 minutes while the user is authenticated and actively using your site.

Profile API

To associate key value traits with a user's browser, for eventual audience assembly, you can call the profile API as follows:

const onSuccess = () => console.log("Profile API success!");
const onFailure = (err) => console.warn("Profile API error: ${err.message}");

const visitorTraits = {
  gender: "M",
  age: 44,
  favColor: "blue",
  hasAccount: true,
};

sdk.profile(visitorTraits).then(onSuccess).catch(onFailure);

The specified visitor traits are associated with the user's browser and can be matched during audience assembly.

Note that visitor traits are key value pairs and have type ProfileTraits:

type ProfileTraits = {
  [key: string]: string | number | boolean;
};

Targeting API

To get the targeting information associated by the configured DCN with the user's browser in real-time, you can call the targeting API as follows:

sdk
  .targeting()
  .then((response) => {
    console.log(`Audience targeting: ${targeting.audience}`);
    console.log(`User targeting: ${targeting.user}`);
  })
  .catch((err) => console.warn(`Targeting API Error: ${err.message}`));

On success, the resulting targeting data is typically sent as part of a subsequent ad call. Therefore we recommend that you either call targeting() before each ad call, or in parallel periodically, caching the resulting targeting data which you then provide in ad calls.

Caching Targeting Data

The targeting API will automatically cache resulting key value data in client storage on success. You can subsequently retrieve the cached key value data as follows:

const cachedTargetingData = sdk.targetingFromCache();
if (cachedTargetingData) {
  console.log(`Audience targeting: ${targeting.audience}`)
  console.log(`User targeting: ${targeting.user}`)
}

You can also clear the locally cached targeting data:

sdk.targetingClearCache();

Note that both targetingFromCache() and targetingClearCache() are synchronous.

Witness API

To send real-time event data from the user's browser to the DCN for eventual audience assembly, you can call the witness API as follows:

const onSuccess = () => console.log("Witness API success!");
const onFailure = (err) => console.warn("Witness API error: ${err.message}");

const eventProperties = {
  property_one: "some_value",
  property_two: 123,
  property_three: false,
};

sdk.witness("event.type.here", eventProperties).then(onSuccess).catch(onFailure);

The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly.

Note that event properties are key value pairs and have type WitnessProperties:

type WitnessProperties = {
  [key: string]: string | number | boolean;
};

Using (script tag)

For each SDK release, a webpack generated browser bundle targeting the browsers list described by npx browserslist "> 0.25%, not dead" can be loaded on a web site via a script tag.

As described in the Installation section above, in order to avoid having to block the rendering of the page, the recommended way to load the SDK via script tag is asynchronously with the async attribute. Therefore, to use the SDK you should take care to push your commands onto the window.optable.cmd array of functions, which are automatically executed by the SDK browser bundle once it has loaded.

The browser bundle exports the same OptableSDK constructor documented in the npm module section above in the optable window object, as optable.SDK

The following shows an example of how to safely initialize the SDK and dispatch an identify API request to a DCN, from an input element after the document was loaded.

<!-- Asynchronously load the SDK as early as possible: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>

<!-- Later in the page: -->
<script>
  // Setup stub that will get replaced once the SDK get loaded
  window.optable = window.optable || { cmd: [] };

  optable.cmd.push(() => {
    // At this point optable.SDK is available and can be used to create a new sdk instance.
    // That instance can be stored anywhere for later referencing.
    // One option is to keep it within the global optable object space.
    optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
  });

  // Now configure DOM content loaded event listener to dispatch identify() API:
  window.addEventListener("DOMContentLoaded", (event) => {
    optable.cmd.push(() => {
      // Fetch input on document load
      const emailInput = document.getElementById("email");
      optable.instance.identify(optable.SDK.eid(emailInput.value)).then(() => console.log("Identify API Success!"));
    });
  });
</script>

<input type="text" id="email" value="some.email@address.com" />

Integrating GAM360

The Optable Web SDK can fetch targeting data from a DCN and map it to be sent to Google Ad Manager 360 ad server account for real-time targeting. It's also capable of intercepting advertising events from the Google Publisher Tag and logging them to a DCN via the witness API.

Targeting key values

Loading the Optable SDK via a script tag on a web page which also uses the Google Publisher Tag, we can further extend the targeting example above to show an integration with a Google Ad Manager 360 ad server account.

It's suggested to load the GAM banner view with an ad even when the call to your DCN targeting() method raises an exception, as shown in the example below:

<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>

<!-- Google Publisher Tag (GPT) async load: -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>

<!-- Optable SDK, GPT, and targeting data initialization: -->
<script>
  window.optable = window.optable || { cmd: [] };
  window.googletag = window.googletag || { cmd: [] };

  // Init Optable SDK via command:
  optable.cmd.push(function () {
    optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
  });

  // Init GPT and disable initial ad load so that we can load targeting data first:
  googletag.cmd.push(() => {
    adSlot = googletag
      .defineSlot(...)
      .addService(googletag.pubads());

    googletag.pubads().enableSingleRequest();
    googletag.pubads().disableInitialLoad();
    googletag.enableServices();
  });
</script>

<!-- Placeholder DIV for adSlot... referenced by googletag.defineSlot() above: -->
<div id="div-gpt-ad-12345-0"></div>

<script>
  // Helper to load GAM ads with optional targeting data:
  var loadGAM = function (tdata = {}) {
    // Sets up page-level targeting in GAM360 GPT:
    window.googletag = window.googletag || { cmd: [] };
    googletag.cmd.push(function () {
      for (const [key, values] of Object.entries(tdata)) {
        googletag.pubads().setTargeting(key, values);
      }

      // Explicitly calls refresh() on googletag:
      googletag.pubads().refresh();
    });
  };

  // Call Optable DCN for targeting data and setup GPT page-level targeting, then
  // explicitly refresh GPT ads.
  //
  // NOTE: We load and refresh GPT ads without targeting data when there is an exception,
  // so that GAM ads are always loaded.
  optable.cmd.push(function () {
    optable.instance
      .targetingKeyValues()
      .then(loadGAM)
      .catch((err) => {
        loadGAM();
      });
  });

  googletag.cmd.push(() => {
    googletag.display(adSlot);
  });
</script>

Note the use of googletag.pubads().disableInitialLoad() in the above example. This will disable GAM ads from loading until the call to googletag.pubads().refresh() from the loadGAM() function.

Targeting key values from local cache

It's also possible to avoid disabling of the initial ad load by using the SDK's targetingKeyValuesFromCache() method instead as in the following example:

<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>

<!-- Google Publisher Tag (GPT) async load: -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>

<!-- Optable SDK, GPT, and targeting data initialization: -->
<script>
  window.optable = window.optable || { cmd: [] };
  window.googletag = window.googletag || { cmd: [] };

  // Init Optable SDK via command:
  optable.cmd.push(function () {
    optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
  });

  // Init GPT and disable initial ad load so that we can load targeting data first:
  googletag.cmd.push(() => {
    adSlot = googletag
      .defineSlot(...)
      .addService(googletag.pubads());

    // Attempt to load Optable targeting key values from local cache, then load GAM ads:
    optable.cmd.push(function () {
      const tdata = optable.instance.targetingKeyValuesFromCache();
      for (const [key, values] of Object.entries(tdata)) {
        googletag.pubads().setTargeting(key, values);
      }

      googletag.pubads().enableSingleRequest();
      googletag.enableServices();
    });
  });
</script>

<!-- Placeholder DIV for adSlot... referenced by googletag.defineSlot() above: -->
<div id="div-gpt-ad-12345-0"></div>

<script>
  // Call Optable DCN for targeting data which will update the local cache on success.
  optable.cmd.push(function () {
    optable.instance.targeting().catch((err) => {
      // Maybe log error
    });
  });

  googletag.cmd.push(() => {
    googletag.display(adSlot);
  });
</script>

Note that the above example fetches locally cached targeting key values and calls googletag.pubads().setTargeting() with them. Note also that the usual targeting() call is done as well, though its return value is ignored. This ensures that the local targeting cache is kept updated as activations are modified.

Witnessing ad events

To automatically capture GPT SlotRenderEndedEvent and ImpressionViewableEvent and send log data to your DCN using the witness API, simply install GPT event listeners on the SDK instance as follows:

<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<script>
  window.optable = window.optable || { cmd: [] };
  optable.cmd.push(function () {
    optable.instance.installGPTEventListeners();
  });
</script>

Note that you can call installGPTEventListeners() as many times as you like on an SDK instance, there will only be one set of registered event listeners per instance. Each SDK instance can register its own GPT event listeners.

A working example of both targeting and event witnessing is available in the demo pages.

Integrating Prebid

The Optable Web SDK can fetch targeting data from a DCN and prepare an audience taxonomy object similar to the one described in the prebid.js first party data documentation. The prebidORTB2FromCache() function returns the object from the targeting data stored by targeting() API calls in LocalStorage.

Seller Defined Audiences

The HTML code snippet below shows how prebidORTB2FromCache() can be used to retrieve targeting data from the LocalStorage administered by the Optable SDK, and write Seller Defined Audiences (SDA) into prebid.js which is also loaded into the page, using pbjs.mergeConfig({ ortb2: ortb2 }) as documented in the prebid.js first party data documentation. The targeting() API is also called in order to retrieve and locally store the latest matching activations from dcn.customer.com/my-site.

Note that prebid.js bidder adapters can subsequently retrieve the data from the global config.

An example of how to install the SDA data through pbjs is shown below. The districtMDMX bidder adapter is referenced, though the integration would look similar with any SDA compatible bidder adapters.

For a working demo showing a pbjs and GAM integrated together, see the demo pages section below.

<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>

<!-- Prebid.js lib async load: -->
<script async src="prebid.js"></script>

<!-- Initialize Optable SDK, and targeting call early when possible: -->
<script>
  window.optable = window.optable || { cmd: [] };

  // Init Optable SDK via command:
  optable.cmd.push(function () {
    optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
  });

  // Call Optable DCN for targeting data which will update the local cache on success.
  optable.cmd.push(function () {
    optable.instance.targeting().catch((err) => {
      // Maybe log error
    });
  });
</script>

<!-- Placeholder DIV for adSlot -->
<div id="div-gpt-ad-12345-0"></div>

<!-- Initialize prebid.js -->
<script>
  window.pbjs = window.pbjs || { que: [] };

  var PREBID_TIMEOUT = 3000;
  var FAILSAFE_TIMEOUT = 5000;

  var adUnits = [
    {
      code: "/22081946781/web-sdk-demo/box-ad",
      mediaTypes: {
        banner: {
          sizes: [
            [250, 250],
            [300, 250],
            [200, 200],
          ],
        },
      },
      bids: [
        {
          bidder: "districtmDMX",
          params: {
            dmxid: "/22081946781/web-sdk-demo/box-ad",
            memberid: "102034",
          },
        },
      ],
    },
  ];

  function initAdserver() {
    if (pbjs.initAdserverSet) return;
    pbjs.initAdserverSet = true;
    // ... etc ...
  }

  pbjs.que.push(function () {
    optable.cmd.push(function () {
      const ortb2 = optable.instance.prebidORTB2FromCache();
      pbjs.mergeConfig({ ortb2: ortb2 });

      // ... etc ...

      pbjs.requestBids({
        bidsBackHandler: initAdserver,
        timeout: PREBID_TIMEOUT,
      });
    });
  });

  setTimeout(function () {
    initAdserver();
  }, FAILSAFE_TIMEOUT);
</script>

Custom key values

For bidder adapters that do not support SDA, but that do support targeting private marketplace deals to key values, you can use a similar approach to the Google Ad Manager integration with key values from local cache. For example, for the IX bidder adapter and IX bidder-specific FPD, you can encode the targeting key values as shown below:

<script>
  // ...
  // prior to pbjs.requestBids():
  pbjs.que.push(function () {
    optable.cmd.push(function () {
      const tdata = optable.instance.targetingKeyValuesFromCache();
      var fpd = {};

      /*
       * Flatten targeting key=values from Optable SDK targeting cache
       * into a custom key value object, such that a key K with values
       * V1, V2, ... in the Optable SDK targeting cache is transformed
       * to look like:
       * {
       *   K + V1: 1,
       *   K + V2: 1,
       *   ...
       * }
       *
       * Note that + above indicates string concatenation.
       *
       * Optable DCNs have K configured to "optable" by default, so the
       * above would result in a custom key value "optable_audienceKeyword=1"
       * being set whenever the visitor is matched to the activated audience
       * specified by audienceKeyword by the DCN.
       */
      for (const [key, values] of Object.entries(tdata || {})) {
        for (const seg of values) {
          fpd[key + seg] = "1";
        }
      }

      pbjs.mergeConfig({
        ix: {
          firstPartyData: fpd,
        },
      });
    });

    pbjs.requestBids(...);
  });
</script>

Identifying visitors arriving from Email newsletters

If you send Email newsletters that contain links to your website, then you may want to automatically identify visitors that have clicked on any such links via their Email address.

Insert oeid into your Email newsletter template

To enable automatic identification of visitors originating from your Email newsletter, you first need to include an oeid parameter in the query string of all links to your website in your Email newsletter template. The value of the oeid parameter should be set to the SHA256 hash of the lowercased Email address of the recipient. For example, if you are using Braze to send your newsletters, you can easily encode the SHA256 hash value of the recipient's Email address by setting the oeid parameter in the query string of any links to your website as follows:

oeid={{${email_address} | downcase | sha2}}

The above example uses various personalization tags as documented in Braze's user guide to dynamically insert the required data into an oeid parameter, all of which should make up a part of the destination URL in your template.

Call tryIdentifyFromParams SDK API

On your website destination page, you can call a helper method provided by the SDK which will attempt to parse and validate a given query string parameter as EID (defaults to oeid), when found, it will automatically trigger a call to Optable's identify API.

For example:

<!-- Optable SDK async load: -->
<script async src="https://cdn.optable.co/web-sdk/v0/sdk.js"></script>
<script>
  window.optable = window.optable || { cmd: [] };
  optable.cmd.push(function () {
    optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });

    // Identify using a valid EID (email SHA256) "oeid" query string parameter.
    optable.instance.tryIdentifyFromParams();

    // Or if the EID is being passed through a "email_sha" query string
    // like https://www.mysite.com?origin=newsletter&email_sha=abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789&foo=bar
    // optable.instance.tryIdentifyFromParams("email_sha");

    // Optionally, you can provide a custom prefix as the second argument to tryIdentifyFromParams.
    // This prefix will be used in the constructed identifier, allowing you to capture a value
    // from the URL parameter even if it may not be a SHA256-hashed email.
    // For example, optable.instance.tryIdentifyFromParams("email_md5", "c2");
    // You can find a list of supported prefixes at https://docs.optable.co/optable-documentation/dmp/reference/identifier-types#type-prefixes
  });
</script>

Fetching Google Privacy Sandbox topics

To fetch Google Privacy Sandbox topics using the Optable SDK, you can use the getTopics method. This method asynchronously retrieves topics IDs and taxonomy versions from the Chrome browser. Alternatively, you can use the ingestTopics method. This method invokes getTopics and sends the retrieved topics to the Optable DCN under the trait "topics_api". See the Topics API dictionary for details.

It is recommended to call this method before making ad calls to ensure that the latest topics are available for targeting.

// Optable SDK async load:
<script async src="https://cdn.optable.co/web-sdk/latest/sdk.js"></script>
<script>
  window.optable = window.optable || { cmd: [] };
  optable.cmd.push(function () {
    optable.instance = new optable.SDK({ host: "dcn.customer.com", site: "my-site" });
    // Fetch Google Privacy Sandbox topics and send them to the Optable DCN
    optable.instance.ingestTopics();
  });
</script>

Demo Pages

The demo pages are working examples of both identify and targeting APIs, as well as an integration with the Google Ad Manager 360 ad server, enabling the targeting of ads served by GAM360 to audiences activated in the Optable DCN.

You can browse a recent (but not necessarily the latest) released version of the demo pages at https://demo.optable.co/. The source code to the demos can be found here. The demo pages will connect to the Optable demo DCN at sandbox.optable.co and reference the web site slug web-sdk-demo. The GAM360 targeting demo loads ads from a GAM360 account operated by Optable.

Note that the demo pages at https://demo.optable.co/ will by default rely on secure HTTP first-party cookies as described here. To see an example based on LocalStorage, see the index-nocookies variant here.

To build and run the demos locally, you will need Docker, docker-compose and make:

$ cd path/to/optable-web-sdk
$ make
$ docker-compose up

Then head to https://localhost:8180/ to see the demo pages. You can modify the code in each demo, then run make build and finally refresh the demo pages to see your changes take effect. If you want to test the demos with your own DCN, make sure to update the configuration (hostname and site slug) given to the OptableSDK (see webpack.config.js for the react example).

Note that using HTTP first-party cookies with a local instance of the demos pages pointing to an Optable DCN will not work because https://localhost:8180/ does not share the same top-level domain name .optable.co. We recommend using LocalStorage instead.

0.19.1

16 days ago

0.19.0

1 month ago

0.18.13

1 month ago

0.18.12

1 month ago

0.18.11

1 month ago

0.18.9

2 months ago

0.18.7

2 months ago

0.18.8

2 months ago

0.18.6

2 months ago

0.18.5

3 months ago

0.18.3

3 months ago

0.18.4

3 months ago

0.18.1

3 months ago

0.18.2

3 months ago

0.18.0

3 months ago

0.17.0

3 months ago

0.16.3

3 months ago

0.16.2

3 months ago

0.16.0

4 months ago

0.16.1

4 months ago

0.15.1

4 months ago

0.15.2

4 months ago

0.15.0

4 months ago

0.13.0

7 months ago

0.13.1

7 months ago

0.13.2

7 months ago

0.13.3

7 months ago

0.13.4

6 months ago

0.13.5

6 months ago

0.14.0

6 months ago

0.13.1-rc5

11 months ago

0.13.1-rc4

12 months ago

0.13.1-rc3

12 months ago

0.13.1-rc2

12 months ago

0.13.1-rc1

12 months ago

0.13.1-rc0

12 months ago

0.12.1

1 year ago

0.12.2

1 year ago

0.11.0

1 year ago

0.12.0

1 year ago

0.0.0-xp10

2 years ago

0.0.0-xp11

2 years ago

0.0.0-xp12

2 years ago

0.0.0-xp13

2 years ago

0.0.0-xp1

2 years ago

0.10.0

2 years ago

0.9.1

2 years ago

0.9.1-rc1

2 years ago

0.9.0

3 years ago

0.8.0

3 years ago

0.7.1

3 years ago

0.7.0

3 years ago

0.6.4

3 years ago

0.6.3

3 years ago

0.6.2

3 years ago

0.6.1

3 years ago

0.6.0

3 years ago

0.5.0

3 years ago

0.4.2

4 years ago

0.4.1

4 years ago

0.4.0

4 years ago

0.0.0-lucky3

4 years ago

0.0.0-lucky2

4 years ago

0.0.0-test

4 years ago

0.4.0-rc2

4 years ago

0.3.0

4 years ago

0.2.3

4 years ago

0.2.1

4 years ago

0.2.2

4 years ago

0.2.0

4 years ago

0.1.2

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago

0.1.0-rc3

4 years ago