PluginProbe
Extendify / 3.2.3
Extendify v3.2.3
3.2.3 3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 All 129 releases
extendify / app / Insights.php

Insights.php in Extendify 3.2.3, at app/Insights.php

256 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Insights setup
5 */
6
7 namespace Extendify;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\Mcp\Availability;
12 use Extendify\Mcp\Connections;
13 use Extendify\Mcp\Log;
14 use Extendify\Shared\Services\Sanitizer;
15 use Extendify\PartnerData;
16
17 /**
18 * Controller for handling various Insights related things.
19 * WP code reviewers: This is used in another plugin and not invoked here.
20 */
21
22 class Insights
23 {
24 /**
25 * Option name storing each site's A/B test assignments, keyed by the
26 * screen/feature under test (e.g. 'AutoLaunch.HideEnhanceAI').
27 *
28 * @var string
29 */
30 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound
31 const ACTIVE_TESTS_OPTION = 'extendify_active_tests';
32
33 /**
34 * Tests the plugin knows how to run. Each is rolled independently against
35 * its own rollout percentage, which the partner config supplies.
36 *
37 * @var string[]
38 */
39 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound
40 const AVAILABLE_TESTS = [
41 'AutoLaunch.HideEnhanceAI',
42 'AutoLaunch.SubmitCreateWebsite',
43 'AutoLaunch.DescriptionPlaceholderLaw',
44 'AutoLaunch.MigrateScreen',
45 ];
46
47 /**
48 * Process the readme file to get version and name
49 *
50 * @return void
51 */
52 public function __construct()
53 {
54 // If there isn't a siteId, then create one.
55 if (!\get_option('extendify_site_id', false)) {
56 \update_option('extendify_site_id', \wp_generate_uuid4());
57 }
58
59 if (
60 defined('EXTENDIFY_INSIGHTS_URL')
61 && class_exists('ExtendifyInsights')
62 && !\get_option('extendify_insights_checkedin_once', 0)
63 ) {
64 \update_option('extendify_insights_checkedin_once', gmdate('Y-m-d H:i:s'));
65 // WP code reviewers: This job is defined in another plugin (i.e. it's opt-in).
66 \add_action('init', function () {
67 // Run this once but wait 10 minutes.
68 \wp_schedule_single_event((time() + 10 * MINUTE_IN_SECONDS), 'extendify_insights');
69 \spawn_cron();
70 });
71 }
72
73 $this->filterExternalInsights();
74 $this->setupAdminLoginInsights();
75 }
76
77 /**
78 * Assign A/B variants for the known tests based on the partner's active
79 * tests. Each active test is rolled once;
80 * inactive tests are dropped.
81 *
82 * @param string[] $activeTests Active tests in `Name:Percentage` form
83 * (e.g. 'AutoLaunch.HideEnhanceAI:20'); a bare
84 * name defaults to a 50% rollout.
85 * @return void
86 */
87 public static function setup(array $activeTests = [])
88 {
89 $assignments = \get_option(self::ACTIVE_TESTS_OPTION, []);
90
91 $percentages = [];
92 foreach ($activeTests as $entry) {
93 list($key, $percentage) = array_pad(explode(':', $entry, 2), 2, null);
94 $percentages[$key] = is_numeric($percentage) ? (float) $percentage : 50.0;
95 }
96
97 foreach (self::AVAILABLE_TESTS as $key) {
98 if (!array_key_exists($key, $percentages)) {
99 unset($assignments[$key]);
100 continue;
101 }
102
103 // Roll once so the site keeps the same variant.
104 if (!isset($assignments[$key])) {
105 $assignments[$key] = [
106 // The percentage is variant B's rollout share: 50 -> 50% A / 50% B,
107 // 20 -> 80% A / 20% B.
108 'variant' => random_int(1, 10000) <= $percentages[$key] * 100 ? 'B' : 'A',
109 'percentage' => $percentages[$key],
110 // ISO 8601 (UTC)
111 'assignedAt' => gmdate('c'),
112 ];
113 }
114 }
115
116 \update_option(self::ACTIVE_TESTS_OPTION, Sanitizer::sanitizeArray($assignments));
117 }
118
119 /**
120 * Add additional data to the opt-in insights
121 *
122 * @return void
123 */
124 public function filterExternalInsights()
125 {
126 add_filter('extendify_insights_data', function ($data) {
127 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
128 $readme = file_get_contents(EXTENDIFY_PATH . 'readme.txt');
129 preg_match('/Stable tag: ([0-9.:]+)/', $readme, $version);
130
131 $insights = array_merge($data, [
132 'launch' => Config::$showLaunch,
133 'launchRedirectedAt' => \get_option('extendify_attempted_redirect', null),
134 'launchLoadedAt' => \get_option('extendify_launch_loaded', null),
135 'partner' => defined('EXTENDIFY_PARTNER_ID') ? constant('EXTENDIFY_PARTNER_ID') : null,
136 'siteCreatedAt' => SiteSettings::getSiteCreatedAt(),
137 'assistRouterData' => \get_option('extendify_assist_router', null),
138 'libraryData' => \get_option('extendify_library_site_data', null),
139 'draftSettingsData' => \get_option('extendify_draft_settings', null),
140 'activity' => \get_option('extendify_shared_activity', null),
141 'domainsActivities' => \get_option('extendify_domains_recommendations_activities', null),
142 'extendifyVersion' => ($version[1] ?? null),
143 'siteProfile' => \get_option('extendify_site_profile', null),
144 'pluginSearchTerms' => \get_option('extendify_plugin_search_terms', []),
145 'blockSearchTerms' => \get_option('extendify_block_search_terms', []),
146 'phpVersion' => PHP_VERSION,
147 'themeSearchTerms' => \get_option('extendify_theme_search_terms', []),
148 'license' => PartnerData::setting('license'),
149 'pagesCount' => $this->getPostsCount('page'),
150 'postsCount' => $this->getPostsCount('post'),
151 'lastUpdatedPage' => $this->getLastUpdatedPost('page'),
152 'lastUpdatedPost' => $this->getLastUpdatedPost('post'),
153 'lastLoginAdmin' => $this->getLastAdminLogin(),
154 'hasImprint' => $this->hasImprint(),
155 ]);
156
157 if (Availability::offered()) {
158 $insights['mcp'] = ['usage' => Log::usage(), 'connections' => Connections::count()];
159 }
160
161 return $insights;
162 });
163 }
164
165 /**
166 * Get the number of posts/pages
167 *
168 * @param string $type The type of post/page to get the count for (post or page)
169 * @return int The number of posts/pages
170 */
171 protected function getPostsCount($type = 'post')
172 {
173 $count = wp_count_posts($type);
174 return isset($count->publish) ? (int) $count->publish : 0;
175 }
176
177 /**
178 * Set up admin login insights to monitor when admin users log in
179 *
180 * @return void
181 */
182 protected function setupAdminLoginInsights()
183 {
184 add_action('wp_login', function ($user_login, $user) {
185 // Only get insights for admin users
186 if (user_can($user, 'manage_options')) {
187 update_user_meta($user->ID, 'extendify_last_login', gmdate('Y-m-d H:i:s'));
188 }
189 }, 10, 2);
190 }
191
192 /**
193 * Get the last time a post/page was updated
194 *
195 * @return string|null The last updated post timestamp or null if no posts found
196 */
197 protected function getLastUpdatedPost($type = 'post')
198 {
199 $posts = get_posts([
200 'post_type' => $type,
201 'post_status' => 'publish',
202 'orderby' => 'modified',
203 'order' => 'DESC',
204 'numberposts' => 1,
205 'fields' => 'ids'
206 ]);
207
208 if (!empty($posts)) {
209 $post = get_post($posts[0]);
210 return $post ? $post->post_modified : null;
211 }
212 return null;
213 }
214
215 /**
216 * Get the last time an admin user logged in
217 *
218 * @return string|null The most recent admin login timestamp or null if no data found
219 */
220 protected function getLastAdminLogin()
221 {
222 $admins = get_users([
223 'role' => 'administrator',
224 'meta_key' => 'extendify_last_login',
225 'orderby' => 'meta_value',
226 'order' => 'DESC',
227 'number' => 1
228 ]);
229
230 if (!empty($admins)) {
231 return get_user_meta($admins[0]->ID, 'extendify_last_login', true);
232 }
233 return null;
234 }
235
236 /**
237 * Check if the site has an imprint based on the site profile and language settings
238 *
239 * @return bool True if the site has an imprint, false otherwise
240 */
241 protected function hasImprint()
242 {
243 $siteProfile = \get_option('extendify_site_profile', []);
244 if (empty($siteProfile)) {
245 return false;
246 }
247
248 $imprintLanguages = array_filter(PartnerData::setting('showImprint') ?? [], function ($value) {
249 return $value === get_locale();
250 });
251
252 return !empty($imprintLanguages) && (strtolower($siteProfile['aiSiteCategory']) === 'business' ||
253 strtolower($siteProfile['category']) === 'business');
254 }
255 }
256