AnalyticsAPI Module
This module is responsible for talking to Matomo Analytics. It contains both JS tracking code and an abstraction for retrieving analytics via the API.
This module also syncs analytics to the database locally so the data can be used with eloquent queries, adding support for sorting by analytics metrics and reducing load on Matomo.
Adding a new site to matomo
You should add a new site to Matomo for each app/environment combination. Eg Captivate staging and Captivate demo should both have their own site ID.
At time of writing we are using a shared matomo instance https://district.analytics.stack.host.
You can add a new site to this instance with pre-configured defaults via artisan analytics:new-site
You can list all existing sites with artisan analytics:site-list
Config
Ensure the following environment variables are defined
MATOMO_URL=https://url.to.matomo.instance
MATOMO_SITE_ID=site_id_in_matomo
MATOMO_API_TOKEN=matomo_api_key
Tracking
Tracking is done via JS. Firstly Resouces/js/AnalyticsInit.js is called inside createInertiaApp which adds _paq to window along with the correct site id and tracking dependencies. This should generally need to be modified.
Then Resouces/js/AnalyticsHelper.js is responsible for business logic surrounding tracking. Its primary method is pageView() which gets called on Inertia navigate (page load/change). The pageView adds Custom dimensions which can be used for filtering when retrieving results (via segments). It also adds generic tracking data such as current user, page title, etc.
Custom dimensions
Custom dimensions are key to filtering data in Matomo. It is similar to Custom Variables which is now deprecated (and not part of matomo core). This module uses custom dimensions for:
- Filtering by frontend / dashboard
- Filtering by a specific model
- Filtering by a specific model parent
- Filtering by tenant
- Filtering by the path
Managing custom dimensions
In Matomo
Custom dimensions will be created with correct ID's if you add a new Matomo site via the analytics:new-site artisan command. If doing it manually, the ID's must align with Config/matomo.php as described below.
In the app
All custom dimension keys should be defined in Enums\CustomDimensionKeys, each dimension should map to an id in Config/matomo.php (matomo.dimension_map). The ID is what is shown in
Matomo > Admin (cog) > Websites > Custom dimensions. It is critical that these all match. You can retrieve an ID via CustomDimensionKeys::getId(). Example:
$id = CustomDimensionKeys::getId(CustomDimensionKeys::CONTENT);
// $id equals config('matomo.dimension_map.content') or 4.
The custom dimension ids should also be replicated in AnalyticsHelper.customDimensionsIdMap. This duplication should be fixed in a future ticket DISTCORE-296.
Custom dimension patterns
Enums\CustomDimensionKeys
ROLEexpects to be a role id (visit scope)CONTENTexpects to be$model->type.':'.$model->id, an attribute is available inHasAnalyticsTraitthat exposes this via$model->analytics_key.CONTENT_PARENTsame asCUSTOM_DIMENSION_CONTENTbut for the parentable content.TENANTexpects to be$tenant->idPATHcurrent path (no domain) - calculated automatically by matomo.SECTIONeitherdashorfrontend
Custom dimension scopes
We mainly use page scope dimensions which is added to each page change tracking request and great for filtering. The alternative is visit scope which once set, remains in place for the duration of the visit, this is less useful for filtering.
Adding more custom dimensions
Matomo cloud provides 15 custom dimensions per scope, the self hosted version only has 5, but you can add more. Docs.
Self hosted via docker, docker exec bash into the container, then:
cd /opt/bitnami/matomo && php console customdimensions:add-custom-dimension --scope=action --count=5
Filtering data by custom dimensions
This is all nicely abstracted in AnalyticsService and you can just use the method addDimension(). See Enums\AnalyticsSegmentOperators for available operators such as equals, contains, starts with, etc.
Example, get Visits summary, exclude dash, date range 2023-01-01 to today, current tenant for model page and id 3:
$visits = AnalyticsService::new()
->setRange('2023-01-01', 'today')
->addDimension(
CustomDimensionKeys::getId(CustomDimensionKeys::SECTION),
AnalyticsService::SECTION_NAME_DASH,
'and',
AnalyticsSegmentOperators::NOT_EQUALS
)
->addDimension(CustomDimensionKeys::getId(CustomDimensionKeys::TENANT), $currentTenantId)
->addDimension(CustomDimensionKeys::getId(CustomDimensionKeys::CONTENT), 'page:3')
->getVisitsSummary();
NOTE: Helpers exist in AnalyticsService to abstract dimension adding. For example setCurrentTenant() and setExcludeDash().
Goals
Matomo Goals are used to log conversions. Goals are defined in Matomo > Admin (cog) > Websites > Goals. At time of writing there is only one goal for a survey submission.
In Matomo
Goals will be created with correct ID's if you add a new Matomo site via the analytics:new-site artisan command. If doing it manually, the ID's must align with Config/matomo.php as described below.
In the app
Goal ids are defined in AnalyticsHelper.goals. To trigger a goal, add the code trackGoal(goalName) to the JS callback that indicates a successful goal.
Example in the SurveyForm component:
this.form
.patch(this.survey.url, {
onFinish: () => {
AnalyticsHelper.trackGoal('surveySubmission')
},
})
Analytics Service
Services\AnalyticsService
This class is an abstraction on top of Matomo-PHP-API. You should always use this over the Matomo class directly as it deals with loading config, mocking and more.
As per the above example, you can use this to retrieve filtered results from the Matomo API. Filtering is done via Segments. Custom dimensions are just segments with specific keys.
The AnalyticsService class is well documented, read through the methods and doc blocks to see what it can do.
By default, all results are cached, this can be toggled with setCacheEnabled(true/false).
Mocking the API (testing)
To prevent access to the real API or just use dummy data, you can either:
- Instantiate AnalyticsService with
AnalyticsService::fake() - Toggle via
useMock(true/false) - Set via config
config(['matomo.fake' => true]) - Use environment variable
MATOMO_API_FAKE=true
This will swap out the Matomo-PHP-API class with MatomoMockResponse which returns the same structure just with fake data.
Syncing/Caching analytics to the database
Services\AnalyticsCacheServiceModels\AnalyticsModels\Concerns\HasAnalyticsInterfaceModels\Traits\HasAnalyticsTrait
Any model that implements HasAnalyticsInterface and uses HasAnalyticsTrait will automatically sync getVisitsSummary data. A row is added for each model and the date. This means you can do something like:
$myModelWithAnalytics = MyModelWithAnalyics::where('id', 1)
->with('analytics')
->whereHas('analytics', fn ($q) => $q->whereDate('date', '<=', today()->toDateString()))
->get();
With this sync/cache it means you can leverage eloquent relationships and easily filter, sort, sum, avg and much more.
Triggering a sync
This is run automatically via cron and the job queue. Run lando artisan analytics:cache-update-all-tenants to queue a refresh of all tenants. Then run lando artisan horizon (if not already running) to process the queue.
Sync queue and jobs
The queue used to sync analytics is analytics. Two jobs are involved, the first looks for all models with HasAnalyticsInterface then for each create a new job that syncs analytics since created_at date. Each DB entry contains a last_update date which ensures that past analytics do not get refreshed unnecessarily. Once a model has had an initial sync, subsequent jobs should only sync the current day.
The AnalyticsCacheService has $minsBetweenRefresh property which is the minimum time between a refresh (regardless of how often cron runs). This is set to 3 hours so a sync should not run more often than that.
Report widgets
A result of all of the above is we have a great collection of reporting widgets that can be used in reporting dashboards. Module specific widgets can also easily pull in analytics metrics to enrich standard data.