From 2220918f324d7e4b0f087c064ba3835e7864f2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Jodas?= <12143866+ondrajodas@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:12:28 +0200 Subject: [PATCH] Revert "[AJDA-1176] Skip orphaned manifests" --- phpstan-baseline.neon | 10 + src/Extractor/Extractor.php | 27 +- src/Extractor/Paginator/IPaginator.php | 2 +- src/Extractor/Paginator/ProfilesPaginator.php | 4 +- .../Paginator/PropertiesPaginator.php | 4 +- .../ApplicationTest.php | 73 +++++ .../Paginator/PropertiesPaginatorTest.php | 285 ------------------ tests/data/config_antisampling.json | 52 ++++ tests/data/config_antisampling_adaptive.json | 53 ++++ tests/data/config_mcf.json | 63 ++++ .../expected/data/out/files/.gitkeep | 0 .../expected/data/out/tables/properties.csv | 1 - .../data/out/tables/properties.csv.manifest | 1 - .../expected/data/out/usage.json | 1 - .../source/data/config.json | 54 ---- 15 files changed, 258 insertions(+), 372 deletions(-) delete mode 100644 tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php create mode 100644 tests/data/config_antisampling.json create mode 100644 tests/data/config_antisampling_adaptive.json create mode 100644 tests/data/config_mcf.json delete mode 100644 tests/functional/download-empty-data/expected/data/out/files/.gitkeep delete mode 100644 tests/functional/download-empty-data/expected/data/out/tables/properties.csv delete mode 100644 tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest delete mode 100644 tests/functional/download-empty-data/expected/data/out/usage.json delete mode 100644 tests/functional/download-empty-data/source/data/config.json diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 50883ca..0d514c7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -840,6 +840,11 @@ parameters: count: 1 path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php + - + message: "#^Method Keboola\\\\GoogleAnalyticsExtractor\\\\ApplicationTest\\:\\:assertManifestContainsColumns\\(\\) has parameter \\$expected with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php + - message: "#^Method Keboola\\\\GoogleAnalyticsExtractor\\\\ApplicationTest\\:\\:getConfig\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -850,6 +855,11 @@ parameters: count: 1 path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php + - + message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" + count: 1 + path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php + - message: "#^Parameter \\#2 \\$array of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) expects array\\|ArrayAccess, mixed given\\.$#" count: 2 diff --git a/src/Extractor/Extractor.php b/src/Extractor/Extractor.php index 8c27842..199d9be 100644 --- a/src/Extractor/Extractor.php +++ b/src/Extractor/Extractor.php @@ -60,10 +60,10 @@ public function runProfiles(array $query, array $profiles): array if (isset($query['query'])) { $outputCsv = $this->output->createReport($query); + $this->output->createManifest($outputCsv->getFilename(), $query, ['id'], true); $this->logger->info(sprintf("Running query '%s'", $query['outputTable'])); $downloadedProfiles = false; - $manifestCreated = false; foreach ($profiles as $profile) { $this->logger->info(sprintf('Profile "%s" export started.', $profile['id'])); $apiQuery = $query; @@ -130,16 +130,7 @@ public function runProfiles(array $query, array $profiles): array } } - $rowCount = $paginator->paginate($apiQuery, $report, $outputCsv); - if ($rowCount > 0 && !$manifestCreated) { - $this->output->createManifest( - $outputCsv->getFilename(), - $query, - ['id'], - true, - ); - $manifestCreated = true; - } + $paginator->paginate($apiQuery, $report, $outputCsv); $status[$query['outputTable']][$profile['id']] = 'ok'; } @@ -168,10 +159,10 @@ public function runProperties(array $query, array $properties): array $query['query']['endpoint'] = 'properties'; $outputCsv = $this->output->createReport($query); + $this->output->createManifest($outputCsv->getFilename(), $query, ['id'], true, 'idProperty'); $this->logger->info(sprintf("Running query '%s'", $query['outputTable'])); $downloadedProperties = false; - $manifestCreated = false; foreach ($properties as $property) { $this->logger->info(sprintf('Property "%s" export started.', $property['propertyName'])); if (!empty($query['query']['viewId']) @@ -208,17 +199,7 @@ public function runProperties(array $query, array $properties): array continue; } - $rowCount = $paginator->paginate($apiQuery, $report, $outputCsv); - if ($rowCount > 0 && !$manifestCreated) { - $this->output->createManifest( - $outputCsv->getFilename(), - $query, - ['id'], - true, - 'idProperty', - ); - $manifestCreated = true; - } + $paginator->paginate($apiQuery, $report, $outputCsv); $status[$query['outputTable']][$property['propertyKey']] = 'ok'; } diff --git a/src/Extractor/Paginator/IPaginator.php b/src/Extractor/Paginator/IPaginator.php index efa79dc..2e8bebf 100644 --- a/src/Extractor/Paginator/IPaginator.php +++ b/src/Extractor/Paginator/IPaginator.php @@ -12,5 +12,5 @@ interface IPaginator { public function getOutput(): Output; public function getClient(): Client; - public function paginate(array $query, array $report, CsvFile $csvFile): int; + public function paginate(array $query, array $report, CsvFile $csvFile): void; } diff --git a/src/Extractor/Paginator/ProfilesPaginator.php b/src/Extractor/Paginator/ProfilesPaginator.php index b19b88b..3bf2cc4 100644 --- a/src/Extractor/Paginator/ProfilesPaginator.php +++ b/src/Extractor/Paginator/ProfilesPaginator.php @@ -34,7 +34,7 @@ public function getClient(): Client return $this->client; } - public function paginate(array $query, array $report, CsvFile $csvFile): int + public function paginate(array $query, array $report, CsvFile $csvFile): void { $counter = 0; do { @@ -60,8 +60,6 @@ public function paginate(array $query, array $report, CsvFile $csvFile): int } $query = $nextQuery; } while ($query); - - return $counter; } private function getStartIndex(string $link): string diff --git a/src/Extractor/Paginator/PropertiesPaginator.php b/src/Extractor/Paginator/PropertiesPaginator.php index 13bdff2..c3750c4 100644 --- a/src/Extractor/Paginator/PropertiesPaginator.php +++ b/src/Extractor/Paginator/PropertiesPaginator.php @@ -44,7 +44,7 @@ public function setProperty(array $property): self return $this; } - public function paginate(array $query, array $report, CsvFile $csvFile): int + public function paginate(array $query, array $report, CsvFile $csvFile): void { $localCounter = 0; do { @@ -66,7 +66,5 @@ public function paginate(array $query, array $report, CsvFile $csvFile): int $query = $nextQuery; } while ($report['totals'] > $localCounter); - - return $localCounter; } } diff --git a/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php b/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php index a473bce..f4032ff 100644 --- a/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php +++ b/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php @@ -50,6 +50,72 @@ private function getConfig(string $suffix = ''): array return $config; } + public function testAppRunDailyWalk(): void + { + $this->config = $this->getConfig('_antisampling'); + $this->runProcess(); + + $dailyWalk = $this->getManifestFiles('dailyWalk'); + Assert::assertEquals(1, count($dailyWalk)); + + foreach ($dailyWalk as $file) { + /** @var $file SplFileInfo */ + $this->assertManifestContainsColumns($file->getPathname(), [ + 'id', + 'idProfile', + 'date', + 'sourceMedium', + 'landingPagePath', + 'pageviews', + ]); + } + } + + public function testAppRunAdaptive(): void + { + $this->config = $this->getConfig('_antisampling_adaptive'); + $this->runProcess(); + + $adaptive = $this->getManifestFiles('adaptive'); + Assert::assertEquals(1, count($adaptive)); + + foreach ($adaptive as $file) { + /** @var $file SplFileInfo */ + $this->assertManifestContainsColumns($file->getPathname(), [ + 'id', + 'idProfile', + 'date', + 'sourceMedium', + 'landingPagePath', + 'pageviews', + ]); + } + } + + public function testAppRunMCF(): void + { + $this->config = $this->getConfig('_mcf'); + $this->runProcess(); + + $funnelFiles = $this->getManifestFiles('funnel'); + Assert::assertEquals(1, count($funnelFiles)); + + foreach ($funnelFiles as $file) { + /** @var $file SplFileInfo */ + $this->assertManifestContainsColumns($file->getPathname(), [ + 'id', + 'idProfile', + 'mcf:conversionDate', + 'mcf:sourcePath', + 'mcf:mediumPath', + 'mcf:sourceMedium', + 'mcf:totalConversions', + 'mcf:totalConversionValue', + 'mcf:assistedConversions', + ]); + } + } + public function testAppProfilesProperties(): void { $this->config = $this->getConfig('_empty'); @@ -117,6 +183,13 @@ private function getManifestFiles(string $queryName): Finder ; } + private function assertManifestContainsColumns(string $pathname, array $expected): void + { + $manifest = (array) json_decode(file_get_contents($pathname), true, 512, JSON_THROW_ON_ERROR); + Assert::assertArrayHasKey('columns', $manifest); + Assert::assertEquals($expected, $manifest['columns']); + } + public function appRunDataProvider(): Generator { yield 'configRow' => [ diff --git a/tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php b/tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php deleted file mode 100644 index b3ffe2c..0000000 --- a/tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php +++ /dev/null @@ -1,285 +0,0 @@ -output = $this->createMock(Output::class); - $this->client = $this->createMock(Client::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->paginator = new PropertiesPaginator( - $this->output, - $this->client, - $this->logger, - ); - } - - public function testPaginateSinglePage(): void - { - $property = [ - 'propertyKey' => 'properties/123456789', - 'propertyName' => 'Test Property', - ]; - - $query = [ - 'query' => [ - 'dimensions' => [['name' => 'ga:date']], - 'metrics' => [['name' => 'ga:sessions']], - 'dateRanges' => [ - ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], - ], - ], - ]; - - $report = [ - 'data' => [ - new Result(['ga:sessions' => '100'], ['ga:date' => '2023-01-01']), - new Result(['ga:sessions' => '150'], ['ga:date' => '2023-01-02']), - ], - 'totals' => 2, - 'rowCount' => 2, - ]; - - /** @var CsvFile $csvFile */ - $csvFile = $this->createMock(CsvFile::class); - - $this->paginator->setProperty($property); - - // Expect writeReport to be called once with the correct property ID - $this->output->expects($this->once()) - ->method('writeReport') - ->with($csvFile, $report, '123456789'); - - // Expect logger to be called with progress info - $this->logger->expects($this->once()) - ->method('info') - ->with('Downloaded 2/2 records.'); - - $result = $this->paginator->paginate($query, $report, $csvFile); - - $this->assertEquals(2, $result); - } - - public function testPaginateMultiplePages(): void - { - $property = [ - 'propertyKey' => 'properties/123456789', - 'propertyName' => 'Test Property', - ]; - - $query = [ - 'query' => [ - 'dimensions' => [['name' => 'ga:date']], - 'metrics' => [['name' => 'ga:sessions']], - 'dateRanges' => [ - ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], - ], - ], - ]; - - $firstReport = [ - 'data' => [ - new Result(['ga:sessions' => '100'], ['ga:date' => '2023-01-01']), - new Result(['ga:sessions' => '150'], ['ga:date' => '2023-01-02']), - ], - 'totals' => 4, - 'rowCount' => 2, - ]; - - $secondReport = [ - 'data' => [ - new Result(['ga:sessions' => '200'], ['ga:date' => '2023-01-03']), - new Result(['ga:sessions' => '250'], ['ga:date' => '2023-01-04']), - ], - 'totals' => 4, - 'rowCount' => 2, - ]; - - /** @var CsvFile $csvFile */ - $csvFile = $this->createMock(CsvFile::class); - - $this->paginator->setProperty($property); - - // Expect writeReport to be called twice - $this->output->expects($this->exactly(2)) - ->method('writeReport') - ->with($csvFile, $this->isType('array'), '123456789'); - - // Expect logger to be called twice with progress info - $this->logger->expects($this->exactly(2)) - ->method('info') - ->willReturnOnConsecutiveCalls( - $this->returnValue(null), - $this->returnValue(null), - ); - - // Expect client to be called once for the second page - $this->client->expects($this->once()) - ->method('getPropertyReport') - ->with( - [ - 'query' => [ - 'dimensions' => [['name' => 'ga:date']], - 'metrics' => [['name' => 'ga:sessions']], - 'dateRanges' => [ - ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], - ], - 'offset' => 2, - ], - ], - $property, - ) - ->willReturn($secondReport); - - $result = $this->paginator->paginate($query, $firstReport, $csvFile); - - $this->assertEquals(4, $result); - } - - public function testPaginateEmptyData(): void - { - $property = [ - 'propertyKey' => 'properties/123456789', - 'propertyName' => 'Test Property', - ]; - - $query = [ - 'query' => [ - 'dimensions' => [['name' => 'ga:date']], - 'metrics' => [['name' => 'ga:sessions']], - 'dateRanges' => [ - ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], - ], - ], - ]; - - $report = [ - 'data' => [], - 'totals' => 0, - 'rowCount' => 0, - ]; - - /** @var CsvFile $csvFile */ - $csvFile = $this->createMock(CsvFile::class); - - $this->paginator->setProperty($property); - - // Expect writeReport to be called once even with empty data - $this->output->expects($this->once()) - ->method('writeReport') - ->with($csvFile, $report, '123456789'); - - // Expect logger to be called with progress info - $this->logger->expects($this->once()) - ->method('info') - ->with('Downloaded 0/0 records.'); - - $result = $this->paginator->paginate($query, $report, $csvFile); - - $this->assertEquals(0, $result); - } - - public function testPaginateWithOffsetInQuery(): void - { - $property = [ - 'propertyKey' => 'properties/987654321', - 'propertyName' => 'Test Property 2', - ]; - - $query = [ - 'query' => [ - 'dimensions' => [['name' => 'ga:date']], - 'metrics' => [['name' => 'ga:sessions']], - 'dateRanges' => [ - ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], - ], - 'offset' => 10, // Existing offset should be preserved - ], - ]; - - $firstReport = [ - 'data' => [ - new Result(['ga:sessions' => '100'], ['ga:date' => '2023-01-01']), - ], - 'totals' => 3, - 'rowCount' => 1, - ]; - - $secondReport = [ - 'data' => [ - new Result(['ga:sessions' => '200'], ['ga:date' => '2023-01-02']), - ], - 'totals' => 3, - 'rowCount' => 1, - ]; - - $thirdReport = [ - 'data' => [ - new Result(['ga:sessions' => '300'], ['ga:date' => '2023-01-03']), - ], - 'totals' => 3, - 'rowCount' => 1, - ]; - - /** @var CsvFile $csvFile */ - $csvFile = $this->createMock(CsvFile::class); - - $this->paginator->setProperty($property); - - // Expect writeReport to be called three times - $this->output->expects($this->exactly(3)) - ->method('writeReport') - ->with($csvFile, $this->isType('array'), '987654321'); - - // Expect logger to be called three times - $this->logger->expects($this->exactly(3)) - ->method('info') - ->willReturnOnConsecutiveCalls( - $this->returnValue(null), - $this->returnValue(null), - $this->returnValue(null), - ); - - // Expect client to be called twice for subsequent pages - $callCount = 0; - $this->client->expects($this->exactly(2)) - ->method('getPropertyReport') - ->willReturnCallback(function ($query, $property) use (&$callCount, $secondReport, $thirdReport) { - $callCount++; - if ($callCount === 1) { - $this->assertEquals(1, $query['query']['offset']); - return $secondReport; - } else { - $this->assertEquals(2, $query['query']['offset']); - return $thirdReport; - } - }); - - $result = $this->paginator->paginate($query, $firstReport, $csvFile); - - $this->assertEquals(3, $result); - } - - public function testGetOutput(): void - { - $this->assertSame($this->output, $this->paginator->getOutput()); - } -} diff --git a/tests/data/config_antisampling.json b/tests/data/config_antisampling.json new file mode 100644 index 0000000..7bfd79a --- /dev/null +++ b/tests/data/config_antisampling.json @@ -0,0 +1,52 @@ +{ + "parameters": { + "retriesCount": 1, + "outputBucket": "in.c-ex-google-analytics-cfg1", + "profiles": [ + { + "id": 184062725, + "name": "All Web Site Data", + "webPropertyId": "UA-128209249-1", + "webPropertyName": "Keboola Website", + "accountId": 128209249, + "accountName": "Keboola Website" + }, + { + "id": 88156763, + "name": "All Web Site Data", + "webPropertyId": "UA-128209249-1", + "webPropertyName": "status.keboola.com", + "accountId": 128209249, + "accountName": "Keboola Status" + } + ], + "outputTable": "dailyWalk", + "antisampling": "dailyWalk", + "query": { + "metrics": [ + { + "expression": "ga:pageviews" + } + ], + "dimensions": [ + { + "name": "ga:date" + }, + { + "name": "ga:sourceMedium" + }, + { + "name": "ga:landingPagePath" + } + ], + "filtersExpression": "", + "segments": null, + "dateRanges": [ + { + "startDate": "-3 days", + "endDate": "-1 day" + } + ] + } + } +} diff --git a/tests/data/config_antisampling_adaptive.json b/tests/data/config_antisampling_adaptive.json new file mode 100644 index 0000000..b8f568e --- /dev/null +++ b/tests/data/config_antisampling_adaptive.json @@ -0,0 +1,53 @@ +{ + "parameters": { + "retriesCount": 1, + "outputBucket": "in.c-ex-google-analytics-cfg1", + "profiles": [ + { + "id": 184062725, + "name": "All Web Site Data", + "webPropertyId": "UA-128209249-1", + "webPropertyName": "Keboola Website", + "accountId": 128209249, + "accountName": "Keboola Website" + }, + { + "id": 88156763, + "name": "All Web Site Data", + "webPropertyId": "UA-128209249-1", + "webPropertyName": "status.keboola.com", + "accountId": 128209249, + "accountName": "Keboola Status" + } + ], + "outputTable": "adaptive", + "antisampling": "adaptive", + "query": { + "viewId": "26550866", + "metrics": [ + { + "expression": "ga:pageviews" + } + ], + "dimensions": [ + { + "name": "ga:date" + }, + { + "name": "ga:sourceMedium" + }, + { + "name": "ga:landingPagePath" + } + ], + "filtersExpression": "", + "segments": null, + "dateRanges": [ + { + "startDate": "-3 days", + "endDate": "-1 day" + } + ] + } + } +} diff --git a/tests/data/config_mcf.json b/tests/data/config_mcf.json new file mode 100644 index 0000000..c6c4d3e --- /dev/null +++ b/tests/data/config_mcf.json @@ -0,0 +1,63 @@ +{ + "parameters": { + "outputBucket": "in.c-ex-google-analytics-cfg1", + "retriesCount": 1, + "profiles": [ + { + "id": 184062725, + "name": "All Web Site Data", + "webPropertyId": "UA-128209249-1", + "webPropertyName": "Keboola Website", + "accountId": 128209249, + "accountName": "Keboola Website" + }, + { + "id": 88156763, + "name": "All Web Site Data", + "webPropertyId": "UA-128209249-1", + "webPropertyName": "status.keboola.com", + "accountId": 128209249, + "accountName": "Keboola Status" + } + ], + "outputTable": "funnel", + "endpoint": "mcf", + "antisampling": "dailyWalk", + "query": { + "samplingLevel": "FASTER", + "maxResults": 100, + "metrics": [ + { + "expression": "mcf:totalConversions" + }, + { + "expression": "mcf:totalConversionValue" + }, + { + "expression": "mcf:assistedConversions" + } + ], + "dimensions": [ + { + "name": "mcf:conversionDate" + }, + { + "name": "mcf:sourcePath" + }, + { + "name": "mcf:mediumPath" + }, + { + "name": "mcf:sourceMedium" + } + ], + "filtersExpression": "", + "dateRanges": [ + { + "startDate": "-1 week", + "endDate": "-1 day" + } + ] + } + } +} diff --git a/tests/functional/download-empty-data/expected/data/out/files/.gitkeep b/tests/functional/download-empty-data/expected/data/out/files/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/functional/download-empty-data/expected/data/out/tables/properties.csv b/tests/functional/download-empty-data/expected/data/out/tables/properties.csv deleted file mode 100644 index 1b7864b..0000000 --- a/tests/functional/download-empty-data/expected/data/out/tables/properties.csv +++ /dev/null @@ -1 +0,0 @@ -"properties/403517979","Website - GA4","accounts/128209249","Keboola Website" diff --git a/tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest b/tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest deleted file mode 100644 index 331dcfe..0000000 --- a/tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest +++ /dev/null @@ -1 +0,0 @@ -{"destination":"in.c-keboola-ex-google-analytics-v4-01k6mq8atddf0xbr48p2b9st4h.properties","incremental":true,"columns":["propertyKey","propertyName","accountKey","accountName"],"primary_key":["propertyKey"],"column_metadata":{"propertyKey":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}],"propertyName":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}],"accountKey":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}],"accountName":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}]}} \ No newline at end of file diff --git a/tests/functional/download-empty-data/expected/data/out/usage.json b/tests/functional/download-empty-data/expected/data/out/usage.json deleted file mode 100644 index 7121ffb..0000000 --- a/tests/functional/download-empty-data/expected/data/out/usage.json +++ /dev/null @@ -1 +0,0 @@ -[{"metric":"API Calls","value":1}] \ No newline at end of file diff --git a/tests/functional/download-empty-data/source/data/config.json b/tests/functional/download-empty-data/source/data/config.json deleted file mode 100644 index dd7b4e2..0000000 --- a/tests/functional/download-empty-data/source/data/config.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "authorization": { - "oauth_api": { - "credentials": { - "appKey": "%env(string:CLIENT_ID)%", - "#appSecret": "%env(string:CLIENT_SECRET)%", - "#data": "%env(string:CREDENTIALS_DATA)%" - } - } - }, - "parameters": { - "outputTable": "empty-data", - "query": { - "metrics": [ - { - "name": "active1DayUsers" - }, - { - "name": "active28DayUsers" - } - ], - "dimensions": [ - { - "name": "campaignId" - }, - { - "name": "campaignName" - }, - { - "name": "city" - }, - { - "name": "cityId" - } - ], - "dateRanges": [ - { - "startDate": "2015-08-14", - "endDate": "2015-08-15" - } - ] - }, - "endpoint": "data-api", - "outputBucket": "in.c-keboola-ex-google-analytics-v4-01k6mq8atddf0xbr48p2b9st4h", - "properties": [ - { - "accountKey": "accounts/128209249", - "accountName": "Keboola Website", - "propertyKey": "properties/403517979", - "propertyName": "Website - GA4" - } - ] - } -}