| 1 |
# Tracking Module |
| 2 |
|
| 3 |
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. |
| 4 |
|
| 5 |
--- |
| 6 |
|
| 7 |
## Architecture |
| 8 |
|
| 9 |
| Class | Role | |
| 10 |
|---|---| |
| 11 |
| `Imagify\Tracking\BaseTracking` | Abstract base: `can_track()`, `get_default_event_properties()`, `identify_user()` | |
| 12 |
| `Imagify\Tracking\Tracking` | Concrete: `track_media_optimized()`, `track_settings_saved()` | |
| 13 |
| `Imagify\Tracking\Subscriber` | Subscribes to WordPress hooks and delegates to `Tracking` | |
| 14 |
| `Imagify\Tracking\ServiceProvider` | Registers all module services into the DI container | |
| 15 |
|
| 16 |
The module depends on two Strauss-prefixed vendor classes: |
| 17 |
|
| 18 |
- `Imagify\Dependencies\WPMedia\Mixpanel\Optin` — manages opt-in state |
| 19 |
- `Imagify\Dependencies\WPMedia\Mixpanel\TrackingPlugin` — sends events to Mixpanel |
| 20 |
|
| 21 |
--- |
| 22 |
|
| 23 |
## User identification (`distinct_id`) |
| 24 |
|
| 25 |
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. |
| 26 |
|
| 27 |
`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. |
| 28 |
|
| 29 |
| Priority | Identifier | Rationale | |
| 30 |
|---|---|---| |
| 31 |
| 1 | `get_imagify_user()->email` | Same strategy as WP Rocket: one license = one Mixpanel user across all of its sites | |
| 32 |
| 2 | Host of `get_home_url()` | Fallback for unlicensed sites or an unreachable API, so events still get a stable anonymized identity | |
| 33 |
| 3 | *(none)* | If neither resolves, `identify()` is not called and the event carries no `distinct_id` | |
| 34 |
|
| 35 |
It is called from `get_default_event_properties()` — the single choke point every tracked event goes through. That placement is deliberate: |
| 36 |
|
| 37 |
- It reuses the `get_imagify_user()` result already needed for `license_owner` (transient-cached for 5 minutes), so no extra API call. |
| 38 |
- It never runs on plugin bootstrap, unlike identifying in the constructor. |
| 39 |
|
| 40 |
A per-instance `identified` flag keeps it to one call per instance per request. |
| 41 |
|
| 42 |
> `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. |
| 43 |
|
| 44 |
--- |
| 45 |
|
| 46 |
## Option key |
| 47 |
|
| 48 |
| Option | Default | Description | |
| 49 |
|---|---|---| |
| 50 |
| `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. | |
| 51 |
|
| 52 |
--- |
| 53 |
|
| 54 |
## Hooks subscribed |
| 55 |
|
| 56 |
| Hook | Method | Priority | Args | |
| 57 |
|---|---|---|---| |
| 58 |
| `imagify_after_optimize` | `Subscriber::track_media_optimized` | 10 | 2 (`$process`, `$item`) | |
| 59 |
| `update_option_imagify_settings` | `Subscriber::track_settings_saved` | 10 | 2 (`$old_value`, `$new_value`) | |
| 60 |
| `update_site_option_imagify_settings` | `Subscriber::track_settings_saved` | 10 | 2 (`$option`, `$new_value`) | |
| 61 |
|
| 62 |
`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`). |
| 63 |
|
| 64 |
`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. |
| 65 |
|
| 66 |
--- |
| 67 |
|
| 68 |
## Event: Media Optimized |
| 69 |
|
| 70 |
Fired after a successful full-size optimization. Guards (all must pass): |
| 71 |
|
| 72 |
1. `can_track()` returns `true` (opt-in enabled). |
| 73 |
2. `'full'` is present in `$item['sizes_done']`. |
| 74 |
3. `get_size_data('full')['success'] === true`. |
| 75 |
|
| 76 |
### Event properties |
| 77 |
|
| 78 |
| Property | Source | Notes | |
| 79 |
|---|---|---| |
| 80 |
| `context` | `'wp_plugin'` | Always `wp_plugin` | |
| 81 |
| `license_owner` | SHA-256 hash of `get_imagify_user()->email` | Empty string if not connected | |
| 82 |
| `user_id` | `get_current_user_id()` | `0` during cron/bulk runs | |
| 83 |
| `optimization_level` | `DataInterface::get_optimization_level()` | 0=normal, 1=aggressive, 2=ultra | |
| 84 |
| `media_type` | `MediaInterface::get_mime_type()` | e.g. `image/jpeg` | |
| 85 |
| `original_size` | `get_size_data('full', 'original_size')` | Bytes | |
| 86 |
| `optimized_size` | `get_size_data('full', 'optimized_size')` | Bytes | |
| 87 |
| `savings_percent` | Computed | `0` when `original_size` is `0` | |
| 88 |
| `next_gen_format` | AVIF checked first, then WebP | `'avif'`, `'webp'`, or `null` | |
| 89 |
| `trigger` | Resolved from context | `'auto'`, `'bulk'`, or `'manual'` | |
| 90 |
|
| 91 |
`TrackingPlugin::track_direct()` automatically merges `domain`, `wp_version`, `php_version`, `plugin`, `brand`, and `application` — do NOT include these in `get_default_event_properties()`. |
| 92 |
|
| 93 |
### Trigger resolution |
| 94 |
|
| 95 |
1. `auto` — `$item['data']['is_new_upload']` is truthy. |
| 96 |
2. `bulk` — transient `imagify_wp_optimize_running` or `imagify_custom-folders_optimize_running` is set. |
| 97 |
3. `manual` — fallback. |
| 98 |
|
| 99 |
When `auto` and bulk transients are both true, `auto` wins. |
| 100 |
|
| 101 |
--- |
| 102 |
|
| 103 |
## Event: Settings Saved |
| 104 |
|
| 105 |
Fired each time the Imagify settings option is actually updated (WordPress only fires `update_option_*` when the value changes). Guards: |
| 106 |
|
| 107 |
1. `can_track()` returns `true` (opt-in enabled). |
| 108 |
|
| 109 |
### Event properties |
| 110 |
|
| 111 |
| Property | Source option key | Notes | |
| 112 |
|---|---|---| |
| 113 |
| `context` | — | Always `'wp_plugin'` (from `get_default_event_properties()`) | |
| 114 |
| `license_owner` | — | SHA-256 hash of `get_imagify_user()->email`; empty string if not connected | |
| 115 |
| `user_id` | — | `get_current_user_id()` | |
| 116 |
| `optimization_level` | `optimization_level` | Cast to `int`; `null` if key absent | |
| 117 |
| `auto_optimize_on_upload` | `auto_optimize` | `bool` via `! empty()` | |
| 118 |
| `resize_larger_images` | `resize_larger` | `bool` via `! empty()` | |
| 119 |
| `next_gen_images_webp` | `convert_to_webp` | `bool` via `! empty()` | |
| 120 |
| `next_gen_images_avif` | `convert_to_avif` | `bool` via `! empty()` | |
| 121 |
| `backup_original` | `backup` | `bool` via `! empty()` | |
| 122 |
|
| 123 |
`TrackingPlugin::track_direct()` automatically merges `domain`, `wp_version`, `php_version`, `plugin`, `brand`, and `application` — these are not set manually. |
| 124 |
|
| 125 |
--- |
| 126 |
|
| 127 |
## ServiceProvider bindings |
| 128 |
|
| 129 |
Registered in `config/providers.php` as `Imagify\Tracking\ServiceProvider`. |
| 130 |
|
| 131 |
| Binding | Arguments | |
| 132 |
|---|---| |
| 133 |
| `Imagify\Dependencies\WPMedia\Mixpanel\Optin` | `'imagify'`, `'manage_options'` | |
| 134 |
| `Imagify\Dependencies\WPMedia\Mixpanel\TrackingPlugin` | token, `'imagify <VERSION>'`, `'wp media'`, `'imagify'` | |
| 135 |
| `Imagify\Tracking\Tracking` | `Optin`, `TrackingPlugin` | |
| 136 |
| `Imagify\Tracking\Subscriber` | `Tracking` | |
| 137 |
|