Skip to content

Commit 5734121

Browse files
Merge pull request #9 from matomo-org/PG-4530-alert-via-slack
Pg 4530 alert via slack
2 parents 0d29f39 + 7017116 commit 5734121

17 files changed

Lines changed: 461 additions & 21 deletions

.github/workflows/matomo-tests.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Action for running tests
22
# This file has been automatically created.
33
# To recreate it you can run this command
4-
# ./console generate:test-action --plugin="Slack" --php-versions="7.2,8.4" --schedule-cron="0 5 * * 6"
4+
# ./console generate:test-action --plugin="Slack" --php-versions="7.2,8.4" --dependent-plugins="matomo-org/plugin-CustomAlerts" --schedule-cron="0 5 * * 6"
55

66
name: Plugin Slack Tests
77

@@ -56,6 +56,9 @@ jobs:
5656
redis-service: true
5757
artifacts-pass: ${{ secrets.ARTIFACTS_PASS }}
5858
upload-artifacts: ${{ matrix.php == '7.2' && matrix.target == 'maximum_supported_matomo' }}
59+
artifacts-protected: true
60+
dependent-plugins: 'matomo-org/plugin-CustomAlerts'
61+
github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
5962
UI:
6063
runs-on: ubuntu-24.04
6164
steps:
@@ -74,3 +77,6 @@ jobs:
7477
redis-service: true
7578
artifacts-pass: ${{ secrets.ARTIFACTS_PASS }}
7679
upload-artifacts: true
80+
artifacts-protected: true
81+
dependent-plugins: 'matomo-org/plugin-CustomAlerts'
82+
github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}

EnrichTriggeredAlerts.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
namespace Piwik\Plugins\Slack;
11+
12+
use Piwik\Container\StaticContainer;
13+
use Piwik\Plugins\CustomAlerts\Controller;
14+
15+
class EnrichTriggeredAlerts extends Controller
16+
{
17+
public function __construct()
18+
{
19+
parent::__construct(StaticContainer::get('Piwik\Plugins\API\ProcessedReport'));
20+
}
21+
22+
public function enrichTriggeredAlerts($triggeredAlerts)
23+
{
24+
return parent::enrichTriggeredAlerts($triggeredAlerts);
25+
}
26+
}

Slack.php

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,35 +29,37 @@ class Slack extends Plugin
2929

3030
private static $availableParameters = array(
3131
self::SLACK_CHANNEL_ID_PARAMETER => true,
32-
ScheduledReports::EVOLUTION_GRAPH_PARAMETER => false,
33-
ScheduledReports::DISPLAY_FORMAT_PARAMETER => true,
32+
ScheduledReports::EVOLUTION_GRAPH_PARAMETER => false,
33+
ScheduledReports::DISPLAY_FORMAT_PARAMETER => true,
3434
);
3535

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

4040
private static $managedReportFormats = array(
41-
ReportRenderer::PDF_FORMAT => 'plugins/Morpheus/icons/dist/plugins/pdf.png',
42-
ReportRenderer::CSV_FORMAT => 'plugins/Morpheus/images/export.png',
43-
ReportRenderer::TSV_FORMAT => 'plugins/Morpheus/images/export.png',
41+
ReportRenderer::PDF_FORMAT => 'plugins/Morpheus/icons/dist/plugins/pdf.png',
42+
ReportRenderer::CSV_FORMAT => 'plugins/Morpheus/images/export.png',
43+
ReportRenderer::TSV_FORMAT => 'plugins/Morpheus/images/export.png',
4444
);
4545

