Add Analytics Page and backend logic

[MAILPOET-5088]
This commit is contained in:
David Remer
2023-05-11 08:52:19 +03:00
committed by Aschepikov
parent 8115ab0382
commit f10ef78825
10 changed files with 471 additions and 1 deletions

View File

@@ -0,0 +1,101 @@
<?php declare(strict_types = 1);
namespace MailPoet\AdminPages\Pages;
use MailPoet\AdminPages\PageRenderer;
use MailPoet\Automation\Engine\Mappers\AutomationMapper;
use MailPoet\Automation\Engine\Registry;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Form\AssetsController;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice as WPNotice;
class AutomationAnalytics {
/** @var AssetsController */
private $assetsController;
/** @var PageRenderer */
private $pageRenderer;
/** @var AutomationStorage */
private $automationStorage;
/** @var AutomationMapper */
private $automationMapper;
/** @var Registry */
private $registry;
/** @var WPFunctions */
private $wp;
public function __construct(
AssetsController $assetsController,
PageRenderer $pageRenderer,
AutomationStorage $automationStorage,
AutomationMapper $automationMapper,
Registry $registry,
WPFunctions $wp
) {
$this->assetsController = $assetsController;
$this->pageRenderer = $pageRenderer;
$this->automationStorage = $automationStorage;
$this->automationMapper = $automationMapper;
$this->registry = $registry;
$this->wp = $wp;
}
public function render() {
$this->assetsController->setupAutomationAnalyticsDependencies();
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$automation = $id ? $this->automationStorage->getAutomation($id) : null;
if (!$automation) {
$notice = new WPNotice(
WPNotice::TYPE_ERROR,
__('Automation not found.', 'mailpoet')
);
$notice->displayWPNotice();
$this->pageRenderer->displayPage('blank.html');
return;
}
$this->pageRenderer->displayPage('automation/analytics.html', [
'registry' => $this->buildRegistry(),
'context' => $this->buildContext(),
'automation' => $this->automationMapper->buildAutomation($automation),
'locale_full' => $this->wp->getLocale(),
'api' => [
'root' => rtrim($this->wp->escUrlRaw($this->wp->restUrl()), '/'),
'nonce' => $this->wp->wpCreateNonce('wp_rest'),
],
'jsonapi' => [
'root' => rtrim($this->wp->escUrlRaw(admin_url('admin-ajax.php')), '/'),
],
]);
}
private function buildRegistry(): array {
$steps = [];
foreach ($this->registry->getSteps() as $key => $step) {
$steps[$key] = [
'key' => $step->getKey(),
'name' => $step->getName(),
'args_schema' => $step->getArgsSchema()->toArray(),
];
}
return [
'steps' => $steps,
];
}
private function buildContext(): array {
$data = [];
foreach ($this->registry->getContextFactories() as $key => $factory) {
$data[$key] = $factory();
}
return $data;
}
}

View File

