diff --git a/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php new file mode 100644 index 000000000..0716c34fc --- /dev/null +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTO.php @@ -0,0 +1,210 @@ + + * $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 int + * @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 int + * @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 int + */ + 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 int + * @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 int + */ + 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'; + + /** + * 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( + '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..b12dad955 --- /dev/null +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricDTOTrait.php @@ -0,0 +1,115 @@ + + * 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; + + /** + * 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; + } + } + + /** + * 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() + { + 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..c314f8de7 --- /dev/null +++ b/lib/Cleantalk/Antispam/IntegrationMetrics/IMetricService.php @@ -0,0 +1,458 @@ + + * // 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 +{ + /** + * Creates and initializes a metrics DTO for an integration. + * + * 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) + { + $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; + } + $dto->integration_name = substr($full_class, $position + 1); + self::startGlobalSeeking($dto); + return $dto; + } + return false; + } + + /** + * 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. + * + * @param IntegrationBase|IntegrationByClassBase $integration The integration instance + * + * @return string|false The DTO version string if enabled, false otherwise + */ + private static function getDTOVersion($integration) + { + return $integration->imetric_dto_version ?? false; + } + + /** + * 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 + */ + 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(); + } + + /** + * Gets the current system time in milliseconds. + * + * Uses microtime() for high precision timing. + * + * @return int Current time in milliseconds + */ + private static function getCurrentTimeMS() + { + return (int)(microtime(true) * 1000); + } + + /** + * Gets the current memory usage in kilobytes. + * + * Retrieves memory_get_usage() and converts to KB. + * + * @return int Current memory usage in KB + */ + private static function getCurrentMemoryUsageKb() + { + return (int)(memory_get_usage() / 1024); + } + + /** + * 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 int Peak memory usage in KB + */ + private static function getPeakMemoryUsageKb() + { + return (int)(memory_get_peak_usage() / 1024); + } + + /** + * 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 + */ + 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 + ]; + } + } + } + } + + /** + * 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) { + $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]); + } + } + } + + /** + * 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 + */ + public static function releaseAllSpans(IMetricDTO $dto) + { + foreach ($dto->spans as $_span_name => &$span_content) { + $span_content = self::releaseSpan($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 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 (!$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; + } + + /** + * 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) + { + $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 = '{}'; + } + } + return $out; + } + + /** + * 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) + { + $dto && !$dto->is_released && $dto->custom_fields[$field_name] = $field_value; + } + + /** + * 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') + { + $dto = $integration->getIMetricDTO(); + if (!$dto || $dto->is_released || 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 (\Throwable $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 115e5c04f..34836c68d 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 { @@ -93,7 +96,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', @@ -104,10 +107,16 @@ public function checkSpam($argument, $set_current_integration = '') return true; } + $imetric_dto = IMetricService::getDTO($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; @@ -122,10 +131,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) ) { /** @@ -151,6 +164,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']) : '', diff --git a/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php b/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php index 2428d6387..5bb782c4f 100644 --- a/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php +++ b/lib/Cleantalk/Antispam/Integrations/IntegrationBase.php @@ -2,8 +2,12 @@ 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/Integrations/NinjaForms.php b/lib/Cleantalk/Antispam/Integrations/NinjaForms.php index 8d0acba4c..5c0fc0d4d 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,14 @@ 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'; + } public function getDataForChecking($argument) { global $apbct, $cleantalk_executed; @@ -44,6 +53,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 +73,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 +93,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 +192,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 +264,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 +274,7 @@ public function getGFANew(): GetFieldsAnyDTO */ public function getGFAOld(): GetFieldsAnyDTO { + IMetricService::seek($this, __FUNCTION__); /** * Filter for POST */ @@ -272,7 +288,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 +311,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 +342,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; } diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass.php b/lib/Cleantalk/Antispam/IntegrationsByClass.php index af61d353d..dee6b4b3f 100755 --- a/lib/Cleantalk/Antispam/IntegrationsByClass.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass.php @@ -2,6 +2,7 @@ namespace Cleantalk\Antispam; +use Cleantalk\Antispam\IntegrationMetrics\IMetricService; use Cleantalk\Antispam\IntegrationsByClass\IntegrationByClassBase; class IntegrationsByClass @@ -50,6 +51,11 @@ public function __construct($integrations) */ $integration = new $class(); + $imetric_dto = IMetricService::getDTO($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 diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index bafa1e415..ab1892eb0 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,14 @@ class Woocommerce extends IntegrationByClassBase { private $event_token = null; + /** + * @psalm-suppress PossiblyUnusedMethod + */ + public function __construct() + { + $this->imetric_dto_version = '1.0.0'; + } + /** * @return void * @psalm-suppress PossiblyUnusedMethod @@ -177,6 +187,11 @@ public function checkoutCheck($_data, $errors) { global $apbct, $cleantalk_executed; + IMetricService::seek( + $this, + __FUNCTION__ + ); + if ( count($errors->errors) ) { return; } @@ -214,7 +229,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 +275,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 +293,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 +422,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 +451,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 +479,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 +503,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, ) 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); + } +}