├── .github └── workflows │ ├── PHPStan.yml │ ├── laravel-pint.yml │ ├── run-tests.yml │ └── update-changelog.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── analytics.php ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── phpunit.xml.dist ├── phpunit.xml.dist.bak ├── src ├── Analytics.php ├── AnalyticsServiceProvider.php ├── Credentials.php ├── Exceptions │ ├── InvalidCredentialsArrayException.php │ ├── InvalidCredentialsFileException.php │ ├── InvalidCredentialsJsonStringException.php │ ├── InvalidFilterException.php │ └── InvalidPropertyIdException.php ├── Reports │ └── Reports.php ├── Request │ ├── Dimensions.php │ ├── Filters │ │ ├── AndGroup.php │ │ ├── BetweenFilter.php │ │ ├── Filter.php │ │ ├── FilterContract.php │ │ ├── FilterExpression.php │ │ ├── FilterExpressionContract.php │ │ ├── FilterExpressionField.php │ │ ├── FilterExpressionList.php │ │ ├── FilterField.php │ │ ├── InListFilter.php │ │ ├── NotExpression.php │ │ ├── NumericFilter.php │ │ ├── NumericValueType.php │ │ ├── OrGroup.php │ │ └── StringFilter.php │ ├── Metrics.php │ └── RequestData.php └── Response │ ├── DimensionHeader.php │ ├── DimensionValue.php │ ├── Metadata.php │ ├── MetricHeader.php │ ├── MetricValue.php │ ├── PropertyQuota.php │ ├── Quotas │ ├── ConcurrentRequests.php │ ├── PotentiallyThresholdedRequestsPerHour.php │ ├── ServerErrorsPerProjectPerHour.php │ ├── TokensPerDay.php │ ├── TokensPerHour.php │ └── TokensPerProjectPerHour.php │ ├── ResponseData.php │ ├── Row.php │ └── Total.php └── tests ├── AnalyticsTest.php ├── CredentialsTest.php ├── DimensionsTest.php ├── FiltersTest.php ├── Helpers ├── CustomDimensions.php └── CustomMetrics.php ├── MetricsTest.php ├── ReportTest.php └── TestCase.php /.github/workflows/PHPStan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | phpstan: 11 | name: phpstan 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: '8.1' 20 | coverage: none 21 | 22 | - name: Install composer dependencies 23 | uses: ramsey/composer-install@v1 24 | 25 | - name: Run PHPStan 26 | run: ./vendor/bin/phpstan --error-format=github 27 | -------------------------------------------------------------------------------- /.github/workflows/laravel-pint.yml: -------------------------------------------------------------------------------- 1 | name: Run Laravel Pint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | php-code-styling: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Fix PHP code style issues 18 | uses: aglipanci/laravel-pint-action@latest 19 | with: 20 | pintVersion: 1.18.1 21 | 22 | - name: Commit changes 23 | uses: stefanzweifel/git-auto-commit-action@v4 24 | with: 25 | commit_message: Fix styling 26 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | php: [8.1, 8.2, 8.3] 17 | laravel: [10.*, 11.*] 18 | stability: [prefer-stable] 19 | exclude: 20 | - php: 8.1 21 | laravel: 11.* 22 | include: 23 | - laravel: 10.* 24 | testbench: 8.* 25 | - laravel: 11.* 26 | testbench: 9.* 27 | 28 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 29 | 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php }} 38 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, pcov 39 | coverage: pcov 40 | 41 | - name: Setup problem matchers 42 | run: | 43 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 44 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 45 | 46 | - name: Install dependencies 47 | run: | 48 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 49 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 50 | 51 | - name: List Installed Dependencies 52 | run: composer show -D 53 | 54 | - name: Execute tests 55 | run: vendor/bin/phpunit 56 | 57 | - name: Check coverage 58 | run: vendor/bin/coverage-check build/logs/clover.xml 100 59 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | 2 | name: "Update Changelog" 3 | 4 | on: 5 | release: 6 | types: [released] 7 | 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | with: 16 | ref: main 17 | 18 | - name: Update Changelog 19 | uses: stefanzweifel/changelog-updater-action@v1 20 | with: 21 | latest-version: ${{ github.event.release.name }} 22 | release-notes: ${{ github.event.release.body }} 23 | 24 | - name: Commit updated CHANGELOG 25 | uses: stefanzweifel/git-auto-commit-action@v4 26 | with: 27 | branch: main 28 | commit_message: Update CHANGELOG 29 | file_pattern: CHANGELOG.md 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | phpunit.xml 3 | /vendor/ 4 | .idea 5 | .phpunit.result.cache 6 | .phpunit.cache 7 | /build 8 | /coverage 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Analytics` will be documented in this file. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/garrettmassey/analytics). 6 | 7 | Your pull request cannot be merged unless it passes existing tests, and meets the code coverage requirements set by the repo. 8 | 9 | # Things you could do 10 | If you want to contribute but do not know where to start, this list provides some starting points. 11 | - Set up TravisCI, StyleCI, ScrutinizerCI 12 | - Consider backwards compatability with Laravel 6 & 7, and PHP7.4 + 13 | - Suggest new pre-built queries and reports! 14 | - Contribute when the Google Analytics Data API leaves beta 15 | 16 | 17 | ## Pull Requests 18 | 19 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 20 | 21 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 22 | 23 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 24 | 25 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 26 | 27 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 28 | 29 | 30 | **Happy coding**! 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The license 2 | 3 | Copyright © 2022 [Garrett Massey](https://www.garrettmassey.net/) | [contact@garrettmassey.net](mailto:contact@garrettmassey.net) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Analytics 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Total Downloads][ico-downloads]][link-downloads] 5 | [![Tests][ico-tests]][link-tests] 6 | 7 | Build Google Analytics Data API queries in Laravel with ease! 8 | 9 | Methods currently return an instance of `Gtmassey\LaravelAnalytics\ResponseData`, containing the dimension and metric headers, and results in `rows`. 10 | 11 | **Table of Contents:** 12 | 13 | * [Installation](#instal) 14 | * [Setup](#setup) 15 | * [As ENV file (default)](#env) 16 | * [Separate JSON File](#json) 17 | * [JSON String](#jsonString) 18 | * [Separate Values](#vals) 19 | * [Usage](#usage) 20 | * [Query Builder](#querybuilder) 21 | * [Filtering](#filtering) 22 | * [Default Reports](#defaultreports) 23 | * [Extensibility](#extensibility) 24 | * [Custom Metrics And Dimensions](#custommetrics) 25 | * [Reusable Filters](#reusablefilters) 26 | * [Changelog](#changelog) 27 | * [Testing](#testing) 28 | 29 | ## Installation 30 | 31 | Via Composer 32 | 33 | ```SHELL 34 | composer require gtmassey/laravel-analytics 35 | ``` 36 | 37 | ## Setup 38 | 39 | To use this package, you must have a Google Cloud Service Accounts Credential. 40 | 41 | If you do not have a project set up on Google Cloud Platform, visit [console.cloud.google.com/projectcreate](https://console.cloud.google.com/projectcreate) to create a new project. 42 | 43 | Once you have a project, make sure you have selected that project in the top left corner of the console. 44 | 45 | ![Screen Shot 2022-11-30 at 2 22 35 PM](https://user-images.githubusercontent.com/109831143/204912891-624b9403-12f4-484a-8d12-368d8b56a805.png) 46 | 47 | Select APIs & Services from the quick access cards on the dashboard. 48 | 49 | ![Screen Shot 2022-11-30 at 2 22 54 PM](https://user-images.githubusercontent.com/109831143/204912927-8acb75be-f92e-451c-8e06-cae33430425a.png) 50 | 51 | Make sure you have Google Analytics Data API enabled. NOTE: this is NOT the same API as Google Analytics API. The Data API is the required API for this package. If you do not have the Google Analytics Data API enabled, you can add it to your Cloud Console account by clicking "enable APIs and Services" 52 | 53 | ![Screen Shot 2022-11-30 at 2 23 25 PM](https://user-images.githubusercontent.com/109831143/204913094-e8967a6b-fed8-43c1-afbc-fafa3c6d0907.png) 54 | 55 | You can search for the Google Analytics Data API and enable it through the Google API Library 56 | 57 | ![Screen Shot 2022-11-30 at 2 24 09 PM](https://user-images.githubusercontent.com/109831143/204913299-365425cb-b71e-41fe-aa55-fbefcb7c6b6f.png) 58 | 59 | ![Screen Shot 2022-11-30 at 2 24 21 PM](https://user-images.githubusercontent.com/109831143/204913340-17655e94-8233-404d-9c29-ed100273453c.png) 60 | 61 | Once enabled, select the Google Analytics Data API from the list of APIs, and click the Credentials tab. 62 | 63 | ![Screen Shot 2022-11-30 at 2 24 48 PM](https://user-images.githubusercontent.com/109831143/204913379-d751a05f-5884-4f8e-a952-845312f1cad5.png) 64 | 65 | If you already have a service account set up with this API, you can skip the next step. 66 | 67 | Click the Create Credentials button, and select Service Account. 68 | 69 | ![Screen Shot 2022-11-30 at 2 26 24 PM](https://user-images.githubusercontent.com/109831143/204913480-2eaa83e4-f786-4815-9848-d533587d0a51.png) 70 | 71 | Select the role you want to assign to the service account. For this package, the minimum role is the Viewer role. 72 | 73 | Once your service account has been created, click on the account to go to the IAM & Admin section of Google Cloud Console. 74 | 75 | In the Service Accounts section of the IAM & Admin page, select the appropriate service account, and create a new JSON key for the account: 76 | 77 | ![Screen Shot 2022-11-30 at 2 27 01 PM](https://user-images.githubusercontent.com/109831143/204913731-8907f7ec-17ad-453e-8721-7098d71e3ab9.png) 78 | 79 | ![Screen Shot 2022-11-30 at 2 27 14 PM](https://user-images.githubusercontent.com/109831143/204913810-41a9739b-fdb9-4500-8e81-aaf083bb873c.png) 80 | 81 | Once the key is created, download the JSON file and save it somewhere safe. You will need this file to use this package. If you lose this file, you will have to create a new service account. Google does not let you re-issue keys. 82 | 83 | You can use these credentials in several ways: 84 | 85 | ### As ENV value (default) 86 | 87 | This is ideal setup if you're using only one service account for your application. 88 | 89 | Specify the path to the JSON file in your .env file: 90 | 91 | ```dotenv 92 | GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json 93 | ``` 94 | 95 | ### As a separate JSON file 96 | 97 | If you have multiple service accounts, you can instruct this package to use a specific one: 98 | 99 | ```dotenv 100 | ANALYTICS_CREDENTIALS_USE_ENV=false 101 | ANALYTICS_CREDENTIALS_FILE=/path/to/credentials.json 102 | ``` 103 | 104 | ### As a JSON string 105 | 106 | If you don't want to store the credentials in a file, you can specify the JSON string directly in your .env file: 107 | 108 | ```dotenv 109 | ANALYTICS_CREDENTIALS_USE_ENV=false 110 | ANALYTICS_CREDENTIALS_JSON="{type: service_account, project_id: ...}" 111 | ``` 112 | 113 | ### As separate values 114 | 115 | You can also specify the credentials as separate values in your .env file: 116 | 117 | ```dotenv 118 | ANALYTICS_CREDENTIALS_USE_ENV=false 119 | ANALYTICS_CREDENTIALS_TYPE=service_account 120 | ANALYTICS_CREDENTIALS_PROJECT_ID=... 121 | ANALYTICS_CREDENTIALS_PRIVATE_KEY_ID=... 122 | ANALYTICS_CREDENTIALS_PRIVATE_KEY=... 123 | ANALYTICS_CREDENTIALS_CLIENT_EMAIL=... 124 | ANALYTICS_CREDENTIALS_CLIENT_ID=... 125 | ANALYTICS_CREDENTIALS_AUTH_URI=... 126 | ANALYTICS_CREDENTIALS_TOKEN_URI=... 127 | ANALYTICS_CREDENTIALS_AUTH_PROVIDER_X509_CERT_URL=... 128 | ANALYTICS_CREDENTIALS_CLIENT_X509_CERT_URL=... 129 | ``` 130 | 131 | > **Warning** 132 | > Package will always prioritize `GOOGLE_APPLICATION_CREDENTIALS` env value over other options. If you want to use a separate service account, make sure to set `ANALYTICS_CREDENTIALS_USE_ENV=false`. 133 | 134 | Finally, open Google Analytics, and copy the property ID for the property you want to query. You will need this ID to use this package. 135 | 136 | ![Screen Shot 2022-11-30 at 2 40 42 PM](https://user-images.githubusercontent.com/109831143/204914645-385e0b5c-f248-4dad-b99c-2064f5ca8be6.png) 137 | 138 | Set the property ID in your `.env` file. 139 | 140 | ```dotenv 141 | ANALYTICS_PROPERTY_ID="XXXXXXXXX" 142 | ``` 143 | 144 | Now you're ready to start! 145 | 146 | ## Usage 147 | 148 | Once installation is complete, you can run Google Analytics Data API queries in your application. 149 | 150 | All Google Analytics Data API queries require a date range to be run. Use the `Period` class to generate a period of time for the query. 151 | 152 | ### Query Builder: 153 | 154 | ```php 155 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 156 | use Gtmassey\LaravelAnalytics\Request\Metrics; 157 | use Gtmassey\LaravelAnalytics\Analytics; 158 | use Gtmassey\Period\Period; 159 | use Carbon\Carbon; 160 | 161 | $report = Analytics::query() 162 | ->setMetrics(fn(Metrics $metrics) => $metrics 163 | ->active1DayUsers() 164 | ->active7DayUsers() 165 | ->active28DayUsers() 166 | ) 167 | ->forPeriod(Period::defaultPeriod()) 168 | ->run(); 169 | 170 | $report2 = Analytics::query() 171 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 172 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->pageTitle()) 173 | ->forPeriod(Period::create(Carbon::now()->subDays(30), Carbon::now())) 174 | ->run(); 175 | ``` 176 | 177 | ### Filtering: 178 | 179 | Filtering closely follows [Google Analytics Data API documentation](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FilterExpression), but is built with a bit of convenience and fluid interface in mind. You can filter your query by using `dimensionFilter()` and `metricFilter()` methods. These methods accept a callback that receives an instance of `Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression` class. The class provides a set of methods to build your filter: 180 | 181 | * `filter()` - generic filter method that accepts a dimension or metric name and a `filter callback` 182 | * `filterDimension()` - filter method that accepts a dimension object via callback and a `filter callback` 183 | * `filterMetric()` - filter method that accepts a metric object via callback and a `filter callback` 184 | * `not()` - negates the filter 185 | * `andGroup()` - creates a group of filters that are combined with AND operator 186 | * `orGroup()` - creates a group of filters that are combined with OR operator 187 | 188 | You can check `Gtmassey\LaravelAnalytics\Request\Filters\Filter` [class](https://github.com/gtmassey/laravel-analytics/tree/main/src/Request/Filters/Filter) for a list of available `filter callback` methods. 189 | 190 | #### Examples: 191 | 192 | ##### `filter()` method: 193 | 194 | ```php 195 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 196 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 197 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 198 | use Gtmassey\LaravelAnalytics\Request\Metrics; 199 | use Gtmassey\LaravelAnalytics\Analytics; 200 | use Gtmassey\Period\Period; 201 | 202 | $report = Analytics::query() 203 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 204 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->pageTitle()) 205 | ->forPeriod(Period::defaultPeriod()) 206 | ->dimensionFilter(fn(FilterExpression $filterExpression) => $filterExpression 207 | ->filter('pageTitle', fn(Filter $filter) => $filter->exact('Home')) 208 | ) 209 | ->run(); 210 | ``` 211 | 212 | ##### `filterDimension()` method: 213 | 214 | Using this method you can utilize `Dimensions` class to fluently build your filter without having to know the exact dimension name that's used in the API. 215 | 216 | ```php 217 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 218 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 219 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 220 | use Gtmassey\LaravelAnalytics\Request\Metrics; 221 | use Gtmassey\LaravelAnalytics\Analytics; 222 | use Gtmassey\Period\Period; 223 | 224 | $report = Analytics::query() 225 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 226 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->pageTitle()) 227 | ->forPeriod(Period::defaultPeriod()) 228 | ->dimensionFilter(fn(FilterExpression $filterExpression) => $filterExpression 229 | ->filterDimension( 230 | dimensionsCallback: fn(Dimensions $dimensions) => $dimensions->pageTitle(), 231 | filter: fn(Filter $filter) => $filter->exact('Home') 232 | ) 233 | ) 234 | ->run(); 235 | ``` 236 | 237 | ##### `filterMetric()` method: 238 | 239 | Similar to `filterDimension()` method, you can use this method and utilize `Metrics` class to fluently build your filter without having to know the exact metric name that's used in the API. 240 | 241 | ```php 242 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 243 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 244 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 245 | use Gtmassey\LaravelAnalytics\Request\Metrics; 246 | use Gtmassey\LaravelAnalytics\Analytics; 247 | use Gtmassey\Period\Period; 248 | 249 | $report = Analytics::query() 250 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 251 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->pageTitle()) 252 | ->forPeriod(Period::defaultPeriod()) 253 | ->metricFilter(fn(FilterExpression $filterExpression) => $filterExpression 254 | ->filterMetric( 255 | metricsCallback: fn(Metrics $metrics) => $metrics->sessions(), 256 | filter: fn(Filter $filter) => $filter->greaterThanInt(100) 257 | ) 258 | ) 259 | ->run(); 260 | ``` 261 | 262 | ##### `not()` method: 263 | 264 | ```php 265 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 266 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 267 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 268 | use Gtmassey\LaravelAnalytics\Request\Metrics; 269 | use Gtmassey\LaravelAnalytics\Analytics; 270 | use Gtmassey\Period\Period; 271 | 272 | $report = Analytics::query() 273 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 274 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->pageTitle()) 275 | ->forPeriod(Period::defaultPeriod()) 276 | ->dimensionFilter(fn(FilterExpression $filterExpression) => $filterExpression 277 | ->not(fn(FilterExpression $filterExpression) => $filterExpression 278 | ->filter('pageTitle', fn(Filter $filter) => $filter 279 | ->exact('Home') 280 | ) 281 | ) 282 | ) 283 | ->run(); 284 | ``` 285 | 286 | ##### `andGroup()` method: 287 | 288 | ```php 289 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 290 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 291 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 292 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpressionList; 293 | use Gtmassey\LaravelAnalytics\Request\Metrics; 294 | use Gtmassey\LaravelAnalytics\Analytics; 295 | use Gtmassey\Period\Period; 296 | 297 | $report = Analytics::query() 298 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 299 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->deviceCategory()->browser()) 300 | ->forPeriod(Period::defaultPeriod()) 301 | ->dimensionFilter(fn(FilterExpression $filterExpression) => $filterExpression 302 | ->andGroup(fn(FilterExpressionList $filterExpressionList) => $filterExpressionList 303 | ->filter('deviceCategory', fn(Filter $filter) => $filter 304 | ->exact('Mobile') 305 | ) 306 | ->filter('browser', fn(Filter $filter) => $filter 307 | ->exact('Chrome') 308 | ) 309 | ) 310 | ) 311 | ->run(); 312 | ``` 313 | 314 | ##### `orGroup()` method: 315 | 316 | ```php 317 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 318 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 319 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 320 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpressionList; 321 | use Gtmassey\LaravelAnalytics\Request\Metrics; 322 | use Gtmassey\LaravelAnalytics\Analytics; 323 | use Gtmassey\Period\Period; 324 | 325 | $report = Analytics::query() 326 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 327 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->browser()) 328 | ->forPeriod(Period::defaultPeriod()) 329 | ->dimensionFilter(fn(FilterExpression $filterExpression) => $filterExpression 330 | ->orGroup(fn(FilterExpressionList $filterExpressionList) => $filterExpressionList 331 | ->filter('browser', fn(Filter $filter) => $filter 332 | ->exact('Firefox') 333 | ) 334 | ->filter('browser', fn(Filter $filter) => $filter 335 | ->exact('Chrome') 336 | ) 337 | ) 338 | ) 339 | ->run(); 340 | ``` 341 | 342 | ##### Advanced example: 343 | 344 | You can mix all of the above methods to build a complex filter expression. 345 | 346 | ```php 347 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 348 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 349 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 350 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpressionList; 351 | use Gtmassey\LaravelAnalytics\Request\Metrics; 352 | use Gtmassey\LaravelAnalytics\Analytics; 353 | use Gtmassey\Period\Period; 354 | 355 | $report = Analytics::query() 356 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()->screenPageViews()) 357 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->browser()->deviceCategory()) 358 | ->forPeriod(Period::defaultPeriod()) 359 | ->dimensionFilter(fn(FilterExpression $filterExpression) => $filterExpression 360 | ->andGroup(fn(FilterExpressionList $filterExpressionList) => $filterExpressionList 361 | ->filter('browser', fn(Filter $filter) => $filter 362 | ->contains('safari') 363 | ) 364 | ->not(fn(FilterExpression $filterExpression) => $filterExpression 365 | ->filterDimension( 366 | dimensionsCallback: fn(Dimensions $dimensions) => $dimensions->deviceCategory(), 367 | filter: fn(Filter $filter) => $filter->contains('mobile') 368 | ) 369 | ) 370 | ) 371 | ) 372 | ->metricFilter(fn(FilterExpression $filterExpression) => $filterExpression 373 | ->orGroup(fn(FilterExpressionList $filterExpressionList) => $filterExpressionList 374 | ->filter('sessions', fn(Filter $filter) => $filter 375 | ->greaterThanInt(200) 376 | ) 377 | ->filterMetric( 378 | metricsCallback: fn(Metrics $metrics) => $metrics->sessions(), 379 | filter: fn(Filter $filter) => $filter->lessThanInt(100) 380 | ) 381 | ) 382 | ) 383 | ->run(); 384 | ``` 385 | 386 | ### Default Reports: 387 | 388 | #### getTopEvents() 389 | 390 | ```php 391 | $report = Analytics::getTopEvents(); 392 | ``` 393 | 394 | This method returns the top events for the given period. It accepts a `Gtmassey\Period\Period` object as an optional parameter. 395 | 396 | If a `Gtmassey\Period\Period` object is not passed, it will use the default period set in `Gtmassey\Period\Period::defaultPeriod()`. 397 | 398 | The method will return an instance of `Gtmassey\LaravelAnalytics\Response\ResponseData`, which contains `DimensionHeaders`, `MetricHeaders`, `Rows`, and additional metadata. 399 | 400 | example output: 401 | 402 | ```bash 403 | Gtmassey\LaravelAnalytics\Response\ResponseData { 404 | +dimensionHeaders: Spatie\LaravelData\DataCollection { 405 | +items: array:1 [ 406 | 0 => Gtmassey\LaravelAnalytics\Response\DimensionHeader { 407 | +name: "eventName" 408 | } 409 | ] 410 | } 411 | +metricHeaders: Spatie\LaravelData\DataCollection { 412 | +items: array:1 [ 413 | 0 => Gtmassey\LaravelAnalytics\Response\MetricHeader { 414 | +name: "eventCount" 415 | +type: "TYPE_INTEGER" 416 | } 417 | ] 418 | } 419 | +rows: Spatie\LaravelData\DataCollection { 420 | +items: array:6 [ 421 | 0 => Gtmassey\LaravelAnalytics\Response\Row { 422 | +dimensionValues: Spatie\LaravelData\DataCollection { 423 | +items: array:1 [ 424 | 0 => Gtmassey\LaravelAnalytics\Response\DimensionValue { 425 | +value: "page_view" 426 | } 427 | ] 428 | } 429 | +metricValues: Spatie\LaravelData\DataCollection { 430 | +items: array:1 [ 431 | 0 => Gtmassey\LaravelAnalytics\Response\MetricValue { 432 | +value: "1510" 433 | } 434 | ] 435 | } 436 | } 437 | 1 => Gtmassey\LaravelAnalytics\Response\Row {} 438 | 2 => Gtmassey\LaravelAnalytics\Response\Row {} 439 | 3 => Gtmassey\LaravelAnalytics\Response\Row {} 440 | 4 => Gtmassey\LaravelAnalytics\Response\Row {} 441 | 5 => Gtmassey\LaravelAnalytics\Response\Row {} 442 | ] 443 | } 444 | +totals: null 445 | +rowCount: 6 446 | +metadata: Gtmassey\LaravelAnalytics\Response\Metadata {} 447 | +propertyQuota: null 448 | +kind: "analyticsData#runReport" 449 | } 450 | ``` 451 | 452 | #### getTopPages() 453 | 454 | ```php 455 | $report = Analytics::getTopPages(); 456 | ``` 457 | 458 | This method returns the top pages for the given period. It accepts a `Gtmassey\Period\Period` object as an optional parameter. 459 | 460 | The pages along with the sessions for that page are listed in the `Rows` property of the response. 461 | 462 | #### getUserAcquisitionOverview() 463 | 464 | ```php 465 | $report = Analytics::getUserAcquisitionOverview(); 466 | ``` 467 | 468 | This method returns the user acquisition overview for the given period. It accepts a `Gtmassey\Period\Period` object as an optional parameter. 469 | 470 | The method will return a `ResponseData` object with the number of sessions by the session's primary acquisition source. Primary acquisition sources are either "direct", "Referral", "Organic Search", and "Organic Social". 471 | 472 | #### getUserEngagement() 473 | 474 | ```php 475 | $report = Analytics::getUserEngagement(); 476 | ``` 477 | 478 | This method returns a `ResponseData` object without dimensions. The query only contains metrics. The `ResponseData` object will contain: 479 | 480 | * average session duration, in seconds 481 | * number of engaged sessions 482 | * number of sessions per user 483 | * total number of sessions 484 | 485 | ## Extensibility: 486 | 487 | ### Custom metrics and dimensions: 488 | 489 | You are not limited to the metrics and dimensions provided by this package. You can use any custom metrics and dimensions you have created in Google Analytics. 490 | 491 | Create a new class that extends `Gtmassey\LaravelAnalytics\Request\CustomMetric` or `Gtmassey\LaravelAnalytics\Request\CustomDimension` and implement methods following this format:. 492 | 493 | ```php 494 | namespace App\Analytics; 495 | 496 | use Google\Analytics\Data\V1beta\Metric; 497 | use Gtmassey\LaravelAnalytics\Request\Metrics; 498 | 499 | class CustomMetrics extends Metrics 500 | { 501 | public function customMetric(): self 502 | { 503 | $this->metrics->push(new Metric(['name' => 'customEvent:parameter_name'])); 504 | 505 | return $this; 506 | } 507 | } 508 | ``` 509 | 510 | Bind the class in your `AppServiceProvider`: 511 | 512 | ```php 513 | use Gtmassey\LaravelAnalytics\Request\Metrics; 514 | use App\Analytics\CustomMetrics; 515 | //use Gtmassey\LaravelAnalytics\Request\Dimensions; 516 | //use App\Analytics\CustomDimensions; 517 | 518 | public function boot() 519 | { 520 | $this->app->bind(Metrics::class, CustomMetrics::class); 521 | //$this->app->bind(Dimensions::class, CustomDimensions::class); 522 | } 523 | ``` 524 | 525 | Now you can use the custom metric in your query: 526 | 527 | ```php 528 | use App\Analytics\CustomMetrics; 529 | use Gtmassey\LaravelAnalytics\Analytics; 530 | use Gtmassey\LaravelAnalytics\Period; 531 | 532 | $report = Analytics::query() 533 | ->setMetrics(fn(CustomMetrics $metrics) => $metrics 534 | ->customMetric() 535 | ->sessions() 536 | ) 537 | ->forPeriod(Period::defaultPeriod()) 538 | ->run(); 539 | ``` 540 | 541 | ### Reusable filters: 542 | 543 | You can create reusable filters to use in your queries. Create a new class that extends `Gtmassey\LaravelAnalytics\Analytics` and implement methods following this format: 544 | 545 | ```php 546 | namespace App\Analytics; 547 | 548 | use Gtmassey\LaravelAnalytics\Analytics; 549 | use Gtmassey\LaravelAnalytics\Request\Filters\Filter; 550 | use Gtmassey\LaravelAnalytics\Request\Filters\FilterExpression; 551 | use Gtmassey\LaravelAnalytics\Request\Metrics; 552 | 553 | class CustomAnalytics extends Analytics 554 | { 555 | public function onlySessionsAbove(int $count): static 556 | { 557 | $this->metricFilter(fn(FilterExpression $filterExpression) => $filterExpression 558 | ->filterMetric( 559 | metricsCallback: fn(Metrics $metrics) => $metrics->sessions(), 560 | filter: fn(Filter $filter) => $filter->greaterThanInt($count), 561 | ) 562 | ); 563 | 564 | return $this; 565 | } 566 | } 567 | ``` 568 | 569 | Bind the class in your `AppServiceProvider`: 570 | 571 | ```php 572 | use Gtmassey\LaravelAnalytics\Analytics; 573 | use App\Analytics\CustomAnalytics; 574 | 575 | public function boot() 576 | { 577 | $this->app->bind(Analytics::class, CustomAnalytics::class); 578 | } 579 | ``` 580 | 581 | Now you can use the custom filter in your query: 582 | 583 | ```php 584 | use App\Analytics\CustomAnalytics; 585 | use Gtmassey\LaravelAnalytics\Period; 586 | use Gtmassey\LaravelAnalytics\Request\Dimensions; 587 | use Gtmassey\LaravelAnalytics\Request\Metrics; 588 | 589 | $report = CustomAnalytics::query() 590 | ->setMetrics(fn(Metrics $metrics) => $metrics->sessions()) 591 | ->setDimensions(fn(Dimensions $dimensions) => $dimensions->browser()) 592 | ->forPeriod(Period::defaultPeriod()) 593 | ->onlySessionsAbove(100) 594 | ->run(); 595 | ``` 596 | 597 | ## Change log 598 | 599 | Read [CHANGELOG.md](CHANGELOG.md) 600 | 601 | ## Testing 602 | 603 | To run tests, run: 604 | 605 | ```bash 606 | composer test 607 | ``` 608 | 609 | Note that this command also runs code coverage analysis. 610 | 611 | ## Contributing 612 | 613 | Check out [the contributing guide](CONTRIBUTING.md) 614 | 615 | ## Security 616 | 617 | If you discover any security related issues, please email contact@garrettmassey.net instead of using the issue tracker. 618 | 619 | ## Credits 620 | 621 | - [Garrett Massey](https://www.garrettmassey.net/) 622 | - [All Contributors][link-contributors] 623 | 624 | Special thanks to [Plytas](https://github.com/Plytas) for their early and significant contributions to the project. Without their help setting things up and their willingness to teach me new tools and techniques, this project would be dead in its tracks. 625 | 626 | And a huge thanks to the team over at [Spatie](https://github.com/spatie) for their continued contributions to the open source community! Some of their work is used in this project, and I have used their packages as a foundation for projects for years. 627 | 628 | ## License 629 | 630 | MIT. Please see the [license file](LICENSE.md) for more information. 631 | 632 | [ico-version]: https://img.shields.io/packagist/v/gtmassey/laravel-analytics.svg?style=flat-square 633 | [ico-downloads]: https://img.shields.io/packagist/dt/gtmassey/laravel-analytics.svg?style=flat-square 634 | [ico-tests]: https://github.com/gtmassey/Analytics/actions/workflows/run-tests.yml/badge.svg 635 | 636 | [link-packagist]: https://packagist.org/packages/gtmassey/laravel-analytics 637 | [link-downloads]: https://packagist.org/packages/gtmassey/laravel-analytics 638 | [link-tests]: https://github.com/gtmassey/Analytics/actions/workflows/run-tests.yml 639 | [link-author]: https://github.com/gtmassey 640 | [link-contributors]: ../../contributors 641 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gtmassey/laravel-analytics", 3 | "description": "Create and run Google Analytics Data API queries in Laravel", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Garrett Massey", 8 | "email": "contact@garrettmassey.net", 9 | "role": "Creator" 10 | }, 11 | { 12 | "name": "Vytautas Smilingis", 13 | "role": "Contributor" 14 | } 15 | ], 16 | "homepage": "https://github.com/gtmassey/laravel-analytics/", 17 | "keywords": [ 18 | "Laravel", 19 | "Analytics", 20 | "Google Analytics" 21 | ], 22 | "require": { 23 | "php": "^8.1|^8.2|^8.3", 24 | "google/analytics-data": "^v0.9.0", 25 | "gtmassey/period": "^1.2.0", 26 | "illuminate/support": "^10.0|^11.0", 27 | "nesbot/carbon": "^2.63", 28 | "spatie/laravel-data": "^3.12", 29 | "spatie/laravel-package-tools": "^1.13" 30 | }, 31 | "require-dev": { 32 | "larastan/larastan": "^2.9", 33 | "laravel/pint": "^1.6", 34 | "nunomaduro/collision": "^7.11.0|^v8.5.0", 35 | "orchestra/testbench": "^v8.27.2|^9.5", 36 | "phpstan/extension-installer": "^1.2", 37 | "phpstan/phpstan-deprecation-rules": "^1.1.2", 38 | "phpstan/phpstan-mockery": "^1.1.1", 39 | "phpstan/phpstan-phpunit": "^1.3.7", 40 | "phpunit/phpunit": "^10", 41 | "rregeer/phpunit-coverage-check": "^0.3.1" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Gtmassey\\LaravelAnalytics\\": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "Gtmassey\\LaravelAnalytics\\Tests\\": "tests" 51 | } 52 | }, 53 | "scripts": { 54 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 55 | "analyze": "vendor/bin/phpstan analyse --xdebug", 56 | "test": "./vendor/bin/testbench package:test && ./vendor/bin/coverage-check build/logs/clover.xml 100", 57 | "pint": "./vendor/bin/pint" 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "allow-plugins": { 62 | "phpstan/extension-installer": true 63 | } 64 | }, 65 | "extra": { 66 | "laravel": { 67 | "providers": [ 68 | "Gtmassey\\LaravelAnalytics\\AnalyticsServiceProvider" 69 | ] 70 | } 71 | }, 72 | "minimum-stability": "dev", 73 | "prefer-stable": true 74 | } 75 | -------------------------------------------------------------------------------- /config/analytics.php: -------------------------------------------------------------------------------- 1 | env('ANALYTICS_YEAR_TYPE', 'fiscal'), 6 | 'property_id' => env('ANALYTICS_PROPERTY_ID'), 7 | 8 | 'credentials' => [ 9 | 'use_env' => env('ANALYTICS_CREDENTIALS_USE_ENV', true), 10 | 11 | 'file' => env('ANALYTICS_CREDENTIALS_FILE'), 12 | 13 | 'json' => env('ANALYTICS_CREDENTIALS_JSON'), 14 | 15 | 'array' => env('ANALYTICS_CREDENTIALS_ARRAY', [ 16 | 'type' => env('ANALYTICS_CREDENTIALS_TYPE'), 17 | 'project_id' => env('ANALYTICS_CREDENTIALS_PROJECT_ID'), 18 | 'private_key_id' => env('ANALYTICS_CREDENTIALS_PRIVATE_KEY_ID'), 19 | 'private_key' => env('ANALYTICS_CREDENTIALS_PRIVATE_KEY'), 20 | 'client_email' => env('ANALYTICS_CREDENTIALS_CLIENT_EMAIL'), 21 | 'client_id' => env('ANALYTICS_CREDENTIALS_CLIENT_ID'), 22 | 'auth_uri' => env('ANALYTICS_CREDENTIALS_AUTH_URI'), 23 | 'token_uri' => env('ANALYTICS_CREDENTIALS_TOKEN_URI'), 24 | 'auth_provider_x509_cert_url' => env('ANALYTICS_CREDENTIALS_AUTH_PROVIDER_X509_CERT_URL'), 25 | 'client_x509_cert_url' => env('ANALYTICS_CREDENTIALS_CLIENT_X509_CERT_URL'), 26 | ]), 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtmassey/laravel-analytics/58b9fa25246a1d8031991f86f18d90d5e1e054c1/phpstan-baseline.neon -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: max 6 | paths: 7 | - src 8 | - config 9 | - tests # optional 10 | tmpDir: build/phpstan 11 | 12 | checkOctaneCompatibility: true 13 | checkModelProperties: true 14 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | tests 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Analytics.php: -------------------------------------------------------------------------------- 1 | client = resolve(BetaAnalyticsDataClient::class); 38 | $this->requestData = new RequestData(propertyId: $propertyId); 39 | } 40 | 41 | public static function query(?string $propertyId = null): static 42 | { 43 | /** @var static $analytics */ 44 | $analytics = resolve(Analytics::class, ['propertyId' => $propertyId]); 45 | 46 | return $analytics; 47 | } 48 | 49 | /*************************************** 50 | * Query Builders 51 | ***************************************/ 52 | 53 | /** 54 | * Ability to add metrics to the query using a callback method 55 | * for example: 56 | * $query->setMetrics(function (Metrics $metrics) { $metrics->sessions()->bounceRate(); }); 57 | */ 58 | public function setMetrics(Closure $callback): static 59 | { 60 | /** @var Metrics $metrics */ 61 | $metrics = $callback(resolve(Metrics::class)); 62 | $this->requestData->metrics->push(...$metrics->getMetrics()); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Ability to add dimensions to the query using a callback method 69 | * for example: 70 | * $query->setDimensions(function (Dimensions $dimensions) { $dimensions->pageTitle()->pagePath(); }); 71 | */ 72 | public function setDimensions(Closure $callback): static 73 | { 74 | /** @var Dimensions $dimensions */ 75 | $dimensions = $callback(resolve(Dimensions::class)); 76 | $this->requestData->dimensions->push(...$dimensions->getDimensions()); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * @param Closure(FilterExpression): FilterExpression $callback 83 | */ 84 | public function dimensionFilter(Closure $callback): static 85 | { 86 | $this->requestData->dimensionFilter = $callback(new FilterExpression); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param Closure(FilterExpression): FilterExpression $callback 93 | */ 94 | public function metricFilter(Closure $callback): static 95 | { 96 | $this->requestData->metricFilter = $callback(new FilterExpression); 97 | 98 | return $this; 99 | } 100 | 101 | public function forPeriod(Period $period): static 102 | { 103 | $dateRange = new DateRange([ 104 | 'start_date' => $period->startDate->toDateString(), 105 | 'end_date' => $period->endDate->toDateString(), 106 | ]); 107 | $this->requestData->dateRanges->push($dateRange); 108 | 109 | return $this; 110 | } 111 | 112 | public function withTotals(bool $useTotals = true): static 113 | { 114 | $this->requestData->useTotals = $useTotals; 115 | 116 | return $this; 117 | } 118 | 119 | public function limit(int $limit = 10_000): static 120 | { 121 | $this->requestData->limit = $limit; 122 | 123 | return $this; 124 | } 125 | 126 | public function offset(int $offset = 0): static 127 | { 128 | $this->requestData->offset = $offset; 129 | 130 | return $this; 131 | } 132 | 133 | /*************************************** 134 | * Process and Run Query 135 | ***************************************/ 136 | 137 | /** 138 | * @throws ApiException 139 | */ 140 | public function run(): ResponseData 141 | { 142 | $reportResponse = $this->client->runReport($this->requestData->toArray()); 143 | 144 | return ResponseData::fromReportResponse($reportResponse); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/AnalyticsServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('analytics') 15 | ->hasConfigFile('analytics'); 16 | 17 | $this->app->bind(BetaAnalyticsDataClient::class, function () { 18 | $credentials = resolve(Credentials::class)->parse(); 19 | 20 | return new BetaAnalyticsDataClient(['credentials' => $credentials]); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Credentials.php: -------------------------------------------------------------------------------- 1 | |null 15 | * 16 | * @throws InvalidCredentialsJsonStringException 17 | * @throws InvalidCredentialsFileException 18 | * @throws InvalidCredentialsArrayException 19 | */ 20 | public function parse(): ?array 21 | { 22 | if (config('analytics.credentials.use_env') && getenv('GOOGLE_APPLICATION_CREDENTIALS')) { 23 | return null; 24 | } 25 | 26 | if (($file = config('analytics.credentials.file')) !== null) { 27 | return $this->credentialsFile($file); 28 | } 29 | 30 | if (($json = config('analytics.credentials.json')) !== null) { 31 | return $this->credentialsJson($json); 32 | } 33 | 34 | return $this->credentialsArray(); 35 | } 36 | 37 | /** 38 | * @return array 39 | * 40 | * @throws InvalidCredentialsFileException 41 | */ 42 | private function credentialsFile(mixed $file): array 43 | { 44 | if (! is_string($file) || empty($file)) { 45 | throw InvalidCredentialsFileException::invalidPath(); 46 | } 47 | 48 | try { 49 | $fileContents = (new Filesystem)->get($file); 50 | } catch (FileNotFoundException $e) { 51 | throw InvalidCredentialsFileException::notFound(previous: $e); 52 | } 53 | 54 | $credentials = json_decode($fileContents, true); 55 | 56 | if (! is_array($credentials)) { 57 | throw InvalidCredentialsFileException::invalidJson(); 58 | } 59 | 60 | return $credentials; 61 | } 62 | 63 | /** 64 | * @return array 65 | * 66 | * @throws InvalidCredentialsJsonStringException 67 | */ 68 | private function credentialsJson(mixed $json): array 69 | { 70 | if (! is_string($json) || empty($json)) { 71 | throw InvalidCredentialsJsonStringException::invalidString(); 72 | } 73 | 74 | $credentials = json_decode($json, true); 75 | 76 | if (! is_array($credentials)) { 77 | throw InvalidCredentialsJsonStringException::invalidJson(); 78 | } 79 | 80 | return $credentials; 81 | } 82 | 83 | /** 84 | * @return array 85 | * 86 | * @throws InvalidCredentialsArrayException 87 | */ 88 | private function credentialsArray(): array 89 | { 90 | $credentials = config('analytics.credentials.array'); 91 | 92 | if (! is_array($credentials) || empty($credentials)) { 93 | throw InvalidCredentialsArrayException::invalidArray(); 94 | } 95 | 96 | return $credentials; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidCredentialsArrayException.php: -------------------------------------------------------------------------------- 1 | setMetrics(fn (Metrics $metric) => $metric->eventCount()) 21 | ->setDimensions(fn (Dimensions $dimension) => $dimension->eventName()) 22 | ->forPeriod($period ?? Period::defaultPeriod()) 23 | ->run(); 24 | } 25 | 26 | /** 27 | * @throws ApiException 28 | */ 29 | public static function getUserAcquisitionOverview(?Period $period = null): ResponseData 30 | { 31 | return Analytics::query() 32 | ->setMetrics(fn (Metrics $metric) => $metric->sessions()) 33 | ->setDimensions(fn (Dimensions $dimension) => $dimension->firstUserDefaultChannelGroup()) 34 | ->forPeriod($period ?? Period::defaultPeriod()) 35 | ->run(); 36 | } 37 | 38 | /** 39 | * @throws ApiException 40 | */ 41 | public static function getTopPages(?Period $period = null): ResponseData 42 | { 43 | return Analytics::query() 44 | ->setMetrics(fn (Metrics $metric) => $metric->sessions()) 45 | ->setDimensions(fn (Dimensions $dimension) => $dimension->pageTitle()) 46 | ->forPeriod($period ?? Period::defaultPeriod()) 47 | ->run(); 48 | } 49 | 50 | /** 51 | * @throws ApiException 52 | */ 53 | public static function getUserEngagement(?Period $period = null): ResponseData 54 | { 55 | return Analytics::query() 56 | ->setMetrics(fn (Metrics $metric) => $metric 57 | ->averageSessionDuration() 58 | ->engagedSessions() 59 | ->sessionsPerUser() 60 | ->sessions() 61 | ) 62 | ->forPeriod($period ?? Period::defaultPeriod()) 63 | ->run(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Request/Dimensions.php: -------------------------------------------------------------------------------- 1 | */ 11 | protected Collection $dimensions; 12 | 13 | public function __construct() 14 | { 15 | $this->dimensions = new Collection; 16 | } 17 | 18 | public function count(): int 19 | { 20 | return $this->dimensions->count(); 21 | } 22 | 23 | public function first(): ?Dimension 24 | { 25 | return $this->dimensions->first(); 26 | } 27 | 28 | /** 29 | * @return Collection 30 | */ 31 | public function getDimensions(): Collection 32 | { 33 | return $this->dimensions; 34 | } 35 | 36 | public function achievementId(): self 37 | { 38 | $this->dimensions->push(new Dimension(['name' => 'achievementId'])); 39 | 40 | return $this; 41 | } 42 | 43 | public function adFormat(): self 44 | { 45 | $this->dimensions->push(new Dimension(['name' => 'adFormat'])); 46 | 47 | return $this; 48 | } 49 | 50 | public function adSourceName(): self 51 | { 52 | $this->dimensions->push(new Dimension(['name' => 'adSourceName'])); 53 | 54 | return $this; 55 | } 56 | 57 | public function adUnitName(): self 58 | { 59 | $this->dimensions->push(new Dimension(['name' => 'adUnitName'])); 60 | 61 | return $this; 62 | } 63 | 64 | public function appVersion(): self 65 | { 66 | $this->dimensions->push(new Dimension(['name' => 'appVersion'])); 67 | 68 | return $this; 69 | } 70 | 71 | public function audienceId(): self 72 | { 73 | $this->dimensions->push(new Dimension(['name' => 'audienceId'])); 74 | 75 | return $this; 76 | } 77 | 78 | public function audienceName(): self 79 | { 80 | $this->dimensions->push(new Dimension(['name' => 'audienceName'])); 81 | 82 | return $this; 83 | } 84 | 85 | public function brandingInterest(): self 86 | { 87 | $this->dimensions->push(new Dimension(['name' => 'brandingInterest'])); 88 | 89 | return $this; 90 | } 91 | 92 | public function browser(): self 93 | { 94 | $this->dimensions->push(new Dimension(['name' => 'browser'])); 95 | 96 | return $this; 97 | } 98 | 99 | public function campaignId(): self 100 | { 101 | $this->dimensions->push(new Dimension(['name' => 'campaignId'])); 102 | 103 | return $this; 104 | } 105 | 106 | public function campaignName(): self 107 | { 108 | $this->dimensions->push(new Dimension(['name' => 'campaignName'])); 109 | 110 | return $this; 111 | } 112 | 113 | public function character(): self 114 | { 115 | $this->dimensions->push(new Dimension(['name' => 'character'])); 116 | 117 | return $this; 118 | } 119 | 120 | public function city(): self 121 | { 122 | $this->dimensions->push(new Dimension(['name' => 'city'])); 123 | 124 | return $this; 125 | } 126 | 127 | public function cityId(): self 128 | { 129 | $this->dimensions->push(new Dimension(['name' => 'cityId'])); 130 | 131 | return $this; 132 | } 133 | 134 | public function cohort(): self 135 | { 136 | $this->dimensions->push(new Dimension(['name' => 'cohort'])); 137 | 138 | return $this; 139 | } 140 | 141 | public function cohortNthDay(): self 142 | { 143 | $this->dimensions->push(new Dimension(['name' => 'cohortNthDay'])); 144 | 145 | return $this; 146 | } 147 | 148 | public function cohortNthMonth(): self 149 | { 150 | $this->dimensions->push(new Dimension(['name' => 'cohortNthMonth'])); 151 | 152 | return $this; 153 | } 154 | 155 | public function cohortNthWeek(): self 156 | { 157 | $this->dimensions->push(new Dimension(['name' => 'cohortNthWeek'])); 158 | 159 | return $this; 160 | } 161 | 162 | public function contentGroup(): self 163 | { 164 | $this->dimensions->push(new Dimension(['name' => 'contentGroup'])); 165 | 166 | return $this; 167 | } 168 | 169 | public function contentId(): self 170 | { 171 | $this->dimensions->push(new Dimension(['name' => 'contentId'])); 172 | 173 | return $this; 174 | } 175 | 176 | public function contentType(): self 177 | { 178 | $this->dimensions->push(new Dimension(['name' => 'contentType'])); 179 | 180 | return $this; 181 | } 182 | 183 | public function country(): self 184 | { 185 | $this->dimensions->push(new Dimension(['name' => 'country'])); 186 | 187 | return $this; 188 | } 189 | 190 | public function countryId(): self 191 | { 192 | $this->dimensions->push(new Dimension(['name' => 'countryId'])); 193 | 194 | return $this; 195 | } 196 | 197 | public function date(): self 198 | { 199 | $this->dimensions->push(new Dimension(['name' => 'date'])); 200 | 201 | return $this; 202 | } 203 | 204 | public function dateHour(): self 205 | { 206 | $this->dimensions->push(new Dimension(['name' => 'dateHour'])); 207 | 208 | return $this; 209 | } 210 | 211 | public function dateHourMinute(): self 212 | { 213 | $this->dimensions->push(new Dimension(['name' => 'dateHourMinute'])); 214 | 215 | return $this; 216 | } 217 | 218 | public function day(): self 219 | { 220 | $this->dimensions->push(new Dimension(['name' => 'day'])); 221 | 222 | return $this; 223 | } 224 | 225 | public function dayOfWeek(): self 226 | { 227 | $this->dimensions->push(new Dimension(['name' => 'dayOfWeek'])); 228 | 229 | return $this; 230 | } 231 | 232 | public function defaultChannelGroup(): self 233 | { 234 | $this->dimensions->push(new Dimension(['name' => 'defaultChannelGroup'])); 235 | 236 | return $this; 237 | } 238 | 239 | public function deviceCategory(): self 240 | { 241 | $this->dimensions->push(new Dimension(['name' => 'deviceCategory'])); 242 | 243 | return $this; 244 | } 245 | 246 | public function deviceModel(): self 247 | { 248 | $this->dimensions->push(new Dimension(['name' => 'deviceModel'])); 249 | 250 | return $this; 251 | } 252 | 253 | public function eventName(): self 254 | { 255 | $this->dimensions->push(new Dimension(['name' => 'eventName'])); 256 | 257 | return $this; 258 | } 259 | 260 | public function fileExtension(): self 261 | { 262 | $this->dimensions->push(new Dimension(['name' => 'fileExtension'])); 263 | 264 | return $this; 265 | } 266 | 267 | public function fileName(): self 268 | { 269 | $this->dimensions->push(new Dimension(['name' => 'fileName'])); 270 | 271 | return $this; 272 | } 273 | 274 | public function firstSessionDate(): self 275 | { 276 | $this->dimensions->push(new Dimension(['name' => 'firstSessionDate'])); 277 | 278 | return $this; 279 | } 280 | 281 | public function firstUserCampaignId(): self 282 | { 283 | $this->dimensions->push(new Dimension(['name' => 'firstUserCampaignId'])); 284 | 285 | return $this; 286 | } 287 | 288 | public function firstUserCampaignName(): self 289 | { 290 | $this->dimensions->push(new Dimension(['name' => 'firstUserCampaignName'])); 291 | 292 | return $this; 293 | } 294 | 295 | public function firstUserDefaultChannelGroup(): self 296 | { 297 | $this->dimensions->push(new Dimension(['name' => 'firstUserDefaultChannelGroup'])); 298 | 299 | return $this; 300 | } 301 | 302 | public function firstUserGoogleAdsAccountName(): self 303 | { 304 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsAccountName'])); 305 | 306 | return $this; 307 | } 308 | 309 | public function firstUserGoogleAdsAdGroupId(): self 310 | { 311 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsAdGroupId'])); 312 | 313 | return $this; 314 | } 315 | 316 | public function firstUserGoogleAdsAdGroupName(): self 317 | { 318 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsAdGroupName'])); 319 | 320 | return $this; 321 | } 322 | 323 | public function firstUserGoogleAdsAdNetworkType(): self 324 | { 325 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsAdNetworkType'])); 326 | 327 | return $this; 328 | } 329 | 330 | public function firstUserGoogleAdsCampaignId(): self 331 | { 332 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsCampaignId'])); 333 | 334 | return $this; 335 | } 336 | 337 | public function firstUserGoogleAdsCampaignName(): self 338 | { 339 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsCampaignName'])); 340 | 341 | return $this; 342 | } 343 | 344 | public function firstUserGoogleAdsCampaignType(): self 345 | { 346 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsCampaignType'])); 347 | 348 | return $this; 349 | } 350 | 351 | public function firstUserGoogleAdsCreativeId(): self 352 | { 353 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsCreativeId'])); 354 | 355 | return $this; 356 | } 357 | 358 | public function firstUserGoogleAdsCustomerId(): self 359 | { 360 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsCustomerId'])); 361 | 362 | return $this; 363 | } 364 | 365 | public function firstUserGoogleAdsKeyword(): self 366 | { 367 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsKeyword'])); 368 | 369 | return $this; 370 | } 371 | 372 | public function firstUserGoogleAdsQuery(): self 373 | { 374 | $this->dimensions->push(new Dimension(['name' => 'firstUserGoogleAdsQuery'])); 375 | 376 | return $this; 377 | } 378 | 379 | public function firstUserManualAdContent(): self 380 | { 381 | $this->dimensions->push(new Dimension(['name' => 'firstUserManualAdContent'])); 382 | 383 | return $this; 384 | } 385 | 386 | public function firstUserManualTerm(): self 387 | { 388 | $this->dimensions->push(new Dimension(['name' => 'firstUserManualTerm'])); 389 | 390 | return $this; 391 | } 392 | 393 | public function firstUserMedium(): self 394 | { 395 | $this->dimensions->push(new Dimension(['name' => 'firstUserMedium'])); 396 | 397 | return $this; 398 | } 399 | 400 | public function firstUserSource(): self 401 | { 402 | $this->dimensions->push(new Dimension(['name' => 'firstUserSource'])); 403 | 404 | return $this; 405 | } 406 | 407 | public function firstUserSourceMedium(): self 408 | { 409 | $this->dimensions->push(new Dimension(['name' => 'firstUserSourceMedium'])); 410 | 411 | return $this; 412 | } 413 | 414 | public function firstUserSourcePlatform(): self 415 | { 416 | $this->dimensions->push(new Dimension(['name' => 'firstUserSourcePlatform'])); 417 | 418 | return $this; 419 | } 420 | 421 | public function fullPageUrl(): self 422 | { 423 | $this->dimensions->push(new Dimension(['name' => 'fullPageUrl'])); 424 | 425 | return $this; 426 | } 427 | 428 | public function googleAdsAccountName(): self 429 | { 430 | $this->dimensions->push(new Dimension(['name' => 'googleAdsAccountName'])); 431 | 432 | return $this; 433 | } 434 | 435 | public function googleAdsAdGroupId(): self 436 | { 437 | $this->dimensions->push(new Dimension(['name' => 'googleAdsAdGroupId'])); 438 | 439 | return $this; 440 | } 441 | 442 | public function googleAdsAdGroupName(): self 443 | { 444 | $this->dimensions->push(new Dimension(['name' => 'googleAdsAdGroupName'])); 445 | 446 | return $this; 447 | } 448 | 449 | public function googleAdsAdNetworkType(): self 450 | { 451 | $this->dimensions->push(new Dimension(['name' => 'googleAdsAdNetworkType'])); 452 | 453 | return $this; 454 | } 455 | 456 | public function googleAdsCampaignId(): self 457 | { 458 | $this->dimensions->push(new Dimension(['name' => 'googleAdsCampaignId'])); 459 | 460 | return $this; 461 | } 462 | 463 | public function googleAdsCampaignName(): self 464 | { 465 | $this->dimensions->push(new Dimension(['name' => 'googleAdsCampaignName'])); 466 | 467 | return $this; 468 | } 469 | 470 | public function googleAdsCampaignType(): self 471 | { 472 | $this->dimensions->push(new Dimension(['name' => 'googleAdsCampaignType'])); 473 | 474 | return $this; 475 | } 476 | 477 | public function googleAdsCreativeId(): self 478 | { 479 | $this->dimensions->push(new Dimension(['name' => 'googleAdsCreativeId'])); 480 | 481 | return $this; 482 | } 483 | 484 | public function googleAdsCustomerId(): self 485 | { 486 | $this->dimensions->push(new Dimension(['name' => 'googleAdsCustomerId'])); 487 | 488 | return $this; 489 | } 490 | 491 | public function googleAdsKeyword(): self 492 | { 493 | $this->dimensions->push(new Dimension(['name' => 'googleAdsKeyword'])); 494 | 495 | return $this; 496 | } 497 | 498 | public function googleAdsQuery(): self 499 | { 500 | $this->dimensions->push(new Dimension(['name' => 'googleAdsQuery'])); 501 | 502 | return $this; 503 | } 504 | 505 | public function groupId(): self 506 | { 507 | $this->dimensions->push(new Dimension(['name' => 'groupId'])); 508 | 509 | return $this; 510 | } 511 | 512 | public function hostName(): self 513 | { 514 | $this->dimensions->push(new Dimension(['name' => 'hostName'])); 515 | 516 | return $this; 517 | } 518 | 519 | public function hour(): self 520 | { 521 | $this->dimensions->push(new Dimension(['name' => 'hour'])); 522 | 523 | return $this; 524 | } 525 | 526 | public function isConversionEvent(): self 527 | { 528 | $this->dimensions->push(new Dimension(['name' => 'isConversionEvent'])); 529 | 530 | return $this; 531 | } 532 | 533 | public function itemAffiliation(): self 534 | { 535 | $this->dimensions->push(new Dimension(['name' => 'itemAffiliation'])); 536 | 537 | return $this; 538 | } 539 | 540 | public function itemBrand(): self 541 | { 542 | $this->dimensions->push(new Dimension(['name' => 'itemBrand'])); 543 | 544 | return $this; 545 | } 546 | 547 | public function itemCategory(): self 548 | { 549 | $this->dimensions->push(new Dimension(['name' => 'itemCategory'])); 550 | 551 | return $this; 552 | } 553 | 554 | public function itemCategory2(): self 555 | { 556 | $this->dimensions->push(new Dimension(['name' => 'itemCategory2'])); 557 | 558 | return $this; 559 | } 560 | 561 | public function itemCategory3(): self 562 | { 563 | $this->dimensions->push(new Dimension(['name' => 'itemCategory3'])); 564 | 565 | return $this; 566 | } 567 | 568 | public function itemCategory4(): self 569 | { 570 | $this->dimensions->push(new Dimension(['name' => 'itemCategory4'])); 571 | 572 | return $this; 573 | } 574 | 575 | public function itemCategory5(): self 576 | { 577 | $this->dimensions->push(new Dimension(['name' => 'itemCategory5'])); 578 | 579 | return $this; 580 | } 581 | 582 | public function itemId(): self 583 | { 584 | $this->dimensions->push(new Dimension(['name' => 'itemId'])); 585 | 586 | return $this; 587 | } 588 | 589 | public function itemListId(): self 590 | { 591 | $this->dimensions->push(new Dimension(['name' => 'itemListId'])); 592 | 593 | return $this; 594 | } 595 | 596 | public function itemListName(): self 597 | { 598 | $this->dimensions->push(new Dimension(['name' => 'itemListName'])); 599 | 600 | return $this; 601 | } 602 | 603 | public function itemName(): self 604 | { 605 | $this->dimensions->push(new Dimension(['name' => 'itemName'])); 606 | 607 | return $this; 608 | } 609 | 610 | public function itemPromotionCreativeName(): self 611 | { 612 | $this->dimensions->push(new Dimension(['name' => 'itemPromotionCreativeName'])); 613 | 614 | return $this; 615 | } 616 | 617 | public function itemPromotionId(): self 618 | { 619 | $this->dimensions->push(new Dimension(['name' => 'itemPromotionId'])); 620 | 621 | return $this; 622 | } 623 | 624 | public function itemPromotionName(): self 625 | { 626 | $this->dimensions->push(new Dimension(['name' => 'itemPromotionName'])); 627 | 628 | return $this; 629 | } 630 | 631 | public function itemVariant(): self 632 | { 633 | $this->dimensions->push(new Dimension(['name' => 'itemVariant'])); 634 | 635 | return $this; 636 | } 637 | 638 | public function landingPage(): self 639 | { 640 | $this->dimensions->push(new Dimension(['name' => 'landingPage'])); 641 | 642 | return $this; 643 | } 644 | 645 | public function language(): self 646 | { 647 | $this->dimensions->push(new Dimension(['name' => 'language'])); 648 | 649 | return $this; 650 | } 651 | 652 | public function languageCode(): self 653 | { 654 | $this->dimensions->push(new Dimension(['name' => 'languageCode'])); 655 | 656 | return $this; 657 | } 658 | 659 | public function level(): self 660 | { 661 | $this->dimensions->push(new Dimension(['name' => 'level'])); 662 | 663 | return $this; 664 | } 665 | 666 | public function linkClasses(): self 667 | { 668 | $this->dimensions->push(new Dimension(['name' => 'linkClasses'])); 669 | 670 | return $this; 671 | } 672 | 673 | public function linkDomain(): self 674 | { 675 | $this->dimensions->push(new Dimension(['name' => 'linkDomain'])); 676 | 677 | return $this; 678 | } 679 | 680 | public function linkId(): self 681 | { 682 | $this->dimensions->push(new Dimension(['name' => 'linkId'])); 683 | 684 | return $this; 685 | } 686 | 687 | public function linkText(): self 688 | { 689 | $this->dimensions->push(new Dimension(['name' => 'linkText'])); 690 | 691 | return $this; 692 | } 693 | 694 | public function linkUrl(): self 695 | { 696 | $this->dimensions->push(new Dimension(['name' => 'linkUrl'])); 697 | 698 | return $this; 699 | } 700 | 701 | public function manualAdContent(): self 702 | { 703 | $this->dimensions->push(new Dimension(['name' => 'manualAdContent'])); 704 | 705 | return $this; 706 | } 707 | 708 | public function manualTerm(): self 709 | { 710 | $this->dimensions->push(new Dimension(['name' => 'manualTerm'])); 711 | 712 | return $this; 713 | } 714 | 715 | public function medium(): self 716 | { 717 | $this->dimensions->push(new Dimension(['name' => 'medium'])); 718 | 719 | return $this; 720 | } 721 | 722 | public function method(): self 723 | { 724 | $this->dimensions->push(new Dimension(['name' => 'method'])); 725 | 726 | return $this; 727 | } 728 | 729 | public function minute(): self 730 | { 731 | $this->dimensions->push(new Dimension(['name' => 'minute'])); 732 | 733 | return $this; 734 | } 735 | 736 | public function mobileDeviceBranding(): self 737 | { 738 | $this->dimensions->push(new Dimension(['name' => 'mobileDeviceBranding'])); 739 | 740 | return $this; 741 | } 742 | 743 | public function mobileDeviceMarketingName(): self 744 | { 745 | $this->dimensions->push(new Dimension(['name' => 'mobileDeviceMarketingName'])); 746 | 747 | return $this; 748 | } 749 | 750 | public function mobileDeviceModel(): self 751 | { 752 | $this->dimensions->push(new Dimension(['name' => 'mobileDeviceModel'])); 753 | 754 | return $this; 755 | } 756 | 757 | public function month(): self 758 | { 759 | $this->dimensions->push(new Dimension(['name' => 'month'])); 760 | 761 | return $this; 762 | } 763 | 764 | public function newVsReturning(): self 765 | { 766 | $this->dimensions->push(new Dimension(['name' => 'newVsReturning'])); 767 | 768 | return $this; 769 | } 770 | 771 | public function nthDay(): self 772 | { 773 | $this->dimensions->push(new Dimension(['name' => 'nthDay'])); 774 | 775 | return $this; 776 | } 777 | 778 | public function nthHour(): self 779 | { 780 | $this->dimensions->push(new Dimension(['name' => 'nthHour'])); 781 | 782 | return $this; 783 | } 784 | 785 | public function nthMinute(): self 786 | { 787 | $this->dimensions->push(new Dimension(['name' => 'nthMinute'])); 788 | 789 | return $this; 790 | } 791 | 792 | public function nthMonth(): self 793 | { 794 | $this->dimensions->push(new Dimension(['name' => 'nthMonth'])); 795 | 796 | return $this; 797 | } 798 | 799 | public function nthWeek(): self 800 | { 801 | $this->dimensions->push(new Dimension(['name' => 'nthWeek'])); 802 | 803 | return $this; 804 | } 805 | 806 | public function nthYear(): self 807 | { 808 | $this->dimensions->push(new Dimension(['name' => 'nthYear'])); 809 | 810 | return $this; 811 | } 812 | 813 | public function operatingSystem(): self 814 | { 815 | $this->dimensions->push(new Dimension(['name' => 'operatingSystem'])); 816 | 817 | return $this; 818 | } 819 | 820 | public function operatingSystemVersion(): self 821 | { 822 | $this->dimensions->push(new Dimension(['name' => 'operatingSystemVersion'])); 823 | 824 | return $this; 825 | } 826 | 827 | public function operatingSystemWithVersion(): self 828 | { 829 | $this->dimensions->push(new Dimension(['name' => 'operatingSystemWithVersion'])); 830 | 831 | return $this; 832 | } 833 | 834 | public function orderCoupon(): self 835 | { 836 | $this->dimensions->push(new Dimension(['name' => 'orderCoupon'])); 837 | 838 | return $this; 839 | } 840 | 841 | public function outbound(): self 842 | { 843 | $this->dimensions->push(new Dimension(['name' => 'outbound'])); 844 | 845 | return $this; 846 | } 847 | 848 | public function pageLocation(): self 849 | { 850 | $this->dimensions->push(new Dimension(['name' => 'pageLocation'])); 851 | 852 | return $this; 853 | } 854 | 855 | public function pagePath(): self 856 | { 857 | $this->dimensions->push(new Dimension(['name' => 'pagePath'])); 858 | 859 | return $this; 860 | } 861 | 862 | public function pagePathPlusQueryString(): self 863 | { 864 | $this->dimensions->push(new Dimension(['name' => 'pagePathPlusQueryString'])); 865 | 866 | return $this; 867 | } 868 | 869 | public function pageReferrer(): self 870 | { 871 | $this->dimensions->push(new Dimension(['name' => 'pageReferrer'])); 872 | 873 | return $this; 874 | } 875 | 876 | public function pageTitle(): self 877 | { 878 | $this->dimensions->push(new Dimension(['name' => 'pageTitle'])); 879 | 880 | return $this; 881 | } 882 | 883 | public function percentScrolled(): self 884 | { 885 | $this->dimensions->push(new Dimension(['name' => 'percentScrolled'])); 886 | 887 | return $this; 888 | } 889 | 890 | public function platform(): self 891 | { 892 | $this->dimensions->push(new Dimension(['name' => 'platform'])); 893 | 894 | return $this; 895 | } 896 | 897 | public function platformDeviceCategory(): self 898 | { 899 | $this->dimensions->push(new Dimension(['name' => 'platformDeviceCategory'])); 900 | 901 | return $this; 902 | } 903 | 904 | public function region(): self 905 | { 906 | $this->dimensions->push(new Dimension(['name' => 'region'])); 907 | 908 | return $this; 909 | } 910 | 911 | public function screenResolution(): self 912 | { 913 | $this->dimensions->push(new Dimension(['name' => 'screenResolution'])); 914 | 915 | return $this; 916 | } 917 | 918 | public function searchTerm(): self 919 | { 920 | $this->dimensions->push(new Dimension(['name' => 'searchTerm'])); 921 | 922 | return $this; 923 | } 924 | 925 | public function sessionCampaignId(): self 926 | { 927 | $this->dimensions->push(new Dimension(['name' => 'sessionCampaignId'])); 928 | 929 | return $this; 930 | } 931 | 932 | public function sessionCampaignName(): self 933 | { 934 | $this->dimensions->push(new Dimension(['name' => 'sessionCampaignName'])); 935 | 936 | return $this; 937 | } 938 | 939 | public function sessionDefaultChannelGroup(): self 940 | { 941 | $this->dimensions->push(new Dimension(['name' => 'sessionDefaultChannelGroup'])); 942 | 943 | return $this; 944 | } 945 | 946 | public function sessionGoogleAdsAccountName(): self 947 | { 948 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsAccountName'])); 949 | 950 | return $this; 951 | } 952 | 953 | public function sessionGoogleAdsAdGroupId(): self 954 | { 955 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsAdGroupId'])); 956 | 957 | return $this; 958 | } 959 | 960 | public function sessionGoogleAdsAdGroupName(): self 961 | { 962 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsAdGroupName'])); 963 | 964 | return $this; 965 | } 966 | 967 | public function sessionGoogleAdsAdNetworkType(): self 968 | { 969 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsAdNetworkType'])); 970 | 971 | return $this; 972 | } 973 | 974 | public function sessionGoogleAdsCampaignId(): self 975 | { 976 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsCampaignId'])); 977 | 978 | return $this; 979 | } 980 | 981 | public function sessionGoogleAdsCampaignName(): self 982 | { 983 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsCampaignName'])); 984 | 985 | return $this; 986 | } 987 | 988 | public function sessionGoogleAdsCampaignType(): self 989 | { 990 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsCampaignType'])); 991 | 992 | return $this; 993 | } 994 | 995 | public function sessionGoogleAdsCreativeId(): self 996 | { 997 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsCreativeId'])); 998 | 999 | return $this; 1000 | } 1001 | 1002 | public function sessionGoogleAdsCustomerId(): self 1003 | { 1004 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsCustomerId'])); 1005 | 1006 | return $this; 1007 | } 1008 | 1009 | public function sessionGoogleAdsKeyword(): self 1010 | { 1011 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsKeyword'])); 1012 | 1013 | return $this; 1014 | } 1015 | 1016 | public function sessionGoogleAdsQuery(): self 1017 | { 1018 | $this->dimensions->push(new Dimension(['name' => 'sessionGoogleAdsQuery'])); 1019 | 1020 | return $this; 1021 | } 1022 | 1023 | public function sessionManualAdContent(): self 1024 | { 1025 | $this->dimensions->push(new Dimension(['name' => 'sessionManualAdContent'])); 1026 | 1027 | return $this; 1028 | } 1029 | 1030 | public function sessionManualTerm(): self 1031 | { 1032 | $this->dimensions->push(new Dimension(['name' => 'sessionManualTerm'])); 1033 | 1034 | return $this; 1035 | } 1036 | 1037 | public function sessionMedium(): self 1038 | { 1039 | $this->dimensions->push(new Dimension(['name' => 'sessionMedium'])); 1040 | 1041 | return $this; 1042 | } 1043 | 1044 | public function sessionSa360AdGroupName(): self 1045 | { 1046 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360AdGroupName'])); 1047 | 1048 | return $this; 1049 | } 1050 | 1051 | public function sessionSa360CampaignId(): self 1052 | { 1053 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360CampaignId'])); 1054 | 1055 | return $this; 1056 | } 1057 | 1058 | public function sessionSa360CampaignName(): self 1059 | { 1060 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360CampaignName'])); 1061 | 1062 | return $this; 1063 | } 1064 | 1065 | public function sessionSa360CreativeFormat(): self 1066 | { 1067 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360CreativeFormat'])); 1068 | 1069 | return $this; 1070 | } 1071 | 1072 | public function sessionSa360EngineAccountId(): self 1073 | { 1074 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360EngineAccountId'])); 1075 | 1076 | return $this; 1077 | } 1078 | 1079 | public function sessionSa360EngineAccountName(): self 1080 | { 1081 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360EngineAccountName'])); 1082 | 1083 | return $this; 1084 | } 1085 | 1086 | public function sessionSa360EngineAccountType(): self 1087 | { 1088 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360EngineAccountType'])); 1089 | 1090 | return $this; 1091 | } 1092 | 1093 | public function sessionSa360Keyword(): self 1094 | { 1095 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360Keyword'])); 1096 | 1097 | return $this; 1098 | } 1099 | 1100 | public function sessionSa360Medium(): self 1101 | { 1102 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360Medium'])); 1103 | 1104 | return $this; 1105 | } 1106 | 1107 | public function sessionSa360Query(): self 1108 | { 1109 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360Query'])); 1110 | 1111 | return $this; 1112 | } 1113 | 1114 | public function sessionSa360Source(): self 1115 | { 1116 | $this->dimensions->push(new Dimension(['name' => 'sessionSa360Source'])); 1117 | 1118 | return $this; 1119 | } 1120 | 1121 | public function sessionSource(): self 1122 | { 1123 | $this->dimensions->push(new Dimension(['name' => 'sessionSource'])); 1124 | 1125 | return $this; 1126 | } 1127 | 1128 | public function sessionSourceMedium(): self 1129 | { 1130 | $this->dimensions->push(new Dimension(['name' => 'sessionSourceMedium'])); 1131 | 1132 | return $this; 1133 | } 1134 | 1135 | public function sessionSourcePlatform(): self 1136 | { 1137 | $this->dimensions->push(new Dimension(['name' => 'sessionSourcePlatform'])); 1138 | 1139 | return $this; 1140 | } 1141 | 1142 | public function shippingTier(): self 1143 | { 1144 | $this->dimensions->push(new Dimension(['name' => 'shippingTier'])); 1145 | 1146 | return $this; 1147 | } 1148 | 1149 | public function signedInWithUserId(): self 1150 | { 1151 | $this->dimensions->push(new Dimension(['name' => 'signedInWithUserId'])); 1152 | 1153 | return $this; 1154 | } 1155 | 1156 | public function source(): self 1157 | { 1158 | $this->dimensions->push(new Dimension(['name' => 'source'])); 1159 | 1160 | return $this; 1161 | } 1162 | 1163 | public function sourceMedium(): self 1164 | { 1165 | $this->dimensions->push(new Dimension(['name' => 'sourceMedium'])); 1166 | 1167 | return $this; 1168 | } 1169 | 1170 | public function sourcePlatform(): self 1171 | { 1172 | $this->dimensions->push(new Dimension(['name' => 'sourcePlatform'])); 1173 | 1174 | return $this; 1175 | } 1176 | 1177 | public function streamId(): self 1178 | { 1179 | $this->dimensions->push(new Dimension(['name' => 'streamId'])); 1180 | 1181 | return $this; 1182 | } 1183 | 1184 | public function streamName(): self 1185 | { 1186 | $this->dimensions->push(new Dimension(['name' => 'streamName'])); 1187 | 1188 | return $this; 1189 | } 1190 | 1191 | public function testDataFilterName(): self 1192 | { 1193 | $this->dimensions->push(new Dimension(['name' => 'testDataFilterName'])); 1194 | 1195 | return $this; 1196 | } 1197 | 1198 | public function transactionId(): self 1199 | { 1200 | $this->dimensions->push(new Dimension(['name' => 'transactionId'])); 1201 | 1202 | return $this; 1203 | } 1204 | 1205 | public function unifiedPagePathScreen(): self 1206 | { 1207 | $this->dimensions->push(new Dimension(['name' => 'unifiedPagePathScreen'])); 1208 | 1209 | return $this; 1210 | } 1211 | 1212 | public function unifiedPageScreen(): self 1213 | { 1214 | $this->dimensions->push(new Dimension(['name' => 'unifiedPageScreen'])); 1215 | 1216 | return $this; 1217 | } 1218 | 1219 | public function unifiedScreenClass(): self 1220 | { 1221 | $this->dimensions->push(new Dimension(['name' => 'unifiedScreenClass'])); 1222 | 1223 | return $this; 1224 | } 1225 | 1226 | public function unifiedScreenName(): self 1227 | { 1228 | $this->dimensions->push(new Dimension(['name' => 'unifiedScreenName'])); 1229 | 1230 | return $this; 1231 | } 1232 | 1233 | public function userAgeBracket(): self 1234 | { 1235 | $this->dimensions->push(new Dimension(['name' => 'userAgeBracket'])); 1236 | 1237 | return $this; 1238 | } 1239 | 1240 | public function userGender(): self 1241 | { 1242 | $this->dimensions->push(new Dimension(['name' => 'userGender'])); 1243 | 1244 | return $this; 1245 | } 1246 | 1247 | public function videoProvider(): self 1248 | { 1249 | $this->dimensions->push(new Dimension(['name' => 'videoProvider'])); 1250 | 1251 | return $this; 1252 | } 1253 | 1254 | public function videoTitle(): self 1255 | { 1256 | $this->dimensions->push(new Dimension(['name' => 'videoTitle'])); 1257 | 1258 | return $this; 1259 | } 1260 | 1261 | public function videoUrl(): self 1262 | { 1263 | $this->dimensions->push(new Dimension(['name' => 'videoUrl'])); 1264 | 1265 | return $this; 1266 | } 1267 | 1268 | public function virtualCurrencyName(): self 1269 | { 1270 | $this->dimensions->push(new Dimension(['name' => 'virtualCurrencyName'])); 1271 | 1272 | return $this; 1273 | } 1274 | 1275 | public function visible(): self 1276 | { 1277 | $this->dimensions->push(new Dimension(['name' => 'visible'])); 1278 | 1279 | return $this; 1280 | } 1281 | 1282 | public function week(): self 1283 | { 1284 | $this->dimensions->push(new Dimension(['name' => 'week'])); 1285 | 1286 | return $this; 1287 | } 1288 | 1289 | public function year(): self 1290 | { 1291 | $this->dimensions->push(new Dimension(['name' => 'year'])); 1292 | 1293 | return $this; 1294 | } 1295 | } 1296 | -------------------------------------------------------------------------------- /src/Request/Filters/AndGroup.php: -------------------------------------------------------------------------------- 1 | expression = $expression(new FilterExpressionList); 20 | } 21 | 22 | public function toRequest(): BaseFilterExpressionList 23 | { 24 | return $this->expression->toRequest(); 25 | } 26 | 27 | public function field(): FilterExpressionField 28 | { 29 | return $this->field; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Request/Filters/BetweenFilter.php: -------------------------------------------------------------------------------- 1 | new NumericValue([ 21 | $this->valueType->value => $this->min, 22 | ]), 23 | 'to_value' => new NumericValue([ 24 | $this->valueType->value => $this->max, 25 | ]), 26 | ]); 27 | } 28 | 29 | public function field(): FilterField 30 | { 31 | return $this->field; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Request/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | fieldName = $fieldName; 20 | } 21 | 22 | public function exact(string $value, bool $caseSensitive = false): static 23 | { 24 | $this->expression = new StringFilter( 25 | matchType: MatchType::EXACT, 26 | value: $value, 27 | caseSensitive: $caseSensitive, 28 | ); 29 | 30 | return $this; 31 | } 32 | 33 | public function beginsWith(string $value, bool $caseSensitive = false): static 34 | { 35 | $this->expression = new StringFilter( 36 | matchType: MatchType::BEGINS_WITH, 37 | value: $value, 38 | caseSensitive: $caseSensitive, 39 | ); 40 | 41 | return $this; 42 | } 43 | 44 | public function endsWith(string $value, bool $caseSensitive = false): static 45 | { 46 | $this->expression = new StringFilter( 47 | matchType: MatchType::ENDS_WITH, 48 | value: $value, 49 | caseSensitive: $caseSensitive, 50 | ); 51 | 52 | return $this; 53 | } 54 | 55 | public function contains(string $value, bool $caseSensitive = false): static 56 | { 57 | $this->expression = new StringFilter( 58 | matchType: MatchType::CONTAINS, 59 | value: $value, 60 | caseSensitive: $caseSensitive, 61 | ); 62 | 63 | return $this; 64 | } 65 | 66 | public function fullRegexp(string $value, bool $caseSensitive = false): static 67 | { 68 | $this->expression = new StringFilter( 69 | matchType: MatchType::FULL_REGEXP, 70 | value: $value, 71 | caseSensitive: $caseSensitive, 72 | ); 73 | 74 | return $this; 75 | } 76 | 77 | public function partialRegexp(string $value, bool $caseSensitive = false): static 78 | { 79 | $this->expression = new StringFilter( 80 | matchType: MatchType::PARTIAL_REGEXP, 81 | value: $value, 82 | caseSensitive: $caseSensitive, 83 | ); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * @param list $values 90 | */ 91 | public function inList(array $values, bool $caseSensitive = false): static 92 | { 93 | $this->expression = new InListFilter( 94 | values: $values, 95 | caseSensitive: $caseSensitive, 96 | ); 97 | 98 | return $this; 99 | } 100 | 101 | public function equalInt(int $value): static 102 | { 103 | $this->expression = new NumericFilter( 104 | operation: Operation::EQUAL, 105 | value: $value, 106 | valueType: NumericValueType::INTEGER, 107 | ); 108 | 109 | return $this; 110 | } 111 | 112 | public function equalFloat(float $value): static 113 | { 114 | $this->expression = new NumericFilter( 115 | operation: Operation::EQUAL, 116 | value: $value, 117 | valueType: NumericValueType::FLOAT, 118 | ); 119 | 120 | return $this; 121 | } 122 | 123 | public function lessThanInt(int $value): static 124 | { 125 | $this->expression = new NumericFilter( 126 | operation: Operation::LESS_THAN, 127 | value: $value, 128 | valueType: NumericValueType::INTEGER, 129 | ); 130 | 131 | return $this; 132 | } 133 | 134 | public function lessThanFloat(float $value): static 135 | { 136 | $this->expression = new NumericFilter( 137 | operation: Operation::LESS_THAN, 138 | value: $value, 139 | valueType: NumericValueType::FLOAT, 140 | ); 141 | 142 | return $this; 143 | } 144 | 145 | public function lessThanOrEqualInt(int $value): static 146 | { 147 | $this->expression = new NumericFilter( 148 | operation: Operation::LESS_THAN_OR_EQUAL, 149 | value: $value, 150 | valueType: NumericValueType::INTEGER, 151 | ); 152 | 153 | return $this; 154 | } 155 | 156 | public function lessThanOrEqualFloat(float $value): static 157 | { 158 | $this->expression = new NumericFilter( 159 | operation: Operation::LESS_THAN_OR_EQUAL, 160 | value: $value, 161 | valueType: NumericValueType::FLOAT, 162 | ); 163 | 164 | return $this; 165 | } 166 | 167 | public function greaterThanInt(int $value): static 168 | { 169 | $this->expression = new NumericFilter( 170 | operation: Operation::GREATER_THAN, 171 | value: $value, 172 | valueType: NumericValueType::INTEGER, 173 | ); 174 | 175 | return $this; 176 | } 177 | 178 | public function greaterThanFloat(float $value): static 179 | { 180 | $this->expression = new NumericFilter( 181 | operation: Operation::GREATER_THAN, 182 | value: $value, 183 | valueType: NumericValueType::FLOAT, 184 | ); 185 | 186 | return $this; 187 | } 188 | 189 | public function greaterThanOrEqualInt(int $value): static 190 | { 191 | $this->expression = new NumericFilter( 192 | operation: Operation::GREATER_THAN_OR_EQUAL, 193 | value: $value, 194 | valueType: NumericValueType::INTEGER, 195 | ); 196 | 197 | return $this; 198 | } 199 | 200 | public function greaterThanOrEqualFloat(float $value): static 201 | { 202 | $this->expression = new NumericFilter( 203 | operation: Operation::GREATER_THAN_OR_EQUAL, 204 | value: $value, 205 | valueType: NumericValueType::FLOAT, 206 | ); 207 | 208 | return $this; 209 | } 210 | 211 | public function betweenInt(int $min, int $max): static 212 | { 213 | $this->expression = new BetweenFilter( 214 | min: $min, 215 | max: $max, 216 | valueType: NumericValueType::INTEGER, 217 | ); 218 | 219 | return $this; 220 | } 221 | 222 | public function betweenFloat(float $min, float $max): static 223 | { 224 | $this->expression = new BetweenFilter( 225 | min: $min, 226 | max: $max, 227 | valueType: NumericValueType::FLOAT, 228 | ); 229 | 230 | return $this; 231 | } 232 | 233 | public function toRequest(): BaseFilter 234 | { 235 | return new BaseFilter([ 236 | 'field_name' => $this->fieldName, 237 | $this->expression?->field()->value => $this->expression?->toRequest(), 238 | ]); 239 | } 240 | 241 | public function field(): FilterExpressionField 242 | { 243 | return $this->field; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/Request/Filters/FilterContract.php: -------------------------------------------------------------------------------- 1 | expression = new AndGroup($filterExpressionList); 21 | 22 | return $this; 23 | } 24 | 25 | /** 26 | * @param Closure(FilterExpressionList): FilterExpressionList $filterExpressionList 27 | */ 28 | public function orGroup(Closure $filterExpressionList): static 29 | { 30 | $this->expression = new OrGroup($filterExpressionList); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @param Closure(FilterExpression): FilterExpression $filterExpression 37 | */ 38 | public function not(Closure $filterExpression): static 39 | { 40 | $this->expression = new NotExpression($filterExpression); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @param Closure(Filter): Filter $filter 47 | */ 48 | public function filter(string $dimension, Closure $filter): static 49 | { 50 | $this->expression = $filter(new Filter($dimension)); 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * @param Closure(Filter): Filter $filter 57 | * 58 | * @throws InvalidFilterException 59 | */ 60 | public function filterDimension(Closure $dimensionsCallback, Closure $filter): static 61 | { 62 | /** @var Dimensions $dimensions */ 63 | $dimensions = $dimensionsCallback(resolve(Dimensions::class)); 64 | 65 | $firstDimension = $dimensions->first(); 66 | 67 | if ($firstDimension === null) { 68 | throw InvalidFilterException::noDimensionFilter(); 69 | } 70 | 71 | return $this->filter($firstDimension->getName(), $filter); 72 | } 73 | 74 | /** 75 | * @param Closure(Filter): Filter $filter 76 | * 77 | * @throws InvalidFilterException 78 | */ 79 | public function filterMetric(Closure $metricsCallback, Closure $filter): static 80 | { 81 | /** @var Metrics $metrics */ 82 | $metrics = $metricsCallback(resolve(Metrics::class)); 83 | 84 | $firstMetric = $metrics->first(); 85 | 86 | if ($firstMetric === null) { 87 | throw InvalidFilterException::noMetricFilter(); 88 | } 89 | 90 | return $this->filter($firstMetric->getName(), $filter); 91 | } 92 | 93 | public function toRequest(): BaseFilterExpression 94 | { 95 | return new BaseFilterExpression([ 96 | $this->expression->field()->value => $this->expression->toRequest(), 97 | ]); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Request/Filters/FilterExpressionContract.php: -------------------------------------------------------------------------------- 1 | */ 14 | private readonly Collection $expressions = new Collection, 15 | ) {} 16 | 17 | /** 18 | * @param Closure(FilterExpressionList): FilterExpressionList $filterExpressionList 19 | */ 20 | public function andGroup(Closure $filterExpressionList): static 21 | { 22 | $this->expressions->push((new FilterExpression)->andGroup($filterExpressionList)); 23 | 24 | return $this; 25 | } 26 | 27 | /** 28 | * @param Closure(FilterExpressionList): FilterExpressionList $filterExpressionList 29 | */ 30 | public function orGroup(Closure $filterExpressionList): static 31 | { 32 | $this->expressions->push((new FilterExpression)->orGroup($filterExpressionList)); 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * @param Closure(FilterExpression): FilterExpression $filterExpression 39 | */ 40 | public function not(Closure $filterExpression): static 41 | { 42 | $this->expressions->push((new FilterExpression)->not($filterExpression)); 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @param Closure(Filter): Filter $filter 49 | */ 50 | public function filter(string $dimension, Closure $filter): static 51 | { 52 | $this->expressions->push((new FilterExpression)->filter($dimension, $filter)); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @param Closure(Filter): Filter $filter 59 | * 60 | * @throws InvalidFilterException 61 | */ 62 | public function filterDimension(Closure $dimensionsCallback, Closure $filter): static 63 | { 64 | $this->expressions->push((new FilterExpression)->filterDimension($dimensionsCallback, $filter)); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @param Closure(Filter): Filter $filter 71 | * 72 | * @throws InvalidFilterException 73 | */ 74 | public function filterMetric(Closure $metricsCallback, Closure $filter): static 75 | { 76 | $this->expressions->push((new FilterExpression)->filterMetric($metricsCallback, $filter)); 77 | 78 | return $this; 79 | } 80 | 81 | public function toRequest(): BaseFilterExpressionList 82 | { 83 | return new BaseFilterExpressionList([ 84 | 'expressions' => $this->expressions 85 | ->map(fn (FilterExpression $filterExpression) => $filterExpression->toRequest()) 86 | ->toArray(), 87 | ]); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Request/Filters/FilterField.php: -------------------------------------------------------------------------------- 1 | $values 11 | */ 12 | public function __construct( 13 | public array $values = [], 14 | public bool $caseSensitive = false, 15 | private readonly FilterField $field = FilterField::IN_LIST_FILTER, 16 | ) {} 17 | 18 | public function field(): FilterField 19 | { 20 | return $this->field; 21 | } 22 | 23 | public function toRequest(): BaseInListFilter 24 | { 25 | return new BaseInListFilter([ 26 | 'values' => $this->values, 27 | 'case_sensitive' => $this->caseSensitive, 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Request/Filters/NotExpression.php: -------------------------------------------------------------------------------- 1 | expression = $expression(new FilterExpression); 20 | } 21 | 22 | public function toRequest(): BaseFilterExpression 23 | { 24 | return $this->expression->toRequest(); 25 | } 26 | 27 | public function field(): FilterExpressionField 28 | { 29 | return $this->field; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Request/Filters/NumericFilter.php: -------------------------------------------------------------------------------- 1 | $this->operation, 22 | 'value' => new NumericValue([ 23 | $this->valueType->value => $this->value, 24 | ]), 25 | ]); 26 | } 27 | 28 | public function field(): FilterField 29 | { 30 | return $this->field; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Request/Filters/NumericValueType.php: -------------------------------------------------------------------------------- 1 | expression = $expression(new FilterExpressionList); 20 | } 21 | 22 | public function toRequest(): BaseFilterExpressionList 23 | { 24 | return $this->expression->toRequest(); 25 | } 26 | 27 | public function field(): FilterExpressionField 28 | { 29 | return $this->field; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Request/Filters/StringFilter.php: -------------------------------------------------------------------------------- 1 | $this->matchType, 21 | 'value' => $this->value, 22 | 'case_sensitive' => $this->caseSensitive, 23 | ]); 24 | } 25 | 26 | public function field(): FilterField 27 | { 28 | return $this->field; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Request/Metrics.php: -------------------------------------------------------------------------------- 1 | */ 11 | protected Collection $metrics; 12 | 13 | public function __construct() 14 | { 15 | $this->metrics = new Collection; 16 | } 17 | 18 | public function count(): int 19 | { 20 | return $this->metrics->count(); 21 | } 22 | 23 | public function first(): ?Metric 24 | { 25 | return $this->metrics->first(); 26 | } 27 | 28 | /** 29 | * @return Collection 30 | */ 31 | public function getMetrics(): Collection 32 | { 33 | return $this->metrics; 34 | } 35 | 36 | public function active1DayUsers(): self 37 | { 38 | $this->metrics->push(new Metric(['name' => 'active1DayUsers'])); 39 | 40 | return $this; 41 | } 42 | 43 | public function active28DayUsers(): self 44 | { 45 | $this->metrics->push(new Metric(['name' => 'active28DayUsers'])); 46 | 47 | return $this; 48 | } 49 | 50 | public function active7DayUsers(): self 51 | { 52 | $this->metrics->push(new Metric(['name' => 'active7DayUsers'])); 53 | 54 | return $this; 55 | } 56 | 57 | public function activeUsers(): self 58 | { 59 | $this->metrics->push(new Metric(['name' => 'activeUsers'])); 60 | 61 | return $this; 62 | } 63 | 64 | public function adUnitExposure(): self 65 | { 66 | $this->metrics->push(new Metric(['name' => 'adUnitExposure'])); 67 | 68 | return $this; 69 | } 70 | 71 | public function addToCarts(): self 72 | { 73 | $this->metrics->push(new Metric(['name' => 'addToCarts'])); 74 | 75 | return $this; 76 | } 77 | 78 | public function advertiserAdClicks(): self 79 | { 80 | $this->metrics->push(new Metric(['name' => 'advertiserAdClicks'])); 81 | 82 | return $this; 83 | } 84 | 85 | public function advertiserAdCost(): self 86 | { 87 | $this->metrics->push(new Metric(['name' => 'advertiserAdCost'])); 88 | 89 | return $this; 90 | } 91 | 92 | public function advertiserAdCostPerClick(): self 93 | { 94 | $this->metrics->push(new Metric(['name' => 'advertiserAdCostPerClick'])); 95 | 96 | return $this; 97 | } 98 | 99 | public function advertiserAdCostPerConversion(): self 100 | { 101 | $this->metrics->push(new Metric(['name' => 'advertiserAdCostPerConversion'])); 102 | 103 | return $this; 104 | } 105 | 106 | public function advertiserAdImpressions(): self 107 | { 108 | $this->metrics->push(new Metric(['name' => 'advertiserAdImpressions'])); 109 | 110 | return $this; 111 | } 112 | 113 | public function averagePurchaseRevenue(): self 114 | { 115 | $this->metrics->push(new Metric(['name' => 'averagePurchaseRevenue'])); 116 | 117 | return $this; 118 | } 119 | 120 | public function averagePurchaseRevenuePerPayingUser(): self 121 | { 122 | $this->metrics->push(new Metric(['name' => 'averagePurchaseRevenuePerPayingUser'])); 123 | 124 | return $this; 125 | } 126 | 127 | public function averagePurchaseRevenuePerUser(): self 128 | { 129 | $this->metrics->push(new Metric(['name' => 'averagePurchaseRevenuePerUser'])); 130 | 131 | return $this; 132 | } 133 | 134 | public function averageRevenuePerUser(): self 135 | { 136 | $this->metrics->push(new Metric(['name' => 'averageRevenuePerUser'])); 137 | 138 | return $this; 139 | } 140 | 141 | public function averageSessionDuration(): self 142 | { 143 | $this->metrics->push(new Metric(['name' => 'averageSessionDuration'])); 144 | 145 | return $this; 146 | } 147 | 148 | public function bounceRate(): self 149 | { 150 | $this->metrics->push(new Metric(['name' => 'bounceRate'])); 151 | 152 | return $this; 153 | } 154 | 155 | public function cartToViewRate(): self 156 | { 157 | $this->metrics->push(new Metric(['name' => 'cartToViewRate'])); 158 | 159 | return $this; 160 | } 161 | 162 | public function checkouts(): self 163 | { 164 | $this->metrics->push(new Metric(['name' => 'checkouts'])); 165 | 166 | return $this; 167 | } 168 | 169 | public function cohortActiveUsers(): self 170 | { 171 | $this->metrics->push(new Metric(['name' => 'cohortActiveUsers'])); 172 | 173 | return $this; 174 | } 175 | 176 | public function cohortTotalUsers(): self 177 | { 178 | $this->metrics->push(new Metric(['name' => 'cohortTotalUsers'])); 179 | 180 | return $this; 181 | } 182 | 183 | public function conversions(): self 184 | { 185 | $this->metrics->push(new Metric(['name' => 'conversions'])); 186 | 187 | return $this; 188 | } 189 | 190 | public function crashAffectedUsers(): self 191 | { 192 | $this->metrics->push(new Metric(['name' => 'crashAffectedUsers'])); 193 | 194 | return $this; 195 | } 196 | 197 | public function crashFreeUsersRate(): self 198 | { 199 | $this->metrics->push(new Metric(['name' => 'crashFreeUsersRate'])); 200 | 201 | return $this; 202 | } 203 | 204 | public function dauPerMau(): self 205 | { 206 | $this->metrics->push(new Metric(['name' => 'dauPerMau'])); 207 | 208 | return $this; 209 | } 210 | 211 | public function dauPerWau(): self 212 | { 213 | $this->metrics->push(new Metric(['name' => 'dauPerWau'])); 214 | 215 | return $this; 216 | } 217 | 218 | public function ecommercePurchases(): self 219 | { 220 | $this->metrics->push(new Metric(['name' => 'ecommercePurchases'])); 221 | 222 | return $this; 223 | } 224 | 225 | public function engagedSessions(): self 226 | { 227 | $this->metrics->push(new Metric(['name' => 'engagedSessions'])); 228 | 229 | return $this; 230 | } 231 | 232 | public function engagementRate(): self 233 | { 234 | $this->metrics->push(new Metric(['name' => 'engagementRate'])); 235 | 236 | return $this; 237 | } 238 | 239 | public function eventCount(): self 240 | { 241 | $this->metrics->push(new Metric(['name' => 'eventCount'])); 242 | 243 | return $this; 244 | } 245 | 246 | public function eventCountPerUser(): self 247 | { 248 | $this->metrics->push(new Metric(['name' => 'eventCountPerUser'])); 249 | 250 | return $this; 251 | } 252 | 253 | public function eventValue(): self 254 | { 255 | $this->metrics->push(new Metric(['name' => 'eventValue'])); 256 | 257 | return $this; 258 | } 259 | 260 | public function eventsPerSession(): self 261 | { 262 | $this->metrics->push(new Metric(['name' => 'eventsPerSession'])); 263 | 264 | return $this; 265 | } 266 | 267 | public function firstTimePurchaserConversionRate(): self 268 | { 269 | $this->metrics->push(new Metric(['name' => 'firstTimePurchaserConversionRate'])); 270 | 271 | return $this; 272 | } 273 | 274 | public function firstTimePurchasers(): self 275 | { 276 | $this->metrics->push(new Metric(['name' => 'firstTimePurchasers'])); 277 | 278 | return $this; 279 | } 280 | 281 | public function firstTimePurchasersPerNewUser(): self 282 | { 283 | $this->metrics->push(new Metric(['name' => 'firstTimePurchasersPerNewUser'])); 284 | 285 | return $this; 286 | } 287 | 288 | public function itemListClickEvents(): self 289 | { 290 | $this->metrics->push(new Metric(['name' => 'itemListClickEvents'])); 291 | 292 | return $this; 293 | } 294 | 295 | public function itemListClickThroughRate(): self 296 | { 297 | $this->metrics->push(new Metric(['name' => 'itemListClickThroughRate'])); 298 | 299 | return $this; 300 | } 301 | 302 | public function itemListViewEvents(): self 303 | { 304 | $this->metrics->push(new Metric(['name' => 'itemListViewEvents'])); 305 | 306 | return $this; 307 | } 308 | 309 | public function itemPromotionClickThroughRate(): self 310 | { 311 | $this->metrics->push(new Metric(['name' => 'itemPromotionClickThroughRate'])); 312 | 313 | return $this; 314 | } 315 | 316 | public function itemRevenue(): self 317 | { 318 | $this->metrics->push(new Metric(['name' => 'itemRevenue'])); 319 | 320 | return $this; 321 | } 322 | 323 | public function itemViewEvents(): self 324 | { 325 | $this->metrics->push(new Metric(['name' => 'itemViewEvents'])); 326 | 327 | return $this; 328 | } 329 | 330 | public function itemsAddedToCart(): self 331 | { 332 | $this->metrics->push(new Metric(['name' => 'itemsAddedToCart'])); 333 | 334 | return $this; 335 | } 336 | 337 | public function itemsCheckedOut(): self 338 | { 339 | $this->metrics->push(new Metric(['name' => 'itemsCheckedOut'])); 340 | 341 | return $this; 342 | } 343 | 344 | public function itemsClickedInList(): self 345 | { 346 | $this->metrics->push(new Metric(['name' => 'itemsClickedInList'])); 347 | 348 | return $this; 349 | } 350 | 351 | public function itemsClickedInPromotion(): self 352 | { 353 | $this->metrics->push(new Metric(['name' => 'itemsClickedInPromotion'])); 354 | 355 | return $this; 356 | } 357 | 358 | public function itemsPurchased(): self 359 | { 360 | $this->metrics->push(new Metric(['name' => 'itemsPurchased'])); 361 | 362 | return $this; 363 | } 364 | 365 | public function itemsViewed(): self 366 | { 367 | $this->metrics->push(new Metric(['name' => 'itemsViewed'])); 368 | 369 | return $this; 370 | } 371 | 372 | public function itemsViewedInList(): self 373 | { 374 | $this->metrics->push(new Metric(['name' => 'itemsViewedInList'])); 375 | 376 | return $this; 377 | } 378 | 379 | public function itemsViewedInPromotion(): self 380 | { 381 | $this->metrics->push(new Metric(['name' => 'itemsViewedInPromotion'])); 382 | 383 | return $this; 384 | } 385 | 386 | public function newUsers(): self 387 | { 388 | $this->metrics->push(new Metric(['name' => 'newUsers'])); 389 | 390 | return $this; 391 | } 392 | 393 | public function organicGoogleSearchAveragePosition(): self 394 | { 395 | $this->metrics->push(new Metric(['name' => 'organicGoogleSearchAveragePosition'])); 396 | 397 | return $this; 398 | } 399 | 400 | public function organicGoogleSearchClickThroughRate(): self 401 | { 402 | $this->metrics->push(new Metric(['name' => 'organicGoogleSearchClickThroughRate'])); 403 | 404 | return $this; 405 | } 406 | 407 | public function organicGoogleSearchClicks(): self 408 | { 409 | $this->metrics->push(new Metric(['name' => 'organicGoogleSearchClicks'])); 410 | 411 | return $this; 412 | } 413 | 414 | public function organicGoogleSearchImpressions(): self 415 | { 416 | $this->metrics->push(new Metric(['name' => 'organicGoogleSearchImpressions'])); 417 | 418 | return $this; 419 | } 420 | 421 | public function promotionClicks(): self 422 | { 423 | $this->metrics->push(new Metric(['name' => 'promotionClicks'])); 424 | 425 | return $this; 426 | } 427 | 428 | public function promotionViews(): self 429 | { 430 | $this->metrics->push(new Metric(['name' => 'promotionViews'])); 431 | 432 | return $this; 433 | } 434 | 435 | public function publisherAdClicks(): self 436 | { 437 | $this->metrics->push(new Metric(['name' => 'publisherAdClicks'])); 438 | 439 | return $this; 440 | } 441 | 442 | public function publisherAdImpressions(): self 443 | { 444 | $this->metrics->push(new Metric(['name' => 'publisherAdImpressions'])); 445 | 446 | return $this; 447 | } 448 | 449 | public function purchaseRevenue(): self 450 | { 451 | $this->metrics->push(new Metric(['name' => 'purchaseRevenue'])); 452 | 453 | return $this; 454 | } 455 | 456 | public function purchaseToViewRate(): self 457 | { 458 | $this->metrics->push(new Metric(['name' => 'purchaseToViewRate'])); 459 | 460 | return $this; 461 | } 462 | 463 | public function purchaserConversionRate(): self 464 | { 465 | $this->metrics->push(new Metric(['name' => 'purchaserConversionRate'])); 466 | 467 | return $this; 468 | } 469 | 470 | public function returnOnAdSpend(): self 471 | { 472 | $this->metrics->push(new Metric(['name' => 'returnOnAdSpend'])); 473 | 474 | return $this; 475 | } 476 | 477 | public function screenPageViews(): self 478 | { 479 | $this->metrics->push(new Metric(['name' => 'screenPageViews'])); 480 | 481 | return $this; 482 | } 483 | 484 | public function screenPageViewsPerSession(): self 485 | { 486 | $this->metrics->push(new Metric(['name' => 'screenPageViewsPerSession'])); 487 | 488 | return $this; 489 | } 490 | 491 | public function sessionConversionRate(): self 492 | { 493 | $this->metrics->push(new Metric(['name' => 'sessionConversionRate'])); 494 | 495 | return $this; 496 | } 497 | 498 | public function sessions(): self 499 | { 500 | $this->metrics->push(new Metric(['name' => 'sessions'])); 501 | 502 | return $this; 503 | } 504 | 505 | public function sessionsPerUser(): self 506 | { 507 | $this->metrics->push(new Metric(['name' => 'sessionsPerUser'])); 508 | 509 | return $this; 510 | } 511 | 512 | public function shippingAmount(): self 513 | { 514 | $this->metrics->push(new Metric(['name' => 'shippingAmount'])); 515 | 516 | return $this; 517 | } 518 | 519 | public function taxAmount(): self 520 | { 521 | $this->metrics->push(new Metric(['name' => 'taxAmount'])); 522 | 523 | return $this; 524 | } 525 | 526 | public function totalAdRevenue(): self 527 | { 528 | $this->metrics->push(new Metric(['name' => 'totalAdRevenue'])); 529 | 530 | return $this; 531 | } 532 | 533 | public function totalPurchasers(): self 534 | { 535 | $this->metrics->push(new Metric(['name' => 'totalPurchasers'])); 536 | 537 | return $this; 538 | } 539 | 540 | public function totalRevenue(): self 541 | { 542 | $this->metrics->push(new Metric(['name' => 'totalRevenue'])); 543 | 544 | return $this; 545 | } 546 | 547 | public function totalUsers(): self 548 | { 549 | $this->metrics->push(new Metric(['name' => 'totalUsers'])); 550 | 551 | return $this; 552 | } 553 | 554 | public function transactions(): self 555 | { 556 | $this->metrics->push(new Metric(['name' => 'transactions'])); 557 | 558 | return $this; 559 | } 560 | 561 | public function transactionsPerPurchaser(): self 562 | { 563 | $this->metrics->push(new Metric(['name' => 'transactionsPerPurchaser'])); 564 | 565 | return $this; 566 | } 567 | 568 | public function userConversionRate(): self 569 | { 570 | $this->metrics->push(new Metric(['name' => 'userConversionRate'])); 571 | 572 | return $this; 573 | } 574 | 575 | public function userEngagementDuration(): self 576 | { 577 | $this->metrics->push(new Metric(['name' => 'userEngagementDuration'])); 578 | 579 | return $this; 580 | } 581 | 582 | public function wauPerMau(): self 583 | { 584 | $this->metrics->push(new Metric(['name' => 'wauPerMau'])); 585 | 586 | return $this; 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /src/Request/RequestData.php: -------------------------------------------------------------------------------- 1 | $dateRanges 17 | * @param Collection $metrics 18 | * @param Collection $dimensions 19 | */ 20 | class RequestData extends Data 21 | { 22 | public function __construct( 23 | public string $propertyId, 24 | /** @var Collection */ 25 | public Collection $dateRanges = new Collection, 26 | /** @var Collection */ 27 | public Collection $metrics = new Collection, 28 | /** @var Collection */ 29 | public Collection $dimensions = new Collection, 30 | 31 | public ?FilterExpression $dimensionFilter = null, 32 | 33 | public ?FilterExpression $metricFilter = null, 34 | 35 | public bool $returnPropertyQuota = true, 36 | 37 | public bool $useTotals = false, 38 | 39 | public int $limit = 10_000, 40 | 41 | public int $offset = 0, 42 | ) {} 43 | 44 | /** @return array{property: string, dateRanges: DateRange[], dimensions: Dimension[], metrics: Metric[], dimensionFilter: BaseFilterExpression|null, returnPropertyQuota: bool, metricAggregations: int[]} */ 45 | public function toArray(): array 46 | { 47 | return [ 48 | 'property' => 'properties/'.$this->propertyId, 49 | 'dateRanges' => $this->dateRanges->all(), 50 | 'dimensions' => $this->dimensions->unique()->all(), 51 | 'metrics' => $this->metrics->unique()->all(), 52 | 'dimensionFilter' => $this->dimensionFilter?->toRequest(), 53 | 'metricFilter' => $this->metricFilter?->toRequest(), 54 | 'returnPropertyQuota' => $this->returnPropertyQuota, 55 | 'metricAggregations' => $this->useTotals ? [MetricAggregation::TOTAL] : [], 56 | 'limit' => $this->limit, 57 | 'offset' => $this->offset, 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Response/DimensionHeader.php: -------------------------------------------------------------------------------- 1 | |null $dimensionHeaders 14 | * @param DataCollection $metricHeaders 15 | * @param DataCollection $rows 16 | * @param DataCollection|null $totals 17 | */ 18 | public function __construct( 19 | #[DataCollectionOf(DimensionHeader::class)] 20 | public ?DataCollection $dimensionHeaders, 21 | #[DataCollectionOf(MetricHeader::class)] 22 | public DataCollection $metricHeaders, 23 | #[DataCollectionOf(Row::class)] 24 | public DataCollection $rows, 25 | #[DataCollectionOf(Total::class)] 26 | public ?DataCollection $totals, 27 | public int $rowCount, 28 | public Metadata $metadata, 29 | public ?PropertyQuota $propertyQuota, 30 | public string $kind, 31 | ) {} 32 | 33 | public static function fromReportResponse(RunReportResponse $reportResponse): static 34 | { 35 | $json = $reportResponse->serializeToJsonString(); 36 | 37 | $report = json_decode($json, true); 38 | 39 | return self::from($report + ['rows' => new DataCollection(Row::class, []), 'rowCount' => 0]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Response/Row.php: -------------------------------------------------------------------------------- 1 | |null $dimensionValues 13 | * @param DataCollection $metricValues 14 | */ 15 | public function __construct( 16 | #[DataCollectionOf(DimensionValue::class)] 17 | public ?DataCollection $dimensionValues, 18 | #[DataCollectionOf(MetricValue::class)] 19 | public DataCollection $metricValues, 20 | ) {} 21 | } 22 | -------------------------------------------------------------------------------- /src/Response/Total.php: -------------------------------------------------------------------------------- 1 | $dimensionValues 13 | * @param DataCollection $metricValues 14 | */ 15 | public function __construct( 16 | #[DataCollectionOf(DimensionValue::class)] 17 | public DataCollection $dimensionValues, 18 | #[DataCollectionOf(MetricValue::class)] 19 | public DataCollection $metricValues, 20 | ) {} 21 | } 22 | -------------------------------------------------------------------------------- /tests/AnalyticsTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Analytics::class, new Analytics); 36 | } 37 | 38 | public function test_constructor_with_propertyid(): void 39 | { 40 | config()->set('analytics.property_id', 'def'); 41 | $newPropertyId = 'abc'; 42 | $analytics = new Analytics($newPropertyId); 43 | $this->assertInstanceOf(Analytics::class, $analytics); 44 | 45 | /** @var RequestData $requestData */ 46 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 47 | 48 | $this->assertEquals($newPropertyId, $requestData->propertyId); 49 | } 50 | 51 | public function test_property_string_is_empty_exception(): void 52 | { 53 | config()->offsetUnset('analytics.property_id'); 54 | 55 | $this->expectException(InvalidPropertyIdException::class); 56 | $this->expectExceptionMessage(InvalidPropertyIdException::MESSAGE_INVALID_PROPERTY_ID); 57 | 58 | Analytics::query(); 59 | } 60 | 61 | public function test_property_string_is_not_string_exception(): void 62 | { 63 | config()->set('analytics.property_id', 1234); 64 | 65 | $this->expectException(InvalidPropertyIdException::class); 66 | $this->expectExceptionMessage(InvalidPropertyIdException::MESSAGE_INVALID_PROPERTY_ID); 67 | 68 | Analytics::query(); 69 | } 70 | 71 | public function test_set_metrics(): void 72 | { 73 | $analytics = Analytics::query() 74 | ->setMetrics(fn (Metrics $metrics) => $metrics 75 | ->sessions() 76 | ->bounceRate() 77 | ); 78 | 79 | $this->assertInstanceOf(Analytics::class, $analytics); 80 | 81 | /** @var RequestData $requestData */ 82 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 83 | $requestMetrics = $requestData->metrics->map(fn (Metric $metric) => $metric->getName())->toArray(); 84 | 85 | $this->assertEquals(['sessions', 'bounceRate'], $requestMetrics); 86 | } 87 | 88 | public function test_custom_metrics(): void 89 | { 90 | app()->bind(Metrics::class, CustomMetrics::class); 91 | 92 | $analytics = Analytics::query() 93 | ->setMetrics(fn (CustomMetrics $metrics) => $metrics 94 | ->customMetric() 95 | ->sessions() 96 | ); 97 | 98 | $this->assertInstanceOf(Analytics::class, $analytics); 99 | 100 | /** @var RequestData $requestData */ 101 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 102 | $requestMetrics = $requestData->metrics->map(fn (Metric $metric) => $metric->getName())->toArray(); 103 | 104 | $this->assertEquals(['customMetric', 'sessions'], $requestMetrics); 105 | } 106 | 107 | public function test_set_dimensions(): void 108 | { 109 | $analytics = Analytics::query() 110 | ->setDimensions(fn (Dimensions $dimensions) => $dimensions 111 | ->browser() 112 | ); 113 | 114 | $this->assertInstanceOf(Analytics::class, $analytics); 115 | 116 | /** @var RequestData $requestData */ 117 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 118 | $requestDimensions = $requestData->dimensions->map(fn (Dimension $dimension) => $dimension->getName())->toArray(); 119 | 120 | $this->assertEquals(['browser'], $requestDimensions); 121 | } 122 | 123 | public function test_custom_dimensions(): void 124 | { 125 | app()->bind(Dimensions::class, CustomDimensions::class); 126 | 127 | $analytics = Analytics::query() 128 | ->setDimensions(fn (CustomDimensions $dimensions) => $dimensions 129 | ->customDimension() 130 | ->browser() 131 | ); 132 | 133 | $this->assertInstanceOf(Analytics::class, $analytics); 134 | 135 | /** @var RequestData $requestData */ 136 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 137 | $requestDimensions = $requestData->dimensions->map(fn (Dimension $dimension) => $dimension->getName())->toArray(); 138 | 139 | $this->assertEquals(['customDimension', 'browser'], $requestDimensions); 140 | } 141 | 142 | public function test_limit(): void 143 | { 144 | $analytics = Analytics::query() 145 | ->limit(5); 146 | 147 | $this->assertInstanceOf(Analytics::class, $analytics); 148 | 149 | /** @var RequestData $requestData */ 150 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 151 | 152 | $this->assertEquals(5, $requestData->limit); 153 | 154 | $analytics->limit(); 155 | 156 | $this->assertEquals(10_000, $requestData->limit); 157 | } 158 | 159 | public function test_offset(): void 160 | { 161 | $analytics = Analytics::query() 162 | ->offset(5); 163 | 164 | $this->assertInstanceOf(Analytics::class, $analytics); 165 | 166 | /** @var RequestData $requestData */ 167 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 168 | 169 | $this->assertEquals(5, $requestData->offset); 170 | 171 | $analytics->offset(); 172 | 173 | $this->assertEquals(0, $requestData->offset); 174 | } 175 | 176 | public function test_for_period(): void 177 | { 178 | CarbonImmutable::setTestNow(CarbonImmutable::parse('2022-10-10')); 179 | 180 | $analytics = Analytics::query() 181 | ->forPeriod(Period::lastWeek() 182 | ); 183 | 184 | $this->assertInstanceOf(Analytics::class, $analytics); 185 | 186 | /** @var RequestData $requestData */ 187 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 188 | $requestDateRange = $requestData->dateRanges->first(); 189 | 190 | if ($requestDateRange === null) { 191 | $this->fail('Request date range is null'); 192 | } 193 | 194 | $this->assertEquals('2022-10-03', $requestDateRange->getStartDate()); 195 | $this->assertEquals('2022-10-09', $requestDateRange->getEndDate()); 196 | } 197 | 198 | public function test_with_totals(): void 199 | { 200 | $analytics = Analytics::query() 201 | ->withTotals(); 202 | 203 | $this->assertInstanceOf(Analytics::class, $analytics); 204 | 205 | /** @var RequestData $requestData */ 206 | $requestData = (new ReflectionProperty(Analytics::class, 'requestData'))->getValue($analytics); 207 | 208 | $this->assertTrue($requestData->useTotals); 209 | 210 | $analytics->withTotals(false); 211 | 212 | $this->assertFalse($requestData->useTotals); 213 | } 214 | 215 | /** 216 | * @throws ApiException 217 | */ 218 | public function test_run(): void 219 | { 220 | CarbonImmutable::setTestNow(CarbonImmutable::parse('2022-10-10')); 221 | 222 | $responseMock = $this->mock(RunReportResponse::class, function (MockInterface $mock) { 223 | $mock->shouldReceive('serializeToJsonString') 224 | ->once() 225 | ->andReturn(json_encode([ 226 | 'dimensionHeaders' => [ 227 | [ 228 | 'name' => 'browser', 229 | ], 230 | ], 231 | 'metricHeaders' => [ 232 | [ 233 | 'name' => 'sessions', 234 | 'type' => 'TYPE_INTEGER', 235 | ], 236 | [ 237 | 'name' => 'bounceRate', 238 | 'type' => 'TYPE_DOUBLE', 239 | ], 240 | ], 241 | 'rows' => [ 242 | [ 243 | 'dimensionValues' => [ 244 | 245 | [ 246 | 'value' => 'Browser1', 247 | ], 248 | ], 249 | 'metricValues' => [ 250 | [ 251 | 'value' => 123, 252 | ], 253 | [ 254 | 'value' => 0.123, 255 | ], 256 | ], 257 | ], 258 | [ 259 | 'dimensionValues' => [ 260 | 261 | [ 262 | 'value' => 'Browser2', 263 | ], 264 | ], 265 | 'metricValues' => [ 266 | [ 267 | 'value' => 456, 268 | ], 269 | [ 270 | 'value' => 0.456, 271 | ], 272 | ], 273 | ], 274 | ], 275 | 'totals' => [ 276 | [ 277 | 'dimensionValues' => [ 278 | [ 279 | 'value' => 'RESERVED_TOTAL', 280 | ], 281 | ], 282 | 'metricValues' => [ 283 | [ 284 | 'value' => 579, 285 | ], 286 | [ 287 | 'value' => 0.579, 288 | ], 289 | ], 290 | ], 291 | ], 292 | 'rowCount' => 2, 293 | 'metadata' => [ 294 | 'currencyCode' => 'USD', 295 | 'timeZone' => 'UTC', 296 | ], 297 | 'propertyQuota' => [ 298 | 'tokensPerDay' => [ 299 | 'consumed' => 9, 300 | 'remaining' => 24821, 301 | ], 302 | 'tokensPerHour' => [ 303 | 'consumed' => 9, 304 | 'remaining' => 4981, 305 | ], 306 | 'concurrentRequests' => [ 307 | 'remaining' => 10, 308 | ], 309 | 'serverErrorsPerProjectPerHour' => [ 310 | 'remaining' => 10, 311 | ], 312 | 'potentiallyThresholdedRequestsPerHour' => [ 313 | 'remaining' => 120, 314 | ], 315 | 'tokensPerProjectPerHour' => [ 316 | 'consumed' => 9, 317 | 'remaining' => 1231, 318 | ], 319 | ], 320 | 'kind' => 'analyticsData#runReport', 321 | ])); 322 | }); 323 | 324 | $this->mock(BetaAnalyticsDataClient::class, function (MockInterface $mock) use ($responseMock) { 325 | $mock->shouldReceive('runReport') 326 | ->with(Mockery::on(function (array $reportRequest) { 327 | /** @var array{property: string, dateRanges: DateRange[], dimensions: Dimension[], metrics: Metric[], returnPropertyQuota: bool, metricAggregations: int[]} $reportRequest */ 328 | $this->assertEquals('properties/test123', $reportRequest['property']); 329 | 330 | $this->assertCount(1, $reportRequest['dateRanges']); 331 | $this->assertEquals('2022-10-03', $reportRequest['dateRanges'][0]->getStartDate()); 332 | $this->assertEquals('2022-10-09', $reportRequest['dateRanges'][0]->getEndDate()); 333 | 334 | $this->assertCount(2, $reportRequest['metrics']); 335 | $this->assertEquals('sessions', $reportRequest['metrics'][0]->getName()); 336 | $this->assertEquals('bounceRate', $reportRequest['metrics'][1]->getName()); 337 | 338 | $this->assertCount(1, $reportRequest['dimensions']); 339 | $this->assertEquals('browser', $reportRequest['dimensions'][0]->getName()); 340 | 341 | $this->assertTrue($reportRequest['returnPropertyQuota']); 342 | 343 | $this->assertCount(1, $reportRequest['metricAggregations']); 344 | $this->assertEquals(MetricAggregation::TOTAL, $reportRequest['metricAggregations'][0]); 345 | 346 | return true; 347 | })) 348 | ->once() 349 | ->andReturn($responseMock); 350 | }); 351 | 352 | $response = Analytics::query() 353 | ->setMetrics(fn (Metrics $metrics) => $metrics 354 | ->sessions() 355 | ->bounceRate() 356 | ) 357 | ->setDimensions(fn (Dimensions $dimensions) => $dimensions 358 | ->browser() 359 | ) 360 | ->forPeriod(Period::lastWeek()) 361 | ->withTotals() 362 | ->run(); 363 | 364 | $this->assertInstanceOf(ResponseData::class, $response); 365 | 366 | $response->dimensionHeaders?->each(function (DimensionHeader $dimensionHeader) { 367 | $this->assertInstanceOf(DimensionHeader::class, $dimensionHeader); 368 | $this->assertEquals('browser', $dimensionHeader->name); 369 | }); 370 | 371 | $this->assertInstanceOf(MetricHeader::class, ($metricHeader = $response->metricHeaders->items()[0])); 372 | $this->assertEquals('sessions', $metricHeader->name); 373 | $this->assertEquals('TYPE_INTEGER', $metricHeader->type); 374 | 375 | $this->assertInstanceOf(MetricHeader::class, ($metricHeader = $response->metricHeaders->items()[1])); 376 | $this->assertEquals('bounceRate', $metricHeader->name); 377 | $this->assertEquals('TYPE_DOUBLE', $metricHeader->type); 378 | 379 | $this->assertEquals(2, $response->rowCount); 380 | $this->assertCount(2, $response->rows); 381 | 382 | $this->assertInstanceOf(Row::class, ($row = $response->rows->items()[0])); 383 | $this->assertEquals(1, $row->dimensionValues?->count()); 384 | $this->assertEquals('Browser1', $row->dimensionValues?->items()[0]->value); 385 | $this->assertCount(2, $row->metricValues); 386 | $this->assertEquals(123, $row->metricValues->items()[0]->value); 387 | $this->assertEquals(0.123, $row->metricValues->items()[1]->value); 388 | 389 | $this->assertInstanceOf(Row::class, ($row = $response->rows->items()[1])); 390 | $this->assertEquals(1, $row->dimensionValues?->count()); 391 | $this->assertEquals('Browser2', $row->dimensionValues?->items()[0]->value); 392 | $this->assertCount(2, $row->metricValues); 393 | $this->assertEquals(456, $row->metricValues->items()[0]->value); 394 | $this->assertEquals(0.456, $row->metricValues->items()[1]->value); 395 | 396 | $this->assertEquals(1, $response->totals?->count()); 397 | $this->assertInstanceOf(Total::class, ($totalRow = $response->totals?->items()[0])); 398 | $this->assertEquals(1, $totalRow->dimensionValues->count()); 399 | $this->assertEquals('RESERVED_TOTAL', $totalRow->dimensionValues->items()[0]->value); 400 | $this->assertCount(2, $totalRow->metricValues); 401 | $this->assertEquals(579, $totalRow->metricValues->items()[0]->value); 402 | $this->assertEquals(0.579, $totalRow->metricValues->items()[1]->value); 403 | 404 | $this->assertInstanceOf(PropertyQuota::class, $response->propertyQuota); 405 | $this->assertEquals(9, $response->propertyQuota->tokensPerDay->consumed); 406 | $this->assertEquals(24821, $response->propertyQuota->tokensPerDay->remaining); 407 | $this->assertEquals(9, $response->propertyQuota->tokensPerHour->consumed); 408 | $this->assertEquals(4981, $response->propertyQuota->tokensPerHour->remaining); 409 | $this->assertEquals(10, $response->propertyQuota->concurrentRequests->remaining); 410 | $this->assertEquals(10, $response->propertyQuota->serverErrorsPerProjectPerHour->remaining); 411 | $this->assertEquals(120, $response->propertyQuota->potentiallyThresholdedRequestsPerHour->remaining); 412 | $this->assertEquals(9, $response->propertyQuota->tokensPerProjectPerHour->consumed); 413 | $this->assertEquals(1231, $response->propertyQuota->tokensPerProjectPerHour->remaining); 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /tests/CredentialsTest.php: -------------------------------------------------------------------------------- 1 | disk = Storage::fake('testing-storage'); 21 | } 22 | 23 | /** 24 | * @param array $credentials 25 | */ 26 | private function setupCredentialsFile(array $credentials = [], string $fileName = 'test-credentials'): string 27 | { 28 | $encodedCredentials = json_encode($credentials + $this->credentials()); 29 | 30 | if (! $encodedCredentials) { 31 | $this->fail('Failed to encode credentials'); 32 | } 33 | 34 | $this->disk->put("$fileName.json", $encodedCredentials); 35 | 36 | return $this->disk->path("$fileName.json"); 37 | } 38 | 39 | /** 40 | * @return array|null 41 | * 42 | * @throws InvalidCredentialsJsonStringException 43 | * @throws InvalidCredentialsFileException 44 | * @throws InvalidCredentialsArrayException 45 | */ 46 | private function getApplicationCredentials(): ?array 47 | { 48 | return resolve(Credentials::class)->parse(); 49 | } 50 | 51 | /** 52 | * @throws InvalidCredentialsFileException 53 | * @throws InvalidCredentialsJsonStringException 54 | * @throws InvalidCredentialsArrayException 55 | */ 56 | public function test_client_init_with_default_credentials_env(): void 57 | { 58 | $credentialsFile = $this->setupCredentialsFile(); 59 | 60 | putenv('GOOGLE_APPLICATION_CREDENTIALS='.$credentialsFile); 61 | 62 | $this->assertNull($this->getApplicationCredentials()); 63 | 64 | putenv('GOOGLE_APPLICATION_CREDENTIALS='); 65 | } 66 | 67 | /** 68 | * @throws InvalidCredentialsJsonStringException 69 | * @throws InvalidCredentialsFileException 70 | * @throws InvalidCredentialsArrayException 71 | */ 72 | public function test_client_init_with_credentials_file(): void 73 | { 74 | $credentials = ['project_id' => 'testing-credentials-file'] + $this->credentials(); 75 | $credentialsFile = $this->setupCredentialsFile(['project_id' => 'testing-credentials-file']); 76 | 77 | config()->set('analytics.credentials.file', $credentialsFile); 78 | 79 | $this->assertSame($credentials, $this->getApplicationCredentials()); 80 | 81 | config()->offsetUnset('analytics.credentials.file'); 82 | } 83 | 84 | /** 85 | * @throws InvalidCredentialsJsonStringException 86 | * @throws InvalidCredentialsFileException 87 | * @throws InvalidCredentialsArrayException 88 | */ 89 | public function test_client_init_with_credentials_file_while_default_google_application_credentials_exist(): void 90 | { 91 | $defaultCredentialsFile = $this->setupCredentialsFile(['project_id' => 'do_not_use'], 'default-credentials'); 92 | putenv('GOOGLE_APPLICATION_CREDENTIALS='.$defaultCredentialsFile); 93 | 94 | config()->set('analytics.credentials.use_env', false); 95 | 96 | $credentials = ['project_id' => 'testing-credentials-file-with-default'] + $this->credentials(); 97 | $credentialsFile = $this->setupCredentialsFile(['project_id' => 'testing-credentials-file-with-default']); 98 | 99 | config()->set('analytics.credentials.file', $credentialsFile); 100 | 101 | $this->assertSame($credentials, $this->getApplicationCredentials()); 102 | 103 | config()->offsetUnset('analytics.credentials.file'); 104 | putenv('GOOGLE_APPLICATION_CREDENTIALS='); 105 | } 106 | 107 | /** 108 | * @throws InvalidCredentialsJsonStringException 109 | * @throws InvalidCredentialsFileException 110 | * @throws InvalidCredentialsArrayException 111 | */ 112 | public function test_client_init_with_credentials_json_string(): void 113 | { 114 | $credentials = ['project_id' => 'testing-credentials-json'] + $this->credentials(); 115 | 116 | config()->set('analytics.credentials.json', json_encode($credentials)); 117 | 118 | $this->assertSame($credentials, $this->getApplicationCredentials()); 119 | 120 | config()->offsetUnset('analytics.credentials.json'); 121 | } 122 | 123 | /** 124 | * @throws InvalidCredentialsFileException 125 | * @throws InvalidCredentialsJsonStringException 126 | * @throws InvalidCredentialsArrayException 127 | */ 128 | public function test_client_init_with_credentials_array(): void 129 | { 130 | $credentials = ['project_id' => 'testing-credentials-array'] + $this->credentials(); 131 | 132 | config()->set('analytics.credentials.array', $credentials); 133 | 134 | $this->assertSame($credentials, $this->getApplicationCredentials()); 135 | 136 | config()->offsetUnset('analytics.credentials.array'); 137 | } 138 | 139 | /** 140 | * @throws InvalidCredentialsFileException 141 | * @throws InvalidCredentialsJsonStringException 142 | * @throws InvalidCredentialsArrayException 143 | */ 144 | public function test_client_init_with_separate_credential_values(): void 145 | { 146 | config()->offsetUnset('analytics.credentials.array'); 147 | 148 | $credentials = ['project_id' => 'testing-credentials-separate-values'] + $this->credentials(); 149 | 150 | foreach ($credentials as $key => $value) { 151 | config()->set('analytics.credentials.array.'.$key, $value); 152 | } 153 | 154 | $this->assertSame($credentials, $this->getApplicationCredentials()); 155 | 156 | config()->offsetUnset('analytics.credentials.array'); 157 | } 158 | 159 | /** 160 | * @throws InvalidCredentialsJsonStringException 161 | * @throws InvalidCredentialsArrayException 162 | */ 163 | public function test_invalid_credentials_file_path_exception(): void 164 | { 165 | config()->offsetUnset('analytics.credentials.array'); 166 | config()->set('analytics.credentials.file', ''); 167 | 168 | $this->expectException(InvalidCredentialsFileException::class); 169 | $this->expectExceptionMessage(InvalidCredentialsFileException::MESSAGE_INVALID_PATH); 170 | 171 | $this->getApplicationCredentials(); 172 | 173 | config()->offsetUnset('analytics.credentials.file'); 174 | } 175 | 176 | /** 177 | * @throws InvalidCredentialsJsonStringException 178 | * @throws InvalidCredentialsArrayException 179 | */ 180 | public function test_credentials_file_does_not_exist_exception(): void 181 | { 182 | config()->offsetUnset('analytics.credentials.array'); 183 | config()->set('analytics.credentials.file', 'invalid-file.json'); 184 | 185 | $this->expectException(InvalidCredentialsFileException::class); 186 | $this->expectExceptionMessage(InvalidCredentialsFileException::MESSAGE_NOT_FOUND); 187 | 188 | $this->getApplicationCredentials(); 189 | 190 | config()->offsetUnset('analytics.credentials.file'); 191 | } 192 | 193 | /** 194 | * @throws InvalidCredentialsJsonStringException 195 | * @throws InvalidCredentialsArrayException 196 | */ 197 | public function test_credentials_file_is_not_a_valid_json_exception(): void 198 | { 199 | config()->offsetUnset('analytics.credentials.array'); 200 | 201 | $this->disk->put('invalid-json.json', 'invalid-json'); 202 | $credentialsFile = $this->disk->path('invalid-json.json'); 203 | 204 | config()->set('analytics.credentials.file', $credentialsFile); 205 | 206 | $this->expectException(InvalidCredentialsFileException::class); 207 | $this->expectExceptionMessage(InvalidCredentialsFileException::MESSAGE_INVALID_JSON); 208 | 209 | $this->getApplicationCredentials(); 210 | 211 | config()->offsetUnset('analytics.credentials.file'); 212 | } 213 | 214 | /** 215 | * @throws InvalidCredentialsFileException 216 | * @throws InvalidCredentialsArrayException 217 | */ 218 | public function test_invalid_credentials_json_string_exception(): void 219 | { 220 | config()->offsetUnset('analytics.credentials.array'); 221 | config()->set('analytics.credentials.json', ''); 222 | 223 | $this->expectException(InvalidCredentialsJsonStringException::class); 224 | $this->expectExceptionMessage(InvalidCredentialsJsonStringException::MESSAGE_INVALID_STRING); 225 | 226 | $this->getApplicationCredentials(); 227 | 228 | config()->offsetUnset('analytics.credentials.json'); 229 | } 230 | 231 | /** 232 | * @throws InvalidCredentialsFileException 233 | * @throws InvalidCredentialsArrayException 234 | */ 235 | public function test_credentials_json_string_is_not_a_valid_json_exception(): void 236 | { 237 | config()->offsetUnset('analytics.credentials.array'); 238 | config()->set('analytics.credentials.json', 'invalid-json'); 239 | 240 | $this->expectException(InvalidCredentialsJsonStringException::class); 241 | $this->expectExceptionMessage(InvalidCredentialsJsonStringException::MESSAGE_INVALID_JSON); 242 | 243 | $this->getApplicationCredentials(); 244 | 245 | config()->offsetUnset('analytics.credentials.json'); 246 | } 247 | 248 | /** 249 | * @throws InvalidCredentialsFileException 250 | * @throws InvalidCredentialsJsonStringException 251 | */ 252 | public function test_invalid_credentials_array_exception(): void 253 | { 254 | config()->set('analytics.credentials.array', ''); 255 | 256 | $this->expectException(InvalidCredentialsArrayException::class); 257 | $this->expectExceptionMessage(InvalidCredentialsArrayException::MESSAGE_INVALID_ARRAY); 258 | 259 | $this->getApplicationCredentials(); 260 | 261 | config()->offsetUnset('analytics.credentials.array'); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /tests/Helpers/CustomDimensions.php: -------------------------------------------------------------------------------- 1 | dimensions->push(new Dimension(['name' => 'customDimension'])); 13 | 14 | return $this; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Helpers/CustomMetrics.php: -------------------------------------------------------------------------------- 1 | metrics->push(new Metric(['name' => 'customMetric'])); 13 | 14 | return $this; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/MetricsTest.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'method' => fn (Metrics $metrics) => $metrics->active1DayUsers(), 17 | 'metric' => 'active1DayUsers', 18 | ]; 19 | 20 | yield 'active28DayUsers' => [ 21 | 'method' => fn (Metrics $metrics) => $metrics->active28DayUsers(), 22 | 'metric' => 'active28DayUsers', 23 | ]; 24 | 25 | yield 'active7DayUsers' => [ 26 | 'method' => fn (Metrics $metrics) => $metrics->active7DayUsers(), 27 | 'metric' => 'active7DayUsers', 28 | ]; 29 | 30 | yield 'activeUsers' => [ 31 | 'method' => fn (Metrics $metrics) => $metrics->activeUsers(), 32 | 'metric' => 'activeUsers', 33 | ]; 34 | 35 | yield 'adUnitExposure' => [ 36 | 'method' => fn (Metrics $metrics) => $metrics->adUnitExposure(), 37 | 'metric' => 'adUnitExposure', 38 | ]; 39 | 40 | yield 'addToCarts' => [ 41 | 'method' => fn (Metrics $metrics) => $metrics->addToCarts(), 42 | 'metric' => 'addToCarts', 43 | ]; 44 | 45 | yield 'advertiserAdClicks' => [ 46 | 'method' => fn (Metrics $metrics) => $metrics->advertiserAdClicks(), 47 | 'metric' => 'advertiserAdClicks', 48 | ]; 49 | 50 | yield 'advertiserAdCost' => [ 51 | 'method' => fn (Metrics $metrics) => $metrics->advertiserAdCost(), 52 | 'metric' => 'advertiserAdCost', 53 | ]; 54 | 55 | yield 'advertiserAdCostPerClick' => [ 56 | 'method' => fn (Metrics $metrics) => $metrics->advertiserAdCostPerClick(), 57 | 'metric' => 'advertiserAdCostPerClick', 58 | ]; 59 | 60 | yield 'advertiserAdCostPerConversion' => [ 61 | 'method' => fn (Metrics $metrics) => $metrics->advertiserAdCostPerConversion(), 62 | 'metric' => 'advertiserAdCostPerConversion', 63 | ]; 64 | 65 | yield 'advertiserAdImpressions' => [ 66 | 'method' => fn (Metrics $metrics) => $metrics->advertiserAdImpressions(), 67 | 'metric' => 'advertiserAdImpressions', 68 | ]; 69 | 70 | yield 'averagePurchaseRevenue' => [ 71 | 'method' => fn (Metrics $metrics) => $metrics->averagePurchaseRevenue(), 72 | 'metric' => 'averagePurchaseRevenue', 73 | ]; 74 | 75 | yield 'averagePurchaseRevenuePerPayingUser' => [ 76 | 'method' => fn (Metrics $metrics) => $metrics->averagePurchaseRevenuePerPayingUser(), 77 | 'metric' => 'averagePurchaseRevenuePerPayingUser', 78 | ]; 79 | 80 | yield 'averagePurchaseRevenuePerUser' => [ 81 | 'method' => fn (Metrics $metrics) => $metrics->averagePurchaseRevenuePerUser(), 82 | 'metric' => 'averagePurchaseRevenuePerUser', 83 | ]; 84 | 85 | yield 'averageRevenuePerUser' => [ 86 | 'method' => fn (Metrics $metrics) => $metrics->averageRevenuePerUser(), 87 | 'metric' => 'averageRevenuePerUser', 88 | ]; 89 | 90 | yield 'averageSessionDuration' => [ 91 | 'method' => fn (Metrics $metrics) => $metrics->averageSessionDuration(), 92 | 'metric' => 'averageSessionDuration', 93 | ]; 94 | 95 | yield 'bounceRate' => [ 96 | 'method' => fn (Metrics $metrics) => $metrics->bounceRate(), 97 | 'metric' => 'bounceRate', 98 | ]; 99 | 100 | yield 'cartToViewRate' => [ 101 | 'method' => fn (Metrics $metrics) => $metrics->cartToViewRate(), 102 | 'metric' => 'cartToViewRate', 103 | ]; 104 | 105 | yield 'checkouts' => [ 106 | 'method' => fn (Metrics $metrics) => $metrics->checkouts(), 107 | 'metric' => 'checkouts', 108 | ]; 109 | 110 | yield 'cohortActiveUsers' => [ 111 | 'method' => fn (Metrics $metrics) => $metrics->cohortActiveUsers(), 112 | 'metric' => 'cohortActiveUsers', 113 | ]; 114 | 115 | yield 'cohortTotalUsers' => [ 116 | 'method' => fn (Metrics $metrics) => $metrics->cohortTotalUsers(), 117 | 'metric' => 'cohortTotalUsers', 118 | ]; 119 | 120 | yield 'conversions' => [ 121 | 'method' => fn (Metrics $metrics) => $metrics->conversions(), 122 | 'metric' => 'conversions', 123 | ]; 124 | 125 | yield 'crashAffectedUsers' => [ 126 | 'method' => fn (Metrics $metrics) => $metrics->crashAffectedUsers(), 127 | 'metric' => 'crashAffectedUsers', 128 | ]; 129 | 130 | yield 'crashFreeUsersRate' => [ 131 | 'method' => fn (Metrics $metrics) => $metrics->crashFreeUsersRate(), 132 | 'metric' => 'crashFreeUsersRate', 133 | ]; 134 | 135 | yield 'dauPerMau' => [ 136 | 'method' => fn (Metrics $metrics) => $metrics->dauPerMau(), 137 | 'metric' => 'dauPerMau', 138 | ]; 139 | 140 | yield 'dauPerWau' => [ 141 | 'method' => fn (Metrics $metrics) => $metrics->dauPerWau(), 142 | 'metric' => 'dauPerWau', 143 | ]; 144 | 145 | yield 'ecommercePurchases' => [ 146 | 'method' => fn (Metrics $metrics) => $metrics->ecommercePurchases(), 147 | 'metric' => 'ecommercePurchases', 148 | ]; 149 | 150 | yield 'engagedSessions' => [ 151 | 'method' => fn (Metrics $metrics) => $metrics->engagedSessions(), 152 | 'metric' => 'engagedSessions', 153 | ]; 154 | 155 | yield 'engagementRate' => [ 156 | 'method' => fn (Metrics $metrics) => $metrics->engagementRate(), 157 | 'metric' => 'engagementRate', 158 | ]; 159 | 160 | yield 'eventCount' => [ 161 | 'method' => fn (Metrics $metrics) => $metrics->eventCount(), 162 | 'metric' => 'eventCount', 163 | ]; 164 | 165 | yield 'eventCountPerUser' => [ 166 | 'method' => fn (Metrics $metrics) => $metrics->eventCountPerUser(), 167 | 'metric' => 'eventCountPerUser', 168 | ]; 169 | 170 | yield 'eventValue' => [ 171 | 'method' => fn (Metrics $metrics) => $metrics->eventValue(), 172 | 'metric' => 'eventValue', 173 | ]; 174 | 175 | yield 'eventsPerSession' => [ 176 | 'method' => fn (Metrics $metrics) => $metrics->eventsPerSession(), 177 | 'metric' => 'eventsPerSession', 178 | ]; 179 | 180 | yield 'firstTimePurchaserConversionRate' => [ 181 | 'method' => fn (Metrics $metrics) => $metrics->firstTimePurchaserConversionRate(), 182 | 'metric' => 'firstTimePurchaserConversionRate', 183 | ]; 184 | 185 | yield 'firstTimePurchasers' => [ 186 | 'method' => fn (Metrics $metrics) => $metrics->firstTimePurchasers(), 187 | 'metric' => 'firstTimePurchasers', 188 | ]; 189 | 190 | yield 'firstTimePurchasersPerNewUser' => [ 191 | 'method' => fn (Metrics $metrics) => $metrics->firstTimePurchasersPerNewUser(), 192 | 'metric' => 'firstTimePurchasersPerNewUser', 193 | ]; 194 | 195 | yield 'itemListClickEvents' => [ 196 | 'method' => fn (Metrics $metrics) => $metrics->itemListClickEvents(), 197 | 'metric' => 'itemListClickEvents', 198 | ]; 199 | 200 | yield 'itemListClickThroughRate' => [ 201 | 'method' => fn (Metrics $metrics) => $metrics->itemListClickThroughRate(), 202 | 'metric' => 'itemListClickThroughRate', 203 | ]; 204 | 205 | yield 'itemListViewEvents' => [ 206 | 'method' => fn (Metrics $metrics) => $metrics->itemListViewEvents(), 207 | 'metric' => 'itemListViewEvents', 208 | ]; 209 | 210 | yield 'itemPromotionClickThroughRate' => [ 211 | 'method' => fn (Metrics $metrics) => $metrics->itemPromotionClickThroughRate(), 212 | 'metric' => 'itemPromotionClickThroughRate', 213 | ]; 214 | 215 | yield 'itemRevenue' => [ 216 | 'method' => fn (Metrics $metrics) => $metrics->itemRevenue(), 217 | 'metric' => 'itemRevenue', 218 | ]; 219 | 220 | yield 'itemViewEvents' => [ 221 | 'method' => fn (Metrics $metrics) => $metrics->itemViewEvents(), 222 | 'metric' => 'itemViewEvents', 223 | ]; 224 | 225 | yield 'itemsAddedToCart' => [ 226 | 'method' => fn (Metrics $metrics) => $metrics->itemsAddedToCart(), 227 | 'metric' => 'itemsAddedToCart', 228 | ]; 229 | 230 | yield 'itemsCheckedOut' => [ 231 | 'method' => fn (Metrics $metrics) => $metrics->itemsCheckedOut(), 232 | 'metric' => 'itemsCheckedOut', 233 | ]; 234 | 235 | yield 'itemsClickedInList' => [ 236 | 'method' => fn (Metrics $metrics) => $metrics->itemsClickedInList(), 237 | 'metric' => 'itemsClickedInList', 238 | ]; 239 | 240 | yield 'itemsClickedInPromotion' => [ 241 | 'method' => fn (Metrics $metrics) => $metrics->itemsClickedInPromotion(), 242 | 'metric' => 'itemsClickedInPromotion', 243 | ]; 244 | 245 | yield 'itemsPurchased' => [ 246 | 'method' => fn (Metrics $metrics) => $metrics->itemsPurchased(), 247 | 'metric' => 'itemsPurchased', 248 | ]; 249 | 250 | yield 'itemsViewed' => [ 251 | 'method' => fn (Metrics $metrics) => $metrics->itemsViewed(), 252 | 'metric' => 'itemsViewed', 253 | ]; 254 | 255 | yield 'itemsViewedInList' => [ 256 | 'method' => fn (Metrics $metrics) => $metrics->itemsViewedInList(), 257 | 'metric' => 'itemsViewedInList', 258 | ]; 259 | 260 | yield 'itemsViewedInPromotion' => [ 261 | 'method' => fn (Metrics $metrics) => $metrics->itemsViewedInPromotion(), 262 | 'metric' => 'itemsViewedInPromotion', 263 | ]; 264 | 265 | yield 'newUsers' => [ 266 | 'method' => fn (Metrics $metrics) => $metrics->newUsers(), 267 | 'metric' => 'newUsers', 268 | ]; 269 | 270 | yield 'organicGoogleSearchAveragePosition' => [ 271 | 'method' => fn (Metrics $metrics) => $metrics->organicGoogleSearchAveragePosition(), 272 | 'metric' => 'organicGoogleSearchAveragePosition', 273 | ]; 274 | 275 | yield 'organicGoogleSearchClickThroughRate' => [ 276 | 'method' => fn (Metrics $metrics) => $metrics->organicGoogleSearchClickThroughRate(), 277 | 'metric' => 'organicGoogleSearchClickThroughRate', 278 | ]; 279 | 280 | yield 'organicGoogleSearchClicks' => [ 281 | 'method' => fn (Metrics $metrics) => $metrics->organicGoogleSearchClicks(), 282 | 'metric' => 'organicGoogleSearchClicks', 283 | ]; 284 | 285 | yield 'organicGoogleSearchImpressions' => [ 286 | 'method' => fn (Metrics $metrics) => $metrics->organicGoogleSearchImpressions(), 287 | 'metric' => 'organicGoogleSearchImpressions', 288 | ]; 289 | 290 | yield 'promotionClicks' => [ 291 | 'method' => fn (Metrics $metrics) => $metrics->promotionClicks(), 292 | 'metric' => 'promotionClicks', 293 | ]; 294 | 295 | yield 'promotionViews' => [ 296 | 'method' => fn (Metrics $metrics) => $metrics->promotionViews(), 297 | 'metric' => 'promotionViews', 298 | ]; 299 | 300 | yield 'publisherAdClicks' => [ 301 | 'method' => fn (Metrics $metrics) => $metrics->publisherAdClicks(), 302 | 'metric' => 'publisherAdClicks', 303 | ]; 304 | 305 | yield 'publisherAdImpressions' => [ 306 | 'method' => fn (Metrics $metrics) => $metrics->publisherAdImpressions(), 307 | 'metric' => 'publisherAdImpressions', 308 | ]; 309 | 310 | yield 'purchaseRevenue' => [ 311 | 'method' => fn (Metrics $metrics) => $metrics->purchaseRevenue(), 312 | 'metric' => 'purchaseRevenue', 313 | ]; 314 | 315 | yield 'purchaseToViewRate' => [ 316 | 'method' => fn (Metrics $metrics) => $metrics->purchaseToViewRate(), 317 | 'metric' => 'purchaseToViewRate', 318 | ]; 319 | 320 | yield 'purchaserConversionRate' => [ 321 | 'method' => fn (Metrics $metrics) => $metrics->purchaserConversionRate(), 322 | 'metric' => 'purchaserConversionRate', 323 | ]; 324 | 325 | yield 'returnOnAdSpend' => [ 326 | 'method' => fn (Metrics $metrics) => $metrics->returnOnAdSpend(), 327 | 'metric' => 'returnOnAdSpend', 328 | ]; 329 | 330 | yield 'screenPageViews' => [ 331 | 'method' => fn (Metrics $metrics) => $metrics->screenPageViews(), 332 | 'metric' => 'screenPageViews', 333 | ]; 334 | 335 | yield 'screenPageViewsPerSession' => [ 336 | 'method' => fn (Metrics $metrics) => $metrics->screenPageViewsPerSession(), 337 | 'metric' => 'screenPageViewsPerSession', 338 | ]; 339 | 340 | yield 'sessionConversionRate' => [ 341 | 'method' => fn (Metrics $metrics) => $metrics->sessionConversionRate(), 342 | 'metric' => 'sessionConversionRate', 343 | ]; 344 | 345 | yield 'sessions' => [ 346 | 'method' => fn (Metrics $metrics) => $metrics->sessions(), 347 | 'metric' => 'sessions', 348 | ]; 349 | 350 | yield 'sessionsPerUser' => [ 351 | 'method' => fn (Metrics $metrics) => $metrics->sessionsPerUser(), 352 | 'metric' => 'sessionsPerUser', 353 | ]; 354 | 355 | yield 'shippingAmount' => [ 356 | 'method' => fn (Metrics $metrics) => $metrics->shippingAmount(), 357 | 'metric' => 'shippingAmount', 358 | ]; 359 | 360 | yield 'taxAmount' => [ 361 | 'method' => fn (Metrics $metrics) => $metrics->taxAmount(), 362 | 'metric' => 'taxAmount', 363 | ]; 364 | 365 | yield 'totalAdRevenue' => [ 366 | 'method' => fn (Metrics $metrics) => $metrics->totalAdRevenue(), 367 | 'metric' => 'totalAdRevenue', 368 | ]; 369 | 370 | yield 'totalPurchasers' => [ 371 | 'method' => fn (Metrics $metrics) => $metrics->totalPurchasers(), 372 | 'metric' => 'totalPurchasers', 373 | ]; 374 | 375 | yield 'totalRevenue' => [ 376 | 'method' => fn (Metrics $metrics) => $metrics->totalRevenue(), 377 | 'metric' => 'totalRevenue', 378 | ]; 379 | 380 | yield 'totalUsers' => [ 381 | 'method' => fn (Metrics $metrics) => $metrics->totalUsers(), 382 | 'metric' => 'totalUsers', 383 | ]; 384 | 385 | yield 'transactions' => [ 386 | 'method' => fn (Metrics $metrics) => $metrics->transactions(), 387 | 'metric' => 'transactions', 388 | ]; 389 | 390 | yield 'transactionsPerPurchaser' => [ 391 | 'method' => fn (Metrics $metrics) => $metrics->transactionsPerPurchaser(), 392 | 'metric' => 'transactionsPerPurchaser', 393 | ]; 394 | 395 | yield 'userConversionRate' => [ 396 | 'method' => fn (Metrics $metrics) => $metrics->userConversionRate(), 397 | 'metric' => 'userConversionRate', 398 | ]; 399 | 400 | yield 'userEngagementDuration' => [ 401 | 'method' => fn (Metrics $metrics) => $metrics->userEngagementDuration(), 402 | 'metric' => 'userEngagementDuration', 403 | ]; 404 | 405 | yield 'wauPerMau' => [ 406 | 'method' => fn (Metrics $metrics) => $metrics->wauPerMau(), 407 | 'metric' => 'wauPerMau', 408 | ]; 409 | } 410 | 411 | /** 412 | * @param Closure(Metrics): Metrics $method 413 | * 414 | * @dataProvider metricProvider 415 | */ 416 | public function test_predefined_metrics(Closure $method, string $metric): void 417 | { 418 | $metrics = new Metrics; 419 | $metrics = $method($metrics); 420 | 421 | $this->assertEquals(1, $metrics->count()); 422 | $this->assertInstanceOf(Collection::class, $metrics->getMetrics()); 423 | $this->assertInstanceOf(Metric::class, $metrics->getMetrics()->first()); 424 | $this->assertEquals($metric, $metrics->getMetrics()->first()->getName()); 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /tests/ReportTest.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'fakeResponse' => [ 24 | 'dimensionHeaders' => [ 25 | [ 26 | 'name' => 'eventName', 27 | ], 28 | ], 29 | 'metricHeaders' => [ 30 | [ 31 | 'name' => 'eventCount', 32 | 'type' => 'TYPE_INTEGER', 33 | ], 34 | ], 35 | 'rows' => [ 36 | [ 37 | 'dimensionValues' => [ 38 | [ 39 | 'value' => 'testEvent1', 40 | ], 41 | ], 42 | 'metricValues' => [ 43 | [ 44 | 'value' => '222', 45 | ], 46 | ], 47 | ], 48 | [ 49 | 'dimensionValues' => [ 50 | [ 51 | 'value' => 'testEvent2', 52 | ], 53 | ], 54 | 'metricValues' => [ 55 | [ 56 | 'value' => '111', 57 | ], 58 | ], 59 | ], 60 | ], 61 | 'totals' => [ 62 | [ 63 | 'dimensionValues' => [ 64 | [ 65 | 'value' => 'RESERVED_TOTAL', 66 | ], 67 | ], 68 | 'metricValues' => [ 69 | [ 70 | 'value' => '333', 71 | ], 72 | ], 73 | ], 74 | ], 75 | 'rowCount' => 2, 76 | 'metadata' => [ 77 | 'currencyCode' => 'USD', 78 | 'timeZone' => 'UTC', 79 | ], 80 | 'kind' => 'analyticsData#runReport', 81 | ], 82 | 'assertRequest' => function (array $reportRequest) { 83 | /** @var array{property: string, dateRanges: DateRange[], dimensions: Dimension[], metrics: Metric[]} $reportRequest */ 84 | self::assertEquals('properties/'.'test123', $reportRequest['property']); 85 | 86 | self::assertCount(1, $reportRequest['dateRanges']); 87 | self::assertEquals('2022-10-22', $reportRequest['dateRanges'][0]->getStartDate()); 88 | self::assertEquals('2022-11-21', $reportRequest['dateRanges'][0]->getEndDate()); 89 | 90 | self::assertCount(1, $reportRequest['dimensions']); 91 | self::assertEquals('eventName', $reportRequest['dimensions'][0]->getName()); 92 | 93 | self::assertCount(1, $reportRequest['metrics']); 94 | self::assertEquals('eventCount', $reportRequest['metrics'][0]->getName()); 95 | 96 | return true; 97 | }, 98 | 'reportCall' => fn () => Analytics::getTopEvents(), 99 | ]; 100 | 101 | yield 'getUserAcquisitionOverview' => [ 102 | 'fakeResponse' => [ 103 | 'dimensionHeaders' => [ 104 | [ 105 | 'name' => 'firstUserDefaultChannelGroup', 106 | ], 107 | ], 108 | 'metricHeaders' => [ 109 | [ 110 | 'name' => 'sessions', 111 | 'type' => 'TYPE_INTEGER', 112 | ], 113 | ], 114 | 'rows' => [ 115 | [ 116 | 'dimensionValues' => [ 117 | [ 118 | 'value' => 'Direct', 119 | ], 120 | ], 121 | 'metricValues' => [ 122 | [ 123 | 'value' => '222', 124 | ], 125 | ], 126 | ], 127 | [ 128 | 'dimensionValues' => [ 129 | [ 130 | 'value' => 'Referral', 131 | ], 132 | ], 133 | 'metricValues' => [ 134 | [ 135 | 'value' => '111', 136 | ], 137 | ], 138 | ], 139 | [ 140 | 'dimensionValues' => [ 141 | [ 142 | 'value' => 'Organic Search', 143 | ], 144 | ], 145 | 'metricValues' => [ 146 | [ 147 | 'value' => '111', 148 | ], 149 | ], 150 | ], 151 | [ 152 | 'dimensionValues' => [ 153 | [ 154 | 'value' => 'Organic Social', 155 | ], 156 | ], 157 | 'metricValues' => [ 158 | [ 159 | 'value' => '111', 160 | ], 161 | ], 162 | ], 163 | ], 164 | 'rowCount' => 4, 165 | 'metadata' => [ 166 | 'currencyCode' => 'USD', 167 | 'timeZone' => 'UTC', 168 | ], 169 | 'kind' => 'analyticsData#runReport', 170 | ], 171 | 'assertRequest' => function (array $reportRequest) { 172 | /** @var array{property: string, dateRanges: DateRange[], dimensions: Dimension[], metrics: Metric[]} $reportRequest */ 173 | self::assertEquals('properties/'.'test123', $reportRequest['property']); 174 | 175 | self::assertCount(1, $reportRequest['dateRanges']); 176 | self::assertEquals('2022-10-22', $reportRequest['dateRanges'][0]->getStartDate()); 177 | self::assertEquals('2022-11-21', $reportRequest['dateRanges'][0]->getEndDate()); 178 | 179 | self::assertCount(1, $reportRequest['dimensions']); 180 | self::assertEquals('firstUserDefaultChannelGroup', $reportRequest['dimensions'][0]->getName()); 181 | 182 | self::assertCount(1, $reportRequest['metrics']); 183 | self::assertEquals('sessions', $reportRequest['metrics'][0]->getName()); 184 | 185 | return true; 186 | }, 187 | 'reportCall' => fn () => Analytics::getUserAcquisitionOverview(), 188 | ]; 189 | 190 | yield 'getTopPages' => [ 191 | 'fakeResponse' => [ 192 | 'dimensionHeaders' => [ 193 | [ 194 | 'name' => 'pageTitle', 195 | ], 196 | ], 197 | 'metricHeaders' => [ 198 | [ 199 | 'name' => 'sessions', 200 | 'type' => 'TYPE_INTEGER', 201 | ], 202 | ], 203 | 'rows' => [ 204 | [ 205 | 'dimensionValues' => [ 206 | [ 207 | 'value' => 'Page Title 1', 208 | ], 209 | ], 210 | 'metricValues' => [ 211 | [ 212 | 'value' => '222', 213 | ], 214 | ], 215 | ], 216 | [ 217 | 'dimensionValues' => [ 218 | [ 219 | 'value' => 'Page Title 2', 220 | ], 221 | ], 222 | 'metricValues' => [ 223 | [ 224 | 'value' => '111', 225 | ], 226 | ], 227 | ], 228 | [ 229 | 'dimensionValues' => [ 230 | [ 231 | 'value' => 'Page Title 3', 232 | ], 233 | ], 234 | 'metricValues' => [ 235 | [ 236 | 'value' => '111', 237 | ], 238 | ], 239 | ], 240 | ], 241 | 'rowCount' => 3, 242 | 'metadata' => [ 243 | 'currencyCode' => 'USD', 244 | 'timeZone' => 'UTC', 245 | ], 246 | 'kind' => 'analyticsData#runReport', 247 | ], 248 | 'assertRequest' => function (array $reportRequest) { 249 | /** @var array{property: string, dateRanges: DateRange[], dimensions: Dimension[], metrics: Metric[]} $reportRequest */ 250 | self::assertEquals('properties/'.'test123', $reportRequest['property']); 251 | 252 | self::assertCount(1, $reportRequest['dateRanges']); 253 | self::assertEquals('2022-10-22', $reportRequest['dateRanges'][0]->getStartDate()); 254 | self::assertEquals('2022-11-21', $reportRequest['dateRanges'][0]->getEndDate()); 255 | 256 | self::assertCount(1, $reportRequest['dimensions']); 257 | self::assertEquals('pageTitle', $reportRequest['dimensions'][0]->getName()); 258 | 259 | self::assertCount(1, $reportRequest['metrics']); 260 | self::assertEquals('sessions', $reportRequest['metrics'][0]->getName()); 261 | 262 | return true; 263 | }, 264 | 'reportCall' => fn () => Analytics::getTopPages(), 265 | ]; 266 | 267 | yield 'getUserEngagement' => [ 268 | 'fakeResponse' => [ 269 | 'metricHeaders' => [ 270 | [ 271 | 'name' => 'averageSessionDuration', 272 | 'type' => 'TYPE_SECONDS', 273 | ], 274 | [ 275 | 'name' => 'engagedSessions', 276 | 'type' => 'TYPE_INTEGER', 277 | ], 278 | [ 279 | 'name' => 'sessionsPerUser', 280 | 'type' => 'TYPE_FLOAT', 281 | ], 282 | [ 283 | 'name' => 'sessions', 284 | 'type' => 'TYPE_INTEGER', 285 | ], 286 | ], 287 | 'rows' => [ 288 | [ 289 | 'metricValues' => [ 290 | [ 291 | 'value' => '386.96577397089948', 292 | ], 293 | [ 294 | 'value' => '256', 295 | ], 296 | [ 297 | 'value' => '1.5555555555555556', 298 | ], 299 | [ 300 | 'value' => '378', 301 | ], 302 | ], 303 | ], 304 | ], 305 | 'rowCount' => 4, 306 | 'metadata' => [ 307 | 'currencyCode' => 'USD', 308 | 'timeZone' => 'UTC', 309 | ], 310 | 'kind' => 'analyticsData#runReport', 311 | ], 312 | 'assertRequest' => function (array $reportRequest) { 313 | /** @var array{property: string, dateRanges: DateRange[], dimensions: Dimension[], metrics: Metric[]} $reportRequest */ 314 | self::assertEquals('properties/'.'test123', $reportRequest['property']); 315 | 316 | self::assertCount(1, $reportRequest['dateRanges']); 317 | self::assertEquals('2022-10-22', $reportRequest['dateRanges'][0]->getStartDate()); 318 | self::assertEquals('2022-11-21', $reportRequest['dateRanges'][0]->getEndDate()); 319 | 320 | self::assertCount(4, $reportRequest['metrics']); 321 | self::assertEquals('averageSessionDuration', $reportRequest['metrics'][0]->getName()); 322 | self::assertEquals('engagedSessions', $reportRequest['metrics'][1]->getName()); 323 | self::assertEquals('sessionsPerUser', $reportRequest['metrics'][2]->getName()); 324 | self::assertEquals('sessions', $reportRequest['metrics'][3]->getName()); 325 | 326 | return true; 327 | }, 328 | 'reportCall' => fn () => Analytics::getUserEngagement(), 329 | ]; 330 | } 331 | 332 | /** 333 | * @dataProvider reportProvider 334 | * 335 | * @param array $fakeResponse 336 | */ 337 | public function test_reports(array $fakeResponse, Closure $assertRequest, Closure $reportCall): void 338 | { 339 | CarbonImmutable::setTestNow(CarbonImmutable::create(2022, 11, 21)); 340 | 341 | $responseMock = $this->mock(RunReportResponse::class, function (MockInterface $mock) use ($fakeResponse) { 342 | $mock->shouldReceive('serializeToJsonString') 343 | ->once() 344 | ->andReturn(json_encode($fakeResponse)); 345 | }); 346 | 347 | $this->mock(BetaAnalyticsDataClient::class, function (MockInterface $mock) use ($responseMock, $assertRequest) { 348 | $mock->shouldReceive('runReport') 349 | ->with(Mockery::on($assertRequest)) 350 | ->once() 351 | ->andReturn($responseMock); 352 | }); 353 | 354 | $this->assertInstanceOf(ResponseData::class, $reportCall()); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('analytics.property_id', 'test123'); 26 | config()->set('analytics.credentials.array', $this->credentials()); 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | protected function credentials(): array 33 | { 34 | return [ 35 | 'type' => 'service_account', 36 | 'project_id' => 'bogus-project', 37 | 'private_key_id' => 'bogus-id', 38 | 'private_key' => 'bogus-key', 39 | 'client_email' => 'bogus-user@bogus-app.iam.gserviceaccount.com', 40 | 'client_id' => 'bogus-id', 41 | 'auth_uri' => 'https://accounts.google.com/o/oauth2/auth', 42 | 'token_uri' => 'https://accounts.google.com/o/oauth2/token', 43 | 'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs', 44 | 'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/bogus-ser%40bogus-app.iam.gserviceaccount.com', 45 | ]; 46 | } 47 | } 48 | --------------------------------------------------------------------------------