PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
← All changes | includes/class-usage-tracker.php +85 -12 1.0.61.3.3 View file →
@@ -2,10 +2,12 @@
2 2 /**
3 3 * Usage_Tracker — anonymous, opt-in plugin usage analytics.
4 4 *
5 5 * Ported from the WP Insights SDK (the same engine WPDeveloper plugins such as
6 - * EmbedPress ship). Trimmed for xSpeed: no deactivation "goodbye" survey, no
7 - * email marketing capture by default.
6 + * EmbedPress ship). Trimmed for xSpeed: no email marketing capture by default.
7 + * The deactivation "goodbye" survey UI lives in Deactivation_Feedback (shown to
8 + * every admin); it stores the reason in the canonical WPInsight options and
9 + * deactivate_this_plugin() transmits it — see that method.
8 10 *
9 11 * PRIVACY CONTRACT (see CLAUDE.md "Hard do-not" + readme.txt):
10 12 * Nothing is collected or sent until the site admin EXPLICITLY opts in via
11 13 * the setup wizard. `require_optin` is always true. Until `opt_in( true )`
@@ -122,23 +124,94 @@
122 124 }
123 125 }
124 126
125 127 /**
126 - * On deactivation: tell insights we went inactive (only if opted in),
127 - * then clear the cron. No reason survey is collected.
128 + * On deactivation: report to WPInsight that we went inactive, carrying
129 + * the deactivation reason the admin submitted on the Plugins screen (if
130 + * any). Deactivation_Feedback stores that reason in the canonical
131 + * `wpins_deactivation_reason_<slug>` / `wpins_deactivation_details_<slug>`
132 + * options; we read + transmit + delete them here.
133 + *
134 + * Two send paths:
135 + * - Usage analytics ON → the full, site-correlated body (get_data())
136 + * with the reason appended, via the normal send_data() handshake.
137 + * This is the canonical WPInsight deactivation record.
138 + * - Usage analytics OFF → nothing is sent UNLESS the admin explicitly
139 + * submitted the survey; in that case a minimal, reason-only payload
140 + * goes out as per-action consent (no diagnostics inventory).
128 141 */
129 142 public function deactivate_this_plugin() {
130 - if ( ! $this->is_tracking_allowed() ) {
143 + $reason_key = 'wpins_deactivation_reason_' . $this->plugin_name;
144 + $details_key = 'wpins_deactivation_details_' . $this->plugin_name;
145 + $reason = get_option( $reason_key, false );
146 + $details = get_option( $details_key, false );
147 +
148 + if ( $this->is_tracking_allowed() ) {
149 + $body = $this->get_data();
150 + $body['status'] = 'Deactivated';
151 + $body['deactivated_date'] = time();
152 + if ( false !== $reason ) {
153 + $body['deactivation_reason'] = $reason;
154 + }
155 + if ( false !== $details ) {
156 + $body['deactivation_details'] = $details;
157 + }
158 + $this->send_data( $body );
159 +
160 + if ( ! $this->disabled_wp_cron ) {
161 + wp_clear_scheduled_hook( self::EVENT_HOOK );
162 + }
163 + } elseif ( false !== $reason || false !== $details ) {
164 + $this->send_deactivation_feedback( $reason, $details );
165 + }
166 +
167 + // Never let a stored reason linger or double-send on the next cycle.
168 + delete_option( $reason_key );
169 + delete_option( $details_key );
170 + }
171 +
172 + /**
173 + * Minimal, reason-only deactivation report for when usage analytics is
174 + * OFF but the admin submitted the deactivation survey. Sends only plugin
175 + * identity, WP/PHP version, and the reason/details — never the full
176 + * diagnostic body get_data() assembles (no plugin inventory, no theme,
177 + * no xSpeed config). Per-action consent; see the privacy contract at the
178 + * top of this file and readme.txt "External services".
179 + *
180 + * @param string|false $reason Stored deactivation reason label, or false.
181 + * @param string|false $details Stored free-text detail, or false.
182 + */
183 + private function send_deactivation_feedback( $reason, $details ) {
184 + if ( empty( self::API_URL ) ) {
131 185 return;
132 186 }
133 - $body = $this->get_data();
134 - $body['status'] = 'Deactivated';
135 - $body['deactivated_date'] = time();
136 - $this->send_data( $body );
187 + $plugin = $this->plugin_data();
188 + $body = array(
189 + 'plugin_slug' => sanitize_text_field( $this->plugin_name ),
190 + 'url' => get_bloginfo( 'url' ),
191 + 'status' => 'Deactivated',
192 + 'deactivated_date' => time(),
193 + 'site_version' => get_bloginfo( 'version' ),
194 + 'php_version' => phpversion(),
195 + 'wpins_version' => self::WPINS_VERSION,
196 + );
197 + if ( ! empty( $plugin['Name'] ) ) {
198 + $body['plugin'] = sanitize_text_field( $plugin['Name'] );
199 + }
200 + if ( ! empty( $plugin['Version'] ) ) {
201 + $body['version'] = sanitize_text_field( $plugin['Version'] );
202 + }
203 + if ( false !== $this->item_id ) {
204 + $body['item_id'] = $this->item_id;
205 + }
206 + if ( false !== $reason ) {
207 + $body['deactivation_reason'] = sanitize_text_field( $reason );
208 + }
209 + if ( false !== $details ) {
210 + $body['deactivation_details'] = sanitize_text_field( $details );
211 + }
137 212
138 - if ( ! $this->disabled_wp_cron ) {
139 - wp_clear_scheduled_hook( self::EVENT_HOOK );
140 - }
213 + $this->remote_post( $body );
141 214 }
142 215
143 216 /**
144 217 * Cron callback. Bails before any HTTP unless tracking is allowed and