diff --git a/assets/js/src/newsletters/campaign_stats/newsletter_general_stats.tsx b/assets/js/src/newsletters/campaign_stats/newsletter_general_stats.tsx
index f1f9d94a04..5377089822 100644
--- a/assets/js/src/newsletters/campaign_stats/newsletter_general_stats.tsx
+++ b/assets/js/src/newsletters/campaign_stats/newsletter_general_stats.tsx
@@ -22,15 +22,18 @@ export const NewsletterGeneralStats = ({
let percentageClicked = 0;
let percentageOpened = 0;
+ let percentageMachineOpened = 0;
let percentageUnsubscribed = 0;
if (totalSent > 0) {
percentageClicked = (newsletter.statistics.clicked * 100) / totalSent;
percentageOpened = (newsletter.statistics.opened * 100) / totalSent;
+ percentageMachineOpened = (newsletter.statistics.machineOpens * 100) / totalSent;
percentageUnsubscribed = (newsletter.statistics.unsubscribed * 100) / totalSent;
}
// format to 1 decimal place
const percentageClickedDisplay = MailPoet.Num.toLocaleFixed(percentageClicked, 1);
const percentageOpenedDisplay = MailPoet.Num.toLocaleFixed(percentageOpened, 1);
+ const percentageMachineOpenedDisplay = MailPoet.Num.toLocaleFixed(percentageMachineOpened, 1);
const percentageUnsubscribedDisplay = MailPoet.Num.toLocaleFixed(percentageUnsubscribed, 1);
const displayBadges = ((totalSent >= minNewslettersSent)
@@ -47,6 +50,16 @@ export const NewsletterGeneralStats = ({
);
+ const machineOpened = (
+
+
+ {percentageMachineOpenedDisplay}
+ {'% '}
+
+ {MailPoet.I18n.t('percentageMachineOpened')}
+
+ );
+
const unsubscribed = (
@@ -102,6 +115,7 @@ export const NewsletterGeneralStats = ({
{opened}
+ {machineOpened}
{isWoocommerceActive && (
diff --git a/assets/js/src/newsletters/campaign_stats/newsletter_type.ts b/assets/js/src/newsletters/campaign_stats/newsletter_type.ts
index c474f0743c..b925652a6d 100644
--- a/assets/js/src/newsletters/campaign_stats/newsletter_type.ts
+++ b/assets/js/src/newsletters/campaign_stats/newsletter_type.ts
@@ -17,6 +17,7 @@ export type NewsletterType = {
statistics: {
clicked: number;
opened: number;
+ machineOpens: number;
unsubscribed: number;
revenue: {
value: number;
diff --git a/lib/API/JSON/ResponseBuilders/NewslettersResponseBuilder.php b/lib/API/JSON/ResponseBuilders/NewslettersResponseBuilder.php
index 61a258a503..8ce2465b62 100644
--- a/lib/API/JSON/ResponseBuilders/NewslettersResponseBuilder.php
+++ b/lib/API/JSON/ResponseBuilders/NewslettersResponseBuilder.php
@@ -90,6 +90,7 @@ class NewslettersResponseBuilder {
->where('tasks.status', SendingQueue::STATUS_SCHEDULED)
->count();
}
+
if ($relation === self::RELATION_STATISTICS) {
$data['statistics'] = $this->newslettersStatsRepository->getStatistics($newsletter)->asArray();
}
diff --git a/lib/Newsletter/Statistics/NewsletterStatistics.php b/lib/Newsletter/Statistics/NewsletterStatistics.php
index 4d364226e4..4532a86d2c 100644
--- a/lib/Newsletter/Statistics/NewsletterStatistics.php
+++ b/lib/Newsletter/Statistics/NewsletterStatistics.php
@@ -10,6 +10,9 @@ class NewsletterStatistics {
/** @var int */
private $openCount;
+ /** @var int */
+ private $machineOpens;
+
/** @var int */
private $unsubscribeCount;
@@ -27,49 +30,40 @@ class NewsletterStatistics {
$this->wooCommerceRevenue = $wooCommerceRevenue;
}
- /**
- * @return int
- */
- public function getClickCount() {
+ public function getClickCount(): int {
return $this->clickCount;
}
- /**
- * @return int
- */
- public function getOpenCount() {
+ public function getOpenCount(): int {
return $this->openCount;
}
- /**
- * @return int
- */
- public function getUnsubscribeCount() {
+ public function getUnsubscribeCount(): int {
return $this->unsubscribeCount;
}
- /**
- * @return int
- */
- public function getTotalSentCount() {
+ public function getTotalSentCount(): int {
return $this->totalSentCount;
}
- /**
- * @return WooCommerceRevenue|null
- */
- public function getWooCommerceRevenue() {
+ public function getWooCommerceRevenue(): ?WooCommerceRevenue {
return $this->wooCommerceRevenue;
}
- /**
- * @return array
- */
- public function asArray() {
+ public function setMachineOpens(int $machineOpens): void {
+ $this->machineOpens = $machineOpens;
+ }
+
+ public function getMachineOpens(): int {
+ return $this->machineOpens;
+ }
+
+ public function asArray(): array {
return [
- 'clicked' => (int)$this->clickCount,
- 'opened' => (int)$this->openCount,
- 'unsubscribed' => (int)$this->unsubscribeCount,
+ 'clicked' => $this->clickCount,
+ 'opened' => $this->openCount,
+ 'machineOpens' => $this->machineOpens,
+ 'unsubscribed' => $this->unsubscribeCount,
'revenue' => empty($this->wooCommerceRevenue) ? null : $this->wooCommerceRevenue->asArray(),
];
}
diff --git a/lib/Newsletter/Statistics/NewsletterStatisticsRepository.php b/lib/Newsletter/Statistics/NewsletterStatisticsRepository.php
index b1bff8363c..6be3ab49b0 100644
--- a/lib/Newsletter/Statistics/NewsletterStatisticsRepository.php
+++ b/lib/Newsletter/Statistics/NewsletterStatisticsRepository.php
@@ -12,6 +12,7 @@ use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Entities\UserAgentEntity;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoetVendor\Doctrine\ORM\EntityManager;
+use MailPoetVendor\Doctrine\ORM\QueryBuilder;
use MailPoetVendor\Doctrine\ORM\UnexpectedResultException;
/**
@@ -32,13 +33,15 @@ class NewsletterStatisticsRepository extends Repository {
}
public function getStatistics(NewsletterEntity $newsletter): NewsletterStatistics {
- return new NewsletterStatistics(
+ $stats = new NewsletterStatistics(
$this->getStatisticsClickCount($newsletter),
$this->getStatisticsOpenCount($newsletter),
$this->getStatisticsUnsubscribeCount($newsletter),
$this->getTotalSentCount($newsletter),
$this->getWooCommerceRevenue($newsletter)
);
+ $stats->setMachineOpens($this->getStatisticsMachineOpenCount($newsletter));
+ return $stats;
}
/**
@@ -81,6 +84,17 @@ class NewsletterStatisticsRepository extends Repository {
return $counts[$newsletter->getId()] ?? 0;
}
+ public function getStatisticsMachineOpenCount(NewsletterEntity $newsletter): int {
+ $qb = $this->getStatisticsQuery(StatisticsOpenEntity::class, [$newsletter]);
+ $result = $qb->andWhere('(stats.userAgentType = :userAgentType)')
+ ->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE)
+ ->getQuery()
+ ->getResult();
+
+ if (empty($result)) return 0;
+ return $result['cnt'] ?? 0;
+ }
+
public function getStatisticsUnsubscribeCount(NewsletterEntity $newsletter): int {
$counts = $this->getStatisticCounts(StatisticsUnsubscribeEntity::class, [$newsletter]);
return $counts[$newsletter->getId()] ?? 0;
@@ -132,12 +146,7 @@ class NewsletterStatisticsRepository extends Repository {
}
private function getStatisticCounts(string $statisticsEntityName, array $newsletters): array {
- $qb = $this->entityManager->createQueryBuilder()
- ->select('IDENTITY(stats.newsletter) AS id, COUNT(DISTINCT stats.subscriber) as cnt')
- ->from($statisticsEntityName, 'stats')
- ->where('stats.newsletter IN (:newsletters)')
- ->groupBy('stats.newsletter')
- ->setParameter('newsletters', $newsletters);
+ $qb = $this->getStatisticsQuery($statisticsEntityName, $newsletters);
if (in_array($statisticsEntityName, [StatisticsOpenEntity::class, StatisticsClickEntity::class], true)) {
$qb->andWhere('(stats.userAgentType = :userAgentType) OR (stats.userAgentType IS NULL)')
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
@@ -154,6 +163,15 @@ class NewsletterStatisticsRepository extends Repository {
return $counts;
}
+ private function getStatisticsQuery(string $statisticsEntityName, array $newsletters): QueryBuilder {
+ return $this->entityManager->createQueryBuilder()
+ ->select('IDENTITY(stats.newsletter) AS id, COUNT(DISTINCT stats.subscriber) as cnt')
+ ->from($statisticsEntityName, 'stats')
+ ->where('stats.newsletter IN (:newsletters)')
+ ->groupBy('stats.newsletter')
+ ->setParameter('newsletters', $newsletters);
+ }
+
private function getWooCommerceRevenues(array $newsletters) {
if (!$this->wcHelper->isWooCommerceActive()) {
return null;
diff --git a/tests/integration/API/JSON/ResponseBuilders/NewslettersResponseBuilderTest.php b/tests/integration/API/JSON/ResponseBuilders/NewslettersResponseBuilderTest.php
index 9b14d9780f..97839c81fc 100644
--- a/tests/integration/API/JSON/ResponseBuilders/NewslettersResponseBuilderTest.php
+++ b/tests/integration/API/JSON/ResponseBuilders/NewslettersResponseBuilderTest.php
@@ -27,13 +27,16 @@ class NewslettersResponseBuilderTest extends \MailPoetTest {
'opened' => 6,
'clicked' => 4,
'unsubscribed' => 2,
+ 'machineOpens' => 9,
'revenue' => null,
],
];
+ $statistics = new NewsletterStatistics(4, 6, 2, 10, null);
+ $statistics->setMachineOpens(9);
$newsletterStatsRepository = Stub::make(NewsletterStatisticsRepository::class, [
'getTotalSentCount' => $stats['total_sent'],
'getChildrenCount' => $stats['children_count'],
- 'getStatistics' => new NewsletterStatistics(4, 6, 2, 10, null),
+ 'getStatistics' => $statistics,
]);
$newsletterRepository = Stub::make(NewslettersRepository::class);
$newsletterUrl = $this->diContainer->get(Url::class);
diff --git a/views/newsletters.html b/views/newsletters.html
index f13cfa8aa5..18e612aa4d 100644
--- a/views/newsletters.html
+++ b/views/newsletters.html
@@ -389,6 +389,7 @@
'statsReplyToAddress': __('Reply-to'),
'statsTotalSent': __('Sent to'),
'percentageOpened': _x('opened', 'Percentage of subscribers that opened a newsletter link'),
+ 'percentageMachineOpened': _x('machine-opened', 'Percentage of newsletters that were opened by a machine'),
'percentageClicked': _x('clicked', 'Percentage of subscribers that clicked a newsletter link'),
'percentageUnsubscribed': _x('unsubscribed', 'Percentage of subscribers that unsubscribed from a newsletter'),
'readMoreOnStats': __('Read more on stats.'),