0 comments on “My Profile | Subscription | Update Card”

My Profile | Subscription | Update Card

The pre-production release of the 2602.XX.YY versions of the SaaS are setup with a revised React-based subscription processor.

The My Profile | Subscription tab allows the user to…

  • Change the plan (upgrade, downgrade)
  • Update Card
  • Cancel Subscription

Update Card

The update card process appears to be calling the backend, however how Stripe stores payment data and how it is rendered has changed.

0 comments on “Contact Us Loading On Every Page”

Contact Us Loading On Every Page

When a user is logged in, the application stack is calling \MySLP_Contact_Us::initialize on every page load on the MySLP SaaS system. This should only be loaded when a user is interacting with the Contact Us page in the app.

This is being called via \MySLP_loader in the myslp-dashboard code. This happens when plugins_loaded action hooks are called from WordPress per this code block:

	// Load the Customer Profile module AFTER history logger (pri; 15)
	add_action( 'plugins_loaded', function () {
		/**
		 * @return void
		 */
		function myslp_customer_interfaces_loader(): void {
			require_once( MYSLP_PLUGIN_DIR . 'include/customer_profile/MySLP_Customer_Profile.php' );
			require_once( MYSLP_PLUGIN_DIR . 'include/MySLP_Contact_Us.php' );
			require_once( MYSLP_PLUGIN_DIR . 'include/MySLP_Customer_Maintenance.php' );

			MySLP_Contact_Us::get_instance();
		}

		myslp_customer_interfaces_loader();
	}, 15 );
0 comments on “My Profile | Notifications”

My Profile | Notifications

The My Profile page is rendered as a React component as of the 2601.XX release. This is invoke using the WordPress blocks system via the JavaScript wp scripts helper in package.json.

Related PHP Classes and Methods

My Profile is managed via the MySLP Dashboard repo (Store-Locator-Plus/myslp-dashboard).
The \MySLP_Customer_Profile class extends SLP_Base_ReactObject.
SLP_Base_ReactObject is the Store Locator Plus class that acts as the helper to wire PHP data to the JavaScript interface using the defined WordPress blocks system. WordPress blocks are , at their core, React components.

SLP_Base_ReactObject JavaScript Variable Population

This is handled via the extendReactVars method, which is usually extended by child classes.
The return PHP array end up populating the slpReact JavaScript variable.

Most of the MySLP (SaaS code) variables will return a sub-array named mySLP.
This results in the JavaScript variable slpReact.mySLP which contains SaaS specific variables.

For example:
$vars[‘mySLP’][‘subscription’] = $this->get_subscription_data();

The notifications stack uses the MySLP_Customer_Profile::add_notification to build an array of notification messages. These are then consumed by the React ProfilePanel component.

Related React Components

ProfilePanel in WordPress/wp-content/plugins/myslp-dashboard/src/profile/profile.tsx is the primary wrapper for the entire My Profile page React component.

Notifications are handled by a Snackbar component provided by the @mui/material React framework.
It is driven by the JavaScript variables slpReact.mySLP.notifications array.
Each element is an object with a message<string> and severity<string> property.
If the notifications array is not empty, the Snackbar opens and the message stack is displayed.
The severity element defines the style of the Snackbar message interface.

0 comments on “Map Views”

Map Views


AI Summary

Here’s what “map view” (a.k.a. Map Views / mapview_count) covers on internal.storelocatorplus.com, plus where it shows up in the SaaS code paths you’re working with.

What “Map Views” means in MySLP

  • Map Views = $myslp->User->mapview_count, stored as user meta under the key mapview_count and exposed via the \MySLP_User magic getter/setter.    
  • It’s treated as a subscription/billing usage counter (separate from geocoding usage and referer tracking).    

