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);
+ }
+}