FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Use AJAX for activating features / plugins in Performance Lab by b1ink0 · Pull Request #1646 · WordPress/performance · GitHub

Use AJAX for activating features / plugins in Performance Lab by b1ink0 · Pull Request #1646 · WordPress/performance · GitHub
Skip to content

Navigation Menu

Sign in

Use AJAX for activating features / plugins in Performance Lab - #1646

Merged
felixarntz merged 25 commits into
WordPress:trunkfrom
b1ink0:update/use-ajax-for-activate-plugin
Nov 15, 2024
Merged

Use AJAX for activating features / plugins in Performance Lab#1646
felixarntz merged 25 commits into
WordPress:trunkfrom
b1ink0:update/use-ajax-for-activate-plugin

Conversation

b1ink0 commented Nov 11, 2024
edited
Loading

Copy link
Copy Markdown
Contributor

Summary

Fixes #1583

Relevant technical choices

  1. Frontend / UX

    • Features / plugins in Performance Lab are now activated using AJAX improving user experience.
    • No page refresh when activating features / plugins.
    • Multiple plugin can be activated as each plugin activation sends different AJAX request.
    • The click event is added to the anchor link as if JavaScript is enabled then the default behaviour is prevented so it functions as a button else It behaves as a link.
  2. Backend

    • Removed perflab_print_plugin_progress_indicator_script function as it was previously used for adding a progress indicator to the activate button on click. This functionality is now moved to the separate JavaScript file which handles the activation of plugin.
    • Added perflab-plugin-activate-ajax (script handle) script which handles activation of features / plugins using AJAX.
    • Updated perflab_render_plugin_card function to add data attribute data-plugin-slug in plugin activate link.
    • Added new REST API endpoints
      • performance-lab/v1/features/(?P<slug>[a-z0-9_-]+):activate
      • performance-lab/v1/features/(?P<slug>[a-z0-9_-]+)
  3. Unable to Implement fully

    • Regarding the setting URL which is displayed in the admin notices and the plugin card.
    • I have implemented the settings URL part but it only works for the Modern Image Formats plugin.
    • Context: Currently only two plugins provide the settings URL Modern Image Formats and Speculative Loading.
    • It does not work with the Speculative Loading because of how this plugin is loaded.
      • This plugin gets loaded ( loaded in this context is when are its helper files are loaded like settings.php ) on after_setup_theme hook.
      • But when the AJAX callback is triggered this hook has already been executed. As the plugin is activated later and which requries the after_setup_theme hook to fully load the plugin which includes the settings URL.
      • The function which is executed in the after_setup_theme calls a closure function which is stored in the global variable ( plsr_pending_plugin_info for the Speculative Loading plugin ) this closure function fully loads the plugin.
    • I tried and tested this could be fixed by calling the closure function in AJAX callback but we will be hardcoding the global variable name and will have to update the callback function if other plugins also add settings URL.
  4. Demo

demo_updated.mp4

github-actions Bot commented Nov 11, 2024
edited
Loading

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: b1ink0 <b1ink0@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>
Co-authored-by: felixarntz <flixos90@git.wordpress.org>
Co-authored-by: mukeshpanchal27 <mukesh27@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

