From 4c2e528678d6ddbbc11a394ca3721a980a0e9fe6 Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 10:29:05 +0500 Subject: [PATCH 01/13] New. Code. IMetric service. Integrations performance metrics. --- .../IntegrationMetrics/IMetricDTO.php | 73 +++++++ .../IntegrationMetrics/IMetricDTOTrait.php | 37 ++++ .../IntegrationMetrics/IMetricService.php | 201 ++++++++++++++++++ lib/Cleantalk/Antispam/Integrations.php | 24 ++- .../Antispam/Integrations/IntegrationBase.php | 3 + .../Antispam/IntegrationsByClass.php | 14 ++ .../IntegrationByClassBase.php | 4 + 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php create mode 100644 lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php create mode 100644 lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php new file mode 100644 index 000000000..4a4d94f7b --- /dev/null +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php @@ -0,0 +1,73 @@ +getArray()); + } + + public function getArray() + { + $skip_properties = array( + 'is_released', + 'peak_memory_on_start_kb', + 'memory_usage_on_start_kb', + 'timer_on_start_msec', + 'SENDER_INFO_KEY' + ); + return array_map(function ($value) { + return $value; + }, array_diff_key(get_object_vars($this), array_flip($skip_properties))); + } + } diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php new file mode 100644 index 000000000..bce62c63f --- /dev/null +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php @@ -0,0 +1,37 @@ +imetric_dto = $imetric_dto; + if (isset($this->imetric_dto_version)) { + $this->imetric_dto->dto_version = $this->imetric_dto_version; + } + } + + /** + * @return IMetricDTO|null + */ + public function getIMetricDTO(): ?IMetricDTO + { + return $this->imetric_dto; + } + } diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php new file mode 100644 index 000000000..66398c0e1 --- /dev/null +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -0,0 +1,201 @@ +dto_version = $dto_version; + $full_class = get_class($integration); + $dto->integration_name = substr($full_class, strrpos($full_class, '\\') + 1); + self::startGlobalSeeking($dto); + return $dto; + } + return false; + } + + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * + * @return string|false + */ + private static function getDTOVersion($integration) + { + return $integration->imetric_dto_version ?? false; + } + + /** + * @param IMetricDTO $dto + * + * @return void + */ + private static function startGlobalSeeking(IMetricDTO $dto) + { + $dto->timer_on_start_msec = self::getCurrentTimeMS(); + $dto->memory_usage_on_start_kb = self::getCurrentMemoryUsageKb(); + $dto->peak_memory_on_start_kb = self::getPeakMemoryUsageKb(); + } + + /** + * @return float + */ + private static function getCurrentTimeMS() + { + return round(microtime(true) * 1000, 1); + } + + /** + * @return float + */ + private static function getCurrentMemoryUsageKb() + { + return round(memory_get_usage() / 1024); + } + + /** + * @return float + */ + private static function getPeakMemoryUsageKb() + { + return round(memory_get_peak_usage() / 1024); + } + + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * @param string $span_name + * + * @return void + */ + public static function seek($integration, string $span_name = 'undefined_span') + { + if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { + $dto = $integration->getImetricDTO(); + if ($dto && !$dto->is_released) { + if (!isset($dto->spans[$span_name])) { + $dto->spans[$span_name] = [ + 'time_msec' => self::getCurrentTimeMS(), + 'memory_kb' => self::getCurrentMemoryUsageKb(), + 'memory_peak_kb' => self::getPeakMemoryUsageKb(), + 'released' => false + ]; + } + } + } + } + + public static function lease($integration, string $span_name = 'undefined_span') + { + if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { + $dto = $integration->getImetricDTO(); + if ($dto && !$dto->is_released) { + $dto->spans[$span_name] = self::releaseSpan($dto->spans[$span_name]); + } + } + } + + /** + * @param IMetricDTO $dto + * + * @return void + */ + public static function releaseAllSpans(IMetricDTO $dto) + { + foreach ($dto->spans as $_span_name => &$span_content) { + $span_content = self::releaseSpan($span_content); + } + } + + /** + * @param array $span_content + * + * @return array + */ + private static function releaseSpan(array $span_content) + { + if ( + isset( + $span_content['released'], + $span_content['time_msec'], + $span_content['memory_kb'], + $span_content['memory_peak_kb'] + ) + ) { + if (!$span_content['released']) { + $span_content['time_msec'] = self::getCurrentTimeMS() - $span_content['time_msec']; + $span_content['memory_kb'] = self::getCurrentMemoryUsageKb() - $span_content['memory_kb']; + $span_content['memory_peak_kb'] = self::getPeakMemoryUsageKb() - $span_content['memory_peak_kb']; + $span_content['released'] = true; + } + } + return $span_content; + } + + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * @return string + */ + public static function finalizeDTO($integration) + { + $out = false; + $dto = $integration->getImetricDTO(); + if ($dto && !$dto->is_released) { + $dto->peak_memory_diff_kb = self::getPeakMemoryUsageKb() - $dto->peak_memory_on_start_kb; + $dto->total_exec_time_ms = self::getCurrentTimeMS() - $dto->timer_on_start_msec; + self::releaseAllSpans($dto); + $dto->is_released = true; + $out = $dto->getJSON(); + } + if (!$out) { + $out = new IMetricDTO(); + $out = $out->getJSON(); + } + return $out; + } + + /** + * @param IMetricDTO|null $dto + * @param string $field_name + * @param mixed $field_value + */ + public static function setCustomField($dto = null, $field_name = 'field', $field_value = null) + { + $dto && !$dto->is_released && $dto->custom_fields[$field_name] = $field_value; + } + + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * @param array $vars + * @param string $span + */ + public static function dumpVarsSize($integration, array $vars = [], string $span = 'undefined_variable_span'): void { + $dto = $integration->getImetricDTO(); + if (!$dto || empty($vars)) { + return; + } + $max_var_size_kb = 0; + foreach ($vars as $_name => $value) { + try { + $size = strlen(serialize($value)); + $current_size = round($size / 1024, 1); + if ($current_size > $max_var_size_kb) { + $max_var_size_kb = $current_size; + } + } catch (\Exception $e) { + // is unserializable, skip + } + } + $dto->variable_peak_kb[$span] = $max_var_size_kb; + } + } diff --git a/lib/Cleantalk/Antispam/Integrations.php b/lib/Cleantalk/Antispam/Integrations.php index dd84e6283..ffbf736d0 100644 --- a/lib/Cleantalk/Antispam/Integrations.php +++ b/lib/Cleantalk/Antispam/Integrations.php @@ -2,7 +2,10 @@ namespace Cleantalk\Antispam; +use Cleantalk\Antispam\IntegrationMetrics\IMetricDTO; +use Cleantalk\Antispam\Integrations\IntegrationBase; use Cleantalk\ApbctWP\Variables\Server; +use Cleantalk\Antispam\IntegrationMetrics\IMetricService; class Integrations { @@ -91,7 +94,7 @@ public function checkSpam($argument, $set_current_integration = '') $class = '\\Cleantalk\\Antispam\\Integrations\\' . $current_integration; if ( class_exists($class) ) { $integration = new $class(); - if ( ! ($integration instanceof \Cleantalk\Antispam\Integrations\IntegrationBase) ) { + if ( ! ($integration instanceof IntegrationBase) ) { // @ToDo have to handle an error do_action( 'apbct_skipped_request', @@ -101,11 +104,22 @@ public function checkSpam($argument, $set_current_integration = '') return true; } + /** + * @var IMetricDTO $imetric_dto + */ + $imetric_dto = IMetricService::getDTO($integration); + /** + * @var IntegrationBase $integration + */ + if ($imetric_dto) { + $integration->setIMetricDTO($imetric_dto); + } /** * Run prepare actions. */ $prepare_actions_result = $integration->doPrepareActions($argument); + if ( !is_bool($prepare_actions_result) ) { //if integration returns not a bool value on this state - exit and return modified argument return $prepare_actions_result; @@ -120,10 +134,14 @@ public function checkSpam($argument, $set_current_integration = '') * Data collection */ // If integration provided it's own method - run this + IMetricService::seek($integration, 'collectBaseCallData'); $integration_base_call_data = $integration->collectBaseCallData(); + IMetricService::lease($integration, 'collectBaseCallData'); // old way legacy + IMetricService::seek($integration, 'getDataForChecking'); $data = $integration->getDataForChecking($argument); + IMetricService::lease($integration, 'getDataForChecking'); if ( ! is_null($data) ) { /** @@ -149,6 +167,9 @@ public function checkSpam($argument, $set_current_integration = '') if ( ! empty($integration_fvd['visible_fields']) ) { $sender_info['apbct_visible_fields'] = $integration_fvd['visible_fields']; } + if ( $integration->getIMetricDTO() ) { + $sender_info[IMetricDTO::$SENDER_INFO_KEY] = IMetricService::finalizeDTO($integration); + } // common case $base_call_data = array( 'message' => ! empty($data['message']) ? json_encode($data['message']) : '', @@ -162,6 +183,7 @@ public function checkSpam($argument, $set_current_integration = '') // Page URL must be an previous page ), ); + error_log('CTDEBUG [' . __FUNCTION__ . '] [$base_call_data] ' . var_export($base_call_data,true)); } // Set registration flag - will be used to select method diff --git a/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php b/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php index 2428d6387..2b1c93cd1 100644 --- a/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php +++ b/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php @@ -2,8 +2,11 @@ namespace Cleantalk\Antispam\Integrations; +use Cleantalk\Antispam\IntegrationMetrics\IMetricDTOTrait; + abstract class IntegrationBase { + use IMetricDTOTrait; public $base_call_result; public $visible_fields_data; diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass.php b/lib/Cleantalk/Antispam/IntegrationsByClass.php index af61d353d..f4056dae2 100755 --- a/lib/Cleantalk/Antispam/IntegrationsByClass.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass.php @@ -2,6 +2,9 @@ namespace Cleantalk\Antispam; +use Cleantalk\Antispam\IntegrationMetrics\IMetricDTO; +use Cleantalk\Antispam\IntegrationMetrics\IMetricService; +use Cleantalk\Antispam\Integrations\IntegrationBase; use Cleantalk\Antispam\IntegrationsByClass\IntegrationByClassBase; class IntegrationsByClass @@ -50,6 +53,17 @@ public function __construct($integrations) */ $integration = new $class(); + /** + * @var IMetricDTO $imetric_dto + */ + $imetric_dto = IMetricService::getDTO($integration); + /** + * @var IntegrationBase $integration + */ + if ($imetric_dto) { + $integration->setIMetricDTO($imetric_dto); + } + // Public work if ($integration->isSkipIntegration()) { continue; diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/IntegrationByClassBase.php b/lib/Cleantalk/Antispam/IntegrationsByClass/IntegrationByClassBase.php index 8fc7cc9e5..e0b069389 100755 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/IntegrationByClassBase.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/IntegrationByClassBase.php @@ -2,8 +2,12 @@ namespace Cleantalk\Antispam\IntegrationsByClass; +use Cleantalk\Antispam\IntegrationMetrics\IMetricDTOTrait; + abstract class IntegrationByClassBase { + use IMetricDTOTrait; + /** * Do not apply actions on hooks if true; * @return bool From 6e3f1bf2521473de80fe8d34b840aa1f160476b7 Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 10:29:17 +0500 Subject: [PATCH 02/13] New. Code. IMetric service. Ninja forms ready. --- .../Antispam/Integrations/NinjaForms.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/Cleantalk/Antispam/Integrations/NinjaForms.php b/lib/Cleantalk/Antispam/Integrations/NinjaForms.php index 8d0acba4c..f23479425 100644 --- a/lib/Cleantalk/Antispam/Integrations/NinjaForms.php +++ b/lib/Cleantalk/Antispam/Integrations/NinjaForms.php @@ -2,6 +2,7 @@ namespace Cleantalk\Antispam\Integrations; +use Cleantalk\Antispam\IntegrationMetrics\IMetricService; use Cleantalk\ApbctWP\DTO\GetFieldsAnyDTO; use Cleantalk\ApbctWP\Escape; use Cleantalk\ApbctWP\GetFieldsAny; @@ -13,6 +14,7 @@ class NinjaForms extends IntegrationBase { private $sender_email = ''; // needs to provide to final actions + public $imetric_dto_version = '1.0.0'; public function getDataForChecking($argument) { global $apbct, $cleantalk_executed; @@ -44,6 +46,7 @@ public function getDataForChecking($argument) $gfa_dto = $this->getGFANew(); } catch (\Exception $_e) { // It is possible here check the reason if the new way collecting fields is not available. + IMetricService::setCustomField($this->imetric_dto, 'nf_old_gfa_used', true); $gfa_dto = $this->getGFAOld(); } @@ -63,6 +66,7 @@ public function getDataForChecking($argument) $form_data = json_decode(stripslashes(Post::getString('formData')), true); } $form_fields = $form_data['fields'] ?? array(); + IMetricService::setCustomField($this->imetric_dto, 'nf_fields_count', count($form_fields)); $gfa_dto = $this->updateEmailNicknameFromNFService($gfa_dto, $form_fields); } @@ -82,6 +86,7 @@ public function getDataForChecking($argument) $fields_visibility_data = GetFieldsAny::getVisibleFieldsData($form_data, true); $this->setVisibleFieldsData($fields_visibility_data); + IMetricService::dumpVarsSize($this, get_defined_vars(), __FUNCTION__); return $gfa_dto->getArray(); } @@ -180,6 +185,7 @@ public function doFinalActions($argument) */ public function getGFANew(): GetFieldsAnyDTO { + IMetricService::seek($this, __FUNCTION__); $form_data = json_decode(Post::getString('formData'), true); if ( ! $form_data ) { $form_data = json_decode(stripslashes(TT::toString(Post::get('formData'))), true); @@ -251,7 +257,9 @@ public function getGFANew(): GetFieldsAnyDTO } } - return ct_gfa_dto($fields, $nf_prior_email, $nickname, $nf_emails_array); + $gfa = ct_gfa_dto($fields, $nf_prior_email, $nickname, $nf_emails_array); + IMetricService::lease($this, __FUNCTION__); + return $gfa; } /** @@ -259,6 +267,7 @@ public function getGFANew(): GetFieldsAnyDTO */ public function getGFAOld(): GetFieldsAnyDTO { + IMetricService::seek($this, __FUNCTION__); /** * Filter for POST */ @@ -272,7 +281,9 @@ public function getGFAOld(): GetFieldsAnyDTO : $input_array; // Return the collected fields data - return ct_gfa_dto($input_data); + $gfa = ct_gfa_dto($input_data); + IMetricService::lease($this, __FUNCTION__); + return $gfa; } @@ -293,6 +304,7 @@ public static function hookPreventSubmission($_some, $_form_id): bool */ public function updateEmailNicknameFromNFService(GetFieldsAnyDTO $gfa_dto, array $nf_form_fields): GetFieldsAnyDTO { + IMetricService::seek($this, __FUNCTION__); if ( function_exists('Ninja_Forms') && !empty($nf_form_fields) ) { /** @psalm-suppress UndefinedFunction */ $nf_form_fields_info = Ninja_Forms()->form()->get_fields(); @@ -323,6 +335,7 @@ public function updateEmailNicknameFromNFService(GetFieldsAnyDTO $gfa_dto, array // if email is empty, fill it with data from Ninja Forms, if not empty, keep DTO $gfa_dto->email = empty($gfa_dto->email) ? $email : $gfa_dto->email; } + IMetricService::lease($this, __FUNCTION__); return $gfa_dto; } From 876f0c82ae4988668ffac5c13c27d36e75b55d82 Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 10:29:27 +0500 Subject: [PATCH 03/13] New. Code. IMetric service. Woocommerce forms ready. --- .../IntegrationsByClass/Woocommerce.php | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index bafa1e415..f6c4ac448 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -4,6 +4,8 @@ use Cleantalk\Antispam\Cleantalk; use Cleantalk\Antispam\CleantalkRequest; +use Cleantalk\Antispam\IntegrationMetrics\IMetricDTO; +use Cleantalk\Antispam\IntegrationMetrics\IMetricService; use Cleantalk\ApbctWP\Sanitize; use Cleantalk\ApbctWP\Variables\Cookie; use Cleantalk\ApbctWP\Variables\Get; @@ -36,6 +38,8 @@ class Woocommerce extends IntegrationByClassBase { private $event_token = null; + public $imetric_dto_version = '1.0.0'; + /** * @return void * @psalm-suppress PossiblyUnusedMethod @@ -177,6 +181,11 @@ public function checkoutCheck($_data, $errors) { global $apbct, $cleantalk_executed; + IMetricService::seek( + $this, + __FUNCTION__ + ); + if ( count($errors->errors) ) { return; } @@ -214,7 +223,11 @@ public function checkoutCheck($_data, $errors) 'sender_email' => $sender_email, 'sender_nickname' => $sender_nickname, 'post_info' => $post_info, - 'sender_info' => array('sender_url' => null, 'sender_emails_array' => $sender_emails_array) + 'sender_info' => array( + 'sender_url' => null, + 'sender_emails_array' => $sender_emails_array, + IMetricDTO::$SENDER_INFO_KEY => IMetricService::finalizeDTO($this) + ) ); $base_call_result = apbct_base_call($base_call_data); @@ -256,6 +269,11 @@ public function checkoutCheckFromRest($order) return; } + IMetricService::seek( + $this, + __FUNCTION__ + ); + $sender_email = $order->get_billing_email(); $sender_nickname = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(); $message = $order->get_customer_note(); @@ -269,7 +287,7 @@ public function checkoutCheckFromRest($order) 'sender_email' => $sender_email, 'sender_nickname' => $sender_nickname, 'post_info' => $post_info, - 'sender_info' => array('sender_url' => null), + 'sender_info' => array('sender_url' => null, IMetricDTO::$SENDER_INFO_KEY => IMetricService::finalizeDTO($this)), 'event_token' => $this->event_token, ); @@ -398,6 +416,11 @@ public function addToCartUnloggedUser() { global $apbct; + IMetricService::seek( + $this, + __FUNCTION__ + ); + $data = Post::get('data'); if (is_array($data) && isset($data['ct_bot_detector_event_token'])) { $event_token = $data['ct_bot_detector_event_token']; @@ -422,7 +445,7 @@ public function addToCartUnloggedUser() 'message' => $message, 'post_info' => $post_info, 'js_on' => apbct_js_test(Sanitize::cleanTextField(Cookie::get('ct_checkjs')), true), - 'sender_info' => array('sender_url' => null), + 'sender_info' => array('sender_url' => null, IMetricDTO::$SENDER_INFO_KEY => IMetricService::finalizeDTO($this)), 'exception_action' => false, 'event_token' => $event_token, ) @@ -450,6 +473,10 @@ public function storeApiAddToCartData($add_to_cart_data, $request) { global $apbct; + IMetricService::seek( + $this, + __FUNCTION__ + ); if ( ! $apbct->stats['no_cookie_data_taken'] && $request->get_param('ct_no_cookie_hidden_field') ) { apbct_form__get_no_cookie_data( ['ct_no_cookie_hidden_field' => $request->get_param('ct_no_cookie_hidden_field')], @@ -470,12 +497,13 @@ public function storeApiAddToCartData($add_to_cart_data, $request) $post_info = array(); $post_info['comment_type'] = 'order__add_to_cart'; $post_info['post_url'] = Sanitize::cleanUrl(Server::get('HTTP_REFERER')); + $base_call_result = apbct_base_call( array( 'message' => $message, 'post_info' => $post_info, 'js_on' => apbct_js_test(Sanitize::cleanTextField(Cookie::get('ct_checkjs')), true), - 'sender_info' => array('sender_url' => null), + 'sender_info' => array('sender_url' => null, IMetricDTO::$SENDER_INFO_KEY => IMetricService::finalizeDTO($this)), 'exception_action' => false, 'event_token' => $event_token, ) From e5a8806abc576f872f89b2a772d7ac54b5180d2d Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 10:51:12 +0500 Subject: [PATCH 04/13] Fix. Auto-test. Before new units. --- .../IntegrationMetrics/IMetricDTO.php | 137 +++---- .../IntegrationMetrics/IMetricDTOTrait.php | 58 +-- .../IntegrationMetrics/IMetricService.php | 342 +++++++++--------- lib/Cleantalk/Antispam/Integrations.php | 8 +- .../Antispam/Integrations/IntegrationBase.php | 1 + .../Antispam/IntegrationsByClass.php | 6 - 6 files changed, 278 insertions(+), 274 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php index 4a4d94f7b..054ace5c6 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php @@ -1,73 +1,80 @@ getArray()); - } + /** + * @return false|string + */ + public function getJSON() + { + return @json_encode($this->getArray()); + } - public function getArray() - { - $skip_properties = array( - 'is_released', - 'peak_memory_on_start_kb', - 'memory_usage_on_start_kb', - 'timer_on_start_msec', - 'SENDER_INFO_KEY' - ); - return array_map(function ($value) { - return $value; - }, array_diff_key(get_object_vars($this), array_flip($skip_properties))); - } + public function getArray() + { + $skip_properties = array( + 'is_released', + 'peak_memory_on_start_kb', + 'memory_usage_on_start_kb', + 'timer_on_start_msec', + 'SENDER_INFO_KEY' + ); + return array_map(function ($value) { + return $value; + }, array_diff_key(get_object_vars($this), array_flip($skip_properties))); } +} diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php index bce62c63f..85344cf8b 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php @@ -1,37 +1,37 @@ imetric_dto = $imetric_dto; - if (isset($this->imetric_dto_version)) { - $this->imetric_dto->dto_version = $this->imetric_dto_version; - } + /** + * @param IMetricDTO $imetric_dto + * + * @return void + */ + public function setIMetricDTO(IMetricDTO $imetric_dto): void + { + $this->imetric_dto = $imetric_dto; + if (isset($this->imetric_dto_version)) { + $this->imetric_dto->dto_version = $this->imetric_dto_version; } + } - /** - * @return IMetricDTO|null - */ - public function getIMetricDTO(): ?IMetricDTO - { - return $this->imetric_dto; - } + /** + * @return IMetricDTO|null + */ + public function getIMetricDTO() + { + return $this->imetric_dto; } +} diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php index 66398c0e1..eba2de0f5 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -1,201 +1,209 @@ dto_version = $dto_version; - $full_class = get_class($integration); - $dto->integration_name = substr($full_class, strrpos($full_class, '\\') + 1); - self::startGlobalSeeking($dto); - return $dto; + $dto_version = self::getDTOVersion($integration); + if ($dto_version) { + $dto = new IMetricDTO(); + $dto->dto_version = $dto_version; + $full_class = get_class($integration); + $position = strrpos($full_class, '\\'); + if (!$position) { + $position = -1; } - return false; + $dto->integration_name = substr($full_class, $position + 1); + self::startGlobalSeeking($dto); + return $dto; } + return false; + } - /** - * @param IntegrationBase|IntegrationByClassBase $integration - * - * @return string|false - */ - private static function getDTOVersion($integration) - { - return $integration->imetric_dto_version ?? false; - } + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * + * @return string|false + */ + private static function getDTOVersion($integration) + { + return $integration->imetric_dto_version ?? false; + } - /** - * @param IMetricDTO $dto - * - * @return void - */ - private static function startGlobalSeeking(IMetricDTO $dto) - { - $dto->timer_on_start_msec = self::getCurrentTimeMS(); - $dto->memory_usage_on_start_kb = self::getCurrentMemoryUsageKb(); - $dto->peak_memory_on_start_kb = self::getPeakMemoryUsageKb(); - } + /** + * @param IMetricDTO $dto + * + * @return void + */ + private static function startGlobalSeeking(IMetricDTO $dto) + { + $dto->timer_on_start_msec = self::getCurrentTimeMS(); + $dto->memory_usage_on_start_kb = self::getCurrentMemoryUsageKb(); + $dto->peak_memory_on_start_kb = self::getPeakMemoryUsageKb(); + } - /** - * @return float - */ - private static function getCurrentTimeMS() - { - return round(microtime(true) * 1000, 1); - } + /** + * @return float + */ + private static function getCurrentTimeMS() + { + return round(microtime(true) * 1000, 1); + } - /** - * @return float - */ - private static function getCurrentMemoryUsageKb() - { - return round(memory_get_usage() / 1024); - } + /** + * @return float + */ + private static function getCurrentMemoryUsageKb() + { + return round(memory_get_usage() / 1024); + } - /** - * @return float - */ - private static function getPeakMemoryUsageKb() - { - return round(memory_get_peak_usage() / 1024); - } + /** + * @return float + */ + private static function getPeakMemoryUsageKb() + { + return round(memory_get_peak_usage() / 1024); + } - /** - * @param IntegrationBase|IntegrationByClassBase $integration - * @param string $span_name - * - * @return void - */ - public static function seek($integration, string $span_name = 'undefined_span') - { - if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { - $dto = $integration->getImetricDTO(); - if ($dto && !$dto->is_released) { - if (!isset($dto->spans[$span_name])) { - $dto->spans[$span_name] = [ - 'time_msec' => self::getCurrentTimeMS(), - 'memory_kb' => self::getCurrentMemoryUsageKb(), - 'memory_peak_kb' => self::getPeakMemoryUsageKb(), - 'released' => false - ]; - } + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * @param string $span_name + * + * @return void + */ + public static function seek($integration, string $span_name = 'undefined_span') + { + if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { + $dto = $integration->getImetricDTO(); + if ($dto && !$dto->is_released) { + if (!isset($dto->spans[$span_name])) { + $dto->spans[$span_name] = [ + 'time_msec' => self::getCurrentTimeMS(), + 'memory_kb' => self::getCurrentMemoryUsageKb(), + 'memory_peak_kb' => self::getPeakMemoryUsageKb(), + 'released' => false + ]; } } } + } - public static function lease($integration, string $span_name = 'undefined_span') - { - if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { - $dto = $integration->getImetricDTO(); - if ($dto && !$dto->is_released) { - $dto->spans[$span_name] = self::releaseSpan($dto->spans[$span_name]); - } + public static function lease($integration, string $span_name = 'undefined_span') + { + if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { + $dto = $integration->getImetricDTO(); + if ($dto && !$dto->is_released) { + $dto->spans[$span_name] = self::releaseSpan($dto->spans[$span_name]); } } + } - /** - * @param IMetricDTO $dto - * - * @return void - */ - public static function releaseAllSpans(IMetricDTO $dto) - { - foreach ($dto->spans as $_span_name => &$span_content) { - $span_content = self::releaseSpan($span_content); - } + /** + * @param IMetricDTO $dto + * + * @return void + */ + public static function releaseAllSpans(IMetricDTO $dto) + { + foreach ($dto->spans as $_span_name => &$span_content) { + $span_content = self::releaseSpan($span_content); } + } - /** - * @param array $span_content - * - * @return array - */ - private static function releaseSpan(array $span_content) - { - if ( - isset( - $span_content['released'], - $span_content['time_msec'], - $span_content['memory_kb'], - $span_content['memory_peak_kb'] - ) - ) { - if (!$span_content['released']) { - $span_content['time_msec'] = self::getCurrentTimeMS() - $span_content['time_msec']; - $span_content['memory_kb'] = self::getCurrentMemoryUsageKb() - $span_content['memory_kb']; - $span_content['memory_peak_kb'] = self::getPeakMemoryUsageKb() - $span_content['memory_peak_kb']; - $span_content['released'] = true; - } + /** + * @param array $span_content + * + * @return array + */ + private static function releaseSpan(array $span_content) + { + if ( + isset( + $span_content['released'], + $span_content['time_msec'], + $span_content['memory_kb'], + $span_content['memory_peak_kb'] + ) + ) { + if (!$span_content['released']) { + $span_content['time_msec'] = self::getCurrentTimeMS() - $span_content['time_msec']; + $span_content['memory_kb'] = self::getCurrentMemoryUsageKb() - $span_content['memory_kb']; + $span_content['memory_peak_kb'] = self::getPeakMemoryUsageKb() - $span_content['memory_peak_kb']; + $span_content['released'] = true; } - return $span_content; } + return $span_content; + } - /** - * @param IntegrationBase|IntegrationByClassBase $integration - * @return string - */ - public static function finalizeDTO($integration) - { - $out = false; - $dto = $integration->getImetricDTO(); - if ($dto && !$dto->is_released) { - $dto->peak_memory_diff_kb = self::getPeakMemoryUsageKb() - $dto->peak_memory_on_start_kb; - $dto->total_exec_time_ms = self::getCurrentTimeMS() - $dto->timer_on_start_msec; - self::releaseAllSpans($dto); - $dto->is_released = true; - $out = $dto->getJSON(); - } + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * @return string + */ + public static function finalizeDTO($integration) + { + $out = false; + $dto = $integration->getImetricDTO(); + if ($dto && !$dto->is_released) { + $dto->peak_memory_diff_kb = self::getPeakMemoryUsageKb() - $dto->peak_memory_on_start_kb; + $dto->total_exec_time_ms = self::getCurrentTimeMS() - $dto->timer_on_start_msec; + self::releaseAllSpans($dto); + $dto->is_released = true; + $out = $dto->getJSON(); + } + if (!$out) { + $out = new IMetricDTO(); + $out = $out->getJSON(); if (!$out) { - $out = new IMetricDTO(); - $out = $out->getJSON(); + $out = '{}'; } - return $out; } + return $out; + } - /** - * @param IMetricDTO|null $dto - * @param string $field_name - * @param mixed $field_value - */ - public static function setCustomField($dto = null, $field_name = 'field', $field_value = null) - { - $dto && !$dto->is_released && $dto->custom_fields[$field_name] = $field_value; - } + /** + * @param IMetricDTO|null $dto + * @param string $field_name + * @param mixed $field_value + */ + public static function setCustomField($dto = null, $field_name = 'field', $field_value = null) + { + $dto && !$dto->is_released && $dto->custom_fields[$field_name] = $field_value; + } - /** - * @param IntegrationBase|IntegrationByClassBase $integration - * @param array $vars - * @param string $span - */ - public static function dumpVarsSize($integration, array $vars = [], string $span = 'undefined_variable_span'): void { - $dto = $integration->getImetricDTO(); - if (!$dto || empty($vars)) { - return; - } - $max_var_size_kb = 0; - foreach ($vars as $_name => $value) { - try { - $size = strlen(serialize($value)); - $current_size = round($size / 1024, 1); - if ($current_size > $max_var_size_kb) { - $max_var_size_kb = $current_size; - } - } catch (\Exception $e) { - // is unserializable, skip + /** + * @param IntegrationBase|IntegrationByClassBase $integration + * @param array $vars + * @param string $span + */ + public static function dumpVarsSize($integration, array $vars = [], string $span = 'undefined_variable_span') + { + $dto = $integration->getImetricDTO(); + if (!$dto || empty($vars)) { + return; + } + $max_var_size_kb = 0; + foreach ($vars as $_name => $value) { + try { + $size = strlen(serialize($value)); + $current_size = round($size / 1024, 1); + if ($current_size > $max_var_size_kb) { + $max_var_size_kb = $current_size; } + } catch (\Exception $e) { + // is unserializable, skip } - $dto->variable_peak_kb[$span] = $max_var_size_kb; } + $dto->variable_peak_kb[$span] = $max_var_size_kb; } +} diff --git a/lib/Cleantalk/Antispam/Integrations.php b/lib/Cleantalk/Antispam/Integrations.php index 7ba1c23b5..34836c68d 100644 --- a/lib/Cleantalk/Antispam/Integrations.php +++ b/lib/Cleantalk/Antispam/Integrations.php @@ -106,13 +106,8 @@ public function checkSpam($argument, $set_current_integration = '') return true; } - /** - * @var IMetricDTO $imetric_dto - */ + $imetric_dto = IMetricService::getDTO($integration); - /** - * @var IntegrationBase $integration - */ if ($imetric_dto) { $integration->setIMetricDTO($imetric_dto); } @@ -185,7 +180,6 @@ public function checkSpam($argument, $set_current_integration = '') // Page URL must be an previous page ), ); - error_log('CTDEBUG [' . __FUNCTION__ . '] [$base_call_data] ' . var_export($base_call_data,true)); } // Set registration flag - will be used to select method diff --git a/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php b/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php index 2b1c93cd1..5bb782c4f 100644 --- a/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php +++ b/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php @@ -7,6 +7,7 @@ abstract class IntegrationBase { use IMetricDTOTrait; + public $base_call_result; public $visible_fields_data; diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass.php b/lib/Cleantalk/Antispam/IntegrationsByClass.php index f4056dae2..e883eefee 100755 --- a/lib/Cleantalk/Antispam/IntegrationsByClass.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass.php @@ -53,13 +53,7 @@ public function __construct($integrations) */ $integration = new $class(); - /** - * @var IMetricDTO $imetric_dto - */ $imetric_dto = IMetricService::getDTO($integration); - /** - * @var IntegrationBase $integration - */ if ($imetric_dto) { $integration->setIMetricDTO($imetric_dto); } From 2d61cf642b7224a598b173dff27b430bbf2d83cf Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 11:11:24 +0500 Subject: [PATCH 05/13] Upd. Dock blocks and comments. --- .../IntegrationMetrics/IMetricDTO.php | 132 +++++++- .../IntegrationMetrics/IMetricDTOTrait.php | 82 ++++- .../IntegrationMetrics/IMetricService.php | 305 ++++++++++++++++-- 3 files changed, 488 insertions(+), 31 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php index 054ace5c6..367bab6b7 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php @@ -2,68 +2,198 @@ namespace Cleantalk\Antispam\IntegrationMetrics; +/** + * Integration Metrics Data Transfer Object + * + * This class captures performance metrics for integration processing, including execution time, + * memory usage, and custom performance data. It acts as a container for performance telemetry + * that can be serialized to JSON for transmission to monitoring/analytics systems. + * + * The class is designed to work with IMetricService which handles metric collection and + * span lifecycle management (creation, measurement, finalization). + * + * Usage Example: + * + * $dto = new IMetricDTO(); + * $dto->integration_name = 'WooCommerce'; + * $dto->custom_fields['order_count'] = 5; + * + * $json = $dto->getJSON(); + * // Send $json to analytics backend + * + * + * @see IMetricService for metric lifecycle management + * @see IMetricDTOTrait for integration with IntegrationBase and IntegrationByClassBase + */ class IMetricDTO { /** + * Name of the integration being measured (e.g., 'WooCommerce', 'NinjaForms'). + * Set by IMetricService::getDTO() when creating a new DTO instance. + * * @var string * @psalm-suppress PossiblyUnusedProperty */ public $integration_name = 'unset'; + /** + * Version of the metrics schema/format. + * Can be overridden via IMetricDTOTrait::$imetric_dto_version on the integration class. + * * @var string * @psalm-suppress PossiblyUnusedProperty */ public $dto_version = '1.0.0'; + /** + * Peak memory usage difference in kilobytes (KB) during integration processing. + * Calculated as: peak_memory_at_end - peak_memory_on_start + * Set during IMetricService::finalizeDTO() + * * @var float * @psalm-suppress PossiblyUnusedProperty */ public $peak_memory_diff_kb = 0; + /** + * Total execution time in milliseconds (ms) for the entire integration processing. + * Calculated as: end_time_ms - timer_on_start_msec + * Set during IMetricService::finalizeDTO() + * * @var float * @psalm-suppress PossiblyUnusedProperty */ public $total_exec_time_ms = 0; + /** + * Custom performance fields added by the integration. + * Format: field_name => field_value + * Example: ['form_fields_count' => 15, 'validation_passed' => true] + * Populated via IMetricService::setCustomField() + * * @var array * @psalm-suppress PossiblyUnusedProperty */ public $custom_fields = array(); + /** + * Named time spans tracking specific operations within the integration. + * Format: span_name => ['time_msec' => float, 'memory_kb' => float, 'memory_peak_kb' => float, 'released' => bool] + * + * Each span captures: + * - time_msec: Duration in milliseconds (calculated by IMetricService::lease()) + * - memory_kb: Memory usage in KB (calculated by IMetricService::lease()) + * - memory_peak_kb: Peak memory in KB (calculated by IMetricService::lease()) + * - released: Whether the span has been finalized (true = measurements complete) + * + * Spans are created via IMetricService::seek() and finalized via IMetricService::lease() + * + * @var array + */ public $spans = array(); + /** + * Peak memory usage in KB for specific variable groups, tracked by span name. + * Format: span_name => peak_kb_value + * Example: ['form_data_vars' => 42.5, 'post_data_vars' => 128.3] + * Populated via IMetricService::dumpVarsSize() + * * @var array * @psalm-suppress PossiblyUnusedProperty */ public $variable_peak_kb = array(); + /** + * Internal: Timer value (in ms) captured at metric start. + * Used to calculate total_exec_time_ms = current_time - timer_on_start_msec + * Set by IMetricService::startGlobalSeeking() and should not be modified directly. + * Excluded from JSON output via getArray() + * * @var float */ public $timer_on_start_msec = 0; + /** + * Internal: Memory usage (in KB) captured at metric start. + * Used to track relative memory usage throughout integration processing. + * Set by IMetricService::startGlobalSeeking() and should not be modified directly. + * Excluded from JSON output via getArray() + * * @var float * @psalm-suppress PossiblyUnusedProperty */ public $memory_usage_on_start_kb = 0; + /** + * Internal: Peak memory usage (in KB) captured at metric start. + * Used to calculate peak_memory_diff_kb = peak_memory_at_end - peak_memory_on_start_kb + * Set by IMetricService::startGlobalSeeking() and should not be modified directly. + * Excluded from JSON output via getArray() + * * @var float */ public $peak_memory_on_start_kb = 0; + /** + * Internal: Flag indicating whether metric finalization is complete. + * When true, no further span creation or field updates are allowed. + * Set by IMetricService::finalizeDTO() + * Excluded from JSON output via getArray() + * * @var bool */ public $is_released = false; + /** + * JSON key name used when embedding this DTO in sender info. + * Used to identify metric data in analytics payloads. + * + * @var string + */ public static $SENDER_INFO_KEY = 'imetric'; /** - * @return false|string + * Serializes the DTO to JSON string. + * + * Converts the DTO object to a JSON-encoded string suitable for transmission to analytics systems. + * Internally calls getArray() to filter out internal properties before encoding. + * + * @return false|string JSON string representation of the DTO, or false if JSON encoding fails + * + * @see getArray() for the exact properties included in the output */ public function getJSON() { return @json_encode($this->getArray()); } + /** + * Returns the DTO as an associative array, excluding internal properties. + * + * This method filters out internal tracking properties that are not meant to be transmitted: + * - is_released: tracks finalization state + * - peak_memory_on_start_kb: baseline for calculations + * - memory_usage_on_start_kb: baseline for calculations + * - timer_on_start_msec: baseline for calculations + * - SENDER_INFO_KEY: static metadata key + * + * All other public properties are included in the output array. + * + * Usage: + * + * $dto = new IMetricDTO(); + * $dto->integration_name = 'MyForm'; + * $dto->custom_fields['status'] = 'success'; + * + * $array = $dto->getArray(); + * // $array now contains integration_name, dto_version, spans, custom_fields, etc. + * // but not is_released or *_on_start_* properties + * + * + * @return array Associative array of DTO properties ready for JSON serialization + * + * @see getJSON() for JSON-encoded output + */ public function getArray() { $skip_properties = array( diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php index 85344cf8b..b12dad955 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php @@ -2,33 +2,111 @@ namespace Cleantalk\Antispam\IntegrationMetrics; +/** + * Integration Metrics DTO Trait + * + * Provides metric storage and access methods for integration classes. + * This trait should be used in IntegrationBase and IntegrationByClassBase subclasses + * to enable performance metric collection. + * + * The trait manages: + * - Storage of the IMetricDTO instance + * - Schema version configuration for the metrics + * - Getter/setter methods for safe access + * + * Usage Example: + * + * class MyIntegration extends IntegrationBase { + * use IMetricDTOTrait; // Already included in base class + * + * public function __construct() { + * $this->imetric_dto_version = '2.1.0'; // Override schema version if needed + * } + * } + * + * $integration = new MyIntegration(); + * $dto = IMetricService::getDTO($integration); + * $integration->setIMetricDTO($dto); + * // Now metrics can be collected via IMetricService + * + * + * @see IntegrationBase + * @see IntegrationByClassBase + * @see IMetricService + */ trait IMetricDTOTrait { /** + * Stores the metrics DTO instance for this integration. + * Access via getIMetricDTO() method. + * Should not be accessed directly - use getter/setter methods instead. + * * @var IMetricDTO|null */ protected $imetric_dto = null; /** + * Custom schema version for the metrics DTO. + * If set before calling setIMetricDTO(), this value will override the default DTO version. + * Useful for versioning different integration metrics implementations. + * + * Default: null (uses IMetricDTO default version) + * + * Example: + * + * $integration->imetric_dto_version = '2.5.0'; + * IMetricService::getDTO($integration); // Will set dto_version to '2.5.0' + * + * * @var string|null */ public $imetric_dto_version = null; /** - * @param IMetricDTO $imetric_dto + * Stores the provided IMetricDTO instance and applies version override if set. + * + * This method is called by IMetricService or integration setup code to assign + * a metrics DTO to this integration. If $imetric_dto_version is set on the integration, + * it will override the DTO's default version. + * + * Usage: + * + * $dto = new IMetricDTO(); + * $dto->integration_name = 'WooCommerce'; + * $integration->setIMetricDTO($dto); + * // Now $integration->getIMetricDTO() returns the same DTO + * + * + * @param IMetricDTO $imetric_dto The metrics DTO instance to store * * @return void */ public function setIMetricDTO(IMetricDTO $imetric_dto): void { $this->imetric_dto = $imetric_dto; + // Apply version override if the integration specifies a custom version if (isset($this->imetric_dto_version)) { $this->imetric_dto->dto_version = $this->imetric_dto_version; } } /** - * @return IMetricDTO|null + * Retrieves the stored IMetricDTO instance. + * + * Returns the metrics DTO that was previously set via setIMetricDTO(), + * or null if no DTO has been assigned to this integration. + * + * Usage: + * + * $dto = $integration->getIMetricDTO(); + * if ($dto) { + * IMetricService::seek($integration, 'operation_name'); + * // ... perform operation ... + * IMetricService::lease($integration, 'operation_name'); + * } + * + * + * @return IMetricDTO|null The stored metrics DTO, or null if not set */ public function getIMetricDTO() { diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php index eba2de0f5..93f698a2a 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -5,12 +5,82 @@ use Cleantalk\Antispam\Integrations\IntegrationBase; use Cleantalk\Antispam\IntegrationsByClass\IntegrationByClassBase; +/** + * Integration Metrics Service + * + * Orchestrates performance metric collection for integration processing. + * Provides a comprehensive API for tracking execution time, memory usage, and custom performance data. + * + * The service manages the full lifecycle of metrics: + * 1. Creation: getDTO() - Creates and initializes a metrics DTO + * 2. Collection: seek() / lease() - Records named time spans for operations + * 3. Finalization: finalizeDTO() - Computes final metrics and serializes to JSON + * + * Key Concepts: + * - Spans: Named checkpoints that track execution time and memory between seek() and lease() + * - Timer: Records elapsed time and memory delta for operations + * - Custom Fields: Arbitrary data added by integrations + * - Variable Tracking: Monitors maximum memory usage of specific variables + * + * Complete Workflow Example: + * + * // 1. Initialize metrics + * $integration = new MyIntegration(); + * $dto = IMetricService::getDTO($integration); + * if ($dto) { + * $integration->setIMetricDTO($dto); + * + * // 2. Record operations as spans + * IMetricService::seek($integration, 'form_validation'); + * // ... validate form fields ... + * IMetricService::lease($integration, 'form_validation'); + * + * // 3. Add custom data + * IMetricService::setCustomField($dto, 'field_count', count($fields)); + * IMetricService::dumpVarsSize($integration, ['data' => $data], 'data_size'); + * + * // 4. Finalize and retrieve metrics + * $json = IMetricService::finalizeDTO($integration); + * // Send $json to analytics backend + * } + * + * + * Integration Requirements: + * - Class must have $imetric_dto_version property set (or be null to skip metrics) + * - Class should use IMetricDTOTrait and extend IntegrationBase or IntegrationByClassBase + * + * @see IMetricDTO + * @see IMetricDTOTrait + */ class IMetricService { /** - * @param IntegrationBase|IntegrationByClassBase $integration + * Creates and initializes a metrics DTO for an integration. * - * @return IMetricDTO|false + * Checks if the integration has enabled metrics (via $imetric_dto_version property). + * If enabled, creates a new IMetricDTO instance, sets the integration name from class name, + * and captures initial timing/memory values. + * + * This method should be called at the beginning of integration processing, + * typically before any metric collection begins. + * + * The integration_name is automatically extracted from the class name by removing + * the namespace, making it human-readable (e.g., '\Foo\Bar\WooCommerce' -> 'WooCommerce'). + * + * Usage: + * + * $dto = IMetricService::getDTO($integration); + * if ($dto) { + * $integration->setIMetricDTO($dto); + * // Metrics are now available for collection + * } else { + * // Integration doesn't have metrics enabled + * } + * + * + * @param IntegrationBase|IntegrationByClassBase $integration The integration instance + * + * @return IMetricDTO|false Initialized DTO if metrics are enabled, false otherwise */ public static function getDTO($integration) { @@ -31,9 +101,14 @@ public static function getDTO($integration) } /** - * @param IntegrationBase|IntegrationByClassBase $integration + * Checks if the integration has metrics enabled. + * + * Reads the $imetric_dto_version property. If it's set (not null) and truthy, + * metrics collection is enabled for this integration. * - * @return string|false + * @param IntegrationBase|IntegrationByClassBase $integration The integration instance + * + * @return string|false The DTO version string if enabled, false otherwise */ private static function getDTOVersion($integration) { @@ -41,7 +116,15 @@ private static function getDTOVersion($integration) } /** - * @param IMetricDTO $dto + * Captures initial timing and memory values for metric tracking. + * + * Records the starting state of the system (current time, memory usage, peak memory). + * These baseline values are used later to calculate deltas (how much time and memory + * were consumed during integration processing). + * + * Called internally by getDTO() - do not call directly. + * + * @param IMetricDTO $dto The DTO to initialize * * @return void */ @@ -53,7 +136,11 @@ private static function startGlobalSeeking(IMetricDTO $dto) } /** - * @return float + * Gets the current system time in milliseconds. + * + * Uses microtime() for high precision timing. + * + * @return float Current time in milliseconds, rounded to 1 decimal place */ private static function getCurrentTimeMS() { @@ -61,7 +148,11 @@ private static function getCurrentTimeMS() } /** - * @return float + * Gets the current memory usage in kilobytes. + * + * Retrieves memory_get_usage() and converts to KB. + * + * @return float Current memory usage in KB */ private static function getCurrentMemoryUsageKb() { @@ -69,7 +160,12 @@ private static function getCurrentMemoryUsageKb() } /** - * @return float + * Gets the peak memory usage in kilobytes. + * + * Retrieves memory_get_peak_usage() and converts to KB. + * Note: Peak memory can only increase, never decrease during execution. + * + * @return float Peak memory usage in KB */ private static function getPeakMemoryUsageKb() { @@ -77,8 +173,27 @@ private static function getPeakMemoryUsageKb() } /** - * @param IntegrationBase|IntegrationByClassBase $integration - * @param string $span_name + * Records the start of a named operation span. + * + * Creates a new span with the current timing and memory values. + * Spans track execution time and resource usage between seek() and lease(). + * Can only create each named span once - subsequent calls for the same span are ignored. + * + * Only works if: + * - Integration has a valid IMetricDTO + * - DTO has not been released (finalized) + * - Span name doesn't already exist + * + * Typical usage with lease(): + * + * IMetricService::seek($integration, 'database_query'); + * // ... execute database operations ... + * IMetricService::lease($integration, 'database_query'); + * // Now $dto->spans['database_query'] contains timing/memory delta + * + * + * @param IntegrationBase|IntegrationByClassBase $integration The integration instance + * @param string $span_name Unique name for this span (e.g., 'form_validation', 'user_check') * * @return void */ @@ -99,6 +214,42 @@ public static function seek($integration, string $span_name = 'undefined_span') } } + /** + * Records the end of a named operation span and calculates deltas. + * + * Finalizes a span by: + * - Calculating elapsed time since seek() was called + * - Calculating memory delta + * - Calculating peak memory delta + * - Marking span as released (complete) + * + * Only works if: + * - Integration has a valid IMetricDTO + * - DTO has not been released + * - Span exists and has not been released yet + * + * If span doesn't exist or is already released, has no effect. + * + * After calling lease(), the span values contain: + * - time_msec: Time elapsed (milliseconds) + * - memory_kb: Memory delta (KB used during span) + * - memory_peak_kb: Peak memory delta (KB) + * - released: true (marks span as finalized) + * + * Usage: + * + * IMetricService::seek($integration, 'validation'); + * // ... perform validation (takes 50ms, uses 2MB) ... + * IMetricService::lease($integration, 'validation'); + * // Now: spans['validation']['time_msec'] ≈ 50 + * // spans['validation']['memory_kb'] ≈ 2048 + * + * + * @param IntegrationBase|IntegrationByClassBase $integration The integration instance + * @param string $span_name Name of the span to finalize + * + * @return void + */ public static function lease($integration, string $span_name = 'undefined_span') { if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { @@ -110,7 +261,13 @@ public static function lease($integration, string $span_name = 'undefined_span') } /** - * @param IMetricDTO $dto + * Finalizes all open spans in a DTO. + * + * Iterates through all spans and calls releaseSpan() to finalize them. + * Spans that are already released are left unchanged. + * Typically called by finalizeDTO() as part of metric completion. + * + * @param IMetricDTO $dto The DTO containing spans to finalize * * @return void */ @@ -122,19 +279,31 @@ public static function releaseAllSpans(IMetricDTO $dto) } /** - * @param array $span_content + * Completes a single span by calculating deltas. + * + * Converts span values from absolute measurements to deltas: + * - time_msec: Changed from start_time to elapsed_time + * - memory_kb: Changed from start_memory to used_memory + * - memory_peak_kb: Changed from start_peak to peak_delta + * - released: Changed to true + * + * If span is already released, returns unchanged. + * If span is missing required fields, returns unchanged. + * + * Internal method called by lease() and releaseAllSpans() - typically not called directly. + * + * @param array $span_content The span data to finalize * - * @return array + * @return array Finalized span with delta values */ private static function releaseSpan(array $span_content) { - if ( - isset( - $span_content['released'], - $span_content['time_msec'], - $span_content['memory_kb'], - $span_content['memory_peak_kb'] - ) + if (isset( + $span_content['released'], + $span_content['time_msec'], + $span_content['memory_kb'], + $span_content['memory_peak_kb'] + ) ) { if (!$span_content['released']) { $span_content['time_msec'] = self::getCurrentTimeMS() - $span_content['time_msec']; @@ -147,8 +316,43 @@ private static function releaseSpan(array $span_content) } /** - * @param IntegrationBase|IntegrationByClassBase $integration - * @return string + * Completes metric collection and returns serialized JSON. + * + * This is the final step in metric lifecycle. It: + * 1. Finalizes all spans (calls releaseAllSpans()) + * 2. Calculates overall metrics: + * - peak_memory_diff_kb: Peak memory change during processing + * - total_exec_time_ms: Total elapsed time + * 3. Marks DTO as released (no further updates allowed) + * 4. Serializes to JSON + * + * If DTO is missing or already released, returns a default empty DTO as JSON. + * + * Should be called at the end of integration processing, typically in error handlers + * or finally blocks to ensure metrics are always captured. + * + * Complete usage pattern: + * + * try { + * $integration = new MyIntegration(); + * $dto = IMetricService::getDTO($integration); + * if ($dto) { + * $integration->setIMetricDTO($dto); + * // ... perform integration work ... + * } + * } finally { + * $metrics_json = IMetricService::finalizeDTO($integration); + * // Send metrics to backend + * $response->add_custom_data('metrics', $metrics_json); + * } + * + * + * @param IntegrationBase|IntegrationByClassBase $integration The integration instance + * + * @return string JSON-encoded metrics. Returns '{}' if encoding fails. + * + * @see getDTO() + * @see releaseAllSpans() */ public static function finalizeDTO($integration) { @@ -172,9 +376,26 @@ public static function finalizeDTO($integration) } /** - * @param IMetricDTO|null $dto - * @param string $field_name - * @param mixed $field_value + * Adds a custom field to the DTO's custom_fields array. + * + * Custom fields allow integrations to attach arbitrary performance data. + * Examples: form field count, validation result, user ID, etc. + * + * Safely handles null DTO and released DTO (silently ignores). + * + * Usage: + * + * IMetricService::setCustomField($dto, 'form_fields_count', 15); + * IMetricService::setCustomField($dto, 'validation_passed', true); + * IMetricService::setCustomField($dto, 'processing_stage', 'pre_submission'); + * // Now $dto->custom_fields contains all three fields + * + * + * @param IMetricDTO|null $dto The DTO to update (if null, method is no-op) + * @param string $field_name Custom field key name + * @param mixed $field_value Custom field value (string, number, boolean, array, etc.) + * + * @return void */ public static function setCustomField($dto = null, $field_name = 'field', $field_value = null) { @@ -182,9 +403,37 @@ public static function setCustomField($dto = null, $field_name = 'field', $field } /** - * @param IntegrationBase|IntegrationByClassBase $integration - * @param array $vars - * @param string $span + * Tracks the peak memory usage of specific variables. + * + * Serializes each variable and records the maximum serialized size in KB. + * Useful for profiling large data structures being processed by integrations. + * + * Silently skips: + * - Non-serializable objects (unserializable values are caught and skipped) + * - Empty variable arrays + * - Integrations without a DTO + * + * Usage: + * + * $form_data = ['field1' => 'value', 'nested' => ['x' => 'y', ...]]; + * $user_data = get_user_meta($user_id); + * + * IMetricService::dumpVarsSize( + * $integration, + * ['form' => $form_data, 'user' => $user_data], + * 'user_form_data_size' + * ); + * // Now $dto->variable_peak_kb['user_form_data_size'] = largest serialized size + * + * + * The stored value represents the maximum size of any single variable serialized, + * not the sum of all variables. This helps identify which data structure is largest. + * + * @param IntegrationBase|IntegrationByClassBase $integration The integration instance + * @param array $vars Key-value pairs where values are variables to profile + * @param string $span Name for grouping related variable measurements + * + * @return void */ public static function dumpVarsSize($integration, array $vars = [], string $span = 'undefined_variable_span') { From 73e42187a76cf0a815b1023b0adaa71c449232bd Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 11:11:42 +0500 Subject: [PATCH 06/13] Upd. Code. New units for iMetric. --- .../IntegrationMetrics/IMetricDTOTest.php | 87 +++++ .../IMetricDTOTraitTest.php | 55 +++ .../IntegrationMetrics/IMetricServiceTest.php | 324 ++++++++++++++++++ 3 files changed, 466 insertions(+) create mode 100644 tests/Antispam/IntegrationMetrics/IMetricDTOTest.php create mode 100644 tests/Antispam/IntegrationMetrics/IMetricDTOTraitTest.php create mode 100644 tests/Antispam/IntegrationMetrics/IMetricServiceTest.php diff --git a/tests/Antispam/IntegrationMetrics/IMetricDTOTest.php b/tests/Antispam/IntegrationMetrics/IMetricDTOTest.php new file mode 100644 index 000000000..7562809ec --- /dev/null +++ b/tests/Antispam/IntegrationMetrics/IMetricDTOTest.php @@ -0,0 +1,87 @@ +assertSame('unset', $dto->integration_name); + $this->assertSame('1.0.0', $dto->dto_version); + $this->assertSame(0, $dto->peak_memory_diff_kb); + $this->assertSame(0, $dto->total_exec_time_ms); + $this->assertSame(array(), $dto->custom_fields); + $this->assertSame(array(), $dto->spans); + $this->assertSame(array(), $dto->variable_peak_kb); + $this->assertSame(0, $dto->timer_on_start_msec); + $this->assertSame(0, $dto->memory_usage_on_start_kb); + $this->assertSame(0, $dto->peak_memory_on_start_kb); + $this->assertFalse($dto->is_released); + } + + public function testSenderInfoKey() + { + $this->assertSame('imetric', IMetricDTO::$SENDER_INFO_KEY); + } + + public function testGetArrayContainsPublishedProperties() + { + $dto = new IMetricDTO(); + $dto->integration_name = 'MyIntegration'; + $dto->dto_version = '2.5.7'; + $dto->peak_memory_diff_kb = 10.5; + $dto->total_exec_time_ms = 123.4; + $dto->custom_fields = array('foo' => 'bar'); + $dto->spans = array('span1' => array('time_msec' => 1)); + $dto->variable_peak_kb = array('span1' => 2.2); + + $array = $dto->getArray(); + + $this->assertArrayHasKey('integration_name', $array); + $this->assertArrayHasKey('dto_version', $array); + $this->assertArrayHasKey('peak_memory_diff_kb', $array); + $this->assertArrayHasKey('total_exec_time_ms', $array); + $this->assertArrayHasKey('custom_fields', $array); + $this->assertArrayHasKey('spans', $array); + $this->assertArrayHasKey('variable_peak_kb', $array); + + $this->assertSame('MyIntegration', $array['integration_name']); + $this->assertSame('2.5.7', $array['dto_version']); + $this->assertSame(10.5, $array['peak_memory_diff_kb']); + $this->assertSame(123.4, $array['total_exec_time_ms']); + $this->assertSame(array('foo' => 'bar'), $array['custom_fields']); + } + + public function testGetArraySkipsInternalProperties() + { + $dto = new IMetricDTO(); + $dto->is_released = true; + $dto->peak_memory_on_start_kb = 999; + $dto->memory_usage_on_start_kb = 888; + $dto->timer_on_start_msec = 777; + + $array = $dto->getArray(); + + $this->assertArrayNotHasKey('is_released', $array); + $this->assertArrayNotHasKey('peak_memory_on_start_kb', $array); + $this->assertArrayNotHasKey('memory_usage_on_start_kb', $array); + $this->assertArrayNotHasKey('timer_on_start_msec', $array); + $this->assertArrayNotHasKey('SENDER_INFO_KEY', $array); + } + + public function testGetJSONMatchesArray() + { + $dto = new IMetricDTO(); + $dto->integration_name = 'Foo'; + $dto->custom_fields = array('a' => 1, 'b' => 'two'); + + $json = $dto->getJSON(); + + $this->assertIsString($json); + $decoded = json_decode($json, true); + $this->assertSame($dto->getArray(), $decoded); + } +} diff --git a/tests/Antispam/IntegrationMetrics/IMetricDTOTraitTest.php b/tests/Antispam/IntegrationMetrics/IMetricDTOTraitTest.php new file mode 100644 index 000000000..2268b0d21 --- /dev/null +++ b/tests/Antispam/IntegrationMetrics/IMetricDTOTraitTest.php @@ -0,0 +1,55 @@ +assertNull($fixture->getIMetricDTO()); + $this->assertNull($fixture->imetric_dto_version); + } + + public function testSetIMetricDTOStoresInstance() + { + $fixture = new IMetricDTOTraitFixture(); + $dto = new IMetricDTO(); + + $fixture->setIMetricDTO($dto); + + $this->assertSame($dto, $fixture->getIMetricDTO()); + } + + public function testSetIMetricDTOKeepsDefaultVersionWhenNoOverride() + { + $fixture = new IMetricDTOTraitFixture(); + $dto = new IMetricDTO(); + $original_version = $dto->dto_version; + + $fixture->setIMetricDTO($dto); + + $this->assertSame($original_version, $fixture->getIMetricDTO()->dto_version); + } + + public function testSetIMetricDTOOverridesVersionWhenSet() + { + $fixture = new IMetricDTOTraitFixture(); + $fixture->imetric_dto_version = '3.1.4'; + $dto = new IMetricDTO(); + + $fixture->setIMetricDTO($dto); + + $this->assertSame('3.1.4', $fixture->getIMetricDTO()->dto_version); + $this->assertSame('3.1.4', $dto->dto_version); + } +} diff --git a/tests/Antispam/IntegrationMetrics/IMetricServiceTest.php b/tests/Antispam/IntegrationMetrics/IMetricServiceTest.php new file mode 100644 index 000000000..da0b01b13 --- /dev/null +++ b/tests/Antispam/IntegrationMetrics/IMetricServiceTest.php @@ -0,0 +1,324 @@ +imetric_dto; + } + } +} + +class IMetricServiceTest extends TestCase +{ + public function testGetDTOReturnsFalseWhenVersionNotSet() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $this->assertFalse(IMetricService::getDTO($integration)); + } + + public function testGetDTOReturnsFalseForNonIntegration() + { + $integration = new IMetricServiceNonIntegrationFixture(); + $this->assertFalse(IMetricService::getDTO($integration)); + } + + public function testGetDTOReturnsPopulatedDTO() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $integration->imetric_dto_version = '9.9.9'; + + $dto = IMetricService::getDTO($integration); + + $this->assertInstanceOf(IMetricDTO::class, $dto); + $this->assertSame('9.9.9', $dto->dto_version); + $this->assertSame('IMetricServiceIntegrationBaseFixture', $dto->integration_name); + $this->assertGreaterThan(0, $dto->timer_on_start_msec); + $this->assertGreaterThanOrEqual(0, $dto->memory_usage_on_start_kb); + $this->assertGreaterThanOrEqual(0, $dto->peak_memory_on_start_kb); + $this->assertFalse($dto->is_released); + } + + public function testGetDTOWorksForIntegrationByClassBase() + { + $integration = new IMetricServiceIntegrationByClassBaseFixture(); + $integration->imetric_dto_version = '1.2.3'; + + $dto = IMetricService::getDTO($integration); + + $this->assertInstanceOf(IMetricDTO::class, $dto); + $this->assertSame('1.2.3', $dto->dto_version); + $this->assertSame('IMetricServiceIntegrationByClassBaseFixture', $dto->integration_name); + } + + public function testSeekCreatesSpan() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $integration->setIMetricDTO($dto); + + IMetricService::seek($integration, 'my_span'); + + $this->assertArrayHasKey('my_span', $dto->spans); + $span = $dto->spans['my_span']; + $this->assertFalse($span['released']); + $this->assertArrayHasKey('time_msec', $span); + $this->assertArrayHasKey('memory_kb', $span); + $this->assertArrayHasKey('memory_peak_kb', $span); + } + + public function testSeekIsNoOpForNonIntegration() + { + $integration = new IMetricServiceNonIntegrationFixture(); + $dto = new IMetricDTO(); + $integration->imetric_dto = $dto; + + IMetricService::seek($integration, 'my_span'); + + $this->assertSame(array(), $dto->spans); + } + + public function testSeekIsNoOpWhenDTOReleased() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $dto->is_released = true; + $integration->setIMetricDTO($dto); + + IMetricService::seek($integration, 'my_span'); + + $this->assertSame(array(), $dto->spans); + } + + public function testSeekIsNoOpWhenDTOMissing() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + + IMetricService::seek($integration, 'my_span'); + + $this->assertNull($integration->getIMetricDTO()); + } + + public function testSeekDoesNotOverwriteExistingSpan() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $integration->setIMetricDTO($dto); + + IMetricService::seek($integration, 'my_span'); + $original = $dto->spans['my_span']; + usleep(1000); + IMetricService::seek($integration, 'my_span'); + + $this->assertSame($original, $dto->spans['my_span']); + } + + public function testLeaseMarksSpanReleased() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $integration->setIMetricDTO($dto); + + IMetricService::seek($integration, 'my_span'); + usleep(1000); + IMetricService::lease($integration, 'my_span'); + + $this->assertTrue($dto->spans['my_span']['released']); + $this->assertGreaterThanOrEqual(0, $dto->spans['my_span']['time_msec']); + } + + public function testLeaseIsNoOpForNonIntegration() + { + $integration = new IMetricServiceNonIntegrationFixture(); + $dto = new IMetricDTO(); + $dto->spans = array('my_span' => array( + 'time_msec' => 1.0, + 'memory_kb' => 1.0, + 'memory_peak_kb' => 1.0, + 'released' => false, + )); + $integration->imetric_dto = $dto; + + IMetricService::lease($integration, 'my_span'); + + $this->assertFalse($dto->spans['my_span']['released']); + } + + public function testReleaseAllSpansReleasesEverySpan() + { + $dto = new IMetricDTO(); + $dto->spans = array( + 'a' => array('time_msec' => 1.0, 'memory_kb' => 1.0, 'memory_peak_kb' => 1.0, 'released' => false), + 'b' => array('time_msec' => 2.0, 'memory_kb' => 2.0, 'memory_peak_kb' => 2.0, 'released' => false), + ); + + IMetricService::releaseAllSpans($dto); + + $this->assertTrue($dto->spans['a']['released']); + $this->assertTrue($dto->spans['b']['released']); + } + + public function testReleaseAllSpansSkipsAlreadyReleased() + { + $dto = new IMetricDTO(); + $dto->spans = array( + 'a' => array('time_msec' => 5.0, 'memory_kb' => 5.0, 'memory_peak_kb' => 5.0, 'released' => true), + ); + + IMetricService::releaseAllSpans($dto); + + $this->assertSame(5.0, $dto->spans['a']['time_msec']); + $this->assertTrue($dto->spans['a']['released']); + } + + public function testFinalizeDTOReleasesAndReturnsJSON() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $integration->imetric_dto_version = '1.0.0'; + $dto = IMetricService::getDTO($integration); + $integration->setIMetricDTO($dto); + IMetricService::seek($integration, 'span_a'); + usleep(500); + + $json = IMetricService::finalizeDTO($integration); + + $this->assertIsString($json); + $decoded = json_decode($json, true); + $this->assertIsArray($decoded); + $this->assertTrue($dto->is_released); + $this->assertArrayHasKey('span_a', $decoded['spans']); + $this->assertTrue($decoded['spans']['span_a']['released']); + $this->assertGreaterThanOrEqual(0, $decoded['total_exec_time_ms']); + } + + public function testFinalizeDTOReturnsDefaultWhenDTOMissing() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + + $json = IMetricService::finalizeDTO($integration); + + $this->assertIsString($json); + $decoded = json_decode($json, true); + $this->assertIsArray($decoded); + $this->assertSame('unset', $decoded['integration_name']); + } + + public function testFinalizeDTOReturnsDefaultWhenDTOAlreadyReleased() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $dto->is_released = true; + $integration->setIMetricDTO($dto); + + $json = IMetricService::finalizeDTO($integration); + + $decoded = json_decode($json, true); + $this->assertSame('unset', $decoded['integration_name']); + } + + public function testSetCustomFieldStoresValue() + { + $dto = new IMetricDTO(); + + IMetricService::setCustomField($dto, 'foo', 'bar'); + + $this->assertSame('bar', $dto->custom_fields['foo']); + } + + public function testSetCustomFieldIsNoOpOnReleasedDTO() + { + $dto = new IMetricDTO(); + $dto->is_released = true; + + IMetricService::setCustomField($dto, 'foo', 'bar'); + + $this->assertArrayNotHasKey('foo', $dto->custom_fields); + } + + public function testSetCustomFieldIsNoOpWhenDTONull() + { + IMetricService::setCustomField(null, 'foo', 'bar'); + $this->assertTrue(true); + } + + public function testDumpVarsSizeStoresMaxSerializedSize() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $integration->setIMetricDTO($dto); + $small = 'x'; + $large = str_repeat('y', 2048); + + IMetricService::dumpVarsSize($integration, array('small' => $small, 'large' => $large), 'vars_span'); + + $this->assertArrayHasKey('vars_span', $dto->variable_peak_kb); + $expected_kb = round(strlen(serialize($large)) / 1024, 1); + $this->assertSame($expected_kb, $dto->variable_peak_kb['vars_span']); + } + + public function testDumpVarsSizeIsNoOpWhenVarsEmpty() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $integration->setIMetricDTO($dto); + + IMetricService::dumpVarsSize($integration, array(), 'vars_span'); + + $this->assertSame(array(), $dto->variable_peak_kb); + } + + public function testDumpVarsSizeIsNoOpWhenDTOMissing() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + + IMetricService::dumpVarsSize($integration, array('a' => 'b'), 'vars_span'); + + $this->assertNull($integration->getIMetricDTO()); + } + + public function testDumpVarsSizeUsesDefaultSpanName() + { + $integration = new IMetricServiceIntegrationBaseFixture(); + $dto = new IMetricDTO(); + $integration->setIMetricDTO($dto); + + IMetricService::dumpVarsSize($integration, array('a' => 'value')); + + $this->assertArrayHasKey('undefined_variable_span', $dto->variable_peak_kb); + } +} From 367910f909eddecd16410ab100eb01e3133590a2 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Sep 2026 11:26:27 +0500 Subject: [PATCH 07/13] CP. Span lease logic updated. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Antispam/IntegrationMetrics/IMetricService.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php index 93f698a2a..617783bb1 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -252,12 +252,12 @@ public static function seek($integration, string $span_name = 'undefined_span') */ public static function lease($integration, string $span_name = 'undefined_span') { - if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { - $dto = $integration->getImetricDTO(); - if ($dto && !$dto->is_released) { - $dto->spans[$span_name] = self::releaseSpan($dto->spans[$span_name]); - } - } +if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { + $dto = $integration->getIMetricDTO(); + if ($dto && !$dto->is_released && isset($dto->spans[$span_name]) && is_array($dto->spans[$span_name])) { + $dto->spans[$span_name] = self::releaseSpan($dto->spans[$span_name]); + } +} } /** From c9fd5688519c9ad6ac91c7372f7df3cb9644f2e4 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Sep 2026 11:27:19 +0500 Subject: [PATCH 08/13] Cp. dumpVarsSize updated to prevent mutations if DTO not released. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php index 617783bb1..3b6ef2f58 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -437,8 +437,8 @@ public static function setCustomField($dto = null, $field_name = 'field', $field */ public static function dumpVarsSize($integration, array $vars = [], string $span = 'undefined_variable_span') { - $dto = $integration->getImetricDTO(); - if (!$dto || empty($vars)) { + $dto = $integration->getIMetricDTO(); + if (!$dto || $dto->is_released || empty($vars)) { return; } $max_var_size_kb = 0; From c6b807448a1cbbc6da901a7994eba89018aa7ad8 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Sep 2026 11:29:41 +0500 Subject: [PATCH 09/13] CP. Delete unused imports. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Cleantalk/Antispam/IntegrationsByClass.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass.php b/lib/Cleantalk/Antispam/IntegrationsByClass.php index e883eefee..dee6b4b3f 100755 --- a/lib/Cleantalk/Antispam/IntegrationsByClass.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass.php @@ -2,9 +2,7 @@ namespace Cleantalk\Antispam; -use Cleantalk\Antispam\IntegrationMetrics\IMetricDTO; use Cleantalk\Antispam\IntegrationMetrics\IMetricService; -use Cleantalk\Antispam\Integrations\IntegrationBase; use Cleantalk\Antispam\IntegrationsByClass\IntegrationByClassBase; class IntegrationsByClass From a2200bf2595d74cfd430ca357bb7bc26fc950e19 Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 11:32:32 +0500 Subject: [PATCH 10/13] CP. Fixed exception type. --- lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php index 3b6ef2f58..f28c24b91 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -449,7 +449,7 @@ public static function dumpVarsSize($integration, array $vars = [], string $span if ($current_size > $max_var_size_kb) { $max_var_size_kb = $current_size; } - } catch (\Exception $e) { + } catch (\Throwable $e) { // is unserializable, skip } } From c25ceee3c34ba56f65bc6b03f27c57a74afd5894 Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 11:34:03 +0500 Subject: [PATCH 11/13] CP. Fixed dto-version set. --- lib/Cleantalk/Antispam/Integrations/NinjaForms.php | 5 ++++- lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/Cleantalk/Antispam/Integrations/NinjaForms.php b/lib/Cleantalk/Antispam/Integrations/NinjaForms.php index f23479425..5d2d65a7e 100644 --- a/lib/Cleantalk/Antispam/Integrations/NinjaForms.php +++ b/lib/Cleantalk/Antispam/Integrations/NinjaForms.php @@ -14,7 +14,10 @@ class NinjaForms extends IntegrationBase { private $sender_email = ''; // needs to provide to final actions - public $imetric_dto_version = '1.0.0'; + public function __construct() + { + $this->imetric_dto_version = '1.0.0'; + } public function getDataForChecking($argument) { global $apbct, $cleantalk_executed; diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index f6c4ac448..c53130206 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -38,7 +38,10 @@ class Woocommerce extends IntegrationByClassBase { private $event_token = null; - public $imetric_dto_version = '1.0.0'; + public function __construct() + { + $this->imetric_dto_version = '1.0.0'; + } /** * @return void From c867c68f7b0e12077674a5bb4a506299c0cdf6ee Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 11:36:24 +0500 Subject: [PATCH 12/13] CP. Fixed indents. --- .../Antispam/IntegrationMetrics/IMetricService.php | 12 ++++++------ lib/Cleantalk/Antispam/Integrations/NinjaForms.php | 4 ++++ .../Antispam/IntegrationsByClass/Woocommerce.php | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php index f28c24b91..191147c5a 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -252,12 +252,12 @@ public static function seek($integration, string $span_name = 'undefined_span') */ public static function lease($integration, string $span_name = 'undefined_span') { -if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { - $dto = $integration->getIMetricDTO(); - if ($dto && !$dto->is_released && isset($dto->spans[$span_name]) && is_array($dto->spans[$span_name])) { - $dto->spans[$span_name] = self::releaseSpan($dto->spans[$span_name]); - } -} + if ($integration instanceof IntegrationBase || $integration instanceof IntegrationByClassBase) { + $dto = $integration->getIMetricDTO(); + if ($dto && !$dto->is_released && isset($dto->spans[$span_name]) && is_array($dto->spans[$span_name])) { + $dto->spans[$span_name] = self::releaseSpan($dto->spans[$span_name]); + } + } } /** diff --git a/lib/Cleantalk/Antispam/Integrations/NinjaForms.php b/lib/Cleantalk/Antispam/Integrations/NinjaForms.php index 5d2d65a7e..5c0fc0d4d 100644 --- a/lib/Cleantalk/Antispam/Integrations/NinjaForms.php +++ b/lib/Cleantalk/Antispam/Integrations/NinjaForms.php @@ -14,6 +14,10 @@ class NinjaForms extends IntegrationBase { private $sender_email = ''; // needs to provide to final actions + + /** + * @psalm-suppress PossiblyUnusedMethod + */ public function __construct() { $this->imetric_dto_version = '1.0.0'; diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index c53130206..ab1892eb0 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -38,6 +38,9 @@ class Woocommerce extends IntegrationByClassBase { private $event_token = null; + /** + * @psalm-suppress PossiblyUnusedMethod + */ public function __construct() { $this->imetric_dto_version = '1.0.0'; From d84bf9ff48d9fab6e575b32a3af81ba1a651b9dd Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 2 Sep 2026 19:50:47 +0500 Subject: [PATCH 13/13] Fix. Imetric. Cast to int float values. --- .../Antispam/IntegrationMetrics/IMetricDTO.php | 10 +++++----- .../Antispam/IntegrationMetrics/IMetricService.php | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php index 367bab6b7..0716c34fc 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php @@ -50,7 +50,7 @@ class IMetricDTO * Calculated as: peak_memory_at_end - peak_memory_on_start * Set during IMetricService::finalizeDTO() * - * @var float + * @var int * @psalm-suppress PossiblyUnusedProperty */ public $peak_memory_diff_kb = 0; @@ -60,7 +60,7 @@ class IMetricDTO * Calculated as: end_time_ms - timer_on_start_msec * Set during IMetricService::finalizeDTO() * - * @var float + * @var int * @psalm-suppress PossiblyUnusedProperty */ public $total_exec_time_ms = 0; @@ -109,7 +109,7 @@ class IMetricDTO * Set by IMetricService::startGlobalSeeking() and should not be modified directly. * Excluded from JSON output via getArray() * - * @var float + * @var int */ public $timer_on_start_msec = 0; @@ -119,7 +119,7 @@ class IMetricDTO * Set by IMetricService::startGlobalSeeking() and should not be modified directly. * Excluded from JSON output via getArray() * - * @var float + * @var int * @psalm-suppress PossiblyUnusedProperty */ public $memory_usage_on_start_kb = 0; @@ -130,7 +130,7 @@ class IMetricDTO * Set by IMetricService::startGlobalSeeking() and should not be modified directly. * Excluded from JSON output via getArray() * - * @var float + * @var int */ public $peak_memory_on_start_kb = 0; diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php index 191147c5a..c314f8de7 100644 --- a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -140,11 +140,11 @@ private static function startGlobalSeeking(IMetricDTO $dto) * * Uses microtime() for high precision timing. * - * @return float Current time in milliseconds, rounded to 1 decimal place + * @return int Current time in milliseconds */ private static function getCurrentTimeMS() { - return round(microtime(true) * 1000, 1); + return (int)(microtime(true) * 1000); } /** @@ -152,11 +152,11 @@ private static function getCurrentTimeMS() * * Retrieves memory_get_usage() and converts to KB. * - * @return float Current memory usage in KB + * @return int Current memory usage in KB */ private static function getCurrentMemoryUsageKb() { - return round(memory_get_usage() / 1024); + return (int)(memory_get_usage() / 1024); } /** @@ -165,11 +165,11 @@ private static function getCurrentMemoryUsageKb() * Retrieves memory_get_peak_usage() and converts to KB. * Note: Peak memory can only increase, never decrease during execution. * - * @return float Peak memory usage in KB + * @return int Peak memory usage in KB */ private static function getPeakMemoryUsageKb() { - return round(memory_get_peak_usage() / 1024); + return (int)(memory_get_peak_usage() / 1024); } /**