You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2023/03/31 09:23:36 UTC

[unomi] branch backport-UNOMI-758 created (now edff92fde)

This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a change to branch backport-UNOMI-758
in repository https://gitbox.apache.org/repos/asf/unomi.git


      at edff92fde fix cherry-pick

This branch includes the following new commits:

     new ce11c954f UNOMI-758 : async save of export config (#601)
     new edff92fde fix cherry-pick

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[unomi] 01/02: UNOMI-758 : async save of export config (#601)

Posted by jk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch backport-UNOMI-758
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit ce11c954f91007d711e51bfdc614447f5b47f7f4
Author: David Griffon <dg...@jahia.com>
AuthorDate: Fri Mar 31 10:44:18 2023 +0200

    UNOMI-758 : async save of export config (#601)
    
    * UNOMI-758 : async save of export config
    
    * small cleanup
    
    * small cleanup
    
    * small cleanup
    
    * Perf improvements for oneshot exports
    
    ---------
    
    Co-authored-by: Kevan <ke...@jahia.com>
---
 .../unomi/router/api/IRouterCamelContext.java      |    4 +-
 .../apache/unomi/router/api/RouterConstants.java   |    4 +
 .../org/apache/unomi/router/api/RouterUtils.java   |    4 +
 .../services/ImportExportConfigurationService.java |   16 +-
 .../unomi/router/core/bean/CollectProfileBean.java |    1 +
 .../router/core/context/RouterCamelContext.java    |  147 +-
 .../router/core/event/UpdateCamelRouteEvent.java   |    9 -
 .../core/event/UpdateCamelRouteEventHandler.java   |    6 +-
 .../processor/ExportRouteCompletionProcessor.java  |    9 +-
 .../route/ProfileExportCollectRouteBuilder.java    |   18 +-
 .../route/ProfileExportProducerRouteBuilder.java   |    6 +-
 .../resources/OSGI-INF/blueprint/blueprint.xml     |    3 +-
 .../services/AbstractConfigurationServiceImpl.java |   83 -
 .../router/services/AbstractCustomServiceImpl.java |   76 -
 .../services/ExportConfigurationServiceImpl.java   |   43 +-
 .../services/ImportConfigurationServiceImpl.java   |   38 +-
 .../router/services/ProfileExportServiceImpl.java  |   54 +-
 .../router/services/ProfileImportServiceImpl.java  |    9 +-
 .../resources/OSGI-INF/blueprint/blueprint.xml     |   23 +-
 .../web-tracker/wab/dist/unomi-web-tracker.js      | 3867 ++++++++++++++++++++
 .../web-tracker/wab/dist/unomi-web-tracker.js.map  |    1 +
 .../web-tracker/wab/dist/unomi-web-tracker.min.js  |    1 +
 .../unomi/itests/ProfileImportRankingIT.java       |    5 +-
 23 files changed, 4104 insertions(+), 323 deletions(-)

diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java
index d35c3d4a0..5ec1adb57 100644
--- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java
+++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java
@@ -23,7 +23,9 @@ public interface IRouterCamelContext {
 
     void killExistingRoute(String routeId, boolean fireEvent) throws Exception;
 
-    void updateProfileReaderRoute(Object configuration, boolean fireEvent) throws Exception;
+    void updateProfileImportReaderRoute(String configId, boolean fireEvent) throws Exception;
+
+    void updateProfileExportReaderRoute(String configId, boolean fireEvent) throws Exception;
 
     void setTracing(boolean tracing);
 }
diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java
index 3b04703ca..5ef19fe44 100644
--- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java
+++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java
@@ -20,6 +20,10 @@ package org.apache.unomi.router.api;
  * Created by amidani on 13/06/2017.
  */
 public interface RouterConstants {
+    enum CONFIG_CAMEL_REFRESH {
+        UPDATED,
+        REMOVED
+    }
 
     String CONFIG_TYPE_NOBROKER = "nobroker";
     String CONFIG_TYPE_KAFKA = "kafka";
diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterUtils.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterUtils.java
index 464f90839..a535206c9 100644
--- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterUtils.java
+++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterUtils.java
@@ -18,6 +18,7 @@ package org.apache.unomi.router.api;
 
 import org.apache.unomi.api.PropertyType;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
 
@@ -27,6 +28,9 @@ import java.util.Map;
 public class RouterUtils {
 
     public static ImportExportConfiguration addExecutionEntry(ImportExportConfiguration configuration, Map execution, int executionsHistorySize) {
+        if (configuration.getExecutions() == null) {
+            configuration.setExecutions(new ArrayList<>());
+        }
         if (configuration.getExecutions().size() >= executionsHistorySize) {
             int oldestExecIndex = 0;
             long oldestExecDate = (Long) configuration.getExecutions().get(0).get(RouterConstants.KEY_EXECS_DATE);
diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java
index dd561e745..edb103cc5 100644
--- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java
+++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java
@@ -17,10 +17,11 @@
 package org.apache.unomi.router.api.services;
 
 import org.apache.unomi.router.api.ExportConfiguration;
-import org.apache.unomi.router.api.IRouterCamelContext;
 import org.apache.unomi.router.api.ImportConfiguration;
+import org.apache.unomi.router.api.RouterConstants;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * A service to access and operate on {@link ImportConfiguration}s / {@link ExportConfiguration}s.
@@ -60,15 +61,8 @@ public interface ImportExportConfigurationService<T> {
     void delete(String configId);
 
     /**
-     * Set the router camel context to share
-     *
-     * @param routerCamelContext the router Camel context to use for all operations
-     */
-    void setRouterCamelContext(IRouterCamelContext routerCamelContext);
-
-    /**
-     * Retrieve the configured router camel context
-     * @return the configured instance, or null if not configured
+     * Used by camel route system to get the latest changes on configs and reflect changes on camel routes if necessary
+     * @return map of configId per operation to be done in camel
      */
-    IRouterCamelContext getRouterCamelContext();
+    Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> consumeConfigsToBeRefresh();
 }
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java
index 452501974..1ea03eb3a 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java
@@ -29,6 +29,7 @@ public class CollectProfileBean {
     private PersistenceService persistenceService;
 
     public List<Profile> extractProfileBySegment(String segment) {
+        // TODO: UNOMI-759 avoid loading all profiles in RAM here
         return persistenceService.query("segments", segment,null, Profile.class);
     }
 
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
index fdcebfa24..4ad24e2d1 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
@@ -24,6 +24,7 @@ import org.apache.camel.model.RouteDefinition;
 import org.apache.unomi.api.services.ClusterService;
 import org.apache.unomi.api.services.ConfigSharingService;
 import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.api.services.SchedulerService;
 import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.router.api.ExportConfiguration;
 import org.apache.unomi.router.api.IRouterCamelContext;
@@ -37,20 +38,23 @@ import org.apache.unomi.router.core.processor.ImportConfigByFileNameProcessor;
 import org.apache.unomi.router.core.processor.ImportRouteCompletionProcessor;
 import org.apache.unomi.router.core.processor.UnomiStorageProcessor;
 import org.apache.unomi.router.core.route.*;
-import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleEvent;
-import org.osgi.framework.SynchronousBundleListener;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Map;
+import java.util.TimerTask;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Created by amidani on 04/05/2017.
  */
-public class RouterCamelContext implements SynchronousBundleListener, IRouterCamelContext {
+public class RouterCamelContext implements IRouterCamelContext {
 
     private Logger logger = LoggerFactory.getLogger(RouterCamelContext.class.getName());
     private CamelContext camelContext;
@@ -74,6 +78,11 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
     private ConfigSharingService configSharingService;
     private ClusterService clusterService;
 
+    // TODO UNOMI-572: when fixing UNOMI-572 please remove the usage of the custom ScheduledExecutorService and re-introduce the Unomi Scheduler Service
+    private ScheduledExecutorService scheduler;
+    private Integer configsRefreshInterval = 1000;
+    private ScheduledFuture<?> scheduledFuture;
+
     public static String EVENT_ID_REMOVE = "org.apache.unomi.router.event.remove";
     public static String EVENT_ID_IMPORT = "org.apache.unomi.router.event.import";
     public static String EVENT_ID_EXPORT = "org.apache.unomi.router.event.export";
@@ -102,12 +111,71 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
         camelContext.setTracing(true);
     }
 
-    public void initCamelContext() throws Exception {
+    public void init() throws Exception {
         logger.info("Initialize Camel Context...");
+        scheduler = Executors.newSingleThreadScheduledExecutor();
 
         configSharingService.setProperty(RouterConstants.IMPORT_ONESHOT_UPLOAD_DIR, uploadDir);
         configSharingService.setProperty(RouterConstants.KEY_HISTORY_SIZE, execHistorySize);
 
+        initCamel();
+
+        initTimers();
+        logger.info("Camel Context initialized successfully.");
+    }
+
+    public void destroy() throws Exception {
+        scheduledFuture.cancel(true);
+        if (scheduler != null) {
+            scheduler.shutdown();
+        }
+        //This is to shutdown Camel context
+        //(will stop all routes/components/endpoints etc and clear internal state/cache)
+        this.camelContext.stop();
+        logger.info("Camel context for profile import is shutdown.");
+    }
+
+    private void initTimers() {
+        TimerTask task = new TimerTask() {
+            @Override
+            public void run() {
+                try {
+                    Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> importConfigsToRefresh = importConfigurationService.consumeConfigsToBeRefresh();
+                    Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> exportConfigsToRefresh = exportConfigurationService.consumeConfigsToBeRefresh();
+
+                    for (Map.Entry<String, RouterConstants.CONFIG_CAMEL_REFRESH> importConfigToRefresh : importConfigsToRefresh.entrySet()) {
+                        try {
+                            if (importConfigToRefresh.getValue().equals(RouterConstants.CONFIG_CAMEL_REFRESH.UPDATED)) {
+                                updateProfileImportReaderRoute(importConfigToRefresh.getKey(), true);
+                            } else if (importConfigToRefresh.getValue().equals(RouterConstants.CONFIG_CAMEL_REFRESH.REMOVED)) {
+                                killExistingRoute(importConfigToRefresh.getKey(), true);
+                            }
+                        } catch (Exception e) {
+                            logger.error("Unexpected error while refreshing(" + importConfigToRefresh.getValue() + ") camel route: " + importConfigToRefresh.getKey(), e);
+                        }
+                    }
+
+
+                    for (Map.Entry<String, RouterConstants.CONFIG_CAMEL_REFRESH> exportConfigToRefresh : exportConfigsToRefresh.entrySet()) {
+                        try {
+                            if (exportConfigToRefresh.getValue().equals(RouterConstants.CONFIG_CAMEL_REFRESH.UPDATED)) {
+                                updateProfileExportReaderRoute(exportConfigToRefresh.getKey(), true);
+                            } else if (exportConfigToRefresh.getValue().equals(RouterConstants.CONFIG_CAMEL_REFRESH.REMOVED)) {
+                                killExistingRoute(exportConfigToRefresh.getKey(), true);
+                            }
+                        } catch (Exception e) {
+                            logger.error("Unexpected error while refreshing(" + exportConfigToRefresh.getValue() + ") camel route: " + exportConfigToRefresh.getKey(), e);
+                        }
+                    }
+                } catch (Exception e) {
+                    logger.error("Unexpected error while refreshing import/export camel routes", e);
+                }
+            }
+        };
+        scheduledFuture = scheduler.scheduleWithFixedDelay(task, 0, configsRefreshInterval, TimeUnit.MILLISECONDS);
+    }
+
+    private void initCamel() throws Exception {
         camelContext = new OsgiDefaultCamelContext(bundleContext);
 
         //--IMPORT ROUTES
@@ -142,7 +210,7 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
 
         //Profiles collect
         ProfileExportCollectRouteBuilder profileExportCollectRouteBuilder = new ProfileExportCollectRouteBuilder(kafkaProps, configType);
-        profileExportCollectRouteBuilder.setExportConfigurationService(exportConfigurationService);
+        profileExportCollectRouteBuilder.setExportConfigurationList(exportConfigurationService.getAll());
         profileExportCollectRouteBuilder.setPersistenceService(persistenceService);
         profileExportCollectRouteBuilder.setAllowedEndpoints(allowedEndpoints);
         profileExportCollectRouteBuilder.setJacksonDataFormat(jacksonDataFormat);
@@ -160,21 +228,6 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
         camelContext.addRoutes(profileExportProducerRouteBuilder);
 
         camelContext.start();
-
-        logger.debug("postConstruct {" + bundleContext.getBundle() + "}");
-
-        processBundleStartup(bundleContext);
-        for (Bundle bundle : bundleContext.getBundles()) {
-            if (bundle.getBundleContext() != null) {
-                processBundleStartup(bundle.getBundleContext());
-            }
-        }
-        bundleContext.addBundleListener(this);
-
-        importConfigurationService.setRouterCamelContext(this);
-        exportConfigurationService.setRouterCamelContext(this);
-
-        logger.info("Camel Context {} initialized successfully.");
     }
 
     public void killExistingRoute(String routeId, boolean fireEvent) throws Exception {
@@ -194,17 +247,15 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
         }
     }
 
-    public void updateProfileReaderRoute(Object configuration, boolean fireEvent) throws Exception {
-        if (configuration instanceof ImportConfiguration) {
-            updateProfileImportReaderRoute((ImportConfiguration) configuration, fireEvent);
-        } else {
-            updateProfileExportReaderRoute((ExportConfiguration) configuration, fireEvent);
+    public void updateProfileImportReaderRoute(String configId, boolean fireEvent) throws Exception {
+        killExistingRoute(configId, false);
+
+        ImportConfiguration importConfiguration = importConfigurationService.load(configId);
+        if (importConfiguration == null) {
+            logger.warn("Cannot update profile import reader route, config: {} not found", configId);
+            return;
         }
-    }
 
-    private void updateProfileImportReaderRoute(ImportConfiguration importConfiguration, boolean fireEvent) throws Exception {
-        killExistingRoute(importConfiguration.getItemId(), false);
-        //Handle transforming an import config oneshot <--> recurrent
         if (RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT.equals(importConfiguration.getConfigType())) {
             ProfileImportFromSourceRouteBuilder builder = new ProfileImportFromSourceRouteBuilder(kafkaProps, configType);
             builder.setImportConfigurationList(Arrays.asList(importConfiguration));
@@ -217,19 +268,23 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
 
             if (fireEvent) {
                 UpdateCamelRouteEvent event = new UpdateCamelRouteEvent(EVENT_ID_IMPORT);
-                event.setConfiguration(importConfiguration);
                 clusterService.sendEvent(event);
             }
         }
     }
 
-    private void updateProfileExportReaderRoute(ExportConfiguration exportConfiguration, boolean fireEvent) throws Exception {
-        killExistingRoute(exportConfiguration.getItemId(), false);
-        //Handle transforming an import config oneshot <--> recurrent
+    public void updateProfileExportReaderRoute(String configId, boolean fireEvent) throws Exception {
+        killExistingRoute(configId, false);
+
+        ExportConfiguration exportConfiguration = exportConfigurationService.load(configId);
+        if (exportConfiguration == null) {
+            logger.warn("Cannot update profile export reader route, config: {} not found", configId);
+            return;
+        }
+
         if (RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT.equals(exportConfiguration.getConfigType())) {
             ProfileExportCollectRouteBuilder profileExportCollectRouteBuilder = new ProfileExportCollectRouteBuilder(kafkaProps, configType);
-            profileExportCollectRouteBuilder.setExportConfigurationList(Arrays.asList(exportConfiguration));
-            profileExportCollectRouteBuilder.setExportConfigurationService(exportConfigurationService);
+            profileExportCollectRouteBuilder.setExportConfigurationList(Collections.singletonList(exportConfiguration));
             profileExportCollectRouteBuilder.setPersistenceService(persistenceService);
             profileExportCollectRouteBuilder.setAllowedEndpoints(allowedEndpoints);
             profileExportCollectRouteBuilder.setJacksonDataFormat(jacksonDataFormat);
@@ -238,7 +293,6 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
 
             if (fireEvent) {
                 UpdateCamelRouteEvent event = new UpdateCamelRouteEvent(EVENT_ID_EXPORT);
-                event.setConfiguration(exportConfiguration);
                 clusterService.sendEvent(event);
             }
         }
@@ -303,23 +357,4 @@ public class RouterCamelContext implements SynchronousBundleListener, IRouterCam
     public void setAllowedEndpoints(String allowedEndpoints) {
         this.allowedEndpoints = allowedEndpoints;
     }
-
-    public void preDestroy() throws Exception {
-        bundleContext.removeBundleListener(this);
-        //This is to shutdown Camel context
-        //(will stop all routes/components/endpoints etc and clear internal state/cache)
-        this.camelContext.stop();
-        logger.info("Camel context for profile import is shutdown.");
-    }
-
-    private void processBundleStartup(BundleContext bundleContext) {
-        if (bundleContext == null) {
-            return;
-        }
-    }
-
-    @Override
-    public void bundleChanged(BundleEvent bundleEvent) {
-
-    }
 }
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEvent.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEvent.java
index 7e1dc81d2..2f3d2cb3f 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEvent.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEvent.java
@@ -23,7 +23,6 @@ import org.apache.karaf.cellar.core.event.Event;
  */
 public class UpdateCamelRouteEvent extends Event {
     private String routeId;
-    private Object configuration;
 
     public UpdateCamelRouteEvent(String id) {
         super(id);
@@ -36,12 +35,4 @@ public class UpdateCamelRouteEvent extends Event {
     public void setRouteId(String routeId) {
         this.routeId = routeId;
     }
-
-    public Object getConfiguration() {
-        return configuration;
-    }
-
-    public void setConfiguration(Object configuration) {
-        this.configuration = configuration;
-    }
 }
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEventHandler.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEventHandler.java
index c75207273..91cd09df1 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEventHandler.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/event/UpdateCamelRouteEventHandler.java
@@ -49,8 +49,10 @@ public class UpdateCamelRouteEventHandler extends CellarSupport implements Event
                 logger.debug("Event id is {}", event.getId());
                 if (event.getId().equals(RouterCamelContext.EVENT_ID_REMOVE) && StringUtils.isNotBlank(event.getRouteId())) {
                     routerCamelContext.killExistingRoute(event.getRouteId(), false);
-                } else if ((event.getId().equals(RouterCamelContext.EVENT_ID_IMPORT) || event.getId().equals(RouterCamelContext.EVENT_ID_EXPORT)) && event.getConfiguration() != null) {
-                    routerCamelContext.updateProfileReaderRoute(event.getConfiguration(), false);
+                } else if ((event.getId().equals(RouterCamelContext.EVENT_ID_IMPORT))) {
+                    routerCamelContext.updateProfileImportReaderRoute(event.getRouteId(), false);
+                } else if (event.getId().equals(RouterCamelContext.EVENT_ID_EXPORT)) {
+                    routerCamelContext.updateProfileExportReaderRoute(event.getRouteId(), false);
                 }
             } catch (Exception e) {
                 logger.error("Error when executing event", e);
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java
index 309c7c2a2..414e1c00f 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java
@@ -40,14 +40,17 @@ public class ExportRouteCompletionProcessor implements Processor {
 
     @Override
     public void process(Exchange exchange) throws Exception {
-        ExportConfiguration exportConfig = (ExportConfiguration) exchange.getIn().getHeader(RouterConstants.HEADER_EXPORT_CONFIG);
+        // We load the conf from ES because we are going to increment the execution number
+        ExportConfiguration exportConfiguration = exportConfigurationService.load(exchange.getFromRouteId());
+        if (exportConfiguration == null) {
+            logger.warn("Unable to complete export, config cannot not found: {}", exchange.getFromRouteId());
+            return;
+        }
 
         Map execution = new HashMap();
         execution.put(RouterConstants.KEY_EXECS_DATE, ((Date) exchange.getProperty("CamelCreatedTimestamp")).getTime());
         execution.put(RouterConstants.KEY_EXECS_EXTRACTED, exchange.getProperty("CamelSplitSize"));
 
-        ExportConfiguration exportConfiguration = exportConfigurationService.load(exportConfig.getItemId());
-
         exportConfiguration = (ExportConfiguration) RouterUtils.addExecutionEntry(exportConfiguration, execution, executionsHistorySize);
         exportConfiguration.setStatus(RouterConstants.CONFIG_STATUS_COMPLETE_SUCCESS);
 
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java
index bff0d6d46..e87ec50be 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java
@@ -39,7 +39,6 @@ public class ProfileExportCollectRouteBuilder extends RouterAbstractRouteBuilder
     private static final Logger logger = LoggerFactory.getLogger(ProfileExportCollectRouteBuilder.class);
 
     private List<ExportConfiguration> exportConfigurationList;
-    private ImportExportConfigurationService<ExportConfiguration> exportConfigurationService;
     private PersistenceService persistenceService;
 
     public ProfileExportCollectRouteBuilder(Map<String, String> kafkaProps, String configType) {
@@ -48,16 +47,16 @@ public class ProfileExportCollectRouteBuilder extends RouterAbstractRouteBuilder
 
     @Override
     public void configure() throws Exception {
-        logger.info("Configure Recurrent Route 'Export :: Collect Data'");
-
-        if (exportConfigurationList == null) {
-            exportConfigurationList = exportConfigurationService.getAll();
+        if (exportConfigurationList == null || exportConfigurationList.isEmpty()) {
+            // Nothing to configure
+            return;
         }
 
+        logger.info("Configure Recurrent Route 'Export :: Collect Data'");
+
         CollectProfileBean collectProfileBean = new CollectProfileBean();
         collectProfileBean.setPersistenceService(persistenceService);
 
-
         //Loop on multiple export configuration
         for (final ExportConfiguration exportConfiguration : exportConfigurationList) {
             if (RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT.equals(exportConfiguration.getConfigType()) &&
@@ -74,7 +73,7 @@ public class ProfileExportCollectRouteBuilder extends RouterAbstractRouteBuilder
                                 .autoStartup(exportConfiguration.isActive())
                                 .bean(collectProfileBean, "extractProfileBySegment(" + exportConfiguration.getProperties().get("segment") + ")")
                                 .split(body())
-                                .marshal(jacksonDataFormat)
+                                .marshal(jacksonDataFormat) // TODO: UNOMI-759 avoid unnecessary marshalling
                                 .convertBodyTo(String.class)
                                 .setHeader(RouterConstants.HEADER_EXPORT_CONFIG, constant(exportConfiguration))
                                 .log(LoggingLevel.DEBUG, "BODY : ${body}");
@@ -99,12 +98,7 @@ public class ProfileExportCollectRouteBuilder extends RouterAbstractRouteBuilder
         this.exportConfigurationList = exportConfigurationList;
     }
 
-    public void setExportConfigurationService(ImportExportConfigurationService<ExportConfiguration> exportConfigurationService) {
-        this.exportConfigurationService = exportConfigurationService;
-    }
-
     public void setPersistenceService(PersistenceService persistenceService) {
         this.persistenceService = persistenceService;
     }
-
 }
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java
index 9f25c3bb9..86288019f 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java
@@ -59,10 +59,8 @@ public class ProfileExportProducerRouteBuilder extends RouterAbstractRouteBuilde
             rtDef = from((String) getEndpointURI(RouterConstants.DIRECTION_TO, RouterConstants.DIRECT_EXPORT_DEPOSIT_BUFFER));
         }
 
-        LineBuildProcessor processor = new LineBuildProcessor(profileExportService);
-
-        rtDef.unmarshal(jacksonDataFormat)
-                .process(processor)
+        rtDef.unmarshal(jacksonDataFormat) // TODO: UNOMI-759 avoid unnecessary marshalling
+                .process(new LineBuildProcessor(profileExportService))
                 .aggregate(constant(true), new StringLinesAggregationStrategy())
                 .completionPredicate(exchangeProperty("CamelSplitSize").isEqualTo(exchangeProperty("CamelAggregatedSize")))
                 .eagerCheckCompletion()
diff --git a/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index d61d64f2d..d7b7a36c0 100644
--- a/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -82,7 +82,7 @@
 
 
     <bean id="camelContext" class="org.apache.unomi.router.core.context.RouterCamelContext"
-          init-method="initCamelContext" destroy-method="preDestroy">
+          init-method="init" destroy-method="destroy">
         <property name="configType" value="${router.config.type}"/>
         <property name="allowedEndpoints" value="${config.allowedEndpoints}"/>
         <property name="uploadDir" value="${import.oneshot.uploadDir}"/>
@@ -114,6 +114,7 @@
         <property name="profileService" ref="profileService"/>
         <property name="clusterService" ref="clusterService" />
     </bean>
+    <service id="camelContextOSGI" ref="camelContext" interface="org.apache.unomi.router.api.IRouterCamelContext"/>
 
     <bean id="collectProfileBean" class="org.apache.unomi.router.core.bean.CollectProfileBean">
         <property name="persistenceService" ref="persistenceService"/>
diff --git a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/AbstractConfigurationServiceImpl.java b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/AbstractConfigurationServiceImpl.java
deleted file mode 100644
index e7c5d67af..000000000
--- a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/AbstractConfigurationServiceImpl.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.router.services;
-
-import org.apache.unomi.persistence.spi.PersistenceService;
-import org.apache.unomi.router.api.IRouterCamelContext;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleEvent;
-import org.osgi.framework.SynchronousBundleListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Created by amidani on 26/06/2017.
- */
-public abstract class AbstractConfigurationServiceImpl implements SynchronousBundleListener {
-
-    private static final Logger logger = LoggerFactory.getLogger(AbstractConfigurationServiceImpl.class.getName());
-
-    protected BundleContext bundleContext;
-    protected PersistenceService persistenceService;
-    protected IRouterCamelContext routerCamelContext;
-
-    public void setBundleContext(BundleContext bundleContext) {
-        this.bundleContext = bundleContext;
-    }
-
-    public void setPersistenceService(PersistenceService persistenceService) {
-        this.persistenceService = persistenceService;
-    }
-
-    public void setRouterCamelContext(IRouterCamelContext routerCamelContext) {
-        this.routerCamelContext = routerCamelContext;
-    }
-
-    public IRouterCamelContext getRouterCamelContext() {
-        return routerCamelContext;
-    }
-
-    public void postConstruct() {
-        logger.debug("postConstruct {" + bundleContext.getBundle() + "}");
-
-        processBundleStartup(bundleContext);
-        for (Bundle bundle : bundleContext.getBundles()) {
-            if (bundle.getBundleContext() != null && bundle.getBundleId() != bundleContext.getBundle().getBundleId()) {
-                processBundleStartup(bundle.getBundleContext());
-            }
-        }
-        bundleContext.addBundleListener(this);
-        logger.info("Configuration service initialized.");
-    }
-
-    public void preDestroy() {
-        bundleContext.removeBundleListener(this);
-        logger.info("Configuration service shutdown.");
-    }
-
-    @Override
-    public void bundleChanged(BundleEvent bundleEvent) {
-
-    }
-
-    private void processBundleStartup(BundleContext bundleContext) {
-        if (bundleContext == null) {
-            return;
-        }
-    }
-}
diff --git a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/AbstractCustomServiceImpl.java b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/AbstractCustomServiceImpl.java
deleted file mode 100644
index dc06fff61..000000000
--- a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/AbstractCustomServiceImpl.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.router.services;
-
-import org.apache.unomi.persistence.spi.PersistenceService;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleEvent;
-import org.osgi.framework.SynchronousBundleListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Created by amidani on 30/06/2017.
- */
-public class AbstractCustomServiceImpl implements SynchronousBundleListener {
-
-    private static final Logger logger = LoggerFactory.getLogger(AbstractCustomServiceImpl.class);
-
-    protected PersistenceService persistenceService;
-    protected BundleContext bundleContext;
-
-    public void setPersistenceService(PersistenceService persistenceService) {
-        this.persistenceService = persistenceService;
-    }
-
-    public void setBundleContext(BundleContext bundleContext) {
-        this.bundleContext = bundleContext;
-    }
-
-    public void postConstruct() {
-        logger.debug("postConstruct {" + bundleContext.getBundle() + "}");
-
-        processBundleStartup(bundleContext);
-        for (Bundle bundle : bundleContext.getBundles()) {
-            if (bundle.getBundleContext() != null && bundle.getBundleId() != bundleContext.getBundle().getBundleId()) {
-                processBundleStartup(bundle.getBundleContext());
-            }
-        }
-        bundleContext.addBundleListener(this);
-        logger.info("Import configuration service initialized.");
-    }
-
-    public void preDestroy() {
-        bundleContext.removeBundleListener(this);
-        logger.info("Import configuration service shutdown.");
-    }
-
-    private void processBundleStartup(BundleContext bundleContext) {
-        if (bundleContext == null) {
-            return;
-        }
-    }
-
-    private void processBundleStop(BundleContext bundleContext) {
-    }
-
-    @Override
-    public void bundleChanged(BundleEvent bundleEvent) {
-
-    }
-}
diff --git a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ExportConfigurationServiceImpl.java b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ExportConfigurationServiceImpl.java
index 23787170e..d1b8d8f75 100644
--- a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ExportConfigurationServiceImpl.java
+++ b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ExportConfigurationServiceImpl.java
@@ -16,22 +16,33 @@
  */
 package org.apache.unomi.router.services;
 
+import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.router.api.ExportConfiguration;
-import org.apache.unomi.router.api.IRouterCamelContext;
+import org.apache.unomi.router.api.RouterConstants;
 import org.apache.unomi.router.api.services.ImportExportConfigurationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
+ * Service to manage Configuration of Item to export
  * Created by amidani on 28/04/2017.
  */
-public class ExportConfigurationServiceImpl extends AbstractConfigurationServiceImpl implements ImportExportConfigurationService<ExportConfiguration> {
+public class ExportConfigurationServiceImpl implements ImportExportConfigurationService<ExportConfiguration> {
 
     private static final Logger logger = LoggerFactory.getLogger(ExportConfigurationServiceImpl.class.getName());
 
+
+    private PersistenceService persistenceService;
+
+    public void setPersistenceService(PersistenceService persistenceService) {
+        this.persistenceService = persistenceService;
+    }
+
+    private final Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> camelConfigsToRefresh = new ConcurrentHashMap<>();
+
     public ExportConfigurationServiceImpl() {
         logger.info("Initializing export configuration service...");
     }
@@ -51,29 +62,25 @@ public class ExportConfigurationServiceImpl extends AbstractConfigurationService
         if (exportConfiguration.getItemId() == null) {
             exportConfiguration.setItemId(UUID.randomUUID().toString());
         }
-        if(updateRunningRoute) {
-            try {
-                routerCamelContext.updateProfileReaderRoute(exportConfiguration, true);
-            } catch (Exception e) {
-                logger.error("Error when trying to save/update running Apache Camel Route: {}", exportConfiguration.getItemId());
-            }
-        }
         persistenceService.save(exportConfiguration);
+
+        if (updateRunningRoute) {
+            camelConfigsToRefresh.put(exportConfiguration.getItemId(), RouterConstants.CONFIG_CAMEL_REFRESH.UPDATED);
+        }
+
         return persistenceService.load(exportConfiguration.getItemId(), ExportConfiguration.class);
     }
 
     @Override
     public void delete(String configId) {
-        try {
-            routerCamelContext.killExistingRoute(configId, true);
-        } catch (Exception e) {
-            logger.error("Error when trying to delete running Apache Camel Route: {}", configId);
-        }
         persistenceService.remove(configId, ExportConfiguration.class);
+        camelConfigsToRefresh.put(configId, RouterConstants.CONFIG_CAMEL_REFRESH.REMOVED);
     }
 
     @Override
-    public void setRouterCamelContext(IRouterCamelContext routerCamelContext) {
-        super.setRouterCamelContext(routerCamelContext);
+    public Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> consumeConfigsToBeRefresh() {
+        Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> result = new HashMap<>(camelConfigsToRefresh);
+        camelConfigsToRefresh.clear();
+        return result;
     }
 }
diff --git a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ImportConfigurationServiceImpl.java b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ImportConfigurationServiceImpl.java
index 364ea73fc..a12d2991f 100644
--- a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ImportConfigurationServiceImpl.java
+++ b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ImportConfigurationServiceImpl.java
@@ -16,22 +16,32 @@
  */
 package org.apache.unomi.router.services;
 
-import org.apache.unomi.router.api.IRouterCamelContext;
+import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.router.api.ImportConfiguration;
+import org.apache.unomi.router.api.RouterConstants;
 import org.apache.unomi.router.api.services.ImportExportConfigurationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
+ * Service to manage Configuration of object to import
  * Created by amidani on 28/04/2017.
  */
-public class ImportConfigurationServiceImpl extends AbstractConfigurationServiceImpl implements ImportExportConfigurationService<ImportConfiguration> {
+public class ImportConfigurationServiceImpl implements ImportExportConfigurationService<ImportConfiguration> {
 
     private static final Logger logger = LoggerFactory.getLogger(ImportConfigurationServiceImpl.class.getName());
 
+    private PersistenceService persistenceService;
+
+    public void setPersistenceService(PersistenceService persistenceService) {
+        this.persistenceService = persistenceService;
+    }
+
+    private final Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> camelConfigsToRefresh = new ConcurrentHashMap<>();
+
     public ImportConfigurationServiceImpl() {
         logger.info("Initializing import configuration service...");
     }
@@ -51,12 +61,8 @@ public class ImportConfigurationServiceImpl extends AbstractConfigurationService
         if (importConfiguration.getItemId() == null) {
             importConfiguration.setItemId(UUID.randomUUID().toString());
         }
-        if(updateRunningRoute) {
-            try {
-                routerCamelContext.updateProfileReaderRoute(importConfiguration, true);
-            } catch (Exception e) {
-                logger.error("Error when trying to save/update running Apache Camel Route: {}", importConfiguration.getItemId());
-            }
+        if (updateRunningRoute) {
+            camelConfigsToRefresh.put(importConfiguration.getItemId(), RouterConstants.CONFIG_CAMEL_REFRESH.UPDATED);
         }
         persistenceService.save(importConfiguration);
         return persistenceService.load(importConfiguration.getItemId(), ImportConfiguration.class);
@@ -64,16 +70,14 @@ public class ImportConfigurationServiceImpl extends AbstractConfigurationService
 
     @Override
     public void delete(String configId) {
-        try {
-            routerCamelContext.killExistingRoute(configId, true);
-        } catch (Exception e) {
-            logger.error("Error when trying to delete running Apache Camel Route: {}", configId);
-        }
         persistenceService.remove(configId, ImportConfiguration.class);
+        camelConfigsToRefresh.put(configId, RouterConstants.CONFIG_CAMEL_REFRESH.REMOVED);
     }
 
     @Override
-    public void setRouterCamelContext(IRouterCamelContext routerCamelContext) {
-        super.setRouterCamelContext(routerCamelContext);
+    public Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> consumeConfigsToBeRefresh() {
+        Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> result = new HashMap<>(camelConfigsToRefresh);
+        camelConfigsToRefresh.clear();
+        return result;
     }
 }
diff --git a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java
index 1b35e963d..c3197b61e 100644
--- a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java
+++ b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileExportServiceImpl.java
@@ -18,9 +18,13 @@ package org.apache.unomi.router.services;
 
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.PartialList;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.conditions.Condition;
 import org.apache.unomi.api.services.ConfigSharingService;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.router.api.ExportConfiguration;
 import org.apache.unomi.router.api.RouterConstants;
 import org.apache.unomi.router.api.RouterUtils;
@@ -33,24 +37,51 @@ import java.util.*;
 /**
  * Created by amidani on 30/06/2017.
  */
-public class ProfileExportServiceImpl extends AbstractCustomServiceImpl implements ProfileExportService {
+public class ProfileExportServiceImpl implements ProfileExportService {
 
     private static final Logger logger = LoggerFactory.getLogger(ProfileExportServiceImpl.class.getName());
 
+
+    private PersistenceService persistenceService;
+    private DefinitionsService definitionsService;
     private ConfigSharingService configSharingService;
 
+    public void setPersistenceService(PersistenceService persistenceService) {
+        this.persistenceService = persistenceService;
+    }
+
+    public void setDefinitionsService(DefinitionsService definitionsService) {
+        this.definitionsService = definitionsService;
+    }
+
+    public void setConfigSharingService(ConfigSharingService configSharingService) {
+        this.configSharingService = configSharingService;
+    }
+
     public String extractProfilesBySegment(ExportConfiguration exportConfiguration) {
-        List<Profile> profileList = persistenceService.query("segments", (String) exportConfiguration.getProperty("segment"), null, Profile.class);
+        Collection<PropertyType> propertiesDef = persistenceService.query("target", "profiles", null, PropertyType.class);
+
+        Condition segmentCondition = new Condition();
+        segmentCondition.setConditionType(definitionsService.getConditionType("profileSegmentCondition"));
+        segmentCondition.setParameter("segments", Collections.singletonList((String) exportConfiguration.getProperty("segment")));
+        segmentCondition.setParameter("matchType", "in");
+
         StringBuilder csvContent = new StringBuilder();
-        for (Profile profile : profileList) {
-            csvContent.append(convertProfileToCSVLine(profile, exportConfiguration));
-            csvContent.append(RouterUtils.getCharFromLineSeparator(exportConfiguration.getLineSeparator()));
+        PartialList<Profile> profiles = persistenceService.query(segmentCondition, null, Profile.class, 0, 1000, "10m");
+        int counter = 0;
+        while (profiles != null && profiles.getList().size() > 0) {
+            List<Profile> scrolledProfiles = profiles.getList();
+            for (Profile profile : scrolledProfiles) {
+                csvContent.append(convertProfileToCSVLine(profile, exportConfiguration, propertiesDef));
+                csvContent.append(RouterUtils.getCharFromLineSeparator(exportConfiguration.getLineSeparator()));
+            }
+            counter += scrolledProfiles.size();
+            profiles = persistenceService.continueScrollQuery(Profile.class, profiles.getScrollIdentifier(), profiles.getScrollTimeValidity());
         }
-        logger.debug("Exporting {} extracted profiles.", profileList.size());
 
         Map execution = new HashMap();
         execution.put(RouterConstants.KEY_EXECS_DATE, new Date().getTime());
-        execution.put(RouterConstants.KEY_EXECS_EXTRACTED, profileList.size());
+        execution.put(RouterConstants.KEY_EXECS_EXTRACTED, counter);
 
         exportConfiguration = (ExportConfiguration) RouterUtils.addExecutionEntry(exportConfiguration, execution, Integer.parseInt((String) configSharingService.getProperty(RouterConstants.KEY_HISTORY_SIZE)));
         persistenceService.save(exportConfiguration);
@@ -59,7 +90,12 @@ public class ProfileExportServiceImpl extends AbstractCustomServiceImpl implemen
     }
 
     public String convertProfileToCSVLine(Profile profile, ExportConfiguration exportConfiguration) {
+        // TODO: UNOMI-759 querying this everytimes
         Collection<PropertyType> propertiesDef = persistenceService.query("target", "profiles", null, PropertyType.class);
+        return convertProfileToCSVLine(profile, exportConfiguration, propertiesDef);
+    }
+
+    public String convertProfileToCSVLine(Profile profile, ExportConfiguration exportConfiguration, Collection<PropertyType> propertiesDef) {
         Map<String, String> mapping = (Map<String, String>) exportConfiguration.getProperty("mapping");
         String lineToWrite = "";
         for (int i = 0; i < mapping.size(); i++) {
@@ -104,8 +140,4 @@ public class ProfileExportServiceImpl extends AbstractCustomServiceImpl implemen
         return lineToWrite;
     }
 
-    public void setConfigSharingService(ConfigSharingService configSharingService) {
-        this.configSharingService = configSharingService;
-    }
-
 }
diff --git a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileImportServiceImpl.java b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileImportServiceImpl.java
index 86431034f..578aee22c 100644
--- a/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileImportServiceImpl.java
+++ b/extensions/router/router-service/src/main/java/org/apache/unomi/router/services/ProfileImportServiceImpl.java
@@ -18,6 +18,7 @@ package org.apache.unomi.router.services;
 
 import org.apache.commons.beanutils.BeanUtils;
 import org.apache.unomi.api.Profile;
+import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.router.api.ProfileToImport;
 import org.apache.unomi.router.api.services.ProfileImportService;
 import org.slf4j.Logger;
@@ -29,10 +30,16 @@ import java.util.List;
 /**
  * Created by amidani on 18/05/2017.
  */
-public class ProfileImportServiceImpl extends AbstractCustomServiceImpl implements ProfileImportService {
+public class ProfileImportServiceImpl implements ProfileImportService {
 
     private static final Logger logger = LoggerFactory.getLogger(ProfileImportServiceImpl.class.getName());
 
+    private PersistenceService persistenceService;
+
+    public void setPersistenceService(PersistenceService persistenceService) {
+        this.persistenceService = persistenceService;
+    }
+
     public boolean saveMergeDeleteImportedProfile(ProfileToImport profileToImport) throws InvocationTargetException, IllegalAccessException {
         logger.debug("Importing profile with ID : {}", profileToImport.getItemId());
         Profile existingProfile = new Profile();
diff --git a/extensions/router/router-service/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/router/router-service/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index c00554cfa..8b187cad6 100644
--- a/extensions/router/router-service/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/extensions/router/router-service/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -22,30 +22,25 @@
 
     <reference id="persistenceService" interface="org.apache.unomi.persistence.spi.PersistenceService"/>
     <reference id="configSharingService" interface="org.apache.unomi.api.services.ConfigSharingService"/>
+    <reference id="definitionsService" interface="org.apache.unomi.api.services.DefinitionsService"/>
 
-    <bean id="importConfigurationServiceImpl" class="org.apache.unomi.router.services.ImportConfigurationServiceImpl"
-          init-method="postConstruct" destroy-method="preDestroy">
+    <bean id="importConfigurationServiceImpl" class="org.apache.unomi.router.services.ImportConfigurationServiceImpl">
         <property name="persistenceService" ref="persistenceService"/>
-        <property name="bundleContext" ref="blueprintBundleContext"/>
     </bean>
     <service id="importConfigurationService" ref="importConfigurationServiceImpl">
         <interfaces>
             <value>org.apache.unomi.router.api.services.ImportExportConfigurationService</value>
-            <value>org.osgi.framework.SynchronousBundleListener</value>
         </interfaces>
         <service-properties>
             <entry key="configDiscriminator" value="IMPORT"/>
         </service-properties>
     </service>
 
-    <bean id="exportConfigurationServiceImpl" class="org.apache.unomi.router.services.ExportConfigurationServiceImpl"
-          init-method="postConstruct" destroy-method="preDestroy">
+    <bean id="exportConfigurationServiceImpl" class="org.apache.unomi.router.services.ExportConfigurationServiceImpl">
         <property name="persistenceService" ref="persistenceService"/>
-        <property name="bundleContext" ref="blueprintBundleContext"/>
     </bean>
     <service id="exportConfigurationService" ref="exportConfigurationServiceImpl">
         <interfaces>
-            <value>org.osgi.framework.SynchronousBundleListener</value>
             <value>org.apache.unomi.router.api.services.ImportExportConfigurationService</value>
         </interfaces>
         <service-properties>
@@ -53,29 +48,23 @@
         </service-properties>
     </service>
 
-    <bean id="profileImportServiceImpl" class="org.apache.unomi.router.services.ProfileImportServiceImpl"
-          init-method="postConstruct" destroy-method="preDestroy">
+    <bean id="profileImportServiceImpl" class="org.apache.unomi.router.services.ProfileImportServiceImpl">
         <property name="persistenceService" ref="persistenceService"/>
-        <property name="bundleContext" ref="blueprintBundleContext"/>
     </bean>
     <service id="profileImportService" ref="profileImportServiceImpl">
         <interfaces>
-            <value>org.osgi.framework.SynchronousBundleListener</value>
             <value>org.apache.unomi.router.api.services.ProfileImportService</value>
         </interfaces>
     </service>
 
-    <bean id="profileExportServiceImpl" class="org.apache.unomi.router.services.ProfileExportServiceImpl"
-          init-method="postConstruct" destroy-method="preDestroy">
+    <bean id="profileExportServiceImpl" class="org.apache.unomi.router.services.ProfileExportServiceImpl">
         <property name="persistenceService" ref="persistenceService"/>
         <property name="configSharingService" ref="configSharingService" />
-        <property name="bundleContext" ref="blueprintBundleContext"/>
+        <property name="definitionsService" ref="definitionsService" />
     </bean>
     <service id="profileExportService" ref="profileExportServiceImpl">
         <interfaces>
-            <value>org.osgi.framework.SynchronousBundleListener</value>
             <value>org.apache.unomi.router.api.services.ProfileExportService</value>
         </interfaces>
     </service>
-
 </blueprint>
diff --git a/extensions/web-tracker/wab/dist/unomi-web-tracker.js b/extensions/web-tracker/wab/dist/unomi-web-tracker.js
new file mode 100644
index 000000000..334517d2b
--- /dev/null
+++ b/extensions/web-tracker/wab/dist/unomi-web-tracker.js
@@ -0,0 +1,3867 @@
+(function () {
+  'use strict';
+
+  function _typeof(obj) {
+    "@babel/helpers - typeof";
+
+    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
+      return typeof obj;
+    } : function (obj) {
+      return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+    }, _typeof(obj);
+  }
+
+  var global$1 = (typeof global !== "undefined" ? global :
+    typeof self !== "undefined" ? self :
+    typeof window !== "undefined" ? window : {});
+
+  var lookup = [];
+  var revLookup = [];
+  var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array;
+  var inited = false;
+  function init () {
+    inited = true;
+    var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+    for (var i = 0, len = code.length; i < len; ++i) {
+      lookup[i] = code[i];
+      revLookup[code.charCodeAt(i)] = i;
+    }
+
+    revLookup['-'.charCodeAt(0)] = 62;
+    revLookup['_'.charCodeAt(0)] = 63;
+  }
+
+  function toByteArray (b64) {
+    if (!inited) {
+      init();
+    }
+    var i, j, l, tmp, placeHolders, arr;
+    var len = b64.length;
+
+    if (len % 4 > 0) {
+      throw new Error('Invalid string. Length must be a multiple of 4')
+    }
+
+    // the number of equal signs (place holders)
+    // if there are two placeholders, than the two characters before it
+    // represent one byte
+    // if there is only one, then the three characters before it represent 2 bytes
+    // this is just a cheap hack to not do indexOf twice
+    placeHolders = b64[len - 2] === '=' ? 2 : b64[len - 1] === '=' ? 1 : 0;
+
+    // base64 is 4/3 + up to two characters of the original data
+    arr = new Arr(len * 3 / 4 - placeHolders);
+
+    // if there are placeholders, only get up to the last complete 4 chars
+    l = placeHolders > 0 ? len - 4 : len;
+
+    var L = 0;
+
+    for (i = 0, j = 0; i < l; i += 4, j += 3) {
+      tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)];
+      arr[L++] = (tmp >> 16) & 0xFF;
+      arr[L++] = (tmp >> 8) & 0xFF;
+      arr[L++] = tmp & 0xFF;
+    }
+
+    if (placeHolders === 2) {
+      tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4);
+      arr[L++] = tmp & 0xFF;
+    } else if (placeHolders === 1) {
+      tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2);
+      arr[L++] = (tmp >> 8) & 0xFF;
+      arr[L++] = tmp & 0xFF;
+    }
+
+    return arr
+  }
+
+  function tripletToBase64 (num) {
+    return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F]
+  }
+
+  function encodeChunk (uint8, start, end) {
+    var tmp;
+    var output = [];
+    for (var i = start; i < end; i += 3) {
+      tmp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]);
+      output.push(tripletToBase64(tmp));
+    }
+    return output.join('')
+  }
+
+  function fromByteArray (uint8) {
+    if (!inited) {
+      init();
+    }
+    var tmp;
+    var len = uint8.length;
+    var extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
+    var output = '';
+    var parts = [];
+    var maxChunkLength = 16383; // must be multiple of 3
+
+    // go through the array every three bytes, we'll deal with trailing stuff later
+    for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
+      parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength)));
+    }
+
+    // pad the end with zeros, but make sure to not forget the extra bytes
+    if (extraBytes === 1) {
+      tmp = uint8[len - 1];
+      output += lookup[tmp >> 2];
+      output += lookup[(tmp << 4) & 0x3F];
+      output += '==';
+    } else if (extraBytes === 2) {
+      tmp = (uint8[len - 2] << 8) + (uint8[len - 1]);
+      output += lookup[tmp >> 10];
+      output += lookup[(tmp >> 4) & 0x3F];
+      output += lookup[(tmp << 2) & 0x3F];
+      output += '=';
+    }
+
+    parts.push(output);
+
+    return parts.join('')
+  }
+
+  function read (buffer, offset, isLE, mLen, nBytes) {
+    var e, m;
+    var eLen = nBytes * 8 - mLen - 1;
+    var eMax = (1 << eLen) - 1;
+    var eBias = eMax >> 1;
+    var nBits = -7;
+    var i = isLE ? (nBytes - 1) : 0;
+    var d = isLE ? -1 : 1;
+    var s = buffer[offset + i];
+
+    i += d;
+
+    e = s & ((1 << (-nBits)) - 1);
+    s >>= (-nBits);
+    nBits += eLen;
+    for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}
+
+    m = e & ((1 << (-nBits)) - 1);
+    e >>= (-nBits);
+    nBits += mLen;
+    for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}
+
+    if (e === 0) {
+      e = 1 - eBias;
+    } else if (e === eMax) {
+      return m ? NaN : ((s ? -1 : 1) * Infinity)
+    } else {
+      m = m + Math.pow(2, mLen);
+      e = e - eBias;
+    }
+    return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
+  }
+
+  function write (buffer, value, offset, isLE, mLen, nBytes) {
+    var e, m, c;
+    var eLen = nBytes * 8 - mLen - 1;
+    var eMax = (1 << eLen) - 1;
+    var eBias = eMax >> 1;
+    var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0);
+    var i = isLE ? 0 : (nBytes - 1);
+    var d = isLE ? 1 : -1;
+    var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0;
+
+    value = Math.abs(value);
+
+    if (isNaN(value) || value === Infinity) {
+      m = isNaN(value) ? 1 : 0;
+      e = eMax;
+    } else {
+      e = Math.floor(Math.log(value) / Math.LN2);
+      if (value * (c = Math.pow(2, -e)) < 1) {
+        e--;
+        c *= 2;
+      }
+      if (e + eBias >= 1) {
+        value += rt / c;
+      } else {
+        value += rt * Math.pow(2, 1 - eBias);
+      }
+      if (value * c >= 2) {
+        e++;
+        c /= 2;
+      }
+
+      if (e + eBias >= eMax) {
+        m = 0;
+        e = eMax;
+      } else if (e + eBias >= 1) {
+        m = (value * c - 1) * Math.pow(2, mLen);
+        e = e + eBias;
+      } else {
+        m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen);
+        e = 0;
+      }
+    }
+
+    for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
+
+    e = (e << mLen) | m;
+    eLen += mLen;
+    for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
+
+    buffer[offset + i - d] |= s * 128;
+  }
+
+  var toString = {}.toString;
+
+  var isArray = Array.isArray || function (arr) {
+    return toString.call(arr) == '[object Array]';
+  };
+
+  /*!
+   * The buffer module from node.js, for the browser.
+   *
+   * @author   Feross Aboukhadijeh <fe...@feross.org> <http://feross.org>
+   * @license  MIT
+   */
+
+  var INSPECT_MAX_BYTES = 50;
+
+  /**
+   * If `Buffer.TYPED_ARRAY_SUPPORT`:
+   *   === true    Use Uint8Array implementation (fastest)
+   *   === false   Use Object implementation (most compatible, even IE6)
+   *
+   * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,
+   * Opera 11.6+, iOS 4.2+.
+   *
+   * Due to various browser bugs, sometimes the Object implementation will be used even
+   * when the browser supports typed arrays.
+   *
+   * Note:
+   *
+   *   - Firefox 4-29 lacks support for adding new properties to `Uint8Array` instances,
+   *     See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438.
+   *
+   *   - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function.
+   *
+   *   - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of
+   *     incorrect length in some situations.
+
+   * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they
+   * get the Object implementation, which is slower but behaves correctly.
+   */
+  Buffer.TYPED_ARRAY_SUPPORT = global$1.TYPED_ARRAY_SUPPORT !== undefined
+    ? global$1.TYPED_ARRAY_SUPPORT
+    : true;
+
+  /*
+   * Export kMaxLength after typed array support is determined.
+   */
+  kMaxLength();
+
+  function kMaxLength () {
+    return Buffer.TYPED_ARRAY_SUPPORT
+      ? 0x7fffffff
+      : 0x3fffffff
+  }
+
+  function createBuffer (that, length) {
+    if (kMaxLength() < length) {
+      throw new RangeError('Invalid typed array length')
+    }
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      // Return an augmented `Uint8Array` instance, for best performance
+      that = new Uint8Array(length);
+      that.__proto__ = Buffer.prototype;
+    } else {
+      // Fallback: Return an object instance of the Buffer class
+      if (that === null) {
+        that = new Buffer(length);
+      }
+      that.length = length;
+    }
+
+    return that
+  }
+
+  /**
+   * The Buffer constructor returns instances of `Uint8Array` that have their
+   * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of
+   * `Uint8Array`, so the returned instances will have all the node `Buffer` methods
+   * and the `Uint8Array` methods. Square bracket notation works as expected -- it
+   * returns a single octet.
+   *
+   * The `Uint8Array` prototype remains unmodified.
+   */
+
+  function Buffer (arg, encodingOrOffset, length) {
+    if (!Buffer.TYPED_ARRAY_SUPPORT && !(this instanceof Buffer)) {
+      return new Buffer(arg, encodingOrOffset, length)
+    }
+
+    // Common case.
+    if (typeof arg === 'number') {
+      if (typeof encodingOrOffset === 'string') {
+        throw new Error(
+          'If encoding is specified then the first argument must be a string'
+        )
+      }
+      return allocUnsafe(this, arg)
+    }
+    return from(this, arg, encodingOrOffset, length)
+  }
+
+  Buffer.poolSize = 8192; // not used by this implementation
+
+  // TODO: Legacy, not needed anymore. Remove in next major version.
+  Buffer._augment = function (arr) {
+    arr.__proto__ = Buffer.prototype;
+    return arr
+  };
+
+  function from (that, value, encodingOrOffset, length) {
+    if (typeof value === 'number') {
+      throw new TypeError('"value" argument must not be a number')
+    }
+
+    if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
+      return fromArrayBuffer(that, value, encodingOrOffset, length)
+    }
+
+    if (typeof value === 'string') {
+      return fromString(that, value, encodingOrOffset)
+    }
+
+    return fromObject(that, value)
+  }
+
+  /**
+   * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError
+   * if value is a number.
+   * Buffer.from(str[, encoding])
+   * Buffer.from(array)
+   * Buffer.from(buffer)
+   * Buffer.from(arrayBuffer[, byteOffset[, length]])
+   **/
+  Buffer.from = function (value, encodingOrOffset, length) {
+    return from(null, value, encodingOrOffset, length)
+  };
+
+  if (Buffer.TYPED_ARRAY_SUPPORT) {
+    Buffer.prototype.__proto__ = Uint8Array.prototype;
+    Buffer.__proto__ = Uint8Array;
+  }
+
+  function assertSize (size) {
+    if (typeof size !== 'number') {
+      throw new TypeError('"size" argument must be a number')
+    } else if (size < 0) {
+      throw new RangeError('"size" argument must not be negative')
+    }
+  }
+
+  function alloc (that, size, fill, encoding) {
+    assertSize(size);
+    if (size <= 0) {
+      return createBuffer(that, size)
+    }
+    if (fill !== undefined) {
+      // Only pay attention to encoding if it's a string. This
+      // prevents accidentally sending in a number that would
+      // be interpretted as a start offset.
+      return typeof encoding === 'string'
+        ? createBuffer(that, size).fill(fill, encoding)
+        : createBuffer(that, size).fill(fill)
+    }
+    return createBuffer(that, size)
+  }
+
+  /**
+   * Creates a new filled Buffer instance.
+   * alloc(size[, fill[, encoding]])
+   **/
+  Buffer.alloc = function (size, fill, encoding) {
+    return alloc(null, size, fill, encoding)
+  };
+
+  function allocUnsafe (that, size) {
+    assertSize(size);
+    that = createBuffer(that, size < 0 ? 0 : checked(size) | 0);
+    if (!Buffer.TYPED_ARRAY_SUPPORT) {
+      for (var i = 0; i < size; ++i) {
+        that[i] = 0;
+      }
+    }
+    return that
+  }
+
+  /**
+   * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance.
+   * */
+  Buffer.allocUnsafe = function (size) {
+    return allocUnsafe(null, size)
+  };
+  /**
+   * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance.
+   */
+  Buffer.allocUnsafeSlow = function (size) {
+    return allocUnsafe(null, size)
+  };
+
+  function fromString (that, string, encoding) {
+    if (typeof encoding !== 'string' || encoding === '') {
+      encoding = 'utf8';
+    }
+
+    if (!Buffer.isEncoding(encoding)) {
+      throw new TypeError('"encoding" must be a valid string encoding')
+    }
+
+    var length = byteLength(string, encoding) | 0;
+    that = createBuffer(that, length);
+
+    var actual = that.write(string, encoding);
+
+    if (actual !== length) {
+      // Writing a hex string, for example, that contains invalid characters will
+      // cause everything after the first invalid character to be ignored. (e.g.
+      // 'abxxcd' will be treated as 'ab')
+      that = that.slice(0, actual);
+    }
+
+    return that
+  }
+
+  function fromArrayLike (that, array) {
+    var length = array.length < 0 ? 0 : checked(array.length) | 0;
+    that = createBuffer(that, length);
+    for (var i = 0; i < length; i += 1) {
+      that[i] = array[i] & 255;
+    }
+    return that
+  }
+
+  function fromArrayBuffer (that, array, byteOffset, length) {
+    array.byteLength; // this throws if `array` is not a valid ArrayBuffer
+
+    if (byteOffset < 0 || array.byteLength < byteOffset) {
+      throw new RangeError('\'offset\' is out of bounds')
+    }
+
+    if (array.byteLength < byteOffset + (length || 0)) {
+      throw new RangeError('\'length\' is out of bounds')
+    }
+
+    if (byteOffset === undefined && length === undefined) {
+      array = new Uint8Array(array);
+    } else if (length === undefined) {
+      array = new Uint8Array(array, byteOffset);
+    } else {
+      array = new Uint8Array(array, byteOffset, length);
+    }
+
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      // Return an augmented `Uint8Array` instance, for best performance
+      that = array;
+      that.__proto__ = Buffer.prototype;
+    } else {
+      // Fallback: Return an object instance of the Buffer class
+      that = fromArrayLike(that, array);
+    }
+    return that
+  }
+
+  function fromObject (that, obj) {
+    if (internalIsBuffer(obj)) {
+      var len = checked(obj.length) | 0;
+      that = createBuffer(that, len);
+
+      if (that.length === 0) {
+        return that
+      }
+
+      obj.copy(that, 0, 0, len);
+      return that
+    }
+
+    if (obj) {
+      if ((typeof ArrayBuffer !== 'undefined' &&
+          obj.buffer instanceof ArrayBuffer) || 'length' in obj) {
+        if (typeof obj.length !== 'number' || isnan(obj.length)) {
+          return createBuffer(that, 0)
+        }
+        return fromArrayLike(that, obj)
+      }
+
+      if (obj.type === 'Buffer' && isArray(obj.data)) {
+        return fromArrayLike(that, obj.data)
+      }
+    }
+
+    throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.')
+  }
+
+  function checked (length) {
+    // Note: cannot use `length < kMaxLength()` here because that fails when
+    // length is NaN (which is otherwise coerced to zero.)
+    if (length >= kMaxLength()) {
+      throw new RangeError('Attempt to allocate Buffer larger than maximum ' +
+                           'size: 0x' + kMaxLength().toString(16) + ' bytes')
+    }
+    return length | 0
+  }
+  Buffer.isBuffer = isBuffer;
+  function internalIsBuffer (b) {
+    return !!(b != null && b._isBuffer)
+  }
+
+  Buffer.compare = function compare (a, b) {
+    if (!internalIsBuffer(a) || !internalIsBuffer(b)) {
+      throw new TypeError('Arguments must be Buffers')
+    }
+
+    if (a === b) return 0
+
+    var x = a.length;
+    var y = b.length;
+
+    for (var i = 0, len = Math.min(x, y); i < len; ++i) {
+      if (a[i] !== b[i]) {
+        x = a[i];
+        y = b[i];
+        break
+      }
+    }
+
+    if (x < y) return -1
+    if (y < x) return 1
+    return 0
+  };
+
+  Buffer.isEncoding = function isEncoding (encoding) {
+    switch (String(encoding).toLowerCase()) {
+      case 'hex':
+      case 'utf8':
+      case 'utf-8':
+      case 'ascii':
+      case 'latin1':
+      case 'binary':
+      case 'base64':
+      case 'ucs2':
+      case 'ucs-2':
+      case 'utf16le':
+      case 'utf-16le':
+        return true
+      default:
+        return false
+    }
+  };
+
+  Buffer.concat = function concat (list, length) {
+    if (!isArray(list)) {
+      throw new TypeError('"list" argument must be an Array of Buffers')
+    }
+
+    if (list.length === 0) {
+      return Buffer.alloc(0)
+    }
+
+    var i;
+    if (length === undefined) {
+      length = 0;
+      for (i = 0; i < list.length; ++i) {
+        length += list[i].length;
+      }
+    }
+
+    var buffer = Buffer.allocUnsafe(length);
+    var pos = 0;
+    for (i = 0; i < list.length; ++i) {
+      var buf = list[i];
+      if (!internalIsBuffer(buf)) {
+        throw new TypeError('"list" argument must be an Array of Buffers')
+      }
+      buf.copy(buffer, pos);
+      pos += buf.length;
+    }
+    return buffer
+  };
+
+  function byteLength (string, encoding) {
+    if (internalIsBuffer(string)) {
+      return string.length
+    }
+    if (typeof ArrayBuffer !== 'undefined' && typeof ArrayBuffer.isView === 'function' &&
+        (ArrayBuffer.isView(string) || string instanceof ArrayBuffer)) {
+      return string.byteLength
+    }
+    if (typeof string !== 'string') {
+      string = '' + string;
+    }
+
+    var len = string.length;
+    if (len === 0) return 0
+
+    // Use a for loop to avoid recursion
+    var loweredCase = false;
+    for (;;) {
+      switch (encoding) {
+        case 'ascii':
+        case 'latin1':
+        case 'binary':
+          return len
+        case 'utf8':
+        case 'utf-8':
+        case undefined:
+          return utf8ToBytes(string).length
+        case 'ucs2':
+        case 'ucs-2':
+        case 'utf16le':
+        case 'utf-16le':
+          return len * 2
+        case 'hex':
+          return len >>> 1
+        case 'base64':
+          return base64ToBytes(string).length
+        default:
+          if (loweredCase) return utf8ToBytes(string).length // assume utf8
+          encoding = ('' + encoding).toLowerCase();
+          loweredCase = true;
+      }
+    }
+  }
+  Buffer.byteLength = byteLength;
+
+  function slowToString (encoding, start, end) {
+    var loweredCase = false;
+
+    // No need to verify that "this.length <= MAX_UINT32" since it's a read-only
+    // property of a typed array.
+
+    // This behaves neither like String nor Uint8Array in that we set start/end
+    // to their upper/lower bounds if the value passed is out of range.
+    // undefined is handled specially as per ECMA-262 6th Edition,
+    // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization.
+    if (start === undefined || start < 0) {
+      start = 0;
+    }
+    // Return early if start > this.length. Done here to prevent potential uint32
+    // coercion fail below.
+    if (start > this.length) {
+      return ''
+    }
+
+    if (end === undefined || end > this.length) {
+      end = this.length;
+    }
+
+    if (end <= 0) {
+      return ''
+    }
+
+    // Force coersion to uint32. This will also coerce falsey/NaN values to 0.
+    end >>>= 0;
+    start >>>= 0;
+
+    if (end <= start) {
+      return ''
+    }
+
+    if (!encoding) encoding = 'utf8';
+
+    while (true) {
+      switch (encoding) {
+        case 'hex':
+          return hexSlice(this, start, end)
+
+        case 'utf8':
+        case 'utf-8':
+          return utf8Slice(this, start, end)
+
+        case 'ascii':
+          return asciiSlice(this, start, end)
+
+        case 'latin1':
+        case 'binary':
+          return latin1Slice(this, start, end)
+
+        case 'base64':
+          return base64Slice(this, start, end)
+
+        case 'ucs2':
+        case 'ucs-2':
+        case 'utf16le':
+        case 'utf-16le':
+          return utf16leSlice(this, start, end)
+
+        default:
+          if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
+          encoding = (encoding + '').toLowerCase();
+          loweredCase = true;
+      }
+    }
+  }
+
+  // The property is used by `Buffer.isBuffer` and `is-buffer` (in Safari 5-7) to detect
+  // Buffer instances.
+  Buffer.prototype._isBuffer = true;
+
+  function swap (b, n, m) {
+    var i = b[n];
+    b[n] = b[m];
+    b[m] = i;
+  }
+
+  Buffer.prototype.swap16 = function swap16 () {
+    var len = this.length;
+    if (len % 2 !== 0) {
+      throw new RangeError('Buffer size must be a multiple of 16-bits')
+    }
+    for (var i = 0; i < len; i += 2) {
+      swap(this, i, i + 1);
+    }
+    return this
+  };
+
+  Buffer.prototype.swap32 = function swap32 () {
+    var len = this.length;
+    if (len % 4 !== 0) {
+      throw new RangeError('Buffer size must be a multiple of 32-bits')
+    }
+    for (var i = 0; i < len; i += 4) {
+      swap(this, i, i + 3);
+      swap(this, i + 1, i + 2);
+    }
+    return this
+  };
+
+  Buffer.prototype.swap64 = function swap64 () {
+    var len = this.length;
+    if (len % 8 !== 0) {
+      throw new RangeError('Buffer size must be a multiple of 64-bits')
+    }
+    for (var i = 0; i < len; i += 8) {
+      swap(this, i, i + 7);
+      swap(this, i + 1, i + 6);
+      swap(this, i + 2, i + 5);
+      swap(this, i + 3, i + 4);
+    }
+    return this
+  };
+
+  Buffer.prototype.toString = function toString () {
+    var length = this.length | 0;
+    if (length === 0) return ''
+    if (arguments.length === 0) return utf8Slice(this, 0, length)
+    return slowToString.apply(this, arguments)
+  };
+
+  Buffer.prototype.equals = function equals (b) {
+    if (!internalIsBuffer(b)) throw new TypeError('Argument must be a Buffer')
+    if (this === b) return true
+    return Buffer.compare(this, b) === 0
+  };
+
+  Buffer.prototype.inspect = function inspect () {
+    var str = '';
+    var max = INSPECT_MAX_BYTES;
+    if (this.length > 0) {
+      str = this.toString('hex', 0, max).match(/.{2}/g).join(' ');
+      if (this.length > max) str += ' ... ';
+    }
+    return '<Buffer ' + str + '>'
+  };
+
+  Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) {
+    if (!internalIsBuffer(target)) {
+      throw new TypeError('Argument must be a Buffer')
+    }
+
+    if (start === undefined) {
+      start = 0;
+    }
+    if (end === undefined) {
+      end = target ? target.length : 0;
+    }
+    if (thisStart === undefined) {
+      thisStart = 0;
+    }
+    if (thisEnd === undefined) {
+      thisEnd = this.length;
+    }
+
+    if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) {
+      throw new RangeError('out of range index')
+    }
+
+    if (thisStart >= thisEnd && start >= end) {
+      return 0
+    }
+    if (thisStart >= thisEnd) {
+      return -1
+    }
+    if (start >= end) {
+      return 1
+    }
+
+    start >>>= 0;
+    end >>>= 0;
+    thisStart >>>= 0;
+    thisEnd >>>= 0;
+
+    if (this === target) return 0
+
+    var x = thisEnd - thisStart;
+    var y = end - start;
+    var len = Math.min(x, y);
+
+    var thisCopy = this.slice(thisStart, thisEnd);
+    var targetCopy = target.slice(start, end);
+
+    for (var i = 0; i < len; ++i) {
+      if (thisCopy[i] !== targetCopy[i]) {
+        x = thisCopy[i];
+        y = targetCopy[i];
+        break
+      }
+    }
+
+    if (x < y) return -1
+    if (y < x) return 1
+    return 0
+  };
+
+  // Finds either the first index of `val` in `buffer` at offset >= `byteOffset`,
+  // OR the last index of `val` in `buffer` at offset <= `byteOffset`.
+  //
+  // Arguments:
+  // - buffer - a Buffer to search
+  // - val - a string, Buffer, or number
+  // - byteOffset - an index into `buffer`; will be clamped to an int32
+  // - encoding - an optional encoding, relevant is val is a string
+  // - dir - true for indexOf, false for lastIndexOf
+  function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) {
+    // Empty buffer means no match
+    if (buffer.length === 0) return -1
+
+    // Normalize byteOffset
+    if (typeof byteOffset === 'string') {
+      encoding = byteOffset;
+      byteOffset = 0;
+    } else if (byteOffset > 0x7fffffff) {
+      byteOffset = 0x7fffffff;
+    } else if (byteOffset < -0x80000000) {
+      byteOffset = -0x80000000;
+    }
+    byteOffset = +byteOffset;  // Coerce to Number.
+    if (isNaN(byteOffset)) {
+      // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer
+      byteOffset = dir ? 0 : (buffer.length - 1);
+    }
+
+    // Normalize byteOffset: negative offsets start from the end of the buffer
+    if (byteOffset < 0) byteOffset = buffer.length + byteOffset;
+    if (byteOffset >= buffer.length) {
+      if (dir) return -1
+      else byteOffset = buffer.length - 1;
+    } else if (byteOffset < 0) {
+      if (dir) byteOffset = 0;
+      else return -1
+    }
+
+    // Normalize val
+    if (typeof val === 'string') {
+      val = Buffer.from(val, encoding);
+    }
+
+    // Finally, search either indexOf (if dir is true) or lastIndexOf
+    if (internalIsBuffer(val)) {
+      // Special case: looking for empty string/buffer always fails
+      if (val.length === 0) {
+        return -1
+      }
+      return arrayIndexOf(buffer, val, byteOffset, encoding, dir)
+    } else if (typeof val === 'number') {
+      val = val & 0xFF; // Search for a byte value [0-255]
+      if (Buffer.TYPED_ARRAY_SUPPORT &&
+          typeof Uint8Array.prototype.indexOf === 'function') {
+        if (dir) {
+          return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset)
+        } else {
+          return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset)
+        }
+      }
+      return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir)
+    }
+
+    throw new TypeError('val must be string, number or Buffer')
+  }
+
+  function arrayIndexOf (arr, val, byteOffset, encoding, dir) {
+    var indexSize = 1;
+    var arrLength = arr.length;
+    var valLength = val.length;
+
+    if (encoding !== undefined) {
+      encoding = String(encoding).toLowerCase();
+      if (encoding === 'ucs2' || encoding === 'ucs-2' ||
+          encoding === 'utf16le' || encoding === 'utf-16le') {
+        if (arr.length < 2 || val.length < 2) {
+          return -1
+        }
+        indexSize = 2;
+        arrLength /= 2;
+        valLength /= 2;
+        byteOffset /= 2;
+      }
+    }
+
+    function read (buf, i) {
+      if (indexSize === 1) {
+        return buf[i]
+      } else {
+        return buf.readUInt16BE(i * indexSize)
+      }
+    }
+
+    var i;
+    if (dir) {
+      var foundIndex = -1;
+      for (i = byteOffset; i < arrLength; i++) {
+        if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) {
+          if (foundIndex === -1) foundIndex = i;
+          if (i - foundIndex + 1 === valLength) return foundIndex * indexSize
+        } else {
+          if (foundIndex !== -1) i -= i - foundIndex;
+          foundIndex = -1;
+        }
+      }
+    } else {
+      if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength;
+      for (i = byteOffset; i >= 0; i--) {
+        var found = true;
+        for (var j = 0; j < valLength; j++) {
+          if (read(arr, i + j) !== read(val, j)) {
+            found = false;
+            break
+          }
+        }
+        if (found) return i
+      }
+    }
+
+    return -1
+  }
+
+  Buffer.prototype.includes = function includes (val, byteOffset, encoding) {
+    return this.indexOf(val, byteOffset, encoding) !== -1
+  };
+
+  Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) {
+    return bidirectionalIndexOf(this, val, byteOffset, encoding, true)
+  };
+
+  Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) {
+    return bidirectionalIndexOf(this, val, byteOffset, encoding, false)
+  };
+
+  function hexWrite (buf, string, offset, length) {
+    offset = Number(offset) || 0;
+    var remaining = buf.length - offset;
+    if (!length) {
+      length = remaining;
+    } else {
+      length = Number(length);
+      if (length > remaining) {
+        length = remaining;
+      }
+    }
+
+    // must be an even number of digits
+    var strLen = string.length;
+    if (strLen % 2 !== 0) throw new TypeError('Invalid hex string')
+
+    if (length > strLen / 2) {
+      length = strLen / 2;
+    }
+    for (var i = 0; i < length; ++i) {
+      var parsed = parseInt(string.substr(i * 2, 2), 16);
+      if (isNaN(parsed)) return i
+      buf[offset + i] = parsed;
+    }
+    return i
+  }
+
+  function utf8Write (buf, string, offset, length) {
+    return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length)
+  }
+
+  function asciiWrite (buf, string, offset, length) {
+    return blitBuffer(asciiToBytes(string), buf, offset, length)
+  }
+
+  function latin1Write (buf, string, offset, length) {
+    return asciiWrite(buf, string, offset, length)
+  }
+
+  function base64Write (buf, string, offset, length) {
+    return blitBuffer(base64ToBytes(string), buf, offset, length)
+  }
+
+  function ucs2Write (buf, string, offset, length) {
+    return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length)
+  }
+
+  Buffer.prototype.write = function write (string, offset, length, encoding) {
+    // Buffer#write(string)
+    if (offset === undefined) {
+      encoding = 'utf8';
+      length = this.length;
+      offset = 0;
+    // Buffer#write(string, encoding)
+    } else if (length === undefined && typeof offset === 'string') {
+      encoding = offset;
+      length = this.length;
+      offset = 0;
+    // Buffer#write(string, offset[, length][, encoding])
+    } else if (isFinite(offset)) {
+      offset = offset | 0;
+      if (isFinite(length)) {
+        length = length | 0;
+        if (encoding === undefined) encoding = 'utf8';
+      } else {
+        encoding = length;
+        length = undefined;
+      }
+    // legacy write(string, encoding, offset, length) - remove in v0.13
+    } else {
+      throw new Error(
+        'Buffer.write(string, encoding, offset[, length]) is no longer supported'
+      )
+    }
+
+    var remaining = this.length - offset;
+    if (length === undefined || length > remaining) length = remaining;
+
+    if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) {
+      throw new RangeError('Attempt to write outside buffer bounds')
+    }
+
+    if (!encoding) encoding = 'utf8';
+
+    var loweredCase = false;
+    for (;;) {
+      switch (encoding) {
+        case 'hex':
+          return hexWrite(this, string, offset, length)
+
+        case 'utf8':
+        case 'utf-8':
+          return utf8Write(this, string, offset, length)
+
+        case 'ascii':
+          return asciiWrite(this, string, offset, length)
+
+        case 'latin1':
+        case 'binary':
+          return latin1Write(this, string, offset, length)
+
+        case 'base64':
+          // Warning: maxLength not taken into account in base64Write
+          return base64Write(this, string, offset, length)
+
+        case 'ucs2':
+        case 'ucs-2':
+        case 'utf16le':
+        case 'utf-16le':
+          return ucs2Write(this, string, offset, length)
+
+        default:
+          if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
+          encoding = ('' + encoding).toLowerCase();
+          loweredCase = true;
+      }
+    }
+  };
+
+  Buffer.prototype.toJSON = function toJSON () {
+    return {
+      type: 'Buffer',
+      data: Array.prototype.slice.call(this._arr || this, 0)
+    }
+  };
+
+  function base64Slice (buf, start, end) {
+    if (start === 0 && end === buf.length) {
+      return fromByteArray(buf)
+    } else {
+      return fromByteArray(buf.slice(start, end))
+    }
+  }
+
+  function utf8Slice (buf, start, end) {
+    end = Math.min(buf.length, end);
+    var res = [];
+
+    var i = start;
+    while (i < end) {
+      var firstByte = buf[i];
+      var codePoint = null;
+      var bytesPerSequence = (firstByte > 0xEF) ? 4
+        : (firstByte > 0xDF) ? 3
+        : (firstByte > 0xBF) ? 2
+        : 1;
+
+      if (i + bytesPerSequence <= end) {
+        var secondByte, thirdByte, fourthByte, tempCodePoint;
+
+        switch (bytesPerSequence) {
+          case 1:
+            if (firstByte < 0x80) {
+              codePoint = firstByte;
+            }
+            break
+          case 2:
+            secondByte = buf[i + 1];
+            if ((secondByte & 0xC0) === 0x80) {
+              tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F);
+              if (tempCodePoint > 0x7F) {
+                codePoint = tempCodePoint;
+              }
+            }
+            break
+          case 3:
+            secondByte = buf[i + 1];
+            thirdByte = buf[i + 2];
+            if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) {
+              tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F);
+              if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) {
+                codePoint = tempCodePoint;
+              }
+            }
+            break
+          case 4:
+            secondByte = buf[i + 1];
+            thirdByte = buf[i + 2];
+            fourthByte = buf[i + 3];
+            if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) {
+              tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F);
+              if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) {
+                codePoint = tempCodePoint;
+              }
+            }
+        }
+      }
+
+      if (codePoint === null) {
+        // we did not generate a valid codePoint so insert a
+        // replacement char (U+FFFD) and advance only 1 byte
+        codePoint = 0xFFFD;
+        bytesPerSequence = 1;
+      } else if (codePoint > 0xFFFF) {
+        // encode to utf16 (surrogate pair dance)
+        codePoint -= 0x10000;
+        res.push(codePoint >>> 10 & 0x3FF | 0xD800);
+        codePoint = 0xDC00 | codePoint & 0x3FF;
+      }
+
+      res.push(codePoint);
+      i += bytesPerSequence;
+    }
+
+    return decodeCodePointsArray(res)
+  }
+
+  // Based on http://stackoverflow.com/a/22747272/680742, the browser with
+  // the lowest limit is Chrome, with 0x10000 args.
+  // We go 1 magnitude less, for safety
+  var MAX_ARGUMENTS_LENGTH = 0x1000;
+
+  function decodeCodePointsArray (codePoints) {
+    var len = codePoints.length;
+    if (len <= MAX_ARGUMENTS_LENGTH) {
+      return String.fromCharCode.apply(String, codePoints) // avoid extra slice()
+    }
+
+    // Decode in chunks to avoid "call stack size exceeded".
+    var res = '';
+    var i = 0;
+    while (i < len) {
+      res += String.fromCharCode.apply(
+        String,
+        codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH)
+      );
+    }
+    return res
+  }
+
+  function asciiSlice (buf, start, end) {
+    var ret = '';
+    end = Math.min(buf.length, end);
+
+    for (var i = start; i < end; ++i) {
+      ret += String.fromCharCode(buf[i] & 0x7F);
+    }
+    return ret
+  }
+
+  function latin1Slice (buf, start, end) {
+    var ret = '';
+    end = Math.min(buf.length, end);
+
+    for (var i = start; i < end; ++i) {
+      ret += String.fromCharCode(buf[i]);
+    }
+    return ret
+  }
+
+  function hexSlice (buf, start, end) {
+    var len = buf.length;
+
+    if (!start || start < 0) start = 0;
+    if (!end || end < 0 || end > len) end = len;
+
+    var out = '';
+    for (var i = start; i < end; ++i) {
+      out += toHex(buf[i]);
+    }
+    return out
+  }
+
+  function utf16leSlice (buf, start, end) {
+    var bytes = buf.slice(start, end);
+    var res = '';
+    for (var i = 0; i < bytes.length; i += 2) {
+      res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256);
+    }
+    return res
+  }
+
+  Buffer.prototype.slice = function slice (start, end) {
+    var len = this.length;
+    start = ~~start;
+    end = end === undefined ? len : ~~end;
+
+    if (start < 0) {
+      start += len;
+      if (start < 0) start = 0;
+    } else if (start > len) {
+      start = len;
+    }
+
+    if (end < 0) {
+      end += len;
+      if (end < 0) end = 0;
+    } else if (end > len) {
+      end = len;
+    }
+
+    if (end < start) end = start;
+
+    var newBuf;
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      newBuf = this.subarray(start, end);
+      newBuf.__proto__ = Buffer.prototype;
+    } else {
+      var sliceLen = end - start;
+      newBuf = new Buffer(sliceLen, undefined);
+      for (var i = 0; i < sliceLen; ++i) {
+        newBuf[i] = this[i + start];
+      }
+    }
+
+    return newBuf
+  };
+
+  /*
+   * Need to make sure that buffer isn't trying to write out of bounds.
+   */
+  function checkOffset (offset, ext, length) {
+    if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint')
+    if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length')
+  }
+
+  Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) {
+    offset = offset | 0;
+    byteLength = byteLength | 0;
+    if (!noAssert) checkOffset(offset, byteLength, this.length);
+
+    var val = this[offset];
+    var mul = 1;
+    var i = 0;
+    while (++i < byteLength && (mul *= 0x100)) {
+      val += this[offset + i] * mul;
+    }
+
+    return val
+  };
+
+  Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) {
+    offset = offset | 0;
+    byteLength = byteLength | 0;
+    if (!noAssert) {
+      checkOffset(offset, byteLength, this.length);
+    }
+
+    var val = this[offset + --byteLength];
+    var mul = 1;
+    while (byteLength > 0 && (mul *= 0x100)) {
+      val += this[offset + --byteLength] * mul;
+    }
+
+    return val
+  };
+
+  Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 1, this.length);
+    return this[offset]
+  };
+
+  Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 2, this.length);
+    return this[offset] | (this[offset + 1] << 8)
+  };
+
+  Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 2, this.length);
+    return (this[offset] << 8) | this[offset + 1]
+  };
+
+  Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 4, this.length);
+
+    return ((this[offset]) |
+        (this[offset + 1] << 8) |
+        (this[offset + 2] << 16)) +
+        (this[offset + 3] * 0x1000000)
+  };
+
+  Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 4, this.length);
+
+    return (this[offset] * 0x1000000) +
+      ((this[offset + 1] << 16) |
+      (this[offset + 2] << 8) |
+      this[offset + 3])
+  };
+
+  Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) {
+    offset = offset | 0;
+    byteLength = byteLength | 0;
+    if (!noAssert) checkOffset(offset, byteLength, this.length);
+
+    var val = this[offset];
+    var mul = 1;
+    var i = 0;
+    while (++i < byteLength && (mul *= 0x100)) {
+      val += this[offset + i] * mul;
+    }
+    mul *= 0x80;
+
+    if (val >= mul) val -= Math.pow(2, 8 * byteLength);
+
+    return val
+  };
+
+  Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) {
+    offset = offset | 0;
+    byteLength = byteLength | 0;
+    if (!noAssert) checkOffset(offset, byteLength, this.length);
+
+    var i = byteLength;
+    var mul = 1;
+    var val = this[offset + --i];
+    while (i > 0 && (mul *= 0x100)) {
+      val += this[offset + --i] * mul;
+    }
+    mul *= 0x80;
+
+    if (val >= mul) val -= Math.pow(2, 8 * byteLength);
+
+    return val
+  };
+
+  Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 1, this.length);
+    if (!(this[offset] & 0x80)) return (this[offset])
+    return ((0xff - this[offset] + 1) * -1)
+  };
+
+  Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 2, this.length);
+    var val = this[offset] | (this[offset + 1] << 8);
+    return (val & 0x8000) ? val | 0xFFFF0000 : val
+  };
+
+  Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 2, this.length);
+    var val = this[offset + 1] | (this[offset] << 8);
+    return (val & 0x8000) ? val | 0xFFFF0000 : val
+  };
+
+  Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 4, this.length);
+
+    return (this[offset]) |
+      (this[offset + 1] << 8) |
+      (this[offset + 2] << 16) |
+      (this[offset + 3] << 24)
+  };
+
+  Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 4, this.length);
+
+    return (this[offset] << 24) |
+      (this[offset + 1] << 16) |
+      (this[offset + 2] << 8) |
+      (this[offset + 3])
+  };
+
+  Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 4, this.length);
+    return read(this, offset, true, 23, 4)
+  };
+
+  Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 4, this.length);
+    return read(this, offset, false, 23, 4)
+  };
+
+  Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 8, this.length);
+    return read(this, offset, true, 52, 8)
+  };
+
+  Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) {
+    if (!noAssert) checkOffset(offset, 8, this.length);
+    return read(this, offset, false, 52, 8)
+  };
+
+  function checkInt (buf, value, offset, ext, max, min) {
+    if (!internalIsBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance')
+    if (value > max || value < min) throw new RangeError('"value" argument is out of bounds')
+    if (offset + ext > buf.length) throw new RangeError('Index out of range')
+  }
+
+  Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    byteLength = byteLength | 0;
+    if (!noAssert) {
+      var maxBytes = Math.pow(2, 8 * byteLength) - 1;
+      checkInt(this, value, offset, byteLength, maxBytes, 0);
+    }
+
+    var mul = 1;
+    var i = 0;
+    this[offset] = value & 0xFF;
+    while (++i < byteLength && (mul *= 0x100)) {
+      this[offset + i] = (value / mul) & 0xFF;
+    }
+
+    return offset + byteLength
+  };
+
+  Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    byteLength = byteLength | 0;
+    if (!noAssert) {
+      var maxBytes = Math.pow(2, 8 * byteLength) - 1;
+      checkInt(this, value, offset, byteLength, maxBytes, 0);
+    }
+
+    var i = byteLength - 1;
+    var mul = 1;
+    this[offset + i] = value & 0xFF;
+    while (--i >= 0 && (mul *= 0x100)) {
+      this[offset + i] = (value / mul) & 0xFF;
+    }
+
+    return offset + byteLength
+  };
+
+  Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0);
+    if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value);
+    this[offset] = (value & 0xff);
+    return offset + 1
+  };
+
+  function objectWriteUInt16 (buf, value, offset, littleEndian) {
+    if (value < 0) value = 0xffff + value + 1;
+    for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; ++i) {
+      buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>>
+        (littleEndian ? i : 1 - i) * 8;
+    }
+  }
+
+  Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0);
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset] = (value & 0xff);
+      this[offset + 1] = (value >>> 8);
+    } else {
+      objectWriteUInt16(this, value, offset, true);
+    }
+    return offset + 2
+  };
+
+  Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0);
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset] = (value >>> 8);
+      this[offset + 1] = (value & 0xff);
+    } else {
+      objectWriteUInt16(this, value, offset, false);
+    }
+    return offset + 2
+  };
+
+  function objectWriteUInt32 (buf, value, offset, littleEndian) {
+    if (value < 0) value = 0xffffffff + value + 1;
+    for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; ++i) {
+      buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff;
+    }
+  }
+
+  Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0);
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset + 3] = (value >>> 24);
+      this[offset + 2] = (value >>> 16);
+      this[offset + 1] = (value >>> 8);
+      this[offset] = (value & 0xff);
+    } else {
+      objectWriteUInt32(this, value, offset, true);
+    }
+    return offset + 4
+  };
+
+  Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0);
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset] = (value >>> 24);
+      this[offset + 1] = (value >>> 16);
+      this[offset + 2] = (value >>> 8);
+      this[offset + 3] = (value & 0xff);
+    } else {
+      objectWriteUInt32(this, value, offset, false);
+    }
+    return offset + 4
+  };
+
+  Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) {
+      var limit = Math.pow(2, 8 * byteLength - 1);
+
+      checkInt(this, value, offset, byteLength, limit - 1, -limit);
+    }
+
+    var i = 0;
+    var mul = 1;
+    var sub = 0;
+    this[offset] = value & 0xFF;
+    while (++i < byteLength && (mul *= 0x100)) {
+      if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) {
+        sub = 1;
+      }
+      this[offset + i] = ((value / mul) >> 0) - sub & 0xFF;
+    }
+
+    return offset + byteLength
+  };
+
+  Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) {
+      var limit = Math.pow(2, 8 * byteLength - 1);
+
+      checkInt(this, value, offset, byteLength, limit - 1, -limit);
+    }
+
+    var i = byteLength - 1;
+    var mul = 1;
+    var sub = 0;
+    this[offset + i] = value & 0xFF;
+    while (--i >= 0 && (mul *= 0x100)) {
+      if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) {
+        sub = 1;
+      }
+      this[offset + i] = ((value / mul) >> 0) - sub & 0xFF;
+    }
+
+    return offset + byteLength
+  };
+
+  Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80);
+    if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value);
+    if (value < 0) value = 0xff + value + 1;
+    this[offset] = (value & 0xff);
+    return offset + 1
+  };
+
+  Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000);
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset] = (value & 0xff);
+      this[offset + 1] = (value >>> 8);
+    } else {
+      objectWriteUInt16(this, value, offset, true);
+    }
+    return offset + 2
+  };
+
+  Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000);
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset] = (value >>> 8);
+      this[offset + 1] = (value & 0xff);
+    } else {
+      objectWriteUInt16(this, value, offset, false);
+    }
+    return offset + 2
+  };
+
+  Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000);
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset] = (value & 0xff);
+      this[offset + 1] = (value >>> 8);
+      this[offset + 2] = (value >>> 16);
+      this[offset + 3] = (value >>> 24);
+    } else {
+      objectWriteUInt32(this, value, offset, true);
+    }
+    return offset + 4
+  };
+
+  Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) {
+    value = +value;
+    offset = offset | 0;
+    if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000);
+    if (value < 0) value = 0xffffffff + value + 1;
+    if (Buffer.TYPED_ARRAY_SUPPORT) {
+      this[offset] = (value >>> 24);
+      this[offset + 1] = (value >>> 16);
+      this[offset + 2] = (value >>> 8);
+      this[offset + 3] = (value & 0xff);
+    } else {
+      objectWriteUInt32(this, value, offset, false);
+    }
+    return offset + 4
+  };
+
+  function checkIEEE754 (buf, value, offset, ext, max, min) {
+    if (offset + ext > buf.length) throw new RangeError('Index out of range')
+    if (offset < 0) throw new RangeError('Index out of range')
+  }
+
+  function writeFloat (buf, value, offset, littleEndian, noAssert) {
+    if (!noAssert) {
+      checkIEEE754(buf, value, offset, 4);
+    }
+    write(buf, value, offset, littleEndian, 23, 4);
+    return offset + 4
+  }
+
+  Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) {
+    return writeFloat(this, value, offset, true, noAssert)
+  };
+
+  Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) {
+    return writeFloat(this, value, offset, false, noAssert)
+  };
+
+  function writeDouble (buf, value, offset, littleEndian, noAssert) {
+    if (!noAssert) {
+      checkIEEE754(buf, value, offset, 8);
+    }
+    write(buf, value, offset, littleEndian, 52, 8);
+    return offset + 8
+  }
+
+  Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) {
+    return writeDouble(this, value, offset, true, noAssert)
+  };
+
+  Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) {
+    return writeDouble(this, value, offset, false, noAssert)
+  };
+
+  // copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
+  Buffer.prototype.copy = function copy (target, targetStart, start, end) {
+    if (!start) start = 0;
+    if (!end && end !== 0) end = this.length;
+    if (targetStart >= target.length) targetStart = target.length;
+    if (!targetStart) targetStart = 0;
+    if (end > 0 && end < start) end = start;
+
+    // Copy 0 bytes; we're done
+    if (end === start) return 0
+    if (target.length === 0 || this.length === 0) return 0
+
+    // Fatal error conditions
+    if (targetStart < 0) {
+      throw new RangeError('targetStart out of bounds')
+    }
+    if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds')
+    if (end < 0) throw new RangeError('sourceEnd out of bounds')
+
+    // Are we oob?
+    if (end > this.length) end = this.length;
+    if (target.length - targetStart < end - start) {
+      end = target.length - targetStart + start;
+    }
+
+    var len = end - start;
+    var i;
+
+    if (this === target && start < targetStart && targetStart < end) {
+      // descending copy from end
+      for (i = len - 1; i >= 0; --i) {
+        target[i + targetStart] = this[i + start];
+      }
+    } else if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) {
+      // ascending copy from start
+      for (i = 0; i < len; ++i) {
+        target[i + targetStart] = this[i + start];
+      }
+    } else {
+      Uint8Array.prototype.set.call(
+        target,
+        this.subarray(start, start + len),
+        targetStart
+      );
+    }
+
+    return len
+  };
+
+  // Usage:
+  //    buffer.fill(number[, offset[, end]])
+  //    buffer.fill(buffer[, offset[, end]])
+  //    buffer.fill(string[, offset[, end]][, encoding])
+  Buffer.prototype.fill = function fill (val, start, end, encoding) {
+    // Handle string cases:
+    if (typeof val === 'string') {
+      if (typeof start === 'string') {
+        encoding = start;
+        start = 0;
+        end = this.length;
+      } else if (typeof end === 'string') {
+        encoding = end;
+        end = this.length;
+      }
+      if (val.length === 1) {
+        var code = val.charCodeAt(0);
+        if (code < 256) {
+          val = code;
+        }
+      }
+      if (encoding !== undefined && typeof encoding !== 'string') {
+        throw new TypeError('encoding must be a string')
+      }
+      if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) {
+        throw new TypeError('Unknown encoding: ' + encoding)
+      }
+    } else if (typeof val === 'number') {
+      val = val & 255;
+    }
+
+    // Invalid ranges are not set to a default, so can range check early.
+    if (start < 0 || this.length < start || this.length < end) {
+      throw new RangeError('Out of range index')
+    }
+
+    if (end <= start) {
+      return this
+    }
+
+    start = start >>> 0;
+    end = end === undefined ? this.length : end >>> 0;
+
+    if (!val) val = 0;
+
+    var i;
+    if (typeof val === 'number') {
+      for (i = start; i < end; ++i) {
+        this[i] = val;
+      }
+    } else {
+      var bytes = internalIsBuffer(val)
+        ? val
+        : utf8ToBytes(new Buffer(val, encoding).toString());
+      var len = bytes.length;
+      for (i = 0; i < end - start; ++i) {
+        this[i + start] = bytes[i % len];
+      }
+    }
+
+    return this
+  };
+
+  // HELPER FUNCTIONS
+  // ================
+
+  var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g;
+
+  function base64clean (str) {
+    // Node strips out invalid characters like \n and \t from the string, base64-js does not
+    str = stringtrim(str).replace(INVALID_BASE64_RE, '');
+    // Node converts strings with length < 2 to ''
+    if (str.length < 2) return ''
+    // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
+    while (str.length % 4 !== 0) {
+      str = str + '=';
+    }
+    return str
+  }
+
+  function stringtrim (str) {
+    if (str.trim) return str.trim()
+    return str.replace(/^\s+|\s+$/g, '')
+  }
+
+  function toHex (n) {
+    if (n < 16) return '0' + n.toString(16)
+    return n.toString(16)
+  }
+
+  function utf8ToBytes (string, units) {
+    units = units || Infinity;
+    var codePoint;
+    var length = string.length;
+    var leadSurrogate = null;
+    var bytes = [];
+
+    for (var i = 0; i < length; ++i) {
+      codePoint = string.charCodeAt(i);
+
+      // is surrogate component
+      if (codePoint > 0xD7FF && codePoint < 0xE000) {
+        // last char was a lead
+        if (!leadSurrogate) {
+          // no lead yet
+          if (codePoint > 0xDBFF) {
+            // unexpected trail
+            if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
+            continue
+          } else if (i + 1 === length) {
+            // unpaired lead
+            if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
+            continue
+          }
+
+          // valid lead
+          leadSurrogate = codePoint;
+
+          continue
+        }
+
+        // 2 leads in a row
+        if (codePoint < 0xDC00) {
+          if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
+          leadSurrogate = codePoint;
+          continue
+        }
+
+        // valid surrogate pair
+        codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000;
+      } else if (leadSurrogate) {
+        // valid bmp char, but last char was a lead
+        if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
+      }
+
+      leadSurrogate = null;
+
+      // encode utf8
+      if (codePoint < 0x80) {
+        if ((units -= 1) < 0) break
+        bytes.push(codePoint);
+      } else if (codePoint < 0x800) {
+        if ((units -= 2) < 0) break
+        bytes.push(
+          codePoint >> 0x6 | 0xC0,
+          codePoint & 0x3F | 0x80
+        );
+      } else if (codePoint < 0x10000) {
+        if ((units -= 3) < 0) break
+        bytes.push(
+          codePoint >> 0xC | 0xE0,
+          codePoint >> 0x6 & 0x3F | 0x80,
+          codePoint & 0x3F | 0x80
+        );
+      } else if (codePoint < 0x110000) {
+        if ((units -= 4) < 0) break
+        bytes.push(
+          codePoint >> 0x12 | 0xF0,
+          codePoint >> 0xC & 0x3F | 0x80,
+          codePoint >> 0x6 & 0x3F | 0x80,
+          codePoint & 0x3F | 0x80
+        );
+      } else {
+        throw new Error('Invalid code point')
+      }
+    }
+
+    return bytes
+  }
+
+  function asciiToBytes (str) {
+    var byteArray = [];
+    for (var i = 0; i < str.length; ++i) {
+      // Node's code seems to be doing this and not & 0x7F..
+      byteArray.push(str.charCodeAt(i) & 0xFF);
+    }
+    return byteArray
+  }
+
+  function utf16leToBytes (str, units) {
+    var c, hi, lo;
+    var byteArray = [];
+    for (var i = 0; i < str.length; ++i) {
+      if ((units -= 2) < 0) break
+
+      c = str.charCodeAt(i);
+      hi = c >> 8;
+      lo = c % 256;
+      byteArray.push(lo);
+      byteArray.push(hi);
+    }
+
+    return byteArray
+  }
+
+
+  function base64ToBytes (str) {
+    return toByteArray(base64clean(str))
+  }
+
+  function blitBuffer (src, dst, offset, length) {
+    for (var i = 0; i < length; ++i) {
+      if ((i + offset >= dst.length) || (i >= src.length)) break
+      dst[i + offset] = src[i];
+    }
+    return i
+  }
+
+  function isnan (val) {
+    return val !== val // eslint-disable-line no-self-compare
+  }
+
+
+  // the following is from is-buffer, also by Feross Aboukhadijeh and with same lisence
+  // The _isBuffer check is for Safari 5-7 support, because it's missing
+  // Object.prototype.constructor. Remove this eventually
+  function isBuffer(obj) {
+    return obj != null && (!!obj._isBuffer || isFastBuffer(obj) || isSlowBuffer(obj))
+  }
+
+  function isFastBuffer (obj) {
+    return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
+  }
+
+  // For Node v0.10 support. Remove this eventually.
+  function isSlowBuffer (obj) {
+    return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isFastBuffer(obj.slice(0, 0))
+  }
+
+  class Provider$3 {
+    constructor() {}
+
+    getAll() {
+      return this.data;
+    }
+
+  }
+
+  var provider = Provider$3;
+
+  const Provider$2 = provider;
+
+  class Crawlers$1 extends Provider$2 {
+    constructor() {
+      super();
+      this.data = [' YLT', '^Aether', '^Amazon Simple Notification Service Agent$', '^Amazon-Route53-Health-Check-Service', '^b0t$', '^bluefish ', '^Calypso v\\/', '^COMODO DCV', '^Corax', '^DangDang', '^DavClnt', '^DHSH', '^docker\\/[0-9]', '^Expanse', '^FDM ', '^git\\/', '^Goose\\/', '^Grabber', '^Gradle\\/', '^HTTPClient\\/', '^HTTPing', '^Java\\/', '^Jeode\\/', '^Jetty\\/', '^Mail\\/', '^Mget', '^Microsoft URL Control', '^Mikrotik\\/', '^Netlab360', '^NG\\/[0-9\\.]', '^NING\\/', '^np [...]
+    }
+
+  }
+
+  var crawlers = Crawlers$1;
+
+  const Provider$1 = provider;
+
+  class Exclusions$1 extends Provider$1 {
+    constructor() {
+      super();
+      this.data = ['Safari.[\\d\\.]*', 'Firefox.[\\d\\.]*', ' Chrome.[\\d\\.]*', 'Chromium.[\\d\\.]*', 'MSIE.[\\d\\.]', 'Opera\\/[\\d\\.]*', 'Mozilla.[\\d\\.]*', 'AppleWebKit.[\\d\\.]*', 'Trident.[\\d\\.]*', 'Windows NT.[\\d\\.]*', 'Android [\\d\\.]*', 'Macintosh.', 'Ubuntu', 'Linux', '[ ]Intel', 'Mac OS X [\\d_]*', '(like )?Gecko(.[\\d\\.]*)?', 'KHTML,', 'CriOS.[\\d\\.]*', 'CPU iPhone OS ([0-9_])* like Mac OS X', 'CPU OS ([0-9_])* like Mac OS X', 'iPod', 'compatible', 'x86_..', 'i686',  [...]
+    }
+
+  }
+
+  var exclusions = Exclusions$1;
+
+  const Provider = provider;
+
+  class Headers$1 extends Provider {
+    constructor() {
+      super();
+      this.data = ['USER-AGENT', 'X-OPERAMINI-PHONE-UA', 'X-DEVICE-USER-AGENT', 'X-ORIGINAL-USER-AGENT', 'X-SKYFIRE-PHONE', 'X-BOLT-PHONE-UA', 'DEVICE-STOCK-UA', 'X-UCBROWSER-DEVICE-UA', 'FROM', 'X-SCANNER'];
+    }
+
+  }
+
+  var headers = Headers$1;
+
+  const Crawlers = crawlers;
+  const Exclusions = exclusions;
+  const Headers = headers;
+
+  class Crawler$1 {
+    constructor(request, headers, userAgent) {
+      /**
+       * Init classes
+       */
+      this._init();
+      /**
+       * This request must be an object
+       */
+
+
+      this.request = typeof request === 'object' ? request : {}; // The regex-list must not be used with g-flag!
+      // See: https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-results
+
+      this.compiledRegexList = this.compileRegex(this.crawlers.getAll(), 'i'); // The exclusions should be used with g-flag in order to remove each value.
+
+      this.compiledExclusions = this.compileRegex(this.exclusions.getAll(), 'gi');
+      /**
+       * Set http headers
+       */
+
+      this.setHttpHeaders(headers);
+      /**
+       * Set userAgent
+       */
+
+      this.userAgent = this.setUserAgent(userAgent);
+    }
+    /**
+     * Init Classes Instances
+     */
+
+
+    _init() {
+      this.crawlers = new Crawlers();
+      this.headers = new Headers();
+      this.exclusions = new Exclusions();
+    }
+
+    compileRegex(patterns, flags) {
+      return new RegExp(patterns.join('|'), flags);
+    }
+    /**
+     * Set HTTP headers.
+     */
+
+
+    setHttpHeaders(headers) {
+      // Use the Request headers if httpHeaders is not defined
+      if (typeof headers === 'undefined' || Object.keys(headers).length === 0) {
+        headers = Object.keys(this.request).length ? this.request.headers : {};
+      } // Save the headers.
+
+
+      this.httpHeaders = headers;
+    }
+    /**
+     * Set user agent
+     */
+
+
+    setUserAgent(userAgent) {
+      if (typeof userAgent === 'undefined' || userAgent === null || !userAgent.length) {
+        for (const header of this.getUaHttpHeaders()) {
+          if (Object.keys(this.httpHeaders).indexOf(header.toLowerCase()) >= 0) {
+            userAgent += this.httpHeaders[header.toLowerCase()] + ' ';
+          }
+        }
+      }
+
+      return userAgent;
+    }
+    /**
+     * Get user agent headers
+     */
+
+
+    getUaHttpHeaders() {
+      return this.headers.getAll();
+    }
+    /**
+     * Check user agent string against the regex.
+     */
+
+
+    isCrawler(userAgent = undefined) {
+      if (Buffer.byteLength(userAgent || '', 'utf8') > 4096) {
+        return false;
+      }
+
+      var agent = typeof userAgent === 'undefined' || userAgent === null ? this.userAgent : userAgent; // test on compiled regx
+
+      agent = agent.replace(this.compiledExclusions, '');
+
+      if (agent.trim().length === 0) {
+        return false;
+      }
+
+      var matches = this.compiledRegexList.exec(agent);
+
+      if (matches) {
+        this.matches = matches;
+      }
+
+      return matches !== null ? matches.length ? true : false : false;
+    }
+    /**
+     * Return the matches.
+     */
+
+
+    getMatches() {
+      return this.matches !== undefined ? this.matches.length ? this.matches[0] : null : {};
+    }
+
+  }
+
+  var crawler = Crawler$1;
+
+  const Crawler = crawler;
+  var src = {
+    Crawler,
+
+    middleware(cb) {
+      return (req, res, next) => {
+        // If there is a cb, execute it
+        if (typeof cb === 'function') {
+          cb.call(req, res);
+        } // Initiate
+
+
+        req.Crawler = new Crawler(req);
+        next();
+      };
+    }
+
+  };
+
+  /**
+   * Licensed to the Apache Software Foundation (ASF) under one or more
+   * contributor license agreements.  See the NOTICE file distributed with
+   * this work for additional information regarding copyright ownership.
+   * The ASF licenses this file to You under the Apache License, Version 2.0
+   * (the "License"); you may not use this file except in compliance with
+   * the License.  You may obtain a copy of the License at
+   *
+   * http://www.apache.org/licenses/LICENSE-2.0
+   *
+   * Unless required by applicable law or agreed to in writing, software
+   * distributed under the License is distributed on an "AS IS" BASIS,
+   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   * See the License for the specific language governing permissions and
+   * limitations under the License.
+   */
+
+  function _createForOfIteratorHelper(o, allowArrayLike) {
+    var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
+
+    if (!it) {
+      if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
+        if (it) o = it;
+        var i = 0;
+
+        var F = function F() {};
+
+        return {
+          s: F,
+          n: function n() {
+            if (i >= o.length) return {
+              done: true
+            };
+            return {
+              done: false,
+              value: o[i++]
+            };
+          },
+          e: function e(_e) {
+            throw _e;
+          },
+          f: F
+        };
+      }
+
+      throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
+    }
+
+    var normalCompletion = true,
+        didErr = false,
+        err;
+    return {
+      s: function s() {
+        it = it.call(o);
+      },
+      n: function n() {
+        var step = it.next();
+        normalCompletion = step.done;
+        return step;
+      },
+      e: function e(_e2) {
+        didErr = true;
+        err = _e2;
+      },
+      f: function f() {
+        try {
+          if (!normalCompletion && it.return != null) it.return();
+        } finally {
+          if (didErr) throw err;
+        }
+      }
+    };
+  }
+
+  function _unsupportedIterableToArray(o, minLen) {
+    if (!o) return;
+    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
+    var n = Object.prototype.toString.call(o).slice(8, -1);
+    if (n === "Object" && o.constructor) n = o.constructor.name;
+    if (n === "Map" || n === "Set") return Array.from(o);
+    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
+  }
+
+  function _arrayLikeToArray(arr, len) {
+    if (len == null || len > arr.length) len = arr.length;
+
+    for (var i = 0, arr2 = new Array(len); i < len; i++) {
+      arr2[i] = arr[i];
+    }
+
+    return arr2;
+  }
+
+  var newTracker = function newTracker() {
+    var wem = {
+      /**
+       * This function initialize the tracker
+       *
+       * @param {object} digitalData config of the tracker
+       * @returns {undefined}
+       */
+      initTracker: function initTracker(digitalData) {
+        wem.digitalData = digitalData;
+        wem.trackerProfileIdCookieName = wem.digitalData.wemInitConfig.trackerProfileIdCookieName ? wem.digitalData.wemInitConfig.trackerProfileIdCookieName : 'wem-profile-id';
+        wem.trackerSessionIdCookieName = wem.digitalData.wemInitConfig.trackerSessionIdCookieName ? wem.digitalData.wemInitConfig.trackerSessionIdCookieName : 'wem-session-id';
+        wem.browserGeneratedSessionSuffix = wem.digitalData.wemInitConfig.browserGeneratedSessionSuffix ? wem.digitalData.wemInitConfig.browserGeneratedSessionSuffix : '';
+        wem.disableTrackedConditionsListeners = wem.digitalData.wemInitConfig.disableTrackedConditionsListeners;
+        wem.activateWem = wem.digitalData.wemInitConfig.activateWem;
+        var _wem$digitalData$wemI = wem.digitalData.wemInitConfig,
+            contextServerUrl = _wem$digitalData$wemI.contextServerUrl,
+            timeoutInMilliseconds = _wem$digitalData$wemI.timeoutInMilliseconds,
+            contextServerCookieName = _wem$digitalData$wemI.contextServerCookieName;
+        wem.contextServerCookieName = contextServerCookieName;
+        wem.contextServerUrl = contextServerUrl;
+        wem.timeoutInMilliseconds = timeoutInMilliseconds;
+        wem.formNamesToWatch = [];
+        wem.eventsPrevented = [];
+        wem.sessionID = wem.getCookie(wem.trackerSessionIdCookieName);
+        wem.fallback = false;
+
+        if (wem.sessionID === null) {
+          console.warn('[WEM] sessionID is null !');
+        } else if (!wem.sessionID || wem.sessionID === '') {
+          console.warn('[WEM] empty sessionID, setting to null !');
+          wem.sessionID = null;
+        }
+      },
+
+      /**
+       * This function start the tracker by loading the context in the page
+       * Note: that the tracker will start once the current DOM is complete loaded, using listener on current document: DOMContentLoaded
+       *
+       * @param {object[]} digitalDataOverrides optional, list of digitalData extensions, they will be merged with original digitalData before context loading
+       * @returns {undefined}
+       */
+      startTracker: function startTracker() {
+        var digitalDataOverrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; // Check before start
+
+        var cookieDisabled = !navigator.cookieEnabled;
+        var noSessionID = !wem.sessionID || wem.sessionID === '';
+        var crawlerDetected = navigator.userAgent;
+
+        if (crawlerDetected) {
+          var browserDetector = new src.Crawler();
+          crawlerDetected = browserDetector.isCrawler(navigator.userAgent);
+        }
+
+        if (cookieDisabled || noSessionID || crawlerDetected) {
+          document.addEventListener('DOMContentLoaded', function () {
+            wem._executeFallback('navigator cookie disabled: ' + cookieDisabled + ', no sessionID: ' + noSessionID + ', web crawler detected: ' + crawlerDetected);
+          });
+          return;
+        } // Register base context callback
+
+
+        wem._registerCallback(function () {
+          if (wem.cxs.profileId) {
+            wem.setCookie(wem.trackerProfileIdCookieName, wem.cxs.profileId);
+          }
+
+          if (!wem.cxs.profileId) {
+            wem.removeCookie(wem.trackerProfileIdCookieName);
+          }
+
+          if (!wem.disableTrackedConditionsListeners) {
+            wem._registerListenersForTrackedConditions();
+          }
+        }, 'Default tracker', 0); // Load the context once document is ready
+
+
+        document.addEventListener('DOMContentLoaded', function () {
+          wem.DOMLoaded = true; // enrich digital data considering extensions
+
+          wem._handleDigitalDataOverrides(digitalDataOverrides); // complete already registered events
+
+
+          wem._checkUncompleteRegisteredEvents(); // Dispatch javascript events for the experience (perso/opti displayed from SSR, based on unomi events)
+
+
+          wem._dispatchJSExperienceDisplayedEvents(); // Some event may not need to be send to unomi, check for them and filter them out.
+
+
+          wem._filterUnomiEvents(); // Add referrer info into digitalData.page object.
+
+
+          wem._processReferrer(); // Build view event
+
+
+          var viewEvent = wem.buildEvent('view', wem.buildTargetPage(), wem.buildSource(wem.digitalData.site.siteInfo.siteID, 'site'));
+          viewEvent.flattenedProperties = {}; // Add URLParameters
+
+          if (location.search) {
+            viewEvent.flattenedProperties['URLParameters'] = wem.convertUrlParametersToObj(location.search);
+          } // Add interests
+
+
+          if (wem.digitalData.interests) {
+            viewEvent.flattenedProperties['interests'] = wem.digitalData.interests;
+          } // Register the page view event, it's unshift because it should be the first event, this is just for logical purpose. (page view comes before perso displayed event for example)
+
+
+          wem._registerEvent(viewEvent, true);
+
+          if (wem.activateWem) {
+            wem.loadContext();
+          } else {
+            wem._executeFallback('wem is not activated on current page');
+          }
+        });
+      },
+
+      /**
+       * get the current loaded context from Unomi, will be accessible only after loadContext() have been performed
+       * @returns {object} loaded context
+       */
+      getLoadedContext: function getLoadedContext() {
+        return wem.cxs;
+      },
+
+      /**
+       * In case Unomi contains rules related to HTML forms in the current page.
+       * The logic is simple, in case a rule exists in Unomi targeting a form event within the current webpage path
+       *  - then this form will be identified as form to be watched.
+       * You can reuse this function to get the list of concerned forms in order to attach listeners automatically for those form for example
+       * (not that current tracker is doing that by default, check function: _registerListenersForTrackedConditions())
+       * @returns {string[]} form names/ids in current web page
+       */
+      getFormNamesToWatch: function getFormNamesToWatch() {
+        return wem.formNamesToWatch;
+      },
+
+      /**
+       * Get current session id
+       * @returns {null|string} get current session id
+       */
+      getSessionId: function getSessionId() {
+        return wem.sessionID;
+      },
+
+      /**
+       * This function will register a personalization
+       *
+       * @param {object} personalization the personalization object
+       * @param {object} variants the variants
+       * @param {boolean} [ajax] Deprecated: Ajax rendering is not supported anymore
+       * @param {function} [resultCallback] the callback to be executed after personalization resolved
+       * @returns {undefined}
+       */
+      registerPersonalizationObject: function registerPersonalizationObject(personalization, variants, ajax, resultCallback) {
+        var target = personalization.id;
+
+        wem._registerPersonalizationCallback(personalization, function (result, additionalResultInfos) {
+          var selectedFilter = null;
+          var successfulFilters = [];
+          var inControlGroup = additionalResultInfos && additionalResultInfos.inControlGroup; // In case of control group Unomi is not resolving any strategy or fallback for us. So we have to do the fallback here.
+
+          if (inControlGroup && personalization.strategyOptions && personalization.strategyOptions.fallback) {
+            selectedFilter = variants[personalization.strategyOptions.fallback];
+            successfulFilters.push(selectedFilter);
+          } else {
+            for (var i = 0; i < result.length; i++) {
+              successfulFilters.push(variants[result[i]]);
+            }
+
+            if (successfulFilters.length > 0) {
+              selectedFilter = successfulFilters[0];
+              var minPos = successfulFilters[0].position;
+
+              if (minPos >= 0) {
+                for (var j = 1; j < successfulFilters.length; j++) {
+                  if (successfulFilters[j].position < minPos) {
+                    selectedFilter = successfulFilters[j];
+                  }
+                }
+              }
+            }
+          }
+
+          if (resultCallback) {
+            // execute callback
+            resultCallback(successfulFilters, selectedFilter);
+          } else {
+            if (selectedFilter) {
+              var targetFilters = document.getElementById(target).children;
+
+              for (var fIndex in targetFilters) {
+                var filter = targetFilters.item(fIndex);
+
+                if (filter) {
+                  filter.style.display = filter.id === selectedFilter.content ? '' : 'none';
+                }
+              } // we now add control group information to event if the user is in the control group.
+
+
+              if (inControlGroup) {
+                console.info('[WEM] Profile is in control group for target: ' + target + ', adding to personalization event...');
+                selectedFilter.event.target.properties.inControlGroup = true;
+
+                if (selectedFilter.event.target.properties.variants) {
+                  selectedFilter.event.target.properties.variants.forEach(function (variant) {
+                    return variant.inControlGroup = true;
+                  });
+                }
+              } // send event to unomi
+
+
+              wem.collectEvent(wem._completeEvent(selectedFilter.event), function () {
+                console.info('[WEM] Personalization event successfully collected.');
+              }, function () {
+                console.error('[WEM] Could not send personalization event.');
+              }); //Trigger variant display event for personalization
+
+              wem._dispatchJSExperienceDisplayedEvent(selectedFilter.event);
+            } else {
+              var elements = document.getElementById(target).children;
+
+              for (var eIndex in elements) {
+                var el = elements.item(eIndex);
+                el.style.display = 'none';
+              }
+            }
+          }
+        });
+      },
+
+      /**
+       * This function will register an optimization test or A/B test
+       *
+       * @param {string} optimizationTestNodeId the optimization test node id
+       * @param {string} goalId the associated goal Id
+       * @param {string} containerId the HTML container Id
+       * @param {object} variants the variants
+       * @param {boolean} [ajax] Deprecated: Ajax rendering is not supported anymore
+       * @param {object} [variantsTraffic] the associated traffic allocation
+       * @return {undefined}
+       */
+      registerOptimizationTest: function registerOptimizationTest(optimizationTestNodeId, goalId, containerId, variants, ajax, variantsTraffic) {
+        // check persona panel forced variant
+        var selectedVariantId = wem.getUrlParameter('wemSelectedVariantId-' + optimizationTestNodeId); // check already resolved variant stored in local
+
+        if (selectedVariantId === null) {
+          if (wem.storageAvailable('sessionStorage')) {
+            selectedVariantId = sessionStorage.getItem(optimizationTestNodeId);
+          } else {
+            selectedVariantId = wem.getCookie('selectedVariantId');
+
+            if (selectedVariantId != null && selectedVariantId === '') {
+              selectedVariantId = null;
+            }
+          }
+        } // select random variant and call unomi
+
+
+        if (!(selectedVariantId && variants[selectedVariantId])) {
+          var keys = Object.keys(variants);
+
+          if (variantsTraffic) {
+            var rand = 100 * Math.random() << 0;
+
+            for (var nodeIdentifier in variantsTraffic) {
+              if ((rand -= variantsTraffic[nodeIdentifier]) < 0 && selectedVariantId == null) {
+                selectedVariantId = nodeIdentifier;
+              }
+            }
+          } else {
+            selectedVariantId = keys[keys.length * Math.random() << 0];
+          }
+
+          if (wem.storageAvailable('sessionStorage')) {
+            sessionStorage.setItem(optimizationTestNodeId, selectedVariantId);
+          } else {
+            wem.setCookie('selectedVariantId', selectedVariantId, 1);
+          } // spread event to unomi
+
+
+          wem._registerEvent(wem._completeEvent(variants[selectedVariantId].event));
+        } //Trigger variant display event for optimization
+        // (Wrapped in DOMContentLoaded because opti are resulted synchronously at page load, so we dispatch the JS even after page load, to be sure that listeners are ready)
+
+
+        window.addEventListener('DOMContentLoaded', function () {
+          wem._dispatchJSExperienceDisplayedEvent(variants[selectedVariantId].event);
+        });
+
+        if (selectedVariantId) {
+          // update persona panel selected variant
+          if (window.optimizedContentAreas && window.optimizedContentAreas[optimizationTestNodeId]) {
+            window.optimizedContentAreas[optimizationTestNodeId].selectedVariant = selectedVariantId;
+          } // display the good variant
+
+
+          document.getElementById(variants[selectedVariantId].content).style.display = '';
+        }
+      },
+
+      /**
+       * This function is used to load the current context in the page
+       *
+       * @param {boolean} [skipEvents=false] Should we send the events
+       * @param {boolean} [invalidate=false] Should we invalidate the current context
+       * @param {boolean} [forceReload=false] This function contains an internal check to avoid loading of the context multiple times.
+       *                                      But in some rare cases, it could be useful to force the reloading of the context and bypass the check.
+       * @return {undefined}
+       */
+      loadContext: function loadContext() {
+        var skipEvents = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+        var invalidate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+        var forceReload = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+        if (wem.contextLoaded && !forceReload) {
+          console.log('Context already requested by', wem.contextLoaded);
+          return;
+        }
+
+        var jsonData = {
+          requiredProfileProperties: wem.digitalData.wemInitConfig.requiredProfileProperties,
+          requiredSessionProperties: wem.digitalData.wemInitConfig.requiredSessionProperties,
+          requireSegments: wem.digitalData.wemInitConfig.requireSegments,
+          requireScores: wem.digitalData.wemInitConfig.requireScores,
+          source: wem.buildSourcePage()
+        };
+
+        if (!skipEvents) {
+          jsonData.events = wem.digitalData.events;
+        }
+
+        if (wem.digitalData.personalizationCallback) {
+          jsonData.personalizations = wem.digitalData.personalizationCallback.map(function (x) {
+            return x.personalization;
+          });
+        }
+
+        jsonData.sessionId = wem.sessionID;
+        var contextUrl = wem.contextServerUrl + '/context.json';
+
+        if (invalidate) {
+          contextUrl += '?invalidateSession=true&invalidateProfile=true';
+        }
+
+        wem.ajax({
+          url: contextUrl,
+          type: 'POST',
+          async: true,
+          contentType: 'text/plain;charset=UTF-8',
+          // Use text/plain to avoid CORS preflight
+          jsonData: jsonData,
+          dataType: 'application/json',
+          invalidate: invalidate,
+          success: wem._onSuccess,
+          error: function error() {
+            wem._executeFallback('error during context loading');
+          }
+        });
+        wem.contextLoaded = Error().stack;
+        console.info('[WEM] context loading...');
+      },
+
+      /**
+       * This function will send an event to Apache Unomi
+       * @param {object} event The event object to send, you can build it using wem.buildEvent(eventType, target, source)
+       * @param {function} successCallback optional, will be executed in case of success
+       * @param {function} errorCallback optional, will be executed in case of error
+       * @return {undefined}
+       */
+      collectEvent: function collectEvent(event) {
+        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
+        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+        wem.collectEvents({
+          events: [event]
+        }, successCallback, errorCallback);
+      },
+
+      /**
+       * This function will send the events to Apache Unomi
+       *
+       * @param {object} events Javascript object { events: [event1, event2] }
+       * @param {function} successCallback optional, will be executed in case of success
+       * @param {function} errorCallback optional, will be executed in case of error
+       * @return {undefined}
+       */
+      collectEvents: function collectEvents(events) {
+        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
+        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+
+        if (wem.fallback) {
+          // in case of fallback we don't want to collect any events
+          return;
+        }
+
+        events.sessionId = wem.sessionID ? wem.sessionID : '';
+        var data = JSON.stringify(events);
+        wem.ajax({
+          url: wem.contextServerUrl + '/eventcollector',
+          type: 'POST',
+          async: true,
+          contentType: 'text/plain;charset=UTF-8',
+          // Use text/plain to avoid CORS preflight
+          data: data,
+          dataType: 'application/json',
+          success: successCallback,
+          error: errorCallback
+        });
+      },
+
+      /**
+       * This function will build an event of type click and send it to Apache Unomi
+       *
+       * @param {object} event javascript
+       * @param {function} [successCallback] optional, will be executed if case of success
+       * @param {function} [errorCallback] optional, will be executed if case of error
+       * @return {undefined}
+       */
+      sendClickEvent: function sendClickEvent(event) {
+        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
+        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+
+        if (event.target.id || event.target.name) {
+          console.info('[WEM] Send click event');
+          var targetId = event.target.id ? event.target.id : event.target.name;
+          var clickEvent = wem.buildEvent('click', wem.buildTarget(targetId, event.target.localName), wem.buildSourcePage());
+          var eventIndex = wem.eventsPrevented.indexOf(targetId);
+
+          if (eventIndex !== -1) {
+            wem.eventsPrevented.splice(eventIndex, 0);
+          } else {
+            wem.eventsPrevented.push(targetId);
+            event.preventDefault();
+            var target = event.target;
+            wem.collectEvent(clickEvent, function (xhr) {
+              console.info('[WEM] Click event successfully collected.');
+
+              if (successCallback) {
+                successCallback(xhr);
+              } else {
+                target.click();
+              }
+            }, function (xhr) {
+              console.error('[WEM] Could not send click event.');
+
+              if (errorCallback) {
+                errorCallback(xhr);
+              } else {
+                target.click();
+              }
+            });
+          }
+        }
+      },
+
+      /**
+       * This function will build an event of type video and send it to Apache Unomi
+       *
+       * @param {object} event javascript
+       * @param {function} [successCallback] optional, will be executed if case of success
+       * @param {function} [errorCallback] optional, will be executed if case of error
+       * @return {undefined}
+       */
+      sendVideoEvent: function sendVideoEvent(event) {
+        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
+        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+        console.info('[WEM] catching video event');
+        var videoEvent = wem.buildEvent('video', wem.buildTarget(event.target.id, 'video', {
+          action: event.type
+        }), wem.buildSourcePage());
+        wem.collectEvent(videoEvent, function (xhr) {
+          console.info('[WEM] Video event successfully collected.');
+
+          if (successCallback) {
+            successCallback(xhr);
+          }
+        }, function (xhr) {
+          console.error('[WEM] Could not send video event.');
+
+          if (errorCallback) {
+            errorCallback(xhr);
+          }
+        });
+      },
+
+      /**
+       * This function will invalidate the Apache Unomi session and profile,
+       * by removing the associated cookies, set the loaded context to undefined
+       * and set the session id cookie with a newly generated ID
+       * @return {undefined}
+       */
+      invalidateSessionAndProfile: function invalidateSessionAndProfile() {
+        wem.sessionID = wem.generateGuid() + wem.browserGeneratedSessionSuffix;
+        wem.setCookie(wem.trackerSessionIdCookieName, wem.sessionID, 1);
+        wem.removeCookie(wem.contextServerCookieName);
+        wem.removeCookie(wem.trackerProfileIdCookieName);
+        wem.cxs = undefined;
+      },
+
+      /**
+       * This function return the basic structure for an event, it must be adapted to your need
+       *
+       * @param {string} eventType The name of your event
+       * @param {object} [target] The target object for your event can be build with wem.buildTarget(targetId, targetType, targetProperties)
+       * @param {object} [source] The source object for your event can be build with wem.buildSource(sourceId, sourceType, sourceProperties)
+       * @returns {object} the event
+       */
+      buildEvent: function buildEvent(eventType, target, source) {
+        var event = {
+          eventType: eventType,
+          scope: wem.digitalData.scope
+        };
+
+        if (target) {
+          event.target = target;
+        }
+
+        if (source) {
+          event.source = source;
+        }
+
+        return event;
+      },
+
+      /**
+       * This function return an event of type form
+       *
+       * @param {string} formName The HTML name of id of the form to use in the target of the event
+       * @param {HTMLFormElement} form optional HTML form element, if provided will be used to extract the form fields and populate the form event
+       * @returns {object} the form event
+       */
+      buildFormEvent: function buildFormEvent(formName) {
+        var form = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
+        var formEvent = wem.buildEvent('form', wem.buildTarget(formName, 'form'), wem.buildSourcePage());
+        formEvent.flattenedProperties = {
+          fields: form ? wem._extractFormData(form) : {}
+        };
+        return formEvent;
+      },
+
+      /**
+       * This function return the source object for a source of type page
+       *
+       * @returns {object} the target page
+       */
+      buildTargetPage: function buildTargetPage() {
+        return wem.buildTarget(wem.digitalData.page.pageInfo.pageID, 'page', wem.digitalData.page);
+      },
+
+      /**
+       * This function return the source object for a source of type page
+       *
+       * @returns {object} the source page
+       */
+      buildSourcePage: function buildSourcePage() {
+        return wem.buildSource(wem.digitalData.page.pageInfo.pageID, 'page', wem.digitalData.page);
+      },
+
+      /**
+       * This function return the basic structure for the target of your event
+       *
+       * @param {string} targetId The ID of the target
+       * @param {string} targetType The type of the target
+       * @param {object} [targetProperties] The optional properties of the target
+       * @returns {object} the target
+       */
+      buildTarget: function buildTarget(targetId, targetType) {
+        var targetProperties = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+        return wem._buildObject(targetId, targetType, targetProperties);
+      },
+
+      /**
+       * This function return the basic structure for the source of your event
+       *
+       * @param {string} sourceId The ID of the source
+       * @param {string} sourceType The type of the source
+       * @param {object} [sourceProperties] The optional properties of the source
+       * @returns {object} the source
+       */
+      buildSource: function buildSource(sourceId, sourceType) {
+        var sourceProperties = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+        return wem._buildObject(sourceId, sourceType, sourceProperties);
+      },
+
+      /*************************************/
+
+      /* Utility functions under this line */
+
+      /*************************************/
+
+      /**
+       * This is an utility function to set a cookie
+       *
+       * @param {string} cookieName name of the cookie
+       * @param {string} cookieValue value of the cookie
+       * @param {number} [expireDays] number of days to set the expire date
+       * @return {undefined}
+       */
+      setCookie: function setCookie(cookieName, cookieValue, expireDays) {
+        var expires = '';
+
+        if (expireDays) {
+          var d = new Date();
+          d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000);
+          expires = '; expires=' + d.toUTCString();
+        }
+
+        document.cookie = cookieName + '=' + cookieValue + expires + '; path=/; SameSite=Strict';
+      },
+
+      /**
+       * This is an utility function to get a cookie
+       *
+       * @param {string} cookieName name of the cookie to get
+       * @returns {string} the value of the first cookie with the corresponding name or null if not found
+       */
+      getCookie: function getCookie(cookieName) {
+        var name = cookieName + '=';
+        var ca = document.cookie.split(';');
+
+        for (var i = 0; i < ca.length; i++) {
+          var c = ca[i];
+
+          while (c.charAt(0) == ' ') {
+            c = c.substring(1);
+          }
+
+          if (c.indexOf(name) == 0) {
+            return c.substring(name.length, c.length);
+          }
+        }
+
+        return null;
+      },
+
+      /**
+       * This is an utility function to remove a cookie
+       *
+       * @param {string} cookieName the name of the cookie to rename
+       * @return {undefined}
+       */
+      removeCookie: function removeCookie(cookieName) {
+        wem.setCookie(cookieName, '', -1);
+      },
+
+      /**
+       * This is an utility function to execute AJAX call
+       *
+       * @param {object} options options of the request
+       * @return {undefined}
+       */
+      ajax: function ajax(options) {
+        var xhr = new XMLHttpRequest();
+
+        if ('withCredentials' in xhr) {
+          xhr.open(options.type, options.url, options.async);
+          xhr.withCredentials = true;
+        } else if (typeof XDomainRequest != 'undefined') {
+          /* global XDomainRequest */
+          xhr = new XDomainRequest();
+          xhr.open(options.type, options.url);
+        }
+
+        if (options.contentType) {
+          xhr.setRequestHeader('Content-Type', options.contentType);
+        }
+
+        if (options.dataType) {
+          xhr.setRequestHeader('Accept', options.dataType);
+        }
+
+        if (options.responseType) {
+          xhr.responseType = options.responseType;
+        }
+
+        var requestExecuted = false;
+
+        if (wem.timeoutInMilliseconds !== -1) {
+          setTimeout(function () {
+            if (!requestExecuted) {
+              console.error('[WEM] XML request timeout, url: ' + options.url);
+              requestExecuted = true;
+
+              if (options.error) {
+                options.error(xhr);
+              }
+            }
+          }, wem.timeoutInMilliseconds);
+        }
+
+        xhr.onreadystatechange = function () {
+          if (!requestExecuted) {
+            if (xhr.readyState === 4) {
+              if (xhr.status === 200 || xhr.status === 204 || xhr.status === 304) {
+                if (xhr.responseText != null) {
+                  requestExecuted = true;
+
+                  if (options.success) {
+                    options.success(xhr);
+                  }
+                }
+              } else {
+                requestExecuted = true;
+
+                if (options.error) {
+                  options.error(xhr);
+                }
+
+                console.error('[WEM] XML request error: ' + xhr.statusText + ' (' + xhr.status + ')');
+              }
+            }
+          }
+        };
+
+        if (options.jsonData) {
+          xhr.send(JSON.stringify(options.jsonData));
+        } else if (options.data) {
+          xhr.send(options.data);
+        } else {
+          xhr.send();
+        }
+      },
+
+      /**
+       * This is an utility function to generate a new UUID
+       *
+       * @returns {string} the newly generated UUID
+       */
+      generateGuid: function generateGuid() {
+        function s4() {
+          return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+        }
+
+        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
+      },
+
+      /**
+       * This is an utility function to check if the local storage is available or not
+       * @param {string} type the type of storage to test
+       * @returns {boolean} true in case storage is available, false otherwise
+       */
+      storageAvailable: function storageAvailable(type) {
+        try {
+          var storage = window[type],
+              x = '__storage_test__';
+          storage.setItem(x, x);
+          storage.removeItem(x);
+          return true;
+        } catch (e) {
+          return false;
+        }
+      },
+
+      /**
+       * Dispatch a JavaScript event in current HTML document
+       *
+       * @param {string} name the name of the event
+       * @param {boolean} canBubble does the event can bubble ?
+       * @param {boolean} cancelable is the event cancelable ?
+       * @param {*} detail event details
+       * @return {undefined}
+       */
+      dispatchJSEvent: function dispatchJSEvent(name, canBubble, cancelable, detail) {
+        var event = document.createEvent('CustomEvent');
+        event.initCustomEvent(name, canBubble, cancelable, detail);
+        document.dispatchEvent(event);
+      },
+
+      /**
+       * Fill the wem.digitalData.displayedVariants with the javascript event passed as parameter
+       * @param {object} jsEvent javascript event
+       * @private
+       * @return {undefined}
+       */
+      _fillDisplayedVariants: function _fillDisplayedVariants(jsEvent) {
+        if (!wem.digitalData.displayedVariants) {
+          wem.digitalData.displayedVariants = [];
+        }
+
+        wem.digitalData.displayedVariants.push(jsEvent);
+      },
+
+      /**
+       * This is an utility function to get current url parameter value
+       *
+       * @param {string} name, the name of the parameter
+       * @returns {string} the value of the parameter
+       */
+      getUrlParameter: function getUrlParameter(name) {
+        name = name.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
+        var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
+        var results = regex.exec(window.location.search);
+        return results === null ? null : decodeURIComponent(results[1].replace(/\+/g, ' '));
+      },
+
+      /**
+       * convert the passed query string into JS object.
+       * @param {string} searchString The URL query string
+       * @returns {object} converted URL params
+       */
+      convertUrlParametersToObj: function convertUrlParametersToObj(searchString) {
+        if (!searchString) {
+          return null;
+        }
+
+        return searchString.replace(/^\?/, '') // Only trim off a single leading interrobang.
+        .split('&').reduce(function (result, next) {
+          if (next === '') {
+            return result;
+          }
+
+          var pair = next.split('=');
+          var key = decodeURIComponent(pair[0]);
+          var value = typeof pair[1] !== 'undefined' && decodeURIComponent(pair[1]) || undefined;
+
+          if (Object.prototype.hasOwnProperty.call(result, key)) {
+            // Check to see if this property has been met before.
+            if (Array.isArray(result[key])) {
+              // Is it already an array?
+              result[key].push(value);
+            } else {
+              // Make it an array.
+              result[key] = [result[key], value];
+            }
+          } else {
+            // First time seen, just add it.
+            result[key] = value;
+          }
+
+          return result;
+        }, {});
+      },
+
+      /*************************************/
+
+      /* Private functions under this line */
+
+      /*************************************/
+
+      /**
+       * Used to override the default digitalData values,
+       * the result will impact directly the current instance wem.digitalData
+       *
+       * @param {object[]} digitalDataOverrides list of overrides
+       * @private
+       * @return {undefined}
+       */
+      _handleDigitalDataOverrides: function _handleDigitalDataOverrides(digitalDataOverrides) {
+        if (digitalDataOverrides && digitalDataOverrides.length > 0) {
+          var _iterator = _createForOfIteratorHelper(digitalDataOverrides),
+              _step;
+
+          try {
+            for (_iterator.s(); !(_step = _iterator.n()).done;) {
+              var digitalDataOverride = _step.value;
+              wem.digitalData = wem._deepMergeObjects(digitalDataOverride, wem.digitalData);
+            }
+          } catch (err) {
+            _iterator.e(err);
+          } finally {
+            _iterator.f();
+          }
+        }
+      },
+
+      /**
+       * Check for tracked conditions in the current loaded context, and attach listeners for the known tracked condition types:
+       * - formEventCondition
+       * - videoViewEventCondition
+       * - clickOnLinkEventCondition
+       *
+       * @private
+       * @return {undefined}
+       */
+      _registerListenersForTrackedConditions: function _registerListenersForTrackedConditions() {
+        console.info('[WEM] Check for tracked conditions and attach related HTML listeners');
+        var videoNamesToWatch = [];
+        var clickToWatch = [];
+
+        if (wem.cxs.trackedConditions && wem.cxs.trackedConditions.length > 0) {
+          for (var i = 0; i < wem.cxs.trackedConditions.length; i++) {
+            switch (wem.cxs.trackedConditions[i].type) {
+              case 'formEventCondition':
+                if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.formId) {
+                  wem.formNamesToWatch.push(wem.cxs.trackedConditions[i].parameterValues.formId);
+                }
+
+                break;
+
+              case 'videoViewEventCondition':
+                if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.videoId) {
+                  videoNamesToWatch.push(wem.cxs.trackedConditions[i].parameterValues.videoId);
+                }
+
+                break;
+
+              case 'clickOnLinkEventCondition':
+                if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.itemId) {
+                  clickToWatch.push(wem.cxs.trackedConditions[i].parameterValues.itemId);
+                }
+
+                break;
+            }
+          }
+        }
+
+        var forms = document.querySelectorAll('form');
+
+        for (var formIndex = 0; formIndex < forms.length; formIndex++) {
+          var form = forms.item(formIndex);
+          var formName = form.getAttribute('name') ? form.getAttribute('name') : form.getAttribute('id'); // test attribute data-form-id to not add a listener on FF form
+
+          if (formName && wem.formNamesToWatch.indexOf(formName) > -1 && form.getAttribute('data-form-id') == null) {
+            // add submit listener on form that we need to watch only
+            console.info('[WEM] Watching form ' + formName);
+            form.addEventListener('submit', wem._formSubmitEventListener, true);
+          }
+        }
+
+        for (var videoIndex = 0; videoIndex < videoNamesToWatch.length; videoIndex++) {
+          var videoName = videoNamesToWatch[videoIndex];
+          var video = document.getElementById(videoName) || document.getElementById(wem._resolveId(videoName));
+
+          if (video) {
+            video.addEventListener('play', wem.sendVideoEvent);
+            video.addEventListener('ended', wem.sendVideoEvent);
+            console.info('[WEM] Watching video ' + videoName);
+          } else {
+            console.warn('[WEM] Unable to watch video ' + videoName + ', video not found in the page');
+          }
+        }
+
+        for (var clickIndex = 0; clickIndex < clickToWatch.length; clickIndex++) {
+          var clickIdName = clickToWatch[clickIndex];
+          var click = document.getElementById(clickIdName) || document.getElementById(wem._resolveId(clickIdName)) ? document.getElementById(clickIdName) || document.getElementById(wem._resolveId(clickIdName)) : document.getElementsByName(clickIdName)[0];
+
+          if (click) {
+            click.addEventListener('click', wem.sendClickEvent);
+            console.info('[WEM] Watching click ' + clickIdName);
+          } else {
+            console.warn('[WEM] Unable to watch click ' + clickIdName + ', element not found in the page');
+          }
+        }
+      },
+
+      /**
+       * Check for currently registered events in wem.digitalData.events that would be incomplete:
+       * - autocomplete the event with the current digitalData page infos for the source
+       * @private
+       * @return {undefined}
+       */
+      _checkUncompleteRegisteredEvents: function _checkUncompleteRegisteredEvents() {
+        if (wem.digitalData && wem.digitalData.events) {
+          var _iterator2 = _createForOfIteratorHelper(wem.digitalData.events),
+              _step2;
+
+          try {
+            for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
+              var event = _step2.value;
+
+              wem._completeEvent(event);
+            }
+          } catch (err) {
+            _iterator2.e(err);
+          } finally {
+            _iterator2.f();
+          }
+        }
+      },
+
+      /**
+       * dispatch JavaScript event in current HTML document for perso and opti events contains in digitalData.events
+       * @private
+       * @return {undefined}
+       */
+      _dispatchJSExperienceDisplayedEvents: function _dispatchJSExperienceDisplayedEvents() {
+        if (wem.digitalData && wem.digitalData.events) {
+          var _iterator3 = _createForOfIteratorHelper(wem.digitalData.events),
+              _step3;
+
+          try {
+            for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
+              var event = _step3.value;
+
+              if (event.eventType === 'optimizationTestEvent' || event.eventType === 'personalizationEvent') {
+                wem._dispatchJSExperienceDisplayedEvent(event);
+              }
+            }
+          } catch (err) {
+            _iterator3.e(err);
+          } finally {
+            _iterator3.f();
+          }
+        }
+      },
+
+      /**
+       * build and dispatch JavaScript event in current HTML document for the given Unomi event (perso/opti)
+       * @private
+       * @param {object} experienceUnomiEvent perso/opti Unomi event
+       * @return {undefined}
+       */
+      _dispatchJSExperienceDisplayedEvent: function _dispatchJSExperienceDisplayedEvent(experienceUnomiEvent) {
+        if (!wem.fallback && experienceUnomiEvent && experienceUnomiEvent.target && experienceUnomiEvent.target.properties && experienceUnomiEvent.target.properties.variants && experienceUnomiEvent.target.properties.variants.length > 0) {
+          var typeMapper = {
+            optimizationTestEvent: 'optimization',
+            personalizationEvent: 'personalization'
+          };
+
+          var _iterator4 = _createForOfIteratorHelper(experienceUnomiEvent.target.properties.variants),
+              _step4;
+
+          try {
+            for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
+              var variant = _step4.value;
+              var jsEventDetail = {
+                id: variant.id,
+                name: variant.systemName,
+                displayableName: variant.displayableName,
+                path: variant.path,
+                type: typeMapper[experienceUnomiEvent.eventType],
+                variantType: experienceUnomiEvent.target.properties.type,
+                tags: variant.tags,
+                nodeType: variant.nodeType,
+                wrapper: {
+                  id: experienceUnomiEvent.target.itemId,
+                  name: experienceUnomiEvent.target.properties.systemName,
+                  displayableName: experienceUnomiEvent.target.properties.displayableName,
+                  path: experienceUnomiEvent.target.properties.path,
+                  tags: experienceUnomiEvent.target.properties.tags,
+                  nodeType: experienceUnomiEvent.target.properties.nodeType
+                }
+              };
+
+              if (experienceUnomiEvent.eventType === 'personalizationEvent') {
+                jsEventDetail.wrapper.inControlGroup = experienceUnomiEvent.target.properties.inControlGroup;
+              }
+
+              wem._fillDisplayedVariants(jsEventDetail);
+
+              wem.dispatchJSEvent('displayWemVariant', false, false, jsEventDetail);
+            }
+          } catch (err) {
+            _iterator4.e(err);
+          } finally {
+            _iterator4.f();
+          }
+        }
+      },
+
+      /**
+       * Filter events in digitalData.events that would have the property: event.properties.doNotSendToUnomi
+       * The effect is directly stored in a new version of wem.digitalData.events
+       * @private
+       * @return {undefined}
+       */
+      _filterUnomiEvents: function _filterUnomiEvents() {
+        if (wem.digitalData && wem.digitalData.events) {
+          wem.digitalData.events = wem.digitalData.events.filter(function (event) {
+            return !event.properties || !event.properties.doNotSendToUnomi;
+          }).map(function (event) {
+            if (event.properties) {
+              delete event.properties.doNotSendToUnomi;
+            }
+
+            return event;
+          });
+        }
+      },
+
+      /**
+       * Check if event is incomplete and complete what is missing:
+       * - source: if missing, use the current source page
+       * - scope: if missing, use the current scope
+       * @param {object} event, the event to be checked
+       * @private
+       * @return {object} the complete event
+       */
+      _completeEvent: function _completeEvent(event) {
+        if (!event.source) {
+          event.source = wem.buildSourcePage();
+        }
+
+        if (!event.scope) {
+          event.scope = wem.digitalData.scope;
+        }
+
+        if (event.target && !event.target.scope) {
+          event.target.scope = wem.digitalData.scope;
+        }
+
+        return event;
+      },
+
+      /**
+       * Register an event in the wem.digitalData.events.
+       * Registered event, will be sent automatically during the context loading process.
+       *
+       * Beware this function is useless in case the context is already loaded.
+       * in case the context is already loaded (check: wem.getLoadedContext()), then you should use: wem.collectEvent(s) functions
+       *
+       * @private
+       * @param {object} event the Unomi event to be registered
+       * @param {boolean} unshift optional, if true, the event will be added at the beginning of the list otherwise at the end of the list. (default: false)
+       * @return {undefined}
+       */
+      _registerEvent: function _registerEvent(event) {
+        var unshift = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+        if (wem.digitalData) {
+          if (wem.cxs) {
+            console.error('[WEM] already loaded, too late...');
+            return;
+          }
+        } else {
+          wem.digitalData = {};
+        }
+
+        wem.digitalData.events = wem.digitalData.events || [];
+
+        if (unshift) {
+          wem.digitalData.events.unshift(event);
+        } else {
+          wem.digitalData.events.push(event);
+        }
+      },
+
+      /**
+       * This function allow for registering callback that will be executed once the context is loaded.
+       * @param {function} onLoadCallback the callback to be executed
+       * @param {string} name optional name for the call, used mostly for logging the execution
+       * @param {number} priority optional priority to execute the callbacks in a specific order
+       *                          (default: 5, to leave room for the tracker default callback(s))
+       * @private
+       * @return {undefined}
+       */
+      _registerCallback: function _registerCallback(onLoadCallback) {
+        var name = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
+        var priority = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 5;
+
+        if (wem.digitalData) {
+          if (wem.cxs) {
+            console.info('[WEM] Trying to register context load callback, but context already loaded, executing now...');
+
+            if (onLoadCallback) {
+              console.info('[WEM] executing context load callback: ' + (name ? name : 'Callback without name'));
+              onLoadCallback(wem.digitalData);
+            }
+          } else {
+            console.info('[WEM] registering context load callback: ' + (name ? name : 'Callback without name'));
+
+            if (onLoadCallback) {
+              wem.digitalData.loadCallbacks = wem.digitalData.loadCallbacks || [];
+              wem.digitalData.loadCallbacks.push({
+                priority: priority,
+                name: name,
+                execute: onLoadCallback
+              });
+            }
+          }
+        } else {
+          console.info('[WEM] Trying to register context load callback, but no digitalData conf found, creating it and registering the callback: ' + (name ? name : 'Callback without name'));
+          wem.digitalData = {};
+
+          if (onLoadCallback) {
+            wem.digitalData.loadCallbacks = [];
+            wem.digitalData.loadCallbacks.push({
+              priority: priority,
+              name: name,
+              execute: onLoadCallback
+            });
+          }
+        }
+      },
+
+      /**
+       * Internal function for personalization specific callbacks (used for HTML dom manipulation once we get the context loaded)
+       * @param {object} personalization the personalization
+       * @param {function} callback the callback
+       * @private
+       * @return {undefined}
+       */
+      _registerPersonalizationCallback: function _registerPersonalizationCallback(personalization, callback) {
+        if (wem.digitalData) {
+          if (wem.cxs) {
+            console.error('[WEM] already loaded, too late...');
+          } else {
+            console.info('[WEM] digitalData object present but not loaded, registering sort callback...');
+            wem.digitalData.personalizationCallback = wem.digitalData.personalizationCallback || [];
+            wem.digitalData.personalizationCallback.push({
+              personalization: personalization,
+              callback: callback
+            });
+          }
+        } else {
+          wem.digitalData = {};
+          wem.digitalData.personalizationCallback = wem.digitalData.personalizationCallback || [];
+          wem.digitalData.personalizationCallback.push({
+            personalization: personalization,
+            callback: callback
+          });
+        }
+      },
+
+      /**
+       * Build a simple Unomi object
+       * @param {string} itemId the itemId of the object
+       * @param {string} itemType the itemType of the object
+       * @param {object} properties optional properties for the object
+       * @private
+       * @return {object} the built Unomi JSON object
+       */
+      _buildObject: function _buildObject(itemId, itemType) {
+        var properties = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+        var object = {
+          scope: wem.digitalData.scope,
+          itemId: itemId,
+          itemType: itemType
+        };
+
+        if (properties) {
+          object.properties = properties;
+        }
+
+        return object;
+      },
+
+      /**
+       * Main callback used once the Ajax context request succeed
+       * @param {XMLHttpRequest} xhr the request
+       * @private
+       * @return {undefined}
+       */
+      _onSuccess: function _onSuccess(xhr) {
+        wem.cxs = JSON.parse(xhr.responseText);
+
+        if (wem.digitalData.loadCallbacks && wem.digitalData.loadCallbacks.length > 0) {
+          console.info('[WEM] Found context server load callbacks, calling now...');
+
+          wem._executeLoadCallbacks(wem.digitalData);
+
+          if (wem.digitalData.personalizationCallback) {
+            for (var j = 0; j < wem.digitalData.personalizationCallback.length; j++) {
+              if (wem.cxs.personalizationResults) {
+                // Since Unomi 2.1.0 personalization results are available with more infos
+                var personalizationResult = wem.cxs.personalizationResults[wem.digitalData.personalizationCallback[j].personalization.id];
+                wem.digitalData.personalizationCallback[j].callback(personalizationResult.contentIds, personalizationResult.additionalResultInfos);
+              } else {
+                // probably a version older than Unomi 2.1.0, fallback to old personalization results
+                wem.digitalData.personalizationCallback[j].callback(wem.cxs.personalizations[wem.digitalData.personalizationCallback[j].personalization.id]);
+              }
+            }
+          }
+        }
+      },
+
+      /**
+       * Main callback used once the Ajax context request failed
+       * @param {string} logMessage the log message, to identify the place of failure
+       * @private
+       * @return {undefined}
+       */
+      _executeFallback: function _executeFallback(logMessage) {
+        console.warn('[WEM] execute fallback' + (logMessage ? ': ' + logMessage : '') + ', load fallback callbacks, calling now...');
+        wem.fallback = true;
+        wem.cxs = {};
+
+        wem._executeLoadCallbacks(undefined);
+
+        if (wem.digitalData.personalizationCallback) {
+          for (var i = 0; i < wem.digitalData.personalizationCallback.length; i++) {
+            wem.digitalData.personalizationCallback[i].callback([wem.digitalData.personalizationCallback[i].personalization.strategyOptions.fallback]);
+          }
+        }
+      },
+
+      /**
+       * Executed the registered context loaded callbacks
+       * @param {*} callbackParam param of the callbacks
+       * @private
+       * @return {undefined}
+       */
+      _executeLoadCallbacks: function _executeLoadCallbacks(callbackParam) {
+        if (wem.digitalData.loadCallbacks && wem.digitalData.loadCallbacks.length > 0) {
+          wem.digitalData.loadCallbacks.sort(function (a, b) {
+            return a.priority - b.priority;
+          }).forEach(function (loadCallback) {
+            console.info('[WEM] executing context load callback: ' + (loadCallback.name ? loadCallback.name : 'callback without name'));
+            loadCallback.execute(callbackParam);
+          });
+        }
+      },
+
+      /**
+       * Parse current HTML document referrer information to enrich the digitalData page infos
+       * @private
+       * @return {undefined}
+       */
+      _processReferrer: function _processReferrer() {
+        var referrerURL = wem.digitalData.page.pageInfo.referringURL || document.referrer;
+        var sameDomainReferrer = false;
+
+        if (referrerURL) {
+          // parse referrer URL
+          var referrer = new URL(referrerURL); // Set sameDomainReferrer property
+
+          sameDomainReferrer = referrer.host === window.location.host; // only process referrer if it's not coming from the same site as the current page
+
+          if (!sameDomainReferrer) {
+            // get search element if it exists and extract search query if available
+            var search = referrer.search;
+            var query = undefined;
+
+            if (search && search != '') {
+              // parse parameters
+              var queryParams = [],
+                  param;
+              var queryParamPairs = search.slice(1).split('&');
+
+              for (var i = 0; i < queryParamPairs.length; i++) {
+                param = queryParamPairs[i].split('=');
+                queryParams.push(param[0]);
+                queryParams[param[0]] = param[1];
+              } // try to extract query: q is Google-like (most search engines), p is Yahoo
+
+
+              query = queryParams.q || queryParams.p;
+              query = decodeURIComponent(query).replace(/\+/g, ' ');
+            } // register referrer event
+            // Create deep copy of wem.digitalData.page and add data to pageInfo sub object
+
+
+            if (wem.digitalData && wem.digitalData.page && wem.digitalData.page.pageInfo) {
+              wem.digitalData.page.pageInfo.referrerHost = referrer.host;
+              wem.digitalData.page.pageInfo.referrerQuery = query;
+            }
+          }
+        }
+
+        wem.digitalData.page.pageInfo.sameDomainReferrer = sameDomainReferrer;
+      },
+
+      /**
+       * Listener callback that can be attached to a specific HTML form,
+       * this listener will automatically send the form event to Unomi, by parsing the HTML form data.
+       * (NOTE: the form listener only work for know forms to be watch due to tracked conditions)
+       *
+       * @param {object} event the original HTML form submition event for the watch form.
+       * @private
+       * @return {undefined}
+       */
+      _formSubmitEventListener: function _formSubmitEventListener(event) {
+        console.info('[WEM] Registering form event callback');
+        var form = event.target;
+        var formName = form.getAttribute('name') ? form.getAttribute('name') : form.getAttribute('id');
+
+        if (formName && wem.formNamesToWatch.indexOf(formName) > -1) {
+          console.info('[WEM] catching form ' + formName);
+          var eventCopy = document.createEvent('Event'); // Define that the event name is 'build'.
+
+          eventCopy.initEvent('submit', event.bubbles, event.cancelable);
+          event.stopImmediatePropagation();
+          event.preventDefault();
+          wem.collectEvent(wem.buildFormEvent(formName, form), function () {
+            form.removeEventListener('submit', wem._formSubmitEventListener, true);
+            form.dispatchEvent(eventCopy);
+
+            if (!eventCopy.defaultPrevented && !eventCopy.cancelBubble) {
+              form.submit();
+            }
+
+            form.addEventListener('submit', wem._formSubmitEventListener, true);
+          }, function (xhr) {
+            console.error('[WEM] Error while collecting form event: ' + xhr.status + ' ' + xhr.statusText);
+            xhr.abort();
+            form.removeEventListener('submit', wem._formSubmitEventListener, true);
+            form.dispatchEvent(eventCopy);
+
+            if (!eventCopy.defaultPrevented && !eventCopy.cancelBubble) {
+              form.submit();
+            }
+
+            form.addEventListener('submit', wem._formSubmitEventListener, true);
+          });
+        }
+      },
+
+      /**
+       * Utility function to extract data from an HTML form.
+       *
+       * @param {HTMLFormElement} form the HTML form element
+       * @private
+       * @return {object} the form data as JSON
+       */
+      _extractFormData: function _extractFormData(form) {
+        var params = {};
+
+        for (var i = 0; i < form.elements.length; i++) {
+          var e = form.elements[i]; // ignore empty and undefined key (e.name)
+
+          if (e.name) {
+            switch (e.nodeName) {
+              case 'TEXTAREA':
+              case 'INPUT':
+                switch (e.type) {
+                  case 'checkbox':
+                    var checkboxes = document.querySelectorAll('input[name="' + e.name + '"]');
+
+                    if (checkboxes.length > 1) {
+                      if (!params[e.name]) {
+                        params[e.name] = [];
+                      }
+
+                      if (e.checked) {
+                        params[e.name].push(e.value);
+                      }
+                    }
+
+                    break;
+
+                  case 'radio':
+                    if (e.checked) {
+                      params[e.name] = e.value;
+                    }
+
+                    break;
+
+                  default:
+                    if (!e.value || e.value == '') {
+                      // ignore element if no value is provided
+                      break;
+                    }
+
+                    params[e.name] = e.value;
+                }
+
+                break;
+
+              case 'SELECT':
+                if (e.options && e.options[e.selectedIndex]) {
+                  if (e.multiple) {
+                    params[e.name] = [];
+
+                    for (var j = 0; j < e.options.length; j++) {
+                      if (e.options[j].selected) {
+                        params[e.name].push(e.options[j].value);
+                      }
+                    }
+                  } else {
+                    params[e.name] = e.options[e.selectedIndex].value;
+                  }
+                }
+
+                break;
+            }
+          }
+        }
+
+        return params;
+      },
+
+      /**
+       * Internal function used for mapping ids when current HTML document ids doesn't match ids stored in Unomi backend
+       * @param {string} id the id to resolve
+       * @return {string} the resolved id or the original id if not match found
+       * @private
+       */
+      _resolveId: function _resolveId(id) {
+        if (wem.digitalData.sourceLocalIdentifierMap) {
+          var source = Object.keys(wem.digitalData.sourceLocalIdentifierMap).filter(function (source) {
+            return id.indexOf(source) > 0;
+          });
+          return source ? id.replace(source, wem.digitalData.sourceLocalIdentifierMap[source]) : id;
+        }
+
+        return id;
+      },
+
+      /**
+       * Enable or disable tracking in current page
+       * @param {boolean} enable true will enable the tracking feature, otherwise they will be disabled
+       * @param {function} callback an optional callback that can be used to perform additional logic based on enabling/disabling results
+       * @private
+       * @return {undefined}
+       */
+      _enableWem: function _enableWem(enable) {
+        var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; // display fallback if wem is not enable
+
+        wem.fallback = !enable; // remove cookies, reset cxs
+
+        if (!enable) {
+          wem.cxs = {};
+          document.cookie = wem.trackerProfileIdCookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
+          document.cookie = wem.contextServerCookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
+          delete wem.contextLoaded;
+        } else {
+          if (wem.DOMLoaded) {
+            wem.loadContext();
+          } else {
+            // As Dom loaded listener not triggered, enable global value.
+            wem.activateWem = true;
+          }
+        }
+
+        if (callback) {
+          callback(enable);
+        }
+
+        console.log("[WEM] successfully ".concat(enable ? 'enabled' : 'disabled', " tracking in current page"));
+      },
+
+      /**
+       * Utility function used to merge two JSON object together (arrays are concat for example)
+       * @param {object} source the source object for merge
+       * @param {object} target the target object for merge
+       * @private
+       * @return {object} the merged results
+       */
+      _deepMergeObjects: function _deepMergeObjects(source, target) {
+        if (!wem._isObject(target) || !wem._isObject(source)) {
+          return source;
+        }
+
+        Object.keys(source).forEach(function (key) {
+          var targetValue = target[key];
+          var sourceValue = source[key]; // concat arrays || merge objects || add new props
+
+          if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
+            target[key] = targetValue.concat(sourceValue);
+          } else if (wem._isObject(targetValue) && wem._isObject(sourceValue)) {
+            target[key] = wem._deepMergeObjects(sourceValue, Object.assign({}, targetValue));
+          } else {
+            target[key] = sourceValue;
+          }
+        });
+        return target;
+      },
+
+      /**
+       * Utility function used to check if the given variable is a JavaScript object.
+       * @param {*} obj the variable to check
+       * @private
+       * @return {boolean} true if the variable is an object, false otherwise
+       */
+      _isObject: function _isObject(obj) {
+        return obj && _typeof(obj) === 'object';
+      }
+    };
+    return wem;
+  };
+  /*
+   * Licensed to the Apache Software Foundation (ASF) under one or more
+   * contributor license agreements.  See the NOTICE file distributed with
+   * this work for additional information regarding copyright ownership.
+   * The ASF licenses this file to You under the Apache License, Version 2.0
+   * (the "License"); you may not use this file except in compliance with
+   * the License.  You may obtain a copy of the License at
+   *
+   *      http://www.apache.org/licenses/LICENSE-2.0
+   *
+   * Unless required by applicable law or agreed to in writing, software
+   * distributed under the License is distributed on an "AS IS" BASIS,
+   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   * See the License for the specific language governing permissions and
+   * limitations under the License.
+   */
+
+
+  var useTracker = function useTracker() {
+    return newTracker();
+  };
+
+  /*
+   * Licensed to the Apache Software Foundation (ASF) under one or more
+   * contributor license agreements.  See the NOTICE file distributed with
+   * this work for additional information regarding copyright ownership.
+   * The ASF licenses this file to You under the Apache License, Version 2.0
+   * (the "License"); you may not use this file except in compliance with
+   * the License.  You may obtain a copy of the License at
+   *
+   *      http://www.apache.org/licenses/LICENSE-2.0
+   *
+   * Unless required by applicable law or agreed to in writing, software
+   * distributed under the License is distributed on an "AS IS" BASIS,
+   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   * See the License for the specific language governing permissions and
+   * limitations under the License.
+   */
+  // we will store an instance in the current browser window to make it accessible
+
+  window.unomiWebTracker = useTracker();
+
+})();
+//# sourceMappingURL=unomi-web-tracker.js.map
diff --git a/extensions/web-tracker/wab/dist/unomi-web-tracker.js.map b/extensions/web-tracker/wab/dist/unomi-web-tracker.js.map
new file mode 100644
index 000000000..d91fdebe4
--- /dev/null
+++ b/extensions/web-tracker/wab/dist/unomi-web-tracker.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"unomi-web-tracker.js","sources":["../node_modules/@babel/runtime/helpers/esm/typeof.js","../node_modules/es6-crawler-detect/src/lib/crawler/provider.js","../node_modules/es6-crawler-detect/src/lib/crawler/crawlers.js","../node_modules/es6-crawler-detect/src/lib/crawler/exclusions.js","../node_modules/es6-crawler-detect/src/lib/crawler/headers.js","../node_modules/es6-crawler-detect/src/lib/crawler.js","../node_modules/es6-crawler-detect/src/index.js","../node_modules [...]
\ No newline at end of file
diff --git a/extensions/web-tracker/wab/dist/unomi-web-tracker.min.js b/extensions/web-tracker/wab/dist/unomi-web-tracker.min.js
new file mode 100644
index 000000000..5e1925a54
--- /dev/null
+++ b/extensions/web-tracker/wab/dist/unomi-web-tracker.min.js
@@ -0,0 +1 @@
+!function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}var t="undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},r=[],n=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,i=!1;function a(){i=!0;for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij [...]
diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileImportRankingIT.java b/itests/src/test/java/org/apache/unomi/itests/ProfileImportRankingIT.java
index 91594265a..cb27646e0 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ProfileImportRankingIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ProfileImportRankingIT.java
@@ -21,6 +21,7 @@ import org.apache.unomi.api.PartialList;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.PropertyType;
 import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.router.api.IRouterCamelContext;
 import org.apache.unomi.router.api.ImportConfiguration;
 import org.apache.unomi.router.api.RouterConstants;
 import org.apache.unomi.router.api.services.ImportExportConfigurationService;
