From d37b351df0e1bdbbad1eaaca8a543c2ae07750b8 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 20 Mar 2018 12:31:11 -0700 Subject: [PATCH] Introduce Terminus metrics command. (#1835) --- CONTRIBUTING.md | 22 +- composer.json | 4 +- composer.lock | 22 +- src/Collections/APICollection.php | 14 +- src/Collections/Metrics.php | 207 ++++++++++++++++ src/Commands/Env/MetricsCommand.php | 199 ++++++++++++++++ src/Models/Environment.php | 12 + src/Models/Metric.php | 21 ++ tests/features/metrics.feature | 15 ++ tests/fixtures/metrics.yml | 221 ++++++++++++++++++ .../Commands/Env/MetricsCommandTest.php | 79 +++++++ 11 files changed, 804 insertions(+), 12 deletions(-) create mode 100644 src/Collections/Metrics.php create mode 100644 src/Commands/Env/MetricsCommand.php create mode 100644 src/Models/Metric.php create mode 100644 tests/features/metrics.feature create mode 100644 tests/fixtures/metrics.yml create mode 100644 tests/unit_tests/Commands/Env/MetricsCommandTest.php diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 590e964bd..402706949 100755 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,7 +75,11 @@ The Terminus 1.x unit tests can be run via: ### Functional Tests -The functional test files are in the `tests/features` directory. To run the entire test suite for Terminus 0.x: +The functional test files are in the `tests/features` directory. Any test which touches the backed is mocked with [VCR](http://php-vcr.github.io). + +#### Running existing tests + +To run the entire test suite for Terminus 0.x: `vendor/bin/behat -c=tests/config/behat.yml` @@ -92,6 +96,22 @@ The functional test files for the new version of Terminus are in the `tests/acti More information can be found by running `vendor/bin/behat --help`. +#### Recording new tests + +To record a new test, configure the `parameters` section of the file [tests/config/behat.yml](tests/config/behat.yml) as follows: +``` +parameters: + user_id: '[[YOUR-USER-ID-HERE]]' + username: '[[YOUR-EMAIL-ADDRESS-HERE]]' + host: 'terminus.pantheon.io:443' + vcr_mode: 'new_episodes' + machine_token: '[[YOUR-MACHINE-TOKEN-HERE]]' +``` +Then, run a single test as described above. VCR will then call the backend and record the results received in the specified .yml file. This is done for any Behat scenario labeled `@vcr filename.yml`. Pick a filename appropriate for the test. + +Once the VCR .yml file has been saved, you may restore your behat.yml configuration file to its previous state (at a minimum, set `vcr_mode` back to `none`). Subsequent test runs will pull data from the VCR .yml file to satisfy future web requests. + +You may need to add yourself to the [team of the behat-tests site](https://admin.dashboard.pantheon.io/sites/e885f5fe-6644-4df6-a292-68b2b57c33ad#dev/code) (Pantheon employees) or use a different test site. Once you have captured the events you would like to record, hand-sanitize them of any sensitive information such as machine tokens and bearer authorization headers. Versioning ---------- diff --git a/composer.json b/composer.json index 3c1443878..b2194944c 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "require": { "php": ">=5.5.9", "composer/semver": "^1.4", + "consolidation/output-formatters": "^3.2", "consolidation/robo": "^1.1.0", "guzzlehttp/guzzle": "^6.2", "psy/psysh": "^0.8", @@ -32,7 +33,7 @@ "bin/terminus" ], "scripts": { - "behat": "COMPOSER_PROCESS_TIMEOUT=3600 SHELL_INTERACTIVE=true behat --colors -c=tests/config/behat.yml --suite=default", + "behat": "SHELL_INTERACTIVE=true behat --colors -c=tests/config/behat.yml --suite=default", "cbf": "phpcbf --standard=PSR2 -n tests/unit_tests/* bin/terminus src/*", "clover": "phpunit -c tests/config/phpunit.xml.dist --coverage-clover tests/logs/clover.xml", "coveralls": "coveralls -v -c tests/config/coveralls.xml", @@ -51,6 +52,7 @@ "squizlabs/php_codesniffer": "^2.7" }, "config": { + "process-timeout": 3600, "optimize-autoloader": true, "preferred-install": "dist", "sort-packages": true, diff --git a/composer.lock b/composer.lock index 00ee60f1e..43565487a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "7da09195477ca65e1224c1bf10fee3ae", + "content-hash": "7efb80847bf5d63e1857757a65510a1e", "packages": [ { "name": "composer/semver", @@ -224,16 +224,16 @@ }, { "name": "consolidation/output-formatters", - "version": "3.1.13", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "3188461e965b32148c8fb85261833b2b72d34b8c" + "reference": "da889e4bce19f145ca4ec5b1725a946f4eb625a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/3188461e965b32148c8fb85261833b2b72d34b8c", - "reference": "3188461e965b32148c8fb85261833b2b72d34b8c", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/da889e4bce19f145ca4ec5b1725a946f4eb625a9", + "reference": "da889e4bce19f145ca4ec5b1725a946f4eb625a9", "shasum": "" }, "require": { @@ -242,11 +242,17 @@ "symfony/finder": "^2.5|^3|^4" }, "require-dev": { - "phpunit/phpunit": "^4.8", - "satooshi/php-coveralls": "^1.0.2 | dev-master", + "g-1-a/composer-test-scenarios": "^2", + "phpunit/phpunit": "^5.7.27", + "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.7", + "symfony/console": "3.2.3", + "symfony/var-dumper": "^2.8|^3|^4", "victorjonsson/markdowndocs": "^1.3" }, + "suggest": { + "symfony/var-dumper": "For using the var_dump formatter" + }, "type": "library", "extra": { "branch-alias": { @@ -269,7 +275,7 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2017-11-29T15:25:38+00:00" + "time": "2018-03-20T15:18:32+00:00" }, { "name": "consolidation/robo", diff --git a/src/Collections/APICollection.php b/src/Collections/APICollection.php index 7b7a01b89..bd0152569 100644 --- a/src/Collections/APICollection.php +++ b/src/Collections/APICollection.php @@ -83,10 +83,20 @@ public function getUrl() * @return array */ protected function requestData() + { + return $this->requestDataAtUrl($this->getUrl(), $this->getFetchArgs()); + } + + /** + * Make a request at a specific URL + * @param string $url address to fetch + * @param array $args request arguments (@see APICollection::getFetchArgs()) + * @return array + */ + protected function requestDataAtUrl($url, $args = []) { $default_args = ['options' => ['method' => 'get',],]; - $args = array_merge($default_args, $this->getFetchArgs()); - $url = $this->getUrl(); + $args = array_merge($default_args, $args); if ($this->isPaged()) { $results = $this->request()->pagedRequest($url, $args); diff --git a/src/Collections/Metrics.php b/src/Collections/Metrics.php new file mode 100644 index 000000000..845694d9c --- /dev/null +++ b/src/Collections/Metrics.php @@ -0,0 +1,207 @@ +period; + } + + /** + * @param string $value + */ + public function setPeriod($value) + { + return $this->setParameter('period', $value); + } + + /** + * @return string + */ + public function getDatapoints() + { + return $this->datapoints; + } + + /** + * @param string $value + */ + public function setDatapoints($value) + { + return $this->setParameter('datapoints', $value); + } + + /** + * @param string $value + */ + protected function setParameter($parameter, $value) + { + if ($this->$parameter === $value) { + return $this; + } + // Clear our cache whenever our parameters are changed + $this->setData([]); + $this->$parameter = $value; + return $this; + } + + /** + * Our API returns our data series inside a 'timeseries' element. + * To be compatible with Terminus' data model, we need to unwrap + * this and return an array of data items. We also convert the + * timestamp from seconds to the date format used elsewhere in Terminus. + */ + protected function requestData() + { + $rawPagesServed = $this->requestDataAtUrl($this->getUrlForSeries('pageviews'), $this->getFetchArgs()); + + if (empty($rawPagesServed->timeseries)) { + throw new \Exception("No data available."); + } + + $rawVisits = $this->requestDataAtUrl($this->getUrlForSeries('visits'), $this->getFetchArgs()); + + // The data is passed to us with the data series of primary + // interest to us nested inside a 'timeseries' element. The + // requirements for an EnvironmentOwnedCollection or any + // TerminusCollection is that the request data must return + // a list of all of our data items. + // (@see TerminusCollection::fetch()) + // We also need to ensure that the elements of our timeseries + // have unique IDs. We will use the time value for this. + $pageviewData = $this->assignIds($rawPagesServed->timeseries, 'timestamp'); + $visitData = $this->assignIds($rawVisits->timeseries, 'timestamp'); + $combineddata = $this->combineRawData($pageviewData, $visitData); + + // Convert the timestamp to a datetime. The timestamp will remain + // as the row key. + $data = array_map( + function ($item) { + $item->datetime = gmdate("Y-m-d\TH:i:s", $item->timestamp); + unset($item->timestamp); + return $item; + }, + $combineddata + ); + + // Our parent class is already caching our data series; we will + // store the other items in a 'metadata' field. We will avoid + // caching the raw data because that would duplicate data + // already cached. + unset($rawPagesServed->timeseries); + $this->metadata = $rawPagesServed; + + return $data; + } + + /** + * Combine the raw pages served data with the raw unique visits data + * @param array $rawPagesServed + * @param array $rawVisits + * @return array + */ + protected function combineRawData($rawPagesServed, $rawVisits) + { + $result = $rawVisits; + foreach ($result as $time => $item) { + $item->visits = $item->value; + $result[$time]->pages_served = $rawPagesServed[$time]->value; + unset($result[$time]->value); + } + return $result; + } + + /** + * When serializing the metrics again, wrap the timeseries data + * back inside a 'timeseries' element and then union in the metadata. + * @return array + */ + public function serialize() + { + $timeseries = parent::serialize(); + return (array) $this->metadata + ['timeseries' => $timeseries]; + } + + /** + * Convert an array with numeric indexes to an associative array whose + * indexes are taken from one of the data elements. + * @param $data An array of items with numeric indexes + * @param $keyId The id of the element in each item that is the key + * + * @return associative array of the same input items with new keys + */ + protected function assignIds($data, $keyId) + { + // Return an array consisting of all of the values of + // the data column identified by $keyId. + $keys = array_map( + function ($item) use ($keyId) { + return $item->$keyId; + }, + $data + ); + + return array_combine($keys, $data); + } + + /** + * Fill in the parameters for the desired request. + * @param string $seriesId + * @return string + */ + protected function getUrlForSeries($seriesId) + { + $url = $this->getUrl(); + $tr = [ + '{series}' => $seriesId, + '{period}' => $this->getPeriod(), + '{datapoints}' => $this->getDatapoints(), + ]; + return strtr($url, $tr); + } +} diff --git a/src/Commands/Env/MetricsCommand.php b/src/Commands/Env/MetricsCommand.php new file mode 100644 index 000000000..6d8217d4e --- /dev/null +++ b/src/Commands/Env/MetricsCommand.php @@ -0,0 +1,199 @@ +. Displays metrics for 's environment. + * @usage Displays metrics for 's live environment. + * @usage --fields=datetime,pages_served Displays only the pages + * served for each date period. + */ + public function metrics( + $site_env, + $options = [ + 'period' => self::DAILY_PERIOD, + 'datapoints' => 'auto' + ] + ) { + list(, $env) = $this->getUnfrozenSiteEnv($site_env, 'live'); + + if ($env->getName() != 'live') { + throw new \Exception('Metrics are only supported for the "live" environment for now.'); + } + + $data = $env->getMetrics() + ->setPeriod($options['period']) + ->setDatapoints($this->selectDatapoints($options['datapoints'], $options['period'])) + ->serialize(); + + return (new RowsOfFieldsWithMetadata($data)) + ->setDataKey('timeseries') + ->addRenderer( + new NumericCellRenderer($data['timeseries'], ['visits' => 6, 'pages_served' => 12]) + ) + ->addRendererFunction( + function ($key, $cellData, FormatterOptions $options, $rowData) { + if ($key == 'datetime') { + $cellData = str_replace('T00:00:00', '', $cellData); + } + return $cellData; + } + ); + } + + /** + * Find the maximum width of any data item in the specified column. + * @param array $data + * @param string $column + * @return int + */ + protected function findWidth($data, $column) + { + $maxWidth = 0; + foreach ($data as $row) { + $str = number_format($row[$column]); + $maxWidth = max($maxWidth, strlen($str)); + } + return $maxWidth; + } + + /** + * Determine the value we should use for 'datapoints' given a specific period. + * @param string $datapoints + * @param string $period + * @return string + */ + protected function selectDatapoints($datapoints, $period) + { + if (!$datapoints || ($datapoints == 'auto')) { + return $this->defaultDatapoints($period); + } + return $datapoints; + } + + /** + * Ensure that the user did not supply an invalid value for 'period'. + * + * @hook validate alpha:env:metrics + * @param CommandData $commandData + */ + public function validateOptions(CommandData $commandData) + { + $validGranularities = [ + self::DAILY_PERIOD, + self::WEEKLY_PERIOD, + self::MONTHLY_PERIOD, + ]; + + $input = $commandData->input(); + $this->validateOptionValue($input, 'period', $validGranularities); + $this->validateItemWithinRange($input, 'datapoints', 1, $this->datapointsMaximum($input->getOption('period')), ['auto']); + } + + /** + * Test to see if an option value is one of the provided values + * @param InputInterface $input + * @param string $optionName + * @param string[] $validValues + */ + protected function validateOptionValue(InputInterface $input, $optionName, array $validValues) + { + $value = $input->getOption($optionName); + if (!in_array($value, $validValues)) { + throw new \Exception("'{$value}' is an invalid value for {$optionName}: must be one of " . implode(', ', $validValues)); + } + } + + /** + * Check to see if the specified item is within the specified minimum/maximum range. + * @param InputInterface $input + * @param string $optionName + * @param string $minimum + * @param string $maximum + * @param string $exceptionalValues + */ + protected function validateItemWithinRange(InputInterface $input, $optionName, $minimum, $maximum, $exceptionalValues = []) + { + $value = $input->getOption($optionName); + if (in_array($value, $exceptionalValues)) { + return; + } + $orOneOf = (count($exceptionalValues) == 0) ? '' : (count($exceptionalValues) == 1) ? 'or ' : 'or one of '; + if (($value < $minimum) || ($value > $maximum)) { + throw new \Exception("'{$value}' is an invalid value for {$optionName}: must be between {$minimum} and {$maximum} (inclusive) {$orOneOf}" . implode(', ', $exceptionalValues)); + } + } + + /** + * Default datapoints to 12 / 28 if 'auto' is specified + * @param string $period + * @return string + */ + public function defaultDatapoints($period) + { + // For now, out default values will just be the maximums for the period. + // We should change this if we increase the maximums. + return $this->datapointsMaximum($period); + } + + /** + * Return the maximum datapoint value for the provided period. + * @param string $period + * @return string + */ + public function datapointsMaximum($period) + { + $defaultPeriodValues = [ + self::DAILY_PERIOD => self::DEFAULT_DAILY_DATAPOINTS, + self::WEEKLY_PERIOD => self::DEFAULT_WEEKLY_DATAPOINTS, + self::MONTHLY_PERIOD => self::DEFAULT_MONTHLY_DATAPOINTS, + ]; + + return $defaultPeriodValues[$period]; + } +} diff --git a/src/Models/Environment.php b/src/Models/Environment.php index 1f45f6aa8..84955fad2 100755 --- a/src/Models/Environment.php +++ b/src/Models/Environment.php @@ -8,6 +8,7 @@ use Pantheon\Terminus\Collections\Bindings; use Pantheon\Terminus\Collections\Commits; use Pantheon\Terminus\Collections\Domains; +use Pantheon\Terminus\Collections\Metrics; use Pantheon\Terminus\Collections\Workflows; use Pantheon\Terminus\Helpers\LocalMachineHelper; use Pantheon\Terminus\Friends\SiteInterface; @@ -435,6 +436,17 @@ public function getCommits() return $this->commits; } + /** + * @return Metrics + */ + public function getMetrics() + { + if (empty($this->metrics)) { + $this->metrics = $this->getContainer()->get(Metrics::class, [['environment' => $this,],]); + } + return $this->metrics; + } + /** * @return Domains */ diff --git a/src/Models/Metric.php b/src/Models/Metric.php new file mode 100644 index 000000000..035ff139d --- /dev/null +++ b/src/Models/Metric.php @@ -0,0 +1,21 @@ + $this->get('datetime'), + 'pages_served' => $this->get('pages_served'), + 'visits' => $this->get('visits'), + ]; + } +} diff --git a/tests/features/metrics.feature b/tests/features/metrics.feature new file mode 100644 index 000000000..87d8c95d8 --- /dev/null +++ b/tests/features/metrics.feature @@ -0,0 +1,15 @@ +Feature: Checking metrics for an environment + In order to determine how much traffic is being sent to my site + As a user + In need to be able to check the metrics logs + + Background: I am authenticated and have a site called [[test_site_name]] + Given I am authenticated + And a site named "[[test_site_name]]" + + @vcr metrics.yml + Scenario: Checking metrics + When I run "terminus alpha:env:metrics [[test_site_name]]" + Then I should get: "Period Visits Pages Served" + And I should get: "2018-03-14 159 1,335" + And I should get: "2018-03-15 172 650" diff --git a/tests/fixtures/metrics.yml b/tests/fixtures/metrics.yml new file mode 100644 index 000000000..d5cd3c60b --- /dev/null +++ b/tests/fixtures/metrics.yml @@ -0,0 +1,221 @@ + +- + request: + method: POST + url: 'https://terminus.pantheon.io:443/api/authorize/machine-token' + headers: + Host: 'terminus.pantheon.io:443' + Expect: null + Accept-Encoding: null + Content-type: application/json + User-Agent: 'Terminus/1.7.2-dev (php_version=7.1.11&script=bin/terminus)' + Authorization: 'Bearer b3a42ba5-755d-42ca-9109-21bde32809d0:d43e3d5a-2bbd-11e8-b0c1-42010a800117:iJ23lnHckv90z4oqZLPxa' + Accept: null + body: '{"machine_token":"IHakX_vOVAlKOJize6idsBaBmRrwbORikCYUE9em1Ve3P","client":"terminus"}' + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Server: nginx + Date: 'Mon, 19 Mar 2018 23:30:44 GMT' + Content-Type: 'application/json; charset=utf-8' + Content-Length: '182' + Connection: keep-alive + X-Pantheon-Trace-Id: 8b73c3f0-2bcd-11e8-8606-3b39de44999c + X-Frame-Options: deny + Access-Control-Allow-Methods: GET + Access-Control-Allow-Headers: 'Origin, Content-Type, Accept' + Cache-Control: 'private, max-age=0, no-cache, no-store' + Pragma: no-cache + Vary: Accept-Encoding + Strict-Transport-Security: max-age=31536000 + body: '{"session":"b3a42ba5-755d-42ca-9109-21bde32809d0:8bb3a0c4-2bcd-11e8-aa59-42010a8000a6:rcwKhgc0cUyDuiKwoyAvA","expires_at":1523921444,"user_id":"b3a42ba5-755d-42ca-9109-21bde32809d0"}' +- + request: + method: GET + url: 'https://terminus.pantheon.io:443/api/users/b3a42ba5-755d-42ca-9109-21bde32809d0' + headers: + Host: 'terminus.pantheon.io:443' + Accept-Encoding: null + Content-type: application/json + User-Agent: 'Terminus/1.7.2-dev (php_version=7.1.11&script=bin/terminus)' + Authorization: 'Bearer b3a42ba5-755d-42ca-9109-21bde32809d0:8bb3a0c4-2bcd-11e8-aa59-42010a8000a6:rcwKhgc0cUyDuiKwoyAvA' + Accept: null + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Server: nginx + Date: 'Mon, 19 Mar 2018 23:30:45 GMT' + Content-Type: application/json + Content-Length: '3818' + Connection: keep-alive + X-Pantheon-Trace-Id: 8bf0bc20-2bcd-11e8-ab0a-29707aa61c6f + X-Frame-Options: deny + Access-Control-Allow-Methods: GET + Access-Control-Allow-Headers: 'Origin, Content-Type, Accept' + Cache-Control: 'private, max-age=0, no-cache, no-store' + Pragma: no-cache + Vary: Accept-Encoding + Strict-Transport-Security: max-age=31536000 + body: '{"profile": {"tracking_first_organization_invite": 1449093173, "invites_to_nonuser": 9, "seen_first_time_user_popover": true, "experiments": {"welcome_video": "not_shown"}, "full_name": "Greg Anderson", "pullFromLive": true, "web_services_business": false, "initial_identity_strategy": null, "invites_sent": 93, "google_adwords_paid_for_site_do_send": null, "verify": 1, "tracking_first_code_push": 1422477215, "invites_to_user": 84, "registration_context": null, "job_function": "developer", "tracking_first_workflow_in_live": 1421959071, "tracking_first_team_invite": 1434660321, "firstname": "Greg", "invites_to_site": 84, "lastname": "Anderson", "pda_campaign": null, "copyCodeUpdatePhp": true, "google_adwords_pushed_code_sent": 1423761165, "last-org-spinup": "e9527ac9-e2e0-460f-acf1-04f7e2f5a7ba", "tracking_first_site_create": 1421872714, "initial_identity_name": null, "guilty_of_abuse": null, "invites_to_org": 9, "minimize_jit_docs": false, "tracking_first_site_upgrade": 1442949394, "seens": {"cms-installation": true, "global-cdn": true, "skip-cms-installation": {"e66cf7e7-8ae0-4c6c-b54e-78b1e07024d0": true, "f1d0a2c1-cdd2-425c-8fb8-ba9d23fb5d5f": true, "ac2022fe-4223-4e80-a290-42a0db87a1d6": true, "6c5321a3-6e73-4e68-b5a9-e64acbc65218": true, "76096370-5d2d-411f-8927-c73b15dd9e13": true, "96b46d27-2ef3-4729-bfeb-6ab8175f06b6": true}}, "google_adwords_paid_for_site_sent": 1442949428, "modified": 1421871703.483313, "maxdevsites": 2, "google_adwords_account_registered_sent": 1421872118, "organization": "Pantheon Systems, Inc."}, "feature_flags": [{"name": "Self-Serve Upstreams", "default": false, "enabled": false, "visible": false, "optional": false, "id": "self-serve_upstreams"}, {"name": "Experimental Products", "default": false, "enabled": false, "visible": false, "optional": false, "id": "experimental-products"}, {"name": "annotate git tags", "default": false, "enabled": false, "visible": false, "optional": false, "id": "annotate_git_tags"}, {"name": "Cacheserver Add-on", "default": false, "enabled": false, "visible": false, "optional": false, "id": "cacheserver-addon"}, {"name": "Apollo Spinup", "default": true, "enabled": true, "visible": false, "optional": false, "id": "apollo-spinup"}, {"name": "Pantheon One", "default": false, "enabled": false, "visible": false, "optional": false, "id": "one"}, {"name": "Unified User Account Screen ", "default": false, "enabled": false, "visible": true, "optional": false, "id": "apollo-user"}, {"name": "Indexserver Add-on", "default": false, "enabled": false, "visible": true, "optional": false, "id": "indexserver-addon"}, {"name": "Wordpress", "default": false, "enabled": false, "visible": false, "optional": false, "id": "wordpress"}, {"name": "Apollo Self-Service Toggle", "default": true, "enabled": true, "visible": false, "optional": false, "id": "apollo-toggle"}, {"name": "Site Audit Checks", "default": false, "enabled": false, "visible": false, "optional": false, "id": "site_audit_checks"}, {"description": "Enables the new Desk API 2.0 Interface", "default": false, "enabled": true, "visible": true, "percentage": 100, "optional": false, "id": "desk", "name": "desk"}, {"name": "org tags", "default": false, "enabled": false, "visible": true, "optional": false, "id": "org-tags"}, {"name": "Apollo Dashboard", "default": true, "enabled": true, "visible": false, "optional": false, "id": "apollo"}, {"name": "Org Upstream Updates", "default": false, "enabled": false, "visible": false, "optional": false, "id": "org-has-code"}], "user_id": "b3a42ba5-755d-42ca-9109-21bde32809d0", "created_at": 1421871703, "dev_sites_count": 1, "id": "b3a42ba5-755d-42ca-9109-21bde32809d0", "destination_organization_id": null, "is_registered": true, "created_organization_id": null, "password": "SCRUBBED", "email": "greg@getpantheon.com"}' +- + request: + method: GET + url: 'https://terminus.pantheon.io:443/api/site-names/behat-tests' + headers: + Host: 'terminus.pantheon.io:443' + Accept-Encoding: null + Content-type: application/json + User-Agent: 'Terminus/1.7.2-dev (php_version=7.1.11&script=bin/terminus)' + Authorization: 'Bearer b3a42ba5-755d-42ca-9109-21bde32809d0:8bb3a0c4-2bcd-11e8-aa59-42010a8000a6:rcwKhgc0cUyDuiKwoyAvA' + Accept: null + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Server: nginx + Date: 'Mon, 19 Mar 2018 23:30:50 GMT' + Content-Type: 'application/json; charset=utf-8' + Transfer-Encoding: chunked + Connection: keep-alive + X-Pantheon-Trace-Id: 8ef43820-2bcd-11e8-8b6d-bbad4fe9a3cd + X-Frame-Options: deny + Access-Control-Allow-Methods: GET + Access-Control-Allow-Headers: 'Origin, Content-Type, Accept' + Cache-Control: 'private, max-age=0, no-cache, no-store' + Pragma: no-cache + Vary: Accept-Encoding + Strict-Transport-Security: max-age=31536000 + body: '{"id": "e885f5fe-6644-4df6-a292-68b2b57c33ad", "name": "behat-tests"}' +- + request: + method: GET + url: 'https://terminus.pantheon.io:443/api/sites/e885f5fe-6644-4df6-a292-68b2b57c33ad?site_state=true' + headers: + Host: 'terminus.pantheon.io:443' + Accept-Encoding: null + Content-type: application/json + User-Agent: 'Terminus/1.7.2-dev (php_version=7.1.11&script=bin/terminus)' + Authorization: 'Bearer b3a42ba5-755d-42ca-9109-21bde32809d0:8bb3a0c4-2bcd-11e8-aa59-42010a8000a6:rcwKhgc0cUyDuiKwoyAvA' + Accept: null + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Server: nginx + Date: 'Mon, 19 Mar 2018 23:30:51 GMT' + Content-Type: application/json + Content-Length: '3954' + Connection: keep-alive + X-Pantheon-Trace-Id: 8f7e9dd0-2bcd-11e8-a162-fda472eb539b + X-Frame-Options: deny + Access-Control-Allow-Methods: GET + Access-Control-Allow-Headers: 'Origin, Content-Type, Accept' + Cache-Control: 'private, max-age=0, no-cache, no-store' + Pragma: no-cache + Vary: Accept-Encoding + Strict-Transport-Security: max-age=31536000 + body: '{"created": 1495653132, "created_by_user_id": "cbc3d67f-f2b9-44a0-8dd6-8d63265af96e", "current_num_domains": 0, "framework": "wordpress", "frozen": false, "holder_id": "cbc3d67f-f2b9-44a0-8dd6-8d63265af96e", "holder_type": "user", "last_code_push": {"timestamp": "2018-03-19T21:20:22", "user_uuid": null}, "last_frozen_at": 1513054413, "last_unfrozen_at": 1521494248, "name": "behat-tests", "owner": "cbc3d67f-f2b9-44a0-8dd6-8d63265af96e", "php_version": "55", "preferred_zone": "us-central1", "service_level": "free", "service_level_updated_at": 1495653132, "upstream": {"repository_branch": "master", "machine_name": "wordpress", "product_id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf", "url": "https://github.com/pantheon-systems/WordPress", "label": "WordPress", "organization_id": "", "framework": "wordpress", "branch": "master", "repository_url": "https://github.com/pantheon-systems/WordPress", "type": "core", "id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf"}, "label": "behat-tests", "id": "e885f5fe-6644-4df6-a292-68b2b57c33ad", "holder": {"profile": {"utm_term": "", "tracking_first_organization_invite": 1479332608, "invites_to_nonuser": 4, "seen_first_time_user_popover": true, "utm_content": "/", "experiments": {"welcome_video": "shown"}, "full_name": "Sara McCutcheon", "pullFromLive": true, "utm_device": "", "web_services_business": null, "initial_identity_strategy": null, "utm_campaign": "pantheon.io (organic)", "invites_sent": 14, "verify": "037eadb020d51ccddba9e06a64908c98", "tracking_first_code_push": 1428811227, "google_adwords_account_registered_sent": 1428707350, "invites_to_user": 10, "registration_context": null, "utm_medium": "", "job_function": "developer", "tracking_first_workflow_in_live": 1428811293, "tracking_first_team_invite": 1436464837, "firstname": "Sara", "invites_to_site": 13, "lastname": "McCutcheon", "pda_campaign": null, "utm_source": "https://www.bing.com/search?setmkt=en-US&q=pantheon+san+francisco", "google_adwords_pushed_code_sent": 1428811242, "last-org-spinup": "none", "tracking_first_site_create": 1428723370, "initial_identity_name": null, "guilty_of_abuse": null, "invites_to_org": 1, "tracking_first_site_upgrade": 1437784612, "seens": {"global-cdn": true, "terminus-1": true}, "google_adwords_paid_for_site_sent": 1438018300, "modified": 1492930415, "maxdevsites": 2, "lead_type": "", "organization": " Pantheon Systems, Inc"}, "id": "cbc3d67f-f2b9-44a0-8dd6-8d63265af96e", "email": "tesladethray@gmail.com"}, "settings": {"allow_domains": false, "max_num_cdes": 10, "last_frozen_at": 1513054413, "stunnel": false, "min_backups": 0, "owner": "cbc3d67f-f2b9-44a0-8dd6-8d63265af96e", "secure_runtime_access": false, "current_num_domains": 0, "allow_indexserver": false, "created_by_user_id": "cbc3d67f-f2b9-44a0-8dd6-8d63265af96e", "failover_appserver": 0, "pingdom": 0, "cacheserver": 1, "support_plan": "regular_support", "on_server_development": false, "service_level_updated_at": 1495653132, "label": "behat-tests", "appserver": 1, "allow_read_slaves": false, "indexserver": 1, "php_version": "55", "php_channel": "stable", "allow_cacheserver": false, "ssl_enabled": null, "service_level": "free", "dedicated_ip": null, "dbserver": 1, "framework": "wordpress", "max_total_domains": 0, "upstream": {"url": "https://github.com/pantheon-systems/WordPress", "product_id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf", "branch": "master"}, "guilty_of_abuse": null, "preferred_zone": "us-central1", "drush_version": 5, "last_unfrozen_at": 1521494248, "pingdom_chance": 0, "holder_id": "cbc3d67f-f2b9-44a0-8dd6-8d63265af96e", "name": "behat-tests", "created": 1495653132, "frozen": false, "max_backups": 0, "holder_type": "user", "replica_verification_strategy": "pt-heartbeat", "pingdom_manually_enabled": false, "last_code_push": {"timestamp": "2018-03-19T21:20:22", "user_uuid": null}}, "base_domain": null, "attributes": {"hostname_limit": true, "label": "behat-tests", "m3_ui": true}, "add_ons": []}' +- + request: + method: GET + url: 'https://terminus.pantheon.io:443/api/sites/e885f5fe-6644-4df6-a292-68b2b57c33ad/environments' + headers: + Host: 'terminus.pantheon.io:443' + Accept-Encoding: null + Content-type: application/json + User-Agent: 'Terminus/1.7.2-dev (php_version=7.1.11&script=bin/terminus)' + Authorization: 'Bearer b3a42ba5-755d-42ca-9109-21bde32809d0:8bb3a0c4-2bcd-11e8-aa59-42010a8000a6:rcwKhgc0cUyDuiKwoyAvA' + Accept: null + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Server: nginx + Date: 'Mon, 19 Mar 2018 23:30:57 GMT' + Content-Type: 'application/json; charset=utf-8' + Transfer-Encoding: chunked + Connection: keep-alive + X-Pantheon-Trace-Id: 93092420-2bcd-11e8-8b6d-bbad4fe9a3cd + X-Frame-Options: deny + Access-Control-Allow-Methods: GET + Access-Control-Allow-Headers: 'Origin, Content-Type, Accept' + Cache-Control: 'private, max-age=0, no-cache, no-store' + Pragma: no-cache + Vary: Accept-Encoding + Strict-Transport-Security: max-age=31536000 + body: '{"live": {"is_initialized": false, "environment_created": 1495653140, "dns_zone": "pantheonsite.io", "randseed": "4DJUHVLCDJKWGXPPI01B9HGQABH6MUVR", "maintenance": {"enabled": false}, "lock": {"username": null, "password": null, "locked": false}, "styx_clusters_for_cache_clear": ["styx-03.pantheon.io", "styx-1-gcp-fastly-stub.pantheon.io", "edge.live.getpantheon.com", "styx-fe1.pantheon.io", "styx-02.pantheon.io", "styx-fe2.pantheon.io", "styx-yolo.pantheon.io", "styx-fe4.pantheon.io", "styx-fe3.pantheon.io", "styx-01.pantheon.io"], "styx_cluster": "styx-fe2.pantheon.io"}, "test": {"is_initialized": false, "environment_created": 1495653136, "dns_zone": "pantheonsite.io", "randseed": "Q24G5YUX33INCLVJ1IMP63PJN5AEOPX8", "maintenance": {"enabled": false}, "lock": {"username": null, "password": null, "locked": false}, "styx_clusters_for_cache_clear": ["styx-03.pantheon.io", "styx-1-gcp-fastly-stub.pantheon.io", "edge.live.getpantheon.com", "styx-fe1.pantheon.io", "styx-02.pantheon.io", "styx-fe2.pantheon.io", "styx-yolo.pantheon.io", "styx-fe4.pantheon.io", "styx-fe3.pantheon.io", "styx-01.pantheon.io"], "styx_cluster": "styx-fe2.pantheon.io"}, "dev": {"quicksilver_configuration": {}, "is_initialized": true, "watchers": 0, "php_version": "70", "diffstat": {}, "on_server_development": false, "environment_created": 1495653132, "dns_zone": "pantheonsite.io", "randseed": "EJ8WGJ9J2K3LLGZISD6G0AF4GJUB1T8G", "target_commit": "fbc8d8abdaffdce40abe04e1f9e09d62284b06ff", "target_ref": "refs/heads/master", "maintenance": {"enabled": false}, "lock": {"username": null, "password": null, "locked": false}, "environment_variables": {"php_version": 7.0}, "styx_clusters_for_cache_clear": ["styx-03.pantheon.io", "styx-1-gcp-fastly-stub.pantheon.io", "edge.live.getpantheon.com", "styx-fe1.pantheon.io", "styx-02.pantheon.io", "styx-fe2.pantheon.io", "styx-yolo.pantheon.io", "styx-fe4.pantheon.io", "styx-fe3.pantheon.io", "styx-01.pantheon.io"], "styx_cluster": "styx-fe2.pantheon.io"}}' +- + request: + method: GET + url: 'https://terminus.pantheon.io:443/api/sites/e885f5fe-6644-4df6-a292-68b2b57c33ad/environments/live/pageviews?granularity=day&datapoints=28' + headers: + Host: 'terminus.pantheon.io:443' + Accept-Encoding: null + Content-type: application/json + User-Agent: 'Terminus/1.7.2-dev (php_version=7.1.11&script=bin/terminus)' + Authorization: 'Bearer b3a42ba5-755d-42ca-9109-21bde32809d0:8bb3a0c4-2bcd-11e8-aa59-42010a8000a6:rcwKhgc0cUyDuiKwoyAvA' + Accept: null + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Server: nginx + Date: 'Mon, 19 Mar 2018 23:30:59 GMT' + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + X-Pantheon-Trace-Id: 93da56d0-2bcd-11e8-bd3f-c7d181c9b3a4 + X-Frame-Options: deny + Access-Control-Allow-Methods: GET + Access-Control-Allow-Headers: 'Origin, Content-Type, Accept' + Cache-Control: 'private, max-age=0, no-cache, no-store' + Pragma: no-cache + Vary: Accept-Encoding + Strict-Transport-Security: max-age=31536000 + body: '{"timeseries": [{"timestamp": 1519084800, "value": null}, {"timestamp": 1519171200, "value": null}, {"timestamp": 1519257600, "value": null}, {"timestamp": 1519344000, "value": null}, {"timestamp": 1519430400, "value": null}, {"timestamp": 1519516800, "value": null}, {"timestamp": 1519603200, "value": null}, {"timestamp": 1519689600, "value": 96}, {"timestamp": 1519776000, "value": 1092}, {"timestamp": 1519862400, "value": 659}, {"timestamp": 1519948800, "value": null}, {"timestamp": 1520035200, "value": null}, {"timestamp": 1520121600, "value": null}, {"timestamp": 1520208000, "value": 642}, {"timestamp": 1520294400, "value": 543}, {"timestamp": 1520380800, "value": 667}, {"timestamp": 1520467200, "value": 688}, {"timestamp": 1520553600, "value": 548}, {"timestamp": 1520640000, "value": 580}, {"timestamp": 1520726400, "value": 784}, {"timestamp": 1520812800, "value": 532}, {"timestamp": 1520899200, "value": 1068}, {"timestamp": 1520985600, "value": 1335}, {"timestamp": 1521072000, "value": 650}, {"timestamp": 1521158400, "value": 1328}, {"timestamp": 1521244800, "value": 2937}, {"timestamp": 1521331200, "value": 688}, {"timestamp": 1521417600, "value": 659}], "summary": null}' +- + request: + method: GET + url: 'https://terminus.pantheon.io:443/api/sites/e885f5fe-6644-4df6-a292-68b2b57c33ad/environments/live/visits?granularity=day&datapoints=28' + headers: + Host: 'terminus.pantheon.io:443' + Accept-Encoding: null + Content-type: application/json + User-Agent: 'Terminus/1.7.2-dev (php_version=7.1.11&script=bin/terminus)' + Authorization: 'Bearer b3a42ba5-755d-42ca-9109-21bde32809d0:8bb3a0c4-2bcd-11e8-aa59-42010a8000a6:rcwKhgc0cUyDuiKwoyAvA' + Accept: null + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Server: nginx + Date: 'Mon, 19 Mar 2018 23:31:01 GMT' + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + X-Pantheon-Trace-Id: 95544ca0-2bcd-11e8-a383-6b1bd00a898c + X-Frame-Options: deny + Access-Control-Allow-Methods: GET + Access-Control-Allow-Headers: 'Origin, Content-Type, Accept' + Cache-Control: 'private, max-age=0, no-cache, no-store' + Pragma: no-cache + Vary: Accept-Encoding + Strict-Transport-Security: max-age=31536000 + body: '{"timeseries": [{"timestamp": 1519084800, "value": null}, {"timestamp": 1519171200, "value": null}, {"timestamp": 1519257600, "value": null}, {"timestamp": 1519344000, "value": null}, {"timestamp": 1519430400, "value": null}, {"timestamp": 1519516800, "value": null}, {"timestamp": 1519603200, "value": null}, {"timestamp": 1519689600, "value": 40}, {"timestamp": 1519776000, "value": 121}, {"timestamp": 1519862400, "value": 128}, {"timestamp": 1519948800, "value": null}, {"timestamp": 1520035200, "value": null}, {"timestamp": 1520121600, "value": null}, {"timestamp": 1520208000, "value": 208}, {"timestamp": 1520294400, "value": 183}, {"timestamp": 1520380800, "value": 223}, {"timestamp": 1520467200, "value": 201}, {"timestamp": 1520553600, "value": 192}, {"timestamp": 1520640000, "value": 172}, {"timestamp": 1520726400, "value": 203}, {"timestamp": 1520812800, "value": 166}, {"timestamp": 1520899200, "value": 143}, {"timestamp": 1520985600, "value": 159}, {"timestamp": 1521072000, "value": 172}, {"timestamp": 1521158400, "value": 183}, {"timestamp": 1521244800, "value": 242}, {"timestamp": 1521331200, "value": 180}, {"timestamp": 1521417600, "value": 162}], "summary": null}' + diff --git a/tests/unit_tests/Commands/Env/MetricsCommandTest.php b/tests/unit_tests/Commands/Env/MetricsCommandTest.php new file mode 100644 index 000000000..e5eea1e1a --- /dev/null +++ b/tests/unit_tests/Commands/Env/MetricsCommandTest.php @@ -0,0 +1,79 @@ +command = new MetricsCommand($this->getConfig()); + $this->command->setLogger($this->logger); + $this->command->setSites($this->sites); + + $this->metrics = $this->getMockBuilder(Metrics::class) + ->disableOriginalConstructor() + ->getMock(); + + // Ignore the calls to the fluid initializers in the Metrics class. + $this->metrics->method('setSeriesId')->willReturn($this->metrics); + $this->metrics->method('setPeriod')->willReturn($this->metrics); + $this->metrics->method('setDatapoints')->willReturn($this->metrics); + $this->metrics->method('selectDatapoints')->willReturn(2); + + $this->environment->method('getMetrics')->willReturn($this->metrics); + $this->environment->method('getName')->willReturn('live'); + + $this->metric_1_attribs = [ + 'id' => '1517443200', + 'datetime' => '2018-02-01T00:00:00', + 'value' => '1197', + ]; + $this->metric_1 = new Metric((object)$this->metric_1_attribs); + $this->metric_2_attribs = [ + 'id' => '1519862400', + 'datetime' => '2018-03-01T00:00:00', + 'value' => '5111', + ]; + $this->metric_2 = new Metric((object)$this->metric_2_attribs); + } + + /** + * Tests the env:metrics command success with all parameters + */ + public function testLog() + { + $data = [ + 'timeseries' => [ + '1517443200' => [ + 'datetime' => '2018-02-01T00:00:00', + 'value' => '1197', + ], + '1519862400' => [ + 'datetime' => '2018-03-01T00:00:0', + 'value' => '1197', + ], + ], + 'summary' => null, + ]; + $this->environment->id = 'live'; + $this->metrics->method('serialize') + ->willReturn($data); + + $out = $this->command->metrics('mysite.live'); + + $this->assertInstanceOf('Consolidation\OutputFormatters\StructuredData\RowsOfFields', $out); + $this->assertEquals($data, $out->getArrayCopy()); + } +}