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:37 UTC

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

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"));