@@ -51,11 +52,13 @@ public class ProfileImportRankingIT extends BaseIT {
     protected ImportExportConfigurationService<ImportConfiguration> importConfigurationService;
     @Inject @Filter(timeout = 600000)
     protected ProfileService profileService;
+    @Inject @Filter(timeout = 600000)
+    protected IRouterCamelContext routerCamelContext;
 
     @Test
     public void testImportRanking() throws InterruptedException {
 
-        importConfigurationService.getRouterCamelContext().setTracing(true);
+        routerCamelContext.setTracing(true);
 
         /*** Create Missing Properties ***/
         PropertyType propertyTypeUciId = new PropertyType(new Metadata("integration", "uciId", "UCI ID", "UCI ID"));


[unomi] 02/02: fix cherry-pick

Posted by jk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch backport-UNOMI-758
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit edff92fde5578016a62f0483c1d48264cd7e1375
Author: Kevan <ke...@jahia.com>
AuthorDate: Fri Mar 31 11:23:21 2023 +0200

    fix cherry-pick
---
 .../web-tracker/wab/dist/unomi-web-tracker.js      | 3867 --------------------
 .../web-tracker/wab/dist/unomi-web-tracker.js.map  |    1 -
 .../web-tracker/wab/dist/unomi-web-tracker.min.js  |    1 -
 3 files changed, 3869 deletions(-)

diff --git a/extensions/web-tracker/wab/dist/unomi-web-tracker.js b/extensions/web-tracker/wab/dist/unomi-web-tracker.js
deleted file mode 100644
index 334517d2b..000000000
--- a/extensions/web-tracker/wab/dist/unomi-web-tracker.js
+++ /dev/null
@@ -1,3867 +0,0 @@
-(function () {
-  'use strict';
-
-  function _typeof(obj) {
-    "@babel/helpers - typeof";
-
-    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
-      return typeof obj;
-    } : function (obj) {
-      return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
-    }, _typeof(obj);
-  }
-
-  var global$1 = (typeof global !== "undefined" ? global :
-    typeof self !== "undefined" ? self :
-    typeof window !== "undefined" ? window : {});
-
-  var lookup = [];
-  var revLookup = [];
-  var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array;
-  var inited = false;
-  function init () {
-    inited = true;
-    var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
-    for (var i = 0, len = code.length; i < len; ++i) {
-      lookup[i] = code[i];
-      revLookup[code.charCodeAt(i)] = i;
-    }
-
-    revLookup['-'.charCodeAt(0)] = 62;
-    revLookup['_'.charCodeAt(0)] = 63;
-  }
-
-  function toByteArray (b64) {
-    if (!inited) {
-      init();
-    }
-    var i, j, l, tmp, placeHolders, arr;
-    var len = b64.length;
-
-    if (len % 4 > 0) {
-      throw new Error('Invalid string. Length must be a multiple of 4')
-    }
-
-    // the number of equal signs (place holders)
-    // if there are two placeholders, than the two characters before it
-    // represent one byte
-    // if there is only one, then the three characters before it represent 2 bytes
-    // this is just a cheap hack to not do indexOf twice
-    placeHolders = b64[len - 2] === '=' ? 2 : b64[len - 1] === '=' ? 1 : 0;
-
-    // base64 is 4/3 + up to two characters of the original data
-    arr = new Arr(len * 3 / 4 - placeHolders);
-
-    // if there are placeholders, only get up to the last complete 4 chars
-    l = placeHolders > 0 ? len - 4 : len;
-
-    var L = 0;
-
-    for (i = 0, j = 0; i < l; i += 4, j += 3) {
-      tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)];
-      arr[L++] = (tmp >> 16) & 0xFF;
-      arr[L++] = (tmp >> 8) & 0xFF;
-      arr[L++] = tmp & 0xFF;
-    }
-
-    if (placeHolders === 2) {
-      tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4);
-      arr[L++] = tmp & 0xFF;
-    } else if (placeHolders === 1) {
-      tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2);
-      arr[L++] = (tmp >> 8) & 0xFF;
-      arr[L++] = tmp & 0xFF;
-    }
-
-    return arr
-  }
-
-  function tripletToBase64 (num) {
-    return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F]
-  }
-
-  function encodeChunk (uint8, start, end) {
-    var tmp;
-    var output = [];
-    for (var i = start; i < end; i += 3) {
-      tmp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]);
-      output.push(tripletToBase64(tmp));
-    }
-    return output.join('')
-  }
-
-  function fromByteArray (uint8) {
-    if (!inited) {
-      init();
-    }
-    var tmp;
-    var len = uint8.length;
-    var extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
-    var output = '';
-    var parts = [];
-    var maxChunkLength = 16383; // must be multiple of 3
-
-    // go through the array every three bytes, we'll deal with trailing stuff later
-    for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
-      parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength)));
-    }
-
-    // pad the end with zeros, but make sure to not forget the extra bytes
-    if (extraBytes === 1) {
-      tmp = uint8[len - 1];
-      output += lookup[tmp >> 2];
-      output += lookup[(tmp << 4) & 0x3F];
-      output += '==';
-    } else if (extraBytes === 2) {
-      tmp = (uint8[len - 2] << 8) + (uint8[len - 1]);
-      output += lookup[tmp >> 10];
-      output += lookup[(tmp >> 4) & 0x3F];
-      output += lookup[(tmp << 2) & 0x3F];
-      output += '=';
-    }
-
-    parts.push(output);
-
-    return parts.join('')
-  }
-
-  function read (buffer, offset, isLE, mLen, nBytes) {
-    var e, m;
-    var eLen = nBytes * 8 - mLen - 1;
-    var eMax = (1 << eLen) - 1;
-    var eBias = eMax >> 1;
-    var nBits = -7;
-    var i = isLE ? (nBytes - 1) : 0;
-    var d = isLE ? -1 : 1;
-    var s = buffer[offset + i];
-
-    i += d;
-
-    e = s & ((1 << (-nBits)) - 1);
-    s >>= (-nBits);
-    nBits += eLen;
-    for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}
-
-    m = e & ((1 << (-nBits)) - 1);
-    e >>= (-nBits);
-    nBits += mLen;
-    for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}
-
-    if (e === 0) {
-      e = 1 - eBias;
-    } else if (e === eMax) {
-      return m ? NaN : ((s ? -1 : 1) * Infinity)
-    } else {
-      m = m + Math.pow(2, mLen);
-      e = e - eBias;
-    }
-    return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
-  }
-
-  function write (buffer, value, offset, isLE, mLen, nBytes) {
-    var e, m, c;
-    var eLen = nBytes * 8 - mLen - 1;
-    var eMax = (1 << eLen) - 1;
-    var eBias = eMax >> 1;
-    var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0);
-    var i = isLE ? 0 : (nBytes - 1);
-    var d = isLE ? 1 : -1;
-    var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0;
-
-    value = Math.abs(value);
-
-    if (isNaN(value) || value === Infinity) {
-      m = isNaN(value) ? 1 : 0;
-      e = eMax;
-    } else {
-      e = Math.floor(Math.log(value) / Math.LN2);
-      if (value * (c = Math.pow(2, -e)) < 1) {
-        e--;
-        c *= 2;
-      }
-      if (e + eBias >= 1) {
-        value += rt / c;
-      } else {
-        value += rt * Math.pow(2, 1 - eBias);
-      }
-      if (value * c >= 2) {
-        e++;
-        c /= 2;
-      }
-
-      if (e + eBias >= eMax) {
-        m = 0;
-        e = eMax;
-      } else if (e + eBias >= 1) {
-        m = (value * c - 1) * Math.pow(2, mLen);
-        e = e + eBias;
-      } else {
-        m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen);
-        e = 0;
-      }
-    }
-
-    for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
-
-    e = (e << mLen) | m;
-    eLen += mLen;
-    for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
-
-    buffer[offset + i - d] |= s * 128;
-  }
-
-  var toString = {}.toString;
-
-  var isArray = Array.isArray || function (arr) {
-    return toString.call(arr) == '[object Array]';
-  };
-
-  /*!
-   * The buffer module from node.js, for the browser.
-   *
-   * @author   Feross Aboukhadijeh <fe...@feross.org> <http://feross.org>
-   * @license  MIT
-   */
-
-  var INSPECT_MAX_BYTES = 50;
-
-  /**
-   * If `Buffer.TYPED_ARRAY_SUPPORT`:
-   *   === true    Use Uint8Array implementation (fastest)
-   *   === false   Use Object implementation (most compatible, even IE6)
-   *
-   * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,
-   * Opera 11.6+, iOS 4.2+.
-   *
-   * Due to various browser bugs, sometimes the Object implementation will be used even
-   * when the browser supports typed arrays.
-   *
-   * Note:
-   *
-   *   - Firefox 4-29 lacks support for adding new properties to `Uint8Array` instances,
-   *     See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438.
-   *
-   *   - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function.
-   *
-   *   - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of
-   *     incorrect length in some situations.
-
-   * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they
-   * get the Object implementation, which is slower but behaves correctly.
-   */
-  Buffer.TYPED_ARRAY_SUPPORT = global$1.TYPED_ARRAY_SUPPORT !== undefined
-    ? global$1.TYPED_ARRAY_SUPPORT
-    : true;
-
-  /*
-   * Export kMaxLength after typed array support is determined.
-   */
-  kMaxLength();
-
-  function kMaxLength () {
-    return Buffer.TYPED_ARRAY_SUPPORT
-      ? 0x7fffffff
-      : 0x3fffffff
-  }
-
-  function createBuffer (that, length) {
-    if (kMaxLength() < length) {
-      throw new RangeError('Invalid typed array length')
-    }
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      // Return an augmented `Uint8Array` instance, for best performance
-      that = new Uint8Array(length);
-      that.__proto__ = Buffer.prototype;
-    } else {
-      // Fallback: Return an object instance of the Buffer class
-      if (that === null) {
-        that = new Buffer(length);
-      }
-      that.length = length;
-    }
-
-    return that
-  }
-
-  /**
-   * The Buffer constructor returns instances of `Uint8Array` that have their
-   * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of
-   * `Uint8Array`, so the returned instances will have all the node `Buffer` methods
-   * and the `Uint8Array` methods. Square bracket notation works as expected -- it
-   * returns a single octet.
-   *
-   * The `Uint8Array` prototype remains unmodified.
-   */
-
-  function Buffer (arg, encodingOrOffset, length) {
-    if (!Buffer.TYPED_ARRAY_SUPPORT && !(this instanceof Buffer)) {
-      return new Buffer(arg, encodingOrOffset, length)
-    }
-
-    // Common case.
-    if (typeof arg === 'number') {
-      if (typeof encodingOrOffset === 'string') {
-        throw new Error(
-          'If encoding is specified then the first argument must be a string'
-        )
-      }
-      return allocUnsafe(this, arg)
-    }
-    return from(this, arg, encodingOrOffset, length)
-  }
-
-  Buffer.poolSize = 8192; // not used by this implementation
-
-  // TODO: Legacy, not needed anymore. Remove in next major version.
-  Buffer._augment = function (arr) {
-    arr.__proto__ = Buffer.prototype;
-    return arr
-  };
-
-  function from (that, value, encodingOrOffset, length) {
-    if (typeof value === 'number') {
-      throw new TypeError('"value" argument must not be a number')
-    }
-
-    if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
-      return fromArrayBuffer(that, value, encodingOrOffset, length)
-    }
-
-    if (typeof value === 'string') {
-      return fromString(that, value, encodingOrOffset)
-    }
-
-    return fromObject(that, value)
-  }
-
-  /**
-   * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError
-   * if value is a number.
-   * Buffer.from(str[, encoding])
-   * Buffer.from(array)
-   * Buffer.from(buffer)
-   * Buffer.from(arrayBuffer[, byteOffset[, length]])
-   **/
-  Buffer.from = function (value, encodingOrOffset, length) {
-    return from(null, value, encodingOrOffset, length)
-  };
-
-  if (Buffer.TYPED_ARRAY_SUPPORT) {
-    Buffer.prototype.__proto__ = Uint8Array.prototype;
-    Buffer.__proto__ = Uint8Array;
-  }
-
-  function assertSize (size) {
-    if (typeof size !== 'number') {
-      throw new TypeError('"size" argument must be a number')
-    } else if (size < 0) {
-      throw new RangeError('"size" argument must not be negative')
-    }
-  }
-
-  function alloc (that, size, fill, encoding) {
-    assertSize(size);
-    if (size <= 0) {
-      return createBuffer(that, size)
-    }
-    if (fill !== undefined) {
-      // Only pay attention to encoding if it's a string. This
-      // prevents accidentally sending in a number that would
-      // be interpretted as a start offset.
-      return typeof encoding === 'string'
-        ? createBuffer(that, size).fill(fill, encoding)
-        : createBuffer(that, size).fill(fill)
-    }
-    return createBuffer(that, size)
-  }
-
-  /**
-   * Creates a new filled Buffer instance.
-   * alloc(size[, fill[, encoding]])
-   **/
-  Buffer.alloc = function (size, fill, encoding) {
-    return alloc(null, size, fill, encoding)
-  };
-
-  function allocUnsafe (that, size) {
-    assertSize(size);
-    that = createBuffer(that, size < 0 ? 0 : checked(size) | 0);
-    if (!Buffer.TYPED_ARRAY_SUPPORT) {
-      for (var i = 0; i < size; ++i) {
-        that[i] = 0;
-      }
-    }
-    return that
-  }
-
-  /**
-   * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance.
-   * */
-  Buffer.allocUnsafe = function (size) {
-    return allocUnsafe(null, size)
-  };
-  /**
-   * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance.
-   */
-  Buffer.allocUnsafeSlow = function (size) {
-    return allocUnsafe(null, size)
-  };
-
-  function fromString (that, string, encoding) {
-    if (typeof encoding !== 'string' || encoding === '') {
-      encoding = 'utf8';
-    }
-
-    if (!Buffer.isEncoding(encoding)) {
-      throw new TypeError('"encoding" must be a valid string encoding')
-    }
-
-    var length = byteLength(string, encoding) | 0;
-    that = createBuffer(that, length);
-
-    var actual = that.write(string, encoding);
-
-    if (actual !== length) {
-      // Writing a hex string, for example, that contains invalid characters will
-      // cause everything after the first invalid character to be ignored. (e.g.
-      // 'abxxcd' will be treated as 'ab')
-      that = that.slice(0, actual);
-    }
-
-    return that
-  }
-
-  function fromArrayLike (that, array) {
-    var length = array.length < 0 ? 0 : checked(array.length) | 0;
-    that = createBuffer(that, length);
-    for (var i = 0; i < length; i += 1) {
-      that[i] = array[i] & 255;
-    }
-    return that
-  }
-
-  function fromArrayBuffer (that, array, byteOffset, length) {
-    array.byteLength; // this throws if `array` is not a valid ArrayBuffer
-
-    if (byteOffset < 0 || array.byteLength < byteOffset) {
-      throw new RangeError('\'offset\' is out of bounds')
-    }
-
-    if (array.byteLength < byteOffset + (length || 0)) {
-      throw new RangeError('\'length\' is out of bounds')
-    }
-
-    if (byteOffset === undefined && length === undefined) {
-      array = new Uint8Array(array);
-    } else if (length === undefined) {
-      array = new Uint8Array(array, byteOffset);
-    } else {
-      array = new Uint8Array(array, byteOffset, length);
-    }
-
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      // Return an augmented `Uint8Array` instance, for best performance
-      that = array;
-      that.__proto__ = Buffer.prototype;
-    } else {
-      // Fallback: Return an object instance of the Buffer class
-      that = fromArrayLike(that, array);
-    }
-    return that
-  }
-
-  function fromObject (that, obj) {
-    if (internalIsBuffer(obj)) {
-      var len = checked(obj.length) | 0;
-      that = createBuffer(that, len);
-
-      if (that.length === 0) {
-        return that
-      }
-
-      obj.copy(that, 0, 0, len);
-      return that
-    }
-
-    if (obj) {
-      if ((typeof ArrayBuffer !== 'undefined' &&
-          obj.buffer instanceof ArrayBuffer) || 'length' in obj) {
-        if (typeof obj.length !== 'number' || isnan(obj.length)) {
-          return createBuffer(that, 0)
-        }
-        return fromArrayLike(that, obj)
-      }
-
-      if (obj.type === 'Buffer' && isArray(obj.data)) {
-        return fromArrayLike(that, obj.data)
-      }
-    }
-
-    throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.')
-  }
-
-  function checked (length) {
-    // Note: cannot use `length < kMaxLength()` here because that fails when
-    // length is NaN (which is otherwise coerced to zero.)
-    if (length >= kMaxLength()) {
-      throw new RangeError('Attempt to allocate Buffer larger than maximum ' +
-                           'size: 0x' + kMaxLength().toString(16) + ' bytes')
-    }
-    return length | 0
-  }
-  Buffer.isBuffer = isBuffer;
-  function internalIsBuffer (b) {
-    return !!(b != null && b._isBuffer)
-  }
-
-  Buffer.compare = function compare (a, b) {
-    if (!internalIsBuffer(a) || !internalIsBuffer(b)) {
-      throw new TypeError('Arguments must be Buffers')
-    }
-
-    if (a === b) return 0
-
-    var x = a.length;
-    var y = b.length;
-
-    for (var i = 0, len = Math.min(x, y); i < len; ++i) {
-      if (a[i] !== b[i]) {
-        x = a[i];
-        y = b[i];
-        break
-      }
-    }
-
-    if (x < y) return -1
-    if (y < x) return 1
-    return 0
-  };
-
-  Buffer.isEncoding = function isEncoding (encoding) {
-    switch (String(encoding).toLowerCase()) {
-      case 'hex':
-      case 'utf8':
-      case 'utf-8':
-      case 'ascii':
-      case 'latin1':
-      case 'binary':
-      case 'base64':
-      case 'ucs2':
-      case 'ucs-2':
-      case 'utf16le':
-      case 'utf-16le':
-        return true
-      default:
-        return false
-    }
-  };
-
-  Buffer.concat = function concat (list, length) {
-    if (!isArray(list)) {
-      throw new TypeError('"list" argument must be an Array of Buffers')
-    }
-
-    if (list.length === 0) {
-      return Buffer.alloc(0)
-    }
-
-    var i;
-    if (length === undefined) {
-      length = 0;
-      for (i = 0; i < list.length; ++i) {
-        length += list[i].length;
-      }
-    }
-
-    var buffer = Buffer.allocUnsafe(length);
-    var pos = 0;
-    for (i = 0; i < list.length; ++i) {
-      var buf = list[i];
-      if (!internalIsBuffer(buf)) {
-        throw new TypeError('"list" argument must be an Array of Buffers')
-      }
-      buf.copy(buffer, pos);
-      pos += buf.length;
-    }
-    return buffer
-  };
-
-  function byteLength (string, encoding) {
-    if (internalIsBuffer(string)) {
-      return string.length
-    }
-    if (typeof ArrayBuffer !== 'undefined' && typeof ArrayBuffer.isView === 'function' &&
-        (ArrayBuffer.isView(string) || string instanceof ArrayBuffer)) {
-      return string.byteLength
-    }
-    if (typeof string !== 'string') {
-      string = '' + string;
-    }
-
-    var len = string.length;
-    if (len === 0) return 0
-
-    // Use a for loop to avoid recursion
-    var loweredCase = false;
-    for (;;) {
-      switch (encoding) {
-        case 'ascii':
-        case 'latin1':
-        case 'binary':
-          return len
-        case 'utf8':
-        case 'utf-8':
-        case undefined:
-          return utf8ToBytes(string).length
-        case 'ucs2':
-        case 'ucs-2':
-        case 'utf16le':
-        case 'utf-16le':
-          return len * 2
-        case 'hex':
-          return len >>> 1
-        case 'base64':
-          return base64ToBytes(string).length
-        default:
-          if (loweredCase) return utf8ToBytes(string).length // assume utf8
-          encoding = ('' + encoding).toLowerCase();
-          loweredCase = true;
-      }
-    }
-  }
-  Buffer.byteLength = byteLength;
-
-  function slowToString (encoding, start, end) {
-    var loweredCase = false;
-
-    // No need to verify that "this.length <= MAX_UINT32" since it's a read-only
-    // property of a typed array.
-
-    // This behaves neither like String nor Uint8Array in that we set start/end
-    // to their upper/lower bounds if the value passed is out of range.
-    // undefined is handled specially as per ECMA-262 6th Edition,
-    // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization.
-    if (start === undefined || start < 0) {
-      start = 0;
-    }
-    // Return early if start > this.length. Done here to prevent potential uint32
-    // coercion fail below.
-    if (start > this.length) {
-      return ''
-    }
-
-    if (end === undefined || end > this.length) {
-      end = this.length;
-    }
-
-    if (end <= 0) {
-      return ''
-    }
-
-    // Force coersion to uint32. This will also coerce falsey/NaN values to 0.
-    end >>>= 0;
-    start >>>= 0;
-
-    if (end <= start) {
-      return ''
-    }
-
-    if (!encoding) encoding = 'utf8';
-
-    while (true) {
-      switch (encoding) {
-        case 'hex':
-          return hexSlice(this, start, end)
-
-        case 'utf8':
-        case 'utf-8':
-          return utf8Slice(this, start, end)
-
-        case 'ascii':
-          return asciiSlice(this, start, end)
-
-        case 'latin1':
-        case 'binary':
-          return latin1Slice(this, start, end)
-
-        case 'base64':
-          return base64Slice(this, start, end)
-
-        case 'ucs2':
-        case 'ucs-2':
-        case 'utf16le':
-        case 'utf-16le':
-          return utf16leSlice(this, start, end)
-
-        default:
-          if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
-          encoding = (encoding + '').toLowerCase();
-          loweredCase = true;
-      }
-    }
-  }
-
-  // The property is used by `Buffer.isBuffer` and `is-buffer` (in Safari 5-7) to detect
-  // Buffer instances.
-  Buffer.prototype._isBuffer = true;
-
-  function swap (b, n, m) {
-    var i = b[n];
-    b[n] = b[m];
-    b[m] = i;
-  }
-
-  Buffer.prototype.swap16 = function swap16 () {
-    var len = this.length;
-    if (len % 2 !== 0) {
-      throw new RangeError('Buffer size must be a multiple of 16-bits')
-    }
-    for (var i = 0; i < len; i += 2) {
-      swap(this, i, i + 1);
-    }
-    return this
-  };
-
-  Buffer.prototype.swap32 = function swap32 () {
-    var len = this.length;
-    if (len % 4 !== 0) {
-      throw new RangeError('Buffer size must be a multiple of 32-bits')
-    }
-    for (var i = 0; i < len; i += 4) {
-      swap(this, i, i + 3);
-      swap(this, i + 1, i + 2);
-    }
-    return this
-  };
-
-  Buffer.prototype.swap64 = function swap64 () {
-    var len = this.length;
-    if (len % 8 !== 0) {
-      throw new RangeError('Buffer size must be a multiple of 64-bits')
-    }
-    for (var i = 0; i < len; i += 8) {
-      swap(this, i, i + 7);
-      swap(this, i + 1, i + 6);
-      swap(this, i + 2, i + 5);
-      swap(this, i + 3, i + 4);
-    }
-    return this
-  };
-
-  Buffer.prototype.toString = function toString () {
-    var length = this.length | 0;
-    if (length === 0) return ''
-    if (arguments.length === 0) return utf8Slice(this, 0, length)
-    return slowToString.apply(this, arguments)
-  };
-
-  Buffer.prototype.equals = function equals (b) {
-    if (!internalIsBuffer(b)) throw new TypeError('Argument must be a Buffer')
-    if (this === b) return true
-    return Buffer.compare(this, b) === 0
-  };
-
-  Buffer.prototype.inspect = function inspect () {
-    var str = '';
-    var max = INSPECT_MAX_BYTES;
-    if (this.length > 0) {
-      str = this.toString('hex', 0, max).match(/.{2}/g).join(' ');
-      if (this.length > max) str += ' ... ';
-    }
-    return '<Buffer ' + str + '>'
-  };
-
-  Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) {
-    if (!internalIsBuffer(target)) {
-      throw new TypeError('Argument must be a Buffer')
-    }
-
-    if (start === undefined) {
-      start = 0;
-    }
-    if (end === undefined) {
-      end = target ? target.length : 0;
-    }
-    if (thisStart === undefined) {
-      thisStart = 0;
-    }
-    if (thisEnd === undefined) {
-      thisEnd = this.length;
-    }
-
-    if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) {
-      throw new RangeError('out of range index')
-    }
-
-    if (thisStart >= thisEnd && start >= end) {
-      return 0
-    }
-    if (thisStart >= thisEnd) {
-      return -1
-    }
-    if (start >= end) {
-      return 1
-    }
-
-    start >>>= 0;
-    end >>>= 0;
-    thisStart >>>= 0;
-    thisEnd >>>= 0;
-
-    if (this === target) return 0
-
-    var x = thisEnd - thisStart;
-    var y = end - start;
-    var len = Math.min(x, y);
-
-    var thisCopy = this.slice(thisStart, thisEnd);
-    var targetCopy = target.slice(start, end);
-
-    for (var i = 0; i < len; ++i) {
-      if (thisCopy[i] !== targetCopy[i]) {
-        x = thisCopy[i];
-        y = targetCopy[i];
-        break
-      }
-    }
-
-    if (x < y) return -1
-    if (y < x) return 1
-    return 0
-  };
-
-  // Finds either the first index of `val` in `buffer` at offset >= `byteOffset`,
-  // OR the last index of `val` in `buffer` at offset <= `byteOffset`.
-  //
-  // Arguments:
-  // - buffer - a Buffer to search
-  // - val - a string, Buffer, or number
-  // - byteOffset - an index into `buffer`; will be clamped to an int32
-  // - encoding - an optional encoding, relevant is val is a string
-  // - dir - true for indexOf, false for lastIndexOf
-  function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) {
-    // Empty buffer means no match
-    if (buffer.length === 0) return -1
-
-    // Normalize byteOffset
-    if (typeof byteOffset === 'string') {
-      encoding = byteOffset;
-      byteOffset = 0;
-    } else if (byteOffset > 0x7fffffff) {
-      byteOffset = 0x7fffffff;
-    } else if (byteOffset < -0x80000000) {
-      byteOffset = -0x80000000;
-    }
-    byteOffset = +byteOffset;  // Coerce to Number.
-    if (isNaN(byteOffset)) {
-      // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer
-      byteOffset = dir ? 0 : (buffer.length - 1);
-    }
-
-    // Normalize byteOffset: negative offsets start from the end of the buffer
-    if (byteOffset < 0) byteOffset = buffer.length + byteOffset;
-    if (byteOffset >= buffer.length) {
-      if (dir) return -1
-      else byteOffset = buffer.length - 1;
-    } else if (byteOffset < 0) {
-      if (dir) byteOffset = 0;
-      else return -1
-    }
-
-    // Normalize val
-    if (typeof val === 'string') {
-      val = Buffer.from(val, encoding);
-    }
-
-    // Finally, search either indexOf (if dir is true) or lastIndexOf
-    if (internalIsBuffer(val)) {
-      // Special case: looking for empty string/buffer always fails
-      if (val.length === 0) {
-        return -1
-      }
-      return arrayIndexOf(buffer, val, byteOffset, encoding, dir)
-    } else if (typeof val === 'number') {
-      val = val & 0xFF; // Search for a byte value [0-255]
-      if (Buffer.TYPED_ARRAY_SUPPORT &&
-          typeof Uint8Array.prototype.indexOf === 'function') {
-        if (dir) {
-          return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset)
-        } else {
-          return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset)
-        }
-      }
-      return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir)
-    }
-
-    throw new TypeError('val must be string, number or Buffer')
-  }
-
-  function arrayIndexOf (arr, val, byteOffset, encoding, dir) {
-    var indexSize = 1;
-    var arrLength = arr.length;
-    var valLength = val.length;
-
-    if (encoding !== undefined) {
-      encoding = String(encoding).toLowerCase();
-      if (encoding === 'ucs2' || encoding === 'ucs-2' ||
-          encoding === 'utf16le' || encoding === 'utf-16le') {
-        if (arr.length < 2 || val.length < 2) {
-          return -1
-        }
-        indexSize = 2;
-        arrLength /= 2;
-        valLength /= 2;
-        byteOffset /= 2;
-      }
-    }
-
-    function read (buf, i) {
-      if (indexSize === 1) {
-        return buf[i]
-      } else {
-        return buf.readUInt16BE(i * indexSize)
-      }
-    }
-
-    var i;
-    if (dir) {
-      var foundIndex = -1;
-      for (i = byteOffset; i < arrLength; i++) {
-        if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) {
-          if (foundIndex === -1) foundIndex = i;
-          if (i - foundIndex + 1 === valLength) return foundIndex * indexSize
-        } else {
-          if (foundIndex !== -1) i -= i - foundIndex;
-          foundIndex = -1;
-        }
-      }
-    } else {
-      if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength;
-      for (i = byteOffset; i >= 0; i--) {
-        var found = true;
-        for (var j = 0; j < valLength; j++) {
-          if (read(arr, i + j) !== read(val, j)) {
-            found = false;
-            break
-          }
-        }
-        if (found) return i
-      }
-    }
-
-    return -1
-  }
-
-  Buffer.prototype.includes = function includes (val, byteOffset, encoding) {
-    return this.indexOf(val, byteOffset, encoding) !== -1
-  };
-
-  Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) {
-    return bidirectionalIndexOf(this, val, byteOffset, encoding, true)
-  };
-
-  Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) {
-    return bidirectionalIndexOf(this, val, byteOffset, encoding, false)
-  };
-
-  function hexWrite (buf, string, offset, length) {
-    offset = Number(offset) || 0;
-    var remaining = buf.length - offset;
-    if (!length) {
-      length = remaining;
-    } else {
-      length = Number(length);
-      if (length > remaining) {
-        length = remaining;
-      }
-    }
-
-    // must be an even number of digits
-    var strLen = string.length;
-    if (strLen % 2 !== 0) throw new TypeError('Invalid hex string')
-
-    if (length > strLen / 2) {
-      length = strLen / 2;
-    }
-    for (var i = 0; i < length; ++i) {
-      var parsed = parseInt(string.substr(i * 2, 2), 16);
-      if (isNaN(parsed)) return i
-      buf[offset + i] = parsed;
-    }
-    return i
-  }
-
-  function utf8Write (buf, string, offset, length) {
-    return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length)
-  }
-
-  function asciiWrite (buf, string, offset, length) {
-    return blitBuffer(asciiToBytes(string), buf, offset, length)
-  }
-
-  function latin1Write (buf, string, offset, length) {
-    return asciiWrite(buf, string, offset, length)
-  }
-
-  function base64Write (buf, string, offset, length) {
-    return blitBuffer(base64ToBytes(string), buf, offset, length)
-  }
-
-  function ucs2Write (buf, string, offset, length) {
-    return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length)
-  }
-
-  Buffer.prototype.write = function write (string, offset, length, encoding) {
-    // Buffer#write(string)
-    if (offset === undefined) {
-      encoding = 'utf8';
-      length = this.length;
-      offset = 0;
-    // Buffer#write(string, encoding)
-    } else if (length === undefined && typeof offset === 'string') {
-      encoding = offset;
-      length = this.length;
-      offset = 0;
-    // Buffer#write(string, offset[, length][, encoding])
-    } else if (isFinite(offset)) {
-      offset = offset | 0;
-      if (isFinite(length)) {
-        length = length | 0;
-        if (encoding === undefined) encoding = 'utf8';
-      } else {
-        encoding = length;
-        length = undefined;
-      }
-    // legacy write(string, encoding, offset, length) - remove in v0.13
-    } else {
-      throw new Error(
-        'Buffer.write(string, encoding, offset[, length]) is no longer supported'
-      )
-    }
-
-    var remaining = this.length - offset;
-    if (length === undefined || length > remaining) length = remaining;
-
-    if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) {
-      throw new RangeError('Attempt to write outside buffer bounds')
-    }
-
-    if (!encoding) encoding = 'utf8';
-
-    var loweredCase = false;
-    for (;;) {
-      switch (encoding) {
-        case 'hex':
-          return hexWrite(this, string, offset, length)
-
-        case 'utf8':
-        case 'utf-8':
-          return utf8Write(this, string, offset, length)
-
-        case 'ascii':
-          return asciiWrite(this, string, offset, length)
-
-        case 'latin1':
-        case 'binary':
-          return latin1Write(this, string, offset, length)
-
-        case 'base64':
-          // Warning: maxLength not taken into account in base64Write
-          return base64Write(this, string, offset, length)
-
-        case 'ucs2':
-        case 'ucs-2':
-        case 'utf16le':
-        case 'utf-16le':
-          return ucs2Write(this, string, offset, length)
-
-        default:
-          if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
-          encoding = ('' + encoding).toLowerCase();
-          loweredCase = true;
-      }
-    }
-  };
-
-  Buffer.prototype.toJSON = function toJSON () {
-    return {
-      type: 'Buffer',
-      data: Array.prototype.slice.call(this._arr || this, 0)
-    }
-  };
-
-  function base64Slice (buf, start, end) {
-    if (start === 0 && end === buf.length) {
-      return fromByteArray(buf)
-    } else {
-      return fromByteArray(buf.slice(start, end))
-    }
-  }
-
-  function utf8Slice (buf, start, end) {
-    end = Math.min(buf.length, end);
-    var res = [];
-
-    var i = start;
-    while (i < end) {
-      var firstByte = buf[i];
-      var codePoint = null;
-      var bytesPerSequence = (firstByte > 0xEF) ? 4
-        : (firstByte > 0xDF) ? 3
-        : (firstByte > 0xBF) ? 2
-        : 1;
-
-      if (i + bytesPerSequence <= end) {
-        var secondByte, thirdByte, fourthByte, tempCodePoint;
-
-        switch (bytesPerSequence) {
-          case 1:
-            if (firstByte < 0x80) {
-              codePoint = firstByte;
-            }
-            break
-          case 2:
-            secondByte = buf[i + 1];
-            if ((secondByte & 0xC0) === 0x80) {
-              tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F);
-              if (tempCodePoint > 0x7F) {
-                codePoint = tempCodePoint;
-              }
-            }
-            break
-          case 3:
-            secondByte = buf[i + 1];
-            thirdByte = buf[i + 2];
-            if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) {
-              tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F);
-              if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) {
-                codePoint = tempCodePoint;
-              }
-            }
-            break
-          case 4:
-            secondByte = buf[i + 1];
-            thirdByte = buf[i + 2];
-            fourthByte = buf[i + 3];
-            if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) {
-              tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F);
-              if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) {
-                codePoint = tempCodePoint;
-              }
-            }
-        }
-      }
-
-      if (codePoint === null) {
-        // we did not generate a valid codePoint so insert a
-        // replacement char (U+FFFD) and advance only 1 byte
-        codePoint = 0xFFFD;
-        bytesPerSequence = 1;
-      } else if (codePoint > 0xFFFF) {
-        // encode to utf16 (surrogate pair dance)
-        codePoint -= 0x10000;
-        res.push(codePoint >>> 10 & 0x3FF | 0xD800);
-        codePoint = 0xDC00 | codePoint & 0x3FF;
-      }
-
-      res.push(codePoint);
-      i += bytesPerSequence;
-    }
-
-    return decodeCodePointsArray(res)
-  }
-
-  // Based on http://stackoverflow.com/a/22747272/680742, the browser with
-  // the lowest limit is Chrome, with 0x10000 args.
-  // We go 1 magnitude less, for safety
-  var MAX_ARGUMENTS_LENGTH = 0x1000;
-
-  function decodeCodePointsArray (codePoints) {
-    var len = codePoints.length;
-    if (len <= MAX_ARGUMENTS_LENGTH) {
-      return String.fromCharCode.apply(String, codePoints) // avoid extra slice()
-    }
-
-    // Decode in chunks to avoid "call stack size exceeded".
-    var res = '';
-    var i = 0;
-    while (i < len) {
-      res += String.fromCharCode.apply(
-        String,
-        codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH)
-      );
-    }
-    return res
-  }
-
-  function asciiSlice (buf, start, end) {
-    var ret = '';
-    end = Math.min(buf.length, end);
-
-    for (var i = start; i < end; ++i) {
-      ret += String.fromCharCode(buf[i] & 0x7F);
-    }
-    return ret
-  }
-
-  function latin1Slice (buf, start, end) {
-    var ret = '';
-    end = Math.min(buf.length, end);
-
-    for (var i = start; i < end; ++i) {
-      ret += String.fromCharCode(buf[i]);
-    }
-    return ret
-  }
-
-  function hexSlice (buf, start, end) {
-    var len = buf.length;
-
-    if (!start || start < 0) start = 0;
-    if (!end || end < 0 || end > len) end = len;
-
-    var out = '';
-    for (var i = start; i < end; ++i) {
-      out += toHex(buf[i]);
-    }
-    return out
-  }
-
-  function utf16leSlice (buf, start, end) {
-    var bytes = buf.slice(start, end);
-    var res = '';
-    for (var i = 0; i < bytes.length; i += 2) {
-      res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256);
-    }
-    return res
-  }
-
-  Buffer.prototype.slice = function slice (start, end) {
-    var len = this.length;
-    start = ~~start;
-    end = end === undefined ? len : ~~end;
-
-    if (start < 0) {
-      start += len;
-      if (start < 0) start = 0;
-    } else if (start > len) {
-      start = len;
-    }
-
-    if (end < 0) {
-      end += len;
-      if (end < 0) end = 0;
-    } else if (end > len) {
-      end = len;
-    }
-
-    if (end < start) end = start;
-
-    var newBuf;
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      newBuf = this.subarray(start, end);
-      newBuf.__proto__ = Buffer.prototype;
-    } else {
-      var sliceLen = end - start;
-      newBuf = new Buffer(sliceLen, undefined);
-      for (var i = 0; i < sliceLen; ++i) {
-        newBuf[i] = this[i + start];
-      }
-    }
-
-    return newBuf
-  };
-
-  /*
-   * Need to make sure that buffer isn't trying to write out of bounds.
-   */
-  function checkOffset (offset, ext, length) {
-    if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint')
-    if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length')
-  }
-
-  Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) {
-    offset = offset | 0;
-    byteLength = byteLength | 0;
-    if (!noAssert) checkOffset(offset, byteLength, this.length);
-
-    var val = this[offset];
-    var mul = 1;
-    var i = 0;
-    while (++i < byteLength && (mul *= 0x100)) {
-      val += this[offset + i] * mul;
-    }
-
-    return val
-  };
-
-  Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) {
-    offset = offset | 0;
-    byteLength = byteLength | 0;
-    if (!noAssert) {
-      checkOffset(offset, byteLength, this.length);
-    }
-
-    var val = this[offset + --byteLength];
-    var mul = 1;
-    while (byteLength > 0 && (mul *= 0x100)) {
-      val += this[offset + --byteLength] * mul;
-    }
-
-    return val
-  };
-
-  Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 1, this.length);
-    return this[offset]
-  };
-
-  Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 2, this.length);
-    return this[offset] | (this[offset + 1] << 8)
-  };
-
-  Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 2, this.length);
-    return (this[offset] << 8) | this[offset + 1]
-  };
-
-  Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 4, this.length);
-
-    return ((this[offset]) |
-        (this[offset + 1] << 8) |
-        (this[offset + 2] << 16)) +
-        (this[offset + 3] * 0x1000000)
-  };
-
-  Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 4, this.length);
-
-    return (this[offset] * 0x1000000) +
-      ((this[offset + 1] << 16) |
-      (this[offset + 2] << 8) |
-      this[offset + 3])
-  };
-
-  Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) {
-    offset = offset | 0;
-    byteLength = byteLength | 0;
-    if (!noAssert) checkOffset(offset, byteLength, this.length);
-
-    var val = this[offset];
-    var mul = 1;
-    var i = 0;
-    while (++i < byteLength && (mul *= 0x100)) {
-      val += this[offset + i] * mul;
-    }
-    mul *= 0x80;
-
-    if (val >= mul) val -= Math.pow(2, 8 * byteLength);
-
-    return val
-  };
-
-  Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) {
-    offset = offset | 0;
-    byteLength = byteLength | 0;
-    if (!noAssert) checkOffset(offset, byteLength, this.length);
-
-    var i = byteLength;
-    var mul = 1;
-    var val = this[offset + --i];
-    while (i > 0 && (mul *= 0x100)) {
-      val += this[offset + --i] * mul;
-    }
-    mul *= 0x80;
-
-    if (val >= mul) val -= Math.pow(2, 8 * byteLength);
-
-    return val
-  };
-
-  Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 1, this.length);
-    if (!(this[offset] & 0x80)) return (this[offset])
-    return ((0xff - this[offset] + 1) * -1)
-  };
-
-  Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 2, this.length);
-    var val = this[offset] | (this[offset + 1] << 8);
-    return (val & 0x8000) ? val | 0xFFFF0000 : val
-  };
-
-  Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 2, this.length);
-    var val = this[offset + 1] | (this[offset] << 8);
-    return (val & 0x8000) ? val | 0xFFFF0000 : val
-  };
-
-  Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 4, this.length);
-
-    return (this[offset]) |
-      (this[offset + 1] << 8) |
-      (this[offset + 2] << 16) |
-      (this[offset + 3] << 24)
-  };
-
-  Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 4, this.length);
-
-    return (this[offset] << 24) |
-      (this[offset + 1] << 16) |
-      (this[offset + 2] << 8) |
-      (this[offset + 3])
-  };
-
-  Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 4, this.length);
-    return read(this, offset, true, 23, 4)
-  };
-
-  Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 4, this.length);
-    return read(this, offset, false, 23, 4)
-  };
-
-  Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 8, this.length);
-    return read(this, offset, true, 52, 8)
-  };
-
-  Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) {
-    if (!noAssert) checkOffset(offset, 8, this.length);
-    return read(this, offset, false, 52, 8)
-  };
-
-  function checkInt (buf, value, offset, ext, max, min) {
-    if (!internalIsBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance')
-    if (value > max || value < min) throw new RangeError('"value" argument is out of bounds')
-    if (offset + ext > buf.length) throw new RangeError('Index out of range')
-  }
-
-  Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    byteLength = byteLength | 0;
-    if (!noAssert) {
-      var maxBytes = Math.pow(2, 8 * byteLength) - 1;
-      checkInt(this, value, offset, byteLength, maxBytes, 0);
-    }
-
-    var mul = 1;
-    var i = 0;
-    this[offset] = value & 0xFF;
-    while (++i < byteLength && (mul *= 0x100)) {
-      this[offset + i] = (value / mul) & 0xFF;
-    }
-
-    return offset + byteLength
-  };
-
-  Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    byteLength = byteLength | 0;
-    if (!noAssert) {
-      var maxBytes = Math.pow(2, 8 * byteLength) - 1;
-      checkInt(this, value, offset, byteLength, maxBytes, 0);
-    }
-
-    var i = byteLength - 1;
-    var mul = 1;
-    this[offset + i] = value & 0xFF;
-    while (--i >= 0 && (mul *= 0x100)) {
-      this[offset + i] = (value / mul) & 0xFF;
-    }
-
-    return offset + byteLength
-  };
-
-  Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0);
-    if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value);
-    this[offset] = (value & 0xff);
-    return offset + 1
-  };
-
-  function objectWriteUInt16 (buf, value, offset, littleEndian) {
-    if (value < 0) value = 0xffff + value + 1;
-    for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; ++i) {
-      buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>>
-        (littleEndian ? i : 1 - i) * 8;
-    }
-  }
-
-  Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0);
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset] = (value & 0xff);
-      this[offset + 1] = (value >>> 8);
-    } else {
-      objectWriteUInt16(this, value, offset, true);
-    }
-    return offset + 2
-  };
-
-  Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0);
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset] = (value >>> 8);
-      this[offset + 1] = (value & 0xff);
-    } else {
-      objectWriteUInt16(this, value, offset, false);
-    }
-    return offset + 2
-  };
-
-  function objectWriteUInt32 (buf, value, offset, littleEndian) {
-    if (value < 0) value = 0xffffffff + value + 1;
-    for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; ++i) {
-      buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff;
-    }
-  }
-
-  Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0);
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset + 3] = (value >>> 24);
-      this[offset + 2] = (value >>> 16);
-      this[offset + 1] = (value >>> 8);
-      this[offset] = (value & 0xff);
-    } else {
-      objectWriteUInt32(this, value, offset, true);
-    }
-    return offset + 4
-  };
-
-  Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0);
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset] = (value >>> 24);
-      this[offset + 1] = (value >>> 16);
-      this[offset + 2] = (value >>> 8);
-      this[offset + 3] = (value & 0xff);
-    } else {
-      objectWriteUInt32(this, value, offset, false);
-    }
-    return offset + 4
-  };
-
-  Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) {
-      var limit = Math.pow(2, 8 * byteLength - 1);
-
-      checkInt(this, value, offset, byteLength, limit - 1, -limit);
-    }
-
-    var i = 0;
-    var mul = 1;
-    var sub = 0;
-    this[offset] = value & 0xFF;
-    while (++i < byteLength && (mul *= 0x100)) {
-      if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) {
-        sub = 1;
-      }
-      this[offset + i] = ((value / mul) >> 0) - sub & 0xFF;
-    }
-
-    return offset + byteLength
-  };
-
-  Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) {
-      var limit = Math.pow(2, 8 * byteLength - 1);
-
-      checkInt(this, value, offset, byteLength, limit - 1, -limit);
-    }
-
-    var i = byteLength - 1;
-    var mul = 1;
-    var sub = 0;
-    this[offset + i] = value & 0xFF;
-    while (--i >= 0 && (mul *= 0x100)) {
-      if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) {
-        sub = 1;
-      }
-      this[offset + i] = ((value / mul) >> 0) - sub & 0xFF;
-    }
-
-    return offset + byteLength
-  };
-
-  Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80);
-    if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value);
-    if (value < 0) value = 0xff + value + 1;
-    this[offset] = (value & 0xff);
-    return offset + 1
-  };
-
-  Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000);
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset] = (value & 0xff);
-      this[offset + 1] = (value >>> 8);
-    } else {
-      objectWriteUInt16(this, value, offset, true);
-    }
-    return offset + 2
-  };
-
-  Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000);
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset] = (value >>> 8);
-      this[offset + 1] = (value & 0xff);
-    } else {
-      objectWriteUInt16(this, value, offset, false);
-    }
-    return offset + 2
-  };
-
-  Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000);
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset] = (value & 0xff);
-      this[offset + 1] = (value >>> 8);
-      this[offset + 2] = (value >>> 16);
-      this[offset + 3] = (value >>> 24);
-    } else {
-      objectWriteUInt32(this, value, offset, true);
-    }
-    return offset + 4
-  };
-
-  Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) {
-    value = +value;
-    offset = offset | 0;
-    if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000);
-    if (value < 0) value = 0xffffffff + value + 1;
-    if (Buffer.TYPED_ARRAY_SUPPORT) {
-      this[offset] = (value >>> 24);
-      this[offset + 1] = (value >>> 16);
-      this[offset + 2] = (value >>> 8);
-      this[offset + 3] = (value & 0xff);
-    } else {
-      objectWriteUInt32(this, value, offset, false);
-    }
-    return offset + 4
-  };
-
-  function checkIEEE754 (buf, value, offset, ext, max, min) {
-    if (offset + ext > buf.length) throw new RangeError('Index out of range')
-    if (offset < 0) throw new RangeError('Index out of range')
-  }
-
-  function writeFloat (buf, value, offset, littleEndian, noAssert) {
-    if (!noAssert) {
-      checkIEEE754(buf, value, offset, 4);
-    }
-    write(buf, value, offset, littleEndian, 23, 4);
-    return offset + 4
-  }
-
-  Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) {
-    return writeFloat(this, value, offset, true, noAssert)
-  };
-
-  Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) {
-    return writeFloat(this, value, offset, false, noAssert)
-  };
-
-  function writeDouble (buf, value, offset, littleEndian, noAssert) {
-    if (!noAssert) {
-      checkIEEE754(buf, value, offset, 8);
-    }
-    write(buf, value, offset, littleEndian, 52, 8);
-    return offset + 8
-  }
-
-  Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) {
-    return writeDouble(this, value, offset, true, noAssert)
-  };
-
-  Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) {
-    return writeDouble(this, value, offset, false, noAssert)
-  };
-
-  // copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
-  Buffer.prototype.copy = function copy (target, targetStart, start, end) {
-    if (!start) start = 0;
-    if (!end && end !== 0) end = this.length;
-    if (targetStart >= target.length) targetStart = target.length;
-    if (!targetStart) targetStart = 0;
-    if (end > 0 && end < start) end = start;
-
-    // Copy 0 bytes; we're done
-    if (end === start) return 0
-    if (target.length === 0 || this.length === 0) return 0
-
-    // Fatal error conditions
-    if (targetStart < 0) {
-      throw new RangeError('targetStart out of bounds')
-    }
-    if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds')
-    if (end < 0) throw new RangeError('sourceEnd out of bounds')
-
-    // Are we oob?
-    if (end > this.length) end = this.length;
-    if (target.length - targetStart < end - start) {
-      end = target.length - targetStart + start;
-    }
-
-    var len = end - start;
-    var i;
-
-    if (this === target && start < targetStart && targetStart < end) {
-      // descending copy from end
-      for (i = len - 1; i >= 0; --i) {
-        target[i + targetStart] = this[i + start];
-      }
-    } else if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) {
-      // ascending copy from start
-      for (i = 0; i < len; ++i) {
-        target[i + targetStart] = this[i + start];
-      }
-    } else {
-      Uint8Array.prototype.set.call(
-        target,
-        this.subarray(start, start + len),
-        targetStart
-      );
-    }
-
-    return len
-  };
-
-  // Usage:
-  //    buffer.fill(number[, offset[, end]])
-  //    buffer.fill(buffer[, offset[, end]])
-  //    buffer.fill(string[, offset[, end]][, encoding])
-  Buffer.prototype.fill = function fill (val, start, end, encoding) {
-    // Handle string cases:
-    if (typeof val === 'string') {
-      if (typeof start === 'string') {
-        encoding = start;
-        start = 0;
-        end = this.length;
-      } else if (typeof end === 'string') {
-        encoding = end;
-        end = this.length;
-      }
-      if (val.length === 1) {
-        var code = val.charCodeAt(0);
-        if (code < 256) {
-          val = code;
-        }
-      }
-      if (encoding !== undefined && typeof encoding !== 'string') {
-        throw new TypeError('encoding must be a string')
-      }
-      if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) {
-        throw new TypeError('Unknown encoding: ' + encoding)
-      }
-    } else if (typeof val === 'number') {
-      val = val & 255;
-    }
-
-    // Invalid ranges are not set to a default, so can range check early.
-    if (start < 0 || this.length < start || this.length < end) {
-      throw new RangeError('Out of range index')
-    }
-
-    if (end <= start) {
-      return this
-    }
-
-    start = start >>> 0;
-    end = end === undefined ? this.length : end >>> 0;
-
-    if (!val) val = 0;
-
-    var i;
-    if (typeof val === 'number') {
-      for (i = start; i < end; ++i) {
-        this[i] = val;
-      }
-    } else {
-      var bytes = internalIsBuffer(val)
-        ? val
-        : utf8ToBytes(new Buffer(val, encoding).toString());
-      var len = bytes.length;
-      for (i = 0; i < end - start; ++i) {
-        this[i + start] = bytes[i % len];
-      }
-    }
-
-    return this
-  };
-
-  // HELPER FUNCTIONS
-  // ================
-
-  var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g;
-
-  function base64clean (str) {
-    // Node strips out invalid characters like \n and \t from the string, base64-js does not
-    str = stringtrim(str).replace(INVALID_BASE64_RE, '');
-    // Node converts strings with length < 2 to ''
-    if (str.length < 2) return ''
-    // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
-    while (str.length % 4 !== 0) {
-      str = str + '=';
-    }
-    return str
-  }
-
-  function stringtrim (str) {
-    if (str.trim) return str.trim()
-    return str.replace(/^\s+|\s+$/g, '')
-  }
-
-  function toHex (n) {
-    if (n < 16) return '0' + n.toString(16)
-    return n.toString(16)
-  }
-
-  function utf8ToBytes (string, units) {
-    units = units || Infinity;
-    var codePoint;
-    var length = string.length;
-    var leadSurrogate = null;
-    var bytes = [];
-
-    for (var i = 0; i < length; ++i) {
-      codePoint = string.charCodeAt(i);
-
-      // is surrogate component
-      if (codePoint > 0xD7FF && codePoint < 0xE000) {
-        // last char was a lead
-        if (!leadSurrogate) {
-          // no lead yet
-          if (codePoint > 0xDBFF) {
-            // unexpected trail
-            if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
-            continue
-          } else if (i + 1 === length) {
-            // unpaired lead
-            if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
-            continue
-          }
-
-          // valid lead
-          leadSurrogate = codePoint;
-
-          continue
-        }
-
-        // 2 leads in a row
-        if (codePoint < 0xDC00) {
-          if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
-          leadSurrogate = codePoint;
-          continue
-        }
-
-        // valid surrogate pair
-        codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000;
-      } else if (leadSurrogate) {
-        // valid bmp char, but last char was a lead
-        if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD);
-      }
-
-      leadSurrogate = null;
-
-      // encode utf8
-      if (codePoint < 0x80) {
-        if ((units -= 1) < 0) break
-        bytes.push(codePoint);
-      } else if (codePoint < 0x800) {
-        if ((units -= 2) < 0) break
-        bytes.push(
-          codePoint >> 0x6 | 0xC0,
-          codePoint & 0x3F | 0x80
-        );
-      } else if (codePoint < 0x10000) {
-        if ((units -= 3) < 0) break
-        bytes.push(
-          codePoint >> 0xC | 0xE0,
-          codePoint >> 0x6 & 0x3F | 0x80,
-          codePoint & 0x3F | 0x80
-        );
-      } else if (codePoint < 0x110000) {
-        if ((units -= 4) < 0) break
-        bytes.push(
-          codePoint >> 0x12 | 0xF0,
-          codePoint >> 0xC & 0x3F | 0x80,
-          codePoint >> 0x6 & 0x3F | 0x80,
-          codePoint & 0x3F | 0x80
-        );
-      } else {
-        throw new Error('Invalid code point')
-      }
-    }
-
-    return bytes
-  }
-
-  function asciiToBytes (str) {
-    var byteArray = [];
-    for (var i = 0; i < str.length; ++i) {
-      // Node's code seems to be doing this and not & 0x7F..
-      byteArray.push(str.charCodeAt(i) & 0xFF);
-    }
-    return byteArray
-  }
-
-  function utf16leToBytes (str, units) {
-    var c, hi, lo;
-    var byteArray = [];
-    for (var i = 0; i < str.length; ++i) {
-      if ((units -= 2) < 0) break
-
-      c = str.charCodeAt(i);
-      hi = c >> 8;
-      lo = c % 256;
-      byteArray.push(lo);
-      byteArray.push(hi);
-    }
-
-    return byteArray
-  }
-
-
-  function base64ToBytes (str) {
-    return toByteArray(base64clean(str))
-  }
-
-  function blitBuffer (src, dst, offset, length) {
-    for (var i = 0; i < length; ++i) {
-      if ((i + offset >= dst.length) || (i >= src.length)) break
-      dst[i + offset] = src[i];
-    }
-    return i
-  }
-
-  function isnan (val) {
-    return val !== val // eslint-disable-line no-self-compare
-  }
-
-
-  // the following is from is-buffer, also by Feross Aboukhadijeh and with same lisence
-  // The _isBuffer check is for Safari 5-7 support, because it's missing
-  // Object.prototype.constructor. Remove this eventually
-  function isBuffer(obj) {
-    return obj != null && (!!obj._isBuffer || isFastBuffer(obj) || isSlowBuffer(obj))
-  }
-
-  function isFastBuffer (obj) {
-    return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
-  }
-
-  // For Node v0.10 support. Remove this eventually.
-  function isSlowBuffer (obj) {
-    return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isFastBuffer(obj.slice(0, 0))
-  }
-
-  class Provider$3 {
-    constructor() {}
-
-    getAll() {
-      return this.data;
-    }
-
-  }
-
-  var provider = Provider$3;
-
-  const Provider$2 = provider;
-
-  class Crawlers$1 extends Provider$2 {
-    constructor() {
-      super();
-      this.data = [' YLT', '^Aether', '^Amazon Simple Notification Service Agent$', '^Amazon-Route53-Health-Check-Service', '^b0t$', '^bluefish ', '^Calypso v\\/', '^COMODO DCV', '^Corax', '^DangDang', '^DavClnt', '^DHSH', '^docker\\/[0-9]', '^Expanse', '^FDM ', '^git\\/', '^Goose\\/', '^Grabber', '^Gradle\\/', '^HTTPClient\\/', '^HTTPing', '^Java\\/', '^Jeode\\/', '^Jetty\\/', '^Mail\\/', '^Mget', '^Microsoft URL Control', '^Mikrotik\\/', '^Netlab360', '^NG\\/[0-9\\.]', '^NING\\/', '^np [...]
-    }
-
-  }
-
-  var crawlers = Crawlers$1;
-
-  const Provider$1 = provider;
-
-  class Exclusions$1 extends Provider$1 {
-    constructor() {
-      super();
-      this.data = ['Safari.[\\d\\.]*', 'Firefox.[\\d\\.]*', ' Chrome.[\\d\\.]*', 'Chromium.[\\d\\.]*', 'MSIE.[\\d\\.]', 'Opera\\/[\\d\\.]*', 'Mozilla.[\\d\\.]*', 'AppleWebKit.[\\d\\.]*', 'Trident.[\\d\\.]*', 'Windows NT.[\\d\\.]*', 'Android [\\d\\.]*', 'Macintosh.', 'Ubuntu', 'Linux', '[ ]Intel', 'Mac OS X [\\d_]*', '(like )?Gecko(.[\\d\\.]*)?', 'KHTML,', 'CriOS.[\\d\\.]*', 'CPU iPhone OS ([0-9_])* like Mac OS X', 'CPU OS ([0-9_])* like Mac OS X', 'iPod', 'compatible', 'x86_..', 'i686',  [...]
-    }
-
-  }
-
-  var exclusions = Exclusions$1;
-
-  const Provider = provider;
-
-  class Headers$1 extends Provider {
-    constructor() {
-      super();
-      this.data = ['USER-AGENT', 'X-OPERAMINI-PHONE-UA', 'X-DEVICE-USER-AGENT', 'X-ORIGINAL-USER-AGENT', 'X-SKYFIRE-PHONE', 'X-BOLT-PHONE-UA', 'DEVICE-STOCK-UA', 'X-UCBROWSER-DEVICE-UA', 'FROM', 'X-SCANNER'];
-    }
-
-  }
-
-  var headers = Headers$1;
-
-  const Crawlers = crawlers;
-  const Exclusions = exclusions;
-  const Headers = headers;
-
-  class Crawler$1 {
-    constructor(request, headers, userAgent) {
-      /**
-       * Init classes
-       */
-      this._init();
-      /**
-       * This request must be an object
-       */
-
-
-      this.request = typeof request === 'object' ? request : {}; // The regex-list must not be used with g-flag!
-      // See: https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-results
-
-      this.compiledRegexList = this.compileRegex(this.crawlers.getAll(), 'i'); // The exclusions should be used with g-flag in order to remove each value.
-
-      this.compiledExclusions = this.compileRegex(this.exclusions.getAll(), 'gi');
-      /**
-       * Set http headers
-       */
-
-      this.setHttpHeaders(headers);
-      /**
-       * Set userAgent
-       */
-
-      this.userAgent = this.setUserAgent(userAgent);
-    }
-    /**
-     * Init Classes Instances
-     */
-
-
-    _init() {
-      this.crawlers = new Crawlers();
-      this.headers = new Headers();
-      this.exclusions = new Exclusions();
-    }
-
-    compileRegex(patterns, flags) {
-      return new RegExp(patterns.join('|'), flags);
-    }
-    /**
-     * Set HTTP headers.
-     */
-
-
-    setHttpHeaders(headers) {
-      // Use the Request headers if httpHeaders is not defined
-      if (typeof headers === 'undefined' || Object.keys(headers).length === 0) {
-        headers = Object.keys(this.request).length ? this.request.headers : {};
-      } // Save the headers.
-
-
-      this.httpHeaders = headers;
-    }
-    /**
-     * Set user agent
-     */
-
-
-    setUserAgent(userAgent) {
-      if (typeof userAgent === 'undefined' || userAgent === null || !userAgent.length) {
-        for (const header of this.getUaHttpHeaders()) {
-          if (Object.keys(this.httpHeaders).indexOf(header.toLowerCase()) >= 0) {
-            userAgent += this.httpHeaders[header.toLowerCase()] + ' ';
-          }
-        }
-      }
-
-      return userAgent;
-    }
-    /**
-     * Get user agent headers
-     */
-
-
-    getUaHttpHeaders() {
-      return this.headers.getAll();
-    }
-    /**
-     * Check user agent string against the regex.
-     */
-
-
-    isCrawler(userAgent = undefined) {
-      if (Buffer.byteLength(userAgent || '', 'utf8') > 4096) {
-        return false;
-      }
-
-      var agent = typeof userAgent === 'undefined' || userAgent === null ? this.userAgent : userAgent; // test on compiled regx
-
-      agent = agent.replace(this.compiledExclusions, '');
-
-      if (agent.trim().length === 0) {
-        return false;
-      }
-
-      var matches = this.compiledRegexList.exec(agent);
-
-      if (matches) {
-        this.matches = matches;
-      }
-
-      return matches !== null ? matches.length ? true : false : false;
-    }
-    /**
-     * Return the matches.
-     */
-
-
-    getMatches() {
-      return this.matches !== undefined ? this.matches.length ? this.matches[0] : null : {};
-    }
-
-  }
-
-  var crawler = Crawler$1;
-
-  const Crawler = crawler;
-  var src = {
-    Crawler,
-
-    middleware(cb) {
-      return (req, res, next) => {
-        // If there is a cb, execute it
-        if (typeof cb === 'function') {
-          cb.call(req, res);
-        } // Initiate
-
-
-        req.Crawler = new Crawler(req);
-        next();
-      };
-    }
-
-  };
-
-  /**
-   * Licensed to the Apache Software Foundation (ASF) under one or more
-   * contributor license agreements.  See the NOTICE file distributed with
-   * this work for additional information regarding copyright ownership.
-   * The ASF licenses this file to You under the Apache License, Version 2.0
-   * (the "License"); you may not use this file except in compliance with
-   * the License.  You may obtain a copy of the License at
-   *
-   * http://www.apache.org/licenses/LICENSE-2.0
-   *
-   * Unless required by applicable law or agreed to in writing, software
-   * distributed under the License is distributed on an "AS IS" BASIS,
-   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   * See the License for the specific language governing permissions and
-   * limitations under the License.
-   */
-
-  function _createForOfIteratorHelper(o, allowArrayLike) {
-    var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
-
-    if (!it) {
-      if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
-        if (it) o = it;
-        var i = 0;
-
-        var F = function F() {};
-
-        return {
-          s: F,
-          n: function n() {
-            if (i >= o.length) return {
-              done: true
-            };
-            return {
-              done: false,
-              value: o[i++]
-            };
-          },
-          e: function e(_e) {
-            throw _e;
-          },
-          f: F
-        };
-      }
-
-      throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
-    }
-
-    var normalCompletion = true,
-        didErr = false,
-        err;
-    return {
-      s: function s() {
-        it = it.call(o);
-      },
-      n: function n() {
-        var step = it.next();
-        normalCompletion = step.done;
-        return step;
-      },
-      e: function e(_e2) {
-        didErr = true;
-        err = _e2;
-      },
-      f: function f() {
-        try {
-          if (!normalCompletion && it.return != null) it.return();
-        } finally {
-          if (didErr) throw err;
-        }
-      }
-    };
-  }
-
-  function _unsupportedIterableToArray(o, minLen) {
-    if (!o) return;
-    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
-    var n = Object.prototype.toString.call(o).slice(8, -1);
-    if (n === "Object" && o.constructor) n = o.constructor.name;
-    if (n === "Map" || n === "Set") return Array.from(o);
-    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
-  }
-
-  function _arrayLikeToArray(arr, len) {
-    if (len == null || len > arr.length) len = arr.length;
-
-    for (var i = 0, arr2 = new Array(len); i < len; i++) {
-      arr2[i] = arr[i];
-    }
-
-    return arr2;
-  }
-
-  var newTracker = function newTracker() {
-    var wem = {
-      /**
-       * This function initialize the tracker
-       *
-       * @param {object} digitalData config of the tracker
-       * @returns {undefined}
-       */
-      initTracker: function initTracker(digitalData) {
-        wem.digitalData = digitalData;
-        wem.trackerProfileIdCookieName = wem.digitalData.wemInitConfig.trackerProfileIdCookieName ? wem.digitalData.wemInitConfig.trackerProfileIdCookieName : 'wem-profile-id';
-        wem.trackerSessionIdCookieName = wem.digitalData.wemInitConfig.trackerSessionIdCookieName ? wem.digitalData.wemInitConfig.trackerSessionIdCookieName : 'wem-session-id';
-        wem.browserGeneratedSessionSuffix = wem.digitalData.wemInitConfig.browserGeneratedSessionSuffix ? wem.digitalData.wemInitConfig.browserGeneratedSessionSuffix : '';
-        wem.disableTrackedConditionsListeners = wem.digitalData.wemInitConfig.disableTrackedConditionsListeners;
-        wem.activateWem = wem.digitalData.wemInitConfig.activateWem;
-        var _wem$digitalData$wemI = wem.digitalData.wemInitConfig,
-            contextServerUrl = _wem$digitalData$wemI.contextServerUrl,
-            timeoutInMilliseconds = _wem$digitalData$wemI.timeoutInMilliseconds,
-            contextServerCookieName = _wem$digitalData$wemI.contextServerCookieName;
-        wem.contextServerCookieName = contextServerCookieName;
-        wem.contextServerUrl = contextServerUrl;
-        wem.timeoutInMilliseconds = timeoutInMilliseconds;
-        wem.formNamesToWatch = [];
-        wem.eventsPrevented = [];
-        wem.sessionID = wem.getCookie(wem.trackerSessionIdCookieName);
-        wem.fallback = false;
-
-        if (wem.sessionID === null) {
-          console.warn('[WEM] sessionID is null !');
-        } else if (!wem.sessionID || wem.sessionID === '') {
-          console.warn('[WEM] empty sessionID, setting to null !');
-          wem.sessionID = null;
-        }
-      },
-
-      /**
-       * This function start the tracker by loading the context in the page
-       * Note: that the tracker will start once the current DOM is complete loaded, using listener on current document: DOMContentLoaded
-       *
-       * @param {object[]} digitalDataOverrides optional, list of digitalData extensions, they will be merged with original digitalData before context loading
-       * @returns {undefined}
-       */
-      startTracker: function startTracker() {
-        var digitalDataOverrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; // Check before start
-
-        var cookieDisabled = !navigator.cookieEnabled;
-        var noSessionID = !wem.sessionID || wem.sessionID === '';
-        var crawlerDetected = navigator.userAgent;
-
-        if (crawlerDetected) {
-          var browserDetector = new src.Crawler();
-          crawlerDetected = browserDetector.isCrawler(navigator.userAgent);
-        }
-
-        if (cookieDisabled || noSessionID || crawlerDetected) {
-          document.addEventListener('DOMContentLoaded', function () {
-            wem._executeFallback('navigator cookie disabled: ' + cookieDisabled + ', no sessionID: ' + noSessionID + ', web crawler detected: ' + crawlerDetected);
-          });
-          return;
-        } // Register base context callback
-
-
-        wem._registerCallback(function () {
-          if (wem.cxs.profileId) {
-            wem.setCookie(wem.trackerProfileIdCookieName, wem.cxs.profileId);
-          }
-
-          if (!wem.cxs.profileId) {
-            wem.removeCookie(wem.trackerProfileIdCookieName);
-          }
-
-          if (!wem.disableTrackedConditionsListeners) {
-            wem._registerListenersForTrackedConditions();
-          }
-        }, 'Default tracker', 0); // Load the context once document is ready
-
-
-        document.addEventListener('DOMContentLoaded', function () {
-          wem.DOMLoaded = true; // enrich digital data considering extensions
-
-          wem._handleDigitalDataOverrides(digitalDataOverrides); // complete already registered events
-
-
-          wem._checkUncompleteRegisteredEvents(); // Dispatch javascript events for the experience (perso/opti displayed from SSR, based on unomi events)
-
-
-          wem._dispatchJSExperienceDisplayedEvents(); // Some event may not need to be send to unomi, check for them and filter them out.
-
-
-          wem._filterUnomiEvents(); // Add referrer info into digitalData.page object.
-
-
-          wem._processReferrer(); // Build view event
-
-
-          var viewEvent = wem.buildEvent('view', wem.buildTargetPage(), wem.buildSource(wem.digitalData.site.siteInfo.siteID, 'site'));
-          viewEvent.flattenedProperties = {}; // Add URLParameters
-
-          if (location.search) {
-            viewEvent.flattenedProperties['URLParameters'] = wem.convertUrlParametersToObj(location.search);
-          } // Add interests
-
-
-          if (wem.digitalData.interests) {
-            viewEvent.flattenedProperties['interests'] = wem.digitalData.interests;
-          } // Register the page view event, it's unshift because it should be the first event, this is just for logical purpose. (page view comes before perso displayed event for example)
-
-
-          wem._registerEvent(viewEvent, true);
-
-          if (wem.activateWem) {
-            wem.loadContext();
-          } else {
-            wem._executeFallback('wem is not activated on current page');
-          }
-        });
-      },
-
-      /**
-       * get the current loaded context from Unomi, will be accessible only after loadContext() have been performed
-       * @returns {object} loaded context
-       */
-      getLoadedContext: function getLoadedContext() {
-        return wem.cxs;
-      },
-
-      /**
-       * In case Unomi contains rules related to HTML forms in the current page.
-       * The logic is simple, in case a rule exists in Unomi targeting a form event within the current webpage path
-       *  - then this form will be identified as form to be watched.
-       * You can reuse this function to get the list of concerned forms in order to attach listeners automatically for those form for example
-       * (not that current tracker is doing that by default, check function: _registerListenersForTrackedConditions())
-       * @returns {string[]} form names/ids in current web page
-       */
-      getFormNamesToWatch: function getFormNamesToWatch() {
-        return wem.formNamesToWatch;
-      },
-
-      /**
-       * Get current session id
-       * @returns {null|string} get current session id
-       */
-      getSessionId: function getSessionId() {
-        return wem.sessionID;
-      },
-
-      /**
-       * This function will register a personalization
-       *
-       * @param {object} personalization the personalization object
-       * @param {object} variants the variants
-       * @param {boolean} [ajax] Deprecated: Ajax rendering is not supported anymore
-       * @param {function} [resultCallback] the callback to be executed after personalization resolved
-       * @returns {undefined}
-       */
-      registerPersonalizationObject: function registerPersonalizationObject(personalization, variants, ajax, resultCallback) {
-        var target = personalization.id;
-
-        wem._registerPersonalizationCallback(personalization, function (result, additionalResultInfos) {
-          var selectedFilter = null;
-          var successfulFilters = [];
-          var inControlGroup = additionalResultInfos && additionalResultInfos.inControlGroup; // In case of control group Unomi is not resolving any strategy or fallback for us. So we have to do the fallback here.
-
-          if (inControlGroup && personalization.strategyOptions && personalization.strategyOptions.fallback) {
-            selectedFilter = variants[personalization.strategyOptions.fallback];
-            successfulFilters.push(selectedFilter);
-          } else {
-            for (var i = 0; i < result.length; i++) {
-              successfulFilters.push(variants[result[i]]);
-            }
-
-            if (successfulFilters.length > 0) {
-              selectedFilter = successfulFilters[0];
-              var minPos = successfulFilters[0].position;
-
-              if (minPos >= 0) {
-                for (var j = 1; j < successfulFilters.length; j++) {
-                  if (successfulFilters[j].position < minPos) {
-                    selectedFilter = successfulFilters[j];
-                  }
-                }
-              }
-            }
-          }
-
-          if (resultCallback) {
-            // execute callback
-            resultCallback(successfulFilters, selectedFilter);
-          } else {
-            if (selectedFilter) {
-              var targetFilters = document.getElementById(target).children;
-
-              for (var fIndex in targetFilters) {
-                var filter = targetFilters.item(fIndex);
-
-                if (filter) {
-                  filter.style.display = filter.id === selectedFilter.content ? '' : 'none';
-                }
-              } // we now add control group information to event if the user is in the control group.
-
-
-              if (inControlGroup) {
-                console.info('[WEM] Profile is in control group for target: ' + target + ', adding to personalization event...');
-                selectedFilter.event.target.properties.inControlGroup = true;
-
-                if (selectedFilter.event.target.properties.variants) {
-                  selectedFilter.event.target.properties.variants.forEach(function (variant) {
-                    return variant.inControlGroup = true;
-                  });
-                }
-              } // send event to unomi
-
-
-              wem.collectEvent(wem._completeEvent(selectedFilter.event), function () {
-                console.info('[WEM] Personalization event successfully collected.');
-              }, function () {
-                console.error('[WEM] Could not send personalization event.');
-              }); //Trigger variant display event for personalization
-
-              wem._dispatchJSExperienceDisplayedEvent(selectedFilter.event);
-            } else {
-              var elements = document.getElementById(target).children;
-
-              for (var eIndex in elements) {
-                var el = elements.item(eIndex);
-                el.style.display = 'none';
-              }
-            }
-          }
-        });
-      },
-
-      /**
-       * This function will register an optimization test or A/B test
-       *
-       * @param {string} optimizationTestNodeId the optimization test node id
-       * @param {string} goalId the associated goal Id
-       * @param {string} containerId the HTML container Id
-       * @param {object} variants the variants
-       * @param {boolean} [ajax] Deprecated: Ajax rendering is not supported anymore
-       * @param {object} [variantsTraffic] the associated traffic allocation
-       * @return {undefined}
-       */
-      registerOptimizationTest: function registerOptimizationTest(optimizationTestNodeId, goalId, containerId, variants, ajax, variantsTraffic) {
-        // check persona panel forced variant
-        var selectedVariantId = wem.getUrlParameter('wemSelectedVariantId-' + optimizationTestNodeId); // check already resolved variant stored in local
-
-        if (selectedVariantId === null) {
-          if (wem.storageAvailable('sessionStorage')) {
-            selectedVariantId = sessionStorage.getItem(optimizationTestNodeId);
-          } else {
-            selectedVariantId = wem.getCookie('selectedVariantId');
-
-            if (selectedVariantId != null && selectedVariantId === '') {
-              selectedVariantId = null;
-            }
-          }
-        } // select random variant and call unomi
-
-
-        if (!(selectedVariantId && variants[selectedVariantId])) {
-          var keys = Object.keys(variants);
-
-          if (variantsTraffic) {
-            var rand = 100 * Math.random() << 0;
-
-            for (var nodeIdentifier in variantsTraffic) {
-              if ((rand -= variantsTraffic[nodeIdentifier]) < 0 && selectedVariantId == null) {
-                selectedVariantId = nodeIdentifier;
-              }
-            }
-          } else {
-            selectedVariantId = keys[keys.length * Math.random() << 0];
-          }
-
-          if (wem.storageAvailable('sessionStorage')) {
-            sessionStorage.setItem(optimizationTestNodeId, selectedVariantId);
-          } else {
-            wem.setCookie('selectedVariantId', selectedVariantId, 1);
-          } // spread event to unomi
-
-
-          wem._registerEvent(wem._completeEvent(variants[selectedVariantId].event));
-        } //Trigger variant display event for optimization
-        // (Wrapped in DOMContentLoaded because opti are resulted synchronously at page load, so we dispatch the JS even after page load, to be sure that listeners are ready)
-
-
-        window.addEventListener('DOMContentLoaded', function () {
-          wem._dispatchJSExperienceDisplayedEvent(variants[selectedVariantId].event);
-        });
-
-        if (selectedVariantId) {
-          // update persona panel selected variant
-          if (window.optimizedContentAreas && window.optimizedContentAreas[optimizationTestNodeId]) {
-            window.optimizedContentAreas[optimizationTestNodeId].selectedVariant = selectedVariantId;
-          } // display the good variant
-
-
-          document.getElementById(variants[selectedVariantId].content).style.display = '';
-        }
-      },
-
-      /**
-       * This function is used to load the current context in the page
-       *
-       * @param {boolean} [skipEvents=false] Should we send the events
-       * @param {boolean} [invalidate=false] Should we invalidate the current context
-       * @param {boolean} [forceReload=false] This function contains an internal check to avoid loading of the context multiple times.
-       *                                      But in some rare cases, it could be useful to force the reloading of the context and bypass the check.
-       * @return {undefined}
-       */
-      loadContext: function loadContext() {
-        var skipEvents = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
-        var invalidate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
-        var forceReload = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
-
-        if (wem.contextLoaded && !forceReload) {
-          console.log('Context already requested by', wem.contextLoaded);
-          return;
-        }
-
-        var jsonData = {
-          requiredProfileProperties: wem.digitalData.wemInitConfig.requiredProfileProperties,
-          requiredSessionProperties: wem.digitalData.wemInitConfig.requiredSessionProperties,
-          requireSegments: wem.digitalData.wemInitConfig.requireSegments,
-          requireScores: wem.digitalData.wemInitConfig.requireScores,
-          source: wem.buildSourcePage()
-        };
-
-        if (!skipEvents) {
-          jsonData.events = wem.digitalData.events;
-        }
-
-        if (wem.digitalData.personalizationCallback) {
-          jsonData.personalizations = wem.digitalData.personalizationCallback.map(function (x) {
-            return x.personalization;
-          });
-        }
-
-        jsonData.sessionId = wem.sessionID;
-        var contextUrl = wem.contextServerUrl + '/context.json';
-
-        if (invalidate) {
-          contextUrl += '?invalidateSession=true&invalidateProfile=true';
-        }
-
-        wem.ajax({
-          url: contextUrl,
-          type: 'POST',
-          async: true,
-          contentType: 'text/plain;charset=UTF-8',
-          // Use text/plain to avoid CORS preflight
-          jsonData: jsonData,
-          dataType: 'application/json',
-          invalidate: invalidate,
-          success: wem._onSuccess,
-          error: function error() {
-            wem._executeFallback('error during context loading');
-          }
-        });
-        wem.contextLoaded = Error().stack;
-        console.info('[WEM] context loading...');
-      },
-
-      /**
-       * This function will send an event to Apache Unomi
-       * @param {object} event The event object to send, you can build it using wem.buildEvent(eventType, target, source)
-       * @param {function} successCallback optional, will be executed in case of success
-       * @param {function} errorCallback optional, will be executed in case of error
-       * @return {undefined}
-       */
-      collectEvent: function collectEvent(event) {
-        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
-        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
-        wem.collectEvents({
-          events: [event]
-        }, successCallback, errorCallback);
-      },
-
-      /**
-       * This function will send the events to Apache Unomi
-       *
-       * @param {object} events Javascript object { events: [event1, event2] }
-       * @param {function} successCallback optional, will be executed in case of success
-       * @param {function} errorCallback optional, will be executed in case of error
-       * @return {undefined}
-       */
-      collectEvents: function collectEvents(events) {
-        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
-        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
-
-        if (wem.fallback) {
-          // in case of fallback we don't want to collect any events
-          return;
-        }
-
-        events.sessionId = wem.sessionID ? wem.sessionID : '';
-        var data = JSON.stringify(events);
-        wem.ajax({
-          url: wem.contextServerUrl + '/eventcollector',
-          type: 'POST',
-          async: true,
-          contentType: 'text/plain;charset=UTF-8',
-          // Use text/plain to avoid CORS preflight
-          data: data,
-          dataType: 'application/json',
-          success: successCallback,
-          error: errorCallback
-        });
-      },
-
-      /**
-       * This function will build an event of type click and send it to Apache Unomi
-       *
-       * @param {object} event javascript
-       * @param {function} [successCallback] optional, will be executed if case of success
-       * @param {function} [errorCallback] optional, will be executed if case of error
-       * @return {undefined}
-       */
-      sendClickEvent: function sendClickEvent(event) {
-        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
-        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
-
-        if (event.target.id || event.target.name) {
-          console.info('[WEM] Send click event');
-          var targetId = event.target.id ? event.target.id : event.target.name;
-          var clickEvent = wem.buildEvent('click', wem.buildTarget(targetId, event.target.localName), wem.buildSourcePage());
-          var eventIndex = wem.eventsPrevented.indexOf(targetId);
-
-          if (eventIndex !== -1) {
-            wem.eventsPrevented.splice(eventIndex, 0);
-          } else {
-            wem.eventsPrevented.push(targetId);
-            event.preventDefault();
-            var target = event.target;
-            wem.collectEvent(clickEvent, function (xhr) {
-              console.info('[WEM] Click event successfully collected.');
-
-              if (successCallback) {
-                successCallback(xhr);
-              } else {
-                target.click();
-              }
-            }, function (xhr) {
-              console.error('[WEM] Could not send click event.');
-
-              if (errorCallback) {
-                errorCallback(xhr);
-              } else {
-                target.click();
-              }
-            });
-          }
-        }
-      },
-
-      /**
-       * This function will build an event of type video and send it to Apache Unomi
-       *
-       * @param {object} event javascript
-       * @param {function} [successCallback] optional, will be executed if case of success
-       * @param {function} [errorCallback] optional, will be executed if case of error
-       * @return {undefined}
-       */
-      sendVideoEvent: function sendVideoEvent(event) {
-        var successCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
-        var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
-        console.info('[WEM] catching video event');
-        var videoEvent = wem.buildEvent('video', wem.buildTarget(event.target.id, 'video', {
-          action: event.type
-        }), wem.buildSourcePage());
-        wem.collectEvent(videoEvent, function (xhr) {
-          console.info('[WEM] Video event successfully collected.');
-
-          if (successCallback) {
-            successCallback(xhr);
-          }
-        }, function (xhr) {
-          console.error('[WEM] Could not send video event.');
-
-          if (errorCallback) {
-            errorCallback(xhr);
-          }
-        });
-      },
-
-      /**
-       * This function will invalidate the Apache Unomi session and profile,
-       * by removing the associated cookies, set the loaded context to undefined
-       * and set the session id cookie with a newly generated ID
-       * @return {undefined}
-       */
-      invalidateSessionAndProfile: function invalidateSessionAndProfile() {
-        wem.sessionID = wem.generateGuid() + wem.browserGeneratedSessionSuffix;
-        wem.setCookie(wem.trackerSessionIdCookieName, wem.sessionID, 1);
-        wem.removeCookie(wem.contextServerCookieName);
-        wem.removeCookie(wem.trackerProfileIdCookieName);
-        wem.cxs = undefined;
-      },
-
-      /**
-       * This function return the basic structure for an event, it must be adapted to your need
-       *
-       * @param {string} eventType The name of your event
-       * @param {object} [target] The target object for your event can be build with wem.buildTarget(targetId, targetType, targetProperties)
-       * @param {object} [source] The source object for your event can be build with wem.buildSource(sourceId, sourceType, sourceProperties)
-       * @returns {object} the event
-       */
-      buildEvent: function buildEvent(eventType, target, source) {
-        var event = {
-          eventType: eventType,
-          scope: wem.digitalData.scope
-        };
-
-        if (target) {
-          event.target = target;
-        }
-
-        if (source) {
-          event.source = source;
-        }
-
-        return event;
-      },
-
-      /**
-       * This function return an event of type form
-       *
-       * @param {string} formName The HTML name of id of the form to use in the target of the event
-       * @param {HTMLFormElement} form optional HTML form element, if provided will be used to extract the form fields and populate the form event
-       * @returns {object} the form event
-       */
-      buildFormEvent: function buildFormEvent(formName) {
-        var form = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
-        var formEvent = wem.buildEvent('form', wem.buildTarget(formName, 'form'), wem.buildSourcePage());
-        formEvent.flattenedProperties = {
-          fields: form ? wem._extractFormData(form) : {}
-        };
-        return formEvent;
-      },
-
-      /**
-       * This function return the source object for a source of type page
-       *
-       * @returns {object} the target page
-       */
-      buildTargetPage: function buildTargetPage() {
-        return wem.buildTarget(wem.digitalData.page.pageInfo.pageID, 'page', wem.digitalData.page);
-      },
-
-      /**
-       * This function return the source object for a source of type page
-       *
-       * @returns {object} the source page
-       */
-      buildSourcePage: function buildSourcePage() {
-        return wem.buildSource(wem.digitalData.page.pageInfo.pageID, 'page', wem.digitalData.page);
-      },
-
-      /**
-       * This function return the basic structure for the target of your event
-       *
-       * @param {string} targetId The ID of the target
-       * @param {string} targetType The type of the target
-       * @param {object} [targetProperties] The optional properties of the target
-       * @returns {object} the target
-       */
-      buildTarget: function buildTarget(targetId, targetType) {
-        var targetProperties = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
-        return wem._buildObject(targetId, targetType, targetProperties);
-      },
-
-      /**
-       * This function return the basic structure for the source of your event
-       *
-       * @param {string} sourceId The ID of the source
-       * @param {string} sourceType The type of the source
-       * @param {object} [sourceProperties] The optional properties of the source
-       * @returns {object} the source
-       */
-      buildSource: function buildSource(sourceId, sourceType) {
-        var sourceProperties = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
-        return wem._buildObject(sourceId, sourceType, sourceProperties);
-      },
-
-      /*************************************/
-
-      /* Utility functions under this line */
-
-      /*************************************/
-
-      /**
-       * This is an utility function to set a cookie
-       *
-       * @param {string} cookieName name of the cookie
-       * @param {string} cookieValue value of the cookie
-       * @param {number} [expireDays] number of days to set the expire date
-       * @return {undefined}
-       */
-      setCookie: function setCookie(cookieName, cookieValue, expireDays) {
-        var expires = '';
-
-        if (expireDays) {
-          var d = new Date();
-          d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000);
-          expires = '; expires=' + d.toUTCString();
-        }
-
-        document.cookie = cookieName + '=' + cookieValue + expires + '; path=/; SameSite=Strict';
-      },
-
-      /**
-       * This is an utility function to get a cookie
-       *
-       * @param {string} cookieName name of the cookie to get
-       * @returns {string} the value of the first cookie with the corresponding name or null if not found
-       */
-      getCookie: function getCookie(cookieName) {
-        var name = cookieName + '=';
-        var ca = document.cookie.split(';');
-
-        for (var i = 0; i < ca.length; i++) {
-          var c = ca[i];
-
-          while (c.charAt(0) == ' ') {
-            c = c.substring(1);
-          }
-
-          if (c.indexOf(name) == 0) {
-            return c.substring(name.length, c.length);
-          }
-        }
-
-        return null;
-      },
-
-      /**
-       * This is an utility function to remove a cookie
-       *
-       * @param {string} cookieName the name of the cookie to rename
-       * @return {undefined}
-       */
-      removeCookie: function removeCookie(cookieName) {
-        wem.setCookie(cookieName, '', -1);
-      },
-
-      /**
-       * This is an utility function to execute AJAX call
-       *
-       * @param {object} options options of the request
-       * @return {undefined}
-       */
-      ajax: function ajax(options) {
-        var xhr = new XMLHttpRequest();
-
-        if ('withCredentials' in xhr) {
-          xhr.open(options.type, options.url, options.async);
-          xhr.withCredentials = true;
-        } else if (typeof XDomainRequest != 'undefined') {
-          /* global XDomainRequest */
-          xhr = new XDomainRequest();
-          xhr.open(options.type, options.url);
-        }
-
-        if (options.contentType) {
-          xhr.setRequestHeader('Content-Type', options.contentType);
-        }
-
-        if (options.dataType) {
-          xhr.setRequestHeader('Accept', options.dataType);
-        }
-
-        if (options.responseType) {
-          xhr.responseType = options.responseType;
-        }
-
-        var requestExecuted = false;
-
-        if (wem.timeoutInMilliseconds !== -1) {
-          setTimeout(function () {
-            if (!requestExecuted) {
-              console.error('[WEM] XML request timeout, url: ' + options.url);
-              requestExecuted = true;
-
-              if (options.error) {
-                options.error(xhr);
-              }
-            }
-          }, wem.timeoutInMilliseconds);
-        }
-
-        xhr.onreadystatechange = function () {
-          if (!requestExecuted) {
-            if (xhr.readyState === 4) {
-              if (xhr.status === 200 || xhr.status === 204 || xhr.status === 304) {
-                if (xhr.responseText != null) {
-                  requestExecuted = true;
-
-                  if (options.success) {
-                    options.success(xhr);
-                  }
-                }
-              } else {
-                requestExecuted = true;
-
-                if (options.error) {
-                  options.error(xhr);
-                }
-
-                console.error('[WEM] XML request error: ' + xhr.statusText + ' (' + xhr.status + ')');
-              }
-            }
-          }
-        };
-
-        if (options.jsonData) {
-          xhr.send(JSON.stringify(options.jsonData));
-        } else if (options.data) {
-          xhr.send(options.data);
-        } else {
-          xhr.send();
-        }
-      },
-
-      /**
-       * This is an utility function to generate a new UUID
-       *
-       * @returns {string} the newly generated UUID
-       */
-      generateGuid: function generateGuid() {
-        function s4() {
-          return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
-        }
-
-        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
-      },
-
-      /**
-       * This is an utility function to check if the local storage is available or not
-       * @param {string} type the type of storage to test
-       * @returns {boolean} true in case storage is available, false otherwise
-       */
-      storageAvailable: function storageAvailable(type) {
-        try {
-          var storage = window[type],
-              x = '__storage_test__';
-          storage.setItem(x, x);
-          storage.removeItem(x);
-          return true;
-        } catch (e) {
-          return false;
-        }
-      },
-
-      /**
-       * Dispatch a JavaScript event in current HTML document
-       *
-       * @param {string} name the name of the event
-       * @param {boolean} canBubble does the event can bubble ?
-       * @param {boolean} cancelable is the event cancelable ?
-       * @param {*} detail event details
-       * @return {undefined}
-       */
-      dispatchJSEvent: function dispatchJSEvent(name, canBubble, cancelable, detail) {
-        var event = document.createEvent('CustomEvent');
-        event.initCustomEvent(name, canBubble, cancelable, detail);
-        document.dispatchEvent(event);
-      },
-
-      /**
-       * Fill the wem.digitalData.displayedVariants with the javascript event passed as parameter
-       * @param {object} jsEvent javascript event
-       * @private
-       * @return {undefined}
-       */
-      _fillDisplayedVariants: function _fillDisplayedVariants(jsEvent) {
-        if (!wem.digitalData.displayedVariants) {
-          wem.digitalData.displayedVariants = [];
-        }
-
-        wem.digitalData.displayedVariants.push(jsEvent);
-      },
-
-      /**
-       * This is an utility function to get current url parameter value
-       *
-       * @param {string} name, the name of the parameter
-       * @returns {string} the value of the parameter
-       */
-      getUrlParameter: function getUrlParameter(name) {
-        name = name.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
-        var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
-        var results = regex.exec(window.location.search);
-        return results === null ? null : decodeURIComponent(results[1].replace(/\+/g, ' '));
-      },
-
-      /**
-       * convert the passed query string into JS object.
-       * @param {string} searchString The URL query string
-       * @returns {object} converted URL params
-       */
-      convertUrlParametersToObj: function convertUrlParametersToObj(searchString) {
-        if (!searchString) {
-          return null;
-        }
-
-        return searchString.replace(/^\?/, '') // Only trim off a single leading interrobang.
-        .split('&').reduce(function (result, next) {
-          if (next === '') {
-            return result;
-          }
-
-          var pair = next.split('=');
-          var key = decodeURIComponent(pair[0]);
-          var value = typeof pair[1] !== 'undefined' && decodeURIComponent(pair[1]) || undefined;
-
-          if (Object.prototype.hasOwnProperty.call(result, key)) {
-            // Check to see if this property has been met before.
-            if (Array.isArray(result[key])) {
-              // Is it already an array?
-              result[key].push(value);
-            } else {
-              // Make it an array.
-              result[key] = [result[key], value];
-            }
-          } else {
-            // First time seen, just add it.
-            result[key] = value;
-          }
-
-          return result;
-        }, {});
-      },
-
-      /*************************************/
-
-      /* Private functions under this line */
-
-      /*************************************/
-
-      /**
-       * Used to override the default digitalData values,
-       * the result will impact directly the current instance wem.digitalData
-       *
-       * @param {object[]} digitalDataOverrides list of overrides
-       * @private
-       * @return {undefined}
-       */
-      _handleDigitalDataOverrides: function _handleDigitalDataOverrides(digitalDataOverrides) {
-        if (digitalDataOverrides && digitalDataOverrides.length > 0) {
-          var _iterator = _createForOfIteratorHelper(digitalDataOverrides),
-              _step;
-
-          try {
-            for (_iterator.s(); !(_step = _iterator.n()).done;) {
-              var digitalDataOverride = _step.value;
-              wem.digitalData = wem._deepMergeObjects(digitalDataOverride, wem.digitalData);
-            }
-          } catch (err) {
-            _iterator.e(err);
-          } finally {
-            _iterator.f();
-          }
-        }
-      },
-
-      /**
-       * Check for tracked conditions in the current loaded context, and attach listeners for the known tracked condition types:
-       * - formEventCondition
-       * - videoViewEventCondition
-       * - clickOnLinkEventCondition
-       *
-       * @private
-       * @return {undefined}
-       */
-      _registerListenersForTrackedConditions: function _registerListenersForTrackedConditions() {
-        console.info('[WEM] Check for tracked conditions and attach related HTML listeners');
-        var videoNamesToWatch = [];
-        var clickToWatch = [];
-
-        if (wem.cxs.trackedConditions && wem.cxs.trackedConditions.length > 0) {
-          for (var i = 0; i < wem.cxs.trackedConditions.length; i++) {
-            switch (wem.cxs.trackedConditions[i].type) {
-              case 'formEventCondition':
-                if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.formId) {
-                  wem.formNamesToWatch.push(wem.cxs.trackedConditions[i].parameterValues.formId);
-                }
-
-                break;
-
-              case 'videoViewEventCondition':
-                if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.videoId) {
-                  videoNamesToWatch.push(wem.cxs.trackedConditions[i].parameterValues.videoId);
-                }
-
-                break;
-
-              case 'clickOnLinkEventCondition':
-                if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.itemId) {
-                  clickToWatch.push(wem.cxs.trackedConditions[i].parameterValues.itemId);
-                }
-
-                break;
-            }
-          }
-        }
-
-        var forms = document.querySelectorAll('form');
-
-        for (var formIndex = 0; formIndex < forms.length; formIndex++) {
-          var form = forms.item(formIndex);
-          var formName = form.getAttribute('name') ? form.getAttribute('name') : form.getAttribute('id'); // test attribute data-form-id to not add a listener on FF form
-
-          if (formName && wem.formNamesToWatch.indexOf(formName) > -1 && form.getAttribute('data-form-id') == null) {
-            // add submit listener on form that we need to watch only
-            console.info('[WEM] Watching form ' + formName);
-            form.addEventListener('submit', wem._formSubmitEventListener, true);
-          }
-        }
-
-        for (var videoIndex = 0; videoIndex < videoNamesToWatch.length; videoIndex++) {
-          var videoName = videoNamesToWatch[videoIndex];
-          var video = document.getElementById(videoName) || document.getElementById(wem._resolveId(videoName));
-
-          if (video) {
-            video.addEventListener('play', wem.sendVideoEvent);
-            video.addEventListener('ended', wem.sendVideoEvent);
-            console.info('[WEM] Watching video ' + videoName);
-          } else {
-            console.warn('[WEM] Unable to watch video ' + videoName + ', video not found in the page');
-          }
-        }
-
-        for (var clickIndex = 0; clickIndex < clickToWatch.length; clickIndex++) {
-          var clickIdName = clickToWatch[clickIndex];
-          var click = document.getElementById(clickIdName) || document.getElementById(wem._resolveId(clickIdName)) ? document.getElementById(clickIdName) || document.getElementById(wem._resolveId(clickIdName)) : document.getElementsByName(clickIdName)[0];
-
-          if (click) {
-            click.addEventListener('click', wem.sendClickEvent);
-            console.info('[WEM] Watching click ' + clickIdName);
-          } else {
-            console.warn('[WEM] Unable to watch click ' + clickIdName + ', element not found in the page');
-          }
-        }
-      },
-
-      /**
-       * Check for currently registered events in wem.digitalData.events that would be incomplete:
-       * - autocomplete the event with the current digitalData page infos for the source
-       * @private
-       * @return {undefined}
-       */
-      _checkUncompleteRegisteredEvents: function _checkUncompleteRegisteredEvents() {
-        if (wem.digitalData && wem.digitalData.events) {
-          var _iterator2 = _createForOfIteratorHelper(wem.digitalData.events),
-              _step2;
-
-          try {
-            for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
-              var event = _step2.value;
-
-              wem._completeEvent(event);
-            }
-          } catch (err) {
-            _iterator2.e(err);
-          } finally {
-            _iterator2.f();
-          }
-        }
-      },
-
-      /**
-       * dispatch JavaScript event in current HTML document for perso and opti events contains in digitalData.events
-       * @private
-       * @return {undefined}
-       */
-      _dispatchJSExperienceDisplayedEvents: function _dispatchJSExperienceDisplayedEvents() {
-        if (wem.digitalData && wem.digitalData.events) {
-          var _iterator3 = _createForOfIteratorHelper(wem.digitalData.events),
-              _step3;
-
-          try {
-            for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
-              var event = _step3.value;
-
-              if (event.eventType === 'optimizationTestEvent' || event.eventType === 'personalizationEvent') {
-                wem._dispatchJSExperienceDisplayedEvent(event);
-              }
-            }
-          } catch (err) {
-            _iterator3.e(err);
-          } finally {
-            _iterator3.f();
-          }
-        }
-      },
-
-      /**
-       * build and dispatch JavaScript event in current HTML document for the given Unomi event (perso/opti)
-       * @private
-       * @param {object} experienceUnomiEvent perso/opti Unomi event
-       * @return {undefined}
-       */
-      _dispatchJSExperienceDisplayedEvent: function _dispatchJSExperienceDisplayedEvent(experienceUnomiEvent) {
-        if (!wem.fallback && experienceUnomiEvent && experienceUnomiEvent.target && experienceUnomiEvent.target.properties && experienceUnomiEvent.target.properties.variants && experienceUnomiEvent.target.properties.variants.length > 0) {
-          var typeMapper = {
-            optimizationTestEvent: 'optimization',
-            personalizationEvent: 'personalization'
-          };
-
-          var _iterator4 = _createForOfIteratorHelper(experienceUnomiEvent.target.properties.variants),
-              _step4;
-
-          try {
-            for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
-              var variant = _step4.value;
-              var jsEventDetail = {
-                id: variant.id,
-                name: variant.systemName,
-                displayableName: variant.displayableName,
-                path: variant.path,
-                type: typeMapper[experienceUnomiEvent.eventType],
-                variantType: experienceUnomiEvent.target.properties.type,
-                tags: variant.tags,
-                nodeType: variant.nodeType,
-                wrapper: {
-                  id: experienceUnomiEvent.target.itemId,
-                  name: experienceUnomiEvent.target.properties.systemName,
-                  displayableName: experienceUnomiEvent.target.properties.displayableName,
-                  path: experienceUnomiEvent.target.properties.path,
-                  tags: experienceUnomiEvent.target.properties.tags,
-                  nodeType: experienceUnomiEvent.target.properties.nodeType
-                }
-              };
-
-              if (experienceUnomiEvent.eventType === 'personalizationEvent') {
-                jsEventDetail.wrapper.inControlGroup = experienceUnomiEvent.target.properties.inControlGroup;
-              }
-
-              wem._fillDisplayedVariants(jsEventDetail);
-
-              wem.dispatchJSEvent('displayWemVariant', false, false, jsEventDetail);
-            }
-          } catch (err) {
-            _iterator4.e(err);
-          } finally {
-            _iterator4.f();
-          }
-        }
-      },
-
-      /**
-       * Filter events in digitalData.events that would have the property: event.properties.doNotSendToUnomi
-       * The effect is directly stored in a new version of wem.digitalData.events
-       * @private
-       * @return {undefined}
-       */
-      _filterUnomiEvents: function _filterUnomiEvents() {
-        if (wem.digitalData && wem.digitalData.events) {
-          wem.digitalData.events = wem.digitalData.events.filter(function (event) {
-            return !event.properties || !event.properties.doNotSendToUnomi;
-          }).map(function (event) {
-            if (event.properties) {
-              delete event.properties.doNotSendToUnomi;
-            }
-
-            return event;
-          });
-        }
-      },
-
-      /**
-       * Check if event is incomplete and complete what is missing:
-       * - source: if missing, use the current source page
-       * - scope: if missing, use the current scope
-       * @param {object} event, the event to be checked
-       * @private
-       * @return {object} the complete event
-       */
-      _completeEvent: function _completeEvent(event) {
-        if (!event.source) {
-          event.source = wem.buildSourcePage();
-        }
-
-        if (!event.scope) {
-          event.scope = wem.digitalData.scope;
-        }
-
-        if (event.target && !event.target.scope) {
-          event.target.scope = wem.digitalData.scope;
-        }
-
-        return event;
-      },
-
-      /**
-       * Register an event in the wem.digitalData.events.
-       * Registered event, will be sent automatically during the context loading process.
-       *
-       * Beware this function is useless in case the context is already loaded.
-       * in case the context is already loaded (check: wem.getLoadedContext()), then you should use: wem.collectEvent(s) functions
-       *
-       * @private
-       * @param {object} event the Unomi event to be registered
-       * @param {boolean} unshift optional, if true, the event will be added at the beginning of the list otherwise at the end of the list. (default: false)
-       * @return {undefined}
-       */
-      _registerEvent: function _registerEvent(event) {
-        var unshift = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
-
-        if (wem.digitalData) {
-          if (wem.cxs) {
-            console.error('[WEM] already loaded, too late...');
-            return;
-          }
-        } else {
-          wem.digitalData = {};
-        }
-
-        wem.digitalData.events = wem.digitalData.events || [];
-
-        if (unshift) {
-          wem.digitalData.events.unshift(event);
-        } else {
-          wem.digitalData.events.push(event);
-        }
-      },
-
-      /**
-       * This function allow for registering callback that will be executed once the context is loaded.
-       * @param {function} onLoadCallback the callback to be executed
-       * @param {string} name optional name for the call, used mostly for logging the execution
-       * @param {number} priority optional priority to execute the callbacks in a specific order
-       *                          (default: 5, to leave room for the tracker default callback(s))
-       * @private
-       * @return {undefined}
-       */
-      _registerCallback: function _registerCallback(onLoadCallback) {
-        var name = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
-        var priority = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 5;
-
-        if (wem.digitalData) {
-          if (wem.cxs) {
-            console.info('[WEM] Trying to register context load callback, but context already loaded, executing now...');
-
-            if (onLoadCallback) {
-              console.info('[WEM] executing context load callback: ' + (name ? name : 'Callback without name'));
-              onLoadCallback(wem.digitalData);
-            }
-          } else {
-            console.info('[WEM] registering context load callback: ' + (name ? name : 'Callback without name'));
-
-            if (onLoadCallback) {
-              wem.digitalData.loadCallbacks = wem.digitalData.loadCallbacks || [];
-              wem.digitalData.loadCallbacks.push({
-                priority: priority,
-                name: name,
-                execute: onLoadCallback
-              });
-            }
-          }
-        } else {
-          console.info('[WEM] Trying to register context load callback, but no digitalData conf found, creating it and registering the callback: ' + (name ? name : 'Callback without name'));
-          wem.digitalData = {};
-
-          if (onLoadCallback) {
-            wem.digitalData.loadCallbacks = [];
-            wem.digitalData.loadCallbacks.push({
-              priority: priority,
-              name: name,
-              execute: onLoadCallback
-            });
-          }
-        }
-      },
-
-      /**
-       * Internal function for personalization specific callbacks (used for HTML dom manipulation once we get the context loaded)
-       * @param {object} personalization the personalization
-       * @param {function} callback the callback
-       * @private
-       * @return {undefined}
-       */
-      _registerPersonalizationCallback: function _registerPersonalizationCallback(personalization, callback) {
-        if (wem.digitalData) {
-          if (wem.cxs) {
-            console.error('[WEM] already loaded, too late...');
-          } else {
-            console.info('[WEM] digitalData object present but not loaded, registering sort callback...');
-            wem.digitalData.personalizationCallback = wem.digitalData.personalizationCallback || [];
-            wem.digitalData.personalizationCallback.push({
-              personalization: personalization,
-              callback: callback
-            });
-          }
-        } else {
-          wem.digitalData = {};
-          wem.digitalData.personalizationCallback = wem.digitalData.personalizationCallback || [];
-          wem.digitalData.personalizationCallback.push({
-            personalization: personalization,
-            callback: callback
-          });
-        }
-      },
-
-      /**
-       * Build a simple Unomi object
-       * @param {string} itemId the itemId of the object
-       * @param {string} itemType the itemType of the object
-       * @param {object} properties optional properties for the object
-       * @private
-       * @return {object} the built Unomi JSON object
-       */
-      _buildObject: function _buildObject(itemId, itemType) {
-        var properties = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
-        var object = {
-          scope: wem.digitalData.scope,
-          itemId: itemId,
-          itemType: itemType
-        };
-
-        if (properties) {
-          object.properties = properties;
-        }
-
-        return object;
-      },
-
-      /**
-       * Main callback used once the Ajax context request succeed
-       * @param {XMLHttpRequest} xhr the request
-       * @private
-       * @return {undefined}
-       */
-      _onSuccess: function _onSuccess(xhr) {
-        wem.cxs = JSON.parse(xhr.responseText);
-
-        if (wem.digitalData.loadCallbacks && wem.digitalData.loadCallbacks.length > 0) {
-          console.info('[WEM] Found context server load callbacks, calling now...');
-
-          wem._executeLoadCallbacks(wem.digitalData);
-
-          if (wem.digitalData.personalizationCallback) {
-            for (var j = 0; j < wem.digitalData.personalizationCallback.length; j++) {
-              if (wem.cxs.personalizationResults) {
-                // Since Unomi 2.1.0 personalization results are available with more infos
-                var personalizationResult = wem.cxs.personalizationResults[wem.digitalData.personalizationCallback[j].personalization.id];
-                wem.digitalData.personalizationCallback[j].callback(personalizationResult.contentIds, personalizationResult.additionalResultInfos);
-              } else {
-                // probably a version older than Unomi 2.1.0, fallback to old personalization results
-                wem.digitalData.personalizationCallback[j].callback(wem.cxs.personalizations[wem.digitalData.personalizationCallback[j].personalization.id]);
-              }
-            }
-          }
-        }
-      },
-
-      /**
-       * Main callback used once the Ajax context request failed
-       * @param {string} logMessage the log message, to identify the place of failure
-       * @private
-       * @return {undefined}
-       */
-      _executeFallback: function _executeFallback(logMessage) {
-        console.warn('[WEM] execute fallback' + (logMessage ? ': ' + logMessage : '') + ', load fallback callbacks, calling now...');
-        wem.fallback = true;
-        wem.cxs = {};
-
-        wem._executeLoadCallbacks(undefined);
-
-        if (wem.digitalData.personalizationCallback) {
-          for (var i = 0; i < wem.digitalData.personalizationCallback.length; i++) {
-            wem.digitalData.personalizationCallback[i].callback([wem.digitalData.personalizationCallback[i].personalization.strategyOptions.fallback]);
-          }
-        }
-      },
-
-      /**
-       * Executed the registered context loaded callbacks
-       * @param {*} callbackParam param of the callbacks
-       * @private
-       * @return {undefined}
-       */
-      _executeLoadCallbacks: function _executeLoadCallbacks(callbackParam) {
-        if (wem.digitalData.loadCallbacks && wem.digitalData.loadCallbacks.length > 0) {
-          wem.digitalData.loadCallbacks.sort(function (a, b) {
-            return a.priority - b.priority;
-          }).forEach(function (loadCallback) {
-            console.info('[WEM] executing context load callback: ' + (loadCallback.name ? loadCallback.name : 'callback without name'));
-            loadCallback.execute(callbackParam);
-          });
-        }
-      },
-
-      /**
-       * Parse current HTML document referrer information to enrich the digitalData page infos
-       * @private
-       * @return {undefined}
-       */
-      _processReferrer: function _processReferrer() {
-        var referrerURL = wem.digitalData.page.pageInfo.referringURL || document.referrer;
-        var sameDomainReferrer = false;
-
-        if (referrerURL) {
-          // parse referrer URL
-          var referrer = new URL(referrerURL); // Set sameDomainReferrer property
-
-          sameDomainReferrer = referrer.host === window.location.host; // only process referrer if it's not coming from the same site as the current page
-
-          if (!sameDomainReferrer) {
-            // get search element if it exists and extract search query if available
-            var search = referrer.search;
-            var query = undefined;
-
-            if (search && search != '') {
-              // parse parameters
-              var queryParams = [],
-                  param;
-              var queryParamPairs = search.slice(1).split('&');
-
-              for (var i = 0; i < queryParamPairs.length; i++) {
-                param = queryParamPairs[i].split('=');
-                queryParams.push(param[0]);
-                queryParams[param[0]] = param[1];
-              } // try to extract query: q is Google-like (most search engines), p is Yahoo
-
-
-              query = queryParams.q || queryParams.p;
-              query = decodeURIComponent(query).replace(/\+/g, ' ');
-            } // register referrer event
-            // Create deep copy of wem.digitalData.page and add data to pageInfo sub object
-
-
-            if (wem.digitalData && wem.digitalData.page && wem.digitalData.page.pageInfo) {
-              wem.digitalData.page.pageInfo.referrerHost = referrer.host;
-              wem.digitalData.page.pageInfo.referrerQuery = query;
-            }
-          }
-        }
-
-        wem.digitalData.page.pageInfo.sameDomainReferrer = sameDomainReferrer;
-      },
-
-      /**
-       * Listener callback that can be attached to a specific HTML form,
-       * this listener will automatically send the form event to Unomi, by parsing the HTML form data.
-       * (NOTE: the form listener only work for know forms to be watch due to tracked conditions)
-       *
-       * @param {object} event the original HTML form submition event for the watch form.
-       * @private
-       * @return {undefined}
-       */
-      _formSubmitEventListener: function _formSubmitEventListener(event) {
-        console.info('[WEM] Registering form event callback');
-        var form = event.target;
-        var formName = form.getAttribute('name') ? form.getAttribute('name') : form.getAttribute('id');
-
-        if (formName && wem.formNamesToWatch.indexOf(formName) > -1) {
-          console.info('[WEM] catching form ' + formName);
-          var eventCopy = document.createEvent('Event'); // Define that the event name is 'build'.
-
-          eventCopy.initEvent('submit', event.bubbles, event.cancelable);
-          event.stopImmediatePropagation();
-          event.preventDefault();
-          wem.collectEvent(wem.buildFormEvent(formName, form), function () {
-            form.removeEventListener('submit', wem._formSubmitEventListener, true);
-            form.dispatchEvent(eventCopy);
-
-            if (!eventCopy.defaultPrevented && !eventCopy.cancelBubble) {
-              form.submit();
-            }
-
-            form.addEventListener('submit', wem._formSubmitEventListener, true);
-          }, function (xhr) {
-            console.error('[WEM] Error while collecting form event: ' + xhr.status + ' ' + xhr.statusText);
-            xhr.abort();
-            form.removeEventListener('submit', wem._formSubmitEventListener, true);
-            form.dispatchEvent(eventCopy);
-
-            if (!eventCopy.defaultPrevented && !eventCopy.cancelBubble) {
-              form.submit();
-            }
-
-            form.addEventListener('submit', wem._formSubmitEventListener, true);
-          });
-        }
-      },
-
-      /**
-       * Utility function to extract data from an HTML form.
-       *
-       * @param {HTMLFormElement} form the HTML form element
-       * @private
-       * @return {object} the form data as JSON
-       */
-      _extractFormData: function _extractFormData(form) {
-        var params = {};
-
-        for (var i = 0; i < form.elements.length; i++) {
-          var e = form.elements[i]; // ignore empty and undefined key (e.name)
-
-          if (e.name) {
-            switch (e.nodeName) {
-              case 'TEXTAREA':
-              case 'INPUT':
-                switch (e.type) {
-                  case 'checkbox':
-                    var checkboxes = document.querySelectorAll('input[name="' + e.name + '"]');
-
-                    if (checkboxes.length > 1) {
-                      if (!params[e.name]) {
-                        params[e.name] = [];
-                      }
-
-                      if (e.checked) {
-                        params[e.name].push(e.value);
-                      }
-                    }
-
-                    break;
-
-                  case 'radio':
-                    if (e.checked) {
-                      params[e.name] = e.value;
-                    }
-
-                    break;
-
-                  default:
-                    if (!e.value || e.value == '') {
-                      // ignore element if no value is provided
-                      break;
-                    }
-
-                    params[e.name] = e.value;
-                }
-
-                break;
-
-              case 'SELECT':
-                if (e.options && e.options[e.selectedIndex]) {
-                  if (e.multiple) {
-                    params[e.name] = [];
-
-                    for (var j = 0; j < e.options.length; j++) {
-                      if (e.options[j].selected) {
-                        params[e.name].push(e.options[j].value);
-                      }
-                    }
-                  } else {
-                    params[e.name] = e.options[e.selectedIndex].value;
-                  }
-                }
-
-                break;
-            }
-          }
-        }
-
-        return params;
-      },
-
-      /**
-       * Internal function used for mapping ids when current HTML document ids doesn't match ids stored in Unomi backend
-       * @param {string} id the id to resolve
-       * @return {string} the resolved id or the original id if not match found
-       * @private
-       */
-      _resolveId: function _resolveId(id) {
-        if (wem.digitalData.sourceLocalIdentifierMap) {
-          var source = Object.keys(wem.digitalData.sourceLocalIdentifierMap).filter(function (source) {
-            return id.indexOf(source) > 0;
-          });
-          return source ? id.replace(source, wem.digitalData.sourceLocalIdentifierMap[source]) : id;
-        }
-
-        return id;
-      },
-
-      /**
-       * Enable or disable tracking in current page
-       * @param {boolean} enable true will enable the tracking feature, otherwise they will be disabled
-       * @param {function} callback an optional callback that can be used to perform additional logic based on enabling/disabling results
-       * @private
-       * @return {undefined}
-       */
-      _enableWem: function _enableWem(enable) {
-        var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; // display fallback if wem is not enable
-
-        wem.fallback = !enable; // remove cookies, reset cxs
-
-        if (!enable) {
-          wem.cxs = {};
-          document.cookie = wem.trackerProfileIdCookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
-          document.cookie = wem.contextServerCookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
-          delete wem.contextLoaded;
-        } else {
-          if (wem.DOMLoaded) {
-            wem.loadContext();
-          } else {
-            // As Dom loaded listener not triggered, enable global value.
-            wem.activateWem = true;
-          }
-        }
-
-        if (callback) {
-          callback(enable);
-        }
-
-        console.log("[WEM] successfully ".concat(enable ? 'enabled' : 'disabled', " tracking in current page"));
-      },
-
-      /**
-       * Utility function used to merge two JSON object together (arrays are concat for example)
-       * @param {object} source the source object for merge
-       * @param {object} target the target object for merge
-       * @private
-       * @return {object} the merged results
-       */
-      _deepMergeObjects: function _deepMergeObjects(source, target) {
-        if (!wem._isObject(target) || !wem._isObject(source)) {
-          return source;
-        }
-
-        Object.keys(source).forEach(function (key) {
-          var targetValue = target[key];
-          var sourceValue = source[key]; // concat arrays || merge objects || add new props
-
-          if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
-            target[key] = targetValue.concat(sourceValue);
-          } else if (wem._isObject(targetValue) && wem._isObject(sourceValue)) {
-            target[key] = wem._deepMergeObjects(sourceValue, Object.assign({}, targetValue));
-          } else {
-            target[key] = sourceValue;
-          }
-        });
-        return target;
-      },
-
-      /**
-       * Utility function used to check if the given variable is a JavaScript object.
-       * @param {*} obj the variable to check
-       * @private
-       * @return {boolean} true if the variable is an object, false otherwise
-       */
-      _isObject: function _isObject(obj) {
-        return obj && _typeof(obj) === 'object';
-      }
-    };
-    return wem;
-  };
-  /*
-   * Licensed to the Apache Software Foundation (ASF) under one or more
-   * contributor license agreements.  See the NOTICE file distributed with
-   * this work for additional information regarding copyright ownership.
-   * The ASF licenses this file to You under the Apache License, Version 2.0
-   * (the "License"); you may not use this file except in compliance with
-   * the License.  You may obtain a copy of the License at
-   *
-   *      http://www.apache.org/licenses/LICENSE-2.0
-   *
-   * Unless required by applicable law or agreed to in writing, software
-   * distributed under the License is distributed on an "AS IS" BASIS,
-   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   * See the License for the specific language governing permissions and
-   * limitations under the License.
-   */
-
-
-  var useTracker = function useTracker() {
-    return newTracker();
-  };
-
-  /*
-   * Licensed to the Apache Software Foundation (ASF) under one or more
-   * contributor license agreements.  See the NOTICE file distributed with
-   * this work for additional information regarding copyright ownership.
-   * The ASF licenses this file to You under the Apache License, Version 2.0
-   * (the "License"); you may not use this file except in compliance with
-   * the License.  You may obtain a copy of the License at
-   *
-   *      http://www.apache.org/licenses/LICENSE-2.0
-   *
-   * Unless required by applicable law or agreed to in writing, software
-   * distributed under the License is distributed on an "AS IS" BASIS,
-   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   * See the License for the specific language governing permissions and
-   * limitations under the License.
-   */
-  // we will store an instance in the current browser window to make it accessible
-
-  window.unomiWebTracker = useTracker();
-
-})();
-//# sourceMappingURL=unomi-web-tracker.js.map
diff --git a/extensions/web-tracker/wab/dist/unomi-web-tracker.js.map b/extensions/web-tracker/wab/dist/unomi-web-tracker.js.map
deleted file mode 100644
index d91fdebe4..000000000
--- a/extensions/web-tracker/wab/dist/unomi-web-tracker.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"unomi-web-tracker.js","sources":["../node_modules/@babel/runtime/helpers/esm/typeof.js","../node_modules/es6-crawler-detect/src/lib/crawler/provider.js","../node_modules/es6-crawler-detect/src/lib/crawler/crawlers.js","../node_modules/es6-crawler-detect/src/lib/crawler/exclusions.js","../node_modules/es6-crawler-detect/src/lib/crawler/headers.js","../node_modules/es6-crawler-detect/src/lib/crawler.js","../node_modules/es6-crawler-detect/src/index.js","../node_modules [...]
\ No newline at end of file
diff --git a/extensions/web-tracker/wab/dist/unomi-web-tracker.min.js b/extensions/web-tracker/wab/dist/unomi-web-tracker.min.js
deleted file mode 100644
index 5e1925a54..000000000
--- a/extensions/web-tracker/wab/dist/unomi-web-tracker.min.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}var t="undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},r=[],n=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,i=!1;function a(){i=!0;for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij [...]