@cro-dev/ab-testing-toolkit v1.0.6
A/B Testing Toolkit
Powered by CROdev - A/B Test Development Made Easy
Are you looking for a complete boilerplate for your A/B test development? Try out our A/B Testing Boilerplate.
Table of Contents
Getting started
This toolkit is designed to help you creating your A/B tests. Have fun using it!
Questions or feedback? max@cro-dev.de
Install
Install with npm:
npm install ab-testing-toolkitInstall with yarn:
yarn add ab-testing-toolkitUsage Example
import { elem, qs, setClickEvent } from 'ab-testing-toolkit';
elem('.example', (example) => {
if(!example) return;
// do something...
example.map(entry => {
setClickEvent(qs('image', entry), 'EXERPIMENT-PREFIX', () => {
// add API to push a custom conversion goal
});
});
});Helper Functions
elem
The elem function is using document.querySelector() internally.
The elem function continuously checks for the presence of DOM elements matching a specified CSS selector or evaluates a custom condition provided as a function. It executes a callback when the condition is met or times out after 10 seconds.
Parameters
- selector (string | function): A CSS selector string to query elements from the DOM. Alternatively, a function that evaluates to true when the desired condition is met.
- callback (function): A function called when either:
- Matching elements are found, and the result is passed as an argument.
- The timeout of 10 seconds is reached without finding any elements or satisfying the condition, in which case false is passed to the callback.
Behavior
- If selector is a string, it is converted into a function that queries elements using qsa(selector).
- The function iteratively checks for the presence of elements or evaluates the condition every 33 milliseconds.
- If elements are found or the condition returns a truthy value, the callback is invoked with the result.
- If no elements are found within the 10-second window, the callback is called with false.
Usage Example
elem('.my-class', elements => {
if (elements) {
console.log('Found elements:', elements);
} else {
console.log('No elements found within the timeout period.');
}
});Using a Custom Condition
elem(() => document.querySelector('#special-element') !== null, result => {
if (result) {
console.log('Special element is now available.');
} else {
console.log('Element not found within the timeout period.');
}
});Notes
- The timeout period is hard-coded to 10 seconds.
- The polling interval is 33 milliseconds.
elemSync
elemSync is an asynchronous function that waits for DOM elements to be available by utilizing a provided elem function. It returns a Promise that resolves with the elements matching the specified CSS selector.
Parameters
- selector (string): A CSS selector used to query the desired DOM elements.
Return Value
- Returns a Promise that resolves to: elements: The elements found by the elem function, based on the provided selector.
Usage Example
(async () => {
const elements = await elemSync(".my-class");
if(!elements) return;
console.log('Elements found:', elements);
})();elemSync('.my-class').then(elements => {
if (elements) {
console.log('Elements found:', elements);
} else {
console.log('No elements found.');
}
});Assumptions
The function assumes that elem is a pre-existing function that accepts a CSS selector and a callback, providing matched elements asynchronously.
Notes
The code comment about resolving with false after 10 seconds appears to be misleading since the function does not implement such logic. If this behavior is required, it needs to be explicitly added.
share
If you want to share JavaScript code between multiple files, you can use the share function.
The share function adds a new method (func) to the global AB_TESTING_TOOLKIT object under the specified name (name). This function is typically used for A/B testing scenarios to dynamically extend the toolkit with custom functions. After adding the function, it logs a message to the console and executes an optional exec function, if defined.
Parameters
- name (string): The name of the method being added to the AB_TESTING_TOOLKIT object. This name will be used as a key under which the func will be stored.
- func (function): The function that will be associated with the specified name and added to the AB_TESTING_TOOLKIT object. This function can be any custom logic you wish to add to the toolkit.
Usage Example
share("myCustomFunction", () => {
console.log("This is my custom function for A/B testing!");
});The share function returns window.AB_TESTING_TOOLKIT, if you want to use it directly.
// Calling the added function
window.AB_TESTING_TOOLKIT.myCustomFunction();Notes
- The function is useful in scenarios like A/B testing frameworks where new functionalities or methods need to be dynamically added to a global toolkit.
- The exec function, if defined, will be invoked after the function is added. This is useful for triggering additional actions after extending the toolkit.
- The AB_TESTING_TOOLKIT object is intended to store shared functionality that can be accessed globally throughout the application.
exec
The exec function manages and executes functions stored in the AB_TESTING_TOOLKIT global object. It can immediately execute a specified function if it exists or queue the function for execution later. The function also provides a mechanism to execute all queued functions in the toolkit.
Parameters
- name (string): The name of the function in AB_TESTING_TOOLKIT to be executed. If name is omitted, all queued functions are executed instead.
- params (any): Parameters to pass to the function when it is executed. This can be any type of data supported by JavaScript, including objects, strings, numbers, or arrays.
Usage Example
// Immediately execute an existing function
exec("sampleFunction", { test: true });
// Execute all queued functions
exec();Notes
- The exec function allows delayed execution for functions that may not yet be defined when the call is made.
- It uses a queue system (AB_TESTING_TOOLKIT.exec) to manage functions and parameters, ensuring they are called when available.
ready
The ready function is using the DOMContentLoaded Event.
The ready function ensures that a specified callback function is executed as soon as the DOM content has fully loaded, making it a more reliable alternative to directly placing code inside the DOMContentLoaded event listener. It checks if the document has already finished loading or if it needs to wait for the DOMContentLoaded event to fire.
Parameters
- callback (function): A function to be executed once the DOM is ready. This function will be invoked either immediately if the DOM is already loaded or when the DOMContentLoaded event is triggered.
Usage Example
ready(() => {
console.log("The DOM is fully loaded and ready to be manipulated.");
// Your code to interact with the DOM here
});Notes
- This function is useful to ensure that your JavaScript code interacts with the DOM only after it has fully loaded, preventing errors related to manipulating elements that are not yet available.
- It supports both modern browsers and older versions of Internet Explorer (which use attachEvent instead of addEventListener).
- The DOMContentLoaded event is fired once the HTML is completely loaded and parsed, without waiting for images or other resources, which makes it faster than waiting for the load event.
punshout
The punshout function monitors the browser window’s width and executes a specified callback when the width is smaller than or equal to a provided threshold (width). It also ensures that the callback is only triggered once and listens for window resize events to recheck the condition if necessary.
Parameters
- width (number): The width threshold (in pixels). The callback will be triggered when the window width is smaller than or equal to this value.
- prefix (string): A prefix that is used to prevent multiple event listeners from being added. The function checks for the existence of a property (windowprefix + '_resize') to ensure that the resize event listener is only added once.
- callback (function): A function to be executed once the window width becomes smaller than or equal to the specified width. The function is executed only once and will not trigger again unless the condition changes and is met again.
Usage Example
The punshout function executes your callback function, if the breakpoint is lower as specified, in the example 768px.
punshout(768, 'myPrefix', () => {
console.log("The window width is now smaller than or equal to 768px!");
});Notes
- The callback function is executed only once when the window width is smaller than or equal to the specified width threshold.
- The prefix is used to ensure that the event listener is only added once to prevent unnecessary duplication of event listeners, which could lead to performance issues.
- The
getWidth()function is used to retrieve the current window width. - The function uses the resize event listener to recheck the window width when the window is resized.
pushMetric
You can use the pushMetric function for sending goals or segments (unique and multiple).
Note: Please add your respective API call to your A/B testing tool before using this function. Examples for Kameleoon, Optimizely and VWO are already included.
The pushMetric function logs and optionally tracks unique conversion metrics during A/B testing experiments. It stores metric IDs in a global array (window.AB_TESTING_TOOLKIT) and provides placeholders for integrating with third-party analytics tools.
Parameters
- id (string): The identifier for the metric or goal to be tracked.
- unique (boolean) optional: If true, the function ensures the metric is only tracked once by checking if the id is already present in window.OE_METRICS. Defaults to false.
Usage Example
Track a Metric Once
pushMetric('goal_123', true);Tracks the metric with ID goal_123 only if it hasn’t been tracked before.
Track the Same Metric Multiple Times
pushMetric('goal_123');Allows repeated tracking of the same metric goal_456.
Notes
- Integration Required: The function contains placeholder code that must be replaced with actual API calls for analytics tools.
- Error Handling: Catches and logs any errors during the process, ensuring robust behavior.
- Global State: Uses window.AB_TESTING_TOOLKIT to maintain state, which persists across function calls.
pushHistory
The pushHistory function updates the browser’s history stack by adding a new entry with a modified URL path that includes a hash fragment (#step). This function is useful for tracking navigation steps or states within a single-page application (SPA) without reloading the page.
Parameters
- step (string): A string representing the step or identifier to be appended as a hash fragment to the current URL. If step is falsy (e.g., null, undefined, or an empty string), the function exits without making changes.
Usage Example
pushHistory('myStep');Notes
- The function does not cause a page reload, making it suitable for SPAs or dynamic user interfaces that track navigation or states.
- Using window.history.pushState() allows developers to maintain a consistent navigation experience while managing state changes within the app.
- The browser back and forward buttons will navigate through these history entries as expected.
- To handle changes when the user navigates using the browser’s history buttons, you may need to listen for the popstate event:
window.addEventListener("popstate", event => {
console.log("History changed:", event.state);
});setPreload
The setPreload function preloads a list of image assets by creating new Image objects and setting their src attributes to the provided image URLs. This technique ensures that images are downloaded and cached by the browser before they are needed, improving the loading speed and user experience when these images are displayed.
Parameters
-images (Array): An array of image URLs to be preloaded.
Usage Example
setPreload(['https://www.example-image.com/image-01.png', 'https://www.example-image.com/image-02.png']);Notes
- Performance Improvement: Preloading images can help reduce loading delays for image-heavy websites or applications by ensuring assets are ready before they are requested for display.
- Browser Support: The method used in this function (new Image()) is widely supported across modern browsers.
- Caution: Preloading a large number of high-resolution images may impact network performance and device memory usage.
Wrapper Functions
qs
The qs function is just a wrapper for document.querySelector().
The qs function is a utility for selecting a single DOM element using a CSS selector. It simplifies element selection by optionally allowing searches within a specific parent element.
Parameters
- selector (string): A CSS selector string used to identify the desired element.
- parent (Element | Document) Optional: The element or document context within which the selector will be applied. Defaults to document if not provided.
Return Value
- Returns the first element within the specified parent that matches the given selector.
- If no matching element is found, it returns null.
Usage Example
const button = qs('.submit-button');
if (button) {
button.addEventListener('click', () => console.log('Button clicked!'));
}Selecting an element within a specific parent container
onst container = document.getElementById('form-container');
const inputField = qs('input[name="username"]', container);
if (inputField) {
inputField.focus();
}qsa
The qsa function is just a wrapper for document.querySelectorAll().
The qsa function is a utility for selecting multiple DOM elements using a CSS selector. It allows querying within a specific parent element or the entire document.
Parameters
- selector (string): A CSS selector string used to identify the desired elements.
- parent (Element | Document) Optional: The element or document context within which the selector will be applied. Defaults to document if not provided.
Return Value
- Returns a NodeList containing all elements that match the given selector within the specified parent context.
- If no elements match, an empty NodeList is returned.
Usage Example
const buttons = qsa('.button-class');
buttons.forEach(button => {
button.addEventListener('click', () => console.log('Button clicked!'));
});Selecting elements within a specific parent container
const container = document.getElementById('nav-container');
const links = qsa('a', container);
links.forEach(link => {
console.log('Link text:', link.textContent);
});addClass
The addClass function adds a specified CSS class to a given HTML element if the class is not already present. It ensures that the operation only proceeds when the element and class name are valid.
Parameters
- element (HTMLElement): The target HTML element to which the class should be added.
- selector (string): The CSS class name to add to the element.
Return Value
- Returns true if the class is successfully added.
- Returns undefined if the operation is not performed due to missing or invalid input.
Usage Example
const button = qs('.my-button');
addClass(button, 'active');Adds the class active to the element with the class my-button.
removeClass
The removeClass function removes a specified CSS class from an HTML element if the class is present. It ensures that the operation only proceeds when valid input is provided.
Parameters
- element (HTMLElement): The target HTML element from which the class should be removed.
- selector (string): The CSS class name to be removed from the element.
Return Value
- Returns true if the class is successfully removed.
- Returns undefined if the operation is not performed due to invalid or missing input.
Usage Example
const button = qs('.my-button');
removeClass(button, 'active');hasClass
The hasClass function checks whether a specified CSS class is present on a given HTML element. It ensures that the input parameters are valid before performing the check.
Parameters
- element (HTMLElement): The target HTML element to be checked for the specified class.
- selector (string): The CSS class name to check for.
Return Value
- Returns true if the element contains the specified class.
- Returns false if the class is not present or if the input is invalid.
Usage Example
const button = qs('.my-button');
if (hasClass(button, 'active')) {
console.log('Button is active!');
}addPrefix
The addPrefix function is a utility that applies a CSS class to the root element of a document. It is typically used to add a global modifier or theme-related class for styling purposes.
Parameters
- selector (string): The name of the CSS class to be added to the element.
Return Value
- The function does not explicitly return a value but delegates to addClass, whose behavior depends on its implementation.
Usage Example
addPrefix('dark-mode');After executing this code, the element will look like this if dark-mode is successfully applied:
<html class="dark-mode">Notes
The function directly modifies the root element (document.documentElement), affecting all styles tied to the specified class.
removePrefix
The removePrefix function removes a specified CSS class from the root element of a document. It is typically used to reset or revert global styles applied to the document.
Parameters
- selector (string): The name of the CSS class to be removed from the element.
Return Value
- The function does not explicitly return a value but delegates to removeClass, whose behavior depends on its implementation.
Usage Example
removePrefix('dark-mode');After executing this code, if the class was previously applied, the element would change as follows:
<html class="">Notes
The function directly modifies the root element (document.documentElement), affecting all styles tied to the specified class.
setClickEvent
The setClickEvent function adds a click event listener to one or more elements specified by a CSS selector or direct reference. It tracks clicks by either invoking a callback function or pushing a metric event when the element is clicked. It prevents duplicate event bindings by marking elements using a data attribute.
Parameters
- selector (string | HTMLElement): A CSS selector string or a direct reference to an HTML element to which the click event listener will be added.
- prefix (string): A unique identifier used as a data attribute (data-prefix) to ensure that the click listener is not added multiple times to the same element.
- callback (function | string): If a function, it is executed when the element is clicked. If a string, it is assumed to be a metric identifier passed to pushMetric() for analytics tracking.
Usage Example
setClickEvent('.btn-submit', 'trackClick', event => {
console.log('Button clicked!', event.target);
});Adds a click listener to elements with the class .btn-submit and logs a message when clicked.
setClickEvent('.btn-track', 'metricTrack', 'goal_123');Tracks a metric with ID goal_123 when any element with the class .btn-track is clicked.
Notes
- Duplicate Prevention: Ensures event listeners are not added multiple times by using the data-prefix attribute.
- Callback Flexibility: Supports both custom callback functions and analytics tracking.
- Dependency: Relies on the elem() utility function for element selection. Ensure elem() is available for this function to work correctly.
Util Functions
scrollTo
The scrollTo function smoothly scrolls the page to a specified vertical position (pixel) over a given duration (duration). It uses an easing function (easeInOutQuad) to create a smooth scrolling animation.
Parameters
- pixel (number): The target vertical scroll position (in pixels) to scroll to. This value can be any integer representing the desired scroll position from the top of the page.
- duration (number): The duration (in milliseconds) over which the scroll animation should take place. This determines how quickly the page will scroll to the specified position.
Easing Function (easeInOutQuad)
The easing function Math.easeInOutQuad provides a smooth, non-linear transition, starting slowly, accelerating in the middle, and then decelerating towards the end. This creates a more natural, visually pleasing scroll animation.
Usage Example
const scrollToPosition = 2000;
const scrollSpeed = 500;
scrollTo(scrollToPosition, scrollSpeed);Notes
- The scroll position is updated for both document.documentElement and document.body to ensure compatibility across different browsers and document structures.
- The function creates a smooth scroll effect using the easeInOutQuad easing method.
- The scroll occurs asynchronously, with a timeout of 20ms between each update to the scroll position (increment).
- To stop or interrupt the scroll, you would need to add external logic to cancel the timeouts or animations.
Potential Improvements
If you need to ensure better performance, consider using requestAnimationFrame instead of setTimeout, which can optimize the animation for smoother performance, especially on devices with lower processing power.
inViewport
Note: If you are using an SPA framework or library and your element is suddenly no longer observed, an adjustment to the function may be necessary. I have marked a TODO here, which you can activate as required.
The inViewport function is used to monitor an HTML element’s visibility in the viewport as the user scrolls. When the element enters or exits the viewport, the provided callback function is invoked. This can be useful for triggering certain actions when an element becomes visible, such as lazy loading content or triggering animations.
Parameters
- selector (string | HTMLElement):
- If a string is provided, it should be a CSS selector that will be used to find the element within the document.
- If an HTMLElement is provided, the function directly uses it to check for visibility.
- prefix (string): A unique string used as a prefix to avoid duplicate event listeners. It is also used to mark elements with a custom data attribute to track if the element has already been processed.
- callback (function): A function that will be invoked whenever the visibility status of the element changes (when it enters or exits the viewport). It is passed the current visibility status (true if in the viewport, false if not).
Return Value
The function does not return any value. Instead, it triggers the callback whenever the element enters or exits the viewport.
Usage Example
inViewport('.my-element', 'elementPrefix', status => {
if (status) {
console.log('The element is now in the viewport!');
} else {
console.log('The element is no longer in the viewport!');
}
});This will monitor the visibility of an element with the class .my-element. The callback will log a message when the element enters or exits the viewport.
const myElement = qs('.my-element');
inViewport(myElement, 'elementPrefix', (status) => {
if (status) {
console.log('The element is now in the viewport!');
} else {
console.log('The element is no longer in the viewport!');
}
});This will monitor the visibility of the myElement directly, using the provided HTMLElement.
Notes
- The function adds an event listener for the scroll event on the window, which may trigger frequently. If performance becomes an issue, consider throttling or debouncing the scroll event handler.
- This function is particularly useful in Single Page Applications (SPAs) or dynamic content sites, where elements may be loaded dynamically and visibility checks need to be performed after the element is rendered.
isElementInViewport
The isElementInViewport function checks if an HTML element is currently visible within the viewport. It determines whether the element is fully or partially visible, and also checks if the element is positioned below the top of the viewport.
Parameters
- element (HTMLElement): The target HTML element to check for visibility within the viewport.
Return Value
Returns an object with two properties:
- below (boolean):
- true if the element is positioned below the top of the viewport.
- false if the element is above the top of the viewport.
- status (boolean):
- true if the entire element is visible within the viewport (the element’s top, bottom, left, and right edges must all be within the bounds of the viewport).
- false if any part of the element is outside the viewport.
If the element’s width and height are 0, the function will return undefined since the element is not considered visible.
Usage Example
const element = qs('.my-element');
const visibility = isElementInViewport(element);
if (visibility && visibility.status) {
console.log('The element is fully visible in the viewport.');
} else {
console.log('The element is not fully visible in the viewport.');
}Checks whether an element with the class .my-element is fully visible in the viewport and logs the result.
const element = qs('.my-element');
const visibility = isElementInViewport(element);
if (visibility && visibility.below) {
console.log('The element is below the top of the viewport.');
} else {
console.log('The element is above the top of the viewport.');
}Checks if the element is below the top of the viewport.
Notes
- Zero Dimensions: If the element has 0 width and 0 height, the function returns undefined as it cannot be considered visible.
- Edge Boundaries: The status will return false if any edge of the element is outside the viewport (even if only partially).
isMobile
The isMobile function detects if the current user is accessing the website from a mobile device by examining the user agent string of the browser.
Return Value
- Boolean: true if the user agent string contains “android” or “mobile”, indicating a mobile device, false otherwise.
Usage Example
if (isMobile()) {
console.log('User is on a mobile device.');
} else {
console.log('User is on a desktop or non-mobile device.');
}Notes
- This function provides a simple check for mobile devices but may not cover all edge cases or device types.
- It may return true for some tablets or hybrid devices depending on their user agent strings.
- For more robust device detection, consider using dedicated libraries like Modernizr or Mobile Detect.
getWidth
The getWidth function calculates and returns the maximum width of the current document, accounting for different properties that reflect the document’s width under various scenarios.
Return Value
- Number: The maximum width of the document in pixels.
Usage Example
const pageWidth = getWidth();
console.log(`The current document width is ${pageWidth}px.`);Notes
- This function is useful when needing to adapt UI elements or layouts based on the total document width.
- It accounts for varying browser implementations and conditions such as scrollbars or borders that might affect width calculations.
setCookie
The setCookie function creates or updates a cookie with a specified name, value, and expiration date. It sets the cookie to be accessible across the entire website by using a path of /.
Parameters
- cookieName (string): The name of the cookie to be set.
- cookieValue (string): The value to be assigned to the cookie.
- expiryDays (number): The number of days until the cookie expires.
Usage Example
// This code creates a cookie named username with the value JohnDoe that will expire in 7 days.
setCookie("username", "JohnDoe", 7);Notes
- Cookies set by this function are accessible site-wide (path=/).
- Ensure cookie names are unique to avoid overwriting existing cookies.
getCookie
The getCookie function retrieves the value of a specified cookie from the browser’s document.
Parameters
- cookieName (string): The name of the cookie whose value you want to retrieve.
Return Value
- String: The value of the specified cookie.
- Returns an empty string ("") if the cookie does not exist.
Usage Example
document.cookie = "username=JohnDoe";
const username = getCookie("username");
if (username) {
console.log(`Welcome back, ${username}!`);
} else {
console.log("No username cookie found.");
}Notes
- The function assumes the cookie format follows the standard name=value convention.
- Special characters in cookie values are handled correctly due to decodeURIComponent().
getStore
The getStore function retrieves data from a specified storage (localStorage or sessionStorage) and optionally returns a specific property of the stored data object. The function handles JSON parsing and gracefully manages errors if the stored data is not in a valid JSON format.
Parameters
- name (string): The key name under which the data is stored.
- key (string | null) optional: The property to retrieve from the stored object. If omitted or null, the entire object is returned.
- storage (string) optional: The storage type from which to retrieve the data. Options: "localStorage" (default) or "sessionStorage".
Usage Example
const theme = getStore('userSettings', 'theme');
console.log(theme); // Outputs: 'dark'Without a second parameter as a key, you get the entire data object back
const data = getStore('myExampleObject');setStore
The setStore function saves or updates an object in client-side storage (localStorage or sessionStorage). It merges the new key-value pair with existing data stored under the specified name key, ensuring structured data persistence across user sessions.
Parameters
- name (string): The name of the storage key used to save or retrieve the data.
- key (string): The property name in the object to store or update.
- value (any): The value associated with the specified key.
- storage (string) optional: Specifies the storage type. Options are: "localStorage" (default) or "sessionStorage"
Usage Example
setStore('userSettings', 'theme', 'dark');This stores { theme: 'dark' } in localStorage under the key 'userSettings'.
setStore('sessionData', 'isAuthenticated', true, 'sessionStorage');This stores { isAuthenticated: true } in sessionStorage under the key 'sessionData'.
Notes
- The function relies on a properly implemented getStore() function to retrieve existing data.
- Data is stored as JSON strings and should be parsed back into objects when retrieved.
- Storage Limits: Both localStorage and sessionStorage typically have a 5MB storage limit.
- Best Practices: Avoid storing sensitive information in client-side storage for security reasons.
setMutationObserver
The setMutationObserver function sets up a MutationObserver on a DOM element matching the given selector. The observer watches for mutations (changes) to the element, and when such changes occur, it triggers the provided callback function. This function returns a promise, resolving once the observer is successfully set, or rejecting if any errors occur.
Parameters
- selector (string): The CSS selector used to find the target DOM element to observe.
- prefix (string): A unique identifier used as a data attribute (data-prefix) to ensure that the observer is not added multiple times to the same element.
- callback (function): The callback function to be executed when mutations are detected on the target element.
- options (object): An optional object specifying which mutations should be observed. The default value is:
- attributes: Observe changes to the element’s attributes.
- childList: Observe changes to the child nodes of the element.
- subtree: Observe changes to the entire subtree of the element.
Return Value
Returns a Promise: Resolves when the mutation observer is successfully set up. Rejects if the provided callback is not a function or if any errors occur during the process.
Usage Example
setMutationObserver('.example', 'experimentPrefix', () => {
console.log('Mutation detected on the element!');
});Notes
- Element Selection: Uses elemSync(selector) to select the element synchronously. If the element is not found, the promise will reject.
- Observer Activation: The observer is activated only once for each element, as indicated by the data-prefix attribute.
- Error Handling: If the callback is not a function, or if an error occurs while setting up the observer, the promise is rejected with an error message.
- Mutation Types: By default, the observer listens for changes to attributes, child nodes, and the entire subtree. These can be customized via the options parameter.
License
Copyright (c) 2025 CROdev.
Licensed under The MIT License (MIT).