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