How map views are counted (increment)

  • The docs state map views are incremented whenever \MySLP_REST_API::get_map_options() is called — “theoretically/assumed to be whenever the map is rendered.”
    • Practically: anything that triggers the “map options” REST call is part of the map-view counting surface area (including unexpected extra calls).
    • \MySLP_REST_API::get_map_options() is only called via the REST API endpoint:
      myslp/v2/locations-map
      • This REST endpoint lives in the rest_path JavaScript encoded variable.
        This fires from:
        • SLP style/LocatorPreview.js script
        • MySLP front-end/location.js
          • Where this is called via jQuery.ajax GET with properties…
            • url: … + rest_path (myslp/v2/locations-map)
            • data.action: get_myslp_data
            • data._jsonp: ‘initMySLP’

When map views reset

Map views reset to 0 via \myslp_extend_plan() (in the myslp-dashboard-helpers module), and internal docs list these callers:    

  • \MySLP_Dashboard_Controller::check_subscription() (when a subscription was expired and is renewed automatically)
  • \MySLP_Recurring_Payments::initialize() PayPal-only path → get_paypal_checkout_details() → myslp_extend_plan()
  • Profile update hooks (personal_options_update / edit_user_profile_update) via \MySLP_Customer_Maintenance::update_myslp_profile_management()

Where “map views” appear in user-facing subscription messaging

  • In plan-change logging/messaging: when a subscription changes, the message explicitly calls out map view limits and overage billing (“Each map view over this limit is billed at $X per N views.”). This is in the SaaS logging code you shared (log_subscription_changed()), and it references MySLP_User::VIEW_OVERAGE_PRICE and MySLP_User::VIEW_OVERAGE_BLOCKSIZE.
  • In renewal/extension logging: the History Logger includes “Prior month map views: %d” when extending a plan (if mapview_count is present). (That’s in the plugin code shown in your editor.)

Related “don’t confuse these with map views” items

  • Referer logging is not “map views.” Internal docs tie referer tracking to geocoding validation (slp_rest_geocode_invalid_referer) and note it’s called during geocoding requests, not every map render.
    • So: map views ≈ map options endpoint calls; referer tracking ≈ geocode request path.
  • Referer tracking is stored in myslp->User->referer_urls
    • Extended via \MySLP_User::log_referer
      • Called via hook firing on slp_rest_geocode_invalid_referer via \MySLP::log_and_validate_referer
    • This is stored in the blog_option ‘user_subscription_status’

Operational/performance note: map renders can trigger multiple REST option reads

  • There’s an internal debugging write-up about excess option lookups during a “single map render,” discovered around SLP 5.13.8—specifically repeated lookups of option values (like label_directions). That’s relevant because map view counting is tied to a REST “options” call, and repeated calls can inflate usage and load if not controlled.  
  • A separate internal post shows how map-related REST responses can differ (e.g., inline styles returned in the “locations-map” call), reinforcing that map rendering often involves REST payloads that can change between environments.  
0 comments on “Power : $slplus is null in UI handler”

Power : $slplus is null in UI handler

SLP should not allow the older version of Power (2511.06.01 or earlier) to run.

Got error 'PHP message: PHP Fatal error:  
Uncaught Error: Call to a member function addon() on null 
in /bitnami/wordpress/wp-content/plugins/slp-power/include/module/ui/SLP_Power_UI.php:41

From..

		global $slplus;
		$this->addon = $slplus->addon( 'power' );

Resolution

Must use Power 2511.06.02 with the latest version of SLP.

SLP had to be updated to test for the minimum version of the Power add on at 2511.06.02 or higher.

0 comments on “Updating SLP version wipes out Google API Key”

Updating SLP version wipes out Google API Key

With the WordPress plugin, if you update the version of SLP the Google API key is erased.

This likely impacts other settings as well.

Reproduction

  • Login to admin on localhost (Docker dev container)
  • Go to Store Locator Plus | Options
  • Add a Google API key
  • In the code update wp-content/plugins/store-locator-plus/store-locator-plus.php and change the version
  • Reload the options page

The Google API keys are blank.

0 comments on “Hide Store Pages Menu on SaaS”

Hide Store Pages Menu on SaaS

A recent update to the Store Locator Plus® WordPress plugin or Power add on  have re-introduced the Pages menu item on the sidebar in the SaaS application.

Task

Remove “Pages” from the sidebar menu on the SaaS application.

