| 1 |
# Linno Telemetry SDK |
| 2 |
|
| 3 |
Privacy-first telemetry SDK for Linno WordPress plugins. |
| 4 |
|
| 5 |
## Overview |
| 6 |
|
| 7 |
The Linno Telemetry SDK is a Composer package that provides privacy-first telemetry tracking for WordPress plugins. It enforces user consent, standardizes event payloads, and supports both the PostHog and OpenPanel analytics platforms. |
| 8 |
|
| 9 |
## Compliance and Development Guidelines (MUST READ) |
| 10 |
|
| 11 |
The SDK's core purpose is to handle data transmission securely and ethically. Developers using this SDK **must** adhere to strict consent and disclosure requirements. |
| 12 |
|
| 13 |
* **Internal Compliance Mandates:** For a complete list of requirements regarding PII collection, opt-in placement, and WordPress.org submission rules, please see our detailed **[](PRIVACY_GUIDELINE.mdPrivacy Implementation Guideline](PRIVACY_GUIDELINE.md](PRIVACY_GUIDELINE.md)**. |
| 14 |
*(This document details mandatory steps for GDPR/WP.org compliance when implementing the SDK.)* |
| 15 |
|
| 16 |
|
| 17 |
## Features |
| 18 |
|
| 19 |
- **Privacy-First**: Enforces user consent before sending most data (lifecycle events do not require consent). |
| 20 |
- **Easy Integration**: Simple config-array constructor — only `pluginFile` and `slug` are required. |
| 21 |
- **Canonical Event Taxonomy**: Library-owned events are emitted under a stable `activation/*` namespace. |
| 22 |
- **Lifecycle Events**: Tracks plugin activation and deactivation via the standard WordPress hook system. |
| 23 |
- **Optional PLG Triggers**: Define `setup` / `onboarding`, and `aha` / `kui` triggers only when needed — omitting them leaves those modules disabled by default. |
| 24 |
- **Custom Events**: Send arbitrary events with any name and optional properties through a PHP API _or_ a WordPress action hook. |
| 25 |
- **Non-Fatal Telemetry**: Missing drivers and send failures are logged and silently dropped — they never interrupt plugin execution. |
| 26 |
- **Multi-Driver Support**: Works with PostHog and OpenPanel; falls back to a safe NullDriver when no driver is configured. |
| 27 |
- **Asynchronous Sending**: Consented custom events are queued and sent via WP-Cron to prevent performance impact. |
| 28 |
- **WordPress Native**: Uses WordPress APIs and follows WordPress coding standards. |
| 29 |
- **Secure**: HTTPS-only transmission, nonce verification, input sanitization. |
| 30 |
- **Internationalized**: All user-facing strings are translatable. |
| 31 |
|
| 32 |
## Requirements |
| 33 |
|
| 34 |
- PHP 7.4 or higher |
| 35 |
- WordPress 5.0 or higher |
| 36 |
|
| 37 |
## Installation |
| 38 |
|
| 39 |
### Step 1: Configure Composer |
| 40 |
|
| 41 |
Add the VCS repository to your `composer.json`: |
| 42 |
|
| 43 |
```json |
| 44 |
"repositories": [ |
| 45 |
{ |
| 46 |
"type": "vcs", |
| 47 |
"url": "git@github.com:CODEREXLTD/linno-telemetry.git" |
| 48 |
} |
| 49 |
] |
| 50 |
``` |
| 51 |
|
| 52 |
### Step 2: Install via Composer |
| 53 |
|
| 54 |
In your WordPress plugin directory, run: |
| 55 |
|
| 56 |
```bash |
| 57 |
composer require linno/telemetry:dev-master |
| 58 |
``` |
| 59 |
|
| 60 |
### Step 3: Require Autoloader |
| 61 |
|
| 62 |
In your main plugin file, require the Composer autoloader: |
| 63 |
|
| 64 |
```php |
| 65 |
require_once __DIR__ . '/vendor/autoload.php'; |
| 66 |
``` |
| 67 |
|
| 68 |
That's it! You're ready to use the SDK. |
| 69 |
|
| 70 |
## Quick Start |
| 71 |
|
| 72 |
Here's a complete example of integrating the SDK into your WordPress plugin: |
| 73 |
|
| 74 |
```php |
| 75 |
<?php |
| 76 |
/** |
| 77 |
* Plugin Name: My Awesome Plugin |
| 78 |
* Description: An awesome WordPress plugin with telemetry |
| 79 |
* Version: 1.0.0 |
| 80 |
* Author: Your Name |
| 81 |
* Text Domain: my-awesome-plugin |
| 82 |
*/ |
| 83 |
|
| 84 |
if (!defined('ABSPATH')) { exit; } |
| 85 |
|
| 86 |
require_once __DIR__ . '/vendor/autoload.php'; |
| 87 |
|
| 88 |
use LinnoSDK\Telemetry\Client; |
| 89 |
|
| 90 |
// Optional display customizations |
| 91 |
Client::set_text_domain( 'my-awesome-plugin' ); |
| 92 |
Client::set_privacy_url( 'https://your-site.com/privacy-policy/' ); |
| 93 |
Client::set_consent_service_name( 'My Analytics' ); |
| 94 |
|
| 95 |
// Initialize the client — only 'pluginFile' and 'slug' are required. |
| 96 |
$telemetry_client = new Client([ |
| 97 |
'pluginFile' => __FILE__, |
| 98 |
'slug' => 'my-awesome-plugin', |
| 99 |
'pluginName' => 'My Awesome Plugin', |
| 100 |
'version' => '1.0.0', |
| 101 |
|
| 102 |
// Choose a driver. Omit to run with no driver (events silently dropped). |
| 103 |
'driver' => 'open_panel', // or 'posthog' |
| 104 |
'apiKey' => 'op_YOUR_CLIENT_ID', |
| 105 |
'apiSecret' => 'sec_YOUR_API_SECRET', |
| 106 |
]); |
| 107 |
|
| 108 |
// Optional: define automatic triggers for onboarding and AHA milestones. |
| 109 |
// Every key is optional — omitting a key disables that module. |
| 110 |
$telemetry_client->define_triggers([ |
| 111 |
|
| 112 |
// Fires activation/onboarding_completed once — use 'setup' or 'onboarding' |
| 113 |
'setup' => 'my_plugin_setup_complete', |
| 114 |
|
| 115 |
// Fires retention/feature_used for each defined feature |
| 116 |
'feature_used' => [ |
| 117 |
'funnel_created' => [ |
| 118 |
'hook' => 'my_plugin_funnel_created', |
| 119 |
], |
| 120 |
], |
| 121 |
|
| 122 |
// Fires activation/aha_reached — use 'aha' (canonical) or 'kui' (legacy alias) |
| 123 |
'aha' => [ |
| 124 |
'order_received' => [ |
| 125 |
'hook' => 'woocommerce_order_created', |
| 126 |
'threshold' => ['count' => 2, 'period' => 'week'], |
| 127 |
'callback' => function( $order_id ) { |
| 128 |
return ['order_id' => $order_id]; |
| 129 |
}, |
| 130 |
], |
| 131 |
'funnel_published' => [ |
| 132 |
'hook' => 'my_plugin_funnel_published', |
| 133 |
], |
| 134 |
], |
| 135 |
]); |
| 136 |
// Initialization, activation/deactivation hooks, and the custom-event action |
| 137 |
// hook are all registered inside the constructor — no extra init() call needed. |
| 138 |
``` |
| 139 |
|
| 140 |
### What Happens Next? |
| 141 |
|
| 142 |
1. **Plugin Activation**: The SDK internally registers the activation hook. When the plugin activates, it emits `activation/plugin_activated`. |
| 143 |
2. **Global Consent Notice (One Time)**: On the first Linno plugin installation, an admin notice asks for telemetry consent. |
| 144 |
3. **Shared Consent Across Linno Plugins**: Once allowed (or declined), the choice is reused for all other Linno plugins on that same site. |
| 145 |
4. **Table Creation After Consent**: The telemetry queue table is created only after consent is allowed, and only once per site. |
| 146 |
5. **Deactivation Feedback**: Upon deactivation, a modal will prompt the user for a reason, which triggers `activation/plugin_deactivated`. Handled automatically. |
| 147 |
6. **Asynchronous Sending**: Consented custom events are added to a local queue and sent via a daily WP-Cron job. |
| 148 |
|
| 149 |
### Onboarding Consent Flow (Important) |
| 150 |
|
| 151 |
If your plugin asks for consent inside a custom onboarding wizard (instead of using the default admin notice), activation happens first, so `plugin_activated` is initially marked as pending. |
| 152 |
|
| 153 |
When the user allows tracking in onboarding, call: |
| 154 |
|
| 155 |
```php |
| 156 |
$telemetry_client->set_optin_state( 'yes' ); |
| 157 |
``` |
| 158 |
|
| 159 |
This now automatically: |
| 160 |
|
| 161 |
- creates the queue table (if needed), and |
| 162 |
- flushes pending `plugin_activated` tracking exactly once. |
| 163 |
|
| 164 |
If your onboarding stores consent in your own option first, call this right after saving to keep telemetry state in sync: |
| 165 |
|
| 166 |
```php |
| 167 |
$telemetry_client->sync_consent_state(); |
| 168 |
``` |
| 169 |
|
| 170 |
Or use the global helper (no direct client call needed): |
| 171 |
|
| 172 |
```php |
| 173 |
linno_telemetry_sync_consent_state( __FILE__ ); |
| 174 |
``` |
| 175 |
|
| 176 |
No manual `plugin_activated` tracking is needed in your plugin. The SDK now also recovers this event when telemetry is initialized after activation (common in setup-wizard-driven bootstraps). |
| 177 |
|
| 178 |
If your wizard writes the consent option directly (without calling SDK methods), the SDK will still detect consent on `init()` and flush pending `plugin_activated` on the next request. |
| 179 |
|
| 180 |
## Canonical Event Names |
| 181 |
|
| 182 |
The SDK emits all library-owned events under the `activation/*` namespace for a stable analytics taxonomy: |
| 183 |
|
| 184 |
| Trigger | Emitted Event Name | |
| 185 |
|---|---| |
| 186 |
| Plugin activation | `activation/plugin_activated` | |
| 187 |
| Plugin deactivation | `activation/plugin_deactivated` | |
| 188 |
| Onboarding / setup | `activation/onboarding_completed` | |
| 189 |
| Feature Used | `retention/feature_used` | |
| 190 |
| AHA / KUI milestone | `activation/aha_reached` | |
| 191 |
|
| 192 |
Custom events submitted via `Client::track()` or the `<slug>_telemetry_track` WordPress action are passed through **unchanged** — the SDK never alters caller-supplied event names. |
| 193 |
|
| 194 |
## Custom Events |
| 195 |
|
| 196 |
### PHP API |
| 197 |
|
| 198 |
```php |
| 199 |
// Any event name; optional associative properties array; optional consent override. |
| 200 |
$telemetry_client->track( 'post_published', [ 'post_id' => 42 ] ); |
| 201 |
``` |
| 202 |
|
| 203 |
### WordPress Action Hook |
| 204 |
|
| 205 |
The SDK registers `<slug>_telemetry_track` during initialization. Fire it from anywhere: |
| 206 |
|
| 207 |
```php |
| 208 |
do_action( 'my-awesome-plugin_telemetry_track', 'post_published', [ 'post_id' => 42 ] ); |
| 209 |
``` |
| 210 |
|
| 211 |
Both paths accept any event name and an optional associative properties array, and route through the same consent-gated queue path. |
| 212 |
|
| 213 |
## Trigger System |
| 214 |
|
| 215 |
### Setup / Onboarding (fires `activation/onboarding_completed` once) |
| 216 |
|
| 217 |
```php |
| 218 |
$telemetry_client->define_triggers([ |
| 219 |
'setup' => 'my_plugin_setup_complete', // legacy key |
| 220 |
// 'onboarding' => 'my_plugin_setup_complete', // canonical alias — same behavior |
| 221 |
]); |
| 222 |
``` |
| 223 |
|
| 224 |
### Feature Used (fires `retention/feature_used`) |
| 225 |
|
| 226 |
```php |
| 227 |
$telemetry_client->define_triggers([ |
| 228 |
'feature_used' => [ |
| 229 |
'funnel_created' => [ |
| 230 |
'hook' => 'my_plugin_funnel_created', |
| 231 |
'callback' => function( $funnel_id ) { |
| 232 |
return ['funnel_id' => $funnel_id]; |
| 233 |
}, |
| 234 |
], |
| 235 |
], |
| 236 |
]); |
| 237 |
``` |
| 238 |
|
| 239 |
Alternatively, use the static convenience method to register a feature-used event from anywhere in your codebase after the client is initialized: |
| 240 |
|
| 241 |
```php |
| 242 |
use LinnoSDK\Telemetry\Client; |
| 243 |
|
| 244 |
// Fires retention/feature_used with feature='Export Settings' when the hook is triggered. |
| 245 |
Client::add_feature_used_event( 'my_plugin_settings_exported', 'Export Settings' ); |
| 246 |
|
| 247 |
// With optional extra parameters. |
| 248 |
Client::add_feature_used_event( 'my_plugin_settings_imported', 'Import Settings', [ 'source' => 'file' ] ); |
| 249 |
``` |
| 250 |
|
| 251 |
Then trigger the corresponding WordPress action in your plugin code: |
| 252 |
|
| 253 |
```php |
| 254 |
function my_plugin_export_settings() { |
| 255 |
// ... export logic ... |
| 256 |
do_action( 'my_plugin_settings_exported' ); |
| 257 |
} |
| 258 |
``` |
| 259 |
|
| 260 |
### AHA / KUI Milestones (fires `activation/aha_reached`) |
| 261 |
|
| 262 |
```php |
| 263 |
$telemetry_client->define_triggers([ |
| 264 |
// 'aha' is the canonical key; 'kui' is the legacy alias — both work. |
| 265 |
'aha' => [ |
| 266 |
'order_received' => [ |
| 267 |
'hook' => 'woocommerce_order_created', |
| 268 |
'threshold' => ['count' => 2, 'period' => 'week'], |
| 269 |
], |
| 270 |
'funnel_published' => [ |
| 271 |
'hook' => 'my_plugin_funnel_published', // fires every time |
| 272 |
], |
| 273 |
], |
| 274 |
]); |
| 275 |
``` |
| 276 |
|
| 277 |
`activation/aha_reached` events include an `indicator` property with the milestone name for downstream filtering. |
| 278 |
|
| 279 |
### Custom Trigger (pass-through event name) |
| 280 |
|
| 281 |
Register a trigger that fires a developer-supplied event name on any hook: |
| 282 |
|
| 283 |
```php |
| 284 |
$telemetry_client->triggers() |
| 285 |
->on( 'page_created', 'my_plugin_page_created', function( $page_id ) { |
| 286 |
return ['page_id' => $page_id]; |
| 287 |
}); |
| 288 |
``` |
| 289 |
|
| 290 |
## Events Not Requiring Consent |
| 291 |
|
| 292 |
The SDK automatically tracks these events **without requiring user consent**: |
| 293 |
|
| 294 |
- **`activation/plugin_activated`**: When the plugin is activated. |
| 295 |
- Includes: `site_url`, `unique_id`. |
| 296 |
- **`activation/plugin_deactivated`**: When the plugin is deactivated. |
| 297 |
- Includes: `site_url`, `unique_id`, `reason`. |
| 298 |
|
| 299 |
**Why no opt-in required?** These lifecycle events contain no personal data (no email, name, or user profile fields). |
| 300 |
|
| 301 |
## Non-Fatal Driver Behavior |
| 302 |
|
| 303 |
The SDK is designed to never interrupt plugin execution: |
| 304 |
|
| 305 |
- **No driver configured** → a warning is written to `error_log` and events are silently dropped. |
| 306 |
- **Unrecognized driver name** → same as above. |
| 307 |
- **Driver `send()` fails** → the failure is logged to `error_log` and the event is dropped. |
| 308 |
|
| 309 |
No exceptions are thrown during normal event submission. |
| 310 |
|
| 311 |
## Data Collected (with User Consent) |
| 312 |
|
| 313 |
With user consent, the SDK collects: |
| 314 |
|
| 315 |
- Site URL |
| 316 |
- Plugin name and version |
| 317 |
- Event timestamps |
| 318 |
- Unique site profile ID (anonymous) |
| 319 |
- Custom event properties (as defined by developer) |
| 320 |
|
| 321 |
**No sensitive personal data** is collected beyond what is strictly necessary for anonymous usage analytics and product improvement, and only with explicit user consent. |
| 322 |
|
| 323 |
## Appsero Consent Compatibility |
| 324 |
|
| 325 |
The SDK supports migration from Appsero consent keys so existing users are not prompted again. |
| 326 |
|
| 327 |
- Primary key: `linno_telemetry_allow_tracking` |
| 328 |
- Legacy pattern: `{plugin_slug}_allow_tracking` |
| 329 |
- Also checks known legacy keys: |
| 330 |
- `best-woocommerce-feed_allow_tracking` |
| 331 |
- `wpvr_allow_tracking` |
| 332 |
- `wpfunnels_allow_tracking` |
| 333 |
- `cart-lift_allow_tracking` |
| 334 |
- `creatorlms_allow_tracking` |
| 335 |
- `mail-mint_allow_tracking` |
| 336 |
|
| 337 |
If a legacy key exists with `yes` or `no` and `linno_telemetry_allow_tracking` is not set, the value is automatically reused and migrated to the Linno key. |
| 338 |
|
| 339 |
## Using the PostHog Driver |
| 340 |
|
| 341 |
```bash |
| 342 |
composer require posthog/posthog-php |
| 343 |
``` |
| 344 |
|
| 345 |
```php |
| 346 |
$client = new Client([ |
| 347 |
'pluginFile' => __FILE__, |
| 348 |
'slug' => 'my-awesome-plugin', |
| 349 |
'driver' => 'posthog', |
| 350 |
'driver_config' => [ |
| 351 |
'host' => 'https://app.posthog.com', |
| 352 |
'api_key' => 'phc_YOUR_POSTHOG_API_KEY', |
| 353 |
], |
| 354 |
]); |
| 355 |
``` |
| 356 |
|
| 357 |
## Using the OpenPanel Driver |
| 358 |
|
| 359 |
```php |
| 360 |
$client = new Client([ |
| 361 |
'pluginFile' => __FILE__, |
| 362 |
'slug' => 'my-awesome-plugin', |
| 363 |
'driver' => 'open_panel', |
| 364 |
'apiKey' => 'op_YOUR_CLIENT_ID', |
| 365 |
'apiSecret' => 'sec_YOUR_API_SECRET', |
| 366 |
]); |
| 367 |
``` |
| 368 |
|
| 369 |
## License |
| 370 |
GPL-2.0-or-later |
| 371 |
|
| 372 |
## Support |
| 373 |
For support, please contact support@linno.co |
| 374 |
|