@@ -0,0 +1,137 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Controller;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Integrations\MailPoet\Actions\SendEmailAction;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Entities\Query;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\WooCommerce\Helper;
class OverviewStatisticsController {
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var NewsletterStatisticsRepository */
private $newsletterStatisticsRepository;
/** @var Helper */
private $wooCommerceHelper;
public function __construct(
NewslettersRepository $newslettersRepository,
NewsletterStatisticsRepository $newsletterStatisticsRepository,
Helper $wooCommerceHelper
) {
$this->newslettersRepository = $newslettersRepository;
$this->newsletterStatisticsRepository = $newsletterStatisticsRepository;
$this->wooCommerceHelper = $wooCommerceHelper;
}
public function getStatisticsForAutomation(Automation $automation, Query $query): array {
$emails = $this->getEmailsFromAutomation($automation);
$formattedEmptyRevenue = $this->wooCommerceHelper->getRawPrice(
0,
[
'currency' => $this->wooCommerceHelper->getWoocommerceCurrency(),
]
);
$data = [
'total' => ['current' => 0, 'previous' => 0],
'opened' => ['current' => 0, 'previous' => 0],
'clicked' => ['current' => 0, 'previous' => 0],
'orders' => ['current' => 0, 'previous' => 0],
'revenue' => ['current' => 0, 'previous' => 0],
'revenue_formatted' => [
'current' => $formattedEmptyRevenue,
'previous' => $formattedEmptyRevenue,
],
];
if (!$emails) {
return $data;
}
$requiredData = [
'totals',
StatisticsClickEntity::class,
StatisticsOpenEntity::class,
WooCommerceRevenue::class,
];
$currentStatistics = $this->newsletterStatisticsRepository->getBatchStatistics(
$emails,
$query->getPrimaryAfter(),
$query->getPrimaryBefore(),
$requiredData
);
foreach ($currentStatistics as $statistic) {
$data['total']['current'] += $statistic->getTotalSentCount();
$data['opened']['current'] += $statistic->getOpenCount();
$data['clicked']['current'] += $statistic->getClickCount();
$data['orders']['current'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
$data['revenue']['current'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
}
$previousStatistics = $this->newsletterStatisticsRepository->getBatchStatistics(
$emails,
$query->getSecondaryAfter(),
$query->getSecondaryBefore(),
$requiredData
);
foreach ($previousStatistics as $statistic) {
$data['total']['previous'] += $statistic->getTotalSentCount();
$data['opened']['previous'] += $statistic->getOpenCount();
$data['clicked']['previous'] += $statistic->getClickCount();
$data['orders']['previous'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getOrdersCount() : 0;
$data['revenue']['previous'] += $statistic->getWooCommerceRevenue() ? $statistic->getWooCommerceRevenue()->getValue() : 0;
}
$data['revenue_formatted']['current'] = $this->wooCommerceHelper->getRawPrice(
$data['revenue']['current'],
[
'currency' => $this->wooCommerceHelper->getWoocommerceCurrency(),
]
);
$data['revenue_formatted']['previous'] = $this->wooCommerceHelper->getRawPrice(
$data['revenue']['previous'],
[
'currency' => $this->wooCommerceHelper->getWoocommerceCurrency(),
]
);
return $data;
}
private function getEmailsFromAutomation(Automation $automation): array {
return array_filter(array_map(
function(Step $step) {
$emailId = $step->getArgs()['email_id'] ?? null;
if (!$emailId) {
return null;
}
return $this->newslettersRepository->findOneById((int)$emailId);
},
array_filter(
array_values($automation->getSteps()),
function ($step) {
return in_array(
$step->getKey(),
[
SendEmailAction::KEY,
],
true
);
}
)
));
}
}

View File

@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Endpoints;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Automation\Engine\API\Endpoint;
use MailPoet\Automation\Engine\Exceptions\NotFoundException;
use MailPoet\Automation\Engine\Exceptions\RuntimeException;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Controller\OverviewStatisticsController;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Entities\Query;
use MailPoet\Validator\Builder;
class OverviewEndpoint extends Endpoint {
/** @var AutomationStorage */
private $automationStorage;
/** @var OverviewStatisticsController */
private $overviewStatisticsController;
public function __construct(
AutomationStorage $automationStorage,
OverviewStatisticsController $overviewStatisticsController
) {
$this->automationStorage = $automationStorage;
$this->overviewStatisticsController = $overviewStatisticsController;
}
public function handle(Request $request): Response {
$automationid = $request->getParam('id');
if (!is_int($automationid)) {
throw new RuntimeException('Invalid automation id');
}
$automation = $this->automationStorage->getAutomation($automationid);
if (!$automation) {
throw new NotFoundException(__('Automation not found', 'mailpoet'));
}
$query = Query::fromRequest($request);
$result = $this->overviewStatisticsController->getStatisticsForAutomation($automation, $query);
return new Response($result);
}
public static function getRequestSchema(): array {
return [
'id' => Builder::integer()->required(),
'query' => Builder::object(
[
'primary' => Builder::object(
[
'after' => Builder::string()->formatDateTime()->required(),
'before' => Builder::string()->formatDateTime()->required(),
]
),
'secondary' => Builder::object(
[
'after' => Builder::string()->formatDateTime()->required(),
'before' => Builder::string()->formatDateTime()->required(),
]
),
]
),
];
}
}

View File

@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics\Entities;
use MailPoet\API\REST\Request;
class Query {
/** @var \DateTimeImmutable */
private $primaryAfter;
/** @var \DateTimeImmutable */
private $primaryBefore;
/** @var \DateTimeImmutable */
private $secondaryAfter;
/** @var \DateTimeImmutable */
private $secondaryBefore;
public function __construct(
\DateTimeImmutable $primaryAfter,
\DateTimeImmutable $primaryBefore,
\DateTimeImmutable $secondaryAfter,
\DateTimeImmutable $secondaryBefore
) {
$this->primaryAfter = $primaryAfter;
$this->primaryBefore = $primaryBefore;
$this->secondaryAfter = $secondaryAfter;
$this->secondaryBefore = $secondaryBefore;
}
public function getPrimaryAfter(): \DateTimeImmutable {
return $this->primaryAfter;
}
public function getPrimaryBefore(): \DateTimeImmutable {
return $this->primaryBefore;
}
public function getSecondaryAfter(): \DateTimeImmutable {
return $this->secondaryAfter;
}
public function getSecondaryBefore(): \DateTimeImmutable {
return $this->secondaryBefore;
}
public static function fromRequest(Request $request): self {
$query = $request->getParam('query');
if (!is_array($query)) {
throw new \InvalidArgumentException('Invalid query parameters');
}
$primary = $query['primary'] ?? null;
$secondary = $query['secondary'] ?? null;
if (!is_array($primary) || !is_array($secondary)) {
throw new \InvalidArgumentException('Invalid query parameters');
}
$primaryAfter = $primary['after'] ?? null;
$primaryBefore = $primary['before'] ?? null;
$secondaryAfter = $secondary['after'] ?? null;
$secondaryBefore = $secondary['before'] ?? null;
if (
!is_string($primaryAfter) ||
!is_string($primaryBefore) ||
!is_string($secondaryAfter) ||
!is_string($secondaryBefore)
) {
throw new \InvalidArgumentException('Invalid query parameters');
}
return new self(
new \DateTimeImmutable($primaryAfter),
new \DateTimeImmutable($primaryBefore),
new \DateTimeImmutable($secondaryAfter),
new \DateTimeImmutable($secondaryBefore)
);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\MailPoet\Analytics;
use MailPoet\API\REST\API;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Endpoints\OverviewEndpoint;
class RegisterAnalytics {
/** @var WordPress */
private $wordPress;
public function __construct(
WordPress $wordPress
) {
$this->wordPress = $wordPress;
}
public function register(): void {
$this->wordPress->addAction(Hooks::API_INITIALIZE, function (API $api) {
$api->registerPostRoute('automation/analytics/overview', OverviewEndpoint::class);
});
}
}

View File

@@ -5,6 +5,7 @@ namespace MailPoet\Automation\Integrations\MailPoet;
use MailPoet\Automation\Engine\Integration;
use MailPoet\Automation\Engine\Registry;
use MailPoet\Automation\Integrations\MailPoet\Actions\SendEmailAction;
use MailPoet\Automation\Integrations\MailPoet\Analytics\RegisterAnalytics;
use MailPoet\Automation\Integrations\MailPoet\Hooks\AutomationEditorLoadingHooks;
use MailPoet\Automation\Integrations\MailPoet\Hooks\CreateAutomationRunHook;
use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
@@ -49,6 +50,9 @@ class MailPoetIntegration implements Integration {
/** @var SubscriberSubjectToWordPressUserSubjectTransformer */
private $subscriberToWordPressUserTransformer;
/** @var RegisterAnalytics */
private $registerAnalytics;
public function __construct(
ContextFactory $contextFactory,
SegmentSubject $segmentSubject,
@@ -60,7 +64,8 @@ class MailPoetIntegration implements Integration {
UserRegistrationTrigger $userRegistrationTrigger,
SendEmailAction $sendEmailAction,
AutomationEditorLoadingHooks $automationEditorLoadingHooks,
CreateAutomationRunHook $createAutomationRunHook
CreateAutomationRunHook $createAutomationRunHook,
RegisterAnalytics $registerAnalytics
) {
$this->contextFactory = $contextFactory;
$this->segmentSubject = $segmentSubject;
@@ -73,6 +78,7 @@ class MailPoetIntegration implements Integration {
$this->sendEmailAction = $sendEmailAction;
$this->automationEditorLoadingHooks = $automationEditorLoadingHooks;
$this->createAutomationRunHook = $createAutomationRunHook;
$this->registerAnalytics = $registerAnalytics;
}
public function register(Registry $registry): void {
@@ -97,5 +103,7 @@ class MailPoetIntegration implements Integration {
$this->automationEditorLoadingHooks->init();
$this->createAutomationRunHook->init();
$this->registerAnalytics->register();
}
}

View File

@@ -3,6 +3,7 @@
namespace MailPoet\Config;
use MailPoet\AdminPages\Pages\Automation;
use MailPoet\AdminPages\Pages\AutomationAnalytics;
use MailPoet\AdminPages\Pages\AutomationEditor;
use MailPoet\AdminPages\Pages\AutomationTemplates;
use MailPoet\AdminPages\Pages\ExperimentalFeatures;
@@ -49,6 +50,7 @@ class Menu {
const LOGS_PAGE_SLUG = 'mailpoet-logs';
const AUTOMATIONS_PAGE_SLUG = 'mailpoet-automation';
const AUTOMATION_EDITOR_PAGE_SLUG = 'mailpoet-automation-editor';
const AUTOMATION_ANALYTICS_PAGE_SLUG = 'mailpoet-automation-analytics';
const AUTOMATION_TEMPLATES_PAGE_SLUG = 'mailpoet-automation-templates';
const LANDINGPAGE_PAGE_SLUG = 'mailpoet-landingpage';
@@ -507,6 +509,16 @@ class Menu {
[$this, 'automationEditor']
);
// Automation analytics
$this->wp->addSubmenuPage(
self::AUTOMATIONS_PAGE_SLUG,
$this->setPageTitle(__('Automation Analytics', 'mailpoet')),
esc_html__('Automation Analytics', 'mailpoet'),
AccessControl::PERMISSION_MANAGE_AUTOMATIONS,
self::AUTOMATION_ANALYTICS_PAGE_SLUG,
[$this, 'automationAnalytics']
);
// Automation templates
$this->wp->addSubmenuPage(
@@ -576,6 +588,10 @@ class Menu {
$this->container->get(AutomationEditor::class)->render();
}
public function automationAnalytics() {
$this->container->get(AutomationAnalytics::class)->render();
}
public function experimentalFeatures() {
$this->container->get(ExperimentalFeatures::class)->render();
}

View File

@@ -34,6 +34,7 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\AdminPages\Pages\Automation::class)->setPublic(true);
$container->autowire(\MailPoet\AdminPages\Pages\AutomationTemplates::class)->setPublic(true);
$container->autowire(\MailPoet\AdminPages\Pages\AutomationEditor::class)->setPublic(true);
$container->autowire(\MailPoet\AdminPages\Pages\AutomationAnalytics::class)->setPublic(true);
$container->autowire(\MailPoet\AdminPages\Pages\ExperimentalFeatures::class)->setPublic(true);
$container->autowire(\MailPoet\AdminPages\Pages\FormEditor::class)->setPublic(true);
$container->autowire(\MailPoet\AdminPages\Pages\Forms::class)->setPublic(true);
@@ -191,6 +192,10 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\SubjectTransformers\WordPressUserSubjectToWooCommerceCustomerSubjectTransformer::class)->setPublic(true)->setShared(false);
//Automation Analytics
$container->autowire(\MailPoet\Automation\Integrations\MailPoet\Analytics\RegisterAnalytics::class)->setPublic(true);
$container->autowire(\MailPoet\Automation\Integrations\MailPoet\Analytics\Endpoints\OverviewEndpoint::class)->setPublic(true);
$container->autowire(\MailPoet\Automation\Integrations\MailPoet\Analytics\Controller\OverviewStatisticsController::class)->setPublic(true);
// Config
$container->autowire(\MailPoet\Config\AccessControl::class)->setPublic(true);
$container->autowire(\MailPoet\Config\Activator::class)->setPublic(true);

View File

@@ -150,6 +150,22 @@ EOL;
$this->wp->wpSetScriptTranslations('automation_editor', 'mailpoet');
}
public function setupAutomationAnalyticsDependencies(): void {
$this->wp->wpEnqueueScript(
'automation_analytics',
Env::$assetsUrl . '/dist/js/' . $this->renderer->getJsAsset('automation_analytics.js'),
[],
Env::$version,
true
);
$this->wp->wpSetScriptTranslations('automation_analytics', 'mailpoet');
$this->wp->wpEnqueueStyle(
'automation_analytics',
Env::$assetsUrl . '/dist/css/' . $this->renderer->getCssAsset('mailpoet-automation-analytics.css')
);
}
public function setupAutomationTemplatesDependencies(): void {
$this->wp->wpEnqueueScript(
'automation_templates',

View File

@@ -0,0 +1,15 @@
<% extends 'layout.html' %>
<% block content %>
<div id="mailpoet_automation_analytics" class="woocommerce-page"></div>
<script type="text/javascript">
var mailpoet_locale_full = <%= json_encode(locale_full) %>;
var mailpoet_automation_api = <%= json_encode(api) %>;
var mailpoet_json_api = <%= json_encode(jsonapi) %>;
var mailpoet_automation_registry = <%= json_encode(registry) %>;
var mailpoet_automation_context = <%= json_encode(context) %>;
var mailpoet_automation = <%= automation ? json_encode(automation) : 'undefined' %>;
</script>
<% endblock %>