Reproduction

  1. Login to the SaaS platform
  2. Switch to a user with Professional or Enterprise level access
  3. Go to Options on the sidebar
  4. Check the Enabled Pages checkbox

Result

Pages appears on the sidebar.

Expected Result

Do not show pages on the sidebar until this is fully functional.

Resolution

0 comments on “Google Maps Should Load Async”

Google Maps Should Load Async

When testing a Store Locator Plus® for WordPress setup with the base plugin and Premier plugin active…

Google Maps JavaScript API has been loaded directly without loading=async. This can result in suboptimal performance. For best-practice loading patterns please see https://goo.gle/js-api-loading

0 comments on “WP Distribution : Some Fonts / Images Missing”

WP Distribution : Some Fonts / Images Missing

While testing Power : Imports Are Not Working other issue were noted about missing images and fonts.

On the WordPress QC Site looking in the JavaScript console shows some WOFF/TTF and image files are missing in the CSS stack.

Need to update the Store Locator Plus® plugin distribution.


🔲  All CSS fonts (woff/woff2/ttf) files are missing
Check the distribution packaging ruleset in the AWS CodeBuilder https://qc.storelocatorplus.com/wp-content/plugins/store-locator-plus/css/fonts/fontawesome-webfont.woff2?v=4.7.0
https://qc.storelocatorplus.com/wp-content/plugins/store-locator-plus/css/fonts/fontawesome-webfont.ttf?v=4.7.0
https://qc.storelocatorplus.com/wp-content/plugins/store-locator-plus/css/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular

🔲  Some CSS images are missing
https://qc.storelocatorplus.com/wp-content/plugins/store-locator-plus/css/admin/DataTables-1.10.24/images/sort_both.png
https://qc.storelocatorplus.com/wp-content/plugins/store-locator-plus/css/admin/DataTables-1.10.24/images/sort_asc.png

Power : Imports Are Not Working

