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.'),