westonruter added this to the performance-lab n.e.x.t milestone Nov 11, 2024
// Enqueue plugin activate AJAX script and localize script data.
wp_enqueue_script(
'perflab-plugin-activate-ajax',
PERFLAB_PLUGIN_DIR_URL . 'includes/admin/plugin-activate-ajax.js',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This PERFLAB_PLUGIN_DIR_URL is plugin_dir_url( PERFLAB_MAIN_FILE ), but I believe here it would be better to use plugins_url():

Suggested change
PERFLAB_PLUGIN_DIR_URL . 'includes/admin/plugin-activate-ajax.js',
plugins_url( 'includes/admin/plugin-activate-ajax.js', PERFLAB_MAIN_FILE ),

The reason is that this function allows for filtering as needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@westonruter I don't think we should randomly change it here though. If not using plugins_url() leads to a problem, let's discuss that in a separate issue and implement properly if needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I've filed #1647 for this.

Comment thread plugins/performance-lab/load.php Outdated
define( 'PERFLAB_VERSION', '3.5.1' );
define( 'PERFLAB_MAIN_FILE', __FILE__ );
define( 'PERFLAB_PLUGIN_DIR_PATH', plugin_dir_path( PERFLAB_MAIN_FILE ) );
define( 'PERFLAB_PLUGIN_DIR_URL', plugin_dir_url( PERFLAB_MAIN_FILE ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

If my suggestion above is accepted, this can then be removed.

Suggested change
define( 'PERFLAB_PLUGIN_DIR_URL', plugin_dir_url( PERFLAB_MAIN_FILE ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

See above, let's discuss separately, it's not really relevant for this enhancement.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

At least let's avoid introducing a new constant in this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

That's fair, we can simply use plugin_dir_url( PERFLAB_MAIN_FILE ) in the wp_enqueue_script() call directly. I'm personally not a fan of having constants for all this either, particularly as the values are filterable, like you're saying.

westonruter Nov 11, 2024
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I've filed #1647 for the filtering.

*/
$( document ).on(
'click',
'.perflab-install-active-plugin',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

What if someone clicks this button while another request is currently in flight? Should it short-circuit so as to avoid duplicating requests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I think as the request could have been half finished we should not short-circuit the request. I think it will be better to just disable it until the request is finished. But as the click event is on the anchor tag we will have to add pointer-events: none CSS to the anchor link using JavaScript to disable it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

The pointer-events style wouldn't address the issue of the button being activated by keyboard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

As the click event is on the anchor tag and not button we will have to add some other work around for disabling on keyboard one is setting its tabindex attribute to -1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I tested this but if user is already focused then tabindex will not work. One other way is to add a attribute like aria-disabled and then check on click event if it has that attribute then return from the execution. Should I add tabindex and aria-disabled both?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

You could add the disabled attribute, but that wouldn't be good because it would lose the tab focus for users navigating with the keyboard. Instead of this, WordPress has style rules for when you add the disabled class to a button so that it will look disabled. Adding aria-disabled here too would be good. But then for how to detect whether request is currently in progress, you could just short-circuit the click handler if the button has the updating-message class, or if it has the disabled class, or if it has the aria-disabled attribute.

}

// @ts-ignore
} )( window.jQuery );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Does it make sense to make jQuery a dependency? Would plain JS not be feasible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

+1, it would be great to use vanilla JavaScript rather than relying on jQuery, to set a good example for performance.

Comment on lines +95 to +99
showAdminNotice(
__( 'Feature activated.', 'performance-lab' ),
'success',
pluginSettingsURL
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

In the demo video you showed that this notice is causing layout shifts, resulting in you accidentally clicking the title of a plugin instead of the Activate button. I don't think an admin notice is even needed here, because the button transitions from Activate to Activating... to Active.

Copy link
Copy Markdown
Member

It does not work with the Speculative Loading because of how this plugin is loaded.

  • This plugin gets loaded ( loaded in this context is when are its helper files are loaded like settings.php ) on after_setup_theme hook.
  • But when the AJAX callback is triggered this hook has already been executed. As the plugin is activated later and which requries the after_setup_theme hook to fully load the plugin which includes the settings URL.
  • The function which is executed in the after_setup_theme calls a closure function which is stored in the global variable ( plsr_pending_plugin_info for the Speculative Loading plugin ) this closure function fully loads the plugin.

So the issue is that when a plugin is activated, its source files are included, but they are included after the after_setup_theme action has fired, so then it is too late for the plugin to initialize in order to add the plugin_action_links_* filter. Interesting.

Given that plugin activation inherently happens partyway through another WP execution lifecycle after many of the actions potentially used by the activated plugin have already fired, I think it makes sense to introduce a secondary endpoint exclusively for obtaining the settings link. So you'd have one endpoint for activating the plugin and another endpoint for getting the settings link. This should be more reliable across how plugins bootstrap themselves.

felixarntz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks for the PR @b1ink0!

Not much to add from myself to the feedback @westonruter already provided. It would be great to not have to use jQuery for this.

wp_enqueue_script(
'perflab-plugin-activate-ajax',
PERFLAB_PLUGIN_DIR_URL . 'includes/admin/plugin-activate-ajax.js',
array( 'jquery', 'wp-i18n', 'wp-a11y' ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Related to @westonruter's previous feedback, it would be great not to use jQuery for the functionality, but rather vanilla JavaScript (e.g. fetch() function).

Copy link
Copy Markdown
Member

Given that plugin activation inherently happens partyway through another WP execution lifecycle after many of the actions potentially used by the activated plugin have already fired, I think it makes sense to introduce a secondary endpoint exclusively for obtaining the settings link. So you'd have one endpoint for activating the plugin and another endpoint for getting the settings link. This should be more reliable across how plugins bootstrap themselves.

This said, it seems like we can avoid having to introduce a separate endpoint by tweaking how Speculative Loading bootstraps. Instead of:

static function ( string $global_var_name, string $version, Closure $load ): void {
if ( ! isset( $GLOBALS[ $global_var_name ] ) ) {
$bootstrap = static function () use ( $global_var_name ): void {
if (
isset( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] )
&&
$GLOBALS[ $global_var_name ]['load'] instanceof Closure
&&
is_string( $GLOBALS[ $global_var_name ]['version'] )
) {
call_user_func( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] );
unset( $GLOBALS[ $global_var_name ] );
}
};
// Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used
// because it is the first action that fires once the theme is loaded.
add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN );
}
// Register this copy of the plugin.
if (
// Register this copy if none has been registered yet.
! isset( $GLOBALS[ $global_var_name ]['version'] )
||
// Or register this copy if the version greater than what is currently registered.
version_compare( $version, $GLOBALS[ $global_var_name ]['version'], '>' )
||
// Otherwise, register this copy if it is actually the one installed in the directory for plugins.
rtrim( WP_PLUGIN_DIR, '/' ) === dirname( __DIR__ )
) {
$GLOBALS[ $global_var_name ]['version'] = $version;
$GLOBALS[ $global_var_name ]['load'] = $load;
}
}

We could do this:

	static function ( string $global_var_name, string $version, Closure $load ): void {
		$needs_bootstrap = ! isset( $GLOBALS[ $global_var_name ] );

		// Register this copy of the plugin.
		if (
			// Register this copy if none has been registered yet.
			! isset( $GLOBALS[ $global_var_name ]['version'] )
			||
			// Or register this copy if the version greater than what is currently registered.
			version_compare( $version, $GLOBALS[ $global_var_name ]['version'], '>' )
			||
			// Otherwise, register this copy if it is actually the one installed in the directory for plugins.
			rtrim( WP_PLUGIN_DIR, '/' ) === dirname( __DIR__ )
		) {
			$GLOBALS[ $global_var_name ]['version'] = $version;
			$GLOBALS[ $global_var_name ]['load']    = $load;
		}

		if ( $needs_bootstrap ) {
			$bootstrap = static function () use ( $global_var_name ): void {
				if (
					isset( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] )
					&&
					$GLOBALS[ $global_var_name ]['load'] instanceof Closure
					&&
					is_string( $GLOBALS[ $global_var_name ]['version'] )
				) {
					call_user_func( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] );
					unset( $GLOBALS[ $global_var_name ] );
				}
			};

			// Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used
			// because it is the first action that fires once the theme is loaded.
			if ( did_action( 'after_setup_theme' ) ) {
				$bootstrap();
			} else {
				add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN );
			}
		}
	}

