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

akshayrai09 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 31d916f  [TE] Adding Entity GroupKey Anomalies Email Reporter (#4433)
31d916f is described below

commit 31d916ff6563d7cac38eac0d0b9d824825bfacf0
Author: Akshay Rai <[email protected]>
AuthorDate: Wed Jul 17 08:30:47 2019 -0700

    [TE] Adding Entity GroupKey Anomalies Email Reporter (#4433)
    
    Includes a new template to send out alert emails for entity level anomalies 
with groupKey.
---
 .../alert/content/BaseEmailContentFormatter.java   |  87 ++++++++----
 .../content/EntityGroupByContentFormatter.java     | 147 +++++++++++++++++++++
 ...HierarchicalAnomaliesEmailContentFormatter.java |   1 +
 .../MetricAnomaliesEmailContentFormatter.java      |   2 +
 ...nboardingNotificationEmailContentFormatter.java |   1 +
 .../dashboard/resources/v2/AnomaliesResource.java  |  24 ++--
 .../thirdeye/detection/DefaultDataProvider.java    |   2 +-
 .../alert/scheme/DetectionEmailAlerter.java        |   4 +-
 .../wrapper/ChildKeepingMergeWrapper.java          |  16 ++-
 .../thirdeye/detection/wrapper/GrouperWrapper.java |   2 +-
 .../detector/entity-groupby-anomaly-report.ftl     |  97 ++++++++++++++
 .../alert/content/ContentFormatterUtils.java       |  48 +++++++
 ...=> TestEntityGroupByEmailContentFormatter.java} | 136 ++++++++++---------
 ...HierarchicalAnomaliesEmailContentFormatter.java |  20 +--
 .../TestMetricAnomaliesEmailContentFormatter.java  |  20 +--
 ...TestOnboardingNotificationContentFormatter.java |  20 +--
 .../pinot/thirdeye/datalayer/DaoTestUtils.java     |  13 ++
 .../thirdeye/detection/yaml/YamlResourceTest.java  |   2 +-
 .../tools/RunAdhocDatabaseQueriesTool.java         |  33 ++++-
 .../detection/yaml/alertconfig/alert-config-5.yaml |   2 +-
 ...est-entity-groupby-email-content-formatter.html | 105 +++++++++++++++
 21 files changed, 619 insertions(+), 163 deletions(-)

diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
index 79600b7..dd06e34 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
@@ -190,42 +190,46 @@ public abstract class BaseEmailContentFormatter 
implements EmailContentFormatter
   protected abstract void updateTemplateDataByAnomalyResults(Map<String, 
Object> templateData,
       Collection<AnomalyResult> anomalies, EmailContentFormatterContext 
context);
 
-  /**
-   * Add the auxiliary email information into parameter map
-   * @param alertConfigDTO
-   * @param groupId
-   * @param groupName
-   * @param anomalies
-   * @return
-   */
-  protected Map<String, Object> getTemplateData(AlertConfigDTO alertConfigDTO, 
Long groupId, String groupName,
-      Collection<AnomalyResult> anomalies) {
-    Map<String, Object> templateData = new HashMap<>();
-
-    DateTimeZone timeZone = 
DateTimeZone.forTimeZone(AlertTaskRunnerV2.DEFAULT_TIME_ZONE);
-
+  protected void enrichMetricInfo(Map<String, Object> templateData, 
Collection<AnomalyResult> anomalies) {
     Set<String> metrics = new TreeSet<>();
     Set<String> datasets = new TreeSet<>();
-    List<MergedAnomalyResultDTO> mergedAnomalyResults = new ArrayList<>();
 
     Map<String, MetricConfigDTO> metricsMap = new TreeMap<>();
-
-    // Calculate start and end time of the anomalies
-    DateTime startTime = DateTime.now();
-    DateTime endTime = new DateTime(0l);
     for (AnomalyResult anomalyResult : anomalies) {
       if (anomalyResult instanceof MergedAnomalyResultDTO) {
         MergedAnomalyResultDTO mergedAnomaly = (MergedAnomalyResultDTO) 
anomalyResult;
-        mergedAnomalyResults.add(mergedAnomaly);
         datasets.add(mergedAnomaly.getCollection());
         metrics.add(mergedAnomaly.getMetric());
 
         MetricConfigDTO metric = 
this.metricDAO.findByMetricAndDataset(mergedAnomaly.getMetric(), 
mergedAnomaly.getCollection());
         if (metric != null) {
-          // NOTE: our stale freemarker version doesn't play nice with 
non-string keys
           metricsMap.put(metric.getId().toString(), metric);
         }
       }
+    }
+
+    templateData.put("datasetsCount", datasets.size());
+    templateData.put("datasets", StringUtils.join(datasets, ", "));
+    templateData.put("metricsCount", metrics.size());
+    templateData.put("metrics", StringUtils.join(metrics, ", "));
+    templateData.put("metricsMap", metricsMap);
+  }
+
+  protected Map<String, Object> getTemplateData(AlertConfigDTO alertConfigDTO, 
Long groupId, String groupName,
+      Collection<AnomalyResult> anomalies) {
+    Map<String, Object> templateData = new HashMap<>();
+
+    DateTimeZone timeZone = 
DateTimeZone.forTimeZone(TimeZone.getTimeZone(DEFAULT_TIME_ZONE));
+    List<MergedAnomalyResultDTO> mergedAnomalyResults = new ArrayList<>();
+
+    // Calculate start and end time of the anomalies
+    DateTime startTime = DateTime.now();
+    DateTime endTime = new DateTime(0l);
+    for (AnomalyResult anomalyResult : anomalies) {
+      if (anomalyResult instanceof MergedAnomalyResultDTO) {
+        MergedAnomalyResultDTO mergedAnomaly = (MergedAnomalyResultDTO) 
anomalyResult;
+        mergedAnomalyResults.add(mergedAnomaly);
+      }
       if (anomalyResult.getStartTime() < startTime.getMillis()) {
         startTime = new DateTime(anomalyResult.getStartTime(), dateTimeZone);
       }
@@ -236,11 +240,6 @@ public abstract class BaseEmailContentFormatter implements 
EmailContentFormatter
 
     PrecisionRecallEvaluator precisionRecallEvaluator = new 
PrecisionRecallEvaluator(new DummyAlertFilter(), mergedAnomalyResults);
 
-    templateData.put("datasetsCount", datasets.size());
-    templateData.put("datasets", StringUtils.join(datasets, ", "));
-    templateData.put("metricsCount", metrics.size());
-    templateData.put("metrics", StringUtils.join(metrics, ", "));
-    templateData.put("metricsMap", metricsMap);
     templateData.put("anomalyCount", anomalies.size());
     templateData.put("startTime", getDateString(startTime));
     templateData.put("endTime", getDateString(endTime));
@@ -592,6 +591,10 @@ public abstract class BaseEmailContentFormatter implements 
EmailContentFormatter
     String endTime;
     String timezone;
     String issueType;
+    String score;
+    Double weight;
+    String groupKey;
+    String entityName;
 
     private static String RAW_VALUE_FORMAT = "%.0f";
     private static String PERCENTAGE_FORMAT = "%.2f %%";
@@ -793,6 +796,38 @@ public abstract class BaseEmailContentFormatter implements 
EmailContentFormatter
       this.issueType = issueType;
     }
 
+    public void setWeight(Double weight) {
+      this.weight = weight;
+    }
+
+    public double getWeight() {
+      return weight;
+    }
+
+    public String getScore() {
+      return score;
+    }
+
+    public void setScore(String score) {
+      this.score = score;
+    }
+
+    public String getEntityName() {
+      return entityName;
+    }
+
+    public void setEntityName(String entityName) {
+      this.entityName = entityName;
+    }
+
+    public String getGroupKey() {
+      return groupKey;
+    }
+
+    public void setGroupKey(String groupKey) {
+      this.groupKey = groupKey;
+    }
+
     public boolean isPositiveWoWLift() {
       return positiveWoWLift;
     }
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/EntityGroupByContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/EntityGroupByContentFormatter.java
new file mode 100644
index 0000000..a20ed2a
--- /dev/null
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/EntityGroupByContentFormatter.java
@@ -0,0 +1,147 @@
+/*
+ * 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.pinot.thirdeye.alert.content;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.pinot.thirdeye.anomalydetection.context.AnomalyResult;
+import org.apache.pinot.thirdeye.datalayer.bao.DetectionConfigManager;
+import org.apache.pinot.thirdeye.datalayer.dto.DetectionConfigDTO;
+import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
+import org.apache.pinot.thirdeye.datasource.DAORegistry;
+import org.apache.pinot.thirdeye.util.ThirdEyeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * This email formatter generates a report/alert from the anomalies having a 
groupKey
+ */
+public class EntityGroupByContentFormatter extends BaseEmailContentFormatter{
+  private static final Logger LOG = 
LoggerFactory.getLogger(EntityGroupByContentFormatter.class);
+
+  private static final String EMAIL_TEMPLATE = "emailTemplate";
+  private static final String DEFAULT_EMAIL_TEMPLATE = 
"entity-groupby-anomaly-report.ftl";
+
+  static final String PROP_ENTITY_NAME = "entityName";
+  static final String PROP_ANOMALY_SCORE = "groupScore";
+  static final String PROP_GROUP_KEY = "groupKey";
+
+  private DetectionConfigManager configDAO = null;
+  private Map<String, Double> entityAnomalyToScoreMap = new HashMap<>();
+  private Multimap<String, AnomalyReportEntity> entityAnomalyReports = 
ArrayListMultimap.create();
+  private Map<String, String> entityToAnomalyIdsMap = new HashMap<>();
+
+  public EntityGroupByContentFormatter() {}
+
+  @Override
+  public void init(Properties properties, EmailContentFormatterConfiguration 
configuration) {
+    super.init(properties, configuration);
+    this.emailTemplate = properties.getProperty(EMAIL_TEMPLATE, 
DEFAULT_EMAIL_TEMPLATE);
+    this.configDAO = DAORegistry.getInstance().getDetectionConfigManager();
+  }
+
+  @Override
+  protected void updateTemplateDataByAnomalyResults(Map<String, Object> 
templateData,
+      Collection<AnomalyResult> anomalies, EmailContentFormatterContext 
context) {
+    DetectionConfigDTO config = null;
+    Preconditions.checkArgument(anomalies != null && !anomalies.isEmpty(), 
"Report has empty anomalies");
+    for (AnomalyResult anomalyResult : anomalies) {
+      if (!(anomalyResult instanceof MergedAnomalyResultDTO)) {
+        LOG.warn("Anomaly result {} isn't an instance of 
MergedAnomalyResultDTO. Skip from alert.", anomalyResult);
+        continue;
+      }
+
+      MergedAnomalyResultDTO anomaly = (MergedAnomalyResultDTO) anomalyResult;
+      if (config == null) {
+        config = this.configDAO.findById(anomaly.getDetectionConfigId());
+        Preconditions.checkNotNull(config, String.format("Cannot find 
detection config %d", anomaly.getDetectionConfigId()));
+      }
+
+      updateEntityToAnomalyDetailsMap(anomaly, config);
+    }
+
+    List<Map.Entry<String, Double>> anomaliesSortedByScores = new 
ArrayList<>(entityAnomalyToScoreMap.entrySet());
+    anomaliesSortedByScores.sort(Map.Entry.comparingByValue());
+    List<String> entityList = 
anomaliesSortedByScores.stream().map(Map.Entry::getKey).collect(Collectors.toList());
+    entityList.sort(Collections.reverseOrder());
+
+    templateData.put("emailHeading", config.getName());
+    templateData.put("entityToAnomalyIdsMap", entityToAnomalyIdsMap);
+    templateData.put("entitySortedByScoreList", entityList);
+    templateData.put("entityToAnomalyDetailsMap", 
entityAnomalyReports.asMap());
+  }
+
+  /**
+   * Recursively find the anomalies having a groupKey and display them in the 
email
+   */
+  private void updateEntityToAnomalyDetailsMap(MergedAnomalyResultDTO anomaly, 
DetectionConfigDTO detectionConfig) {
+    if (anomaly.getProperties() != null && 
anomaly.getProperties().containsKey(PROP_GROUP_KEY)) {
+      AnomalyReportEntity anomalyReport = new 
AnomalyReportEntity(String.valueOf(anomaly.getId()),
+          getAnomalyURL(anomaly, 
emailContentFormatterConfiguration.getDashboardHost()),
+          ThirdEyeUtils.getRoundedValue(anomaly.getAvgBaselineVal()),
+          ThirdEyeUtils.getRoundedValue(anomaly.getAvgCurrentVal()), 0d, null,
+          getTimeDiffInHours(anomaly.getStartTime(), anomaly.getEndTime()), 
getFeedbackValue(anomaly.getFeedback()),
+          detectionConfig.getName(), detectionConfig.getDescription(), 
anomaly.getMetric(),
+          getDateString(anomaly.getStartTime(), dateTimeZone), 
getDateString(anomaly.getEndTime(), dateTimeZone),
+          getTimezoneString(dateTimeZone), getIssueType(anomaly));
+
+      // Criticality score ranges from [0 to infinity)
+      double score = -1;
+      if (anomaly.getProperties().containsKey(PROP_ANOMALY_SCORE)) {
+        score = 
Double.parseDouble(anomaly.getProperties().get(PROP_ANOMALY_SCORE));
+        anomalyReport.setScore(ThirdEyeUtils.getRoundedValue(score));
+      } else {
+        anomalyReport.setScore("-");
+      }
+      anomalyReport.setWeight(anomaly.getWeight());
+      anomalyReport.setGroupKey(anomaly.getProperties().get(PROP_GROUP_KEY));
+      
anomalyReport.setEntityName(anomaly.getProperties().getOrDefault(PROP_ENTITY_NAME,
 "UNKNOWN_ENTITY"));
+
+      Set<Long> childIds = new HashSet<>();
+      if (anomaly.getChildIds() != null) {
+        childIds.addAll(anomaly.getChildIds());
+      }
+
+      // include notified alerts only in the email
+      if (!includeSentAnomaliesOnly || anomaly.isNotified()) {
+        
entityToAnomalyIdsMap.put(anomaly.getProperties().get(PROP_ENTITY_NAME), 
Joiner.on(",").join(childIds));
+        
entityAnomalyToScoreMap.put(anomaly.getProperties().get(PROP_ENTITY_NAME), 
score);
+        
entityAnomalyReports.put(anomaly.getProperties().get(PROP_ENTITY_NAME), 
anomalyReport);
+      }
+    } else {
+      for (MergedAnomalyResultDTO childAnomaly : anomaly.getChildren()) {
+        updateEntityToAnomalyDetailsMap(childAnomaly, detectionConfig);
+      }
+    }
+  }
+}
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
index 5c69d06..fd79eb9 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
@@ -82,6 +82,7 @@ public class HierarchicalAnomaliesEmailContentFormatter 
extends BaseEmailContent
   @Override
   protected void updateTemplateDataByAnomalyResults(Map<String, Object> 
templateData,
       Collection<AnomalyResult> anomalies, EmailContentFormatterContext 
context) {
+    enrichMetricInfo(templateData, anomalies);
     List<AnomalyReportEntity> rootAnomalyDetails = new ArrayList<>();
     SortedMap<String, List<AnomalyReportEntity>> leafAnomalyDetails = new 
TreeMap<>();
     List<String> anomalyIds = new ArrayList<>();
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
index 385fe95..51d8610 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
@@ -71,6 +71,8 @@ public class MetricAnomaliesEmailContentFormatter extends 
BaseEmailContentFormat
   @Override
   protected void updateTemplateDataByAnomalyResults(Map<String, Object> 
templateData,
       Collection<AnomalyResult> anomalies, EmailContentFormatterContext 
context) {
+    enrichMetricInfo(templateData, anomalies);
+
     DateTime windowStart = DateTime.now();
     DateTime windowEnd = new DateTime(0);
 
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
index a2bcb30..1cc1a50 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
@@ -59,6 +59,7 @@ public class OnboardingNotificationEmailContentFormatter 
extends BaseEmailConten
   @Override
   protected void updateTemplateDataByAnomalyResults(Map<String, Object> 
templateData,
       Collection<AnomalyResult> anomalies, EmailContentFormatterContext 
context) {
+    enrichMetricInfo(templateData, anomalies);
     AnomalyFunctionDTO anomalyFunctionSpec = context.getAnomalyFunctionSpec();
     for (AnomalyResult anomalyResult : anomalies) {
       if (!(anomalyResult instanceof MergedAnomalyResultDTO)) {
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
index 6344d49..984d482 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
@@ -95,6 +95,7 @@ import 
org.apache.pinot.thirdeye.datalayer.dto.GroupedAnomalyResultsDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MetricConfigDTO;
 import org.apache.pinot.thirdeye.datalayer.pojo.AlertConfigBean;
+import org.apache.pinot.thirdeye.datalayer.pojo.MergedAnomalyResultBean;
 import org.apache.pinot.thirdeye.datasource.DAORegistry;
 import org.apache.pinot.thirdeye.datasource.ThirdEyeCacheRegistry;
 import org.apache.pinot.thirdeye.datasource.cache.QueryCache;
@@ -303,7 +304,7 @@ public class AnomaliesResource {
 
     List<MergedAnomalyResultDTO> mergedAnomalies = 
mergedAnomalyResultDAO.findByTime(startTime, endTime);
     AnomaliesWrapper anomaliesWrapper =
-        constructAnomaliesWrapperFromMergedAnomalies(mergedAnomalies, 
searchFiltersJSON, pageNumber, filterOnly);
+        
constructAnomaliesWrapperFromMergedAnomalies(removeChildren(mergedAnomalies), 
searchFiltersJSON, pageNumber, filterOnly);
     return anomaliesWrapper;
   }
 
@@ -312,7 +313,6 @@ public class AnomaliesResource {
    * @param startTime
    * @param endTime
    * @param anomalyIdsString
-   * @param functionName
    * @return
    * @throws Exception
    */
@@ -323,7 +323,6 @@ public class AnomaliesResource {
       @PathParam("endTime") Long endTime,
       @PathParam("pageNumber") int pageNumber,
       @QueryParam("anomalyIds") String anomalyIdsString,
-      @QueryParam("functionName") String functionName,
       @QueryParam("searchFilters") String searchFiltersJSON,
       @QueryParam("filterOnly") @DefaultValue("false") boolean filterOnly) 
throws Exception {
 
@@ -369,7 +368,7 @@ public class AnomaliesResource {
     }
     List<MergedAnomalyResultDTO> mergedAnomalies = 
getAnomaliesForMetricIdsInRange(metricIds, startTime, endTime);
     AnomaliesWrapper
-        anomaliesWrapper = 
constructAnomaliesWrapperFromMergedAnomalies(mergedAnomalies, 
searchFiltersJSON, pageNumber, filterOnly);
+        anomaliesWrapper = 
constructAnomaliesWrapperFromMergedAnomalies(removeChildren(mergedAnomalies), 
searchFiltersJSON, pageNumber, filterOnly);
     return anomaliesWrapper;
   }
 
@@ -419,7 +418,7 @@ public class AnomaliesResource {
     }
 
     AnomaliesWrapper
-        anomaliesWrapper = 
constructAnomaliesWrapperFromMergedAnomalies(mergedAnomalies, 
searchFiltersJSON, pageNumber, filterOnly);
+        anomaliesWrapper = 
constructAnomaliesWrapperFromMergedAnomalies(removeChildren(mergedAnomalies), 
searchFiltersJSON, pageNumber, filterOnly);
     return anomaliesWrapper;
   }
 
@@ -680,6 +679,13 @@ public class AnomaliesResource {
     return endDateTime.minus(periodToSubtract).getMillis();
   }
 
+  /**
+   * Removes child anomalies
+   */
+  private List<MergedAnomalyResultDTO> 
removeChildren(List<MergedAnomalyResultDTO> mergedAnomalies) {
+    mergedAnomalies.removeIf(MergedAnomalyResultBean::isChild);
+    return mergedAnomalies;
+  }
 
   /**
    * Constructs AnomaliesWrapper object from a list of merged anomalies
@@ -689,14 +695,6 @@ public class AnomaliesResource {
 
     AnomaliesWrapper anomaliesWrapper = new AnomaliesWrapper();
 
-    // remove child anomalies
-    Iterator<MergedAnomalyResultDTO> itAnomaly = mergedAnomalies.iterator();
-    while (itAnomaly.hasNext()) {
-      if (itAnomaly.next().isChild()) {
-        itAnomaly.remove();
-      }
-    }
-
     //filter the anomalies
     SearchFilters searchFilters = new SearchFilters();
     if (searchFiltersJSON != null) {
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
index 4d890bf..fedd27f 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
@@ -248,7 +248,7 @@ public class DefaultDataProvider implements DataProvider {
       // filter all child anomalies. those are kept in the parent anomaly 
children set.
       anomalies = Collections2.filter(anomalies, mergedAnomaly -> 
mergedAnomaly != null && !mergedAnomaly.isChild());
 
-      //LOG.info("Fetched {} anomalies between (startTime = {}, endTime = {}) 
with confid Id = {}", anomalies.size(), slice.getStart(), slice.getEnd(), 
configId);
+      LOG.info("Fetched {} anomalies between (startTime = {}, endTime = {}) 
with confid Id = {}", anomalies.size(), slice.getStart(), slice.getEnd(), 
configId);
       output.putAll(slice, anomalies);
     }
     return output;
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
index 1f963df..e059a4e 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
@@ -131,7 +131,7 @@ public class DetectionEmailAlerter extends 
DetectionAlertScheme {
 
   public enum EmailTemplate {
     DEFAULT_EMAIL,
-    ENTITY_REPORT
+    ENTITY_GROUPBY_REPORT
   }
 
   /**
@@ -148,7 +148,7 @@ public class DetectionEmailAlerter extends 
DetectionAlertScheme {
         LOG.info("Using the " + DEFAULT_EMAIL_FORMATTER_TYPE + " email 
template.");
         return 
EmailContentFormatterFactory.fromClassName(DEFAULT_EMAIL_FORMATTER_TYPE);
 
-      case ENTITY_REPORT:
+      case ENTITY_GROUPBY_REPORT:
         LOG.info("Using the " + template + " email template.");
         return 
EmailContentFormatterFactory.fromClassName(ENTITY_REPORT_FORMATTER_TYPE);
 
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
index f3fa13c..89438aa 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
@@ -26,6 +26,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.pinot.thirdeye.datalayer.dto.DetectionConfigDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
 import org.apache.pinot.thirdeye.detection.DataProvider;
@@ -39,6 +40,8 @@ import 
org.apache.pinot.thirdeye.detection.algorithm.MergeWrapper;
  * Merge anomalies regardless of anomaly merge key.
  */
 public class ChildKeepingMergeWrapper extends BaselineFillingMergeWrapper {
+  private static final String PROP_GROUP_KEY = "groupKey";
+
   public ChildKeepingMergeWrapper(DataProvider provider, DetectionConfigDTO 
config, long startTime, long endTime) {
     super(provider, config, startTime, endTime);
   }
@@ -69,8 +72,14 @@ public class ChildKeepingMergeWrapper extends 
BaselineFillingMergeWrapper {
         continue;
       }
 
+      // Prevent merging of grouped anomalies
+      String groupKey = "";
+      if (anomaly.getProperties().containsKey(PROP_GROUP_KEY)) {
+        groupKey = anomaly.getProperties().get(PROP_GROUP_KEY);
+      }
+
       MergeWrapper.AnomalyKey key =
-          new MergeWrapper.AnomalyKey(anomaly.getMetric(), 
anomaly.getCollection(), anomaly.getDimensions(), "", "");
+          new MergeWrapper.AnomalyKey(anomaly.getMetric(), 
anomaly.getCollection(), anomaly.getDimensions(), groupKey, "");
       MergedAnomalyResultDTO parent = parents.get(key);
 
       if (parent == null || anomaly.getStartTime() - parent.getEndTime() > 
this.maxGap) {
@@ -97,10 +106,11 @@ public class ChildKeepingMergeWrapper extends 
BaselineFillingMergeWrapper {
       }
     }
 
-    // refill current and baseline values for qualified parent anomalies
+    // Refill current and baseline values for qualified parent anomalies
+    // Ignore filling baselines for exiting parent anomalies and grouped 
anomalies
     Collection<MergedAnomalyResultDTO> parentAnomalies = 
Collections2.filter(output,
         mergedAnomaly -> mergedAnomaly != null && 
!mergedAnomaly.getChildren().isEmpty() && !isExistingAnomaly(
-            existingParentAnomalies, mergedAnomaly));
+            existingParentAnomalies, mergedAnomaly) && 
!StringUtils.isBlank(mergedAnomaly.getMetric()));
     super.fillCurrentAndBaselineValue(new ArrayList<>(parentAnomalies));
     return output;
   }
diff --git 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
index 7d38bba..cfffd14 100644
--- 
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
+++ 
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
@@ -67,9 +67,9 @@ public class GrouperWrapper extends DetectionPipeline {
     this.grouperName = 
DetectionUtils.getComponentKey(MapUtils.getString(config.getProperties(), 
PROP_GROUPER));
     
Preconditions.checkArgument(this.config.getComponents().containsKey(this.grouperName));
     this.grouper = (Grouper) this.config.getComponents().get(this.grouperName);
+
     
Preconditions.checkArgument(this.config.getProperties().containsKey(PROP_ENTITY_NAME));
     this.entityName = 
this.config.getProperties().get(PROP_ENTITY_NAME).toString();
-
   }
 
   /**
diff --git 
a/thirdeye/thirdeye-pinot/src/main/resources/org/apache/pinot/thirdeye/detector/entity-groupby-anomaly-report.ftl
 
b/thirdeye/thirdeye-pinot/src/main/resources/org/apache/pinot/thirdeye/detector/entity-groupby-anomaly-report.ftl
new file mode 100644
index 0000000..97a06ee
--- /dev/null
+++ 
b/thirdeye/thirdeye-pinot/src/main/resources/org/apache/pinot/thirdeye/detector/entity-groupby-anomaly-report.ftl
@@ -0,0 +1,97 @@
+<#import "lib/utils.ftl" as utils>
+
+<head>
+  <link href="https://fonts.googleapis.com/css?family=Open+Sans"; 
rel="stylesheet">
+</head>
+
+<body style="background-color: #EDF0F3;">
+<table border="0" cellpadding="0" cellspacing="0" style="width:100%; 
font-family: 'Proxima Nova','Arial','Helvetica Neue',Helvetica,sans-serif; 
font-size:14px; margin:0 auto; max-width: 50%; min-width: 700px; 
background-color: #F3F6F8;">
+  <!-- White bar on top -->
+  <tr>
+    <td align="center" style="padding: 6px 24px;">
+      <span style="color: red; font-size: 35px; vertical-align: 
middle;">&nbsp;&#9888;&nbsp;</span>
+      <span style="color: rgba(0,0,0,0.75); font-size: 18px; font-weight: 
bold; letter-spacing: 2px; vertical-align: middle;">ACTION REQUIRED ON THIRDEYE 
ALERT</span>
+    </td>
+  </tr>
+
+  <!-- Blue header on top -->
+  <tr>
+    <td style="font-size: 16px; padding: 12px; background-color: #0073B1; 
color: #FFF; text-align: center;">
+      <span style="font-size: 18px; font-weight: bold; letter-spacing: 2px; 
vertical-align: middle;">Report: ${emailHeading}</span>
+    </td>
+  </tr>
+
+  <tr>
+    <td>
+      <table border="0" cellpadding="0" cellspacing="0" style="border:1px 
solid #E9E9E9; border-radius: 2px; width: 100%;">
+
+        <!-- List all the alerts -->
+        <#list entitySortedByScoreList as entity>
+          <@utils.addBlock title="" align="left">
+            <!-- Display Entity Name -->
+            <p>
+              <span style="color: #1D1D1D; font-size: 20px; font-weight: bold; 
display:inline-block; vertical-align: middle;">Entity:&nbsp;</span>
+              <span style="color: #606060; font-size: 20px; text-decoration: 
none; display:inline-block; vertical-align: middle; width: 70%; white-space: 
nowrap; overflow: hidden; text-overflow: ellipsis;">${entity}</span>
+            </p>
+
+            <!-- List all the anomalies under this entity in a table -->
+            <table border="0" width="100%" align="center" style="width:100%; 
padding:0; margin:0; border-collapse: collapse;text-align:left;">
+              <tr style="text-align:center; background-color: #F6F8FA; 
border-top: 2px solid #C7D1D8; border-bottom: 2px solid #C7D1D8;">
+                <th style="text-align:left; padding: 6px 12px; font-size: 
12px; font-weight: bold; line-height: 20px;">Feature</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Criticality Score*</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Current</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Predicted</th>
+              </tr>
+
+              <#list entityToAnomalyDetailsMap[entity] as anomaly>
+                <tr style="border-bottom: 1px solid #C7D1D8;">
+                  <td style="padding: 6px 12px;white-space: nowrap;">
+                    <a style="font-weight: bold; text-decoration: none; 
font-size:14px; line-height:20px; color: #0073B1;" 
href="${dashboardHost}/app/#/anomalies?anomalyIds=${entityToAnomalyIdsMap[entity]}"
+                       target="_blank">${anomaly.groupKey}</a>
+                    <span style="color: rgba(0,0,0,0.6); font-size:12px; 
line-height:16px;">${anomaly.duration}</span>
+                  </td>
+                  <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">${anomaly.score}</td>
+                  <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">${anomaly.currentVal}</td>
+                  <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">
+                    ${anomaly.baselineVal}
+                    <div style="font-size: 12px; 
color:${anomaly.positiveLift?string('#3A8C18','#ee1620')};">(${anomaly.positiveLift?string('+','')}${anomaly.lift})</div>
+                  </td>
+                </tr>
+              </#list>
+            </table>
+
+          </@utils.addBlock>
+        </#list>
+
+        <!-- Reference Links -->
+        <#if referenceLinks?has_content>
+          <@utils.addBlock title="Useful Links" align="left">
+            <table border="0" align="center" style="table-layout: fixed; 
width:100%; padding:0; margin:0; border-collapse: collapse; text-align:left;">
+              <#list referenceLinks?keys as referenceLinkKey>
+                <tr style="border-bottom: 1px solid #C7D1D8; padding: 16px;">
+                  <td style="padding: 6px 12px;">
+                    <a href="${referenceLinks[referenceLinkKey]}" 
style="text-decoration: none; color:#0073B1; font-size:12px; 
font-weight:bold;">${referenceLinkKey}</a>
+                  </td>
+                </tr>
+              </#list>
+            </table>
+          </@utils.addBlock>
+        </#if>
+
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td style="text-align: center; background-color: #EDF0F3; font-size: 12px; 
font-family:'Proxima Nova','Arial', 'Helvetica Neue',Helvetica, sans-serif; 
color: #737373; padding: 12px;">
+      <p style="margin-top:0;"> You are receiving this email because you have 
subscribed to ThirdEye Alert Service for
+        <strong>${alertConfigName}</strong>.</p>
+      <p>
+        If you have any questions regarding this report, please email
+        <a style="color: #33aada;" href="mailto:[email protected]"; 
target="_top">[email protected]</a>
+      </p>
+    </td>
+  </tr>
+
+</table>
+</body>
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/ContentFormatterUtils.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/ContentFormatterUtils.java
new file mode 100644
index 0000000..47c2405
--- /dev/null
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/ContentFormatterUtils.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2014-2018 LinkedIn Corp. ([email protected])
+ *
+ * Licensed 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.pinot.thirdeye.alert.content;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import org.apache.commons.mail.HtmlEmail;
+import org.apache.pinot.thirdeye.alert.commons.EmailEntity;
+
+
+public class ContentFormatterUtils {
+
+  static String getHtmlContent(String htmlPath) throws IOException {
+    StringBuilder htmlContent;
+    try (BufferedReader br = new BufferedReader(new FileReader(htmlPath))) {
+      htmlContent = new StringBuilder();
+      for (String line = br.readLine(); line != null; line = br.readLine()) {
+        htmlContent.append(line).append("\n");
+      }
+    }
+
+    return htmlContent.toString();
+  }
+
+  static String getEmailHtml(EmailEntity emailEntity) throws Exception {
+    HtmlEmail email = emailEntity.getContent();
+    Field field = email.getClass().getDeclaredField("html");
+    field.setAccessible(true);
+
+    return field.get(email).toString();
+  }
+}
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestEntityGroupByEmailContentFormatter.java
similarity index 59%
copy from 
thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
copy to 
thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestEntityGroupByEmailContentFormatter.java
index bcc3808..8ce134d 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestEntityGroupByEmailContentFormatter.java
@@ -16,6 +16,15 @@
 
 package org.apache.pinot.thirdeye.alert.content;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import org.apache.pinot.thirdeye.alert.commons.EmailEntity;
 import org.apache.pinot.thirdeye.anomaly.ThirdEyeAnomalyConfiguration;
 import org.apache.pinot.thirdeye.anomaly.monitor.MonitorConfiguration;
@@ -24,26 +33,15 @@ import org.apache.pinot.thirdeye.anomaly.utils.EmailUtils;
 import org.apache.pinot.thirdeye.anomalydetection.context.AnomalyResult;
 import org.apache.pinot.thirdeye.common.time.TimeGranularity;
 import org.apache.pinot.thirdeye.datalayer.DaoTestUtils;
-import org.apache.pinot.thirdeye.datalayer.bao.AnomalyFunctionManager;
 import org.apache.pinot.thirdeye.datalayer.bao.DAOTestBase;
+import org.apache.pinot.thirdeye.datalayer.bao.DetectionConfigManager;
 import org.apache.pinot.thirdeye.datalayer.bao.MergedAnomalyResultManager;
 import org.apache.pinot.thirdeye.datalayer.bao.MetricConfigManager;
-import org.apache.pinot.thirdeye.datalayer.dto.AlertConfigDTO;
-import org.apache.pinot.thirdeye.datalayer.dto.AnomalyFunctionDTO;
+import org.apache.pinot.thirdeye.datalayer.dto.DetectionConfigDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MetricConfigDTO;
 import org.apache.pinot.thirdeye.datasource.DAORegistry;
 import 
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.testng.Assert;
@@ -51,15 +49,16 @@ import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import static 
org.apache.pinot.thirdeye.alert.content.EntityGroupByContentFormatter.*;
 import static org.apache.pinot.thirdeye.anomaly.SmtpConfiguration.*;
 
 
-public class TestMetricAnomaliesEmailContentFormatter {
+public class TestEntityGroupByEmailContentFormatter {
   private static final String TEST = "test";
   private int id = 0;
   private String dashboardHost = "http://localhost:8080/dashboard";;
   private DAOTestBase testDAOProvider;
-  private AnomalyFunctionManager anomalyFunctionDAO;
+  private DetectionConfigManager detectionDAO;
   private MergedAnomalyResultManager mergedAnomalyResultDAO;
   private MetricConfigManager metricDAO;
 
@@ -67,7 +66,7 @@ public class TestMetricAnomaliesEmailContentFormatter {
   public void beforeClass(){
     testDAOProvider = DAOTestBase.getInstance();
     DAORegistry daoRegistry = DAORegistry.getInstance();
-    anomalyFunctionDAO = daoRegistry.getAnomalyFunctionDAO();
+    detectionDAO = daoRegistry.getDetectionConfigManager();
     mergedAnomalyResultDAO = daoRegistry.getMergedAnomalyResultDAO();
     metricDAO = daoRegistry.getMetricConfigDAO();
   }
@@ -77,9 +76,18 @@ public class TestMetricAnomaliesEmailContentFormatter {
     testDAOProvider.cleanup();
   }
 
+  /**
+   * Entity Alert Trigger Condition: Sub-Entity-A OR Sub-Entity-B
+   * Where Sub-Entity-A and Sub-Entity-B anomalies have a groupKey and a 
groupScore
+   */
   @Test
   public void testGetEmailEntity() throws Exception {
-    DateTimeZone dateTimeZone = DateTimeZone.forID("America/Los_Angeles");
+    MetricConfigDTO metric = new MetricConfigDTO();
+    metric.setName(TEST);
+    metric.setDataset(TEST);
+    metric.setAlias(TEST + "::" + TEST);
+    metricDAO.save(metric);
+
     ThirdEyeAnomalyConfiguration thirdeyeAnomalyConfig = new 
ThirdEyeAnomalyConfiguration();
     thirdeyeAnomalyConfig.setId(id);
     thirdeyeAnomalyConfig.setDashboardHost(dashboardHost);
@@ -100,57 +108,61 @@ public class TestMetricAnomaliesEmailContentFormatter {
     alerters.put("smtpConfiguration", smtpProps);
     thirdeyeAnomalyConfig.setAlerterConfiguration(alerters);
 
+    DetectionConfigDTO detection = new DetectionConfigDTO();
+    detection.setName("test_report");
+    long id = detectionDAO.save(detection);
 
-    List<AnomalyResult> anomalies = new ArrayList<>();
-    AnomalyFunctionDTO anomalyFunction = 
DaoTestUtils.getTestFunctionSpec(TEST, TEST);
-    anomalyFunctionDAO.save(anomalyFunction);
-    MergedAnomalyResultDTO anomaly = DaoTestUtils.getTestMergedAnomalyResult(
-        new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
-        new DateTime(2017, 11, 6, 13, 0, dateTimeZone).getMillis(),
-        TEST, TEST, 0.1, 1l, new DateTime(2017, 11, 6, 10, 0, 
dateTimeZone).getMillis());
-    anomaly.setFunction(anomalyFunction);
-    anomaly.setAvgCurrentVal(1.1);
-    anomaly.setAvgBaselineVal(1.0);
-    mergedAnomalyResultDAO.save(anomaly);
-    anomalies.add(anomaly);
-    anomaly = DaoTestUtils.getTestMergedAnomalyResult(
+    DateTimeZone dateTimeZone = DateTimeZone.forID("America/Los_Angeles");
+
+    MergedAnomalyResultDTO subGroupedAnomaly1 = 
DaoTestUtils.getTestGroupedAnomalyResult(
         new DateTime(2017, 11, 7, 10, 0, dateTimeZone).getMillis(),
         new DateTime(2017, 11, 7, 17, 0, dateTimeZone).getMillis(),
-        TEST, TEST, 0.1, 1l, new DateTime(2017, 11, 6, 10, 0, 
dateTimeZone).getMillis());
-    anomaly.setFunction(anomalyFunction);
-    anomaly.setAvgCurrentVal(0.9);
-    anomaly.setAvgBaselineVal(1.0);
-    mergedAnomalyResultDAO.save(anomaly);
-    anomalies.add(anomaly);
+        new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
+        id);
+    Map<String, String> properties = new HashMap<>();
+    properties.put(PROP_GROUP_KEY, "group-1");
+    properties.put(PROP_ENTITY_NAME, "sub-entity-A");
+    properties.put(PROP_ANOMALY_SCORE, "10");
+    subGroupedAnomaly1.setProperties(properties);
+    subGroupedAnomaly1.setChildIds(Collections.singleton(1l));
+    mergedAnomalyResultDAO.save(subGroupedAnomaly1);
 
-    MetricConfigDTO metric = new MetricConfigDTO();
-    metric.setName(TEST);
-    metric.setDataset(TEST);
-    metric.setAlias(TEST + "::" + TEST);
-    metricDAO.save(metric);
+    MergedAnomalyResultDTO subGroupedAnomaly2 = 
DaoTestUtils.getTestGroupedAnomalyResult(
+        new DateTime(2017, 11, 7, 10, 0, dateTimeZone).getMillis(),
+        new DateTime(2017, 11, 7, 17, 0, dateTimeZone).getMillis(),
+        new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
+        id);
+    Map<String, String> properties2 = new HashMap<>();
+    properties2.put(PROP_GROUP_KEY, "group-2");
+    properties2.put(PROP_ENTITY_NAME, "sub-entity-B");
+    properties2.put(PROP_ANOMALY_SCORE, "20");
+    subGroupedAnomaly2.setProperties(properties2);
+    subGroupedAnomaly2.setChildIds(Collections.singleton(2l));
+    mergedAnomalyResultDAO.save(subGroupedAnomaly2);
 
-    AlertConfigDTO alertConfigDTO = 
DaoTestUtils.getTestAlertConfiguration("Test Config");
+    MergedAnomalyResultDTO parentGroupedAnomaly = 
DaoTestUtils.getTestGroupedAnomalyResult(
+        new DateTime(2017, 11, 7, 10, 0, dateTimeZone).getMillis(),
+        new DateTime(2017, 11, 7, 17, 0, dateTimeZone).getMillis(),
+        new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
+        id);
+    Set<MergedAnomalyResultDTO> childrenAnomalies = new HashSet<>();
+    childrenAnomalies.add(subGroupedAnomaly1);
+    childrenAnomalies.add(subGroupedAnomaly2);
+    parentGroupedAnomaly.setChildren(childrenAnomalies);
+    mergedAnomalyResultDAO.save(parentGroupedAnomaly);
+    List<AnomalyResult> anomalies = new ArrayList<>();
+    anomalies.add(parentGroupedAnomaly);
 
-    EmailContentFormatter contentFormatter = new 
MetricAnomaliesEmailContentFormatter();
+    EmailContentFormatter contentFormatter = new 
EntityGroupByContentFormatter();
     contentFormatter.init(new Properties(), 
EmailContentFormatterConfiguration.fromThirdEyeAnomalyConfiguration(thirdeyeAnomalyConfig));
-    DetectionAlertFilterRecipients recipients = new 
DetectionAlertFilterRecipients(
-        EmailUtils.getValidEmailAddresses("[email protected]"));
-    EmailEntity emailEntity = contentFormatter.getEmailEntity(alertConfigDTO, 
recipients, TEST,
-        null, "", anomalies, null);
-
-    String htmlPath = 
ClassLoader.getSystemResource("test-metric-anomalies-template.html").getPath();
-    BufferedReader br = new BufferedReader(new FileReader(htmlPath));
-    StringBuilder htmlContent = new StringBuilder();
-    for(String line = br.readLine(); line != null; line = br.readLine()) {
-      htmlContent.append(line + "\n");
-    }
-    br.close();
-
-    HtmlEmail email = emailEntity.getContent();
-    Field field = email.getClass().getDeclaredField("html");
-    field.setAccessible(true);
-    String emailHtml = field.get(email).toString();
-    Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
-        htmlContent.toString().replaceAll("\\s", ""));
+    EmailEntity emailEntity = contentFormatter.getEmailEntity(
+        DaoTestUtils.getTestAlertConfiguration("Test Config"),
+        new 
DetectionAlertFilterRecipients(EmailUtils.getValidEmailAddresses("[email protected]")),
+        TEST, null, "", anomalies, null);
+    String htmlPath = 
ClassLoader.getSystemResource("test-entity-groupby-email-content-formatter.html").getPath();
+
+    Assert.assertEquals(
+        ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+        ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
   }
 }
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
index 72ccb1b..db3d07a 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
@@ -32,16 +32,12 @@ import 
org.apache.pinot.thirdeye.datalayer.dto.AnomalyFunctionDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
 import org.apache.pinot.thirdeye.datasource.DAORegistry;
 import 
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.testng.Assert;
@@ -158,18 +154,8 @@ public class 
TestHierarchicalAnomaliesEmailContentFormatter {
         null, "", anomalies, null);
 
     String htmlPath = 
ClassLoader.getSystemResource("test-hierarchical-metric-anomalies-template.html").getPath();
-    BufferedReader br = new BufferedReader(new FileReader(htmlPath));
-    StringBuilder htmlContent = new StringBuilder();
-    for(String line = br.readLine(); line != null; line = br.readLine()) {
-      htmlContent.append(line + "\n");
-    }
-    br.close();
-
-    HtmlEmail email = emailEntity.getContent();
-    Field field = email.getClass().getDeclaredField("html");
-    field.setAccessible(true);
-    String emailHtml = field.get(email).toString();
-    Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
-        htmlContent.toString().replaceAll("\\s", ""));
+    Assert.assertEquals(
+        ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+        ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
   }
 }
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
index bcc3808..952c5ff 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
@@ -34,16 +34,12 @@ import 
org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MetricConfigDTO;
 import org.apache.pinot.thirdeye.datasource.DAORegistry;
 import 
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.testng.Assert;
@@ -139,18 +135,8 @@ public class TestMetricAnomaliesEmailContentFormatter {
         null, "", anomalies, null);
 
     String htmlPath = 
ClassLoader.getSystemResource("test-metric-anomalies-template.html").getPath();
-    BufferedReader br = new BufferedReader(new FileReader(htmlPath));
-    StringBuilder htmlContent = new StringBuilder();
-    for(String line = br.readLine(); line != null; line = br.readLine()) {
-      htmlContent.append(line + "\n");
-    }
-    br.close();
-
-    HtmlEmail email = emailEntity.getContent();
-    Field field = email.getClass().getDeclaredField("html");
-    field.setAccessible(true);
-    String emailHtml = field.get(email).toString();
-    Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
-        htmlContent.toString().replaceAll("\\s", ""));
+    Assert.assertEquals(
+        ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+        ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
   }
 }
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
index 5e3c587..325d268 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
@@ -33,16 +33,12 @@ import 
org.apache.pinot.thirdeye.datalayer.dto.AnomalyFunctionDTO;
 import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
 import org.apache.pinot.thirdeye.datasource.DAORegistry;
 import 
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.testng.Assert;
@@ -133,19 +129,9 @@ public class TestOnboardingNotificationContentFormatter {
         null, "", anomalies, context);
 
     String htmlPath = 
ClassLoader.getSystemResource("test-onboard-notification-email-content-formatter.html").getPath();
-    BufferedReader br = new BufferedReader(new FileReader(htmlPath));
-    StringBuilder htmlContent = new StringBuilder();
-    for(String line = br.readLine(); line != null; line = br.readLine()) {
-      htmlContent.append(line + "\n");
-    }
-    br.close();
-
-    HtmlEmail email = emailEntity.getContent();
-    Field field = email.getClass().getDeclaredField("html");
-    field.setAccessible(true);
-    String emailHtml = field.get(email).toString();
-    Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
-        htmlContent.toString().replaceAll("\\s", ""));
+    Assert.assertEquals(
+        ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+        ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
   }
 
 }
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
index 3f11d17..cf92740 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
@@ -326,11 +326,24 @@ public class DaoTestUtils {
     anomaly.setMetric(metric);
     anomaly.setWeight(weight);
     anomaly.setFunctionId(functionId);
+    anomaly.setDetectionConfigId(functionId);
     anomaly.setCreatedTime(createdTime);
 
     return anomaly;
   }
 
+  public static MergedAnomalyResultDTO getTestGroupedAnomalyResult(long 
startTime, long endTime, long createdTime, long id) {
+    MergedAnomalyResultDTO anomaly = new MergedAnomalyResultDTO();
+    anomaly.setStartTime(startTime);
+    anomaly.setEndTime(endTime);
+    anomaly.setCollection(null);
+    anomaly.setMetric(null);
+    anomaly.setCreatedTime(createdTime);
+    anomaly.setDetectionConfigId(id);
+
+    return anomaly;
+  }
+
   public static RootcauseSessionDTO getTestRootcauseSessionResult(long start, 
long end, long created, long updated,
       String name, String owner, String text, String granularity, String 
compareMode, Long previousId, Long anomalyId) {
     RootcauseSessionDTO session = new RootcauseSessionDTO();
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
index 4af7323..e61eccd 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
@@ -240,7 +240,7 @@ public class YamlResourceTest {
       Assert.assertEquals(alertDTO.getName(), "Subscription Group Name");
       Assert.assertEquals(alertDTO.getApplication(), "test_application");
       Assert.assertNotNull(alertDTO.getAlertSchemes().get("emailScheme"));
-      
Assert.assertEquals(alertDTO.getAlertSchemes().get("emailScheme").get("template"),
 "ENTITY_REPORT");
+      
Assert.assertEquals(alertDTO.getAlertSchemes().get("emailScheme").get("template"),
 "ENTITY_GROUPBY_REPORT");
       
Assert.assertEquals(alertDTO.getAlertSchemes().get("emailScheme").get("subject"),
 "METRICS");
 
       // Verify if the vector clock is updated with the updated detection
diff --git 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
index 5ac47ca..de90e00 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
+++ 
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
@@ -17,6 +17,7 @@
 package org.apache.pinot.thirdeye.tools;
 
 import org.apache.pinot.thirdeye.anomaly.task.TaskConstants;
+import org.apache.pinot.thirdeye.constant.AnomalyResultSource;
 import org.apache.pinot.thirdeye.datalayer.bao.AlertConfigManager;
 import org.apache.pinot.thirdeye.datalayer.bao.AnomalyFunctionManager;
 import org.apache.pinot.thirdeye.datalayer.bao.ApplicationManager;
@@ -443,10 +444,10 @@ public class RunAdhocDatabaseQueriesTool {
     disableAllActiveFunction(null);
   }
 
-  private void disableAllActiveFunction(Collection<Long> exception){
+  private void disableAllActiveFunction(Collection<Long> excludeIds){
     List<AnomalyFunctionDTO> functionSpecs = 
anomalyFunctionDAO.findAllActiveFunctions();
     for (AnomalyFunctionDTO functionSpec : functionSpecs) {
-      if (functionSpec.getIsActive() && (CollectionUtils.isEmpty(exception) || 
!exception
+      if (functionSpec.getIsActive() && (CollectionUtils.isEmpty(excludeIds) 
|| !excludeIds
           .contains(functionSpec.getId()))) {
         functionSpec.setActive(false);
         anomalyFunctionDAO.update(functionSpec);
@@ -455,6 +456,20 @@ public class RunAdhocDatabaseQueriesTool {
   }
 
   /**
+   * Disable all subscription groups except groups with id in excludeIds
+   */
+  private void disableAllActiveSubsGroups(Collection<Long> excludeIds){
+    List<DetectionAlertConfigDTO> subsConfigs = 
detectionAlertConfigDAO.findAll();
+    for (DetectionAlertConfigDTO subsConfig : subsConfigs) {
+      if (subsConfig.isActive() && (CollectionUtils.isEmpty(excludeIds) || 
!excludeIds
+          .contains(subsConfig.getId()))) {
+        subsConfig.setActive(false);
+        detectionAlertConfigDAO.update(subsConfig);
+      }
+    }
+  }
+
+  /**
    * Generates a report of the status and owner of all the un-subscribed 
anomaly functions
    */
   private void unsubscribedDetections(){
@@ -576,6 +591,20 @@ public class RunAdhocDatabaseQueriesTool {
     }
   }
 
+  /**
+   * Replayed anomalies are flagged accordingly and such anomalies are 
excluded from the email report.
+   * This method removes the replay flag to test an email report from replayed 
results.
+   */
+  private void removeReplayFlagFromAnomalies(long detectionConfigId) {
+    List<MergedAnomalyResultDTO> anomalies = 
mergedResultDAO.findByDetectionConfigAndIdGreaterThan(detectionConfigId,0l);
+    for (MergedAnomalyResultDTO anomaly : anomalies) {
+      if (!anomaly.isChild()) {
+        
anomaly.setAnomalyResultSource(AnomalyResultSource.DEFAULT_ANOMALY_DETECTION);
+        mergedResultDAO.save(anomaly);
+      }
+    }
+  }
+
   public static void main(String[] args) throws Exception {
 
     File persistenceFile = new File(args[0]);
diff --git 
a/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
 
b/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
index 9e7517d..9f04929 100644
--- 
a/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
+++ 
b/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
@@ -24,7 +24,7 @@ recipients:
 alertSchemes:
 - type: EMAIL
   params:
-    template: ENTITY_REPORT
+    template: ENTITY_GROUPBY_REPORT
     subject: METRICS
 - type: IRIS
   params:
diff --git 
a/thirdeye/thirdeye-pinot/src/test/resources/test-entity-groupby-email-content-formatter.html
 
b/thirdeye/thirdeye-pinot/src/test/resources/test-entity-groupby-email-content-formatter.html
new file mode 100644
index 0000000..0d3616f
--- /dev/null
+++ 
b/thirdeye/thirdeye-pinot/src/test/resources/test-entity-groupby-email-content-formatter.html
@@ -0,0 +1,105 @@
+<head>
+  <link href="https://fonts.googleapis.com/css?family=Open+Sans"; 
rel="stylesheet">
+</head>
+
+<body style="background-color: #EDF0F3;">
+<table border="0" cellpadding="0" cellspacing="0" style="width:100%; 
font-family: 'Proxima Nova','Arial','Helvetica Neue',Helvetica,sans-serif; 
font-size:14px; margin:0 auto; max-width: 50%; min-width:700px; 
background-color: #F3F6F8;">
+  <!-- White bar on top -->
+  <tr>
+    <td align="center" style="padding: 6px 24px;">
+      <span style="color: red; font-size: 35px; vertical-align: 
middle;">&nbsp;&#9888;&nbsp;</span>
+      <span style="color: rgba(0,0,0,0.75);font-size: 18px; font-weight: bold; 
letter-spacing: 2px; vertical-align: middle;">ACTION REQUIRED ON THIRDEYE 
ALERT</span>
+    </td>
+  </tr>
+
+  <!-- Blue header on top -->
+  <tr>
+    <td style="font-size: 16px; padding: 12px; background-color: #0073B1; 
color: #FFF; text-align: center;">
+      <span style="font-size: 18px; font-weight: bold; letter-spacing: 2px; 
vertical-align: middle;">Report: test_report</span>
+    </td>
+  </tr>
+
+  <tr>
+    <td>
+      <table border="0" cellpadding="0" cellspacing="0" style="border:1px 
solid #E9E9E9; border-radius: 2px; width: 100%;">
+        <!-- List all the alerts -->
+        <tr>
+          <td style="border-bottom: 1px solid rgba(0,0,0,0.15); padding: 12px 
24px; align: left">
+            <!-- Display Entity Name -->
+            <p>
+              <span style="color: #1D1D1D; font-size: 20px; font-weight: bold; 
display:inline-block; vertical-align: middle;">Entity:&nbsp;</span>
+              <span style="color: #606060; font-size: 20px; text-decoration: 
none; display:inline-block; vertical-align: middle; width: 70%; white-space: 
nowrap; overflow: hidden; text-overflow: ellipsis;">sub-entity-B</span>
+            </p>
+            <!-- List all the anomalies under this entity in a table -->
+            <table border="0" width="100%" align="center" style="width:100%; 
padding:0; margin:0; border-collapse: collapse;text-align:left;">
+              <tr style="text-align:center; background-color: #F6F8FA; 
border-top: 2px solid #C7D1D8; border-bottom: 2px solid #C7D1D8;">
+                <th style="text-align:left; padding: 6px 12px; font-size: 
12px; font-weight: bold; line-height: 20px;">Feature</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Criticality Score*</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Current</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Predicted</th>
+              </tr>
+              <tr style="border-bottom: 1px solid #C7D1D8;">
+                <td style="padding: 6px 12px;white-space: nowrap;">
+                  <a style="font-weight: bold; text-decoration: none; 
font-size:14px; line-height:20px; color: #0073B1;" 
href="http://localhost:8080/dashboard/app/#/anomalies?anomalyIds=2";
+                     target="_blank">group-2</a>
+                  <span style="color: rgba(0,0,0,0.6); font-size:12px; 
line-height:16px;">7 hours</span>
+                </td>
+                <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">20</td>
+                <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">0</td>
+                <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">
+                  0
+                  <div style="font-size: 12px; color:#3A8C18;">(+100.00%)</div>
+                </td>
+              </tr>
+            </table>
+          </td>
+        </tr>
+        <tr>
+          <td style="border-bottom: 1px solid rgba(0,0,0,0.15); padding: 12px 
24px; align: left">
+            <!-- Display Entity Name -->
+            <p>
+              <span style="color: #1D1D1D; font-size: 20px; font-weight: bold; 
display:inline-block; vertical-align: middle;">Entity:&nbsp;</span>
+              <span style="color: #606060; font-size: 20px; text-decoration: 
none; display:inline-block; vertical-align: middle; width: 70%; white-space: 
nowrap; overflow: hidden; text-overflow: ellipsis;">sub-entity-A</span>
+            </p>
+            <!-- List all the anomalies under this entity in a table -->
+            <table border="0" width="100%" align="center" style="width:100%; 
padding:0; margin:0; border-collapse: collapse;text-align:left;">
+              <tr style="text-align:center; background-color: #F6F8FA; 
border-top: 2px solid #C7D1D8; border-bottom: 2px solid #C7D1D8;">
+                <th style="text-align:left; padding: 6px 12px; font-size: 
12px; font-weight: bold; line-height: 20px;">Feature</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Criticality Score*</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Current</th>
+                <th style="padding: 6px 12px; font-size: 12px; font-weight: 
bold; line-height: 20px;">Predicted</th>
+              </tr>
+              <tr style="border-bottom: 1px solid #C7D1D8;">
+                <td style="padding: 6px 12px;white-space: nowrap;">
+                  <a style="font-weight: bold; text-decoration: none; 
font-size:14px; line-height:20px; color: #0073B1;" 
href="http://localhost:8080/dashboard/app/#/anomalies?anomalyIds=1";
+                     target="_blank">group-1</a>
+                  <span style="color: rgba(0,0,0,0.6); font-size:12px; 
line-height:16px;">7 hours</span>
+                </td>
+                <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">10</td>
+                <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">0</td>
+                <td style="color: rgba(0,0,0,0.9); font-size:14px; 
line-height:20px; text-align:center;">
+                  0
+                  <div style="font-size: 12px; color:#3A8C18;">(+100.00%)</div>
+                </td>
+              </tr>
+            </table>
+          </td>
+        </tr>
+
+        <!-- Reference Links -->
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td style="text-align: center; background-color: #EDF0F3; font-size: 12px; 
font-family:'Proxima Nova','Arial', 'Helvetica Neue',Helvetica, sans-serif; 
color: #737373; padding: 12px;">
+      <p style="margin-top:0;"> You are receiving this email because you have 
subscribed to ThirdEye Alert Service for
+        <strong>Test Config</strong>.</p>
+      <p>
+        If you have any questions regarding this report, please email
+        <a style="color: #33aada;" href="mailto:[email protected]"; 
target="_top">[email protected]</a>
+      </p>
+    </td>
+  </tr>
+</table>
+</body>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to