PluginProbe
Extendify / trunk
Extendify vtrunk
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.10.0 → trunk 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 }