Where if after_setup_theme already fired, we then invoke the bootstrap logic immediately. This would fix the issue with Speculative Loading, but this would essentially be a one-off fix that wouldn't necessarily work for other plugins. For example, Optimization Detective initializes at the init action, and at that point it triggers its own od_init action for other plugins to register their own dependent logic. Image Prioritizer, for example, waits to load any of its logic until the od_init action fires. If the Optimization Detective plugin was already installed and active, and then someone uses Ajax to activate the Image Prioritizer plugin, then the od_init action will never fire either, resulting in this exact same problem. So I think it is better to let the PHP process to activate the plugin fully terminate and then to start a new PHP process in a new endpoint to obtain the settings URL. This will avoid all issues with hook ordering across all plugins.

westonruter added [Type] Enhancement A suggestion for improvement of an existing feature [Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only labels Nov 11, 2024
Comment on lines 228 to 229
// Enqueue the a11y script.
wp_enqueue_script( 'wp-a11y' );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Do we need these enqueue now? as it already added through new script dependancy.

b1ink0 Nov 12, 2024
edited
Loading

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We could remove it as it is not being used anywhere other than the AJAX script as it already added in its dependancy. Should I remove it then?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actually, in #1646 (comment) I just posed this change as a possibility but then recommended against it since it would only account for Speculative Loading. It wouldn't account for other plugins that have delayed bootstrapping. So to address the problem of accessing the settings link after a plugin is activated, I think the best solution is to make another request specifically to an endpoint that exposes the settings link URLs.

Aside: I think the REST API would be better for this instead of admin-ajax.

*/
$( document ).on(
'click',
'.perflab-install-active-plugin',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

You could add the disabled attribute, but that wouldn't be good because it would lose the tab focus for users navigating with the keyboard. Instead of this, WordPress has style rules for when you add the disabled class to a button so that it will look disabled. Adding aria-disabled here too would be good. But then for how to detect whether request is currently in progress, you could just short-circuit the click handler if the button has the updating-message class, or if it has the disabled class, or if it has the aria-disabled attribute.

b1ink0 requested a review from westonruter November 13, 2024 13:18

westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Getting closer!

Comment on lines +20 to +25
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Since this endpoint is not a noun, it may make more sense as /plugin:activate. See for example in Optimization Detective:

/**
* Route for storing a URL Metric.
*
* Note the `:store` art of the endpoint follows Google's guidance in AIP-136 for the use of the POST method in a way
* that does not strictly follow the standard usage. Namely, submitting a POST request to this endpoint will either
* create a new `od_url_metrics` post, or it will update an existing post if one already exists for the provided slug.
*
* @link https://google.aip.dev/136
* @var string
*/
const OD_URL_METRICS_ROUTE = '/url-metrics:store';

Suggested change
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';
/**
* Route for activating plugin.
*
* Note the `:activate` art of the endpoint follows Google's guidance in AIP-136 for the use of the POST method in a way
* that does not strictly follow the standard usage.
*
* @link https://google.aip.dev/136
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/plugin:activate;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Indeed this is not a great resource URI for a REST URL, as it's not truly RESTful. REST endpoints should be centered around a resource, in this case a plugin.

But even better would be something like: /plugins/<slug>:activate. This would also be more intuitive in that the required parameter slug would be directly in the route path.

Suggested change
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+):activate';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@felixarntz I was just curious what should be the criteria for deciding whether to add the slug to the URL itself or include it in the body.

felixarntz Nov 14, 2024
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@b1ink0 It's one of the best practices for designing RESTful APIs, they should be centered around resources. See for example https://google.aip.dev/121 and https://google.aip.dev/122. (While these links are for Google's best practices, the best practices are not really specific to Google.)

By centering the API endpoints and their names around resources and their hierarchies, you future-proof the API to remain consistent and intuitive even when you add more and more features to it.

Here, the resource is a plugin. That's why using something like /plugins/<slug> as foundation for the routes is a good starting point. Such an endpoint could be used to get data for a specific plugin, or update data for a plugin for example.

Activating a plugin is not one of the CRUD operations though, so that's where a custom method is needed. See https://google.aip.dev/136 in that regard: That's how /plugins/<slug>:activate makes sense.

For example, if we later needed a method to deactivate a plugin, that would be straightforward via /plugins/<slug>:deactivate. If we used /activate-plugin, this would need to be /deactivate-plugin. That's still somewhat consistent but not centered around the resource and less intuitive. For example, nothing in the name tells you that you have to provide a slug. Additionally, even if you guessed that some identifier for the plugin would be needed, you wouldn't know whether it needs to be a parameter called slug or id or identifier for example. The resource identifier being part of the endpoint path makes that easier to work with.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@westonruter Following up on the other thread (since it probably makes sense to center that conversation about the endpoint naming): Per https://google.aip.dev/122, I think what would make most sense for the settings URL would be to simply be a value on the object returned by GET /plugins/<slug>.

A settings URL is not part of a collection, because a plugin does not have multiple settings URL. So GET /plugins/<slug>/settings-url would not be right. Because it's not an action (as you previously stated), GET /plugins/<slug>:settings-url wouldn't be right either.

The settings URL is one scalar piece of data for a plugin, so it makes most sense to be exposed on a plugin object. Obviously for this PR we don't care about creating a fully fledged GET /plugins/<slug> endpoint, but that's fine. We can simply for now return an object that e.g. only has slug and settingsUrl properties. The schema for such an object could be extended in the future in case we ever found a need for it, without backward compatibility breaks.

So I think going with POST /plugins/<slug>:activate and GET /plugins/<slug> (the latter of which return an object that includes the settings URL) is best in line with the API design guidelines.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@felixarntz Additionally, there is also already a way to activate a plugin using the REST API by setting the status field on the entity to active, without resorting to a special endpoint naming scheme: https://developer.wordpress.org/rest-api/reference/plugins/#update-a-plugin

However, reusing this endpoint is more complicated since we also automatically install the plugin prior to activation, as well as installing and activating any dependencies.

So maybe we should keep our own endpoint for installation and activation but instead of calling it plugin:activate we call it feature:activate to differentiate it from general plugin activation, as these plugins represent performance features.

In the end, this API is almost guaranteed to be used 100% internally and so we don't really need to stress about it being getting it right forever.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I don't think the existing plugin endpoints in Core are a problem. We use a different namespace specific to Performance Lab, so I wouldn't worry about that. It's IMO not unreasonable to provide custom endpoints for similar use-cases under a different namespace when the Core endpoints do not satisfy the requirements we have.

Regarding updating a status field to active, that's how they chose to build it, but doesn't seem that great to me per https://google.aip.dev/216.

That said, I'd be okay using the terminology "features" instead of "plugins" for our endpoints, since that's also what we use in the UI, and I agree it can help differentiate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

So let's go with POST /features/<slug>:activate and GET /features/<slug> then?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Where GET /features/<slug> returns an object with one key, settingsUrl which may be either a string or null, right? Sounds good.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Exactly.

If we like, we could also include e.g. slug in the response object as that's simple enough and clarifies the intent of the endpoint even more (that it's supposed to return plugin information, so not necessarily just the settings URL).

*
* @var string
*/
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url/(?P<slug>[a-z0-9_-]+)';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

See above, better would be:

Suggested change
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+):settings-url';

This way the two routes are also consistently centered around a specific plugin.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

But in this case it's just a regular REST API endpoint. No need for any workaround for a non-RESTy POST request, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

How do you mean? How is this a workaround?

westonruter Nov 14, 2024
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I mean, as I understand, a workaround like this is needed when the REST API endpoint is not a noun and/or the existing HTTP methods don't make semantic sense. According to AIP-136 on custom methods:

Resource-oriented design (AIP-121) uses custom methods to provide a means to express arbitrary actions that are difficult to model using only the standard methods.

What we discussed above in https://github.com/WordPress/performance/pull/1646/files#r1841160336 is to add a verb to the endpoint for use with POST. But here for the settings URL, it's a classic REST API endpoint, like most other REST API endpoints in WordPress, where we're just GET a noun. So so the normal REST design makes more sense to me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

That's a good point. Though the current naming still seems incorrect in that it's the settings URL of a specific plugin. So a more appropriate name would be:

Suggested change
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+)/settings-url';

