Skip to content

Commit

Permalink
Implement telemetry (#234)
Browse files Browse the repository at this point in the history
* Implement telemetry settings (#225)

* Implement telemetry

* Show telemetry documentation link

* Implement telemetry send command and cron

* Simplify `send` method

* Extend telemetry model

* Simplify config route; Add link to config form

* Use JsonSerializable and MessageCountAwareInterface

* Fix downloads fields

* Limit requests to date

* Add `technical_email` to instance config

* Handle null values; Fix tests

* Revert technical email

* Use JsonSerializable

* Update sqls

* Change status code and sync packages count

* Improve tests

* Fix config controller; Use constants

* Update tests and docsUrl
  • Loading branch information
karniv00l authored Jul 24, 2020
1 parent aec3462 commit ce8e749
Show file tree
Hide file tree
Showing 33 changed files with 1,115 additions and 9 deletions.
8 changes: 8 additions & 0 deletions ansible/roles/cron/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@
minute=0
hour="*/2"
job="{{ app_root }}/bin/console repman:security:update-db >>{{ app_root }}/var/log/cron.log 2>&1"

- name: Add telemetry send command to cron
cron:
name="Send telemetry data"
user="{{ system_user }}"
minute=0
hour=0
job="{{ app_root }}/bin/console repman:telemetry:send >>{{ app_root }}/var/log/cron.log 2>&1"
6 changes: 6 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ parameters:
router.request_context.host: '%env(default:domain:APP_PUBLIC_HOST)%'
security_advisories_db_dir: '%env(resolve:SECURITY_ADVISORIES_DB_DIR)%'
security_advisories_db_repo: 'https://github.com/FriendsOfPHP/security-advisories.git'
instance_id_file: '%kernel.project_dir%/var/instance-id'

services:
# default configuration for services in *this* file
Expand All @@ -26,6 +27,7 @@ services:
$resetPasswordTokenTtl: 86400 # 24h
Symfony\Component\HttpFoundation\Session\Session $session: '@session'
$proxyFilesystem: '@proxy.storage'
$instanceIdFile: '%instance_id_file%'

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Expand Down Expand Up @@ -93,6 +95,10 @@ services:
$databaseDir: '%security_advisories_db_dir%'
$databaseRepo: '%security_advisories_db_repo%'

Buddy\Repman\Service\Telemetry:
arguments:
$failedTransport: '@messenger.transport.failed'

### Vendor
Github\Client:
arguments:
Expand Down
4 changes: 4 additions & 0 deletions config/services_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ parameters:
repo_dir: '%kernel.project_dir%/tests/Resources'
security_advisories_db_dir: '%kernel.project_dir%/tests/Resources/fixtures/security/security-advisories'
security_advisories_db_repo: 'bogus'
instance_id_file: '%kernel.cache_dir%/test-instance-id'

services:
Buddy\Repman\Service\Downloader:
Expand Down Expand Up @@ -36,3 +37,6 @@ services:

Buddy\Repman\Service\Security\SecurityChecker:
class: Buddy\Repman\Tests\Doubles\FakeSecurityChecker

Buddy\Repman\Service\Telemetry\TelemetryEndpoint:
class: Buddy\Repman\Tests\Doubles\FakeTelemetryEndpoint
1 change: 1 addition & 0 deletions docker/crontabs/root
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*/5 * * * * /app/bin/console repman:proxy:sync-releases
*/6 * * * * /app/bin/console repman:proxy:sync-metadata
0 */2 * * * /app/bin/console repman:security:update-db
0 0 * * * /app/bin/console repman:telemetry:send
24 changes: 23 additions & 1 deletion src/Command/CreateAdminCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,29 @@

namespace Buddy\Repman\Command;

use Buddy\Repman\Message\Admin\ChangeConfig;
use Buddy\Repman\Message\User\CreateUser;
use Buddy\Repman\Service\Config;
use Buddy\Repman\Service\Telemetry;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Messenger\MessageBusInterface;

final class CreateAdminCommand extends Command
{
private MessageBusInterface $bus;
private Telemetry $telemetry;

public function __construct(MessageBusInterface $bus)
public function __construct(MessageBusInterface $bus, Telemetry $telemetry)
{
$this->bus = $bus;
$this->telemetry = $telemetry;

parent::__construct();
}

Expand Down Expand Up @@ -53,6 +60,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
['ROLE_ADMIN']
));

if (!$this->telemetry->isInstanceIdPresent()) {
$question = new ConfirmationQuestion(
"Allow for sending anonymous usage statistic? [{$this->telemetry->docsUrl()}] (y/n)",
true
);

if ($this->getHelper('question')->ask($input, $output, $question) === true) {
$this->bus->dispatch(new ChangeConfig([
Config::TELEMETRY => Config::TELEMETRY_ENABLED,
]));
}

$this->telemetry->generateInstanceId();
}

$output->writeln(sprintf('Created admin user with id: %s', $id));

return 0;
Expand Down
51 changes: 51 additions & 0 deletions src/Command/SendTelemetryCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Command;

use Buddy\Repman\Service\Config;
use Buddy\Repman\Service\Telemetry;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class SendTelemetryCommand extends Command
{
private Config $config;
private Telemetry $telemetry;

public function __construct(Config $config, Telemetry $telemetry)
{
$this->config = $config;
$this->telemetry = $telemetry;

parent::__construct();
}

/**
* @return void
*/
protected function configure()
{
$this
->setName('repman:telemetry:send')
->setDescription('Send telemetry data');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->telemetry->isInstanceIdPresent()) {
return 0;
}

if (!$this->config->telemetryEnabled()) {
return 0;
}

$this->telemetry
->collectAndSend((new \DateTimeImmutable())->modify('-1 day'));

return 0;
}
}
20 changes: 19 additions & 1 deletion src/Controller/Admin/ConfigController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Buddy\Repman\Form\Type\Admin\ConfigType;
use Buddy\Repman\Message\Admin\ChangeConfig;
use Buddy\Repman\Service\Config;
use Buddy\Repman\Service\Telemetry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -15,10 +16,12 @@
final class ConfigController extends AbstractController
{
private Config $config;
private Telemetry $telemetry;

public function __construct(Config $config)
public function __construct(Config $config, Telemetry $telemetry)
{
$this->config = $config;
$this->telemetry = $telemetry;
}

/**
Expand All @@ -39,4 +42,19 @@ public function edit(Request $request): Response
'form' => $form->createView(),
]);
}

/**
* @Route("/admin/config/telemetry", name="admin_config_toggle_telemetry", methods={"POST","DELETE"})
*/
public function toggleTelemetry(Request $request): Response
{
$this->telemetry->generateInstanceId();
$this->dispatchMessage(new ChangeConfig([
Config::TELEMETRY => $request->isMethod(Request::METHOD_POST)
? Config::TELEMETRY_ENABLED
: Config::TELEMETRY_DISABLED,
]));

return $this->redirectToRoute('index');
}
}
15 changes: 14 additions & 1 deletion src/Controller/IndexController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@

