Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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