# Tracking Module The `Imagify\Tracking` module wires the `wp-media/wp-mixpanel` Composer package into Imagify's DI layer and fires analytics events to Mixpanel when media optimization completes. --- ## Architecture | Class | Role | |---|---| | `Imagify\Tracking\BaseTracking` | Abstract base: `can_track()`, `get_default_event_properties()`, `identify_user()` | | `Imagify\Tracking\Tracking` | Concrete: `track_media_optimized()`, `track_settings_saved()` | | `Imagify\Tracking\Subscriber` | Subscribes to WordPress hooks and delegates to `Tracking` | | `Imagify\Tracking\ServiceProvider` | Registers all module services into the DI container | The module depends on two Strauss-prefixed vendor classes: - `Imagify\Dependencies\WPMedia\Mixpanel\Optin` — manages opt-in state - `Imagify\Dependencies\WPMedia\Mixpanel\TrackingPlugin` — sends events to Mixpanel --- ## User identification (`distinct_id`) Mixpanel needs a `distinct_id` on every event to compute user-level metrics — MAU/DAU, retention, cohorts, funnels, unique users by feature. Without it, events are only aggregatable in bulk. `BaseTracking::identify_user()` registers it by calling `TrackingPlugin::identify()`, which hashes the identifier with **sha224** and registers it as a Mixpanel *super property* on the shared Mixpanel instance — so it lands on every event sent afterwards, including MCP events. No raw email or domain ever leaves the site. | Priority | Identifier | Rationale | |---|---|---| | 1 | `get_imagify_user()->email` | Same strategy as WP Rocket: one license = one Mixpanel user across all of its sites | | 2 | Host of `get_home_url()` | Fallback for unlicensed sites or an unreachable API, so events still get a stable anonymized identity | | 3 | *(none)* | If neither resolves, `identify()` is not called and the event carries no `distinct_id` | It is called from `get_default_event_properties()` — the single choke point every tracked event goes through. That placement is deliberate: - It reuses the `get_imagify_user()` result already needed for `license_owner` (transient-cached for 5 minutes), so no extra API call. - It never runs on plugin bootstrap, unlike identifying in the constructor. A per-instance `identified` flag keeps it to one call per instance per request. > `distinct_id` is a Mixpanel super property, not an event property — it will not appear in `get_default_event_properties()` return values or in the `track_direct()` `$properties` argument. --- ## Option key | Option | Default | Description | |---|---|---| | `imagify_mixpanel_optin` | `false` | Tracks whether the user has opted in to analytics. Set to `1` to enable tracking (e.g., `wp option update imagify_mixpanel_optin 1`). The opt-in toggle UI ships in a follow-up issue. | --- ## Hooks subscribed | Hook | Method | Priority | Args | |---|---|---|---| | `imagify_after_optimize` | `Subscriber::track_media_optimized` | 10 | 2 (`$process`, `$item`) | | `update_option_imagify_settings` | `Subscriber::track_settings_saved` | 10 | 2 (`$old_value`, `$new_value`) | | `update_site_option_imagify_settings` | `Subscriber::track_settings_saved` | 10 | 2 (`$option`, `$new_value`) | `imagify_after_optimize` fires inside ActionScheduler (see `classes/Job/MediaOptimization.php`). Because bulk/cron runs execute without a logged-in user, the module uses `Optin::can_track()` (option-only check) and `TrackingPlugin::track_direct()` (bypasses `current_user_can`). `update_option_imagify_settings` fires on single-site when the option value changes. `update_site_option_imagify_settings` fires on multisite. Note the argument ordering differs between them: on multisite the first argument is the option name string, not the old value — the delegating method casts both args to array to satisfy the strict-typed `Tracking` signature, and the old value is unused downstream. --- ## Event: Media Optimized Fired after a successful full-size optimization. Guards (all must pass): 1. `can_track()` returns `true` (opt-in enabled). 2. `'full'` is present in `$item['sizes_done']`. 3. `get_size_data('full')['success'] === true`. ### Event properties | Property | Source | Notes | |---|---|---| | `context` | `'wp_plugin'` | Always `wp_plugin` | | `license_owner` | SHA-256 hash of `get_imagify_user()->email` | Empty string if not connected | | `user_id` | `get_current_user_id()` | `0` during cron/bulk runs | | `optimization_level` | `DataInterface::get_optimization_level()` | 0=normal, 1=aggressive, 2=ultra | | `media_type` | `MediaInterface::get_mime_type()` | e.g. `image/jpeg` | | `original_size` | `get_size_data('full', 'original_size')` | Bytes | | `optimized_size` | `get_size_data('full', 'optimized_size')` | Bytes | | `savings_percent` | Computed | `0` when `original_size` is `0` | | `next_gen_format` | AVIF checked first, then WebP | `'avif'`, `'webp'`, or `null` | | `trigger` | Resolved from context | `'auto'`, `'bulk'`, or `'manual'` | `TrackingPlugin::track_direct()` automatically merges `domain`, `wp_version`, `php_version`, `plugin`, `brand`, and `application` — do NOT include these in `get_default_event_properties()`. ### Trigger resolution 1. `auto` — `$item['data']['is_new_upload']` is truthy. 2. `bulk` — transient `imagify_wp_optimize_running` or `imagify_custom-folders_optimize_running` is set. 3. `manual` — fallback. When `auto` and bulk transients are both true, `auto` wins. --- ## Event: Settings Saved Fired each time the Imagify settings option is actually updated (WordPress only fires `update_option_*` when the value changes). Guards: 1. `can_track()` returns `true` (opt-in enabled). ### Event properties | Property | Source option key | Notes | |---|---|---| | `context` | — | Always `'wp_plugin'` (from `get_default_event_properties()`) | | `license_owner` | — | SHA-256 hash of `get_imagify_user()->email`; empty string if not connected | | `user_id` | — | `get_current_user_id()` | | `optimization_level` | `optimization_level` | Cast to `int`; `null` if key absent | | `auto_optimize_on_upload` | `auto_optimize` | `bool` via `! empty()` | | `resize_larger_images` | `resize_larger` | `bool` via `! empty()` | | `next_gen_images_webp` | `convert_to_webp` | `bool` via `! empty()` | | `next_gen_images_avif` | `convert_to_avif` | `bool` via `! empty()` | | `backup_original` | `backup` | `bool` via `! empty()` | `TrackingPlugin::track_direct()` automatically merges `domain`, `wp_version`, `php_version`, `plugin`, `brand`, and `application` — these are not set manually. --- ## ServiceProvider bindings Registered in `config/providers.php` as `Imagify\Tracking\ServiceProvider`. | Binding | Arguments | |---|---| | `Imagify\Dependencies\WPMedia\Mixpanel\Optin` | `'imagify'`, `'manage_options'` | | `Imagify\Dependencies\WPMedia\Mixpanel\TrackingPlugin` | token, `'imagify '`, `'wp media'`, `'imagify'` | | `Imagify\Tracking\Tracking` | `Optin`, `TrackingPlugin` | | `Imagify\Tracking\Subscriber` | `Tracking` |