diff --git a/.distignore b/.distignore
index 85b14e6..8def903 100644
--- a/.distignore
+++ b/.distignore
@@ -1,24 +1,23 @@
.git
.github
-.agent
-.claude
.editorconfig
-.env
-.env.example
.gitignore
.distignore
+.dockerignore
Dockerfile.test
docker-compose.yml
phpcs.xml.dist
phpunit.xml.dist
composer.json
-composer.lock
vendor/
tests/
bin/
build/
demo/
node_modules/
+package.sh
+package.json
+pnpm-lock.yaml
CONTRIBUTING.md
CONFIGURATION.md
.wordpress-org/
diff --git a/betterlytics.php b/betterlytics.php
index 5e98a30..f4d891c 100644
--- a/betterlytics.php
+++ b/betterlytics.php
@@ -12,7 +12,7 @@
* Plugin URI: https://github.com/betterlytics/betterlytics-wordpress
* Description: Privacy-first analytics for WordPress. Automatically adds the Betterlytics tracking script and provides easy WordPress action hooks integration.
* Version: 1.0.0
- * Requires at least: 5.0
+ * Requires at least: 6.3
* Requires PHP: 7.4
* Author: Betterlytics
* Author URI: https://betterlytics.io
diff --git a/includes/class-betterlytics.php b/includes/class-betterlytics.php
index e60c4bc..4e37c99 100644
--- a/includes/class-betterlytics.php
+++ b/includes/class-betterlytics.php
@@ -125,8 +125,8 @@ private function define_admin_hooks() {
private function define_public_hooks() {
$plugin_public = new Betterlytics_Public( $this->get_plugin_name(), $this->get_version() );
- $this->loader->add_action( 'wp_head', $plugin_public, 'inject_tracking_script', 1 );
- $this->loader->add_action( 'wp_footer', $plugin_public, 'output_queued_events', 99 );
+ $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'inject_tracking_script', 1 );
+ $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'output_queued_events', 99 );
$this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_event_scripts' );
// Initialize custom hooks integration.
diff --git a/public/class-betterlytics-public.php b/public/class-betterlytics-public.php
index 0e7fbd0..68d0681 100644
--- a/public/class-betterlytics-public.php
+++ b/public/class-betterlytics-public.php
@@ -7,6 +7,10 @@
* @since 1.0.0
*/
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
/**
* Public-facing functionality.
*/
@@ -39,14 +43,15 @@ public function __construct( $plugin_name, $version ) {
}
/**
- * Inject the Betterlytics tracking script.
+ * Enqueue the Betterlytics tracking script.
*
* @since 1.0.0
*/
public function inject_tracking_script() {
+ $options = Betterlytics_Options::get_options();
+
// Debug: Log tracking check.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
- $options = Betterlytics_Options::get_options();
$tracking_status = Betterlytics_Options::is_tracking_enabled() ? 'ENABLED' : 'DISABLED';
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug only.
error_log( '[Betterlytics] Tracking status: ' . $tracking_status );
@@ -58,41 +63,74 @@ public function inject_tracking_script() {
return;
}
- $options = Betterlytics_Options::get_options();
- $site_id = esc_attr( $options['site_id'] );
- $server_url = esc_url( $options['server_url'] );
- $script_url = esc_url( $options['script_url'] );
- $track_outbound = esc_attr( $options['track_outbound']['mode'] );
- $web_vitals = ! empty( $options['track_web_vitals'] ) ? 'true' : 'false';
+ $script_url = $options['script_url'];
- // Debug: Output configuration as HTML comment.
+ // Validate script URL before enqueueing.
+ if ( empty( $script_url ) || ! filter_var( $script_url, FILTER_VALIDATE_URL ) ) {
+ return;
+ }
+
+ // Register and enqueue the external tracking script with async strategy (WP 6.3+).
+ wp_enqueue_script(
+ 'betterlytics-tracker',
+ $script_url,
+ [],
+ // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- External script URL; version managed by remote server.
+ null,
+ [
+ 'strategy' => 'async',
+ 'in_footer' => false,
+ ]
+ );
+
+ // Add filter for data-* attributes.
+ add_filter( 'script_loader_tag', [ $this, 'add_tracker_script_attributes' ], 10, 2 );
+
+ // Build inline script for event queue buffer.
+ $inline_script = 'window.betterlytics = window.betterlytics || { event: function() { (window.betterlytics.q = window.betterlytics.q || []).push(arguments); } };';
+
+ // Add debug logging if WP_DEBUG is enabled.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
- echo "\n";
- echo '\n";
- echo '\n";
- echo '\n";
+ $site_id = esc_js( $options['site_id'] );
+ $server_url = esc_js( $options['server_url'] );
+ $script_url_js = esc_js( $script_url );
+ $track_outbound = esc_js( $options['track_outbound']['mode'] );
+
+ $inline_script .= "\nconsole.log('[Betterlytics] Script injected', { siteId: '{$site_id}', serverUrl: '{$server_url}', scriptUrl: '{$script_url_js}', trackOutbound: '{$track_outbound}' });";
}
- // Output async event queue for reliable tracking before script loads.
- ?>
-
-
-
- .
+ $tag = str_replace( '>', $data_attrs . '>', $tag );
+
+ return $tag;
}
/**
@@ -112,42 +150,44 @@ public function enqueue_event_scripts() {
return;
}
- wp_enqueue_script(
- 'betterlytics-events',
- BETTERLYTICS_PLUGIN_URL . 'public/js/betterlytics-events.js',
- [],
- $this->version,
- true
- );
-
- wp_localize_script(
+ wp_enqueue_script(
'betterlytics-events',
- 'betterlyticsEvents',
- [
- 'trackDownloads' => ! empty( $options['track_downloads']['enabled'] ),
- 'trackCustomHtmlAttr' => ! empty( $options['track_custom_html_attribute']['enabled'] ),
- ]
+ BETTERLYTICS_PLUGIN_URL . 'public/js/betterlytics-events.js',
+ [],
+ $this->version,
+ true
);
+
+ wp_localize_script(
+ 'betterlytics-events',
+ 'betterlyticsEvents',
+ [
+ 'trackDownloads' => ! empty( $options['track_downloads']['enabled'] ),
+ 'trackCustomHtmlAttr' => ! empty( $options['track_custom_html_attribute']['enabled'] ),
+ ]
+ );
}
/**
- * Output queued events as JavaScript.
+ * Output queued events as inline JavaScript.
*
* @since 1.0.0
*/
public function output_queued_events() {
global $betterlytics_queued_events;
- if ( empty( $betterlytics_queued_events ) ) {
+ if ( empty( $betterlytics_queued_events ) || ! wp_script_is( 'betterlytics-tracker', 'enqueued' ) ) {
return;
}
- echo "\n";
+
+ // Add queued events as inline script AFTER the main tracking script.
+ wp_add_inline_script( 'betterlytics-tracker', implode( "\n", $script_lines ), 'after' );
}
}
diff --git a/readme.txt b/readme.txt
index 90935c6..95bf5b0 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,7 +1,7 @@
=== Betterlytics ===
Contributors: betterlytics
Tags: analytics, privacy, gdpr, cookieless, statistics
-Requires at least: 5.0
+Requires at least: 6.3
Tested up to: 6.9
Stable tag: 1.0.0
Requires PHP: 7.4
diff --git a/tests/test-public.php b/tests/test-public.php
index 709f276..21c10f3 100644
--- a/tests/test-public.php
+++ b/tests/test-public.php
@@ -25,10 +25,14 @@ class Test_Betterlytics_Public extends Betterlytics_Test_Case {
public function set_up() {
parent::set_up();
$this->public = new Betterlytics_Public( 'betterlytics', '1.0.0' );
+
+ // Reset WordPress scripts for each test.
+ global $wp_scripts;
+ $wp_scripts = null;
}
/**
- * Test that tracking script is not output when disabled.
+ * Test that tracking script is not enqueued when disabled.
*/
public function test_script_not_output_when_disabled() {
$this->set_options(
@@ -38,15 +42,13 @@ public function test_script_not_output_when_disabled() {
)
);
- ob_start();
$this->public->inject_tracking_script();
- $output = ob_get_clean();
- $this->assertStringNotContainsString( '';
+ $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' );
+
+ $this->assertStringContainsString( 'data-web-vitals="true"', $result );
}
/**
@@ -201,11 +246,13 @@ public function test_web_vitals_disabled() {
)
);
- ob_start();
$this->public->inject_tracking_script();
- $output = ob_get_clean();
- $this->assertStringContainsString( 'data-web-vitals="false"', $output );
+ // Check the data attributes via the filter method.
+ $test_tag = '';
+ $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' );
+
+ $this->assertStringContainsString( 'data-web-vitals="false"', $result );
}
/**
@@ -220,11 +267,13 @@ public function test_outbound_links_domain_mode() {
)
);
- ob_start();
$this->public->inject_tracking_script();
- $output = ob_get_clean();
- $this->assertStringContainsString( 'data-outbound-links="domain"', $output );
+ // Check the data attributes via the filter method.
+ $test_tag = '';
+ $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' );
+
+ $this->assertStringContainsString( 'data-outbound-links="domain"', $result );
}
/**
@@ -239,11 +288,13 @@ public function test_outbound_links_full_mode() {
)
);
- ob_start();
$this->public->inject_tracking_script();
- $output = ob_get_clean();
- $this->assertStringContainsString( 'data-outbound-links="full"', $output );
+ // Check the data attributes via the filter method.
+ $test_tag = '';
+ $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' );
+
+ $this->assertStringContainsString( 'data-outbound-links="full"', $result );
}
/**
@@ -258,10 +309,30 @@ public function test_outbound_links_off_mode() {
)
);
- ob_start();
$this->public->inject_tracking_script();
- $output = ob_get_clean();
- $this->assertStringContainsString( 'data-outbound-links="off"', $output );
+ // Check the data attributes via the filter method.
+ $test_tag = '';
+ $result = $this->public->add_tracker_script_attributes( $test_tag, 'betterlytics-tracker' );
+
+ $this->assertStringContainsString( 'data-outbound-links="off"', $result );
+ }
+
+ /**
+ * Test that filter doesn't modify other script handles.
+ */
+ public function test_filter_ignores_other_handles() {
+ $this->set_options(
+ array(
+ 'enabled' => true,
+ 'site_id' => 'test-site',
+ )
+ );
+
+ $test_tag = '';
+ $result = $this->public->add_tracker_script_attributes( $test_tag, 'other-script' );
+
+ // Should return unchanged.
+ $this->assertEquals( $test_tag, $result );
}
}