From 29f01b65ce47348866924ec01be2a6eb5ec34441 Mon Sep 17 00:00:00 2001 From: WardLavrijsen <70542124+WardLavrijsen@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:49:25 +0200 Subject: [PATCH 1/2] Proxy call tracking assignments through the first-party WordPress plugin. Add an optional call tracking setting and forward DNI requests so website numbers stay first-party. Co-authored-by: Cursor --- README.md | 9 ++++- includes/class-octanist-api.php | 34 ++++++++++++++++ includes/class-octanist-rest.php | 51 ++++++++++++++++++++++++ includes/class-octanist-settings.php | 59 ++++++++++++++++++++++++++-- octanist.php | 6 +-- readme.txt | 8 +++- 6 files changed, 159 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a5d606c..7cf3776 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Tags:** tracking, analytics, forms, leads, conversions **Requires at least:** 6.0 **Tested up to:** 7.0 -**Stable tag:** 4.0.1 +**Stable tag:** 4.1.0 **License:** GPLv2 or later **License URI:** https://www.gnu.org/licenses/gpl-2.0.html @@ -20,6 +20,7 @@ The plugin acts as a first-party proxy for the Octanist pixel: - **Pixel proxy:** The tracking script is served from your own WordPress site at `/wp-json/oct/p` from a local cache, with refreshes handled in the background. - **Event proxy:** Events from the pixel POST to `/wp-json/oct/e`; the plugin forwards them immediately with a one-second upstream timeout so onboarding and live reporting stay responsive. Pixel failures are not persisted in WordPress, which prevents an upstream outage from filling the site database. +- **Call tracking proxy:** The pixel can request a dynamic number at `/wp-json/oct/call-tracking/assign`. The plugin forwards that request to Octanist so website numbers stay first-party. - **Server-side form capture:** Form submissions from supported plugins are captured via server-side action hooks and forwarded synchronously with a longer timeout. Confirmed failures are queued for retry. Supported form plugins: @@ -46,6 +47,12 @@ Listener mode and consent mode are optional settings. ## Changelog +### 4.1.0 + +- Added an optional Call tracking setting, off by default. +- Added a first-party proxy for call tracking assignment (`POST /wp-json/oct/call-tracking/assign`). +- Setup codes can include a call tracking flag (`OCTA1.OCT-XXXXXXXX.s.a.t`). + ### 4.0.1 - Pixel serving now uses the local cache immediately and refreshes upstream in the background. diff --git a/includes/class-octanist-api.php b/includes/class-octanist-api.php index b81abb8..a48c4e9 100644 --- a/includes/class-octanist-api.php +++ b/includes/class-octanist-api.php @@ -8,8 +8,10 @@ class Octanist_Api const COOKIE_PREFIX = 'octa_'; const PIXEL_PATH = '/p'; const EVENT_PATH = '/e'; + const ASSIGN_PATH = '/call-tracking/assign'; const PIXEL_TIMEOUT = 1; const COLLECT_TIMEOUT = 1; + const ASSIGN_TIMEOUT = 8; const FORM_TIMEOUT = 10; const RETRY_TIMEOUT = 3; const FORWARD_TIMEOUT = 1; @@ -145,6 +147,38 @@ public static function forward_event(array $payload, array $opts = []) return $response; } + /** + * Forward a DNI assignment request to the upstream /call-tracking/assign endpoint. + * Blocking: the pixel needs the leased number in the same request. + */ + public static function forward_call_tracking_assignment(array $payload, array $signals = []) + { + $headers = [ + 'Content-Type' => 'application/json', + ]; + + if (!empty($signals['ip'])) { + $headers['X-Forwarded-For'] = $signals['ip']; + $headers['X-Octanist-Client-IP'] = $signals['ip']; + } + if (!empty($signals['country'])) { + $headers['X-Octanist-Client-Country'] = $signals['country']; + } + if (!empty($signals['ua'])) { + $headers['X-Octanist-Client-UA'] = $signals['ua']; + } + + return wp_remote_post(OCTANIST_UPSTREAM . self::ASSIGN_PATH, [ + 'method' => 'POST', + 'timeout' => self::ASSIGN_TIMEOUT, + 'redirection' => 0, + 'blocking' => true, + 'headers' => $headers, + 'body' => wp_json_encode($payload), + 'limit_response_size' => 4096, + ]); + } + public static function pixel_delivery_is_paused(): bool { return (bool) get_transient(self::PIXEL_CIRCUIT_TRANSIENT); diff --git a/includes/class-octanist-rest.php b/includes/class-octanist-rest.php index f368d50..d5869aa 100644 --- a/includes/class-octanist-rest.php +++ b/includes/class-octanist-rest.php @@ -8,6 +8,7 @@ class Octanist_Rest const NAMESPACE = 'oct'; const PIXEL = 'p'; const COLLECT = 'e'; + const ASSIGN = 'call-tracking/assign'; const MAX_BODY = 65536; // 64 KB public static function register(): void @@ -28,6 +29,12 @@ public static function register_routes(): void 'callback' => [__CLASS__, 'collect'], 'permission_callback' => '__return_true', ]); + + register_rest_route(self::NAMESPACE, '/' . self::ASSIGN, [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'assign'], + 'permission_callback' => '__return_true', + ]); } public static function pixel_url(): string @@ -169,6 +176,50 @@ public static function collect(WP_REST_Request $request) return $rest_response; } + public static function assign(WP_REST_Request $request) + { + $raw = $request->get_body(); + if (strlen($raw) > self::MAX_BODY) { + return self::assignment_unavailable(); + } + + $payload = json_decode($raw, true); + if (!is_array($payload)) { + return new WP_REST_Response(null, 400); + } + + $settings = Octanist_Settings::get(); + if (!empty($settings['measurement_id']) && empty($payload['mid'])) { + $payload['mid'] = $settings['measurement_id']; + } + + $signals = Octanist_Api::collect_client_signals(); + $response = Octanist_Api::forward_call_tracking_assignment($payload, $signals); + + if (is_wp_error($response)) { + return self::assignment_unavailable(); + } + + $code = (int) wp_remote_retrieve_response_code($response); + $body = json_decode((string) wp_remote_retrieve_body($response), true); + if (!is_array($body)) { + return self::assignment_unavailable(); + } + + return new WP_REST_Response($body, $code >= 200 && $code < 600 ? $code : 200); + } + + private static function assignment_unavailable(): WP_REST_Response + { + return new WP_REST_Response([ + 'success' => false, + 'enabled' => false, + 'available' => false, + 'phoneNumber' => null, + 'reason' => 'assignment_unavailable', + ], 200); + } + private static function send_headers(array $headers): void { foreach ($headers as $name => $value) { diff --git a/includes/class-octanist-settings.php b/includes/class-octanist-settings.php index e9ba738..1560259 100644 --- a/includes/class-octanist-settings.php +++ b/includes/class-octanist-settings.php @@ -26,6 +26,7 @@ public static function get(): array 'measurement_id' => '', 'listener_mode' => 'server', 'consent_mode' => 'auto', + 'call_tracking' => false, ]; $settings = get_option(self::OPTION, []); if (!is_array($settings)) { @@ -52,6 +53,9 @@ public static function pixel_data_attrs(): array 'data-consent-mode' => $s['consent_mode'], 'data-cookie-mode' => 'server', ]; + if (!empty($s['call_tracking'])) { + $attrs['data-call-tracking'] = 'true'; + } if ($s['listener_mode'] === 'client') { // Value-less attribute, browser pixel binds to forms itself. $attrs['data-forms'] = ''; @@ -92,6 +96,7 @@ public static function register_settings(): void 'measurement_id' => '', 'listener_mode' => 'server', 'consent_mode' => 'auto', + 'call_tracking' => false, ], ]); } @@ -116,6 +121,9 @@ public static function sanitize($input): array $input['measurement_id'] = $decoded['measurement_id']; $input['listener_mode'] = $decoded['listener_mode']; $input['consent_mode'] = $decoded['consent_mode']; + if (array_key_exists('call_tracking', $decoded)) { + $input['call_tracking'] = $decoded['call_tracking']; + } add_settings_error( self::OPTION, @@ -149,6 +157,7 @@ public static function sanitize($input): array 'measurement_id' => $mid, 'listener_mode' => $listener, 'consent_mode' => $consent, + 'call_tracking' => !empty($input['call_tracking']), ]; } @@ -166,7 +175,7 @@ private static function decode_setup_code(string $code) } $parts = explode('.', $code); - if (count($parts) !== 4) { + if (count($parts) !== 4 && count($parts) !== 5) { return new WP_Error( 'octanist_setup_code_shape', __('Setup code should look like OCTA1.OCT-XXXXXXXX.s.a.', 'octanist') @@ -211,11 +220,24 @@ private static function decode_setup_code(string $code) ); } - return [ + $decoded = [ 'measurement_id' => $measurement_id, 'listener_mode' => $listener_mode, 'consent_mode' => $consent_mode, ]; + + if (isset($parts[4])) { + $call_code = sanitize_key((string) $parts[4]); + if (!in_array($call_code, ['t', 'n'], true)) { + return new WP_Error( + 'octanist_setup_code_call_tracking', + __('Setup code contains an invalid call tracking flag.', 'octanist') + ); + } + $decoded['call_tracking'] = $call_code === 't'; + } + + return $decoded; } public static function enqueue_assets($hook): void @@ -362,6 +384,11 @@ class="octanist-input octanist-input--wide"
+
+
+
+ +
@@ -413,6 +440,18 @@ class="octanist-input" + +
+ + + +
@@ -428,7 +467,7 @@ class="octanist-input octanist-input--wide" autocomplete="off" spellcheck="false" aria-label=""> -