On the WordPress Test Site (https://qc.storelocatorplus.com/) the Location Import feature of Store Locator Plus® for WordPress (SLP for WP) is not working.

Location import does work on the local Docker container.

Location import does work on staging and production versions of the SaaS application.

The version of the SLP, Power, Experience, and Premier plugins are the same on QC, localhost (the docker container), and the SaaS deployments.

Reproduction

  1. Login to the QC test server.
  2. Ensure the Store Locator Plus® and Power plugins are active.
  3. Go to Store Locator Plus | Locations on the sidebar menu.
  4. Click the import tab.
  5. Click the upload CSV button.
  6. Choose a sample CSV file like the one noted below.

Result

The import never starts. The progress meter never appears. No new locations that are in the CSV are added to the location list.

Expected Result

An import information window should show the circular loading progress meter and the location import count when finished. New locations appear on the location list.

Resolution

Status: Partially Resolved

Findings

Turns out that if a user downloads a new version of the Power add on (a zip file) and already has a prior version of the slp-power.zip file in the download directory on their laptop, the browser may create a new file named slp-power-2.zip without direct notification or confirmation it has done so.

If the user uploads this file to the WordPress installation it will create a NEW installation of the Power plugin at ./wp-content/plugins/slp-power-2/. This may or may not reside alongside a version of the older installation at ./wp-content/plugins/slp-power/ which is the standard installation path.

Even if a user deactivates and deletes the existing Power add on , which is best practice, before uploading the new slp-power-2.zip , the new path for the plugin will not match the prior path.

This causes the JavaScript for location imports included in the Power add on to not be loaded.

Workaround

Make sure the downloaded zip files for the plugin follow the standard naming convention:

  • SLP plugin should be named store-locator-plus.zip
  • Power add on should be named slp-power.zip

Deactivate and delete the installed versions of any plugins that are being updated.

Upload and install the most recent plugin zip file.

Meta

The Location Import feature is provided by the Power add on (a plugin that works with SLP for WP).

The issue is being tracked on the GitHub SLP project here:
https://github.com/Store-Locator-Plus/myslp_aws_ecs_kit/issues/35

Debugging

Check for JavaScript errors.

  1. 🔲 Import test with Power / Experience / Premier active using slp_test_at_slp_guru_locations CSV file shown below.

slp_test_at_slp_guru_locations.csv

sl_id,sl_store,sl_address,sl_address2,sl_city,sl_state,sl_zip,sl_country,sl_latitude,sl_longitude,sl_tags,sl_description,sl_email,sl_url,sl_hours,sl_phone,sl_fax,sl_image,sl_private,sl_neat_title,featured,rank,category,category_slug,contact,contact_address,contact_email,contact_fax,contact_image,county,department,district,facility_type,first_name,identifier,introduction,last_name,marker,mobile_phone,notes,office_hours,office_phone,region,territory,title,training,year_established
5136,"Amalfi's Italian Restaurant & Pizzeria","664 Long Point Rd",#E,"Mt Pleasant",SC,29464,,32.83928400,-79.85446600,,,,,,,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,
5138,"The Wine Bar","664 Long Point Rd","Unit G","Mt Pleasant",SC,29464,,32.83930200,-79.85423300,,,,,,,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,
5140,"Burtons Grill & Bar","1876 North Highway 17",,"Mt Pleasant",SC,29464,,32.83029500,-79.83291900,,,,,,,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,

Info Gathering Toward Resolution

Path Disclosure

Searching for the difference in the URLs in the JavaScript:

  • Added additional information to the SLP | Info | Environment tab within the SLP plugin showing the plugin directory and path.
    • updated \SLP_REST_Environment::get() to add new environment variables
Site URLhttp://localhosthttp://qc.storelocatorplus.com
Store Locator Plus®2511.04.012511.04.01
Power2510.01.012510.01.01
Experience2510.02.012510.02.01
Premier2506.23.012506.23.01
SLP Network ActiveNoNo
WordPress Version6.8.36.8.3
WordPress Memory Limit40M40M
WordPress Max Memory Limit256M512M
PHP Version8.3.18.2.28
PHP Memory Limit128M512M
PHP Post Max Size64M80M
PHP Peak RAM6 MB8 MB
MySQL Version8.3.011.4.7
Plugin Directory/var/www/html/wp-content/plugins/store-locator-plus//bitnami/wordpress/wp-content/plugins/store-locator-plus/
Plugin URLhttp://localhost/wp-content/plugins/store-locator-plushttps://qc.storelocatorplus.com/wp-content/plugins/store-locator-plus

QC Versus Local HTML Sources

Partial Resolution

If you upload slp-power-12.zip to a WordPress site your new power directory will be ./wp-content/plugins/slp-power-12/

That is NOT and issue if you’ve NEVER had power installed before (my plugins detect whatever random directory you put in the first time).

However if you UPDATE an existing Power plugin that was already installed at wp-content/plugins/slp-power with a zip file named slp-power-12.zip the app will break. First of all you’ll likely end up with TWO power plugins listed in your plugin directory. Out of habit you will likely delete the older one, which inevitably will be the one in wp-content/plugins/slp-power, leaving the revised path of wp-content/plugins/slp-power-12/ as the new path.

Plugin Update Best Practices

Rename your zip files to the base name only:

  • store-locator-plus.zip (or store-locator-le.zip if you are an original old-school SLP user from version 5 or earlier)
  • slp-power.zip
  • slp-experience.zip
  • slp-premier.zip

If it won’t let you, sort folder on your computer by name and delete the old copies…. then rename the downloaded zip file t one of the above.

Install the properly named zip file on WordPress:

First delete the original plugin (deactivate/delete) then upload the new zip file.

This process MIGHT work with the inline update like the pic below, but some systems cannot handle the duplicate directory on the server automatically (some do, but not all) .   Thus it is safer to deactivate/delete first versus using the “fancy updater” that does an inline replace in WordPress.

The inline update process.

New Research : Reproduced On Localhost

I realized renaming the directory from slp-power to slp-power-2 (or 3) in the IDE will NOT change the mount point in the Docker container.  To change that the Docker composer file needs to change the mount point of the volumes.

I created a new composer file that mounts the wp-content/plugins/slp-power directory in the IDE (on the host laptop for Docker) to wp-content/plugins/slp-power-2 in the Docker container.

Shut down the prior Docker container and started a new container with the revised path.

Sure enough, the Power import breaks if the Power add on is NOT installed in ./wp-content/plugins/slp-power/

This indicates an error in the base plugin (SLP) or Power add on that is not allowing the install path to be flexible. It skips loading the required JavaScript library.

Work Toward Resolution

UX Update Show Directories

Let’s show the add on directory paths in the SLP | Info | Environment panel.

Updated \SLP_REST_Environment::get() to show the directory path for any add on directly underneath the plugin version.

Likely Culprit : \SLP_BaseClass_Admin::enqueue_admin_javascript

\SLP_BaseClass_Admin::enqueue_admin_javascript is the likely culprit which is NOT loading wp-content/plugins/slp-power/js/slppower-admin-locations-tab.js if the directory changes to wp-content/plugins/slp-power-2/js/slppower-admin-locations-tab.js

In the following code snippet from \SLP_BaseClass_Admin::enqueue_admin_javascript

		if ( $this->addon->short_slug === 'store-locator-le' ) {
			$base_name = 'slp';
		} else {
			$base_name = preg_replace( '/\W/', '', dirname( $this->addon->slug ) );
		}

Incoming data

$this->addon->slug = 'slp-power-2/slp-power.php'

Returns

$base_name = 'slppower2'

Which later means this code in \SLP_BaseClass_Admin::enqueue_admin_javascript

			case 'manage_locations':
				$files = array(
					'js/admin-locations-tab.min.js',
					'js/admin-locations-tab.js',
					'js/' . $base_name . '-admin-locations-tab.min.js',
					'js/' . $base_name . '-admin-locations-tab.js'
				);
				break;

Is looking for a file named ‘js/slppower2-admin-locations-tab.js’ which does not exist.

The fix via AI Assistant Claude Sonnet 4.5

In the following code snippet from \SLP_BaseClass_Admin::enqueue_admin_javascript

if ( $this->addon->short_slug === 'store-locator-le' ) {
$base_name = 'slp';
} else {
$base_name = preg_replace( '/\W/', '', $this->addon->short_slug);
}

Additional Fixes

In addition to the fix, I renamed wp-content/plugins/slp-power/js/slppower-admin-locations-tab.js to wp-content/plugins/slp-power/js/admin-locations-tab.js

This makes it more consistent with other plugins. It also allows \SLP_BaseClass_Admin::enqueue_admin_javascript to be simplified to:
case 'manage_locations':
$files = array( 'js/admin-locations-tab.min.js', 'js/admin-locations-tab.js' );
break;

Power 2511.05.01 now requires SLP 2511.05.01 with the primary fix being in the SLP main plugin.

AI Summary

⟨ΞPowerImports⟩ ≡ ⟨ΨPathDependency⟩ ⊢ ⟨ΔAssetEnqueue⟩

ΨRootCause:
  ⟨$base_name⟩ ← dirname(⟨slug⟩) ⇨ ⟨directory_name⟩
  IF ⟨install_path⟩ ≠ ⟨canonical_path⟩ THEN
    ⟨slug⟩ = 'slp-power-2/slp-power.php'
    ∴ dirname(⟨slug⟩) = 'slp-power-2'
    ∴ ⟨$base_name⟩ = 'slppower2'
    ∴ ⟨js_file⟩ = 'js/slppower2-admin-locations-tab.js' ↯ 404

ΩResolution:
  1. SLP_BaseClass_Admin.php:376
     BEFORE: $base_name = preg_replace('/\W/', '', dirname($this->addon->slug))
     AFTER:  $base_name = preg_replace('/\W/', '', $this->addon->short_slug)
     
  2. Normalize asset naming:
     RENAME: js/slppower-admin-locations-tab.js 
     TO:     js/admin-locations-tab.js
     
  3. Simplify enqueue logic:
     $files = ['js/admin-locations-tab.min.js', 'js/admin-locations-tab.js']

⊢ ⟨ΔPathIndependence⟩: Assets load correctly regardless of installation directory
⊢ ⟨ΔConsistency⟩: Standardized naming across all add-ons
⊢ ⟨ΔMaintainability⟩: Reduced code complexity

Formula: ⟨short_slug⟩ ⟶ ⟨base_name⟩ ⊥ ⟨directory_structure⟩

Cross Container (ECS) WordPress Session Management

Since containers are ephemeral and each instance handles requests independently, sharing session data requires using a centralized session storage such as AWS ElastiCache.

ElastiCache can be configured for Valkey (open source Reddis) or Memcache. Valkey is lower cost.

Set up ElastiCache

  • Create a Valkey server.
  • Configure a publicly accessible or VPC-limited endpoint, depending on your ECS networking setup.
  • Choose 3 subnets on same network as ECS containers
  • Choose the ECS security group

Configure The Docker Image

Add the Redis extension to PHP and enable it in the php.ini configuration. This configuration uses environment variables so the Redis server can be configured with environment variables for each container instance.

Create The Host Image Builder PHP Ini File

Review the PHP Runtime Configuration page on session settings.

Create ./Docker/Images/Files/php/docker-php-ext-redis.ini

extension=redis.so
session.save_handler = ${PHP_SESSION_SAVE_HANDLER}
session.save_path = ${PHP_SESSION_SAVE_PATH}

Update The Host Dockerfile

Update the host Dockerfile to install Redis and the libs needed to support it. Copy the php ini file into conf.d so it is loaded when PHP starts. This example is from a WordPress 6 image running PHP 8 on Apache.

Create ./Docker/Images/Dockerfile

# -- base image

FROM public.ecr.aws/docker/library/wordpress:6.4.2-php8.3-apache
LABEL authors="lancecleveland" \
      image="WordPress Multisite on Apache"

# -- ports

EXPOSE 443

# -- os utilities

RUN set -eux; \
	apt-get update; \
	apt-get install -y --no-install-recommends \
		dnsutils \
        inetutils-traceroute \
        iputils-ping \
        libz-dev \
        libssl-dev \
        libmagickwand-dev \
	; \
	rm -rf \
        /var/lib/apt/lists/* \
	    /usr/src/wordpress/wp-content/themes/* \
	    /usr/src/wordpress/wp-content/plugins/* \
	    /usr/src/wordpress/wp-config-example.php \
    ;

# -- install Redis PHP extension
RUN pecl channel-update pecl.php.net \
    && pecl install redis \
    && docker-php-ext-enable redis

# -- PHP redis
COPY ./Files/php/docker-php-ext-redis.ini /usr/local/etc/php/conf.d/docker-php-ext-redis.ini

# -- apache rewrite

RUN a2enmod ssl && a2enmod rewrite; \
    mkdir -p /etc/apache2/ssl

# -- apache SSL

COPY ./Files/ssl/*.pem /etc/apache2/ssl/
COPY ./Files/apache/sites-available/*.conf /etc/apache2/sites-available/

# -- WordPress , gets copies to apache root /var/www/html
COPY ./Files/wordpress/ /usr/src/wordpress/

# -- php xdebug

RUN pecl channel-update pecl.php.net
RUN pecl install xdebug \
    && docker-php-ext-enable xdebug

# -- Standard WordPress Env Vars

ENV WORDPRESS_DB_USER="blah_blah_user"
ENV WORDPRESS_DB_NAME="blah_blah_database"
ENV WORDPRESS_TABLE_PREFIX="wp_"
ENV WORDPRESS_DB_CHARSET="utf8"
ENV WORDPRESS_DB_COLLATE=""

Configure The Docker Container

Update Docker Composer and ECS Task Definitions

Docker Composer is for local development container setup. ECS Task definitions are for AWS Cloud Elastic Container Services.

For our local Docker Composer configuration we use a docker-compose secrets file that is not committed to our repository for setting sensitive environment variables.

In this example the PHP_SESSION_* environment variables are read by the PHP startup and substituted in the session.* variables.

./Docker/Composers/Secrets/docker-compose-secrets.yml

This configuration uses local file based session storage. This is what you’d use on a typical single-server development file.

services:
wp:
environment:
PHP_SESSION_SAVE_HANDLER: 'files'
PHP_SESSION_SAVE_PATH: ''

For a PHP connection to a cluster, like we have on our AWS fault-tolerant container clusters you and fault-tolerant ElastiCache clusters you need to set something similar in the Task Definition environment variables using the same names as above.

      PHP_SESSION_SAVE_HANDLER: 'redis'
PHP_SESSION_SAVE_PATH: 'tcp://blah-saas-staging.blah.blah.blah.amazonaws.com:6379?persistent=1&failover=1&timeout=2&read_timeout=2&serialize=php&cluster=redis'

Load Balancer Sticky Sessions Option

Configure your Application Load Balancer (or Elastic Load Balancer) to enable sticky sessions to reduce the need to share session data across containers. Sticky sessions ensure that a user is always directed to the same container instance during their session.

– Application Load Balancer: Enable Session Stickiness.
– Set a **duration-based stickiness** cookie to control how long the user remains connected to the same task/container.

**Note**: Sticky sessions are not ideal for auto-scaling environments or when maintaining container independence is critical, so this should complement, not replace, shared session storage.

Additional Considerations

1. **Security**:
– Encrypt session data in transit using TLS (especially when connecting to Redis or RDS).
– Ensure that only trusted ECS tasks and resources can access session storage by restricting permissions through IAM roles and security groups.

2. **Performance Tuning**:
– Cache session data effectively using low TTLs for Redis or Memcached.
– Monitor ElastiCache or RDS instance performance to prevent bottlenecks caused by session sharing.

3. **Scaling and Resilience**:
– Use multi-AZ configurations for Redis or RDS.
– Consider Redis Cluster for read/write scaling and high availability.

By offloading session management to centralized storage and using ECS best practices, your WordPress instances can efficiently share session information while scaling seamlessly.

Tweaking The Configuration

The cluster is not working exactly as expected.

One container will connect and appears to work properly, but the user experience will swap form a logged in page to a not logged in page mid-session. The assumption is that this is due to the user connection jumping to a different server in the container cluster.

Attempted Resolution: Set PHP session.save_handler to rediscluster

On the staging server the initial php session_save handler (set via environment variable) was set to redis.

Changing this to rediscluster did not change the session switching behavior.

Attempted Resolution: Revise the PHP session_start() call

In WordPress the session_start() was moved from the prior invocation in the WordPress init() hook to the muplugins_loaded hook which loads earlier in the process. This did not seem to have an impact on the issue. Some minor updates to deal with configurations using a Redis Cluster and not were made as well as ensuring we check if a session was already started.

Our Redis Cluster code, invoked during muplugins_loaded with a MySLP_RedisCluster::get_instance() call.

<?php
defined( 'MYSLP_VERSION' ) || exit;


/**
 *
 */
class RedisClusterSessionHandler implements SessionHandlerInterface {
	private $redis;

	public function __construct() {
		$redisClusterEndpoint = get_cfg_var( 'session.save_path' );
		if ( empty( $redisClusterEndpoint ) ) {
			throw new RuntimeException( 'No Redis Cluster endpoint configured' );
		}


		// Parse and extract host/port (handle both single node and cluster)
		$parsedUrl = parse_url( $redisClusterEndpoint );
		$redisHost = $parsedUrl['host'] ?? 'localhost';
		$redisPort = $parsedUrl['port'] ?? 6379;

		// Use an array format required by RedisCluster
		$redisClusterNodes = [ "$redisHost:$redisPort" ];

		try {
			// Initialize RedisCluster
			$this->redis = new RedisCluster( null, $redisClusterNodes, 5, 5, true );
		} catch ( RedisClusterException $e ) {
			throw new RuntimeException( 'Failed to connect to Redis Cluster: ' . $e->getMessage() );
		}

	}

	/**
	 * Initialize session
	 * @link https://php.net/manual/en/sessionhandlerinterface.open.php
	 *
	 * @param $savePath
	 * @param $sessionName
	 *
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function open( $savePath, $sessionName ): bool {
		return true; // No need to do anything here
	}

	/**
	 * Close the session
	 * @link https://php.net/manual/en/sessionhandlerinterface.close.php
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function close(): bool {
		return true; // No need to close anything explicitly
	}

	/**
	 * Read session data
	 * @link https://php.net/manual/en/sessionhandlerinterface.read.php
	 *
	 * @param $sessionId
	 *
	 * @return string <p>
	 * Returns an encoded string of the read data.
	 * If nothing was read, it must return false.
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function read( $sessionId ): string {
		$sessionData = $this->redis->get( "PHPREDIS_SESSION:$sessionId" );

		return $sessionData ?: ''; // Return session data or empty string if not found
	}

	/**
	 * Write session data
	 * @link https://php.net/manual/en/sessionhandlerinterface.write.php
	 *
	 * @param $sessionId
	 * @param string $data <p>
	 * The encoded session data. This data is the
	 * result of the PHP internally encoding
	 * the $_SESSION superglobal to a serialized
	 * string and passing it as this parameter.
	 * Please note sessions use an alternative serialization method.
	 * </p>
	 *
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function write( $sessionId, $data ): bool {
		return $this->redis->setex( "PHPREDIS_SESSION:$sessionId", 3600, $data ); // 1-hour TTL
	}

	/**
	 * Destroy a session
	 * @link https://php.net/manual/en/sessionhandlerinterface.destroy.php
	 *
	 * @param $sessionId
	 *
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function destroy( $sessionId ): bool {
		return $this->redis->del( [ "PHPREDIS_SESSION:$sessionId" ] ) > 0;
	}

	/**
	 * Cleanup old sessions
	 * @link https://php.net/manual/en/sessionhandlerinterface.gc.php
	 *
	 * @param $maxLifetime
	 *
	 * @return int|false <p>
	 * Returns the number of deleted sessions on success, or false on failure. Prior to PHP version 7.1, the function returned true on success.
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function gc( $maxLifetime ): int|false {
		return true; // Redis handles expiration via TTL, so no need to do anything
	}
}

/**
 *
 */
class MySLP_RedisCluster extends MySLP_Base {
	private $redis;

	/**
	 * Catch cluster redirects (MOVED) using the built-in PHP RedisCluster lib
	 * @return void
	 * @throws RedisClusterException
	 */
	final function initialize() {
		$redisClusterEndpoint = get_cfg_var( 'session.save_path' );
		if ( class_exists( 'RedisCluster' ) && ! empty( $redisClusterEndpoint ) ) {
			try {
				$handler = new RedisClusterSessionHandler();
				session_set_save_handler( $handler, true );

			} catch ( RuntimeException $e ) {
				error_log( 'Error initializing RedisClusterSessionHandler: ' . $e->getMessage() );
			}
		}
		if ( ! session_id() && ! headers_sent() ) {
			session_start();
		}
	}
}

SaaS WP Login Processing

  • wp-login.php
    • $reauth = empty( $_REQUEST[‘reauth’] ) ? false : true; is set to false.
    • $user = wp_signon( array() , $secure_cookie “” )
      • do_action( ‘wp_login’ , $user->user_login “lcleveland” , $user “WP_User” is set)
    • if ( empty( $_COOKIE[ LOGGED_IN_COOKIE ] )) is NOT empty
      • LOGGED_IN_COOKIE is something like “wordpress_logged_in_e2ec4afff4940eebb6cd200cc8206825”
        which IS set on this session
    • $requested_redirect_to ==> ‘https://staging.storelocatorplus.com/wp-admin/”
      as set in $_REQUEST[‘redirect_to’]
    • if ( ! is_wp_error( $user ) && ! $reauth ) { // This is executing because user is set and reauth is not set.

Need to set the WP Secrets the same (keys and salts) on ALL nodes in the cluster that share login. The auth (login) cookies have salt and keys in them and with each server generating their own they will not be validated.

Docker has a method to pass these in via an ENV setting.

Image by Robert from Pixabay