4646
public function registerEvents()
4747
{
4848
return [
49-
'ScheduledReports.getReportParameters' => 'getReportParameters',
49+
'ScheduledReports.getReportParameters' => 'getReportParameters',
5050
'ScheduledReports.validateReportParameters' => 'validateReportParameters',
51-
'ScheduledReports.getReportMetadata' => 'getReportMetadata',
52-
'ScheduledReports.getReportTypes' => 'getReportTypes',
53-
'ScheduledReports.getReportFormats' => 'getReportFormats',
54-
'ScheduledReports.getRendererInstance' => 'getRendererInstance',
55-
'ScheduledReports.getReportRecipients' => 'getReportRecipients',
56-
'ScheduledReports.processReports' => 'processReports',
57-
'ScheduledReports.allowMultipleReports' => 'allowMultipleReports',
58-
'ScheduledReports.sendReport' => 'sendReport',
51+
'ScheduledReports.getReportMetadata' => 'getReportMetadata',
52+
'ScheduledReports.getReportTypes' => 'getReportTypes',
53+
'ScheduledReports.getReportFormats' => 'getReportFormats',
54+
'ScheduledReports.getRendererInstance' => 'getRendererInstance',
55+
'ScheduledReports.getReportRecipients' => 'getReportRecipients',
56+
'ScheduledReports.processReports' => 'processReports',
57+
'ScheduledReports.allowMultipleReports' => 'allowMultipleReports',
58+
'ScheduledReports.sendReport' => 'sendReport',
5959
'Template.reportParametersScheduledReports' => 'templateReportParametersScheduledReports',
6060
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
61+
'CustomAlerts.validateReportParameters' => 'validateCustomAlertReportParameters',
62+
'CustomAlerts.sendNewAlerts' => 'sendNewAlerts',
6163
];
6264
}
6365

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

