diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php
index 930af36..007ce3e 100644
--- a/Annotations/AnnotationGenerator.php
+++ b/Annotations/AnnotationGenerator.php
@@ -31,6 +31,34 @@ class AnnotationGenerator
{
public const EXAMPLE_CHAR_LIMIT = 3000;
+ public const GLOBAL_PARAMETER_NAMES = [
+ 'idSite',
+ 'period',
+ 'date',
+ 'segment',
+ 'expanded',
+ 'idSubtable',
+ 'flat',
+ 'filter_pattern',
+ 'filter_column',
+ 'filter_pattern_recursive',
+ 'filter_column_recursive',
+ 'filter_excludelowpop',
+ 'filter_excludelowpop_value',
+ 'filter_sort_column',
+ 'filter_sort_order',
+ 'filter_truncate',
+ 'filter_limit',
+ 'filter_offset',
+ 'keep_summary_row',
+ 'disable_generic_filters',
+ 'disable_queued_filters',
+ 'hideColumns',
+ 'showColumns',
+ 'label',
+ 'idGoal',
+ ];
+
/**
* @var DocumentationGenerator
*/
@@ -41,17 +69,37 @@ class AnnotationGenerator
*/
protected $reportMetadata;
+ /**
+ * @var array[]
+ */
+ protected $missingImportantDataWarnings;
+
public function __construct(DocumentationGenerator $generator)
{
$this->generator = $generator;
+ $this->missingImportantDataWarnings = [];
}
/**
- * Use reflection to generate the OpenAPI annotations to be used by swagger-php.
+ * Generate all the annotations for a plugin's public API endpoints and return them as an array of strings. A string
+ * for each line to be output or written to file.
+ *
+ * @param string $pluginName The name of the plugin. E.g. TagManager
+ * @param bool $writeToFile Indicate whether the results should be written to file. Default is false so that a dry
+ * run won't affect the file-system.
+ * @param bool $useTmpDir Indicate whether the file should be written in Matomo's tmp/ directory. The default is
+ * false, meaning that it will be written in the OpenApi/Annotations/ directory of the plugin, creating the
+ * directory if it doesn't already exist. This is useful if we just want a temp file for comparison. like during
+ * testing.
+ *
+ * @return string[]|array[] The collection of all the lines which make up the generated annotations for the public API
+ * endpoints defined by the plugin.
+ * @throws \Piwik\Exception\PluginDeactivatedException If the plugin is not activated. It should be loaded.
+ * @throws \Throwable
*/
public function generatePluginApiAnnotations(string $pluginName, bool $writeToFile = false, bool $useTmpDir = false): array
{
- BaseValidator::check('plugin', $pluginName, [ new NotEmpty() ]);
+ BaseValidator::check('plugin', $pluginName, [new NotEmpty()]);
Manager::getInstance()->checkIsPluginActivated($pluginName);
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');
@@ -93,12 +141,41 @@ public function generatePluginApiAnnotations(string $pluginName, bool $writeToFi
$this->writeAnnotationsToFile($annotations, $pluginAnnotationPath, $pluginName);
}
- return $annotations;
+ if (count($this->missingImportantDataWarnings) === 0) {
+ return $annotations;
+ }
+
+ $lines = [];
+ foreach ($this->missingImportantDataWarnings as $methodName => $warnings) {
+ if (empty($warnings)) {
+ continue;
+ }
+
+ $lines[] = $methodName . ' has the following warnings:';
+ foreach ($warnings as $paramName => $warningLines) {
+ if (empty($warningLines)) {
+ continue;
+ }
+
+ $lines[] = '- ' . $paramName . ':';
+ $lines[] = " - " . implode("\n - ", $warningLines);
+ }
+ }
+
+ return $lines;
}
- protected function writeAnnotationsToFile(array $annotations, string $filePath, string $pluginName): void
+ /**
+ * Write the collection of annotation lines to file, overwriting the file if it already exists.
+ *
+ * @param array[] $annotations Collection of generated annotations. It's an array of arrays containing the lines
+ * which make up all the annotations which need to be written to file.
+ * @param string $pluginName Name of the plugin. E.g. TagManager
+ *
+ * @return string The full string content of the generated annotations file.
+ */
+ public function getContentForGeneratedAnnotationsFile(array $annotations, string $pluginName): string
{
- $output = '';
$lines = [
'getContentForGeneratedAnnotationsFile($annotations, $pluginName));
}
+ /**
+ * Build the full array of lines for an OA operation, like OA\Get or OA\Post. This pulls data from various sources,
+ * including making API calls to get example responses.
+ *
+ * @param array $rules An array of configs determining which responses to include by default.
+ * @param string $pluginName Name of the plugin. E.g. TagManager.
+ * @param \ReflectionMethod $reflectionMethod The reflective representation of the method to provide metadata.
+ *
+ * @return array
+ * @throws \Throwable
+ */
protected function buildAnnotationForMethod(array $rules, string $pluginName, \ReflectionMethod $reflectionMethod): array
{
$existing = $reflectionMethod->getDocComment();
@@ -151,38 +256,56 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R
$isPost = !empty($rules['plugins'][$pluginName]['methodsRequiringPost'])
&& in_array($methodName, $rules['plugins'][$pluginName]['methodsRequiringPost']);
- return $this->compileOperationLines($path, $opId, $pluginName, $methodName, $params, $responses, $isPost);
+ return $this->compileOperationLines($path, $opId, $pluginName, $params, $responses, $isPost);
}
- protected function getParamInfoFromDocBlock(string $docBlock): array
+ /**
+ * Try to extract the list of parameters and key information about them from the method's doc block string.
+ *
+ * @param string $docBlock The comment block from a method, which hopefully contains the param annotations.
+ *
+ * @return array Of each param provided in the comment block and key information about them like the type and
+ * description, if available. The array can be empty if there are no param annotations present. E.g.
+ * ['idSite' => ['type' => 'integer', 'description' => 'Site ID'], 'date' => ['type' => 'string', 'description' => '']]
+ */
+ public function getParamInfoFromDocBlock(string $docBlock): array
{
- $lexer = new Lexer();
+ $lexer = new Lexer();
$tokens = $lexer->tokenize($docBlock);
$expressionParser = new ConstExprParser();
$parser = new PhpDocParser(new TypeParser($expressionParser), $expressionParser);
- $node = $parser->parse(new TokenIterator($tokens));
+ $node = $parser->parse(new TokenIterator($tokens));
$params = [];
foreach ($node->getParamTagValues() as $param) {
$name = ltrim($param->parameterName, '$');
$params[$name] = [
- 'type' => (string) $param->type,
+ 'type' => (string)$param->type,
// Normalise the description. E.g. remove linebreaks and indentation
- 'description' => trim(preg_replace(['/^\h+/m', '/\R+/u',], ['', ' '], $param->description)),
- 'byRef' => $param->isReference,
+ 'description' => trim(preg_replace(['/^\h+/m', '/\R+/u',], ['', ' '], $param->description)),
+ 'byRef' => $param->isReference,
'variadic' => $param->isVariadic,
];
}
return $params;
}
- protected function getResponseInfoFromDocBlock(string $docBlock): array
+ /**
+ * Try to extract the response-type of a method from the doc block string.
+ *
+ * @param string $docBlock The comment block from a method, which hopefully contains the return annotation.
+ *
+ * @return array The collection of key information about the method's return type if any is found.
+ * E.g. ['type' => 'integer', 'description' => 'The ID of the newly created report.'] or ['type' => null] if no
+ * return annotation is present.
+ */
+ public function getResponseInfoFromDocBlock(string $docBlock): array
{
- $lexer = new Lexer();
+ $lexer = new Lexer();
$tokens = $lexer->tokenize($docBlock);
$expressionParser = new ConstExprParser();
$parser = new PhpDocParser(new TypeParser($expressionParser), $expressionParser);
- $node = $parser->parse(new TokenIterator($tokens));
+ $node = $parser->parse(new TokenIterator($tokens));
$responseInfo = ['type' => null];
$returnTags = $node->getReturnTagValues();
@@ -204,14 +327,53 @@ protected function getResponseInfoFromDocBlock(string $docBlock): array
return $responseInfo;
}
- protected function buildVirtualPath(string $virtualPathTemplate, string $plugin, string $method): string
+ /**
+ * This is a helper method for building the path used for an operation annotation. It takes a path template, like
+ * the one from the config array and populates it with the plugin name and API method name.
+ *
+ * @param string $virtualPathTemplate The template of what the path should be.
+ * E.g. /index.php?module=API&method={plugin}.{method}
+ * @param string $plugin The name of the plugin. E.g. TagManager
+ * @param string $method The name of the API method. E.g. getCustomReport
+ *
+ * @return string The finalised path to be used in an operation annotation.
+ * E.g. /index.php?module=API&method=CustomReports.getConfiguredReport
+ */
+ public function buildVirtualPath(string $virtualPathTemplate, string $plugin, string $method): string
{
return str_replace(['{plugin}', '{method}'], [$plugin, $method], $virtualPathTemplate);
}
- protected function buildParameterAnnotationData(string $paramName, array $paramMetadata, array $paramDocInfo): array
+ /**
+ * Build the key data for the specified parameter. This should be all the data necessary to create an OA\Parameter
+ * annotation object.
+ *
+ * @param string $methodName The name of the method. E.g. getAlert
+ * @param string $paramName The name of the parameter. E.g. idSite or period
+ * @param array $paramMetadata The collection of metadata from the old DocumentationGenerator class. Things like
+ * whether the parameter is typed, is required, or has a default value.
+ * @param array $paramDocInfo The collection of parameter information built from the method doc block. This is
+ * especially useful when the metadata wasn't able to determine the type. We can check the param annotation for the
+ * type and description.
+ *
+ * @return array The array of key information about the parameter like the type (types if more than one is hinted),
+ * the name, whether it's required, default value, and example. Since there may be more than one type from the doc
+ * block, the type is specified as a 'types' array even if there's only one type. E.g.
+ * [
+ * 'name' => 'idSite',
+ * 'types' => ['integer', 'string'],
+ * 'description' => 'The ID of the site.',
+ * 'required' => 'true', // It's a string here, but gets converted to boolean in the annotation.
+ * 'default' => '\Piwik\API\NoDefaultValue', // This class name indicates no default value since falsy values might be valid.
+ * 'example' => 1,
+ * ]
+ */
+ public function buildParameterAnnotationData(string $methodName, string $paramName, array $paramMetadata, array $paramDocInfo): array
{
$docType = strtolower(trim($paramDocInfo['type'] ?? ''));
+ if (empty($docType)) {
+ $this->addMissingImportantDataWarning($methodName, $paramName, 'Type is not specified in comment block.');
+ }
$metaType = strtolower(trim($paramMetadata['type'] ?? $docType));
$type = $metaType === 'string' && $docType !== 'string' ? $docType : $metaType;
// If the signature type is array, but the type hinting provides more, use that instead
@@ -233,6 +395,9 @@ protected function buildParameterAnnotationData(string $paramName, array $paramM
$isRequired = !key_exists('default', $paramMetadata) || $paramMetadata['default'] instanceof NoDefaultValue;
$description = $paramDocInfo['description'] ?? '';
+ if (empty($description)) {
+ $this->addMissingImportantDataWarning($methodName, $paramName, 'Description is not specified in comment block.');
+ }
$example = '';
// Check the description for the example value
if (preg_match('/\[@example\s*=\s*([^\n]+)\]/', $description, $m)) {
@@ -246,6 +411,10 @@ protected function buildParameterAnnotationData(string $paramName, array $paramM
$example = trim($example, '"');
}
+ // Clean up the descriptions a little more like removing linebreaks and escaping double-quotes
+ $description = str_replace("\n", ' ', $description);
+ $description = str_replace('"', '""', $description);
+
return [
'name' => $paramName,
'types' => $typesMap,
@@ -256,6 +425,60 @@ protected function buildParameterAnnotationData(string $paramName, array $paramM
];
}
+ /**
+ * Add an entry to the map of warnings about missing important information, like type and description of parameters
+ * and returns.
+ *
+ * @param string $methodName Name of the method to more easily identify where in the code needs adjustment.
+ * @param string $paramName Name of the parameter or "return" for the response. E.g. idSite, period, return, ...
+ * @param string $message Message indicating what is missing. E.g. "Type is not specified in comment block."
+ *
+ * @return void
+ */
+ protected function addMissingImportantDataWarning(string $methodName, string $paramName, string $message): void
+ {
+ // Make sure that the inner arrays have been initialised and then add the message to the warning map
+ $this->missingImportantDataWarnings[$methodName] = $this->missingImportantDataWarnings[$methodName] ?? [];
+ $this->missingImportantDataWarnings[$methodName][$paramName] = $this->missingImportantDataWarnings[$methodName][$paramName] ?? [];
+ $this->missingImportantDataWarnings[$methodName][$paramName][] = $message;
+ }
+
+ /**
+ * Remove a warning from the collection. This is useful when it's determined after the fact that a parameter has
+ * a global component which can be used, like idSite or period.
+ *
+ * @param string $methodName Name of the method.
+ * @param string $paramName Name of the parameter or "return" for the response. E.g. idSite, period, return, ...
+ *
+ * @return void
+ */
+ protected function removeMissingImportantDataWarning(string $methodName, string $paramName): void
+ {
+ if (empty($this->missingImportantDataWarnings[$methodName][$paramName])) {
+ return;
+ }
+
+ // If it's the only param in the collection for the method, remove the method
+ if (count($this->missingImportantDataWarnings[$methodName]) === 1) {
+ unset($this->missingImportantDataWarnings[$methodName]);
+ return;
+ }
+
+ unset($this->missingImportantDataWarnings[$methodName][$paramName]);
+ }
+
+ /**
+ * Build the collection of parameters and key information about them for the specified method.
+ *
+ * @param array $rules An array of configs determining which responses to include by default.
+ * @param string $plugin Name of the plugin. E.g. TagManager.
+ * @param string $method The name of the method being annotated.
+ * @param \ReflectionMethod $reflectionMethod The reflective representation of the method to provide metadata.
+ *
+ * @return array List of each method parameter and key data points like the data type, whether it's required,
+ * default value, and example value.
+ * @see self::buildParameterAnnotationData() where the parameter data is built.
+ */
protected function determineParameters(array $rules, string $plugin, string $method, \ReflectionMethod $reflectionMethod): array
{
$refs = [];
@@ -269,7 +492,11 @@ protected function determineParameters(array $rules, string $plugin, string $met
}
$paramsMetadata = Proxy::getInstance()->getParametersListWithTypes(Request::getClassNameAPI($plugin), $method);
- $paramsInfo = $this->getParamInfoFromDocBlock($reflectionMethod->getDocComment());
+ $paramsInfo = [];
+ $docBlock = $reflectionMethod->getDocComment();
+ if (!empty($docBlock)) {
+ $paramsInfo = $this->getParamInfoFromDocBlock($docBlock);
+ }
$customParams = [];
foreach ($paramsMetadata as $name => $paramMetadata) {
@@ -280,7 +507,16 @@ protected function determineParameters(array $rules, string $plugin, string $met
continue;
}
- $customParams[] = $this->buildParameterAnnotationData($name, $paramMetadata, $paramInfo);
+ // If the parameter doesn't have a description and matches a global, use a reference to the global instead.
+ $customParamData = $this->buildParameterAnnotationData($method, $name, $paramMetadata, $paramInfo);
+ if (empty($customParamData['description']) && in_array($name, self::GLOBAL_PARAMETER_NAMES)) {
+ $globalParamSuffix = $customParamData['required'] === 'true' ? 'Required' : 'Optional';
+ $refs[] = '#/components/parameters/' . $name . $globalParamSuffix;
+ $this->removeMissingImportantDataWarning($method, $name);
+ continue;
+ }
+
+ $customParams[] = $customParamData;
}
return [
@@ -296,6 +532,7 @@ protected function determineParameters(array $rules, string $plugin, string $met
* @link https://spec.openapis.org/oas/v3.1.1.html#data-types
*
* @param string $type The PHP type from the method signature or doc-block
+ *
* @return string The normalised Data Type to be used in the swagger-php annotation
*/
public function getOpenApiTypeFromPhpType(string $type): string
@@ -331,6 +568,24 @@ public function getOpenApiTypeFromPhpType(string $type): string
return $type;
}
+ /**
+ * Try to build example URLs for a specific API method. This uses the old DocumentationGenerator to build the same
+ * example URLs which have been available on the API documentation page for a long time. E.g. XML, JSON, and TSV.
+ * Unlike the old documentation, this only includes URLs if a valid response was received from the demo server or
+ * local Matomo instance. For example, some endpoints respond that the data structure is not TSV compatible and the
+ * old documentation would still include the link. This shows 'TSV (N/A)' in those instances.
+ *
+ * @param string $pluginName The name of the plugin. E.g. TagManager.
+ * @param string $methodName The name of the plugin specific API method. E.g. getCustomReport.
+ * @param array[] $paramsData The collection of parameter data compiled using reflection and metadata. This includes
+ * types, default values, and examples. It can be used to build URLs using required parameters which aren't globals,
+ * like idSite and period which have established example values.
+ *
+ * @return array The example URLs with only the required query parameters and only if a valid example responses were
+ * received when the URL was queried. Empty string if no URL could be determined or no valid response was received.
+ * E.g. ['xml => 'https://demo...&format=xml', 'json' => 'https://demo...&format=JSON', 'tsv' => 'https://demo...&format=Tsv']
+ * @throws \Throwable
+ */
protected function getApplicableDemoExampleUrls(string $pluginName, string $methodName, array $paramsData): array
{
// Get the example URLs for the success responses
@@ -340,6 +595,14 @@ protected function getApplicableDemoExampleUrls(string $pluginName, string $meth
'date' => 'today',
];
+ // Don't build example URLs for anything that isn't the R in CRUD. E.g. No create, update, or delete.
+ $notAllowedExampleUrlOperations = ['create', 'add', 'save', 'set', 'update', 'delete', 'remove', 'copy', 'duplicate'];
+ foreach ($notAllowedExampleUrlOperations as $operation) {
+ if (stripos($methodName, $operation) === 0) {
+ return [];
+ }
+ }
+
$parametersToReplace = [];
if (!empty($paramsData['custom'])) {
foreach ($paramsData['custom'] as $customParam) {
@@ -393,6 +656,17 @@ protected function getApplicableDemoExampleUrls(string $pluginName, string $meth
];
}
+ /**
+ * Query demo.matomo.cloud for report metadata which can later be used to help determine good example URLs for
+ * specific API endpoints. This method is only used when the example URL can't be determined using the default
+ * method. This only works for endpoints associated with reports and have metadata provided by the containing
+ * plugin. The response is cached as a property so the request is only made once regardless of how many times this
+ * method is called. The exception is if a valid response wasn't received. In that case, it will keep making the
+ * request until a non-empty response is received.
+ *
+ * @return array|array[] The decoded JSON array of all the report metadata from the demo server.
+ * @throws \Exception
+ */
protected function getDemoReportMetadata(): array
{
if (is_array($this->reportMetadata) && count($this->reportMetadata)) {
@@ -400,20 +674,25 @@ protected function getDemoReportMetadata(): array
}
$url = 'https://demo.matomo.cloud/index.php?module=API&method=API.getReportMetadata&format=JSON&idSite=1&hideMetricsDoc=0&showSubtableReports=0&filter_limit=-1&period=day';
- $response = Http::sendHttpRequestBy(
- Http::getTransportMethod(),
- $url,
- $timeout = 10,
- $userAgent = null,
- $destinationPath = null,
- $file = null,
- $followDepth = 0,
- $acceptLanguage = false,
- $acceptInvalidSslCertificate = true,
- $byteRange = false,
- $getExtendedInfo = true,
- $httpMethod = 'GET'
- );
+ try {
+ $response = Http::sendHttpRequestBy(
+ Http::getTransportMethod(),
+ $url,
+ $timeout = 30, // We can use a somewhat longer timeout for this request since it's cached afterward.
+ $userAgent = null,
+ $destinationPath = null,
+ $file = null,
+ $followDepth = 0,
+ $acceptLanguage = false,
+ $acceptInvalidSslCertificate = true,
+ $byteRange = false,
+ $getExtendedInfo = true,
+ $httpMethod = 'GET'
+ );
+ } catch (\Exception $e) {
+ // Add a little bit more context for troubleshooting the failed request
+ throw new \Exception('Error getting report metadata from URL: ' . $url . PHP_EOL . $e, 0, $e);
+ }
if (empty($response['data']) || ($response['status'] ?? 1) !== 200 || strpos($response['data'], 'Error: ') === 0) {
return [];
@@ -424,6 +703,20 @@ protected function getDemoReportMetadata(): array
return $this->reportMetadata;
}
+ /**
+ * Take the example URL and query the endpoint for an example response, hiding subtables. If a response isn't
+ * received from demo.matomo.cloud, it can try using a temporary token to make the request against the current
+ * instance of Matomo.
+ *
+ * @param string $url The full example URL. E.g.
+ * https://demo.matomo.cloud/?module=API&method=CustomReports.getConfiguredReports&idSite=1&format=xml&token_auth=anonymous
+ * @param bool $useLocalToken A boolean indicating whether to get a temporary token and try the request against the
+ * currently running Matomo instance.
+ *
+ * @return string The response received from the API endpoint if no error was received or the response wasn't empty.
+ * An empty string is returned by default.
+ * @throws \Throwable
+ */
protected function getExampleIfAvailable(string $url, bool $useLocalToken = false): string
{
// If the flag to use a temp token is set, get a token and update the request URL
@@ -449,7 +742,8 @@ protected function getExampleIfAvailable(string $url, bool $useLocalToken = fals
$httpMethod = 'GET'
);
} catch (\Throwable $e) {
- throw $e;
+ // Add a little bit more context for troubleshooting the failed request
+ throw new \Exception('Error getting example from URL: ' . $url . PHP_EOL . $e, 0, $e);
}
// If the example didn't load or resulted in an error, simply return an empty string
@@ -460,11 +754,13 @@ protected function getExampleIfAvailable(string $url, bool $useLocalToken = fals
|| stripos($response['data'], '"result":"error"') !== false
|| stripos($response['data'], '') !== false
|| trim($response['data']) === '[]'
+ || (stripos($url, 'format=tsv') !== false && trim($response['data']) === 'No data available')
) {
return '';
}
$body = $response['data'];
+ // Convert the XML responses into a JSON object and then encode it into a string. This is helpful for building schemas.
if (stripos($url, 'format=xml') !== false) {
$body = json_encode($this->convertExampleXmlToObject($body));
}
@@ -472,6 +768,19 @@ protected function getExampleIfAvailable(string $url, bool $useLocalToken = fals
return $body;
}
+ /**
+ * Try to build an example URL for a specific API method using report metadata. This queries the demo server for
+ * report metadata to get examples of existing reports which can be used as example URLS. If no metadata matches the
+ * provided plugin and method, an empty string is returned. Likewise, when no valid response is received for the
+ * URL. NOTE: This should only be used if the old DocumentationGenerator didn't provide example URLs.
+ *
+ * @param string $pluginName The name of the plugin. E.g. TagManager.
+ * @param string $methodName The name of the plugin specific API method. E.g. getCustomReport.
+ *
+ * @return string The example URL with only the required query parameters and only if a valid example response was
+ * received when the URL was queried. Empty string if no URL could be determined or no valid response was received.
+ * @throws \Throwable
+ */
protected function getReportExampleUrlFromMetadata(string $pluginName, string $methodName): string
{
$metadataArray = $this->getDemoReportMetadata();
@@ -486,7 +795,7 @@ protected function getReportExampleUrlFromMetadata(string $pluginName, string $m
// Keep trying until we find a good match
if ($metadata['module'] === $pluginName && $metadata['action'] === $methodName) {
- if (empty($metadata) || empty($metadata['imageGraphUrl'])) {
+ if (empty($metadata['imageGraphUrl'])) {
continue;
}
@@ -512,7 +821,19 @@ protected function getReportExampleUrlFromMetadata(string $pluginName, string $m
return '';
}
- protected function convertExampleXmlToObject(string $xml): array
+ /**
+ * Take an XML string, deserialise it, and convert it into a JSON object structured correctly for an XML schema
+ * example. E.g. Value1
Value2
to ["row" => ["Value1","Value2"]] or
+ * Value1
Value2
to
+ * ["row" => [{"child":"Value1"},{"child":"Value2"}]]
+ *
+ * @param string $xml The XML string of an example response for an API endpoint.
+ *
+ * @return array The array representation of the JSON object example structured correctly for an XML schema. E.g.
+ * ["row" => [{"child":"Value1"},{"child":"Value2"}]]
+ * @throws \Exception
+ */
+ public function convertExampleXmlToObject(string $xml): array
{
$root = new \SimpleXMLElement($xml);
@@ -536,18 +857,35 @@ protected function convertExampleXmlToObject(string $xml): array
return [$result];
}
- // Return the object that goes into example
- return $result; // e.g., [ "row" => [ {...}, {...} ] ]
+ return $result;
}
-
+ /**
+ * Build the array of potential responses for the API method. E.g. a response for 200, 400, 401, etc.
+ *
+ * @param array $rules An array of configs determining which responses to include by default.
+ * @param string $plugin Name of the plugin. E.g. TagManager.
+ * @param string $method The name of the method being annotated.
+ * @param \ReflectionMethod $reflectionMethod The reflective representation of the method to provide metadata.
+ * @param array $paramsData An array of already built method parameter data. This is used while building example
+ * URLs because the generator doesn't know what value to use for non-global parameters like idSite and period. We
+ * check the paramsData to see if an example value was provided for all the required parameters so that an example
+ * can be queried.
+ *
+ * @return array A collection of annotation lines for each of the expected potential responses for the method.
+ * @throws \Throwable
+ */
protected function determineResponses(array $rules, string $plugin, string $method, \ReflectionMethod $reflectionMethod, array $paramsData): array
{
$responses = [];
// Try to determine the success response using the return type and/or doc-block return type
$returnType = $reflectionMethod->getReturnType();
- $responseInfo = $this->getResponseInfoFromDocBlock($reflectionMethod->getDocComment());
+ $responseInfo = [];
+ $docBlock = $reflectionMethod->getDocComment();
+ if (!empty($docBlock)) {
+ $responseInfo = $this->getResponseInfoFromDocBlock($docBlock);
+ }
if (!empty($returnType) && $returnType->isBuiltin()) {
$responseInfo['type'] = $this->getOpenApiTypeFromPhpType(strval($returnType));
}
@@ -592,6 +930,8 @@ protected function determineResponses(array $rules, string $plugin, string $meth
if (!empty($responseInfo['description'])) {
$successArray['description'] = $responseInfo['description'];
+ } elseif (empty($successArray['ref'])) {
+ $this->addMissingImportantDataWarning($method, 'return', 'Description is not specified in comment block.');
}
$responseSchema = !empty($responseInfo['type']) ? $this->buildSchemaObjectArray($responseInfo['type']) : [];
@@ -605,11 +945,7 @@ protected function determineResponses(array $rules, string $plugin, string $meth
if ($type === 'tsv') {
$url .= '&convertToUnicode=0';
}
- try {
- $exampleValue = $this->getExampleIfAvailable($url);
- } catch (\Throwable $e) {
- throw new \Exception('Error getting example from URL: ' . $url . PHP_EOL . $e, 0, $e);
- }
+ $exampleValue = $this->getExampleIfAvailable($url);
// If the example lookup failed, try making the same request locally
$isLocalExample = false;
if (empty($exampleValue)) {
@@ -667,7 +1003,7 @@ protected function determineResponses(array $rules, string $plugin, string $meth
$successArray['description'] = '';
}
} else {
- // Make sure the schema is included in there are no examples
+ // Make sure the schema is included if there are no examples
$successArray['schema'] = $responseSchema;
}
@@ -681,6 +1017,10 @@ protected function determineResponses(array $rules, string $plugin, string $meth
// Append the links to the description with a prefix linebreak. If there's no description, skip the break
$successArray['description'] .= (!empty($successArray['description']) && !empty($descriptionLinks) ? '' : '') . $descriptionLinks;
+ if (empty($successArray['ref']) && empty($descriptionLinks) && empty($successArray['schema'])) {
+ $this->addMissingImportantDataWarning($method, 'return', 'Type could not be determined via comment block or example.');
+ }
+
$responses[] = $successArray;
if (!empty($rules['defaultErrorResponseRefs'])) {
@@ -692,7 +1032,20 @@ protected function determineResponses(array $rules, string $plugin, string $meth
return $responses;
}
- protected function cutExampleCloseToCharLimit(string $exampleValue, string $type): string
+ /**
+ * Take a string example and make sure that it is close to the max char limit. There's a little wiggle room due to
+ * wrapping elements and whitespace characters, but it should be within 100 characters of the limit. To do this, we
+ * deserialise the example based on type and iterate over the first-level properties and append them to a new
+ * example string. If a single property exceeds the limit or will if added to the newly built string, we skip it. If
+ * none of the base properties are small enough, we simply return an empty string.
+ *
+ * @param string $exampleValue The example response received from the demo or other server.
+ * @param string $type The type of the parameter. E.g. xml, json, or tsv
+ *
+ * @return string A new example string within a reasonable variation from the limit. If no row of the example fits
+ * within the limit, the result is an empty string.
+ */
+ public function cutExampleCloseToCharLimit(string $exampleValue, string $type): string
{
if (empty($exampleValue)) {
return '';
@@ -725,7 +1078,7 @@ protected function cutExampleCloseToCharLimit(string $exampleValue, string $type
$rows = $decodedRows['row'];
}
$newRows = [];
- foreach ($rows as $row) {
+ foreach ($rows as $key => $row) {
// Don't add the row if it would exceed the limit
if (
strlen(json_encode($row)) > self::EXAMPLE_CHAR_LIMIT
@@ -734,6 +1087,13 @@ protected function cutExampleCloseToCharLimit(string $exampleValue, string $type
continue;
}
+ // If it's a named element, add it back by name
+ if (is_string($key)) {
+ $newRows[$key] = $row;
+ continue;
+ }
+
+ // Since it wasn't a named row, it must be an array can simply be added back
$newRows[] = $row;
}
@@ -741,7 +1101,7 @@ protected function cutExampleCloseToCharLimit(string $exampleValue, string $type
return '';
}
- if (!empty($decodedRows['row'])) {
+ if (!empty($decodedRows['row']) && is_array($decodedRows['row'])) {
$decodedRows['row'] = $newRows;
} else {
$decodedRows = $newRows;
@@ -750,7 +1110,14 @@ protected function cutExampleCloseToCharLimit(string $exampleValue, string $type
return json_encode($decodedRows);
}
- protected function buildSchemaAnnotationFromJsonExample(array $jsonArrayObject): array
+ /**
+ * Take the deserialised structure of an JSON object and build the lines of an OA\Schema annotation object for it.
+ *
+ * @param array $jsonArrayObject Nested array of properties of the JSON object.
+ *
+ * @return array Collection of potentially nested arrays representing an OA\Property annotation object.
+ */
+ public function buildSchemaAnnotationFromJsonExample(array $jsonArrayObject): array
{
// Since the schema is pretty much the same as the property, let's just build a property and replace the key
$propertyLines = $this->buildPropertyAnnotationFromJsonExample('', $jsonArrayObject);
@@ -758,7 +1125,15 @@ protected function buildSchemaAnnotationFromJsonExample(array $jsonArrayObject):
return ['@OA\Schema' => $propertyLines['@OA\Property']];
}
- protected function buildPropertyAnnotationFromJsonExample(string $propName, array $values): array
+ /**
+ * Take the deserialised structure of an JSON object and build the lines of an OA\Property annotation object for it.
+ *
+ * @param string $propName Name of the JSON property.
+ * @param array $values Nested array of properties of the JSON property.
+ *
+ * @return array Collection of potentially nested arrays representing an OA\Property annotation object.
+ */
+ public function buildPropertyAnnotationFromJsonExample(string $propName, array $values): array
{
$type = 'object';
// If the first key isn't a string, it's an array
@@ -793,7 +1168,7 @@ protected function buildPropertyAnnotationFromJsonExample(string $propName, arra
foreach ($values as $key => $value) {
// If it's not an array, add a simple property string and skip to the next child
if (!is_array($value)) {
- $typesString = '"string", "number", "integer", "boolean", "array", "object", "null"';
+ $typesString = '{"string", "number", "integer", "boolean", "array", "object", "null"}';
if (is_string($value)) {
$typesString = '"string"';
} elseif (is_int($value)) {
@@ -801,7 +1176,7 @@ protected function buildPropertyAnnotationFromJsonExample(string $propName, arra
} elseif (is_bool($value)) {
$typesString = '"boolean"';
}
- $childLines[] = sprintf('@OA\Property(property="%s", type={%s})', $key, $typesString);
+ $childLines[] = sprintf('@OA\Property(property="%s", type=%s)', $key, $typesString);
continue;
}
@@ -811,7 +1186,15 @@ protected function buildPropertyAnnotationFromJsonExample(string $propName, arra
return ['@OA\Property' => array_merge($propertyLines, $childLines)];
}
- protected function buildSchemaAnnotationFromXmlExample(array $xmlArrayObject, string $root = 'result'): array
+ /**
+ * Take the deserialised structure of an XML node and build the lines of an OA\Schema annotation object for it.
+ *
+ * @param array $xmlArrayObject Nested array of properties of the XML node.
+ * @param string $root Name of the root element. The default is 'result'.
+ *
+ * @return array Collection of potentially nested arrays representing an OA\Property annotation object.
+ */
+ public function buildSchemaAnnotationFromXmlExample(array $xmlArrayObject, string $root = 'result'): array
{
$lines = [
'type="object",',
@@ -838,7 +1221,15 @@ protected function buildSchemaAnnotationFromXmlExample(array $xmlArrayObject, st
return ['@OA\Schema' => $lines];
}
- protected function buildPropertyAnnotationFromXmlExample(string $propName, array $values): array
+ /**
+ * Take the deserialised structure of an XML node and build the lines of an OA\Property annotation object for it.
+ *
+ * @param string $propName Name of the XML node.
+ * @param array $values Nested array of properties of the XML node.
+ *
+ * @return array Collection of potentially nested arrays representing an OA\Property annotation object.
+ */
+ public function buildPropertyAnnotationFromXmlExample(string $propName, array $values): array
{
$type = 'object';
if ($propName === 'row') {
@@ -894,7 +1285,14 @@ protected function buildPropertyAnnotationFromXmlExample(string $propName, array
return ['@OA\Property' => array_merge($propertyLines, $childLines)];
}
- protected function removeTrailingCommaFromLastLine(&$lines): void
+ /**
+ * Take a list of lines and remove the trailing comma from the last line.
+ *
+ * @param string[] $lines List of lines for an annotation passed by reference.
+ *
+ * @return void
+ */
+ public function removeTrailingCommaFromLastLine(array &$lines): void
{
if (!empty($lines)) {
$last = array_pop($lines);
@@ -902,7 +1300,18 @@ protected function removeTrailingCommaFromLastLine(&$lines): void
}
}
- protected function buildLinesForAnnotationObject(string $objectName, array $objectProperties, int $indent = 0): array
+ /**
+ * Generic method for building the array of lines for an annotation object. It handles adding the indent based on
+ * the level. For example, if it's nested under 3 other objects, the indent will be 12 spaces (3 x 4-space tabs).
+ *
+ * @param string $objectName The type of object. E.g. OA\Schema or OA\Property
+ * @param array $objectProperties A nested array of the properties of the object. E.g. type, example, OA\Schema, ...
+ * @param int $indent The count of indents/tabs based on the nesting the object. E.g. 0 = none & 2 = indented twice.
+ *
+ * @return array The lines of the annotation object with correct opening/closing characters (usually parenthesis),
+ * and proper indentation for each line.
+ */
+ public function buildLinesForAnnotationObject(string $objectName, array $objectProperties, int $indent = 0): array
{
$indentString = str_repeat(' ', $indent);
$innerIndentString = str_repeat(' ', $indent + 1);
@@ -938,7 +1347,17 @@ protected function buildLinesForAnnotationObject(string $objectName, array $obje
return array_merge([$indentString . $objectName . $openingCharacter], $lines, [$indentString . $closingCharacter . ',']);
}
- protected function buildSchemaObjectArray(string $type, string $subType = '', string $default = NoDefaultValue::class, string $example = ''): array
+ /**
+ * Build the array of lines for the OA\Schema annotation object for a single type.
+ *
+ * @param string $type The type of the parameter. E.g. string, integer, number, boolean, array, ...
+ * @param string $subType This can specify the subtype for arrays. E.g. integer for int[] or string for string[].
+ * @param string $default The optional default value for the type. Default is no value.
+ * @param string $example The optional example value for the type. Default is empty string which indicated no value.
+ *
+ * @return array[]
+ */
+ public function buildSchemaObjectArray(string $type, string $subType = '', string $default = NoDefaultValue::class, string $example = ''): array
{
$schemaMap = ['type="' . $type . '"'];
if (($example) !== '') {
@@ -962,7 +1381,17 @@ protected function buildSchemaObjectArray(string $type, string $subType = '', st
return ['@OA\Schema' => $schemaMap];
}
- protected function wrapStringWithQuotes(string $string, string $type, string $quoteCharacter = '"'): string
+ /**
+ * Wrap an example of default value string with quotes. E.g. "exampleValue". Depending on the type and the value,
+ * the quotes may be omitted.
+ *
+ * @param string $string Value for the example or default.
+ * @param string $type The type of the parameter. E.g. string, integer, number, boolean, array, ...
+ * @param string $quoteCharacter What to wrap the value with, if it should be wrapped. The default is double quote.
+ *
+ * @return string
+ */
+ public function wrapStringWithQuotes(string $string, string $type, string $quoteCharacter = '"'): string
{
if (in_array($type, ['integer', 'boolean', 'array'])) {
return $string;
@@ -976,7 +1405,15 @@ protected function wrapStringWithQuotes(string $string, string $type, string $qu
return "{$quoteCharacter}{$string}{$quoteCharacter}";
}
- protected function shouldIncludeDefault(string $type, string $default = NoDefaultValue::class): bool
+ /**
+ * Indicates whether a specific parameter should include a default value in its annotation.
+ *
+ * @param string $type The type of the parameter. E.g. string, integer, number, boolean, array, ...
+ * @param string $default The default value from reflection or doc block.
+ *
+ * @return bool Whether a default value should be included or not.
+ */
+ public function shouldIncludeDefault(string $type, string $default = NoDefaultValue::class): bool
{
if ($default === NoDefaultValue::class) {
return false;
@@ -990,7 +1427,21 @@ protected function shouldIncludeDefault(string $type, string $default = NoDefaul
return true;
}
- protected function buildSchemaObjectArrays(array $typesMap, string $default = '', string $example = ''): array
+ /**
+ * Build the array for the OA\Schema annotation object for one or more types.
+ *
+ * @param array $typesMap The array of types where the keys are the types and the values are the subtypes, if any.
+ * E.g. ['string' => null, 'array' => 'integer'] for idSites which can be an array or comma-separated-string of IDs.
+ * The string key has a value of null because it has no subtype while the array has 'integer' because values should
+ * be int IDs.
+ * @param string $default The value to use as the default property of the schema. If it's an empty string, no
+ * default is set.
+ * @param string $example The value to use as the example property of the schema. If it's an empty string, no
+ * example is set.
+ *
+ * @return array[] The collection of lines which make up the schema annotation object.
+ */
+ public function buildSchemaObjectArrays(array $typesMap, string $default = '', string $example = ''): array
{
$schemas = [];
foreach ($typesMap as $type => $subType) {
@@ -1004,7 +1455,20 @@ protected function buildSchemaObjectArrays(array $typesMap, string $default = ''
return ['@OA\Schema' => ['oneOf={' => $schemas]];
}
- protected function compileOperationLines(string $path, string $opId, string $plugin, string $method, array $params, array $responses, bool $isPost = false): array
+ /**
+ * Build the full array of lines for an OA operation. E.g. OA\Get or OA\Post
+ *
+ * @param string $path The operation path. E.g. /index.php?module=API&method=CustomReports.getConfiguredReport
+ * @param string $opId The string which uniquely identifies the operation across the entire OpenAPI spec. In order
+ * to avoid potential duplicates, we use the plugin name and method name. E.g. CustomReports.getConfiguredReport
+ * @param string $plugin The name of the plugin. E.g. CustomReports
+ * @param array $params The compiled list of method parameters and key information about them, like type.
+ * @param array $responses compiled list of method expected responses and key information about them, like type.
+ * @param bool $isPost Indicates whether the operation is a POST. The default is false, meaning it's GET.
+ *
+ * @return string[] The array of all the lines of the operation annotation object.
+ */
+ public function compileOperationLines(string $path, string $opId, string $plugin, array $params, array $responses, bool $isPost = false): array
{
$operationValuesMap = [
'path="' . $path . '"',
@@ -1057,8 +1521,6 @@ protected function compileOperationLines(string $path, string $opId, string $plu
$operationValuesMap[] = ['@OA\Response' => $responsePropertyArray];
}
}
- // TODO - Remove this if it's determined that we won't ever use it
- //$operationValuesMap[] = 'x={"runtime"={"entry":"index.php","query":{"module":"API","method":"' . $plugin . '.' . $method . '"}}}';
$lines = $this->buildLinesForAnnotationObject('@OA\\' . ($isPost ? 'Post' : 'Get'), $operationValuesMap);
diff --git a/Annotations/GlobalApiComponents.php b/Annotations/GlobalApiComponents.php
index 2e2f30b..4f35ea8 100644
--- a/Annotations/GlobalApiComponents.php
+++ b/Annotations/GlobalApiComponents.php
@@ -323,6 +323,15 @@
* )
*
* Parameters specific to DataTables and Views
+ * @OA\Parameter(parameter="expandedOptional", name="expanded", in="query",
+ * description="If true, loads all subtables.", required=false,
+ * @OA\Schema(type="integer", enum={0,1}, example=0, default=0))
+ *
+ * @OA\Parameter(parameter="idSubtableOptional", name="idSubtable", in="query",
+ * description="An in-database subtable ID.", required=false,
+ * @OA\Schema(type="integer"))
+ *
+ * Parameters specific to DataTables and Views
* @OA\Parameter(parameter="flatOptional", name="flat", in="query",
* description="Flatten subtables into the parent table.", required=false,
* @OA\Schema(type="integer", enum={0,1}, example=0))
@@ -358,7 +367,7 @@
* description="Row index after which rows are removed.", required=false, @OA\Schema(type="integer"))
*
* @OA\Parameter(parameter="filter_limitOptional", name="filter_limit", in="query",
- * description="Maximum rows to return.", required=false, @OA\Schema(type="integer"))
+ * description="Maximum number of rows to return.", required=false, @OA\Schema(type="integer"))
*
* @OA\Parameter(parameter="filter_offsetOptional", name="filter_offset", in="query",
* description="Row offset.", required=false, @OA\Schema(type="integer"))
@@ -384,8 +393,11 @@
* @OA\Parameter(parameter="labelOptional", name="label", in="query",
* description="Keep only rows with these label(s). Supports path via '>' and arrays.", required=false, @OA\Schema(type="string"))
*
+ * @OA\Parameter(parameter="idGoalRequired", name="idGoal", in="query",
+ * description="The ID of a configured goal.", required=true, @OA\Schema(type="integer"))
+ *
* @OA\Parameter(parameter="idGoalOptional", name="idGoal", in="query",
- * description="Goal ID or special values (overview/minimal/full table).", required=false, @OA\Schema(type="string"))
+ * description="The ID of a configured goal.", required=false, @OA\Schema(type="integer"))
*/
class GlobalApiComponents
{
diff --git a/tests/Resources/ExampleResponses/CustomAlerts.getAlert.json b/tests/Resources/ExampleResponses/CustomAlerts.getAlert.json
new file mode 100644
index 0000000..2a31a00
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomAlerts.getAlert.json
@@ -0,0 +1,23 @@
+{
+ "idalert": 1,
+ "name": "Test Alert",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "greater_than",
+ "metric_matched": 500,
+ "compared_to": 7,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+}
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomAlerts.getAlert.xml b/tests/Resources/ExampleResponses/CustomAlerts.getAlert.xml
new file mode 100644
index 0000000..92e2fa1
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomAlerts.getAlert.xml
@@ -0,0 +1,26 @@
+
+
+ 1
+ Test Alert
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ greater_than
+ 500
+ 7
+ 1
+
+
+
+
+
+
+ 1
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomAlerts.getAlerts.json b/tests/Resources/ExampleResponses/CustomAlerts.getAlerts.json
new file mode 100644
index 0000000..321be39
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomAlerts.getAlerts.json
@@ -0,0 +1,48 @@
+[
+ {
+ "idalert": 1,
+ "name": "Test Alert",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "greater_than",
+ "metric_matched": 500,
+ "compared_to": 7,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idalert": 2,
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomAlerts.getAlerts.xml b/tests/Resources/ExampleResponses/CustomAlerts.getAlerts.xml
new file mode 100644
index 0000000..a3b9ed9
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomAlerts.getAlerts.xml
@@ -0,0 +1,53 @@
+
+
+
+ 1
+ Test Alert
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ greater_than
+ 500
+ 7
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomAlerts.getTriggeredAlerts.json b/tests/Resources/ExampleResponses/CustomAlerts.getTriggeredAlerts.json
new file mode 100644
index 0000000..d02ea32
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomAlerts.getTriggeredAlerts.json
@@ -0,0 +1,698 @@
+[
+ {
+ "idtriggered": 1,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-13 01:55:48",
+ "ts_last_sent": "2024-09-13 01:58:54",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 2,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-13 01:58:48",
+ "ts_last_sent": "2024-09-13 01:58:54",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 3,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-13 02:00:39",
+ "ts_last_sent": "2024-09-13 02:00:46",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 4,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-13 02:55:47",
+ "ts_last_sent": "2024-09-13 02:55:53",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 5,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-16 23:33:41",
+ "ts_last_sent": null,
+ "value_old": "500.000",
+ "value_new": "36.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 6,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-18 03:21:11",
+ "ts_last_sent": "2024-09-18 03:23:05",
+ "value_old": "36.000",
+ "value_new": "1.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 9,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-18 22:18:55",
+ "ts_last_sent": "2024-09-18 22:19:02",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 12,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-09-26 01:18:30",
+ "ts_last_sent": null,
+ "value_old": "2.000",
+ "value_new": "1.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 13,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-10-11 02:26:50",
+ "ts_last_sent": null,
+ "value_old": "1190.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 14,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2024-11-28 20:34:14",
+ "ts_last_sent": null,
+ "value_old": "2.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 15,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-03-27 03:39:26",
+ "ts_last_sent": "2025-03-27 03:39:26",
+ "value_old": "4.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 16,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-04-03 20:59:07",
+ "ts_last_sent": "2025-04-03 20:59:07",
+ "value_old": "243.000",
+ "value_new": "110.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 17,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-04 11:31:47",
+ "ts_last_sent": "2025-05-04 11:31:47",
+ "value_old": "330.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 18,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-10 11:31:23",
+ "ts_last_sent": "2025-05-10 11:31:24",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 19,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-16 11:31:19",
+ "ts_last_sent": "2025-05-16 11:31:19",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 20,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-18 11:31:24",
+ "ts_last_sent": "2025-05-18 11:31:24",
+ "value_old": "2.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 21,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-22 11:31:19",
+ "ts_last_sent": "2025-05-22 11:31:19",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 22,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-25 11:31:24",
+ "ts_last_sent": "2025-05-25 11:31:25",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 23,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-28 11:31:26",
+ "ts_last_sent": "2025-05-28 11:31:27",
+ "value_old": "304.000",
+ "value_new": "3.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 24,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-05-29 11:31:25",
+ "ts_last_sent": "2025-05-29 11:31:25",
+ "value_old": "3.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 25,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-07-06 11:31:34",
+ "ts_last_sent": "2025-07-06 11:31:34",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 26,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-07-16 11:31:35",
+ "ts_last_sent": "2025-07-16 11:31:36",
+ "value_old": "357.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 27,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-08-02 11:31:41",
+ "ts_last_sent": "2025-08-02 11:31:41",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ },
+ {
+ "idtriggered": 28,
+ "idalert": 2,
+ "idsite": 1,
+ "ts_triggered": "2025-08-09 11:31:44",
+ "ts_last_sent": "2025-08-09 11:31:45",
+ "value_old": "1.000",
+ "value_new": "0.000",
+ "name": "Test Visit Drop Previous Day",
+ "login": "someUserName",
+ "period": "day",
+ "report": "VisitsSummary_get",
+ "report_condition": null,
+ "report_matched": null,
+ "report_mediums": [
+ "email"
+ ],
+ "metric": "nb_uniq_visitors",
+ "metric_condition": "percentage_decrease_more_than",
+ "metric_matched": 20,
+ "compared_to": 1,
+ "email_me": 1,
+ "additional_emails": [],
+ "phone_numbers": [],
+ "slack_channel_id": null,
+ "id_sites": [
+ 1
+ ]
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomAlerts.getTriggeredAlerts.xml b/tests/Resources/ExampleResponses/CustomAlerts.getTriggeredAlerts.xml
new file mode 100644
index 0000000..d79beb9
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomAlerts.getTriggeredAlerts.xml
@@ -0,0 +1,747 @@
+
+
+
+ 1
+ 2
+ 1
+ 2024-09-13 01:55:48
+ 2024-09-13 01:58:54
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+ 2
+ 1
+ 2024-09-13 01:58:48
+ 2024-09-13 01:58:54
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 3
+ 2
+ 1
+ 2024-09-13 02:00:39
+ 2024-09-13 02:00:46
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 4
+ 2
+ 1
+ 2024-09-13 02:55:47
+ 2024-09-13 02:55:53
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 5
+ 2
+ 1
+ 2024-09-16 23:33:41
+
+ 500.000
+ 36.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 6
+ 2
+ 1
+ 2024-09-18 03:21:11
+ 2024-09-18 03:23:05
+ 36.000
+ 1.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 9
+ 2
+ 1
+ 2024-09-18 22:18:55
+ 2024-09-18 22:19:02
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 12
+ 2
+ 1
+ 2024-09-26 01:18:30
+
+ 2.000
+ 1.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 13
+ 2
+ 1
+ 2024-10-11 02:26:50
+
+ 1190.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 14
+ 2
+ 1
+ 2024-11-28 20:34:14
+
+ 2.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 15
+ 2
+ 1
+ 2025-03-27 03:39:26
+ 2025-03-27 03:39:26
+ 4.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 16
+ 2
+ 1
+ 2025-04-03 20:59:07
+ 2025-04-03 20:59:07
+ 243.000
+ 110.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 17
+ 2
+ 1
+ 2025-05-04 11:31:47
+ 2025-05-04 11:31:47
+ 330.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 18
+ 2
+ 1
+ 2025-05-10 11:31:23
+ 2025-05-10 11:31:24
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 19
+ 2
+ 1
+ 2025-05-16 11:31:19
+ 2025-05-16 11:31:19
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 20
+ 2
+ 1
+ 2025-05-18 11:31:24
+ 2025-05-18 11:31:24
+ 2.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 21
+ 2
+ 1
+ 2025-05-22 11:31:19
+ 2025-05-22 11:31:19
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 22
+ 2
+ 1
+ 2025-05-25 11:31:24
+ 2025-05-25 11:31:25
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 23
+ 2
+ 1
+ 2025-05-28 11:31:26
+ 2025-05-28 11:31:27
+ 304.000
+ 3.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 24
+ 2
+ 1
+ 2025-05-29 11:31:25
+ 2025-05-29 11:31:25
+ 3.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 25
+ 2
+ 1
+ 2025-07-06 11:31:34
+ 2025-07-06 11:31:34
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 26
+ 2
+ 1
+ 2025-07-16 11:31:35
+ 2025-07-16 11:31:36
+ 357.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 27
+ 2
+ 1
+ 2025-08-02 11:31:41
+ 2025-08-02 11:31:41
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ 28
+ 2
+ 1
+ 2025-08-09 11:31:44
+ 2025-08-09 11:31:45
+ 1.000
+ 0.000
+ Test Visit Drop Previous Day
+ someUserName
+ day
+ VisitsSummary_get
+
+
+
+ email
+
+ nb_uniq_visitors
+ percentage_decrease_more_than
+ 20
+ 1
+ 1
+
+
+
+
+
+
+ 1
+
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.json b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.json
new file mode 100644
index 0000000..575e0ba
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.json
@@ -0,0 +1,14 @@
+[
+ {
+ "value": "url",
+ "name": "Page URL"
+ },
+ {
+ "value": "urlparam",
+ "name": "Page URL Parameter"
+ },
+ {
+ "value": "action_name",
+ "name": "Page Title"
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.tsv b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.tsv
new file mode 100644
index 0000000..f6d25aa
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.tsv
@@ -0,0 +1,4 @@
+value name
+url Page URL
+urlparam Page URL Parameter
+action_name Page Title
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.xml b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.xml
new file mode 100644
index 0000000..7a39759
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableExtractionDimensions.xml
@@ -0,0 +1,15 @@
+
+
+
+ url
+ Page URL
+
+
+ urlparam
+ Page URL Parameter
+
+
+ action_name
+ Page Title
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.json b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.json
new file mode 100644
index 0000000..ed0b1d1
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.json
@@ -0,0 +1,18 @@
+[
+ {
+ "value": "visit",
+ "name": "Visit",
+ "numSlotsAvailable": 15,
+ "numSlotsUsed": 1,
+ "numSlotsLeft": 14,
+ "supportsExtractions": false
+ },
+ {
+ "value": "action",
+ "name": "Action",
+ "numSlotsAvailable": 15,
+ "numSlotsUsed": 5,
+ "numSlotsLeft": 10,
+ "supportsExtractions": true
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.tsv b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.tsv
new file mode 100644
index 0000000..e3d8171
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.tsv
@@ -0,0 +1,3 @@
+value name numSlotsAvailable numSlotsUsed numSlotsLeft supportsExtractions
+visit Visit 15 1 14 0
+action Action 15 5 10 1
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.xml b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.xml
new file mode 100644
index 0000000..24bb6c6
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getAvailableScopes.xml
@@ -0,0 +1,19 @@
+
+
+
+ visit
+ Visit
+ 15
+ 1
+ 14
+ 0
+
+
+ action
+ Action
+ 15
+ 5
+ 10
+ 1
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getConfiguredCustomDimensions.json b/tests/Resources/ExampleResponses/CustomDimensions.getConfiguredCustomDimensions.json
new file mode 100644
index 0000000..fa3ee12
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getConfiguredCustomDimensions.json
@@ -0,0 +1,87 @@
+[
+ {
+ "idcustomdimension": "1",
+ "idsite": "1",
+ "name": "User Type",
+ "index": "1",
+ "scope": "visit",
+ "active": true,
+ "extractions": [],
+ "case_sensitive": true
+ },
+ {
+ "idcustomdimension": "2",
+ "idsite": "1",
+ "name": "Page Author",
+ "index": "1",
+ "scope": "action",
+ "active": true,
+ "extractions": [
+ {
+ "dimension": "url",
+ "pattern": ""
+ }
+ ],
+ "case_sensitive": true
+ },
+ {
+ "idcustomdimension": "3",
+ "idsite": "1",
+ "name": "Page Age",
+ "index": "2",
+ "scope": "action",
+ "active": false,
+ "extractions": [
+ {
+ "dimension": "url",
+ "pattern": ""
+ }
+ ],
+ "case_sensitive": true
+ },
+ {
+ "idcustomdimension": "4",
+ "idsite": "1",
+ "name": "Page Location",
+ "index": "3",
+ "scope": "action",
+ "active": true,
+ "extractions": [
+ {
+ "dimension": "url",
+ "pattern": ""
+ }
+ ],
+ "case_sensitive": true
+ },
+ {
+ "idcustomdimension": "5",
+ "idsite": "1",
+ "name": "Page Type",
+ "index": "4",
+ "scope": "action",
+ "active": true,
+ "extractions": [
+ {
+ "dimension": "url",
+ "pattern": ""
+ }
+ ],
+ "case_sensitive": true
+ },
+ {
+ "idcustomdimension": "6",
+ "idsite": "1",
+ "name": "Diving Rating",
+ "index": "5",
+ "scope": "action",
+ "active": false,
+ "extractions": [
+ {
+ "dimension": "url",
+ "pattern": ""
+ }
+ ],
+ "case_sensitive": true
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getConfiguredCustomDimensions.xml b/tests/Resources/ExampleResponses/CustomDimensions.getConfiguredCustomDimensions.xml
new file mode 100644
index 0000000..4902a4a
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getConfiguredCustomDimensions.xml
@@ -0,0 +1,89 @@
+
+
+
+ 1
+ 1
+ User Type
+ 1
+ visit
+ 1
+
+
+ 1
+
+
+ 2
+ 1
+ Page Author
+ 1
+ action
+ 1
+
+
+ url
+
+
+
+ 1
+
+
+ 3
+ 1
+ Page Age
+ 2
+ action
+ 0
+
+
+ url
+
+
+
+ 1
+
+
+ 4
+ 1
+ Page Location
+ 3
+ action
+ 1
+
+
+ url
+
+
+
+ 1
+
+
+ 5
+ 1
+ Page Type
+ 4
+ action
+ 1
+
+
+ url
+
+
+
+ 1
+
+
+ 6
+ 1
+ Diving Rating
+ 5
+ action
+ 0
+
+
+ url
+
+
+
+ 1
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.json b/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.json
new file mode 100644
index 0000000..afb120b
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.json
@@ -0,0 +1,35 @@
+[
+ {
+ "label": "guest",
+ "nb_uniq_visitors": "140",
+ "nb_visits": "140",
+ "nb_actions": "307",
+ "max_actions": 29,
+ "sum_visit_length": "17864",
+ "bounce_count": "97",
+ "nb_visits_converted": "11",
+ "goals": {
+ "idgoal=6": {
+ "nb_conversions": 1,
+ "nb_visits_converted": 1,
+ "revenue": 2
+ },
+ "idgoal=7": {
+ "nb_conversions": 8,
+ "nb_visits_converted": 8,
+ "revenue": 8
+ },
+ "idgoal=8": {
+ "nb_conversions": 3,
+ "nb_visits_converted": 3,
+ "revenue": 0
+ }
+ },
+ "nb_conversions": 12,
+ "revenue": 10,
+ "avg_time_on_site": 128,
+ "bounce_rate": "69%",
+ "nb_actions_per_visit": 2.2,
+ "segment": "dimension1==guest"
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.tsv b/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.tsv
new file mode 100644
index 0000000..9afbb95
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.tsv
@@ -0,0 +1,2 @@
+label nb_uniq_visitors nb_visits nb_actions max_actions sum_visit_length bounce_count nb_visits_converted goals_idgoal=6_nb_conversions goals_idgoal=6_nb_visits_converted goals_idgoal=6_revenue goals_idgoal=7_nb_conversions goals_idgoal=7_nb_visits_converted goals_idgoal=7_revenue goals_idgoal=8_nb_conversions goals_idgoal=8_nb_visits_converted goals_idgoal=8_revenue nb_conversions revenue avg_time_on_site bounce_rate nb_actions_per_visit metadata_segment
+guest 140 140 307 29 17864 97 11 1 1 2 8 8 8 3 3 0 12 10 128 69% 2.2 dimension1==guest
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.xml b/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.xml
new file mode 100644
index 0000000..cc78beb
--- /dev/null
+++ b/tests/Resources/ExampleResponses/CustomDimensions.getCustomDimension.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ 140
+ 140
+ 307
+ 29
+ 17864
+ 97
+ 11
+
+
+ 1
+ 1
+ 2
+
+
+ 8
+ 8
+ 8
+
+
+ 3
+ 3
+ 0
+
+
+ 12
+ 10
+ 128
+ 69%
+ 2.2
+ dimension1==guest
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.json b/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.json
new file mode 100644
index 0000000..cc86e4f
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.json
@@ -0,0 +1 @@
+["file","database"]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.tsv b/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.tsv
new file mode 100644
index 0000000..b68212d
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.tsv
@@ -0,0 +1,2 @@
+file
+database
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.xml b/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.xml
new file mode 100644
index 0000000..cf23cc6
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getAvailableLogReaders.xml
@@ -0,0 +1,5 @@
+
+
+ file
+ database
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.json b/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.json
new file mode 100644
index 0000000..6272a5d
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.json
@@ -0,0 +1 @@
+["file"]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.tsv b/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.tsv
new file mode 100644
index 0000000..1a010b1
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.tsv
@@ -0,0 +1 @@
+file
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.xml b/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.xml
new file mode 100644
index 0000000..67d4d5d
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getConfiguredLogReaders.xml
@@ -0,0 +1,4 @@
+
+
+ file
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getLogConfig.json b/tests/Resources/ExampleResponses/LogViewer.getLogConfig.json
new file mode 100644
index 0000000..9b9ba44
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getLogConfig.json
@@ -0,0 +1,9 @@
+{
+ "log_writers": [
+ "screen",
+ "file"
+ ],
+ "log_level": "WARN",
+ "logger_file_path": "tmp\/logs\/matomo.log",
+ "logger_syslog_ident": "matomo"
+}
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getLogConfig.xml b/tests/Resources/ExampleResponses/LogViewer.getLogConfig.xml
new file mode 100644
index 0000000..c62062d
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getLogConfig.xml
@@ -0,0 +1,10 @@
+
+
+
+ screen
+ file
+
+ WARN
+ tmp/logs/matomo.log
+ matomo
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getLogEntries.json b/tests/Resources/ExampleResponses/LogViewer.getLogEntries.json
new file mode 100644
index 0000000..6b1ca9e
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getLogEntries.json
@@ -0,0 +1,72 @@
+[
+ {
+ "severity": "",
+ "tag": "",
+ "datetime": "",
+ "requestId": "",
+ "message": ""
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#17 {main} [Query: ?module=API&method=LogViewer.getLogConfig&format=Tsv&token_auth=removed&convertToUnicode=0&hideIdSubDatable=1, CLI mode: 0]"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#16 \/index.php(25): require_once('...')"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#15 \/core\/dispatch.php(33): Piwik\\FrontController->dispatch()"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#14 \/core\/FrontController.php(170): Piwik\\FrontController->doDispatch()"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#13 \/core\/FrontController.php(646): call_user_func_array()"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#12 [internal function]: Piwik\\Plugins\\API\\Controller->index()"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#11 \/plugins\/API\/Controller.php(48): Piwik\\API\\Request->process()"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#10 \/core\/API\/Request.php(282): Piwik\\Context::executeWithQueryParameters()"
+ },
+ {
+ "severity": "ERROR",
+ "tag": "API",
+ "datetime": "2025-09-19 07:55:22 UTC",
+ "requestId": "1bc99",
+ "message": "#9 \/core\/Context.php(29): Piwik\\API\\Request->{closure:Piwik\\API\\Request::process():282}()"
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getLogEntries.tsv b/tests/Resources/ExampleResponses/LogViewer.getLogEntries.tsv
new file mode 100644
index 0000000..6c0842f
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getLogEntries.tsv
@@ -0,0 +1,11 @@
+severity tag datetime requestId message
+
+ERROR API 2025-09-19 08:04:51 UTC 2904f "#17 {main} [Query: ?module=API&method=LogViewer.getLogConfig&format=Tsv&token_auth=removed&convertToUnicode=0&hideIdSubDatable=1, CLI mode: 0]"
+ERROR API 2025-09-19 08:04:51 UTC 2904f #16 /index.php(25): require_once('...')
+ERROR API 2025-09-19 08:04:51 UTC 2904f #15 /core/dispatch.php(33): Piwik\FrontController->dispatch()
+ERROR API 2025-09-19 08:04:51 UTC 2904f #14 /core/FrontController.php(170): Piwik\FrontController->doDispatch()
+ERROR API 2025-09-19 08:04:51 UTC 2904f #13 /core/FrontController.php(646): call_user_func_array()
+ERROR API 2025-09-19 08:04:51 UTC 2904f #12 [internal function]: Piwik\Plugins\API\Controller->index()
+ERROR API 2025-09-19 08:04:51 UTC 2904f #11 /plugins/API/Controller.php(48): Piwik\API\Request->process()
+ERROR API 2025-09-19 08:04:51 UTC 2904f #10 /core/API/Request.php(282): Piwik\Context::executeWithQueryParameters()
+ERROR API 2025-09-19 08:04:51 UTC 2904f #9 /core/Context.php(29): Piwik\API\Request->{closure:Piwik\API\Request::process():282}()
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/LogViewer.getLogEntries.xml b/tests/Resources/ExampleResponses/LogViewer.getLogEntries.xml
new file mode 100644
index 0000000..798d35c
--- /dev/null
+++ b/tests/Resources/ExampleResponses/LogViewer.getLogEntries.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #17 {main} [Query: ?module=API&method=LogViewer.getLogConfig&format=Tsv&token_auth=removed&
+ convertToUnicode=0&hideIdSubDatable=1, CLI mode: 0]
+
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #16 /index.php(25): require_once('...')
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #15 /core/dispatch.php(33): Piwik\FrontController->dispatch()
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #14 /core/FrontController.php(170): Piwik\FrontController->doDispatch()
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #13 /core/FrontController.php(646): call_user_func_array()
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #12 [internal function]: Piwik\Plugins\API\Controller->index()
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #11 /plugins/API/Controller.php(48): Piwik\API\Request->process()
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #10 /core/API/Request.php(282): Piwik\Context::executeWithQueryParameters()
+
+
+ ERROR
+ API
+ 2025-09-19 07:55:22 UTC
+ 1bc99
+ #9 /core/Context.php(29): Piwik\API\Request->{closure:Piwik\API\Request::process():282}()
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.json b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.json
new file mode 100644
index 0000000..4878a50
--- /dev/null
+++ b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.json
@@ -0,0 +1,69 @@
+[
+ {
+ "label": "more",
+ "nb_uniq_visitors": 1,
+ "nb_visits": 2,
+ "nb_actions": "12",
+ "nb_users": 1,
+ "max_actions": 8,
+ "sum_visit_length": "5",
+ "bounce_count": "0",
+ "nb_visits_converted": "2",
+ "goals": {
+ "idgoal=ecommerceOrder": {
+ "nb_conversions": 1,
+ "nb_visits_converted": 1,
+ "revenue": 232.56,
+ "revenue_subtotal": 204,
+ "revenue_tax": 38.76,
+ "revenue_shipping": 10.2,
+ "revenue_discount": 20.4,
+ "items": 3
+ },
+ "idgoal=1": {
+ "nb_conversions": 1,
+ "nb_visits_converted": 1,
+ "revenue": 0
+ }
+ },
+ "nb_conversions": 2,
+ "revenue": 232.56,
+ "segment": "campaignKeyword==more"
+ },
+ {
+ "label": "learnmore",
+ "nb_uniq_visitors": 1,
+ "nb_visits": 1,
+ "nb_actions": "1",
+ "nb_users": 1,
+ "max_actions": 1,
+ "sum_visit_length": "0",
+ "bounce_count": "1",
+ "nb_visits_converted": "0",
+ "segment": "campaignKeyword==learnmore"
+ },
+ {
+ "label": "ordernow",
+ "nb_uniq_visitors": 1,
+ "nb_visits": 1,
+ "nb_actions": "6",
+ "nb_users": 0,
+ "max_actions": 6,
+ "sum_visit_length": "1",
+ "bounce_count": "0",
+ "nb_visits_converted": "0",
+ "segment": "campaignKeyword==ordernow"
+ },
+ {
+ "label": "www.facebook.com",
+ "nb_uniq_visitors": 1,
+ "nb_visits": 1,
+ "nb_actions": "1",
+ "nb_users": 1,
+ "max_actions": 1,
+ "sum_visit_length": "0",
+ "bounce_count": "1",
+ "nb_visits_converted": "0",
+ "segment": "campaignKeyword==www.facebook.com"
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.tsv b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.tsv
new file mode 100644
index 0000000..cb090b6
--- /dev/null
+++ b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.tsv
@@ -0,0 +1,5 @@
+label nb_uniq_visitors nb_visits nb_actions nb_users max_actions sum_visit_length bounce_count nb_visits_converted goals_idgoal=ecommerceOrder_nb_conversions goals_idgoal=ecommerceOrder_nb_visits_converted goals_idgoal=ecommerceOrder_revenue goals_idgoal=ecommerceOrder_revenue_subtotal goals_idgoal=ecommerceOrder_revenue_tax goals_idgoal=ecommerceOrder_revenue_shipping goals_idgoal=ecommerceOrder_revenue_discount goals_idgoal=ecommerceOrder_items goals_idgoal=1_nb_conversions goals_idgoal=1_nb_visits_converted goals_idgoal=1_revenue nb_conversions revenue metadata_segment
+more 1 2 12 1 8 5 0 2 1 1 232.56 204 38.76 10.2 20.4 3 1 1 0 2 232.56 campaignKeyword==more
+learnmore 1 1 1 1 1 0 1 0 campaignKeyword==learnmore
+ordernow 1 1 6 0 6 1 0 0 campaignKeyword==ordernow
+www.facebook.com 1 1 1 1 1 0 1 0 campaignKeyword==www.facebook.com
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.xml b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.xml
new file mode 100644
index 0000000..bc4b055
--- /dev/null
+++ b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getKeyword.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ 1
+ 2
+ 12
+ 1
+ 8
+ 5
+ 0
+ 2
+
+
+ 1
+ 1
+ 232.56
+ 204
+ 38.76
+ 10.2
+ 20.4
+ 3
+
+
+ 1
+ 1
+ 0
+
+
+ 2
+ 232.56
+ campaignKeyword==more
+
+
+
+ 1
+ 1
+ 1
+ 1
+ 1
+ 0
+ 1
+ 0
+ campaignKeyword==learnmore
+
+
+
+ 1
+ 1
+ 6
+ 0
+ 6
+ 1
+ 0
+ 0
+ campaignKeyword==ordernow
+
+
+
+ 1
+ 1
+ 1
+ 1
+ 1
+ 0
+ 1
+ 0
+ campaignKeyword==www.facebook.com
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.json b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.json
new file mode 100644
index 0000000..bc2e135
--- /dev/null
+++ b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.json
@@ -0,0 +1,45 @@
+[
+ {
+ "label": "email-nov2011",
+ "nb_uniq_visitors": 3,
+ "nb_visits": 4,
+ "nb_actions": 19,
+ "nb_users": 2,
+ "max_actions": 8,
+ "sum_visit_length": 6,
+ "bounce_count": 1,
+ "nb_visits_converted": 2,
+ "goals": {
+ "idgoal=ecommerceOrder": {
+ "nb_conversions": 1,
+ "nb_visits_converted": 1,
+ "revenue": 232.56,
+ "revenue_subtotal": 204,
+ "revenue_tax": 38.76,
+ "revenue_shipping": 10.2,
+ "revenue_discount": 20.4,
+ "items": 3
+ },
+ "idgoal=1": {
+ "nb_conversions": 1,
+ "nb_visits_converted": 1,
+ "revenue": 0
+ }
+ },
+ "nb_conversions": 2,
+ "revenue": 232.56,
+ "segment": "campaignName==email-nov2011"
+ },
+ {
+ "label": "google-ads-campaign",
+ "nb_uniq_visitors": 2,
+ "nb_visits": 2,
+ "nb_actions": 2,
+ "nb_users": 2,
+ "max_actions": 1,
+ "sum_visit_length": 0,
+ "bounce_count": 2,
+ "nb_visits_converted": 0,
+ "segment": "campaignName==google-ads-campaign"
+ }
+]
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.tsv b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.tsv
new file mode 100644
index 0000000..aa96dde
--- /dev/null
+++ b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.tsv
@@ -0,0 +1,3 @@
+label nb_uniq_visitors nb_visits nb_actions nb_users max_actions sum_visit_length bounce_count nb_visits_converted goals_idgoal=ecommerceOrder_nb_conversions goals_idgoal=ecommerceOrder_nb_visits_converted goals_idgoal=ecommerceOrder_revenue goals_idgoal=ecommerceOrder_revenue_subtotal goals_idgoal=ecommerceOrder_revenue_tax goals_idgoal=ecommerceOrder_revenue_shipping goals_idgoal=ecommerceOrder_revenue_discount goals_idgoal=ecommerceOrder_items goals_idgoal=1_nb_conversions goals_idgoal=1_nb_visits_converted goals_idgoal=1_revenue nb_conversions revenue metadata_segment
+email-nov2011 3 4 19 2 8 6 1 2 1 1 232.56 204 38.76 10.2 20.4 3 1 1 0 2 232.56 campaignName==email-nov2011
+google-ads-campaign 2 2 2 2 1 0 2 0 campaignName==google-ads-campaign
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.xml b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.xml
new file mode 100644
index 0000000..7650320
--- /dev/null
+++ b/tests/Resources/ExampleResponses/MarketingCampaignsReporting.getName.xml
@@ -0,0 +1,46 @@
+
+
+
+
+ 3
+ 4
+ 19
+ 2
+ 8
+ 6
+ 1
+ 2
+
+
+ 1
+ 1
+ 232.56
+ 204
+ 38.76
+ 10.2
+ 20.4
+ 3
+
+
+ 1
+ 1
+ 0
+
+
+ 2
+ 232.56
+ campaignName==email-nov2011
+
+
+
+ 2
+ 2
+ 2
+ 2
+ 1
+ 0
+ 2
+ 0
+ campaignName==google-ads-campaign
+
+
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponsesNormalised/ExamplesFromDemoByType.json b/tests/Resources/ExampleResponsesNormalised/ExamplesFromDemoByType.json
new file mode 100644
index 0000000..7d25103
--- /dev/null
+++ b/tests/Resources/ExampleResponsesNormalised/ExamplesFromDemoByType.json
@@ -0,0 +1,16 @@
+{
+ "CustomDimensions.getCustomDimension": {
+ "xml": "{\"row\":{\"label\":\"guest\",\"nb_uniq_visitors\":\"140\",\"nb_visits\":\"140\",\"nb_actions\":\"307\",\"max_actions\":\"29\",\"sum_visit_length\":\"17864\",\"bounce_count\":\"97\",\"nb_visits_converted\":\"11\",\"goals\":{\"row\":[{\"nb_conversions\":\"1\",\"nb_visits_converted\":\"1\",\"revenue\":\"2\"},{\"nb_conversions\":\"8\",\"nb_visits_converted\":\"8\",\"revenue\":\"8\"},{\"nb_conversions\":\"3\",\"nb_visits_converted\":\"3\",\"revenue\":\"0\"}]},\"nb_conversions\":\"12\",\"revenue\":\"10\",\"avg_time_on_site\":\"128\",\"bounce_rate\":\"69%\",\"nb_actions_per_visit\":\"2.2\",\"segment\":\"dimension1==guest\"}}",
+ "json": "[{\"label\":\"guest\",\"nb_uniq_visitors\":\"140\",\"nb_visits\":\"140\",\"nb_actions\":\"307\",\"max_actions\":29,\"sum_visit_length\":\"17864\",\"bounce_count\":\"97\",\"nb_visits_converted\":\"11\",\"goals\":{\"idgoal=6\":{\"nb_conversions\":1,\"nb_visits_converted\":1,\"revenue\":2},\"idgoal=7\":{\"nb_conversions\":8,\"nb_visits_converted\":8,\"revenue\":8},\"idgoal=8\":{\"nb_conversions\":3,\"nb_visits_converted\":3,\"revenue\":0}},\"nb_conversions\":12,\"revenue\":10,\"avg_time_on_site\":128,\"bounce_rate\":\"69%\",\"nb_actions_per_visit\":2.2,\"segment\":\"dimension1==guest\"}]",
+ "tsv": "label\tnb_uniq_visitors\tnb_visits\tnb_actions\tmax_actions\tsum_visit_length\tbounce_count\tnb_visits_converted\tgoals_idgoal=6_nb_conversions\tgoals_idgoal=6_nb_visits_converted\tgoals_idgoal=6_revenue\tgoals_idgoal=7_nb_conversions\tgoals_idgoal=7_nb_visits_converted\tgoals_idgoal=7_revenue\tgoals_idgoal=8_nb_conversions\tgoals_idgoal=8_nb_visits_converted\tgoals_idgoal=8_revenue\tnb_conversions\trevenue\tavg_time_on_site\tbounce_rate\tnb_actions_per_visit\tmetadata_segment\nguest\t140\t140\t307\t29\t17864\t97\t11\t1\t1\t2\t8\t8\t8\t3\t3\t0\t12\t10\t128\t69%\t2.2\tdimension1==guest"
+ },
+ "CustomDimensions.getConfiguredCustomDimensions": {
+ "xml": "{\"row\":[{\"idcustomdimension\":\"1\",\"idsite\":\"1\",\"name\":\"User Type\",\"index\":\"1\",\"scope\":\"visit\",\"active\":\"1\",\"extractions\":\"\",\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"2\",\"idsite\":\"1\",\"name\":\"Page Author\",\"index\":\"1\",\"scope\":\"action\",\"active\":\"1\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"3\",\"idsite\":\"1\",\"name\":\"Page Age\",\"index\":\"2\",\"scope\":\"action\",\"active\":\"0\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"4\",\"idsite\":\"1\",\"name\":\"Page Location\",\"index\":\"3\",\"scope\":\"action\",\"active\":\"1\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"5\",\"idsite\":\"1\",\"name\":\"Page Type\",\"index\":\"4\",\"scope\":\"action\",\"active\":\"1\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"6\",\"idsite\":\"1\",\"name\":\"Diving Rating\",\"index\":\"5\",\"scope\":\"action\",\"active\":\"0\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"}]}",
+ "json": "[{\"idcustomdimension\":\"1\",\"idsite\":\"1\",\"name\":\"User Type\",\"index\":\"1\",\"scope\":\"visit\",\"active\":true,\"extractions\":[],\"case_sensitive\":true},{\"idcustomdimension\":\"2\",\"idsite\":\"1\",\"name\":\"Page Author\",\"index\":\"1\",\"scope\":\"action\",\"active\":true,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"3\",\"idsite\":\"1\",\"name\":\"Page Age\",\"index\":\"2\",\"scope\":\"action\",\"active\":false,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"4\",\"idsite\":\"1\",\"name\":\"Page Location\",\"index\":\"3\",\"scope\":\"action\",\"active\":true,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"5\",\"idsite\":\"1\",\"name\":\"Page Type\",\"index\":\"4\",\"scope\":\"action\",\"active\":true,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"6\",\"idsite\":\"1\",\"name\":\"Diving Rating\",\"index\":\"5\",\"scope\":\"action\",\"active\":false,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true}]"
+ },
+ "CustomDimensions.getAvailableScopes": {
+ "xml": "{\"row\":[{\"value\":\"visit\",\"name\":\"Visit\",\"numSlotsAvailable\":\"15\",\"numSlotsUsed\":\"1\",\"numSlotsLeft\":\"14\",\"supportsExtractions\":\"0\"},{\"value\":\"action\",\"name\":\"Action\",\"numSlotsAvailable\":\"15\",\"numSlotsUsed\":\"5\",\"numSlotsLeft\":\"10\",\"supportsExtractions\":\"1\"}]}",
+ "json": "[{\"value\":\"visit\",\"name\":\"Visit\",\"numSlotsAvailable\":15,\"numSlotsUsed\":1,\"numSlotsLeft\":14,\"supportsExtractions\":false},{\"value\":\"action\",\"name\":\"Action\",\"numSlotsAvailable\":15,\"numSlotsUsed\":5,\"numSlotsLeft\":10,\"supportsExtractions\":true}]",
+ "tsv": "value\tname\tnumSlotsAvailable\tnumSlotsUsed\tnumSlotsLeft\tsupportsExtractions\nvisit\tVisit\t15\t1\t14\t0\naction\tAction\t15\t5\t10\t1"
+ }
+}
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponsesNormalised/ExamplesFromLocalByType.json b/tests/Resources/ExampleResponsesNormalised/ExamplesFromLocalByType.json
new file mode 100644
index 0000000..cdfe517
--- /dev/null
+++ b/tests/Resources/ExampleResponsesNormalised/ExamplesFromLocalByType.json
@@ -0,0 +1,92 @@
+{
+ "MarketingCampaignsReporting.getId": {
+ "xml": "",
+ "json": "",
+ "tsv": ""
+ },
+ "MarketingCampaignsReporting.getName": {
+ "xml": "{\"row\":[{\"label\":\"email-nov2011\",\"nb_uniq_visitors\":\"3\",\"nb_visits\":\"4\",\"nb_actions\":\"19\",\"nb_users\":\"2\",\"max_actions\":\"8\",\"sum_visit_length\":\"6\",\"bounce_count\":\"1\",\"nb_visits_converted\":\"2\",\"goals\":{\"row\":[{\"nb_conversions\":\"1\",\"nb_visits_converted\":\"1\",\"revenue\":\"232.56\",\"revenue_subtotal\":\"204\",\"revenue_tax\":\"38.76\",\"revenue_shipping\":\"10.2\",\"revenue_discount\":\"20.4\",\"items\":\"3\"},{\"nb_conversions\":\"1\",\"nb_visits_converted\":\"1\",\"revenue\":\"0\"}]},\"nb_conversions\":\"2\",\"revenue\":\"232.56\",\"segment\":\"campaignName==email-nov2011\"},{\"label\":\"google-ads-campaign\",\"nb_uniq_visitors\":\"2\",\"nb_visits\":\"2\",\"nb_actions\":\"2\",\"nb_users\":\"2\",\"max_actions\":\"1\",\"sum_visit_length\":\"0\",\"bounce_count\":\"2\",\"nb_visits_converted\":\"0\",\"segment\":\"campaignName==google-ads-campaign\"}]}",
+ "json": "[{\"label\":\"email-nov2011\",\"nb_uniq_visitors\":3,\"nb_visits\":4,\"nb_actions\":19,\"nb_users\":2,\"max_actions\":8,\"sum_visit_length\":6,\"bounce_count\":1,\"nb_visits_converted\":2,\"goals\":{\"idgoal=ecommerceOrder\":{\"nb_conversions\":1,\"nb_visits_converted\":1,\"revenue\":232.56,\"revenue_subtotal\":204,\"revenue_tax\":38.76,\"revenue_shipping\":10.2,\"revenue_discount\":20.4,\"items\":3},\"idgoal=1\":{\"nb_conversions\":1,\"nb_visits_converted\":1,\"revenue\":0}},\"nb_conversions\":2,\"revenue\":232.56,\"segment\":\"campaignName==email-nov2011\"},{\"label\":\"google-ads-campaign\",\"nb_uniq_visitors\":2,\"nb_visits\":2,\"nb_actions\":2,\"nb_users\":2,\"max_actions\":1,\"sum_visit_length\":0,\"bounce_count\":2,\"nb_visits_converted\":0,\"segment\":\"campaignName==google-ads-campaign\"}]",
+ "tsv": "label\tnb_uniq_visitors\tnb_visits\tnb_actions\tnb_users\tmax_actions\tsum_visit_length\tbounce_count\tnb_visits_converted\tgoals_idgoal=ecommerceOrder_nb_conversions\tgoals_idgoal=ecommerceOrder_nb_visits_converted\tgoals_idgoal=ecommerceOrder_revenue\tgoals_idgoal=ecommerceOrder_revenue_subtotal\tgoals_idgoal=ecommerceOrder_revenue_tax\tgoals_idgoal=ecommerceOrder_revenue_shipping\tgoals_idgoal=ecommerceOrder_revenue_discount\tgoals_idgoal=ecommerceOrder_items\tgoals_idgoal=1_nb_conversions\tgoals_idgoal=1_nb_visits_converted\tgoals_idgoal=1_revenue\tnb_conversions\trevenue\tmetadata_segment\nemail-nov2011\t3\t4\t19\t2\t8\t6\t1\t2\t1\t1\t232.56\t204\t38.76\t10.2\t20.4\t3\t1\t1\t0\t2\t232.56\tcampaignName==email-nov2011\ngoogle-ads-campaign\t2\t2\t2\t2\t1\t0\t2\t0\t\t\t\t\t\t\t\t\t\t\t\t\t\tcampaignName==google-ads-campaign"
+ },
+ "MarketingCampaignsReporting.getKeyword": {
+ "xml": "{\"row\":[{\"label\":\"more\",\"nb_uniq_visitors\":\"1\",\"nb_visits\":\"2\",\"nb_actions\":\"12\",\"nb_users\":\"1\",\"max_actions\":\"8\",\"sum_visit_length\":\"5\",\"bounce_count\":\"0\",\"nb_visits_converted\":\"2\",\"goals\":{\"row\":[{\"nb_conversions\":\"1\",\"nb_visits_converted\":\"1\",\"revenue\":\"232.56\",\"revenue_subtotal\":\"204\",\"revenue_tax\":\"38.76\",\"revenue_shipping\":\"10.2\",\"revenue_discount\":\"20.4\",\"items\":\"3\"},{\"nb_conversions\":\"1\",\"nb_visits_converted\":\"1\",\"revenue\":\"0\"}]},\"nb_conversions\":\"2\",\"revenue\":\"232.56\",\"segment\":\"campaignKeyword==more\"},{\"label\":\"learnmore\",\"nb_uniq_visitors\":\"1\",\"nb_visits\":\"1\",\"nb_actions\":\"1\",\"nb_users\":\"1\",\"max_actions\":\"1\",\"sum_visit_length\":\"0\",\"bounce_count\":\"1\",\"nb_visits_converted\":\"0\",\"segment\":\"campaignKeyword==learnmore\"},{\"label\":\"ordernow\",\"nb_uniq_visitors\":\"1\",\"nb_visits\":\"1\",\"nb_actions\":\"6\",\"nb_users\":\"0\",\"max_actions\":\"6\",\"sum_visit_length\":\"1\",\"bounce_count\":\"0\",\"nb_visits_converted\":\"0\",\"segment\":\"campaignKeyword==ordernow\"},{\"label\":\"www.facebook.com\",\"nb_uniq_visitors\":\"1\",\"nb_visits\":\"1\",\"nb_actions\":\"1\",\"nb_users\":\"1\",\"max_actions\":\"1\",\"sum_visit_length\":\"0\",\"bounce_count\":\"1\",\"nb_visits_converted\":\"0\",\"segment\":\"campaignKeyword==www.facebook.com\"}]}",
+ "json": "[{\"label\":\"more\",\"nb_uniq_visitors\":1,\"nb_visits\":2,\"nb_actions\":\"12\",\"nb_users\":1,\"max_actions\":8,\"sum_visit_length\":\"5\",\"bounce_count\":\"0\",\"nb_visits_converted\":\"2\",\"goals\":{\"idgoal=ecommerceOrder\":{\"nb_conversions\":1,\"nb_visits_converted\":1,\"revenue\":232.56,\"revenue_subtotal\":204,\"revenue_tax\":38.76,\"revenue_shipping\":10.2,\"revenue_discount\":20.4,\"items\":3},\"idgoal=1\":{\"nb_conversions\":1,\"nb_visits_converted\":1,\"revenue\":0}},\"nb_conversions\":2,\"revenue\":232.56,\"segment\":\"campaignKeyword==more\"},{\"label\":\"learnmore\",\"nb_uniq_visitors\":1,\"nb_visits\":1,\"nb_actions\":\"1\",\"nb_users\":1,\"max_actions\":1,\"sum_visit_length\":\"0\",\"bounce_count\":\"1\",\"nb_visits_converted\":\"0\",\"segment\":\"campaignKeyword==learnmore\"},{\"label\":\"ordernow\",\"nb_uniq_visitors\":1,\"nb_visits\":1,\"nb_actions\":\"6\",\"nb_users\":0,\"max_actions\":6,\"sum_visit_length\":\"1\",\"bounce_count\":\"0\",\"nb_visits_converted\":\"0\",\"segment\":\"campaignKeyword==ordernow\"},{\"label\":\"www.facebook.com\",\"nb_uniq_visitors\":1,\"nb_visits\":1,\"nb_actions\":\"1\",\"nb_users\":1,\"max_actions\":1,\"sum_visit_length\":\"0\",\"bounce_count\":\"1\",\"nb_visits_converted\":\"0\",\"segment\":\"campaignKeyword==www.facebook.com\"}]",
+ "tsv": "label\tnb_uniq_visitors\tnb_visits\tnb_actions\tnb_users\tmax_actions\tsum_visit_length\tbounce_count\tnb_visits_converted\tgoals_idgoal=ecommerceOrder_nb_conversions\tgoals_idgoal=ecommerceOrder_nb_visits_converted\tgoals_idgoal=ecommerceOrder_revenue\tgoals_idgoal=ecommerceOrder_revenue_subtotal\tgoals_idgoal=ecommerceOrder_revenue_tax\tgoals_idgoal=ecommerceOrder_revenue_shipping\tgoals_idgoal=ecommerceOrder_revenue_discount\tgoals_idgoal=ecommerceOrder_items\tgoals_idgoal=1_nb_conversions\tgoals_idgoal=1_nb_visits_converted\tgoals_idgoal=1_revenue\tnb_conversions\trevenue\tmetadata_segment\nmore\t1\t2\t12\t1\t8\t5\t0\t2\t1\t1\t232.56\t204\t38.76\t10.2\t20.4\t3\t1\t1\t0\t2\t232.56\tcampaignKeyword==more\nlearnmore\t1\t1\t1\t1\t1\t0\t1\t0\t\t\t\t\t\t\t\t\t\t\t\t\t\tcampaignKeyword==learnmore\nordernow\t1\t1\t6\t0\t6\t1\t0\t0\t\t\t\t\t\t\t\t\t\t\t\t\t\tcampaignKeyword==ordernow\nwww.facebook.com\t1\t1\t1\t1\t1\t0\t1\t0\t\t\t\t\t\t\t\t\t\t\t\t\t\tcampaignKeyword==www.facebook.com"
+ },
+ "MarketingCampaignsReporting.getSource": {
+ "xml": "",
+ "json": "",
+ "tsv": ""
+ },
+ "MarketingCampaignsReporting.getMedium": {
+ "xml": "",
+ "json": "",
+ "tsv": ""
+ },
+ "MarketingCampaignsReporting.getContent": {
+ "xml": "",
+ "json": "",
+ "tsv": ""
+ },
+ "MarketingCampaignsReporting.getGroup": {
+ "xml": "",
+ "json": "",
+ "tsv": ""
+ },
+ "MarketingCampaignsReporting.getPlacement": {
+ "xml": "",
+ "json": "",
+ "tsv": ""
+ },
+ "MarketingCampaignsReporting.getSourceMedium": {
+ "xml": "",
+ "json": "",
+ "tsv": ""
+ },
+ "LogViewer.getLogEntries": {
+ "xml": "{\"row\":[{\"severity\":\"\",\"tag\":\"\",\"datetime\":\"\",\"requestId\":\"\",\"message\":\"\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#17 {main} [Query: ?module=API&method=LogViewer.getLogConfig&format=Tsv&token_auth=removed&\\n convertToUnicode=0&hideIdSubDatable=1, CLI mode: 0]\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#16 \\/index.php(25): require_once('...')\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#15 \\/core\\/dispatch.php(33): Piwik\\\\FrontController->dispatch()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#14 \\/core\\/FrontController.php(170): Piwik\\\\FrontController->doDispatch()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#13 \\/core\\/FrontController.php(646): call_user_func_array()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#12 [internal function]: Piwik\\\\Plugins\\\\API\\\\Controller->index()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#11 \\/plugins\\/API\\/Controller.php(48): Piwik\\\\API\\\\Request->process()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#10 \\/core\\/API\\/Request.php(282): Piwik\\\\Context::executeWithQueryParameters()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#9 \\/core\\/Context.php(29): Piwik\\\\API\\\\Request->{closure:Piwik\\\\API\\\\Request::process():282}()\"}]}",
+ "json": "[{\"severity\":\"\",\"tag\":\"\",\"datetime\":\"\",\"requestId\":\"\",\"message\":\"\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#17 {main} [Query: ?module=API&method=LogViewer.getLogConfig&format=Tsv&token_auth=removed&convertToUnicode=0&hideIdSubDatable=1, CLI mode: 0]\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#16 \\/index.php(25): require_once('...')\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#15 \\/core\\/dispatch.php(33): Piwik\\\\FrontController->dispatch()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#14 \\/core\\/FrontController.php(170): Piwik\\\\FrontController->doDispatch()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#13 \\/core\\/FrontController.php(646): call_user_func_array()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#12 [internal function]: Piwik\\\\Plugins\\\\API\\\\Controller->index()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#11 \\/plugins\\/API\\/Controller.php(48): Piwik\\\\API\\\\Request->process()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#10 \\/core\\/API\\/Request.php(282): Piwik\\\\Context::executeWithQueryParameters()\"},{\"severity\":\"ERROR\",\"tag\":\"API\",\"datetime\":\"2025-09-19 07:55:22 UTC\",\"requestId\":\"1bc99\",\"message\":\"#9 \\/core\\/Context.php(29): Piwik\\\\API\\\\Request->{closure:Piwik\\\\API\\\\Request::process():282}()\"}]",
+ "tsv": "severity\ttag\tdatetime\trequestId\tmessage\n\t\t\t\t\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t\"#17 {main} [Query: ?module=API&method=LogViewer.getLogConfig&format=Tsv&token_auth=removed&convertToUnicode=0&hideIdSubDatable=1, CLI mode: 0]\"\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#16 /index.php(25): require_once('...')\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#15 /core/dispatch.php(33): Piwik\\FrontController->dispatch()\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#14 /core/FrontController.php(170): Piwik\\FrontController->doDispatch()\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#13 /core/FrontController.php(646): call_user_func_array()\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#12 [internal function]: Piwik\\Plugins\\API\\Controller->index()\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#11 /plugins/API/Controller.php(48): Piwik\\API\\Request->process()\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#10 /core/API/Request.php(282): Piwik\\Context::executeWithQueryParameters()\nERROR\tAPI\t2025-09-19 08:04:51 UTC\t2904f\t#9 /core/Context.php(29): Piwik\\API\\Request->{closure:Piwik\\API\\Request::process():282}()"
+ },
+ "LogViewer.getAvailableLogReaders": {
+ "xml": "{\"row\":[\"file\",\"database\"]}",
+ "json": "[\"file\",\"database\"]",
+ "tsv": "file\ndatabase"
+ },
+ "LogViewer.getConfiguredLogReaders": {
+ "xml": "{\"row\":\"file\"}",
+ "json": "[\"file\"]",
+ "tsv": "file"
+ },
+ "LogViewer.getLogConfig": {
+ "xml": "{\"log_writers\":{\"row\":[\"screen\",\"file\"]},\"log_level\":\"WARN\",\"logger_file_path\":\"tmp\\\/logs\\\/matomo.log\",\"logger_syslog_ident\":\"matomo\"}",
+ "json": "{\"log_writers\":[\"screen\",\"file\"],\"log_level\":\"WARN\",\"logger_file_path\":\"tmp\\\/logs\\\/matomo.log\",\"logger_syslog_ident\":\"matomo\"}",
+ "tsv": ""
+ },
+ "CustomAlerts.getAlert": {
+ "xml": "{\"idalert\":\"1\",\"name\":\"Test Alert\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"greater_than\",\"metric_matched\":\"500\",\"compared_to\":\"7\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}}",
+ "json": "{\"idalert\":1,\"name\":\"Test Alert\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"greater_than\",\"metric_matched\":500,\"compared_to\":7,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]}",
+ "tsv": ""
+ },
+ "CustomAlerts.getAlerts": {
+ "xml": "{\"row\":[{\"idalert\":\"1\",\"name\":\"Test Alert\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"greater_than\",\"metric_matched\":\"500\",\"compared_to\":\"7\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idalert\":\"2\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}}]}",
+ "json": "[{\"idalert\":1,\"name\":\"Test Alert\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"greater_than\",\"metric_matched\":500,\"compared_to\":7,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idalert\":2,\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]}]",
+ "tsv": ""
+ },
+ "CustomAlerts.getTriggeredAlerts": {
+ "xml": "{\"row\":[{\"idtriggered\":\"1\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 01:55:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"2\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 01:58:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"3\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 02:00:39\",\"ts_last_sent\":\"2024-09-13 02:00:46\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"4\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 02:55:47\",\"ts_last_sent\":\"2024-09-13 02:55:53\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"5\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-16 23:33:41\",\"ts_last_sent\":\"\",\"value_old\":\"500.000\",\"value_new\":\"36.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"6\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-18 03:21:11\",\"ts_last_sent\":\"2024-09-18 03:23:05\",\"value_old\":\"36.000\",\"value_new\":\"1.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"9\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-18 22:18:55\",\"ts_last_sent\":\"2024-09-18 22:19:02\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"12\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-26 01:18:30\",\"ts_last_sent\":\"\",\"value_old\":\"2.000\",\"value_new\":\"1.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"13\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-10-11 02:26:50\",\"ts_last_sent\":\"\",\"value_old\":\"1190.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"14\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-11-28 20:34:14\",\"ts_last_sent\":\"\",\"value_old\":\"2.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"15\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-03-27 03:39:26\",\"ts_last_sent\":\"2025-03-27 03:39:26\",\"value_old\":\"4.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"16\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-04-03 20:59:07\",\"ts_last_sent\":\"2025-04-03 20:59:07\",\"value_old\":\"243.000\",\"value_new\":\"110.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"17\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-04 11:31:47\",\"ts_last_sent\":\"2025-05-04 11:31:47\",\"value_old\":\"330.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"18\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-10 11:31:23\",\"ts_last_sent\":\"2025-05-10 11:31:24\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"19\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-16 11:31:19\",\"ts_last_sent\":\"2025-05-16 11:31:19\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"20\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-18 11:31:24\",\"ts_last_sent\":\"2025-05-18 11:31:24\",\"value_old\":\"2.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"21\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-22 11:31:19\",\"ts_last_sent\":\"2025-05-22 11:31:19\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"22\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-25 11:31:24\",\"ts_last_sent\":\"2025-05-25 11:31:25\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"23\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-28 11:31:26\",\"ts_last_sent\":\"2025-05-28 11:31:27\",\"value_old\":\"304.000\",\"value_new\":\"3.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"24\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-05-29 11:31:25\",\"ts_last_sent\":\"2025-05-29 11:31:25\",\"value_old\":\"3.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"25\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-07-06 11:31:34\",\"ts_last_sent\":\"2025-07-06 11:31:34\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"26\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-07-16 11:31:35\",\"ts_last_sent\":\"2025-07-16 11:31:36\",\"value_old\":\"357.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"27\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-08-02 11:31:41\",\"ts_last_sent\":\"2025-08-02 11:31:41\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"28\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2025-08-09 11:31:44\",\"ts_last_sent\":\"2025-08-09 11:31:45\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}}]}",
+ "json": "[{\"idtriggered\":1,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 01:55:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":2,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 01:58:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":3,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 02:00:39\",\"ts_last_sent\":\"2024-09-13 02:00:46\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":4,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 02:55:47\",\"ts_last_sent\":\"2024-09-13 02:55:53\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":5,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-16 23:33:41\",\"ts_last_sent\":null,\"value_old\":\"500.000\",\"value_new\":\"36.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":6,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-18 03:21:11\",\"ts_last_sent\":\"2024-09-18 03:23:05\",\"value_old\":\"36.000\",\"value_new\":\"1.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":9,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-18 22:18:55\",\"ts_last_sent\":\"2024-09-18 22:19:02\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":12,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-26 01:18:30\",\"ts_last_sent\":null,\"value_old\":\"2.000\",\"value_new\":\"1.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":13,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-10-11 02:26:50\",\"ts_last_sent\":null,\"value_old\":\"1190.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":14,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-11-28 20:34:14\",\"ts_last_sent\":null,\"value_old\":\"2.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":15,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-03-27 03:39:26\",\"ts_last_sent\":\"2025-03-27 03:39:26\",\"value_old\":\"4.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":16,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-04-03 20:59:07\",\"ts_last_sent\":\"2025-04-03 20:59:07\",\"value_old\":\"243.000\",\"value_new\":\"110.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":17,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-04 11:31:47\",\"ts_last_sent\":\"2025-05-04 11:31:47\",\"value_old\":\"330.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":18,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-10 11:31:23\",\"ts_last_sent\":\"2025-05-10 11:31:24\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":19,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-16 11:31:19\",\"ts_last_sent\":\"2025-05-16 11:31:19\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":20,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-18 11:31:24\",\"ts_last_sent\":\"2025-05-18 11:31:24\",\"value_old\":\"2.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":21,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-22 11:31:19\",\"ts_last_sent\":\"2025-05-22 11:31:19\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":22,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-25 11:31:24\",\"ts_last_sent\":\"2025-05-25 11:31:25\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":23,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-28 11:31:26\",\"ts_last_sent\":\"2025-05-28 11:31:27\",\"value_old\":\"304.000\",\"value_new\":\"3.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":24,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-05-29 11:31:25\",\"ts_last_sent\":\"2025-05-29 11:31:25\",\"value_old\":\"3.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":25,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-07-06 11:31:34\",\"ts_last_sent\":\"2025-07-06 11:31:34\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":26,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-07-16 11:31:35\",\"ts_last_sent\":\"2025-07-16 11:31:36\",\"value_old\":\"357.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":27,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2025-08-02 11:31:41\",\"ts_last_sent\":\"2025-08-02 11:31:41\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]}]",
+ "tsv": ""
+ },
+ "CustomDimensions.getConfiguredCustomDimensions": {
+ "xml": "{\"row\":[{\"idcustomdimension\":\"1\",\"idsite\":\"1\",\"name\":\"User Type\",\"index\":\"1\",\"scope\":\"visit\",\"active\":\"1\",\"extractions\":\"\",\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"2\",\"idsite\":\"1\",\"name\":\"Page Author\",\"index\":\"1\",\"scope\":\"action\",\"active\":\"1\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"3\",\"idsite\":\"1\",\"name\":\"Page Age\",\"index\":\"2\",\"scope\":\"action\",\"active\":\"0\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"4\",\"idsite\":\"1\",\"name\":\"Page Location\",\"index\":\"3\",\"scope\":\"action\",\"active\":\"1\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"5\",\"idsite\":\"1\",\"name\":\"Page Type\",\"index\":\"4\",\"scope\":\"action\",\"active\":\"1\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"},{\"idcustomdimension\":\"6\",\"idsite\":\"1\",\"name\":\"Diving Rating\",\"index\":\"5\",\"scope\":\"action\",\"active\":\"0\",\"extractions\":{\"row\":{\"dimension\":\"url\",\"pattern\":\"\"}},\"case_sensitive\":\"1\"}]}",
+ "json": "[{\"idcustomdimension\":\"1\",\"idsite\":\"1\",\"name\":\"User Type\",\"index\":\"1\",\"scope\":\"visit\",\"active\":true,\"extractions\":[],\"case_sensitive\":true},{\"idcustomdimension\":\"2\",\"idsite\":\"1\",\"name\":\"Page Author\",\"index\":\"1\",\"scope\":\"action\",\"active\":true,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"3\",\"idsite\":\"1\",\"name\":\"Page Age\",\"index\":\"2\",\"scope\":\"action\",\"active\":false,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"4\",\"idsite\":\"1\",\"name\":\"Page Location\",\"index\":\"3\",\"scope\":\"action\",\"active\":true,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"5\",\"idsite\":\"1\",\"name\":\"Page Type\",\"index\":\"4\",\"scope\":\"action\",\"active\":true,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true},{\"idcustomdimension\":\"6\",\"idsite\":\"1\",\"name\":\"Diving Rating\",\"index\":\"5\",\"scope\":\"action\",\"active\":false,\"extractions\":[{\"dimension\":\"url\",\"pattern\":\"\"}],\"case_sensitive\":true}]",
+ "tsv": ""
+ },
+ "CustomDimensions.getAvailableExtractionDimensions": {
+ "xml": "{\"row\":[{\"value\":\"url\",\"name\":\"Page URL\"},{\"value\":\"urlparam\",\"name\":\"Page URL Parameter\"},{\"value\":\"action_name\",\"name\":\"Page Title\"}]}",
+ "json": "[{\"value\":\"url\",\"name\":\"Page URL\"},{\"value\":\"urlparam\",\"name\":\"Page URL Parameter\"},{\"value\":\"action_name\",\"name\":\"Page Title\"}]",
+ "tsv": "value\tname\nurl\tPage URL\nurlparam\tPage URL Parameter\naction_name\tPage Title"
+ }
+}
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponsesNormalised/ExamplesPostTruncationByType.json b/tests/Resources/ExampleResponsesNormalised/ExamplesPostTruncationByType.json
new file mode 100644
index 0000000..27a0f7a
--- /dev/null
+++ b/tests/Resources/ExampleResponsesNormalised/ExamplesPostTruncationByType.json
@@ -0,0 +1,6 @@
+{
+ "CustomAlerts.getTriggeredAlerts": {
+ "xml": "{\"row\":[{\"idtriggered\":\"1\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 01:55:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"2\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 01:58:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"3\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 02:00:39\",\"ts_last_sent\":\"2024-09-13 02:00:46\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"4\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-13 02:55:47\",\"ts_last_sent\":\"2024-09-13 02:55:53\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}},{\"idtriggered\":\"5\",\"idalert\":\"2\",\"idsite\":\"1\",\"ts_triggered\":\"2024-09-16 23:33:41\",\"ts_last_sent\":\"\",\"value_old\":\"500.000\",\"value_new\":\"36.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":\"\",\"report_matched\":\"\",\"report_mediums\":{\"row\":\"email\"},\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":\"20\",\"compared_to\":\"1\",\"email_me\":\"1\",\"additional_emails\":\"\",\"phone_numbers\":\"\",\"slack_channel_id\":\"\",\"id_sites\":{\"row\":\"1\"}}]}",
+ "json": "[{\"idtriggered\":1,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 01:55:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":2,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 01:58:48\",\"ts_last_sent\":\"2024-09-13 01:58:54\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":3,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 02:00:39\",\"ts_last_sent\":\"2024-09-13 02:00:46\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":4,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-13 02:55:47\",\"ts_last_sent\":\"2024-09-13 02:55:53\",\"value_old\":\"1.000\",\"value_new\":\"0.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]},{\"idtriggered\":5,\"idalert\":2,\"idsite\":1,\"ts_triggered\":\"2024-09-16 23:33:41\",\"ts_last_sent\":null,\"value_old\":\"500.000\",\"value_new\":\"36.000\",\"name\":\"Test Visit Drop Previous Day\",\"login\":\"someUserName\",\"period\":\"day\",\"report\":\"VisitsSummary_get\",\"report_condition\":null,\"report_matched\":null,\"report_mediums\":[\"email\"],\"metric\":\"nb_uniq_visitors\",\"metric_condition\":\"percentage_decrease_more_than\",\"metric_matched\":20,\"compared_to\":1,\"email_me\":1,\"additional_emails\":[],\"phone_numbers\":[],\"slack_channel_id\":null,\"id_sites\":[1]}]"
+ }
+}
\ No newline at end of file
diff --git a/tests/Resources/ExampleResponsesNormalised/ExamplesSchemasByType.json b/tests/Resources/ExampleResponsesNormalised/ExamplesSchemasByType.json
new file mode 100644
index 0000000..9ebf4fe
--- /dev/null
+++ b/tests/Resources/ExampleResponsesNormalised/ExamplesSchemasByType.json
@@ -0,0 +1,86 @@
+{
+ "MarketingCampaignsReporting.getId": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "MarketingCampaignsReporting.getName": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\",{\"@OA\\\\Property\":[\"property=\\\"goals\\\",\",\"type=\\\"object\\\",\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"label\\\", type=\\\"string\\\")\",\"2\":\"@OA\\\\Property(property=\\\"nb_uniq_visitors\\\", type=\\\"integer\\\")\",\"3\":\"@OA\\\\Property(property=\\\"nb_visits\\\", type=\\\"integer\\\")\",\"4\":\"@OA\\\\Property(property=\\\"nb_actions\\\", type=\\\"integer\\\")\",\"5\":\"@OA\\\\Property(property=\\\"nb_users\\\", type=\\\"integer\\\")\",\"6\":\"@OA\\\\Property(property=\\\"max_actions\\\", type=\\\"integer\\\")\",\"7\":\"@OA\\\\Property(property=\\\"sum_visit_length\\\", type=\\\"integer\\\")\",\"8\":\"@OA\\\\Property(property=\\\"bounce_count\\\", type=\\\"integer\\\")\",\"9\":\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"integer\\\")\",\"@OA\\\\Property\":{\"0\":\"property=\\\"goals\\\",\",\"1\":\"type=\\\"object\\\",\",\"@OA\\\\Property\":[\"property=\\\"idgoal=1\\\",\",\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"revenue\\\", type=\\\"integer\\\")\"]},\"10\":\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"11\":\"@OA\\\\Property(property=\\\"revenue\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"12\":\"@OA\\\\Property(property=\\\"segment\\\", type=\\\"string\\\")\"}}}}"
+ },
+ "MarketingCampaignsReporting.getKeyword": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\",{\"@OA\\\\Property\":[\"property=\\\"goals\\\",\",\"type=\\\"object\\\",\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"label\\\", type=\\\"string\\\")\",\"2\":\"@OA\\\\Property(property=\\\"nb_uniq_visitors\\\", type=\\\"integer\\\")\",\"3\":\"@OA\\\\Property(property=\\\"nb_visits\\\", type=\\\"integer\\\")\",\"4\":\"@OA\\\\Property(property=\\\"nb_actions\\\", type=\\\"string\\\")\",\"5\":\"@OA\\\\Property(property=\\\"nb_users\\\", type=\\\"integer\\\")\",\"6\":\"@OA\\\\Property(property=\\\"max_actions\\\", type=\\\"integer\\\")\",\"7\":\"@OA\\\\Property(property=\\\"sum_visit_length\\\", type=\\\"string\\\")\",\"8\":\"@OA\\\\Property(property=\\\"bounce_count\\\", type=\\\"string\\\")\",\"9\":\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"string\\\")\",\"@OA\\\\Property\":{\"0\":\"property=\\\"goals\\\",\",\"1\":\"type=\\\"object\\\",\",\"@OA\\\\Property\":[\"property=\\\"idgoal=1\\\",\",\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"revenue\\\", type=\\\"integer\\\")\"]},\"10\":\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"11\":\"@OA\\\\Property(property=\\\"revenue\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"12\":\"@OA\\\\Property(property=\\\"segment\\\", type=\\\"string\\\")\"}}}}"
+ },
+ "MarketingCampaignsReporting.getSource": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "MarketingCampaignsReporting.getMedium": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "MarketingCampaignsReporting.getContent": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "MarketingCampaignsReporting.getGroup": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "MarketingCampaignsReporting.getPlacement": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "MarketingCampaignsReporting.getSourceMedium": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "LogViewer.getLogEntries": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":[\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"severity\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"tag\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"datetime\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"requestId\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"message\\\", type=\\\"string\\\")\"]}}}"
+ },
+ "LogViewer.getAvailableLogReaders": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"string\\\"\"]}}]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "LogViewer.getConfiguredLogReaders": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "LogViewer.getLogConfig": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":[\"property=\\\"log_writers\\\",\",\"type=\\\"object\\\",\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"string\\\"\"]}}]}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"object\\\",\",\"@OA\\\\Property\":[\"property=\\\"log_writers\\\",\",\"type=\\\"array\\\",\",\"@OA\\\\Items()\"],\"1\":\"@OA\\\\Property(property=\\\"log_level\\\", type=\\\"string\\\")\",\"2\":\"@OA\\\\Property(property=\\\"logger_file_path\\\", type=\\\"string\\\")\",\"3\":\"@OA\\\\Property(property=\\\"logger_syslog_ident\\\", type=\\\"string\\\")\"}}"
+ },
+ "CustomAlerts.getAlert": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "CustomAlerts.getAlerts": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\",{\"@OA\\\\Property\":[\"property=\\\"report_mediums\\\",\",\"type=\\\"object\\\",\"]},{\"@OA\\\\Property\":[\"property=\\\"id_sites\\\",\",\"type=\\\"object\\\",\"]}]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"idalert\\\", type=\\\"integer\\\")\",\"2\":\"@OA\\\\Property(property=\\\"name\\\", type=\\\"string\\\")\",\"3\":\"@OA\\\\Property(property=\\\"login\\\", type=\\\"string\\\")\",\"4\":\"@OA\\\\Property(property=\\\"period\\\", type=\\\"string\\\")\",\"5\":\"@OA\\\\Property(property=\\\"report\\\", type=\\\"string\\\")\",\"6\":\"@OA\\\\Property(property=\\\"report_condition\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"7\":\"@OA\\\\Property(property=\\\"report_matched\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"@OA\\\\Property\":[\"property=\\\"id_sites\\\",\",\"type=\\\"array\\\",\",\"@OA\\\\Items()\"],\"8\":\"@OA\\\\Property(property=\\\"metric\\\", type=\\\"string\\\")\",\"9\":\"@OA\\\\Property(property=\\\"metric_condition\\\", type=\\\"string\\\")\",\"10\":\"@OA\\\\Property(property=\\\"metric_matched\\\", type=\\\"integer\\\")\",\"11\":\"@OA\\\\Property(property=\\\"compared_to\\\", type=\\\"integer\\\")\",\"12\":\"@OA\\\\Property(property=\\\"email_me\\\", type=\\\"integer\\\")\",\"13\":\"@OA\\\\Property(property=\\\"slack_channel_id\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\"}}}}"
+ },
+ "CustomAlerts.deleteAlert": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
+ "json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
+ },
+ "CustomAlerts.getTriggeredAlerts": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\",{\"@OA\\\\Property\":[\"property=\\\"report_mediums\\\",\",\"type=\\\"object\\\",\"]},{\"@OA\\\\Property\":[\"property=\\\"id_sites\\\",\",\"type=\\\"object\\\",\"]}]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"idtriggered\\\", type=\\\"integer\\\")\",\"2\":\"@OA\\\\Property(property=\\\"idalert\\\", type=\\\"integer\\\")\",\"3\":\"@OA\\\\Property(property=\\\"idsite\\\", type=\\\"integer\\\")\",\"4\":\"@OA\\\\Property(property=\\\"ts_triggered\\\", type=\\\"string\\\")\",\"5\":\"@OA\\\\Property(property=\\\"ts_last_sent\\\", type=\\\"string\\\")\",\"6\":\"@OA\\\\Property(property=\\\"value_old\\\", type=\\\"string\\\")\",\"7\":\"@OA\\\\Property(property=\\\"value_new\\\", type=\\\"string\\\")\",\"8\":\"@OA\\\\Property(property=\\\"name\\\", type=\\\"string\\\")\",\"9\":\"@OA\\\\Property(property=\\\"login\\\", type=\\\"string\\\")\",\"10\":\"@OA\\\\Property(property=\\\"period\\\", type=\\\"string\\\")\",\"11\":\"@OA\\\\Property(property=\\\"report\\\", type=\\\"string\\\")\",\"12\":\"@OA\\\\Property(property=\\\"report_condition\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"13\":\"@OA\\\\Property(property=\\\"report_matched\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"@OA\\\\Property\":[\"property=\\\"id_sites\\\",\",\"type=\\\"array\\\",\",\"@OA\\\\Items()\"],\"14\":\"@OA\\\\Property(property=\\\"metric\\\", type=\\\"string\\\")\",\"15\":\"@OA\\\\Property(property=\\\"metric_condition\\\", type=\\\"string\\\")\",\"16\":\"@OA\\\\Property(property=\\\"metric_matched\\\", type=\\\"integer\\\")\",\"17\":\"@OA\\\\Property(property=\\\"compared_to\\\", type=\\\"integer\\\")\",\"18\":\"@OA\\\\Property(property=\\\"email_me\\\", type=\\\"integer\\\")\",\"19\":\"@OA\\\\Property(property=\\\"slack_channel_id\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\"}}}}"
+ },
+ "CustomDimensions.getCustomDimension": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"string\\\"\"]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"label\\\", type=\\\"string\\\")\",\"2\":\"@OA\\\\Property(property=\\\"nb_uniq_visitors\\\", type=\\\"string\\\")\",\"3\":\"@OA\\\\Property(property=\\\"nb_visits\\\", type=\\\"string\\\")\",\"4\":\"@OA\\\\Property(property=\\\"nb_actions\\\", type=\\\"string\\\")\",\"5\":\"@OA\\\\Property(property=\\\"max_actions\\\", type=\\\"integer\\\")\",\"6\":\"@OA\\\\Property(property=\\\"sum_visit_length\\\", type=\\\"string\\\")\",\"7\":\"@OA\\\\Property(property=\\\"bounce_count\\\", type=\\\"string\\\")\",\"8\":\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"string\\\")\",\"@OA\\\\Property\":{\"0\":\"property=\\\"goals\\\",\",\"1\":\"type=\\\"object\\\",\",\"@OA\\\\Property\":[\"property=\\\"idgoal=8\\\",\",\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"revenue\\\", type=\\\"integer\\\")\"]},\"9\":\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"10\":\"@OA\\\\Property(property=\\\"revenue\\\", type=\\\"integer\\\")\",\"11\":\"@OA\\\\Property(property=\\\"avg_time_on_site\\\", type=\\\"integer\\\")\",\"12\":\"@OA\\\\Property(property=\\\"bounce_rate\\\", type=\\\"string\\\")\",\"13\":\"@OA\\\\Property(property=\\\"nb_actions_per_visit\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"14\":\"@OA\\\\Property(property=\\\"segment\\\", type=\\\"string\\\")\"}}}}"
+ },
+ "CustomDimensions.getConfiguredCustomDimensions": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"idcustomdimension\\\", type=\\\"string\\\")\",\"2\":\"@OA\\\\Property(property=\\\"idsite\\\", type=\\\"string\\\")\",\"3\":\"@OA\\\\Property(property=\\\"name\\\", type=\\\"string\\\")\",\"4\":\"@OA\\\\Property(property=\\\"index\\\", type=\\\"string\\\")\",\"5\":\"@OA\\\\Property(property=\\\"scope\\\", type=\\\"string\\\")\",\"6\":\"@OA\\\\Property(property=\\\"active\\\", type=\\\"boolean\\\")\",\"@OA\\\\Property\":[\"property=\\\"extractions\\\",\",\"type=\\\"array\\\",\",\"@OA\\\\Items()\"],\"7\":\"@OA\\\\Property(property=\\\"case_sensitive\\\", type=\\\"boolean\\\")\"}}}}"
+ },
+ "CustomDimensions.getAvailableScopes": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":[\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"value\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"name\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"numSlotsAvailable\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"numSlotsUsed\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"numSlotsLeft\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"supportsExtractions\\\", type=\\\"boolean\\\")\"]}}}"
+ },
+ "CustomDimensions.getAvailableExtractionDimensions": {
+ "xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}",
+ "json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":[\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"value\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"name\\\", type=\\\"string\\\")\"]}}}"
+ }
+}
diff --git a/tests/Resources/MockAnnotationGenerator.php b/tests/Resources/MockAnnotationGenerator.php
new file mode 100644
index 0000000..b7e376c
--- /dev/null
+++ b/tests/Resources/MockAnnotationGenerator.php
@@ -0,0 +1,83 @@
+ 'url', 'pattern' => 'index_(.+).html'), array('dimension' => 'urlparam', 'pattern' => '...'))
+ * Supported dimensions are eg 'url', 'urlparam' and 'action_name'. To get an up to date list of
+ * supported dimensions request the API method `CustomDimensions.getAvailableExtractionDimensions`.
+ * Note: Extractions can be only set for dimensions in scope 'action'.
+ * @param int|bool $caseSensitive '0' if extractions should be applied case insensitive, '1' if extractions should be applied case sensitive
+ * @return int Returns the ID of the configured dimension. Note that the same idDimension will be used for different websites.
+ * @throws \Exception
+ */
+ public function configureNewCustomDimension($idSite, $name, $scope, $active, $extractions = array(), $caseSensitive = true)
+ {
+ return 1;
+ }
+
+ /**
+ * Updates an existing Custom Dimension. This method updates all values, you need to pass existing values of the
+ * dimension if you do not want to reset any value. Requires at least Admin access for the specified website.
+ *
+ * @param int $idDimension The id of a Custom Dimension.
+ * @param int $idSite The idSite the dimension belongs to
+ * @param string $name The name of the dimension
+ * @param int $active '0' if dimension should be inactive, '1' if dimension should be active
+ * @param array $extractions Either an empty array or if extractions shall be used one or multiple extractions
+ * the format array(array('dimension' => 'url', 'pattern' => 'index_(.+).html'), array('dimension' => 'urlparam', 'pattern' => '...'))
+ * Supported dimensions are eg 'url', 'urlparam' and 'action_name'. To get an up to date list of
+ * supported dimensions request the API method `CustomDimensions.getAvailableExtractionDimensions`.
+ * Note: Extractions can be only set for dimensions in scope 'action'.
+ * @param int|bool|null $caseSensitive '0' if extractions should be applied case insensitive, '1' if extractions should be applied case sensitive, null to keep case sensitive unchanged
+ * @throws \Exception
+ */
+ public function configureExistingCustomDimension($idDimension, $idSite, $name, $active, $extractions = array(), $caseSensitive = null)
+ {
+ }
+
+ /**
+ * Get a list of all configured CustomDimensions for a given website. Requires at least Admin access for the
+ * specified website.
+ *
+ * @param int $idSite
+ * @return array
+ */
+ public function getConfiguredCustomDimensions($idSite)
+ {
+ return [];
+ }
+
+ /**
+ * For convenience. Hidden to reduce API surface area.
+ * @hide
+ */
+ public function getConfiguredCustomDimensionsHavingScope($idSite, $scope)
+ {
+ }
+
+ /**
+ * Get a list of all supported scopes that can be used in the API method
+ * `CustomDimensions.configureNewCustomDimension`. The response also contains information whether more Custom
+ * Dimensions can be created or not. Requires at least Admin access for the specified website.
+ *
+ * @param int $idSite
+ * @return array
+ */
+ public function getAvailableScopes($idSite)
+ {
+ return [];
+ }
+
+ /**
+ * Get a list of all available dimensions that can be used in an extraction. Requires at least Admin access
+ * to one website.
+ *
+ * @return array
+ */
+ public function getAvailableExtractionDimensions()
+ {
+ return [];
+ }
+
+ /**
+ * Copies a specified custom report to one or more sites. If a custom report with the same name already exists, the new custom report
+ * will have an automatically adjusted name to make it unique to the assigned site.
+ *
+ * @param int $idSite
+ * @param int $idCustomReport ID of the custom report to duplicate.
+ * @param int[] $idDestinationSites Optional array of IDs identifying which site(s) the new custom report is to be
+ * assigned to. The default is [idSite] when nothing is provided.
+ * @return array
+ * @throws \Exception
+ */
+ public function duplicateCustomReport(int $idSite, int $idCustomReport, array $idDestinationSites = []): array
+ {
+ return [];
+ }
+
+ /**
+ * Adds a new custom report
+ * @param int $idSite
+ * @param string $name The name of the report.
+ * @param string $reportType The type of report you want to create, for example 'table' or 'evolution'.
+ * For a list of available reports call 'CustomReports.getAvailableReportTypes'
+ * @param string[] $metricIds A list of metric IDs. For a list of available metrics call 'CustomReports.getAvailableMetrics'
+ * @param string $categoryId By default, the report will be put into a custom report category unless a specific
+ * categoryId is provided. For a list of available categories call 'CustomReports.getAvailableCategories'.
+ * @param string[] $dimensionIds A list of dimension IDs. For a list of available metrics call 'CustomReports.getAvailableDimensions'
+ * @param bool|string $subcategoryId By default, a new reporting page will be created for this report unless you
+ * specifiy a specific name or subcategoryID. For a list of available subcategories
+ * call 'CustomReports.getAvailableCategories'.
+ * @param string $description An optional description for the report, will be shown in the title help icon of the report.
+ * @param string $segmentFilter An optional segment to filter the report data. Needs to be sent urlencoded.
+ * @param string[] $multipleIdSites An optional list of idsites for which we need to execute the report
+ * @return int
+ */
+ public function addCustomReport($idSite, $name, $reportType, $metricIds, $categoryId = false, $dimensionIds = array(), $subcategoryId = false, $description = '', $segmentFilter = '', $multipleIdSites = [])
+ {
+ return 4;
+ }
+
+ /**
+ * Updates an existing custom report. Be aware that if you change metrics, dimensions, the report type or the segment filter,
+ * previously processed/archived reports may become unavailable and would need to be re-processed.
+ *
+ * @param int $idSite
+ * @param int $idCustomReport
+ * @param string $name The name of the report.
+ * @param string $reportType The type of report you want to create, for example 'table' or 'evolution'.
+ * For a list of available reports call 'CustomReports.getAvailableReportTypes'
+ * @param string[] $metricIds A list of metric IDs. For a list of available metrics call 'CustomReports.getAvailableMetrics'
+ * @param string $categoryId By default, the report will be put into a custom report category unless a specific
+ * categoryId is provided. For a list of available categories call 'CustomReports.getAvailableCategories'.
+ * @param string[] $dimensionIds A list of dimension IDs. For a list of available metrics call 'CustomReports.getAvailableDimensions'
+ * @param bool|string $subcategoryId By default, a new reporting page will be created for this report unless you
+ * specify a specific name or subcategoryID. For a list of available subcategories
+ * call 'CustomReports.getAvailableCategories'.
+ * @param string $description An optional description for the report, will be shown in the title help icon of the report.
+ * @param string $segmentFilter An optional segment to filter the report data. Needs to be sent urlencoded.
+ * @param int[] $subCategoryReportIds List of sub report ids mapped to this report
+ * @param string[] $multipleIdSites An optional list of idSites for which we need to execute the report
+ */
+ public function updateCustomReport(
+ $idSite,
+ $idCustomReport,
+ $name,
+ $reportType,
+ $metricIds,
+ $categoryId = false,
+ $dimensionIds = [],
+ $subcategoryId = false,
+ $description = '',
+ $segmentFilter = '',
+ $subCategoryReportIds = [],
+ $multipleIdSites = []
+ ): void {
+ }
+
+ /**
+ * Get all custom report configurations for a specific site.
+ *
+ * @param int $idSite
+ * @param bool $skipCategoryMetadata
+ * @return array
+ */
+ public function getConfiguredReports($idSite, $skipCategoryMetadata = false)
+ {
+ return [];
+ }
+
+ /**
+ * Get a specific custom report configuration.
+ *
+ * @param int $idSite
+ * @param int $idCustomReport The ID of the custom report. [@example=1]
+ * @return array
+ */
+ public function getConfiguredReport($idSite, $idCustomReport)
+ {
+ return [];
+ }
+
+ /**
+ * Deletes the given custom report.
+ *
+ * When a custom report is deleted, its report will be no longer available in the API and tracked data for this
+ * report might be removed at some point by the system.
+ *
+ * @param int $idSite
+ * @param int $idForm
+ */
+ public function deleteCustomReport($idSite, $idCustomReport): void
+ {
+ }
+
+ /**
+ * Pauses the given custom report.
+ *
+ * When a custom report is paused, its report will be no longer be archived
+ *
+ * @param int $idSite
+ * @param int $idCustomReport
+ */
+ public function pauseCustomReport($idSite, $idCustomReport): void
+ {
+ }
+
+ /**
+ * Resumes the given custom report.
+ *
+ * When a custom report is resumed, its report will start archiving again
+ *
+ * @param int $idSite
+ * @param int $idCustomReport
+ */
+ public function resumeCustomReport($idSite, $idCustomReport): void
+ {
+ }
+
+ /**
+ * Get a list of available categories that can be used in custom reports.
+ *
+ * @param int $idSite
+ * @return array
+ */
+ public function getAvailableCategories($idSite)
+ {
+ return [];
+ }
+
+ /**
+ * Get a list of available report types that can be used in custom reports.
+ *
+ * @return array
+ */
+ public function getAvailableReportTypes()
+ {
+ return [];
+ }
+
+ /**
+ * Get a list of available dimensions that can be used in custom reports.
+ *
+ * @param int $idSite
+ * @return array
+ */
+ public function getAvailableDimensions($idSite)
+ {
+ return [];
+ }
+
+ /**
+ * Get a list of available metrics that can be used in custom reports.
+ *
+ * @param int $idSite
+ * @return array
+ */
+ public function getAvailableMetrics($idSite)
+ {
+ return [];
+ }
+
+ /**
+ * Get report data for a previously created custom report.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idCustomReport
+ * @param bool|string $segment
+ * @param bool $expanded
+ * @param bool $flat
+ * @param int|bool $idSubtable
+ * @param string|bool $columns
+ * @return DataTable\DataTableInterface
+ */
+ public function getCustomReport($idSite, $period, $date, $idCustomReport, $segment = false, $expanded = false, $flat = false, $idSubtable = false, $columns = false)
+ {
+ return new DataTable();
+ }
+
+ /**
+ * Get summary metrics for a specific funnel like the number of conversions, the conversion rate, the number of
+ * entries etc.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idFunnel Either idFunnel or idGoal has to be set
+ * @param int $idGoal Either idFunnel or idGoal has to be set. If goal given, will return the latest funnel for that goal. [@example=4]
+ * @param string $segment
+ *
+ * @return DataTable|DataTable\Map
+ */
+ public function getMetrics($idSite, $period, $date, $idFunnel = false, $idGoal = false, $segment = false)
+ {
+ return new DataTable();
+ }
+
+ /**
+ * Get funnel flow information. The returned datatable will include a row for each step within the funnel
+ * showing information like how many visits have entered or left the funnel at a certain position, how many
+ * have completed a certain step etc.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idFunnel Either idFunnel or idGoal has to be set
+ * @param int $idGoal Either idFunnel or idGoal has to be set. If goal given, will return the latest funnel for that goal. [@example=4]
+ * @param string $segment
+ *
+ * @return DataTable
+ * @throws \Exception
+ */
+ public function getFunnelFlow($idSite, $period, $date, $idFunnel = false, $idGoal = false, $segment = false)
+ {
+ return new DataTable();
+ }
+
+ /**
+ * Get funnel flow information. The returned datatable will include a row for each step within the funnel
+ * showing information like how many visits have entered or left the funnel at a certain position, how many
+ * have completed a certain step etc.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idFunnel Either idFunnel or idGoal has to be set
+ * @param int $idGoal Either idFunnel or idGoal has to be set. If goal given, will return the latest funnel for that goal. [@example=4]
+ * @param string $segment
+ *
+ * @return DataTable
+ * @throws \Exception
+ */
+ public function getFunnelFlowTable($idSite, $period, $date, $idFunnel = false, $idGoal = false, $segment = false)
+ {
+ return new DataTable();
+ }
+
+ /**
+ * Get subTable funnel flow information. The returned datatable will include a row for proceeded, entries, and
+ * exists. If they have any values, they'll have a subTable of their own.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $stepPosition The step number to pull the data for. [@example=1]
+ * @param int $idFunnel Either idFunnel or idGoal has to be set
+ * @param int $idGoal Either idFunnel or idGoal has to be set. If goal given, will return the latest funnel for that goal. [@example=4]
+ * @param string $segment
+ *
+ * @return DataTable
+ * @throws \Exception
+ */
+ public function getFunnelStepSubtable($idSite, $period, $date, $stepPosition, $idFunnel = false, $idGoal = false, $segment = false)
+ {
+ return new DataTable();
+ }
+
+ /**
+ * Get all entry actions for the given funnel at the given step.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idFunnel The ID of the funnel for which to get data. [@example=99]
+ * @param string $segment
+ * @param string $step
+ * @param bool $expanded
+ * @param int|string $idSubtable
+ * @param bool $flat
+ *
+ * @return DataTable
+ */
+ public function getFunnelEntries($idSite, $period, $date, $idFunnel, $segment = false, $step = false, $expanded = false, $idSubtable = false, $flat = false)
+ {
+ return new DataTable();
+ }
+
+ /**
+ * Get all exit actions for the given funnel at the given step.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idFunnel The ID of the funnel for which to get data. [@example=99]
+ * @param string $segment
+ * @param string $step
+ *
+ * @return DataTable
+ */
+ public function getFunnelExits($idSite, $period, $date, $idFunnel, $segment = false, $step = false)
+ {
+ return new DataTable();
+ }
+
+ /**
+ * Get funnel information for this goal.
+ *
+ * @param int $idSite
+ * @param int $idGoal The ID of the goal for which to get funnel data. [@example=4]
+ *
+ * @return array|null Null when no funnel has been configured yet, the funnel otherwise.
+ * @throws \Exception
+ */
+ public function getGoalFunnel($idSite, $idGoal)
+ {
+ return null;
+ }
+
+ /**
+ * Get funnel information for this goal.
+ *
+ * @param int $idSite
+ *
+ * @return array|null Null when no funnel has been configured yet, the funnel otherwise.
+ * @throws \Exception
+ */
+ public function getSalesFunnelForSite($idSite)
+ {
+ return null;
+ }
+
+ /**
+ * Get funnel information by ID.
+ *
+ * @param int $idSite
+ * @param int $idFunnel The ID of the funnel for which to get data. [@example=99]
+ *
+ * @return array|null Null when no funnel has been configured yet, the funnel otherwise.
+ * @throws \Exception
+ */
+ public function getFunnel(int $idSite, int $idFunnel)
+ {
+ return null;
+ }
+
+ /**
+ * Get activated funnels for the current site.
+ *
+ * @param int $idSite
+ *
+ * @return array
+ */
+ public function getAllActivatedFunnelsForSite($idSite)
+ {
+ return [];
+ }
+
+ /**
+ * @param int $idSite
+ *
+ * @return bool
+ */
+ public function hasAnyActivatedFunnelForSite($idSite)
+ {
+ return true;
+ }
+
+ /**
+ * Deletes the given goal funnel.
+ *
+ * @param int $idSite
+ * @param int $idGoal The ID of the goal to which the funnel is tied.
+ *
+ * @throws \Exception
+ */
+ public function deleteGoalFunnel($idSite, $idGoal): void
+ {
+ }
+
+ /**
+ * Deletes the given goal funnel.
+ *
+ * @param int $idSite
+ * @param int $idFunnel
+ *
+ * @throws \Exception
+ */
+ public function deleteNonGoalFunnel(int $idSite, int $idFunnel): void
+ {
+ }
+
+ /**
+ * Sets (overwrites) a funnel for this goal.
+ *
+ * @param int $idSite
+ * @param int $idGoal
+ * @param int $isActivated Whether the funnel is activated. E.g. 0 or 1. As soon as a funnel is activated, a report
+ * will be generated for this funnel.
+ * @param array[] $steps Definitions of each funnel step. If isActivated = true, there has to be at least one step.
+ * E.g. [{'position': 1, 'name': 'Step1', 'pattern_type': 'path_contains', 'pattern': 'path/dir', 'required': 0}]
+ *
+ * @return int The id of the created or updated funnel
+ * @throws \Exception
+ */
+ public function setGoalFunnel($idSite, $idGoal, $isActivated, $steps = [])
+ {
+ return 4;
+ }
+
+ /**
+ * Saves a funnel not tied to a goal.
+ *
+ * @param int $idSite
+ * @param int $idFunnel ID of the funnel since we can't use the idSite and idGoal to identify it
+ * @param string $funnelName The name used to identify the funnel since it's not tied to a goal
+ * @param array $steps Definitions of each funnel step.Definitions of each funnel step.
+ * E.g. [{'position': 1, 'name': 'Step1', 'pattern_type': 'path_contains', 'pattern': 'path/dir', 'required': 0}]
+ *
+ * @return int The id of the created or updated funnel
+ * @throws \Exception
+ */
+ public function saveNonGoalFunnel(int $idSite, int $idFunnel, string $funnelName, array $steps): int
+ {
+ return 4;
+ }
+
+ /**
+ * Get a list of available pattern types that can be used to configure a funnel step.
+ *
+ * @return array
+ * @throws \Exception
+ */
+ public function getAvailablePatternMatches()
+ {
+ return [];
+ }
+
+ /**
+ * Tests whether a URL matches any of the step patterns.
+ *
+ * @param string $url A value used to filter funnel flow by. E.g. URL, path, event category, event name, page title,
+ * goal ID, ... [@example="https://www.example.com/path/dir"]
+ * @param array $steps Definitions of funnel steps.
+ * [@example=[{"position": 1, "name": "Step1", "pattern_type": "path_contains", "pattern": "path/dir", "required": 0}]]
+ * @return array
+ * @throws \Exception
+ */
+ public function testUrlMatchesSteps($url, $steps)
+ {
+ $exampleResponse = [
+ 'url' => 'https://www.example.com/path/dir',
+ 'tests' => [
+ 'matches' => true,
+ 'pattern_type' => 'path_contains',
+ 'pattern' => 'path\/dir',
+ ],
+ ];
+
+ return [];
+ }
+}
diff --git a/tests/Unit/AnnotationGeneratorTest.php b/tests/Unit/AnnotationGeneratorTest.php
index 22beaf4..854b906 100644
--- a/tests/Unit/AnnotationGeneratorTest.php
+++ b/tests/Unit/AnnotationGeneratorTest.php
@@ -8,10 +8,13 @@
*
*/
+declare(strict_types=1);
+
namespace Piwik\Plugins\OpenApiDocs\tests\Unit;
use PHPUnit\Framework\TestCase;
use Piwik\API\DocumentationGenerator;
+use Piwik\API\NoDefaultValue;
use Piwik\Plugins\OpenApiDocs\Annotations\AnnotationGenerator;
/**
@@ -21,6 +24,75 @@
*/
class AnnotationGeneratorTest extends TestCase
{
+ public const TEST_RESOURCES_DIR = __DIR__ . '/../Resources';
+
+ public const EXAMPLE_API_ENDPOINTS = [
+ 'CustomAlerts.getAlert',
+ 'CustomAlerts.getAlerts',
+ 'CustomAlerts.getTriggeredAlerts',
+ 'CustomDimensions.getAvailableExtractionDimensions',
+ 'CustomDimensions.getAvailableScopes',
+ 'CustomDimensions.getConfiguredCustomDimensions',
+ 'CustomDimensions.getCustomDimension',
+ 'LogViewer.getAvailableLogReaders',
+ 'LogViewer.getConfiguredLogReaders',
+ 'LogViewer.getLogConfig',
+ 'LogViewer.getLogEntries',
+ 'MarketingCampaignsReporting.getKeyword',
+ 'MarketingCampaignsReporting.getName',
+ ];
+
+ public const EXAMPLE_RESPONSE_FILE_NAMES = [
+ 'CustomAlerts.deleteAlert.xml',
+ 'CustomAlerts.getAlert.json',
+ 'CustomAlerts.getAlerts.json',
+ 'CustomAlerts.getAlerts.xml',
+ 'CustomAlerts.getAlert.xml',
+ 'CustomAlerts.getTriggeredAlerts.json',
+ 'CustomAlerts.getTriggeredAlerts.xml',
+ 'CustomDimensions.getAvailableExtractionDimensions.json',
+ 'CustomDimensions.getAvailableExtractionDimensions.tsv',
+ 'CustomDimensions.getAvailableExtractionDimensions.xml',
+ 'CustomDimensions.getAvailableScopes.json',
+ 'CustomDimensions.getAvailableScopes.tsv',
+ 'CustomDimensions.getAvailableScopes.xml',
+ 'CustomDimensions.getConfiguredCustomDimensions.json',
+ 'CustomDimensions.getConfiguredCustomDimensions.xml',
+ 'CustomDimensions.getCustomDimension.json',
+ 'CustomDimensions.getCustomDimension.tsv',
+ 'CustomDimensions.getCustomDimension.xml',
+ 'LogViewer.getAvailableLogReaders.json',
+ 'LogViewer.getAvailableLogReaders.tsv',
+ 'LogViewer.getAvailableLogReaders.xml',
+ 'LogViewer.getConfiguredLogReaders.json',
+ 'LogViewer.getConfiguredLogReaders.tsv',
+ 'LogViewer.getConfiguredLogReaders.xml',
+ 'LogViewer.getLogConfig.json',
+ 'LogViewer.getLogConfig.xml',
+ 'LogViewer.getLogEntries.json',
+ 'LogViewer.getLogEntries.tsv',
+ 'LogViewer.getLogEntries.xml',
+ 'MarketingCampaignsReporting.getKeyword.json',
+ 'MarketingCampaignsReporting.getKeyword.tsv',
+ 'MarketingCampaignsReporting.getKeyword.xml',
+ 'MarketingCampaignsReporting.getName.json',
+ 'MarketingCampaignsReporting.getName.tsv',
+ 'MarketingCampaignsReporting.getName.xml',
+ ];
+
+ public const EXAMPLE_API_METHOD_DOC_BLOCK1 = '/**
+ * Copies a specified custom report to one or more sites. If a custom report with the same name already exists, the new custom report
+ * will have an automatically adjusted name to make it unique to the assigned site.
+ *
+ * @param int $idSite
+ * @param int $idCustomReport ID of the custom report to duplicate.
+ * @param int[] $idDestinationSites Optional array of IDs identifying which site(s) the new custom report is to be
+ * assigned to. The default is [idSite] when nothing is provided.
+ *
+ * @return array
+ * @throws Exception
+ */';
+
/**
* @var AnnotationGenerator
*/
@@ -28,11 +100,440 @@ class AnnotationGeneratorTest extends TestCase
public function setUp(): void
{
- parent::setUp();
-
$this->annotationGenerator = new AnnotationGenerator(new DocumentationGenerator());
}
+ /**
+ * @param string $apiEndpoint The identifier of the endpoint, like CustomAlerts.getAlert.
+ * @param string $format
+ *
+ * @return string String contents of the raw response body. If the file isn't found, an empty string is returned.
+ * @throws \Exception
+ */
+ private function getRawExampleResponseForApiEndpoint(string $apiEndpoint, string $format = 'json'): string
+ {
+ if (!in_array(strtolower($format), ['json', 'xml', 'tsv'])) {
+ throw new \Exception('Invalid format: ' . $format . '. Must be: "json", "xml", or "tsv"');
+ }
+
+ return file_get_contents(self::TEST_RESOURCES_DIR . "/ExampleResponses/{$apiEndpoint}.{$format}") ?: '';
+ }
+
+ /**
+ * @param string $plugin
+ * @param string $method
+ * @param string $format
+ *
+ * @return string String contents of the raw response body. If the file isn't found, an empty string is returned.
+ * @throws \Exception
+ */
+ private function getRawExampleResponseForPluginMethod(string $plugin, string $method, string $format = 'json'): string
+ {
+ return $this->getRawExampleResponseForApiEndpoint("{$plugin}.{$method}", $format);
+ }
+
+ /**
+ * Get the map of example responses. The default is returning the map for all the example responses after they've
+ * been normalised for schema generation, but before being truncated.
+ *
+ * @param bool $exampleResponseSchemas Return the generated schemas of the example responses.
+ * @param bool $onlyExamplesThatWereTruncated Return only the example responses that were truncated.
+ *
+ * @return array The map of example responses for a bunch of API endpoints.
+ * E.g. ['plugin.method' => ['json' => '...', 'xml' => '...', 'tsv' => '...']]
+ */
+ private function getExampleResponsesMap(bool $exampleResponseSchemas = false, bool $onlyExamplesThatWereTruncated = false): array
+ {
+ if ($exampleResponseSchemas && $onlyExamplesThatWereTruncated) {
+ throw new \Exception('Only one type of example response can be returned at a time.');
+ }
+
+ if ($exampleResponseSchemas) {
+ $exampleResponseSchemasString = file_get_contents(self::TEST_RESOURCES_DIR . '/ExampleResponsesNormalised/ExamplesSchemasByType.json') ?: '';
+ return json_decode($exampleResponseSchemasString, true) ?? [];
+ }
+
+ if ($onlyExamplesThatWereTruncated) {
+ $exampleResponsesPostTruncationString = file_get_contents(self::TEST_RESOURCES_DIR . '/ExampleResponsesNormalised/ExamplesPostTruncationByType.json') ?: '';
+ return json_decode($exampleResponsesPostTruncationString, true) ?? [];
+ }
+
+ $demoExampleResponsesString = file_get_contents(self::TEST_RESOURCES_DIR . '/ExampleResponsesNormalised/ExamplesFromDemoByType.json') ?: '';
+ $localExampleResponsesString = file_get_contents(self::TEST_RESOURCES_DIR . '/ExampleResponsesNormalised/ExamplesFromLocalByType.json') ?: '';
+ $demoJson = json_decode($demoExampleResponsesString, true) ?? [];
+ $localJson = json_decode($localExampleResponsesString, true) ?? [];
+ return array_merge($demoJson, $localJson);
+ }
+
+ public function testGeneratePluginApiAnnotations(): void
+ {
+ // TODO - Test the generatePluginApiAnnotations method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testGetContentForGeneratedAnnotationsFile(): void
+ {
+ // TODO - getContentForGeneratedAnnotationsFile method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testBuildAnnotationForMethod(): void
+ {
+ // TODO - buildAnnotationForMethod method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testGetParamInfoFromDocBlock(): void
+ {
+ // TODO - Update to use resource file and/or dataprovider to test more than one comment block
+ $expected = [
+ 'idSite' => [
+ 'type' => 'int',
+ 'description' => '',
+ 'byRef' => false,
+ 'variadic' => false,
+ ],
+ 'idCustomReport' => [
+ 'type' => 'int',
+ 'description' => 'ID of the custom report to duplicate.',
+ 'byRef' => false,
+ 'variadic' => false,
+ ],
+ 'idDestinationSites' => [
+ 'type' => 'int[]',
+ 'description' => 'Optional array of IDs identifying which site(s) the new custom report is to be assigned to. The default is [idSite] when nothing is provided.',
+ 'byRef' => false,
+ 'variadic' => false,
+ ],
+ ];
+ $this->assertEquals($expected, $this->annotationGenerator->getParamInfoFromDocBlock(self::EXAMPLE_API_METHOD_DOC_BLOCK1));
+ }
+
+ public function testGetResponseInfoFromDocBlock(): void
+ {
+ // TODO - Update to use resource file and/or dataprovider to test more than one comment block
+ $expected = [
+ 'type' => 'array'
+ ];
+ $this->assertEquals($expected, $this->annotationGenerator->getResponseInfoFromDocBlock(self::EXAMPLE_API_METHOD_DOC_BLOCK1));
+ }
+
+ /**
+ * @dataProvider getTestDataForBuildVirtualPath
+ *
+ * @param string $pathTemplate
+ * @param string $pluginName
+ * @param string $methodName
+ * @param string $expected
+ *
+ * @return void
+ */
+ public function testBuildVirtualPath(string $pathTemplate, string $pluginName, string $methodName, string $expected): void
+ {
+ $this->assertEquals($expected, $this->annotationGenerator->buildVirtualPath($pathTemplate, $pluginName, $methodName));
+ }
+
+ /**
+ * @return iterable
+ */
+ public function getTestDataForBuildVirtualPath(): iterable
+ {
+ yield 'should be empty when all values are empty' => ['', '', '', ''];
+ yield 'should be empty when template is empty' => ['', 'SomePlugin', 'SomeMethod', ''];
+ yield 'should remain the same when template does not include placeholders' => ['/some/test/path', 'SomePlugin', 'SomeMethod', '/some/test/path'];
+ yield 'should replace only plugin when the only placeholder' => ['/{plugin}/test/path', 'SomePlugin', 'SomeMethod', '/SomePlugin/test/path'];
+ yield 'should replace only method when the only placeholder' => ['/{method}/test/path', 'SomePlugin', 'SomeMethod', '/SomeMethod/test/path'];
+ yield 'should include both values when placeholders are present' => ['/{plugin}/{method}/test/path', 'SomePlugin', 'SomeMethod', '/SomePlugin/SomeMethod/test/path'];
+ yield 'should follow placement of placeholders' => ['/{method}/{plugin}/test/path', 'SomePlugin', 'SomeMethod', '/SomeMethod/SomePlugin/test/path'];
+ yield 'should allow duplication of placeholders' => ['/{plugin}/{method}/test/path/{plugin}', 'SomePlugin', 'SomeMethod', '/SomePlugin/SomeMethod/test/path/SomePlugin'];
+ yield 'should work with query parameter format' => ['/index.php?module=API&method={plugin}.{method}', 'SomePlugin', 'SomeMethod', '/index.php?module=API&method=SomePlugin.SomeMethod'];
+ yield 'should work with different names' => ['/index.php?module=API&method={plugin}.{method}', 'TagManager', 'GetContainers', '/index.php?module=API&method=TagManager.GetContainers'];
+ }
+
+ /**
+ * @dataProvider getTestDataForBuildParameterAnnotationData
+ *
+ * @param string $paramName
+ * @param array $paramMetadata
+ * @param array $paramDocInfo
+ * @param array $expected
+ *
+ * @return void
+ */
+ public function testBuildParameterAnnotationData(string $paramName, array $paramMetadata, array $paramDocInfo, array $expected): void
+ {
+ $this->assertEquals($expected, $this->annotationGenerator->buildParameterAnnotationData('someMethodName', $paramName, $paramMetadata, $paramDocInfo));
+ }
+
+ /**
+ * @return iterable
+ */
+ public function getTestDataForBuildParameterAnnotationData(): iterable
+ {
+ yield 'should be default values with no data' => ['', [], [], [
+ 'name' => '',
+ 'types' => ['string' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should be very basic with only param name' => ['someParam', [], [], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should be fine with another param name' => ['idSite', [], [], [
+ 'name' => 'idSite',
+ 'types' => ['string' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should be still have string type when string is provided' => ['someParam', [
+ 'type' => 'string',
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should be integer type when int is provided' => ['someParam', [
+ 'type' => 'int',
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['integer' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should be array type when array is provided' => ['someParam', [
+ 'type' => 'array',
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['array' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should show as not required and use metadata default when provided' => ['someParam', [
+ 'default' => 'SomeDefaultValue',
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => '',
+ 'required' => 'false',
+ 'default' => '"SomeDefaultValue"',
+ 'example' => '',
+ ]];
+ yield 'should not wrap metadata default value when boolean type' => ['someParam', [
+ 'type' => 'bool',
+ 'default' => true,
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['boolean' => null],
+ 'description' => '',
+ 'required' => 'false',
+ 'default' => 'true',
+ 'example' => '',
+ ]];
+ yield 'should still count false boolean as a default value' => ['someParam', [
+ 'type' => 'bool',
+ 'default' => false,
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['boolean' => null],
+ 'description' => '',
+ 'required' => 'false',
+ 'default' => 'false',
+ 'example' => '',
+ ]];
+ yield 'should still count empty string as a default value' => ['someParam', [
+ 'default' => '',
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => '',
+ 'required' => 'false',
+ 'default' => '""',
+ 'example' => '',
+ ]];
+ yield 'should not count the NoDefaultValue class as a default value' => ['someParam', [
+ 'default' => new NoDefaultValue(),
+ ], [], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should not override the metadata type when it is integer' => ['someParam', [
+ 'type' => 'int',
+ ], [
+ 'type' => 'array',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['integer' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should override the metadata type when it is string and docInfo is array' => ['someParam', [
+ 'type' => 'string',
+ ], [
+ 'type' => 'array',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['array' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should use docInfo type when metadata type is empty' => ['someParam', [], [
+ 'type' => 'boolean',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['boolean' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should determine subtype when docInfo type indicates the type of array items' => ['someParam', [], [
+ 'type' => 'int[]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['array' => 'integer'],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should still determine subtype when metadata type is array and docInfo indicates subtype' => ['someParam', [
+ 'type' => 'array',
+ ], [
+ 'type' => 'int[]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['array' => 'integer'],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should allow multiple types when docInfo includes them' => ['someParam', [], [
+ 'type' => 'string|int|int[]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null, 'integer' => null, 'array' => 'integer'],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should allow multiple types when metadata type is string' => ['someParam', [
+ 'type' => 'string',
+ ], [
+ 'type' => 'string|int|int[]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null, 'integer' => null, 'array' => 'integer'],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should not allow multiple types when metadata type is specified' => ['someParam', [
+ 'type' => 'integer',
+ ], [
+ 'type' => 'string|int|int[]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['integer' => null],
+ 'description' => '',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should use docInfo description when provided' => ['someParam', [], [
+ 'description' => 'Some test description.',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => 'Some test description.',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '',
+ ]];
+ yield 'should use example when provided using custom syntax in docInfo description' => ['someParam', [], [
+ 'description' => 'Some test description. [@example=true]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => 'Some test description.',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => 'true',
+ ]];
+ yield 'should use string example when provided using custom syntax in docInfo description' => ['someParam', [], [
+ 'description' => 'Some test description. [@example="Some test example string."]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => 'Some test description.',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => 'Some test example string.',
+ ]];
+ yield 'should allow full JSON in docInfo description examples' => ['someParam', [], [
+ 'description' => 'Some test description. [@example={"key1":"value1","key2":"value2"}]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => 'Some test description.',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '{"key1":"value1","key2":"value2"}',
+ ]];
+ yield 'should allow full JSON array in docInfo description examples' => ['someParam', [], [
+ 'description' => 'Some test description. [@example=[{"key1":"value1","key2":"value2"},{"key3":"value3","key4":"value4"}]]',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => 'Some test description.',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '[{"key1":"value1","key2":"value2"},{"key3":"value3","key4":"value4"}]',
+ ]];
+ yield 'should allow full JSON array examples even when in the middle of the docInfo description' => ['someParam', [], [
+ 'description' => 'Some test description. [@example=[{"key1":"value1","key2":"value2"},{"key3":"value3","key4":"value4"}]] More test description.',
+ ], [
+ 'name' => 'someParam',
+ 'types' => ['string' => null],
+ 'description' => 'Some test description. More test description.',
+ 'required' => 'true',
+ 'default' => 'Piwik\API\NoDefaultValue',
+ 'example' => '[{"key1":"value1","key2":"value2"},{"key3":"value3","key4":"value4"}]',
+ ]];
+ }
+
+ public function testDetermineParameters(): void
+ {
+ // TODO - determineParameters method
+ $this->expectNotToPerformAssertions();
+ }
+
/**
* @dataProvider getTestDataForGetOpenApiTypeFromPhpType
*
@@ -67,4 +568,233 @@ public function getTestDataForGetOpenApiTypeFromPhpType(): iterable
yield 'should be number for float' => ['float', 'number'];
yield 'should be number for double' => ['double', 'number'];
}
+
+ public function testGetApplicableDemoExampleUrls(): void
+ {
+ // TODO - getApplicableDemoExampleUrls method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testGetDemoReportMetadata(): void
+ {
+ // TODO - getDemoReportMetadata method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testGetExampleIfAvailable(): void
+ {
+ // TODO - getExampleIfAvailable method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testGetReportExampleUrlFromMetadata(): void
+ {
+ // TODO - getReportExampleUrlFromMetadata method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testConvertExampleXmlToObject(): void
+ {
+ $normalisedMap = $this->getExampleResponsesMap();
+ foreach (self::EXAMPLE_API_ENDPOINTS as $endpoint) {
+ $content = $this->getRawExampleResponseForApiEndpoint($endpoint, 'xml');
+ $this->assertNotEmpty($content, 'The example response should not be empty for endpoint: ' . $endpoint);
+ $expected = $normalisedMap[$endpoint]['xml'] ?? [];
+ $this->assertEquals($expected, json_encode($this->annotationGenerator->convertExampleXmlToObject($content)), "The converted XML was not as expected for endpoint $endpoint.");
+ }
+ }
+
+ public function testDetermineResponses(): void
+ {
+ // TODO - determineResponses method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testCutExampleCloseToCharLimit(): void
+ {
+ $truncatedMap = $this->getExampleResponsesMap(false, true);
+ $normalisedMap = $this->getExampleResponsesMap();
+ foreach (self::EXAMPLE_API_ENDPOINTS as $endpoint) {
+ $normalisedExamples = $normalisedMap[$endpoint] ?? [];
+
+ foreach ($normalisedExamples as $type => $normalisedExample) {
+ $expectedExample = $normalisedExample;
+ if (!empty($truncatedMap[$endpoint][$type])) {
+ $expectedExample = $truncatedMap[$endpoint][$type];
+ }
+
+ // Skip the endpoints which don't have TSV examples
+ if (
+ (
+ $type === 'tsv'
+ && in_array($endpoint, [
+ 'CustomAlerts.getAlert',
+ 'CustomAlerts.getAlerts',
+ 'CustomAlerts.getTriggeredAlerts',
+ 'CustomDimensions.getConfiguredCustomDimensions',
+ 'LogViewer.getLogConfig',
+ ])
+ )
+ ) {
+ continue;
+ }
+
+ $this->assertNotEmpty($normalisedExample, "The example response should not be empty for endpoint '$endpoint' and type '$type'.");
+ $result = $this->annotationGenerator->cutExampleCloseToCharLimit($normalisedExample, $type);
+ $this->assertLessThanOrEqual(AnnotationGenerator::EXAMPLE_CHAR_LIMIT, strlen($result), "The example response should not exceed the character limit for endpoint '$endpoint' and type '$type'.");
+ $this->assertEquals($expectedExample, $result, "The truncated example was not as expected for endpoint '$endpoint' and type '$type'.");
+ }
+ }
+ }
+
+ public function testBuildSchemaAnnotationFromJsonExample(): void
+ {
+ // TODO - buildSchemaAnnotationFromJsonExample method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testBuildPropertyAnnotationFromJsonExample(): void
+ {
+ // TODO - buildPropertyAnnotationFromJsonExample method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testBuildSchemaAnnotationFromXmlExample(): void
+ {
+ // TODO - buildSchemaAnnotationFromXmlExample method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testBuildPropertyAnnotationFromXmlExample(): void
+ {
+ // TODO - buildPropertyAnnotationFromXmlExample method
+ $this->expectNotToPerformAssertions();
+ }
+
+ /**
+ * @dataProvider getTestDataForRemoveTrailingCommaFromLastLine
+ *
+ * @param array $lines
+ * @param array $expected
+ *
+ * @return void
+ */
+ public function testRemoveTrailingCommaFromLastLine(array $lines, array $expected): void
+ {
+ $this->annotationGenerator->removeTrailingCommaFromLastLine($lines);
+ $this->assertEquals($expected, $lines);
+ }
+
+ /**
+ * @return iterable
+ */
+ public function getTestDataForRemoveTrailingCommaFromLastLine(): iterable
+ {
+ yield 'should be fine with empty arrays' => [[], []];
+ yield 'should be fine with no commas' => [['test'], ['test']];
+ yield 'should be fine with multiple lines and no commas' => [['test1', 'test2'], ['test1', 'test2']];
+ yield 'should remove the trailing comma from the last line' => [['test1,', 'test2,'], ['test1,', 'test2']];
+ yield 'should only remove the trailing comma from the last line' => [['test1,test2,test3,', 'test4,test5,test6,'], ['test1,test2,test3,', 'test4,test5,test6']];
+ yield 'should handle spaced lists correctly' => [['test1, test2, test3,', 'test4, test5, test6,'], ['test1, test2, test3,', 'test4, test5, test6']];
+ yield 'should only remove the comma if it is the last character' => [['test1, test2, test3,', 'test4, test5, test6, '], ['test1, test2, test3,', 'test4, test5, test6, ']];
+ yield 'should handle nested JSON correctly' => [
+ [
+ '@OA\Property(',
+ ' property="dimensions",',
+ ' type="object",',
+ ' @OA\Property(',
+ ' property="row",',
+ ' type="array",',
+ ' @OA\Items(',
+ ' type="string"',
+ ' )',
+ ' )',
+ '),',
+ ],
+ [
+ '@OA\Property(',
+ ' property="dimensions",',
+ ' type="object",',
+ ' @OA\Property(',
+ ' property="row",',
+ ' type="array",',
+ ' @OA\Items(',
+ ' type="string"',
+ ' )',
+ ' )',
+ ')',
+ ]
+ ];
+ }
+
+ public function testBuildLinesForAnnotationObject(): void
+ {
+ // TODO - buildLinesForAnnotationObject method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testBuildSchemaObjectArray(): void
+ {
+ // TODO - buildSchemaObjectArray method
+ $this->expectNotToPerformAssertions();
+ }
+
+ /**
+ * @dataProvider getTestDataForWrapStringWithQuotes
+ *
+ * @param string $stringValue
+ * @param string $type
+ * @param string|null $quoteCharacter
+ *
+ * @return void
+ */
+ public function testWrapStringWithQuotes(string $stringValue, string $type, ?string $quoteCharacter, string $expected): void
+ {
+ $result = $quoteCharacter === null ? $this->annotationGenerator->wrapStringWithQuotes($stringValue, $type) : $this->annotationGenerator->wrapStringWithQuotes($stringValue, $type, $quoteCharacter);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * @return iterable
+ */
+ public function getTestDataForWrapStringWithQuotes(): iterable
+ {
+ yield 'should be empty quoted string if everything is empty' => ['', '', null, '""'];
+ yield 'should be empty quoted string if string type and empty value' => ['', 'string', null, '""'];
+ yield 'should be empty string if integer type and empty value' => ['', 'integer', null, ''];
+ yield 'should be empty string if boolean type and empty value' => ['', 'boolean', null, ''];
+ yield 'should be empty string if array type and empty value' => ['', 'array', null, ''];
+ yield 'should be empty quoted string if string type and quoted empty string value' => ['""', 'string', null, '""'];
+ yield 'should be empty single-quoted string if string type and single-quoted empty string value' => ["''", 'string', null, "''"];
+ yield 'should be empty object string if string type and empty object value' => ['{}', 'string', null, '{}'];
+ yield 'should be use the quote character when provided' => ["''", '', '"', "''"];
+ yield 'should be use the quote character when provided even when not quote' => ['', '', '|', '||'];
+ yield 'should be quoted string if no type and string value' => ['test', '', null, '"test"'];
+ yield 'should be quoted string if string type and string value' => ['test', 'string', null, '"test"'];
+ yield 'should be integer string if integer type and integer string value' => ['12', 'integer', null, '12'];
+ yield 'should be boolean string if boolean type and boolean string value' => ['true', 'boolean', null, 'true'];
+ yield 'should be array string if array type and array string value' => ['[]', 'array', null, '[]'];
+ yield 'should be use the custom quote character when provided even when not quote' => ['test', 'string', '|', "|test|"];
+ yield 'should ignore the custom quote character when integer type' => ['30', 'integer', '|', '30'];
+ yield 'should ignore the custom quote character when boolean type' => ['30', 'boolean', '|', '30'];
+ yield 'should ignore the custom quote character when array type' => ['30', 'array', '|', '30'];
+ }
+
+ public function testShouldIncludeDefault(): void
+ {
+ // TODO - shouldIncludeDefault method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testBuildSchemaObjectArrays(): void
+ {
+ // TODO - buildSchemaObjectArrays method
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testCompileOperationLines(): void
+ {
+ // TODO - compileOperationLines method
+ $this->expectNotToPerformAssertions();
+ }
}