namespace Buddy\Repman\Controller;

use Buddy\Repman\Service\Telemetry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

final class IndexController extends AbstractController
{
private Telemetry $telemetry;

public function __construct(Telemetry $telemetry)
{
$this->telemetry = $telemetry;
}

/**
* @Route(path="/", name="index", methods={"GET"})
*/
public function index(): Response
{
return $this->render('index.html.twig');
$showTelemetryPrompt = !$this->telemetry->isInstanceIdPresent();

return $this->render('index.html.twig', [
'showTelemetryPrompt' => $showTelemetryPrompt,
'telemetryDocsUrl' => $this->telemetry->docsUrl(),
]);
}
}
20 changes: 20 additions & 0 deletions src/Form/Type/Admin/ConfigType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@

namespace Buddy\Repman\Form\Type\Admin;

use Buddy\Repman\Service\Config;
use Buddy\Repman\Service\Telemetry;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class ConfigType extends AbstractType
{
private Telemetry $telemetry;

public function __construct(Telemetry $telemetry)
{
$this->telemetry = $telemetry;
}

public function getBlockPrefix(): string
{
return '';
Expand Down Expand Up @@ -45,6 +54,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'data-style' => 'btn-secondary',
],
])
->add(Config::TELEMETRY, ChoiceType::class, [
'choices' => [
Config::TELEMETRY_ENABLED => Config::TELEMETRY_ENABLED,
Config::TELEMETRY_DISABLED => Config::TELEMETRY_DISABLED,
],
'help' => "Enable collecting and sending anonymous usage data (<a href=\"{$this->telemetry->docsUrl()}\" target=\"_blank\" rel=\"noopener noreferrer\">more info</a>)",
'attr' => [
'class' => 'form-control selectpicker',
'data-style' => 'btn-secondary',
],
])
->add('save', SubmitType::class, ['label' => 'Save'])
;
}
Expand Down
35 changes: 35 additions & 0 deletions src/Migrations/Version20200723105216.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200723105216 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

$this->addSql("INSERT INTO config (key, value) VALUES ('telemetry', 'disabled')");
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

$this->addSql("DELETE FROM config WHERE key IN ('telemetry'");
}
}
31 changes: 31 additions & 0 deletions src/Query/Admin/TelemetryQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Query\Admin;

use Buddy\Repman\Service\Telemetry\Entry\Organization;
use Buddy\Repman\Service\Telemetry\Entry\Package;

interface TelemetryQuery
{
/**
* @return Organization[]
*/
public function organizations(int $limit = 100, int $offset = 0): array;

public function organizationsCount(): int;

/**
* @return Package[]
*/
public function packages(string $organizationId, \DateTimeImmutable $till, int $limit = 100, int $offset = 0): array;

public function packagesCount(string $organizationId): int;

public function usersCount(): int;

public function privateDownloads(\DateTimeImmutable $till): int;

public function proxyDownloads(\DateTimeImmutable $till): int;
}
Loading

0 comments on commit ce8e749

Please sign in to comment.