109111
public function getReportMetadata(&$availableReportMetadata, $reportType, $idSite)
110112
{
111-
if (! self::isSlackEvent($reportType)) {
113+
if (!self::isSlackEvent($reportType)) {
112114
return;
113115
}
114116

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

141143
public function processReports(&$processedReports, $reportType, $outputType, $report)
142144
{
143-
if (! self::isSlackEvent($reportType)) {
145+
if (!self::isSlackEvent($reportType)) {
144146
return;
145147
}
146148

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

154156
public function getRendererInstance(&$reportRenderer, $reportType, $outputType, $report)
155157
{
156-
if (! self::isSlackEvent($reportType)) {
158+
if (!self::isSlackEvent($reportType)) {
157159
return;
158160
}
159161

@@ -202,7 +204,7 @@ public function sendReport(
202204
$period,
203205
$force
204206
) {
205-
if (! self::isSlackEvent($reportType)) {
207+
if (!self::isSlackEvent($reportType)) {
206208
return;
207209
}
208210
$logger = StaticContainer::get(LoggerInterface::class);
@@ -275,6 +277,75 @@ public function uninstall()
275277
return;
276278
}
277279

280+
public function validateCustomAlertReportParameters($parameters, $alertMedium)
281+
{
282+
if ($alertMedium === self::SLACK_TYPE && empty($parameters[self::SLACK_CHANNEL_ID_PARAMETER])) {
283+
throw new \Exception(Piwik::translate('Slack_SlackChannelIdRequiredErrorMessage'));
284+
}
285+
}
286+
287+
public function sendNewAlerts($triggeredAlerts): void
288+
{
289+
if (!empty($triggeredAlerts)) {
290+
$enrichTriggerAlerts = new EnrichTriggeredAlerts();
291+
$triggeredAlerts = $enrichTriggerAlerts->enrichTriggeredAlerts($triggeredAlerts);
292+
$settings = StaticContainer::get(SystemSettings::class);
293+
$token = $settings->slackOauthToken->getValue();
294+
if (empty($token)) {
295+
return;
296+
}
297+
$slackApi = new SlackApi($token);
298+
$groupedAlerts = $this->groupAlertsByChannelId($triggeredAlerts);
299+
foreach ($groupedAlerts as $slackChannelId => $alert) {
300+
if (!$slackApi->sendMessage(implode("\n", $alert['message']), $slackChannelId)) {
301+
$logger = StaticContainer::get(LoggerInterface::class);
302+
$logger->debug('Slack alert failed for following alerts: ' . implode("\n", $alert['name']));
303+
}
304+
}
305+
}
306+
}
307+
308+
private function groupAlertsByChannelId(array $alerts): array
309+
{
310+
$groupedAlerts = [];
311+
foreach ($alerts as $alert) {
312+
if (!in_array(self::SLACK_TYPE, $alert['report_mediums']) || empty($alert['slack_channel_id'])) {
313+
continue;
314+
}
315+
$metric = !empty($alert['reportMetric']) ? $alert['reportMetric'] : $alert['metric'];
316+
$reportName = !empty($alert['reportName']) ? $alert['reportName'] : $alert['report'];
317+
$groupedAlerts[$alert['slack_channel_id']]['message'][] = $this->getAlertMessage($alert, $metric, $reportName);
318+
$groupedAlerts[$alert['slack_channel_id']]['name'][] = $alert['name'];
319+
}
320+
321+
return $groupedAlerts;
322+
}
323+
324+
public function getAlertMessage(array $alert, string $metric, string $reportName): string
325+
{
326+
return Piwik::translate('Slack_SlackAlertContent', [$alert['name'], $alert['siteName'], $metric, $reportName, $this->transformAlertCondition($alert)]);
327+
}
328+
329+
private function transformAlertCondition(array $alert): string
330+
{
331+
switch ($alert['metric_condition']) {
332+
case 'less_than':
333+
return Piwik::translate('CustomAlerts_ValueIsLessThan', [$alert['metric_matched'], $alert['value_new']]);
334+
case 'greater_than':
335+
return Piwik::translate('CustomAlerts_ValueIsGreaterThan', [$alert['metric_matched'], $alert['value_new']]);
336+
case 'decrease_more_than':
337+
return Piwik::translate('CustomAlerts_ValueDecreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
338+
case 'increase_more_than':
339+
return Piwik::translate('CustomAlerts_ValueIncreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
340+
case 'percentage_decrease_more_than':
341+
return Piwik::translate('CustomAlerts_ValuePercentageDecreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
342+
case 'percentage_increase_more_than':
343+
return Piwik::translate('CustomAlerts_ValuePercentageIncreasedMoreThan', [$alert['metric_matched'], $alert['value_old'] ?? '-', $alert['value_new']]);
344+
}
345+
346+
return '';
347+
}
348+
278349
private function reportAlreadySent($report, Period $period)
279350
{
280351
$key = ScheduledReports::OPTION_KEY_LAST_SENT_DATERANGE . $report['idreport'];

SlackApi.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class SlackApi
3434

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

3839
private const SLACK_TIMEOUT = 5000;
3940

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

137-
private function sendHttpRequest(string $url, int $timeout, array $requestBody, array $additionalHeaders = [], $requestBodyAsString = false)
138+
public function sendMessage(string $message, string $channel): bool
139+
{
140+
try {
141+
$response = $this->sendHttpRequest(
142+
self::SLACK_POST_MESSAGE_URL,
143+
self::SLACK_TIMEOUT,
144+
[
145+
'token' => $this->token,
146+
'channel' => $channel,
147+
'text' => $message,
148+
],
149+
['Content-Type' => 'multipart/form-data']
150+
);
151+
} catch (\Exception $e) {
152+
$this->logger->debug('Slack error sendMessage:' . $e->getMessage());
153+
return false;
154+
}
155+
156+
$data = json_decode($response, true);
157+
158+
return !empty($data['ok']);
159+
}
160+
161+
public function sendHttpRequest(string $url, int $timeout, array $requestBody, array $additionalHeaders = [], $requestBodyAsString = false)
138162
{
139163
if ($requestBodyAsString && !empty($requestBody[0])) {
140164
$requestBody = $requestBody[0];

lang/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"PleaseFindYourReport": "Here is your %1$s report for %2$s",
99
"SlackChannelIdRequiredErrorMessage": "Slack Channel ID cannot be empty.",
1010
"SlackChannel": "Slack Channel",
11-
"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"
11+
"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",
12+
"SlackAlertContent": "%1$s has been triggered for website %2$s as the metric %3$s in report %4$s %5$s."
1213
}
1314
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
namespace Piwik\Plugins\Slack\tests;
11+
12+
use Piwik\Tests\Framework\Fixture;
13+
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
14+
use Piwik\Tests\Framework\TestingEnvironmentManipulator;
15+
use Piwik\Plugins\CustomAlerts\API;
16+
17+
/**
18+
* @group Slack
19+
* @group CustomAlertsApiTest
20+
* @group Plugins
21+
*/
22+
class CustomAlertsApiTest extends IntegrationTestCase
23+
{
24+
/**
25+
* @var \Piwik\Plugins\CustomAlerts\API
26+
*/
27+
protected $api;
28+
29+
protected $idSite;
30+
31+
public function setUp(): void
32+
{
33+
self::$fixture->extraPluginsToLoad = array('CustomAlerts');
34+
TestingEnvironmentManipulator::$extraPluginsToLoad = self::$fixture->extraPluginsToLoad;
35+
36+
parent::setUp();
37+
38+
$pluginManager = \Piwik\Plugin\Manager::getInstance();
39+
$pluginManager->loadPlugin('CustomAlerts');
40+
$pluginManager->installLoadedPlugins();
41+
$pluginManager->activatePlugin('CustomAlerts');
42+
$this->api = API::getInstance();
43+
$this->idSite = Fixture::createWebsite('2012-08-09 11:22:33');
44+
}
45+
46+
public function testAddAlertShouldThrowExceptionIfEmptySlackChannelId()
47+
{
48+
$this->expectException(\Exception::class);
49+
$this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage');
50+
$this->addAlert();
51+
}
52+
53+
public function testAddAlertSuccess()
54+
{
55+
$id = $this->addAlert($slackChannelId = 'channelID');
56+
$this->assertEquals(1, $id);
57+
$alert = $this->api->getAlert($id);
58+
$this->assertEquals(['email','slack'], $alert['report_mediums']);
59+
$this->assertEquals($slackChannelId, $alert['slack_channel_id']);
60+
}
61+
62+
public function testUpdateAlertShouldThrowExceptionIfEmptySlackChannelId()
63+
{
64+
$this->expectException(\Exception::class);
65+
$this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage');
66+
$idAlert = $this->addAlert('channelID');
67+
$this->updateAlert($idAlert);
68+
}
69+
70+
public function testUpdateAlertSuccess()
71+
{
72+
$idAlert = $this->addAlert('channelID');
73+
$this->updateAlert($idAlert, 'channelIDNew');
74+
$alert = $this->api->getAlert($idAlert);
75+
$this->assertEquals(['email','slack'], $alert['report_mediums']);
76+
$this->assertEquals('channelIDNew', $alert['slack_channel_id']);
77+
}
78+
79+
private function addAlert($slackChannelId = '')
80+
{
81+
return $this->api->addAlert(
82+
'Test Slack and Email',
83+
$this->idSite,
84+
'day',
85+
1,
86+
[],
87+
[],
88+
'nb_visits',
89+
'less_than',
90+
$metricMatched = 5,
91+
1,
92+
'MultiSites_getOne',
93+
'matches_exactly',
94+
'Piwik',
95+
['email', 'slack'],
96+
$slackChannelId
97+
);
98+
}
99+
100+
private function updateAlert($idAlert, $slackChannelId = '')
101+
{
102+
return $this->api->editAlert(
103+
$idAlert,
104+
'Test Slack and Email',
105+
$this->idSite,
106+
'day',
107+
1,
108+
[],
109+
[],
110+
'nb_visits',
111+
'less_than',
112+
$metricMatched = 5,
113+
1,
114+
'MultiSites_getOne',
115+
'matches_exactly',
116+
'Piwik',
117+
['email', 'slack'],
118+
$slackChannelId
119+
);
120+
}
121+
}

0 commit comments

Comments
 (0)