magendoo / module-merchant-pulse
Magendoo MerchantPulse — Store Health & Insights: scheduled digest emails and proactive alerts for Magento 2 merchants.
Package info
github.com/magendooro/magendoo-m2-merchantpulse
Type:magento2-module
pkg:composer/magendoo/module-merchant-pulse
Fund package maintenance!
Requires
- php: ~8.2|~8.3|~8.4
- magento/framework: *
- magento/module-backend: *
- magento/module-catalog: *
- magento/module-config: *
- magento/module-cron: *
- magento/module-email: *
- magento/module-indexer: *
- magento/module-sales: *
- magento/module-store: *
README
Store Health & Insights for Magento 2
Automated digest emails and proactive health alerts that keep you informed about what's happening in your store — catalog issues, system health, and recent activity — without having to log into the admin every day.
Screenshots
Digest email — health checks grouped into sections (Catalog Quality, Catalog Health, Index & Cron Health, Store Activity) with severity-coloured statuses, delivered on your schedule.
Health Checks — review all 11 checks and run any of them on demand from the admin.
Health Alerts — proactive, throttled alerts with a new → acknowledged → resolved lifecycle.
Features
Scheduled Digest Reports
- Daily, weekly, or monthly digest emails summarising store health
- Configurable delivery time and day (weekly: day of week, monthly: day of month)
- Four digest sections: Catalog Quality, Catalog Health, Index & Cron Health, Store Activity
- HTML email rendered through Magento's email templates and sent via
TransportBuilder - Each report saved to the database with full rendered HTML and JSON payload for audit
11 Health Checks
| Category | Code | Severity | What It Checks |
|---|---|---|---|
| Catalog | stale_price |
warning | Products whose catalog_product_entity.updated_at is older than N days. Caveat: this reflects the product's last edit, not price-change history — EAV value tables carry no timestamps, so a true "price hasn't changed" signal is not available. |
| Catalog | missing_images |
warning | Enabled products with no base image attribute (unset or no_selection), evaluated at store scope |
| Catalog | missing_description |
info | Enabled products with an empty description, evaluated at store scope |
| Catalog | no_categories |
info | Enabled products not assigned to any category |
| Catalog | zero_price |
critical | Enabled simple/virtual products with price ≤ 0 |
| Catalog | expired_special_price |
warning | Products with a special_price set whose special_to_date is before today (UTC) |
| Catalog | duplicate_sku |
critical | Duplicate SKUs in the catalog |
| Catalog | disabled_in_promo |
warning | Disabled products referenced by an active catalog price rule (via catalogrule_product). Cart price rules (salesrule) target products through conditions rather than an enumerated product list and are not covered. |
| System | index_health |
critical | Indexers left in the invalid state (the transient working state is ignored) |
| System | cron_health |
warning | Cron jobs in an error / missed / stuck-running state within the configured Cron Lag window |
| Activity | products_added |
info | Count of products created during the digest period |
Proactive Alerts
- Severity levels:
info,warning,critical(a plain string on each check) - Alert throttling to prevent notification floods (configurable hours)
- Alerts stored in DB with full lifecycle:
new → acknowledged → resolved - Newly fired alerts are also emailed to the configured recipients by the realtime cron
- Admin grid with mass acknowledge / mass resolve actions
Admin UI
- Merchant Pulse top-level menu in Magento Admin
- Digest Reports grid with HTML preview
- Health Alerts grid with severity filter and workflow actions
- Health Checks page showing live check results
- Full system configuration under Stores > Configuration > Magendoo > Merchant Pulse
Email Delivery
Emails are built and sent through Magento's own mail stack: Magento\Framework\Mail\Template\TransportBuilder renders the registered email templates (etc/email_templates.xml → view/adminhtml/email/digest.html and alert.html) and hands the message to the store's configured Magento mail transport. Because CLI and cron have no active area, the send is wrapped in store and admin-area emulation so the template loader and TransportBuilder have the store, locale, design, and area code they require. Delivery therefore honours whatever mail transport the Magento instance is configured to use — no custom SMTP client is involved.
Requirements
- Magento Open Source / Adobe Commerce 2.4.6+ (tested on 2.4.9)
- PHP 8.2, 8.3, or 8.4
Installation
Via Composer
composer require magendoo/module-merchant-pulse bin/magento module:enable Magendoo_MerchantPulse bin/magento setup:upgrade bin/magento setup:di:compile bin/magento cache:flush
Manual
- Copy the module directory to
app/code/Magendoo/MerchantPulse/ - Run:
bin/magento module:enable Magendoo_MerchantPulse bin/magento setup:upgrade bin/magento setup:di:compile bin/magento cache:flush
Configuration
Navigate to Stores > Configuration > Magendoo > Merchant Pulse.
General
| Field | Default | Description |
|---|---|---|
| Enabled | No | Enable/disable the module |
| Digest Timezone | UTC | Timezone used for scheduling digest delivery. Leave empty to fall back to the store's locale timezone. |
Digest Schedule
| Field | Default | Description |
|---|---|---|
| Frequency | Daily | daily / weekly / monthly |
| Delivery Time | 08:00 | Time of day to send the digest (stored as 08,00,00) |
| Day of Week | Monday | (weekly only) Which day to send |
| Day of Month | 1 | (monthly only) Which day to send |
| Digest Sections | catalog,catalog_health,index_health,activity |
Which sections to include |
Recipients
| Field | Description |
|---|---|
| Recipient Email Addresses | One email address per line (commas are also accepted) |
Alerts
| Field | Default | Description |
|---|---|---|
| Enable Alerts | Yes | Enable real-time check alerts |
| Alert Throttle (Hours) | 24 | Minimum hours between repeat alerts for the same check |
Thresholds
| Field | Default | Description |
|---|---|---|
| Stale Products (Days) | 180 | Days without a product update before it is flagged |
| Cron Lag Warning (Minutes) | 60 | Minutes overdue before cron is considered unhealthy |
Quick local setup
bin/magento config:set merchant_pulse/general/enabled 1 bin/magento config:set merchant_pulse/recipients/email_list your@email.com bin/magento cache:flush
Mail is delivered by Magento's configured transport; point that transport at a local mail catcher (MailHog / Mailpit) if you want to inspect messages during development.
CLI Commands
# Run all enabled checks and generate a digest report bin/magento pulse:run --store=1 --type=daily # Run without sending (saves report to DB, no email) bin/magento pulse:run --store=1 --type=daily --dry-run # Override the recipient for this run only bin/magento pulse:run --store=1 --type=daily --email=you@example.com # Send a digest with fine-grained period control (from/to, since-last-run, force) bin/magento pulse:digest --store=1 --type=weekly --since-last-run # Run a single check and print its result bin/magento pulse:check zero_price --store=1 bin/magento pulse:check index_health --store=1 --format=json # List all registered checks with severity and enabled status bin/magento pulse:list-checks --store=1 # Send a test digest; --email overrides the configured recipients bin/magento pulse:test --email=you@example.com --store=1
--email on pulse:run and pulse:test genuinely overrides the recipient list (it is passed through to DigestService::run() as the $recipientsOverride argument). pulse:test reports a non-zero exit and the stored error message when the send fails, rather than always claiming success.
Database Tables
| Table | Purpose |
|---|---|
pulse_report |
Digest run records — stores status, rendered HTML, and JSON payload |
pulse_alert |
Real-time health alerts with lifecycle tracking |
pulse_digest_log |
Scheduling log — records the period window and the last successful send timestamp |
Cron Jobs
The module ships its own cron group (etc/cron_groups.xml) so the custom group is scheduled independently of the default group.
| Job | Schedule | Description |
|---|---|---|
merchantpulse_digest_dispatcher |
Hourly | Checks whether a digest is due (delivery hour/day evaluated in the configured Digest Timezone) and dispatches it |
merchantpulse_realtime_checks |
Every 15 min | Runs the checks, upserts alerts, and emails newly fired alerts |
Extending with Custom Checks
Implement Magendoo\MerchantPulse\Api\CheckInterface and register the check in the CheckPool via di.xml:
// app/code/Vendor/Module/Model/Check/MyCustomCheck.php namespace Vendor\Module\Model\Check; use Magendoo\MerchantPulse\Api\CheckInterface; use Magendoo\MerchantPulse\Api\Data\CheckResultInterface; use Magendoo\MerchantPulse\Model\CheckResult; class MyCustomCheck implements CheckInterface { public function getCode(): string { return 'my_custom_check'; } public function getLabel(): string { return 'My Custom Check'; } public function getSeverity(): string { return 'warning'; } // 'info' | 'warning' | 'critical' public function isEnabled(int $storeId): bool { return true; } public function execute(int $storeId, \DateTimeInterface $from, \DateTimeInterface $to): CheckResultInterface { // ... your logic ... $failingItems = []; // rows/details for the digest and alert $passed = $failingItems === []; // CheckResult(string $checkCode, string $severity, bool $passed, // int $count = 0, array $items = [], string $message = '', array $details = []) return new CheckResult( 'my_custom_check', 'warning', $passed, count($failingItems), $failingItems, $passed ? 'All good.' : count($failingItems) . ' item(s) need attention.' ); } }
<!-- app/code/Vendor/Module/etc/di.xml --> <type name="Magendoo\MerchantPulse\Model\Check\CheckPool"> <arguments> <argument name="checks" xsi:type="array"> <item name="my_custom_check" xsi:type="object">Vendor\Module\Model\Check\MyCustomCheck</item> </argument> </arguments> </type>
Severity is a plain string — there are no SEVERITY_* or STATUS_* constants, and there is no getCategory() method.
Adding a Notification Channel
Only the Email channel ships with the module. To deliver digests and alerts elsewhere, implement Magendoo\MerchantPulse\Api\NotificationChannelInterface and register it in the ChannelPool:
interface NotificationChannelInterface { public function sendDigest(string $renderedHtml, string $subject, array $recipients, int $storeId): void; public function sendAlert(array $alertData, array $recipients, int $storeId): void; public function getCode(): string; public function getLabel(): string; public function isAvailable(int $storeId): bool; }
<!-- app/code/Vendor/Module/etc/di.xml --> <type name="Magendoo\MerchantPulse\Model\Channel\ChannelPool"> <arguments> <argument name="channels" xsi:type="array"> <item name="my_channel" xsi:type="object">Vendor\Module\Model\Channel\MyChannel</item> </argument> </arguments> </type>
Testing
The module ships a PHPUnit unit-test suite under Test/Unit/ covering the checks, the report builder/renderer/period calculator, the digest service, the email channel, the alert dispatcher, the repositories, and the config source models.
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist app/code/Magendoo/MerchantPulse/Test/Unit
A set of legacy end-to-end Playwright specs also lives under Test/functional/ (cli.spec.js, admin.spec.js, email.spec.js). They exercise the CLI, admin grids, and captured email, and require a running Magento instance plus a local mail catcher (MailHog/Mailpit) — they are not part of the unit run.
Architecture
Magendoo_MerchantPulse/
├── Api/ # Service contracts (interfaces)
│ ├── CheckInterface.php
│ ├── NotificationChannelInterface.php
│ ├── ReportRepositoryInterface.php
│ ├── AlertRepositoryInterface.php
│ └── Data/
├── Model/
│ ├── Check/
│ │ ├── AbstractCheck.php
│ │ ├── CheckPool.php # DI-injected pool of all checks
│ │ ├── Catalog/ # 8 catalog health checks
│ │ ├── System/ # 2 system checks (indexer, cron)
│ │ └── Activity/ # 1 activity check (products added)
│ ├── Channel/
│ │ ├── ChannelPool.php # DI-injected pool of notification channels
│ │ └── EmailChannel.php # TransportBuilder + email templates, under store/area emulation
│ ├── Report/
│ │ ├── Builder.php # Runs checks, assembles payload
│ │ ├── Renderer.php # Renders payload to HTML email body
│ │ └── PeriodCalculator.php # Computes from/to window from digest log
│ ├── Alert/
│ │ └── Dispatcher.php # Throttle-aware alert upsert
│ ├── Service/
│ │ └── DigestService.php # Orchestrates build → render → save → send → log
│ └── Config.php # All system config getters
├── Cron/
│ ├── DigestDispatcher.php # Hourly — checks if a digest is due (Digest Timezone)
│ └── RunRealtimeChecks.php # Every 15 min — fires and emails alerts
├── Console/Command/ # 5 CLI commands
├── Controller/Adminhtml/ # Admin controllers
├── Ui/ # UI component data providers + column renderers
└── view/adminhtml/
├── ui_component/ # Grid XML definitions
├── layout/ # Admin page layouts
└── email/ # digest.html + alert.html templates
Key Design Decisions
-
Magento email templates via
TransportBuilder— digests and alerts render through registered.htmltemplates and are sent by the store's configured Magento mail transport. Because CLI/cron have no active area, the send is wrapped in admin-area code emulation plus store environment emulation so the template loader andTransportBuilderhave a store, locale, design, and area code to work with. -
CheckPool / ChannelPool pattern — both pools are DI-injected arrays, making it trivial to add checks or channels from any third-party module without touching core code.
-
Period calculation from
pulse_digest_log— thePeriodCalculatorreads the last successful send timestamp to compute a dynamicfrom/towindow, so activity is not missed even when a cron run is skipped. -
Alert throttling —
Alert\Dispatchersuppresses re-firing the same check alert for a configurable number of hours, preventing notification floods during extended incidents. -
Reports persist rendered HTML — the
pulse_reporttable stores both the JSON payload and the final rendered HTML, so the admin can view exactly what was emailed at any time.
Licence
Open Software Licence 3.0 (OSL-3.0)
Author
Magendoo — florinel.chis@gmail.com


