diff --git a/composer.json b/composer.json index 8f4f9b7b1..f31f4eb8e 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,15 @@ { "type": "vcs", "url": "https://github.com/dgarciabriseno/helioviewer-event-interface.git" + }, + { + "type": "vcs", + "url": "https://github.com/dgarciabriseno/device-detector.git" } ], "require": { - "helioviewer/event-interface": "dev-main" + "helioviewer/event-interface": "dev-main", + "matomo/device-detector": "dev-master" }, "require-dev": { "phpunit/phpunit": "^9.6" diff --git a/composer.lock b/composer.lock index 1d1c71e6c..7d14b285b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,26 +4,26 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2eb1520b71edcf4d455ab99ed04338aa", + "content-hash": "5eea2f72a94055eddd3e75c04c2c59a0", "packages": [ { "name": "guzzlehttp/guzzle", - "version": "7.5.1", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b964ca597e86b752cd994f27293e9fa6b6a95ed9" + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b964ca597e86b752cd994f27293e9fa6b6a95ed9", - "reference": "b964ca597e86b752cd994f27293e9fa6b6a95ed9", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -34,7 +34,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.1", "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.29 || ^9.5.23", "psr/log": "^1.1 || ^2.0 || ^3.0" }, @@ -48,9 +49,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "7.5-dev" } }, "autoload": { @@ -116,7 +114,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.5.1" + "source": "https://github.com/guzzle/guzzle/tree/7.8.0" }, "funding": [ { @@ -132,38 +130,37 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:30:08+00:00" + "time": "2023-08-27T10:20:53+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "b94b2807d85443f9719887892882d0329d1e2598" + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", - "reference": "b94b2807d85443f9719887892882d0329d1e2598", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -200,7 +197,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.2" + "source": "https://github.com/guzzle/promises/tree/2.0.1" }, "funding": [ { @@ -216,20 +213,20 @@ "type": "tidelift" } ], - "time": "2022-08-28T14:55:35+00:00" + "time": "2023-08-03T15:11:55+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.5.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", "shasum": "" }, "require": { @@ -316,7 +313,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.5.0" + "source": "https://github.com/guzzle/psr7/tree/2.6.1" }, "funding": [ { @@ -332,7 +329,7 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:11:26+00:00" + "time": "2023-08-27T10:13:57+00:00" }, { "name": "helioviewer/event-interface", @@ -340,12 +337,12 @@ "source": { "type": "git", "url": "https://github.com/dgarciabriseno/helioviewer-event-interface.git", - "reference": "e39466a33389d256d12089b95452d2ff82190689" + "reference": "9d2bd82b05061fec14e7f8093755b153054135d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dgarciabriseno/helioviewer-event-interface/zipball/e39466a33389d256d12089b95452d2ff82190689", - "reference": "e39466a33389d256d12089b95452d2ff82190689", + "url": "https://api.github.com/repos/dgarciabriseno/helioviewer-event-interface/zipball/9d2bd82b05061fec14e7f8093755b153054135d0", + "reference": "9d2bd82b05061fec14e7f8093755b153054135d0", "shasum": "" }, "require": { @@ -368,11 +365,11 @@ } }, "scripts": { - "test": [ + "test-all": [ "vendor/bin/phpunit --bootstrap tests/bootstrap.php --fail-on-warning tests" ], - "localtest": [ - "vendor/bin/phpunit --bootstrap tests/bootstrap.php --stop-on-failure --fail-on-warning tests" + "test": [ + "vendor/bin/phpunit --bootstrap tests/bootstrap.php --fail-on-warning tests --filter" ] }, "license": [ @@ -380,10 +377,139 @@ ], "description": "Interface for querying external data sources for Helioviewer", "support": { - "source": "https://github.com/dgarciabriseno/helioviewer-event-interface/tree/flares", + "source": "https://github.com/dgarciabriseno/helioviewer-event-interface/tree/main", "issues": "https://github.com/dgarciabriseno/helioviewer-event-interface/issues" }, - "time": "2023-05-01T14:35:22+00:00" + "time": "2023-08-10T14:16:07+00:00" + }, + { + "name": "matomo/device-detector", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/dgarciabriseno/device-detector.git", + "reference": "cf1a083e625fa36531fd6849255ec0ed1acc529d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dgarciabriseno/device-detector/zipball/cf1a083e625fa36531fd6849255ec0ed1acc529d", + "reference": "cf1a083e625fa36531fd6849255ec0ed1acc529d", + "shasum": "" + }, + "require": { + "mustangostang/spyc": "*", + "php": "^7.2|^8.0" + }, + "replace": { + "piwik/device-detector": "self.version" + }, + "require-dev": { + "matthiasmullie/scrapbook": "^1.4.7", + "mayflower/mo4-coding-standard": "^v8.0.0", + "phpstan/phpstan": "^0.12.52", + "phpunit/phpunit": "^8.5.8", + "psr/cache": "^1.0.1", + "psr/simple-cache": "^1.0.1", + "symfony/yaml": "^5.1.7" + }, + "suggest": { + "doctrine/cache": "Can directly be used for caching purpose", + "ext-yaml": "Necessary for using the Pecl YAML parser" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "DeviceDetector\\": "" + }, + "exclude-from-classmap": [ + "Tests/" + ] + }, + "archive": { + "exclude": [ + "/autoload.php" + ] + }, + "scripts": { + "php-cs-fixed": [ + "php vendor/bin/phpcbf" + ] + }, + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The Matomo Team", + "email": "hello@matomo.org", + "homepage": "https://matomo.org/team/" + } + ], + "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, media players, mobile apps, feed readers, libraries, etc), operating systems, devices, brands and models.", + "homepage": "https://matomo.org", + "keywords": [ + "devicedetection", + "parser", + "useragent" + ], + "support": { + "forum": "https://forum.matomo.org/", + "issues": "https://github.com/matomo-org/device-detector/issues", + "wiki": "https://dev.matomo.org/", + "source": "https://github.com/matomo-org/matomo" + }, + "time": "2023-09-14T16:31:52+00:00" + }, + { + "name": "mustangostang/spyc", + "version": "0.6.3", + "source": { + "type": "git", + "url": "git@github.com:mustangostang/spyc.git", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mustangostang/spyc/zipball/4627c838b16550b666d15aeae1e5289dd5b77da0", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "4.3.*@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "files": [ + "Spyc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "mustangostang", + "email": "vlad.andersen@gmail.com" + } + ], + "description": "A simple YAML loader/dumper class for PHP", + "homepage": "https://github.com/mustangostang/spyc/", + "keywords": [ + "spyc", + "yaml", + "yml" + ], + "time": "2019-09-10T13:16:29+00:00" }, { "name": "psr/cache", @@ -1267,16 +1393,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.4", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -1317,9 +1443,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "phar-io/manifest", @@ -1434,16 +1560,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.26", + "version": "9.2.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + "reference": "7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef", + "reference": "7134a5ccaaf0f1c92a4f5501a6c9f98ac4dcc0ef", "shasum": "" }, "require": { @@ -1499,7 +1625,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.28" }, "funding": [ { @@ -1507,7 +1634,7 @@ "type": "github" } ], - "time": "2023-03-06T12:58:08+00:00" + "time": "2023-09-12T14:36:20+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1752,16 +1879,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.7", + "version": "9.6.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" + "reference": "a122c2ebd469b751d774aa0f613dc0d67697653f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a122c2ebd469b751d774aa0f613dc0d67697653f", + "reference": "a122c2ebd469b751d774aa0f613dc0d67697653f", "shasum": "" }, "require": { @@ -1776,7 +1903,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-code-coverage": "^9.2.28", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -1835,7 +1962,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.12" }, "funding": [ { @@ -1851,7 +1978,7 @@ "type": "tidelift" } ], - "time": "2023-04-14T08:58:40+00:00" + "time": "2023-09-12T14:39:31+00:00" }, { "name": "sebastian/cli-parser", @@ -2153,16 +2280,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -2207,7 +2334,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -2215,7 +2342,7 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", @@ -2359,16 +2486,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -2411,7 +2538,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -2419,7 +2546,7 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -2871,7 +2998,8 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "helioviewer/event-interface": 20 + "helioviewer/event-interface": 20, + "matomo/device-detector": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/docroot/statistics/index.php b/docroot/statistics/index.php index 8ead70f6a..e5a9e02a1 100644 --- a/docroot/statistics/index.php +++ b/docroot/statistics/index.php @@ -19,8 +19,8 @@ google.load("visualization", "1", {packages:["corechart"]}); google.setOnLoadCallback(function (e) { temporalResolution = ""; - dateStart = ""; - dateEnd = ""; + dateStart = ""; + dateEnd = ""; setRefreshIntervalFromTemporalResolution(); getUsageStatistics(temporalResolution,dateStart,dateEnd); setInterval(checkSessionTimeout, 1000); @@ -48,13 +48,14 @@
+
- + \ No newline at end of file diff --git a/docroot/statistics/statistics.js b/docroot/statistics/statistics.js index 7d2061c5d..c6d8603f8 100644 --- a/docroot/statistics/statistics.js +++ b/docroot/statistics/statistics.js @@ -17,9 +17,9 @@ var heirarchy = { "Client Sites":["standard","embed","minimal"], "Images":["takeScreenshot","getTile","getClosestImage","getJP2Image-web","getJP2Image-jpip","getJP2Image","downloadScreenshot","getJPX","getJPXClosestToMidPoint", "downloadImage"], "Movies":["buildMovie","getMovieStatus","queueMovie","reQueueMovie","playMovie","downloadMovie","getUserVideos","getObservationDateVideos","uploadMovieToYouTube","checkYouTubeAuth","getYouTubeAuth"], - "Events":["getEventGlossary","getEvents","getFRMs","getEvent","getEventFRMs","getDefaultEventTypes","getEventsByEventLayers","importEvents"], + "Events":["getEventGlossary", "events", "getEvents","getFRMs","getEvent","getEventFRMs","getDefaultEventTypes","getEventsByEventLayers","importEvents"], "Data":["getRandomSeed","getDataSources","getJP2Header","getDataCoverage","getStatus","getNewsFeed","getDataCoverageTimeline","getClosestData","getSolarBodiesGlossary","getSolarBodies","getTrajectoryTime","sciScript-SSWIDL","sciScript-SunPy","getSciDataScript","updateDataCoverage"], - "Other":["shortenURL","getUsageStatistics","movie-notifications-granted","movie-notifications-denied","logNotificationStatistics","launchJHelioviewer"], + "Other":["shortenURL", "goto", "getUsageStatistics","movie-notifications-granted","movie-notifications-denied","logNotificationStatistics","launchJHelioviewer"], "WebGL":["getTexture","getGeometryServiceData"] }; @@ -71,7 +71,7 @@ var checkSessionTimeout = function () { var minutes = Math.abs((initialTime - new Date()) / 1000 / 60); if (minutes > refreshIntervalMinutes) { loadNewStatistics(); - } + } }; var loadNewStatistics = function(){ @@ -189,6 +189,7 @@ var displayUsageStatistics = function (data, timeInterval) { var pieChartHeight, barChartHeight, barChartMargin, summaryRaw, max; var hvTypePieSummary = {}; var notificationPieSummary = {}; + let deviceSummary = data['device_summary']; var movieSourcesSummary = data['movieCommonSources']; var movieLayerCountSummary = data['movieLayerCount']; var screenshotSourcesSummary = data['screenshotCommonSources']; @@ -213,6 +214,7 @@ var displayUsageStatistics = function (data, timeInterval) { createScreenshotLayerCountChart('screenshotLayerCountChart',screenshotLayerCountSummary, pieChartHeight); createMovieSourcesChart('movieSourcesChart', movieSourcesSummary, pieChartHeight); createMovieLayerCountChart('movieLayerCountChart',movieLayerCountSummary, pieChartHeight); + createDeviceChart('deviceChart', deviceSummary, pieChartHeight); var colorMod = 0; var excludeFromCharts = ['movieCommonSources','movieLayerCount','screenshotCommonSources','screenshotLayerCount','summary']; @@ -258,7 +260,7 @@ var displayUsageStatistics = function (data, timeInterval) { content.style.maxHeight = null; } else { content.style.maxHeight = content.scrollHeight + "px"; - } + } console.log(contentId + " " + content.style.maxHeight); } } @@ -271,7 +273,7 @@ var displayUsageStatistics = function (data, timeInterval) { content.style.maxHeight = content.scrollHeight + "px"; } } - } + } // Create bar graphs for each request type /* @@ -367,7 +369,7 @@ var displaySummaryText = function(timeInterval, summary) { } $("#timeRange").hide(); } - + //temporal resolution dropdown change event listener $("#temporalResolution").val(timeInterval).unbind().change(function(e){ temporalResolution = $("#temporalResolution").val(); @@ -626,3 +628,20 @@ var createMovieLayerCountChart = function (id, totals, size) { chart = new google.visualization.PieChart(document.getElementById(id)); chart.draw(data, {width: size, height: size*pieHeightScale, colors: colors, title: "Movie Layer Count"}); }; + +function createDeviceChart(id, deviceSummary, size) { + var chart, width, data = new google.visualization.DataTable(); + + data.addColumn('string', 'device'); + data.addColumn('number', 'requests'); + + let devices = Object.keys(deviceSummary); + + //populate the chart + for(let device of devices){ + data.addRows([[device, deviceSummary[device]]]); + } + + chart = new google.visualization.PieChart(document.getElementById(id)); + chart.draw(data, {width: size, height: size*pieHeightScale, colors: colors, title: "Client Devices"}); +}; \ No newline at end of file diff --git a/install/database/2023_09_13_device_stats.sql b/install/database/2023_09_13_device_stats.sql new file mode 100644 index 000000000..4a4df50d3 --- /dev/null +++ b/install/database/2023_09_13_device_stats.sql @@ -0,0 +1,4 @@ +-- Add device column to statistics table +ALTER TABLE `statistics` ADD COLUMN device VARCHAR(64) DEFAULT 'x'; +-- Add device column to redis_stats table +ALTER TABLE `redis_stats` ADD COLUMN device VARCHAR(64) DEFAULT 'x'; diff --git a/install/helioviewer/db.py b/install/helioviewer/db.py index 0544e64b5..1ef1cbdf0 100644 --- a/install/helioviewer/db.py +++ b/install/helioviewer/db.py @@ -866,6 +866,7 @@ def create_redis_stats_table(cursor): `datetime` datetime NOT NULL, `action` varchar(32) NOT NULL, `count` int unsigned NOT NULL, + `device` VARCHAR(64) DEFAULT 'x', PRIMARY KEY (`datetime`, `action`) ) DEFAULT CHARSET=utf8;""") @@ -1006,6 +1007,7 @@ def create_statistics_table(cursor): `id` INT unsigned NOT NULL auto_increment, `timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `action` VARCHAR(32) NOT NULL, + `device` VARCHAR(64) DEFAULT 'x', PRIMARY KEY (`id`), KEY `date_idx` (`timestamp`,`action`) ) DEFAULT CHARSET=utf8;""") diff --git a/src/Database/Statistics.php b/src/Database/Statistics.php index 738d999be..ef2fbee42 100644 --- a/src/Database/Statistics.php +++ b/src/Database/Statistics.php @@ -12,6 +12,12 @@ * @link https://github.com/Helioviewer-Project/ */ +require_once HV_ROOT_DIR . "/../vendor/autoload.php"; +require_once HV_ROOT_DIR . "/../src/Helper/ArrayExtensions.php"; + +use DeviceDetector\ClientHints; +use DeviceDetector\DeviceDetector; + class Database_Statistics { private $_dbConnection; @@ -93,6 +99,36 @@ public function __construct() { $this->_dbConnection = new Database_DbConnection(); } + /** + * Gets device information from the user agent + */ + protected function _GetDevice() { + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ""; + $clientHints = ClientHints::factory($_SERVER); + + $dd = new DeviceDetector($userAgent, $clientHints); + + // OPTIONAL: Set caching method + // By default static cache is used, which works best within one php process (memory array caching) + // To cache across requests use caching in files or memcache + require_once HV_ROOT_DIR . "/../src/Helper/RedisCache.php"; + $dd->setCache(new RedisCache(HV_REDIS_HOST, HV_REDIS_PORT)); + $dd->parse(); + + if ($dd->isBot()) { + // handle bots,spiders,crawlers,... + return $dd->getBot()["name"]; + } else { + // Assume that if its a browser, then DD can determine whether it's a desktop/smartphone/tablet + if ($dd->isBrowser()) { + return $dd->getDeviceName(); + } else { + // Otherwise, use the client name + return $dd->getClient('name'); + } + } + } + /** * Add a new entry to the `statistics` table * @@ -101,13 +137,16 @@ public function __construct() { * @return boolean */ public function log($action) { + $device = $this->_GetDevice(); $sql = sprintf( "INSERT INTO statistics " . "SET " . "id " . " = NULL, " . "timestamp " . " = NULL, " - . "action " . " = '%s';", - $this->_dbConnection->link->real_escape_string($action) + . "action " . " = '%s', " + . "device " . " = '%s';", + $this->_dbConnection->link->real_escape_string($action), + $this->_dbConnection->link->real_escape_string($device) ); try { $result = $this->_dbConnection->query($sql); @@ -170,7 +209,7 @@ public function logRedis($action, $redis = null){ $dateTimeNearestHour = explode(":",$dateTimeNoMilis)[0] . ":00:00"; // strip out minutes and seconds // Make compound key - $key = HV_REDIS_STATS_PREFIX . "/" . $dateTimeNearestHour . "/" . $action; + $key = HV_REDIS_STATS_PREFIX . "/" . $dateTimeNearestHour . "/" . $action . "/" . $this->_GetDevice(); $redis->incr($key); }catch(Exception $e){ //continue gracefully if redis statistics logging fails @@ -195,6 +234,7 @@ public function saveStatisticsFromRedis($redis){ "datetime" => $keyComponents[1], "action" => $keyComponents[2], "count" => $count, + "device" => $keyComponents[3] ?? 'x', "key" => $key ); array_push($statisticsData, $statistic); @@ -206,11 +246,13 @@ public function saveStatisticsFromRedis($redis){ . "SET " . "datetime " . " = '%s', " . "action " . " = '%s', " - . "count " . " = %d " + . "count " . " = %d, " + . "device = '%s' " . "ON DUPLICATE KEY UPDATE count = %d;", $this->_dbConnection->link->real_escape_string($data["datetime"]), $this->_dbConnection->link->real_escape_string($data["action"]), $this->_dbConnection->link->real_escape_string($data["count"]), + $this->_dbConnection->link->real_escape_string($data['device']), $this->_dbConnection->link->real_escape_string($data["count"]) ); try { @@ -385,17 +427,17 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu "getClosestData" => array(), "getClosestImage" => array(), "getJPX" => array(), - "getJPXClosestToMidPoint" => array(), + "getJPXClosestToMidPoint" => array(), "takeScreenshot" => array(), "uploadMovieToYouTube" => array(), "embed" => array(), - "minimal" => array(), - "standard" => array(), - "sciScript-SSWIDL" => array(), - "sciScript-SunPy" => array(), - "movie-notifications-granted" => array(), - "movie-notifications-denied" => array(), - "getJP2Image-web" => array(), + "minimal" => array(), + "standard" => array(), + "sciScript-SSWIDL" => array(), + "sciScript-SunPy" => array(), + "movie-notifications-granted" => array(), + "movie-notifications-denied" => array(), + "getJP2Image-web" => array(), "getJP2Image-jpip" => array(), "getRandomSeed" => array(), "totalRequests" => array() @@ -407,22 +449,24 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu "getClosestData" => 0, "getClosestImage" => 0, "getJPX" => 0, - "getJPXClosestToMidPoint" => 0, + "getJPXClosestToMidPoint" => 0, "takeScreenshot" => 0, "uploadMovieToYouTube" => 0, "embed" => 0, - "minimal" => 0, - "standard" => 0, - "sciScript-SSWIDL" => 0, - "sciScript-SunPy" => 0, - "movie-notifications-granted" => 0, - "movie-notifications-denied" => 0, - "getJP2Image-web" => 0, + "minimal" => 0, + "standard" => 0, + "sciScript-SSWIDL" => 0, + "sciScript-SunPy" => 0, + "movie-notifications-granted" => 0, + "movie-notifications-denied" => 0, + "getJP2Image-web" => 0, "getJP2Image-jpip" => 0, "getRandomSeed" => 0, "totalRequests" => 0 ); + // Stores the counts for the number of requests come from specific devices + $device_counts = array(); $new_counts = $this->_createCountsArray(); $new_summary = $this->_createSummaryArray(); //final counts summary @@ -470,34 +514,6 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu $dateEnd = toMySQLDateString($date); $intervalEndDate = $dateEnd; - //begin statistics table data gathering - /* - $sql = sprintf( - "SELECT action, COUNT(id) AS count " - . "FROM statistics " - . "WHERE " - . "timestamp BETWEEN '%s' AND '%s' " - . "GROUP BY action;", - $this->_dbConnection->link->real_escape_string($dateStart), - $this->_dbConnection->link->real_escape_string($dateEnd) - ); - try { - $result = $this->_dbConnection->query($sql); - } - catch (Exception $e) { - return false; - } - - // Append counts for each API action during that interval - // to the appropriate array - while ($count = $result->fetch_array(MYSQLI_ASSOC)) { - $num = (int)$count['count']; - - $counts[$count['action']][$i][$dateIndex] = $num; - $summary[$count['action']] += $num; - } - */ - //redis table statistics gathering for total number of calls $sql = sprintf( "SELECT SUM(count) AS count " @@ -525,7 +541,6 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu //additional real-time redis stats for total number of calls $statisticsKeys = $redis->keys(HV_REDIS_STATS_PREFIX . "/*"); - $statisticsData = array(); $realTimeRedisCount = 0; foreach( $statisticsKeys as $key ){ $count = (int)$redis->get($key); @@ -539,6 +554,8 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu if($realTimeRedisCount > 0){ $new_counts['total'][$i][$dateIndex] += $realTimeRedisCount; $new_summary['total'] += $realTimeRedisCount; + $device = $keyComponents[3] ?? 'x'; + array_add($device_counts, $device, $realTimeRedisCount); } //new redis-stats statistics gathering for all api endpoints @@ -567,9 +584,31 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu $new_summary[$count['action']] += $num; } + // Get device request counts + $sql = sprintf( + "SELECT device, SUM(count) AS count " + . "FROM redis_stats " + . "WHERE " + . "datetime >= '%s' AND datetime < '%s'" + ."GROUP BY device; ", + $this->_dbConnection->link->real_escape_string($dateStart), + $this->_dbConnection->link->real_escape_string($dateEnd) + ); + try { + $result = $this->_dbConnection->query($sql); + } + catch (Exception $e) { + return false; + } + + // Append counts for each device + while ($count = $result->fetch_array(MYSQLI_ASSOC)) { + $num = (int)$count['count']; + array_add($device_counts, $count['device'], $num); + } + //additional real-time redis stats for all api endpoints $statisticsKeys = $redis->keys(HV_REDIS_STATS_PREFIX . "/*"); - $statisticsData = array(); foreach( $statisticsKeys as $key ){ $realTimeRedisCount = 0; $count = (int)$redis->get($key); @@ -744,128 +783,152 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu $counts['summary'] = $summary; $new_counts['summary'] = $new_summary; $new_counts['rate_limit_exceeded'] = $rateLimitExceeded; + // 'x' represents requests before we started logging devices + unset($device_counts['x']); + + // 'UNK' represents clients that the device detector didn't recognize. + $device_counts['unrecognized'] = $device_counts['UNK'] ?? 0; + unset($device_counts['UNK']); + + $new_counts['device_summary'] = $device_counts; return json_encode($new_counts); } + /** + * Gets client device statistics. + * This returns a list of devices and the number of requests made by each device over the given time period + * @param string $start Start date of time range that will be passed to the SQL query + * @param string $end End date of time range that will be passed to the SQL query + */ + protected function _QueryDeviceStatistics(string $start, string $end): array { + + } + private function _createCountsArray(){ return array( - "total" => array(), - 'downloadScreenshot' => array(), - 'getClosestImage' => array(), - 'getDataSources' => array(), - 'getJP2Header' => array(), - 'getNewsFeed' => array(), - 'getStatus' => array(), - 'getSciDataScript' => array(), - 'getTile' => array(), - 'getUsageStatistics' => array(), - 'getDataCoverageTimeline' => array(), - 'getDataCoverage' => array(), - 'updateDataCoverage' => array(), - 'shortenURL' => array(), - 'takeScreenshot' => array(), - 'getRandomSeed' => array(), - 'getJP2Image' => array(), - 'getJPX' => array(), - 'getJPXClosestToMidPoint' => array(), - 'launchJHelioviewer' => array(), - 'downloadMovie' => array(), - 'getMovieStatus' => array(), - 'playMovie' => array(), - 'queueMovie' => array(), - 'reQueueMovie' => array(), - 'uploadMovieToYouTube' => array(), - 'checkYouTubeAuth' => array(), - 'getYouTubeAuth' => array(), - 'getUserVideos' => array(), + "total" => array(), + 'downloadScreenshot' => array(), + 'getClosestImage' => array(), + 'getDataSources' => array(), + 'getJP2Header' => array(), + 'getNewsFeed' => array(), + 'getStatus' => array(), + 'getSciDataScript' => array(), + 'getTile' => array(), + 'downloadImage' => array(), + 'getUsageStatistics' => array(), + 'getDataCoverageTimeline' => array(), + 'getDataCoverage' => array(), + 'updateDataCoverage' => array(), + 'shortenURL' => array(), + 'goto' => array(), + 'takeScreenshot' => array(), + 'getRandomSeed' => array(), + 'getJP2Image' => array(), + 'getJPX' => array(), + 'getJPXClosestToMidPoint' => array(), + 'launchJHelioviewer' => array(), + 'downloadMovie' => array(), + 'getMovieStatus' => array(), + 'playMovie' => array(), + 'queueMovie' => array(), + 'reQueueMovie' => array(), + 'uploadMovieToYouTube' => array(), + 'checkYouTubeAuth' => array(), + 'getYouTubeAuth' => array(), + 'getUserVideos' => array(), 'getObservationDateVideos' => array(), - 'getEventFRMs' => array(), - 'getEvent' => array(), - 'getFRMs' => array(), - 'getDefaultEventTypes' => array(), - 'getEvents' => array(), - 'importEvents' => array(), - 'getEventsByEventLayers' => array(), - 'getEventGlossary' => array(), - 'getSolarBodiesGlossary' => array(), - 'getSolarBodies' => array(), - 'getTrajectoryTime' => array(), - 'logNotificationStatistics' => array(), - 'getTexture' => array(), - 'getGeometryServiceData' => array(), - 'buildMovie' => array(),//this one happens in HelioviewerMovie.php - "getClosestData" => array(), - "embed" => array(), - "minimal" => array(), - "standard" => array(), - "sciScript-SSWIDL" => array(), - "sciScript-SunPy" => array(), - "movie-notifications-granted" => array(), - "movie-notifications-denied" => array(), - "getJP2Image-web" => array(), - "getJP2Image-jpip" => array() + 'events' => array(), + 'getEventFRMs' => array(), + 'getEvent' => array(), + 'getFRMs' => array(), + 'getDefaultEventTypes' => array(), + 'getEvents' => array(), + 'importEvents' => array(), + 'getEventsByEventLayers' => array(), + 'getEventGlossary' => array(), + 'getSolarBodiesGlossary' => array(), + 'getSolarBodies' => array(), + 'getTrajectoryTime' => array(), + 'logNotificationStatistics' => array(), + 'getTexture' => array(), + 'getGeometryServiceData' => array(), + 'buildMovie' => array(),//this one happens in HelioviewerMovie.php + "getClosestData" => array(), + "embed" => array(), + "minimal" => array(), + "standard" => array(), + "sciScript-SSWIDL" => array(), + "sciScript-SunPy" => array(), + "movie-notifications-granted" => array(), + "movie-notifications-denied" => array(), + "getJP2Image-web" => array(), + "getJP2Image-jpip" => array() ); } private function _createSummaryArray(){ return array( - "total" => 0, - 'downloadScreenshot' => 0, - 'getClosestImage' => 0, - 'getDataSources' => 0, - 'getJP2Header' => 0, - 'getNewsFeed' => 0, - 'getStatus' => 0, - 'getSciDataScript' => 0, - 'getTile' => 0, - 'getUsageStatistics' => 0, - 'getDataCoverageTimeline' => 0, - 'getDataCoverage' => 0, - 'updateDataCoverage' => 0, - 'shortenURL' => 0, - 'takeScreenshot' => 0, - 'getRandomSeed' => 0, - 'getJP2Image' => 0, - 'getJPX' => 0, - 'getJPXClosestToMidPoint' => 0, - 'launchJHelioviewer' => 0, - 'downloadMovie' => 0, - 'getMovieStatus' => 0, - 'playMovie' => 0, - 'queueMovie' => 0, - 'reQueueMovie' => 0, - 'uploadMovieToYouTube' => 0, - 'checkYouTubeAuth' => 0, - 'getYouTubeAuth' => 0, - 'getUserVideos' => 0, + "total" => 0, + 'downloadScreenshot' => 0, + 'getClosestImage' => 0, + 'getDataSources' => 0, + 'getJP2Header' => 0, + 'getNewsFeed' => 0, + 'getStatus' => 0, + 'getSciDataScript' => 0, + 'getTile' => 0, + 'downloadImage' => 0, + 'getUsageStatistics' => 0, + 'getDataCoverageTimeline' => 0, + 'getDataCoverage' => 0, + 'updateDataCoverage' => 0, + 'shortenURL' => 0, + 'goto' => 0, + 'takeScreenshot' => 0, + 'getRandomSeed' => 0, + 'getJP2Image' => 0, + 'getJPX' => 0, + 'getJPXClosestToMidPoint' => 0, + 'launchJHelioviewer' => 0, + 'downloadMovie' => 0, + 'getMovieStatus' => 0, + 'playMovie' => 0, + 'queueMovie' => 0, + 'reQueueMovie' => 0, + 'uploadMovieToYouTube' => 0, + 'checkYouTubeAuth' => 0, + 'getYouTubeAuth' => 0, + 'getUserVideos' => 0, 'getObservationDateVideos' => 0, - 'getEventFRMs' => 0, - 'getEvent' => 0, - 'getFRMs' => 0, - 'getDefaultEventTypes' => 0, - 'getEvents' => 0, - 'importEvents' => 0, - 'getEventsByEventLayers' => 0, - 'getEventGlossary' => 0, - 'getSolarBodiesGlossary' => 0, - 'getSolarBodies' => 0, - 'getTrajectoryTime' => 0, - 'logNotificationStatistics' => 0, - 'getTexture' => 0, - 'getGeometryServiceData' => 0, - 'buildMovie' => 0,//this one happens in HelioviewerMovie.php - "getClosestData" => 0, - "embed" => 0, - "minimal" => 0, - "standard" => 0, - "sciScript-SSWIDL" => 0, - "sciScript-SunPy" => 0, - "movie-notifications-granted" => 0, - "movie-notifications-denied" => 0, - "getJP2Image-web" => 0, - "getJP2Image-jpip" => 0, - "rate_limit_exceeded" => 0 + 'events' => 0, + 'getEventFRMs' => 0, + 'getEvent' => 0, + 'getFRMs' => 0, + 'getDefaultEventTypes' => 0, + 'getEvents' => 0, + 'importEvents' => 0, + 'getEventsByEventLayers' => 0, + 'getEventGlossary' => 0, + 'getSolarBodiesGlossary' => 0, + 'getSolarBodies' => 0, + 'getTrajectoryTime' => 0, + 'logNotificationStatistics' => 0, + 'getTexture' => 0, + 'getGeometryServiceData' => 0, + 'buildMovie' => 0,//this one happens in HelioviewerMovie.php + "getClosestData" => 0, + "embed" => 0, + "minimal" => 0, + "standard" => 0, + "sciScript-SSWIDL" => 0, + "sciScript-SunPy" => 0, + "movie-notifications-granted" => 0, + "movie-notifications-denied" => 0, + "getJP2Image-web" => 0, + "getJP2Image-jpip" => 0, + "rate_limit_exceeded" => 0 ); } diff --git a/src/Helper/ArrayExtensions.php b/src/Helper/ArrayExtensions.php new file mode 100644 index 000000000..170d612f1 --- /dev/null +++ b/src/Helper/ArrayExtensions.php @@ -0,0 +1,19 @@ +_redis = new Redis(); + $this->_redis->connect($host, $port); + } + + /** + * Return cached value for the given id + * @param string $id + * + * @return mixed + */ + public function fetch(string $id): mixed { + $value = $this->_redis->get($id); + if ($value !== false) { + return unserialize($value); + } + return false; + } + + /** + * Returns if a given cache key exists + * @param string $id + * + * @return bool + */ + public function contains(string $id): bool { + return $this->_redis->exists($id) > 0; + } + + /** + * Add an entry to the cache with an expiration date + * @param string $id + * @param mixed $data + * @param int $lifeTime + * + * @return bool + */ + public function save(string $id, mixed $data, int $lifeTime = 0): bool { + $options = []; + // Set expiration date in options only if lifetime is > 0 + if ($lifeTime > 0) { + $options['EX'] = $lifeTime; + } + return $this->_redis->set($id, serialize($data), $options); + } + + /** + * Delete an entry from the cache + * @param string $id + * + * @return bool + */ + public function delete(string $id): bool { + return $this->_redis->del($id) > 0; + } + + /** + * Flush all cache entries + * @return bool + */ + public function flushAll(): bool { + error_log("Warning: flushAll called on RedisCache instance, but it is unimplemented."); + return true; + } +} \ No newline at end of file diff --git a/tests/unit_tests/statistics/StatisticsTest.php b/tests/unit_tests/statistics/StatisticsTest.php index 84d70214a..ff36b3798 100644 --- a/tests/unit_tests/statistics/StatisticsTest.php +++ b/tests/unit_tests/statistics/StatisticsTest.php @@ -8,6 +8,11 @@ // File under test include_once HV_ROOT_DIR.'/../src/Database/Statistics.php'; +class StatisticsTestHarness extends Database_Statistics { + public function __construct() { parent::__construct(); } + public function GetDevice() { return $this->_GetDevice(); } +} + final class StatisticsTest extends TestCase { public function testGetUsageStatistics(): void @@ -35,4 +40,47 @@ public function testSaveRedisStats(): void $statistics->saveStatisticsFromRedis($redis); } + + public function testGetDevice_NoUserAgent() { + $stats = new StatisticsTestHarness(); + $result = $stats->GetDevice(); + $this->assertEquals("UNK", $result); + } + + /** + * Get device is used to identify the device via the user agent. + * Test with a few known user agents. + */ + public function testGetDevice(): void { + // Known answer tests + $kats = array( + [ + 'UserAgent' => "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1", + 'ExpectedResult' => 'smartphone' + ], + [ + 'UserAgent' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:102.0) Gecko/20100101 Firefox/102.0", + 'ExpectedResult' => 'desktop' + ], + [ + 'UserAgent' => "python-requests/2.31.0", + 'ExpectedResult' => 'Python Requests' + ], + [ + 'UserAgent' => "JHV/SWHV-4.4.2.10777 (x86_64 Mac OS X 13.2.1) Eclipse Adoptium JRE 19.0.2", + 'ExpectedResult' => 'JHelioviewer' + ], + [ + 'UserAgent' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)", + 'ExpectedResult' => 'Applebot' + ] + ); + + $stats = new StatisticsTestHarness(); + foreach ($kats as $kat) { + $_SERVER['HTTP_USER_AGENT'] = $kat['UserAgent']; + $result = $stats->GetDevice(); + $this->assertEquals($kat['ExpectedResult'], $result); + } + } }