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
extendify / app / Insights.php

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

248 lines 8.6 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\Shared\Services\Sanitizer;
12 use Extendify\PartnerData;
13
14 /**
15 * Controller for handling various Insights related things.
16 * WP code reviewers: This is used in another plugin and not invoked here.
17 */
18
19 class Insights
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';
29
30 /**
31 * Tests the plugin knows how to run. Each is rolled independently against
32 * its own rollout percentage, which the partner config supplies.
33 *
34 * @var string[]
35 */
36 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound
37 const AVAILABLE_TESTS = [
38 'AutoLaunch.HideEnhanceAI',
39 'AutoLaunch.SubmitCreateWebsite',
40 'AutoLaunch.DescriptionPlaceholderLaw',
41 'AutoLaunch.MigrateScreen',
42 ];
43
44 /**
45 * Process the readme file to get version and name
46 *
47 * @return void
48 */
49 public function __construct()
50 {
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();
72 }
73
74 /**
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.
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.
82 * @return void
83 */
84 public static function setup(array $activeTests = [])
85 {
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;
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');
246 }
247 }
248