This way it is a resource, but under the plugin it belongs to. Arguably with that approach, the 404 error if there's no settings URL makes more sense too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

See #1646 (comment), discard my previous comment.

Let's continue that discussion on the other thread, to center the conversation about endpoint naming in one place.

felixarntz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@b1ink0 This looks like it's on the right track.

I think going with a REST API is great, though it would have been okay to go with Admin AJAX for this kind of endpoint as well, that would have been a bit simpler. If we use the REST API, I think we should ensure we use it correctly. Most of my feedback is centered around that.

But this is coming along nicely!

}

// Attach the event listener.
document.addEventListener( 'click', handlePluginActivationClick );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Why does this add a listener for the entire document instead of adding listeners to the buttons specifically?

I feel like that would be a more reasonable choice, as it avoids having the listener fire for more or less every click on the page. Since the buttons are present in the HTML response from the beginning, I don't see a need for listening on document.

Comment on lines +187 to +192
return new WP_REST_Response(
array(
'success' => true,
'pluginSettingsURL' => false,
)
);

felixarntz Nov 13, 2024
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I disagree here. The caller provides the plugin slug, not an identifier for the settings URL. I'd argue a 404 would only be appropriate if the plugin slug was for a plugin that's not active or doesn't exist. But if the plugin is active and just doesn't provide a settings URL, then it shouldn't be a 404, since the endpoint's sole purpose is to retrieve the settings URL for an active plugin.

* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error Response.
*/
function perflab_handle_activate_plugin( WP_REST_Request $request ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Now that we expose this as a REST endpoint, I wonder whether we should restrict it to plugins that are part of the Performance Lab program. Otherwise we may need to deal with other implications related to arbitrary other plugins. I feel like limiting to our own would be a good idea, for both of these endpoints.

For example, we may make certain assumptions about our plugins, that aren't reasonable to make for any plugin.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This is already restricted to the PL plugins, is it not? It's using perflab_sanitize_plugin_slug() which returns null if it is not a valid PL plugin slug. But I also suggested an alternative in https://github.com/WordPress/performance/pull/1646/files#r1840894846

So I think this is fine.

Comment on lines +20 to +25
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Indeed this is not a great resource URI for a REST URL, as it's not truly RESTful. REST endpoints should be centered around a resource, in this case a plugin.

But even better would be something like: /plugins/<slug>:activate. This would also be more intuitive in that the required parameter slug would be directly in the route path.

Suggested change
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/activate-plugin';
/**
* Route for activating plugin.
*
* @var string
*/
const PERFLAB_ACTIVATE_PLUGIN_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+):activate';

*
* @var string
*/
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

See above, better would be:

Suggested change
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugin-settings-url';
const PERFLAB_PLUGIN_SETTINGS_URL_ROUTE = '/plugins/(?P<slug>[a-z0-9_-]+):settings-url';