+

@@ -530,6 +569,20 @@ private static function format_consent_mode_help(string $mode): string return $labels[$mode] ?? $labels['auto']; } + private static function format_call_tracking($enabled): string + { + return !empty($enabled) + ? __('On', 'octanist') + : __('Off', 'octanist'); + } + + private static function format_call_tracking_help($enabled): string + { + return !empty($enabled) + ? __('The pixel can replace website phone numbers.', 'octanist') + : __('Leave this off unless Octanist enabled call tracking for this site.', 'octanist'); + } + private static function format_time($ts): string { if (empty($ts)) { diff --git a/octanist.php b/octanist.php index ca6aad1..85659f3 100644 --- a/octanist.php +++ b/octanist.php @@ -1,8 +1,8 @@ Date: Tue, 1 Sep 2026 09:49:30 +0200 Subject: [PATCH 2/2] Release Octanist WordPress plugin 4.1.0 Publish call tracking support and mark the plugin tested up to WordPress 7.1. Co-authored-by: Cursor --- README.md | 3 ++- languages/octanist.pot | 2 +- octanist.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- readme.txt | 3 ++- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7cf3776..6c54e2f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Contributors:** octanist **Tags:** tracking, analytics, forms, leads, conversions **Requires at least:** 6.0 -**Tested up to:** 7.0 +**Tested up to:** 7.1 **Stable tag:** 4.1.0 **License:** GPLv2 or later **License URI:** https://www.gnu.org/licenses/gpl-2.0.html @@ -52,6 +52,7 @@ Listener mode and consent mode are optional settings. - Added an optional Call tracking setting, off by default. - Added a first-party proxy for call tracking assignment (`POST /wp-json/oct/call-tracking/assign`). - Setup codes can include a call tracking flag (`OCTA1.OCT-XXXXXXXX.s.a.t`). +- Tested up to WordPress 7.1. ### 4.0.1 diff --git a/languages/octanist.pot b/languages/octanist.pot index e2f38d7..e194c4a 100644 --- a/languages/octanist.pot +++ b/languages/octanist.pot @@ -2,7 +2,7 @@ # This file is distributed under the same license as the Octanist plugin. msgid "" msgstr "" -"Project-Id-Version: Octanist 4.0.1\n" +"Project-Id-Version: Octanist 4.1.0\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/octanist\n" "POT-Creation-Date: 2026-06-08 08:31+0000\n" "MIME-Version: 1.0\n" diff --git a/octanist.php b/octanist.php index 85659f3..fe13561 100644 --- a/octanist.php +++ b/octanist.php @@ -9,7 +9,7 @@ * License: GPLv2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Requires at least: 6.0 - * Tested up to: 7.0 + * Tested up to: 7.1 */ if (!defined('ABSPATH')) { diff --git a/package-lock.json b/package-lock.json index d25d3cd..158f584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "octanist-wp-plugin", - "version": "4.0.1", + "version": "4.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "octanist-wp-plugin", - "version": "4.0.1", + "version": "4.1.0", "devDependencies": { "@wordpress/env": "^10.0.0" } diff --git a/package.json b/package.json index c5117cf..951723d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "octanist-wp-plugin", "private": true, - "version": "4.0.1", + "version": "4.1.0", "description": "Development tooling for the Octanist WordPress plugin.", "scripts": { "start": "wp-env start", diff --git a/readme.txt b/readme.txt index 239d609..a2ec859 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: octanist Tags: tracking, analytics, forms, leads, conversions Requires at least: 6.0 -Tested up to: 7.0 +Tested up to: 7.1 Stable tag: 4.1.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -66,6 +66,7 @@ Pixel events are forwarded immediately through the first-party WordPress endpoin * **NEW:** Optional call tracking setting (off by default). When enabled, the pixel can replace website phone numbers. * **NEW:** First-party proxy for call tracking assignment (`POST /wp-json/oct/call-tracking/assign`). * **NEW:** Setup codes can include a call tracking flag (`OCTA1.OCT-XXXXXXXX.s.a.t`). +* **COMPATIBILITY:** Tested up to WordPress 7.1. = 4.0.1 = * **PERFORMANCE:** The pixel endpoint serves its local cache immediately and refreshes Octanist upstream in the background.