Skip to content
Merged
8 changes: 7 additions & 1 deletion .github/workflows/matomo-tests.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Action for running tests
# This file has been automatically created.
# To recreate it you can run this command
# ./console generate:test-action --plugin="Slack" --php-versions="7.2,8.4" --schedule-cron="0 5 * * 6"
# ./console generate:test-action --plugin="Slack" --php-versions="7.2,8.4" --dependent-plugins="matomo-org/plugin-CustomAlerts" --schedule-cron="0 5 * * 6"

name: Plugin Slack Tests

Expand Down Expand Up @@ -56,6 +56,9 @@ jobs:
redis-service: true
artifacts-pass: ${{ secrets.ARTIFACTS_PASS }}
upload-artifacts: ${{ matrix.php == '7.2' && matrix.target == 'maximum_supported_matomo' }}
artifacts-protected: true
dependent-plugins: 'matomo-org/plugin-CustomAlerts'
github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
UI:
runs-on: ubuntu-24.04
steps:
Expand All @@ -74,3 +77,6 @@ jobs:
redis-service: true
artifacts-pass: ${{ secrets.ARTIFACTS_PASS }}
upload-artifacts: true
artifacts-protected: true
dependent-plugins: 'matomo-org/plugin-CustomAlerts'
github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
26 changes: 26 additions & 0 deletions EnrichTriggeredAlerts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Slack;

use Piwik\Container\StaticContainer;
use Piwik\Plugins\CustomAlerts\Controller;

class EnrichTriggeredAlerts extends Controller
{
public function __construct()
{
parent::__construct(StaticContainer::get('Piwik\Plugins\API\ProcessedReport'));
}

public function enrichTriggeredAlerts($triggeredAlerts)
{
return parent::enrichTriggeredAlerts($triggeredAlerts);
}
}
107 changes: 89 additions & 18 deletions Slack.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,37 @@ class Slack extends Plugin

private static $availableParameters = array(
self::SLACK_CHANNEL_ID_PARAMETER => true,
ScheduledReports::EVOLUTION_GRAPH_PARAMETER => false,
ScheduledReports::DISPLAY_FORMAT_PARAMETER => true,
ScheduledReports::EVOLUTION_GRAPH_PARAMETER => false,
ScheduledReports::DISPLAY_FORMAT_PARAMETER => true,
);

private static $managedReportTypes = array(
self::SLACK_TYPE => 'plugins/Slack/images/slack.png'
);

private static $managedReportFormats = array(
ReportRenderer::PDF_FORMAT => 'plugins/Morpheus/icons/dist/plugins/pdf.png',
ReportRenderer::CSV_FORMAT => 'plugins/Morpheus/images/export.png',
ReportRenderer::TSV_FORMAT => 'plugins/Morpheus/images/export.png',
ReportRenderer::PDF_FORMAT => 'plugins/Morpheus/icons/dist/plugins/pdf.png',
ReportRenderer::CSV_FORMAT => 'plugins/Morpheus/images/export.png',
ReportRenderer::TSV_FORMAT => 'plugins/Morpheus/images/export.png',
);

public function registerEvents()
{
return [
'ScheduledReports.getReportParameters' => 'getReportParameters',
'ScheduledReports.getReportParameters' => 'getReportParameters',
'ScheduledReports.validateReportParameters' => 'validateReportParameters',
'ScheduledReports.getReportMetadata' => 'getReportMetadata',
'ScheduledReports.getReportTypes' => 'getReportTypes',
'ScheduledReports.getReportFormats' => 'getReportFormats',
'ScheduledReports.getRendererInstance' => 'getRendererInstance',
'ScheduledReports.getReportRecipients' => 'getReportRecipients',
'ScheduledReports.processReports' => 'processReports',
'ScheduledReports.allowMultipleReports' => 'allowMultipleReports',
'ScheduledReports.sendReport' => 'sendReport',
'ScheduledReports.getReportMetadata' => 'getReportMetadata',
'ScheduledReports.getReportTypes' => 'getReportTypes',
'ScheduledReports.getReportFormats' => 'getReportFormats',
'ScheduledReports.getRendererInstance' => 'getRendererInstance',
'ScheduledReports.getReportRecipients' => 'getReportRecipients',
'ScheduledReports.processReports' => 'processReports',
'ScheduledReports.allowMultipleReports' => 'allowMultipleReports',
'ScheduledReports.sendReport' => 'sendReport',
'Template.reportParametersScheduledReports' => 'templateReportParametersScheduledReports',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
'CustomAlerts.validateReportParameters' => 'validateCustomAlertReportParameters',
'CustomAlerts.sendNewAlerts' => 'sendNewAlerts',
];
}

Expand Down Expand Up @@ -108,7 +110,7 @@ public function validateReportParameters(&$parameters, $reportType)