This way the two routes are also consistently centered around a specific plugin.

westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This looks great! Just a couple trivial suggestions, but I'm pre-approving.

b1ink0 requested a review from westonruter November 15, 2024 17:13

felixarntz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@b1ink0 A few more points of follow up feedback. Some nit-picks, but most importantly we have to fix the capability checks.

'type' => 'string',
'description' => __( 'Plugin slug of plugin/feature that needs to be activated.', 'performance-lab' ),
'required' => true,
'validate_callback' => 'perflab_validate_slug_endpoint_arg',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Since this validates against the list of PL features/plugins, should we include an enum entry here with the possible values?

If that feels like too much, I think we should at least mention it in the description, e.g. "Must be one of the Performance Lab feature slugs."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I initially proposed using an enum here, but it won't work unless we then also always require the additional admin-only PHP files used in PerfLab. So what is implemented with this validate_callback is a sort of just-in-time enum. This is already explained in the phpdoc for perflab_validate_slug_endpoint_arg().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

SG. In that case let's just mention it in the description.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

felixarntz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks for all the work on this @b1ink0, and thanks @westonruter for the final iterations!

felixarntz commented Nov 15, 2024
edited
Loading

Copy link
Copy Markdown
Member

@b1ink0 @westonruter I just tested this locally. While it works great for the most part, I get a 500 error when trying to install Performant Translations (probably because it's not installed):

Uncaught Error: Call to undefined function request_filesystem_credentials() in \/var\/www\/html\/wp-admin\/includes\/class-wp-upgrader-skin.php:139

We may have missed to test this with plugins that aren't yet installed. Looks like there are more files to require.

Update: Fixed in e0691bf.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only [Type] Enhancement A suggestion for improvement of an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use AJAX for activating features / plugins in Performance Lab

4 participants

Footer

© 2026 GitHub, Inc.

Back | FazBrowse Home | New Git URL