public function getReportMetadata(&$availableReportMetadata, $reportType, $idSite)
{
if (! self::isSlackEvent($reportType)) {
if (!self::isSlackEvent($reportType)) {
return;
}

Expand Down Expand Up @@ -140,7 +142,7 @@ public function getReportParameters(&$availableParameters, $reportType)

public function processReports(&$processedReports, $reportType, $outputType, $report)
{
if (! self::isSlackEvent($reportType)) {
if (!self::isSlackEvent($reportType)) {
return;
}

Expand All @@ -153,7 +155,7 @@ public function processReports(&$processedReports, $reportType, $outputType, $re

public function getRendererInstance(&$reportRenderer, $reportType, $outputType, $report)
{
if (! self::isSlackEvent($reportType)) {
if (!self::isSlackEvent($reportType)) {
return;
}

Expand Down Expand Up @@ -202,7 +204,7 @@ public function sendReport(
$period,
$force
) {
if (! self::isSlackEvent($reportType)) {
if (!self::isSlackEvent($reportType)) {
return;
}
$logger = StaticContainer::get(LoggerInterface::class);
Expand Down Expand Up @@ -275,6 +277,75 @@ public function uninstall()
return;
}

public function validateCustomAlertReportParameters($parameters, $alertMedium)
{
if ($alertMedium === self::SLACK_TYPE && empty($parameters[self::SLACK_CHANNEL_ID_PARAMETER])) {
throw new \Exception(Piwik::translate('Slack_SlackChannelIdRequiredErrorMessage'));
}
}

public function sendNewAlerts($triggeredAlerts): void
{
if (!empty($triggeredAlerts)) {
$enrichTriggerAlerts = new EnrichTriggeredAlerts();
$triggeredAlerts = $enrichTriggerAlerts->enrichTriggeredAlerts($triggeredAlerts);
$settings = StaticContainer::get(SystemSettings::class);
$token = $settings->slackOauthToken->getValue();
if (empty($token)) {
return;
}
$slackApi = new SlackApi($token);
$groupedAlerts = $this->groupAlertsByChannelId($triggeredAlerts);
foreach ($groupedAlerts as $slackChannelId => $alert) {
if (!$slackApi->sendMessage(implode("\n", $alert['message']), $slackChannelId)) {
$logger = StaticContainer::get(LoggerInterface::class);
$logger->debug('Slack alert failed for following alerts: ' . implode("\n", $alert['name']));
}
}
}
}

private function groupAlertsByChannelId(array $alerts): array
{
$groupedAlerts = [];
foreach ($alerts as $alert) {
if (!in_array(self::SLACK_TYPE, $alert['report_mediums']) || empty($alert['slack_channel_id'])) {
continue;
}
$metric = !empty($alert['reportMetric']) ? $alert['reportMetric'] : $alert['metric'];
$reportName = !empty($alert['reportName']) ? $alert['reportName'] : $alert['report'];
$groupedAlerts[$alert['slack_channel_id']]['message'][] = $this->getAlertMessage($alert, $metric, $reportName);
$groupedAlerts[$alert['slack_channel_id']]['name'][] = $alert['name'];
}

return $groupedAlerts;
}

public function getAlertMessage(array $alert, string $metric, string $reportName): string
{
return Piwik::translate('Slack_SlackAlertContent', [$alert['name'], $alert['siteName'], $metric, $reportName, $this->transformAlertCondition($alert)]);
}

private function transformAlertCondition(array $alert): string
{
switch ($alert['metric_condition']) {
case 'less_than':
return Piwik::translate('CustomAlerts_ValueIsLessThan', [$alert['metric_matched'], $alert['value_new']]);
case 'greater_than':
return Piwik::translate('CustomAlerts_ValueIsGreaterThan', [$alert['metric_matched'], $alert['value_new']]);
case 'decrease_more_than':
return Piwik::translate('CustomAlerts_ValueDecreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
case 'increase_more_than':
return Piwik::translate('CustomAlerts_ValueIncreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
case 'percentage_decrease_more_than':
return Piwik::translate('CustomAlerts_ValuePercentageDecreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
case 'percentage_increase_more_than':
return Piwik::translate('CustomAlerts_ValuePercentageIncreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
}

return '';
}

private function reportAlreadySent($report, Period $period)
{
$key = ScheduledReports::OPTION_KEY_LAST_SENT_DATERANGE . $report['idreport'];
Expand Down
26 changes: 25 additions & 1 deletion SlackApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class SlackApi

private const SLACK_UPLOAD_URL_EXTERNAL = 'https://slack.com/api/files.getUploadURLExternal';
private const SLACK_COMPLETE_UPLOAD_EXTERNAL = 'https://slack.com/api/files.completeUploadExternal';
private const SLACK_POST_MESSAGE_URL = 'https://slack.com/api/chat.postMessage';

private const SLACK_TIMEOUT = 5000;

Expand Down Expand Up @@ -134,7 +135,30 @@ public function completeUploadExternal(string $channel, string $subject): bool
return !empty($data['ok']);
}

private function sendHttpRequest(string $url, int $timeout, array $requestBody, array $additionalHeaders = [], $requestBodyAsString = false)
public function sendMessage(string $message, string $channel): bool
{
try {
$response = $this->sendHttpRequest(
self::SLACK_POST_MESSAGE_URL,
self::SLACK_TIMEOUT,
[
'token' => $this->token,
'channel' => $channel,
'text' => $message,
],
['Content-Type' => 'multipart/form-data']
);
} catch (\Exception $e) {
$this->logger->debug('Slack error sendMessage:' . $e->getMessage());
return false;
}

$data = json_decode($response, true);

return !empty($data['ok']);
}

public function sendHttpRequest(string $url, int $timeout, array $requestBody, array $additionalHeaders = [], $requestBodyAsString = false)
{
if ($requestBodyAsString && !empty($requestBody[0])) {
$requestBody = $requestBody[0];
Expand Down
3 changes: 2 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"PleaseFindYourReport": "Here is your %1$s report for %2$s",
"SlackChannelIdRequiredErrorMessage": "Slack Channel ID cannot be empty.",
"SlackChannel": "Slack Channel",
"SlackEnterYourSlackChannelIdHelpText": "Enter the Slack Channel ID of the channel that will receive these reports. To find the ID, go to Slack and open the channel details > About tab. %1$sLearn more%2$s"
"SlackEnterYourSlackChannelIdHelpText": "Enter the Slack Channel ID of the channel that will receive these reports. To find the ID, go to Slack and open the channel details > About tab. %1$sLearn more%2$s",
"SlackAlertContent": "%1$s has been triggered for website %2$s as the metric %3$s in report %4$s %5$s."
}
}
121 changes: 121 additions & 0 deletions tests/Integration/CustomAlertsApiTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Slack\tests;

use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
use Piwik\Tests\Framework\TestingEnvironmentManipulator;
use Piwik\Plugins\CustomAlerts\API;

/**
* @group Slack
* @group CustomAlertsApiTest
* @group Plugins
*/
class CustomAlertsApiTest extends IntegrationTestCase
{
/**
* @var \Piwik\Plugins\CustomAlerts\API
*/
protected $api;

protected $idSite;

public function setUp(): void
{
self::$fixture->extraPluginsToLoad = array('CustomAlerts');
TestingEnvironmentManipulator::$extraPluginsToLoad = self::$fixture->extraPluginsToLoad;

parent::setUp();

$pluginManager = \Piwik\Plugin\Manager::getInstance();
$pluginManager->loadPlugin('CustomAlerts');
$pluginManager->installLoadedPlugins();
$pluginManager->activatePlugin('CustomAlerts');
$this->api = API::getInstance();
$this->idSite = Fixture::createWebsite('2012-08-09 11:22:33');
}

public function testAddAlertShouldThrowExceptionIfEmptySlackChannelId()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage');
$this->addAlert();
}

public function testAddAlertSuccess()
{
$id = $this->addAlert($slackChannelId = 'channelID');
$this->assertEquals(1, $id);
$alert = $this->api->getAlert($id);
$this->assertEquals(['email','slack'], $alert['report_mediums']);
$this->assertEquals($slackChannelId, $alert['slack_channel_id']);
}

public function testUpdateAlertShouldThrowExceptionIfEmptySlackChannelId()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage');
$idAlert = $this->addAlert('channelID');
$this->updateAlert($idAlert);
}

public function testUpdateAlertSuccess()
{
$idAlert = $this->addAlert('channelID');
$this->updateAlert($idAlert, 'channelIDNew');
$alert = $this->api->getAlert($idAlert);
$this->assertEquals(['email','slack'], $alert['report_mediums']);
$this->assertEquals('channelIDNew', $alert['slack_channel_id']);
}

private function addAlert($slackChannelId = '')
{
return $this->api->addAlert(
'Test Slack and Email',
$this->idSite,
'day',
1,
[],
[],
'nb_visits',
'less_than',
$metricMatched = 5,
1,
'MultiSites_getOne',
'matches_exactly',
'Piwik',
['email', 'slack'],
$slackChannelId
);
}

private function updateAlert($idAlert, $slackChannelId = '')
{
return $this->api->editAlert(
$idAlert,
'Test Slack and Email',
$this->idSite,
'day',
1,
[],
[],
'nb_visits',
'less_than',
$metricMatched = 5,
1,
'MultiSites_getOne',
'matches_exactly',
'Piwik',
['email', 'slack'],
$slackChannelId
);
}
}
Loading