diff --git a/composer.json b/composer.json index c29449e..e2774e9 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ ], "require": { "php": "~8.3.1|~8.4.1", + "always-open/laravel-request-logger": "^3.0", "illuminate/contracts": "~10.0||~11.0||~12.0", "spatie/laravel-data": "~4.16", "spatie/laravel-package-tools": "^1.16" @@ -74,4 +75,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/config/oxylabs-api.php b/config/oxylabs-api.php index b9350d8..0c8654d 100644 --- a/config/oxylabs-api.php +++ b/config/oxylabs-api.php @@ -5,4 +5,5 @@ 'username' => env('OXYLAB_API_USERNAME'), 'password' => env('OXYLAB_API_PASSWORD'), 'auth_method' => env('OXYLAB_API_AUTH_METHOD', 'basic'), + 'request_logging_enabled' => env('OXYLABS_API_REQUEST_LOGGING_ENABLED', true), ]; diff --git a/config/request-logger.php b/config/request-logger.php new file mode 100644 index 0000000..aca6145 --- /dev/null +++ b/config/request-logger.php @@ -0,0 +1,33 @@ + '_request_logs', + + /* + * This is the default suffix applied to models' class names. + * + * Example: The facebook object name would have a request log model of FacebookRequestLog. + */ + 'model_suffix' => 'RequestLog', + + 'model_path' => app_path('Models'), + + 'model_stub' => __DIR__.'/../stubs/model.stub', + + 'migration_path' => database_path('migrations'), + + 'migration_stub' => __DIR__.'/../stubs/migration.stub', + + /* + * Enable the process stamps (sub) package to log which process/url/job invoked the call + */ + 'enable_process_stamps' => true, + + /* + * Precision value used in generating occurred_at for request log + */ + 'log_timestamp_precision' => 3, +]; diff --git a/database/migrations/2000_01_01_000001_create_oxylabs_api_reqeust_logger_table.php b/database/migrations/2000_01_01_000001_create_oxylabs_api_reqeust_logger_table.php new file mode 100644 index 0000000..d4f53e6 --- /dev/null +++ b/database/migrations/2000_01_01_000001_create_oxylabs_api_reqeust_logger_table.php @@ -0,0 +1,53 @@ +id(); + // Will store the relative path of the request (e.g. /addresses/validate) + $table->string('path', 191) + ->index(); + // What parameters were passed in (e.g. ?status=new) + $table->string('params', 512) + ->nullable() + ->fulltext(); + // HTTP method (e.g. POST/PUT/DELETE) + $table->string('http_method', 10) + ->index(); + // Status code (e.g. 200, 204, 429) + $table->smallInteger('response_code', autoIncrement: false, unsigned: true) + ->nullable() + ->index(); + // The entire JSON encoded payload of the request + $table->json('body') + ->nullable(); + // The headers that were part of the request + $table->json('request_headers') + ->nullable(); + // The entire JSON encoded responses + $table->json('response') + ->nullable(); + // The headers that were part of the response + $table->json('response_headers') + ->nullable(); + // Internal exceptions that occurred during the request + $table->string('exception') + ->nullable(); + // When the request was resolved to the millisecond + $table->timestamp('occurred_at', 3)->index(); + $table->timestamps(precision: 3); + $table->processIds(); + }); + } + + public function down(): void + { + Schema::dropIfExists('oxylabs_api_request_logger'); + } +}; diff --git a/database/migrations/create_oxylabs_api_table.php.stub b/database/migrations/create_oxylabs_api_table.php.stub deleted file mode 100644 index ce83abf..0000000 --- a/database/migrations/create_oxylabs_api_table.php.stub +++ /dev/null @@ -1,19 +0,0 @@ -id(); - - // add fields - - $table->timestamps(); - }); - } -}; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 229319e..0e69d55 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,5 +10,4 @@ parameters: checkOctaneCompatibility: true checkModelProperties: true ignoreErrors: - - '#Trait AlwaysOpen\\OxylabsApi\\Traits\\APIStatus is used zero times and is not analysed.#' - - '#Call to function is_string\(\) with string will always evaluate to true.#' \ No newline at end of file + - '#Trait AlwaysOpen\\OxylabsApi\\Traits\\APIStatus is used zero times and is not analysed.#' \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 6c3dd5d..e218cdb 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,23 +1,30 @@ + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" + bootstrap="vendor/autoload.php" + cacheDirectory=".phpunit.cache" + executionOrder="depends,defects" + shortenArraysForExportThreshold="10" + requireCoverageMetadata="false" + beStrictAboutCoverageMetadata="true" + beStrictAboutOutputDuringTests="true" + displayDetailsOnPhpunitDeprecations="true" + failOnPhpunitDeprecation="true" + failOnRisky="true" + failOnWarning="true"> - tests + tests - - - - - - - + + - src + src + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ee68745..8096beb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,31 +1,33 @@ - + - + tests - + + - ./src + src + + + + \ No newline at end of file diff --git a/src/DTOs/Amazon/AmazonSellerResponse.php b/src/DTOs/Amazon/AmazonSellerResponse.php index 591fe4b..864adc3 100644 --- a/src/DTOs/Amazon/AmazonSellerResponse.php +++ b/src/DTOs/Amazon/AmazonSellerResponse.php @@ -14,7 +14,7 @@ class AmazonSellerResponse extends Data public function __construct( /* @var AmazonSellerResult[] $results */ #[DataCollectionOf(AmazonSellerResult::class)] - public readonly array $results, - public readonly PushPullJob $job, + public readonly ?array $results, + public readonly ?PushPullJob $job, ) {} } diff --git a/src/DTOs/Google/GoogleShoppingProductResponse.php b/src/DTOs/Google/GoogleShoppingProductResponse.php index 006bd01..33c3b0b 100644 --- a/src/DTOs/Google/GoogleShoppingProductResponse.php +++ b/src/DTOs/Google/GoogleShoppingProductResponse.php @@ -15,7 +15,7 @@ class GoogleShoppingProductResponse extends Data public function __construct( /* @var GoogleShoppingProductResult[] $results */ #[DataCollectionOf(GoogleShoppingProductResult::class)] - public readonly DataCollection $results, - public readonly PushPullJob $job, + public readonly ?DataCollection $results, + public readonly ?PushPullJob $job, ) {} } diff --git a/src/DTOs/UniversalResponse.php b/src/DTOs/UniversalResponse.php index 82b07a4..27d8798 100644 --- a/src/DTOs/UniversalResponse.php +++ b/src/DTOs/UniversalResponse.php @@ -13,7 +13,7 @@ class UniversalResponse extends Data public function __construct( /* @var UniversalResult[] $results */ #[DataCollectionOf(UniversalResult::class)] - public readonly array $results, - public readonly PushPullJob $job, + public readonly ?array $results, + public readonly ?PushPullJob $job, ) {} } diff --git a/src/Models/OxylabsApiRequestLogger.php b/src/Models/OxylabsApiRequestLogger.php new file mode 100644 index 0000000..f8dcfdf --- /dev/null +++ b/src/Models/OxylabsApiRequestLogger.php @@ -0,0 +1,24 @@ + [ 'Authorization' => 'Bearer '.$this->password, ], - default => throw new \InvalidArgumentException('Invalid authentication method') + default => throw new InvalidArgumentException('Invalid authentication method') }; } @@ -77,97 +85,118 @@ protected function getBaseRequest(): Factory|PendingRequest } /** - * @throws ConnectionException + * @throws RuntimeException */ - public function makeRequest(string $source, array $payload, ?int $allowedRetries = null, int $retries = 0): PushPullJob + public function makePostRequest(string $source, array $payload, ?int $allowedRetries = null): PushPullJob { - $response = $this->getBaseRequest() - ->post($this->baseUrl.'/queries', [ - 'source' => $source, - ...$payload, - ]); + try { + $response = $this->makeRequest( + 'post', + $this->baseUrl.'/queries', + [ + 'source' => $source, + ...$payload, + ], + $allowedRetries ?? 0, + ); + + if (! $response->successful()) { + throw new RuntimeException('API request failed: '.$response->getBody()->getContents(), $response->getStatusCode()); + } - if (! $response->successful()) { - if (str_contains($response->body(), 'Too many requests') && $retries < ($allowedRetries ?? $this->defaultAllowedDispatchRetries ?? 0)) { - sleep(1); + return PushPullJob::from(json_decode($response->getBody()->getContents(), true)); + } catch (Throwable $e) { + throw new RuntimeException('API request failed: '.$e->getMessage(), $e->getCode(), $e); + } + } + + /** + * @throws GuzzleException|Throwable + */ + protected function makeRequest(string $method, string $uri, ?array $payload = null, ?int $retryCount = null): Response + { + $request = new Request( + method: $method, + uri: $uri, + headers: $this->getAuthHeader(), + body: $payload ? json_encode($payload) : null, + ); + + if (config('oxylabs-api.request_logging_enabled', true)) { + $logger = OxylabsApiRequestLogger::makeFromGuzzle($request); + $logger->save(); + } - return $this->makeRequest($source, $payload, $allowedRetries, ++$retries); + /** + * @var Response $response + */ + $response = retry($retryCount ?? 0, function () use ($request, $method, $payload): PromiseInterface|Response { + if (strtolower($method) === 'post') { + return Http::withHeaders($request->getHeaders()) + ->post($request->getUri(), $payload) + ->throw(); + } else { + return Http::withHeaders($request->getHeaders()) + ->get($request->getUri()) + ->throw(); } - throw new RuntimeException('API request failed: '.$response->body()); + }, 2000); + + if (config('oxylabs-api.request_logging_enabled', true)) { + $logger->updateFromResponse($response->toPsrResponse()); } - return PushPullJob::from($response->json()); + return $response; } /** - * @throws ConnectionException * @throws RuntimeException */ public function getResult( string $job_id, ?string $type = null, - int $retryCount = 0, ): ?array { try { - $response = $this->getBaseRequest() - ->get($this->baseUrl."/queries/$job_id/results".($type ? "?type=$type" : '')); - } catch (\Exception $e) { - if ( - $retryCount < 3 - && ( - str_contains($e->getMessage(), 'Too many request') - || str_contains($e->getMessage(), 'SSL') - ) - ) { - sleep(1); - - return $this->getresult($job_id, $type, ++$retryCount); - } - - throw $e; + $response = $this->makeRequest( + 'get', + $this->baseUrl."/queries/$job_id/results".($type ? "?type=$type" : ''), + null, + 3 + ); + } catch (Throwable $e) { + throw new RuntimeException('API request failed: '.$e->getMessage(), $e->getCode(), $e); } if (! $response->successful()) { - throw new RuntimeException('API request failed: '.$response->body(), $response->getStatusCode()); + throw new RuntimeException('API request failed: '.$response->getBody()->getContents(), $response->getStatusCode()); } - return $response->json(); + return json_decode($response->getBody()->getContents(), true); } /** * @throws ConnectionException + * @throws Throwable */ - public function getPushPullJob(string $job_id, int $retryCount = 0): PushPullJob + public function getPushPullJob(string $job_id): PushPullJob { - try { - $response = $this->getBaseRequest() - ->get($this->baseUrl."/queries/$job_id"); - } catch (\Exception $e) { - if ( - $retryCount < 3 - && ( - str_contains($e->getMessage(), 'Too many request') - || str_contains($e->getMessage(), 'SSL') - ) - ) { - sleep(1); - - return $this->getPushPullJob($job_id, ++$retryCount); - } - - throw $e; - } + $response = $this->makeRequest( + 'get', + $this->baseUrl."/queries/$job_id", + retryCount: 3, + ); if (! $response->successful()) { - throw new RuntimeException('API request failed: '.$response->body()); + throw new RuntimeException('API request failed: '.$response->getBody()->getContents(), $response->getStatusCode()); } - return PushPullJob::from($response->json()); + return PushPullJob::from(json_decode($response->getBody()->getContents(), true)); } /** * @throws ConnectionException * @throws RuntimeException + * @throws Throwable */ public function getPushPullResults( string $job_id, @@ -177,23 +206,25 @@ public function getPushPullResults( ?string $type = null, ): ?array { if ($check_status) { - $count = 0; - do { - $count++; - $job = $this->getPushPullJob($job_id); - if ($job->isPending()) { - sleep($status_wait_seconds); - } - // @TODO handle faulted status - } while ($job->isPending() && $count < $status_check_limit); + retry( + $status_check_limit, + function () use ($job_id): PushPullJob { + $job = $this->getPushPullJob($job_id); + if (! $job->isDone()) { + throw new Exception("Job $job_id not completed"); + } + + return $job; + }, + $status_wait_seconds, + ); } return $this->getResult($job_id, $type); } /** - * @throws ConnectionException - * @throws RuntimeException + * @throws ConnectionException|Throwable */ public function getAmazonProductResult( string $job_id, @@ -208,8 +239,7 @@ public function getAmazonProductResult( } /** - * @throws ConnectionException - * @throws RuntimeException + * @throws ConnectionException|RuntimeException|Throwable */ public function getAmazonResult( string $job_id, @@ -224,8 +254,7 @@ public function getAmazonResult( } /** - * @throws ConnectionException - * @throws RuntimeException + * @throws ConnectionException|Throwable */ public function getUniversalResult( string $job_id, @@ -240,8 +269,7 @@ public function getUniversalResult( } /** - * @throws ConnectionException - * @throws RuntimeException + * @throws ConnectionException|Throwable */ public function getAmazonPricingResult( string $job_id, @@ -257,7 +285,7 @@ public function getAmazonPricingResult( /** * @throws ConnectionException - * @throws RuntimeException + * @throws Throwable */ public function getAmazonSellerResult( string $job_id, @@ -288,80 +316,72 @@ public function makeBatchRequest(BatchRequest $payload): PushPullBatchJobRespons } /** - * @throws ConnectionException * @throws RuntimeException */ public function amazon(AmazonRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::TARGET_AMAZON, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::TARGET_AMAZON, $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException * @throws RuntimeException */ public function amazonProduct(AmazonProductRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_AMAZON_PRODUCT, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_AMAZON_PRODUCT, $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException * @throws RuntimeException */ public function amazonSearch(AmazonSearchRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_AMAZON_SEARCH, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_AMAZON_SEARCH, $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException * @throws RuntimeException */ public function amazonPricing(AmazonPricingRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_AMAZON_PRICING, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_AMAZON_PRICING, $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException * @throws RuntimeException */ public function amazonSellers(AmazonSellersRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_AMAZON_SELLERS, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_AMAZON_SELLERS, $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException * @throws RuntimeException */ public function googleSearch(GoogleSearchRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_GOOGLE_SHOPPING_SEARCH, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_GOOGLE_SHOPPING_SEARCH, $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException * @throws RuntimeException */ public function universal(UniversalRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest('universal', $request->toArray(), $allowedRetries); + return $this->makePostRequest('universal', $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException * @throws RuntimeException */ public function googleShoppingProduct(GoogleShoppingProductRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_GOOGLE_SHOPPING_PRODUCT, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_GOOGLE_SHOPPING_PRODUCT, $request->toArray(), $allowedRetries); } /** * @throws ConnectionException - * @throws RuntimeException + * @throws Throwable */ public function getGoogleShoppingProductResult( string $job_id, @@ -376,17 +396,15 @@ public function getGoogleShoppingProductResult( } /** - * @throws ConnectionException * @throws RuntimeException */ public function googleShoppingPricing(GoogleShoppingPricingRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_GOOGLE_SHOPPING_PRICING, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_GOOGLE_SHOPPING_PRICING, $request->toArray(), $allowedRetries); } /** - * @throws ConnectionException - * @throws RuntimeException + * @throws ConnectionException|Throwable */ public function getGoogleShoppingPricingResult( string $job_id, @@ -401,14 +419,17 @@ public function getGoogleShoppingPricingResult( } /** - * @throws ConnectionException * @throws RuntimeException */ public function walmartProduct(WalmartProductRequest $request, ?int $allowedRetries = null): PushPullJob { - return $this->makeRequest(OxylabsApi::SOURCE_WALMART_PRODUCT, $request->toArray(), $allowedRetries); + return $this->makePostRequest(OxylabsApi::SOURCE_WALMART_PRODUCT, $request->toArray(), $allowedRetries); } + /** + * @throws Throwable + * @throws ConnectionException + */ public function getWalmartProductResult( string $job_id, bool $check_status = false, diff --git a/src/OxylabsApiServiceProvider.php b/src/OxylabsApiServiceProvider.php index e738b7f..cea52a9 100644 --- a/src/OxylabsApiServiceProvider.php +++ b/src/OxylabsApiServiceProvider.php @@ -24,5 +24,11 @@ public function boot() __DIR__.'/../config/oxylabs-api.php' => config_path('oxylabs-api.php'), __DIR__.'/../config/data.php' => config_path('data.php'), ], 'config'); + + if (config('oxylabs-api.request_logging_enabled', true)) { + $this->publishesMigrations([ + __DIR__.'/../migrations/*' => database_path('migrations/'), + ]); + } } } diff --git a/tests/BaseTest.php b/tests/BaseTest.php index ce4ad77..085b09e 100644 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -22,15 +22,15 @@ protected function setUp(): void $this->refreshApplication(); } - protected function getFixtureJsonContent(string $name): array + protected function getFixtureJsonContent(string $name): string { $content = $this->getFixtureContent($name); if ($content) { - return json_decode($content, true); + return $content; } - return []; + return '{}'; } protected function getFixtureContent(string $name): false|string diff --git a/tests/Feature/OxylabsApiClientTest.php b/tests/Feature/OxylabsApiClientTest.php index 7780cfe..7a7421a 100644 --- a/tests/Feature/OxylabsApiClientTest.php +++ b/tests/Feature/OxylabsApiClientTest.php @@ -15,10 +15,17 @@ use AlwaysOpen\OxylabsApi\OxylabsApiClient; use AlwaysOpen\OxylabsApi\Tests\BaseTest; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Http; class OxylabsApiClientTest extends BaseTest { + protected function setUp(): void + { + parent::setUp(); + Config::set('oxylabs-api.request_logging_enabled', false); + } + public function test_amazon_product() { Http::fake([ diff --git a/tests/Feature/OxylabsApiFacadeTest.php b/tests/Feature/OxylabsApiFacadeTest.php index 2ef5820..fdfe1e9 100644 --- a/tests/Feature/OxylabsApiFacadeTest.php +++ b/tests/Feature/OxylabsApiFacadeTest.php @@ -5,6 +5,7 @@ use AlwaysOpen\OxylabsApi\DTOs\Amazon\AmazonProductRequest; use AlwaysOpen\OxylabsApi\OxylabsApiFacade; use AlwaysOpen\OxylabsApi\Tests\BaseTest; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Http; class OxylabsApiFacadeTest extends BaseTest @@ -14,13 +15,14 @@ protected function setUp(): void parent::setUp(); Http::preventStrayRequests(); + Config::set('oxylabs-api.request_logging_enabled', false); } public function test_amazon_product() { Http::fake([ 'data.oxylabs.io/v1/queries' => Http::response($this->getFixtureJsonContent('push_pull_job.json'), 200), - 'data.oxylabs.io/v1/queries/7342973874281147393/results/?type=parsed' => Http::response($this->getFixtureJsonContent('amazon_product_result.json'), 200), + 'data.oxylabs.io/v1/queries/7342973874281147393/results?type=parsed' => Http::response($this->getFixtureJsonContent('amazon_product_result.json'), 200), ]); $request = new AmazonProductRequest( diff --git a/tests/Fixtures/amazon_product_listing2.json b/tests/Fixtures/amazon_product_listing2.json new file mode 100644 index 0000000..8f6db68 --- /dev/null +++ b/tests/Fixtures/amazon_product_listing2.json @@ -0,0 +1,573 @@ +{ + "results": [ + { + "content": { + "ads": [ + { + "pos": 1, + "asin": "B0CQRPQK5Q", + "type": "organic_also_viewed", + "price": 279.99, + "title": "Ironton Jobsite Box \u2014 36in. x 16in.", + "images": [ + "https:\/\/m.media-amazon.com\/images\/I\/818am14BKqL._AC_SS57_.jpg" + ], + "rating": 4.2, + "location": "carousel", + "price_upper": 279.99, + "reviews_count": 42, + "is_prime_eligible": true + }, + { + "pos": 2, + "asin": "B0D4QXHYRJ", + "type": "organic_also_viewed", + "price": 419.99, + "title": "Northern Tool Jobsite Storage Box, 5 Cu. Ft., 32in.W, Model# 2032-NTE", + "images": [ + "https:\/\/m.media-amazon.com\/images\/I\/61ivHly3XqL._AC_SS57_.jpg" + ], + "rating": 5, + "location": "carousel", + "price_upper": 419.99, + "reviews_count": 50, + "is_prime_eligible": true + }, + { + "pos": 3, + "asin": "B084XV6W7P", + "type": "organic_also_viewed", + "price": 579.99, + "title": "Crescent JOBOX 48\" Tradesman Steel Chest - CJB637990", + "images": [ + "https:\/\/images-na.ssl-images-amazon.com\/images\/I\/61brwGWxQPL._AC_UL165_SR165,165_.jpg" + ], + "rating": 0, + "location": "carousel", + "price_upper": 579.99, + "reviews_count": 45, + "is_prime_eligible": false + }, + { + "pos": 4, + "asin": "B0D4QXHYRJ", + "type": "organic_also_viewed", + "price": 419.99, + "title": "Northern Tool Jobsite Storage Box, 5 Cu. Ft., 32in.W, Model# 2032-NTE", + "images": [ + "https:\/\/images-na.ssl-images-amazon.com\/images\/I\/61ivHly3XqL._AC_UL165_SR165,165_.jpg" + ], + "rating": 0, + "location": "carousel", + "price_upper": 419.99, + "reviews_count": 50, + "is_prime_eligible": false + }, + { + "pos": 5, + "asin": "B0CQRPQK5Q", + "type": "organic_also_viewed", + "price": 279.99, + "title": "Ironton Jobsite Box \u2014 36in. x 16in.", + "images": [ + "https:\/\/images-na.ssl-images-amazon.com\/images\/I\/818am14BKqL._AC_UL165_SR165,165_.jpg" + ], + "rating": 0, + "location": "carousel", + "price_upper": 279.99, + "reviews_count": 42, + "is_prime_eligible": false + }, + { + "pos": 6, + "asin": "B0BXL5KRRV", + "type": "organic_also_viewed", + "price": 41.98, + "title": "WELKINLAND Heavy-Duty Canvas Tool Pouch - 4PCS, 12\" Waxed Canvas Zipper Tool Pouch", + "images": [ + "data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' height='100%' width='100%' preserveAspectRatio='none' \/%3E", + "https:\/\/m.media-amazon.com\/images\/I\/416cNDgvK4L._AC_SR100,100_QL65_.jpg" + ], + "rating": 4.6, + "location": "carousel", + "price_upper": 58.68, + "reviews_count": 46, + "is_prime_eligible": false + }, + { + "pos": 7, + "asin": "B0DSBP7QFB", + "type": "organic_also_viewed", + "price": 726.89, + "title": "Black Aluminum Contractor Top Side Tool Boxes with Two Lower Drawers, 60''X13.3''X21'' (Black)", + "images": [ + "data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' height='100%' width='100%' preserveAspectRatio='none' \/%3E", + "https:\/\/m.media-amazon.com\/images\/I\/41nchtL5Y7L._AC_SR100,100_QL65_.jpg" + ], + "rating": 0, + "location": "carousel", + "price_upper": 726.89, + "reviews_count": 0, + "is_prime_eligible": false + }, + { + "pos": 8, + "asin": "B0FL1TXVSB", + "type": "organic_also_viewed", + "price": 219.99, + "title": "OAKANDO 7-Drawer Metal Rolling Tool Chest with Wheels,Tool Storage Cabinet with Locking System\uff0cToolbox with Wheels for Garage,Workshop, Repair Shop(Black, 7-Drawer)", + "images": [ + "data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' height='100%' width='100%' preserveAspectRatio='none' \/%3E", + "https:\/\/m.media-amazon.com\/images\/I\/41Qx0w4botL._AC_SR100,100_QL65_.jpg" + ], + "rating": 4.6, + "location": "carousel", + "price_upper": 219.99, + "reviews_count": 46, + "is_prime_eligible": false + }, + { + "pos": 9, + "asin": "B09KGF9GD9", + "type": "organic_also_viewed", + "price": 24.68, + "title": "WORKPRO 16-inch Wide Mouth Tool Bag, Heavy Duty Cloth Tool Storage Bag with Water Proof Molded Base, Adjustable Shoulder Strap", + "images": [ + "data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' height='100%' width='100%' preserveAspectRatio='none' \/%3E", + "https:\/\/m.media-amazon.com\/images\/I\/51fbVH+4YbL._AC_SR100,100_QL65_.jpg" + ], + "rating": 4.7, + "location": "carousel", + "price_upper": 25.98, + "reviews_count": 47, + "is_prime_eligible": false + }, + { + "pos": 10, + "asin": "B0DZWZNZ83", + "type": "organic_also_viewed", + "price": 738.99, + "title": "BeoneYuu Black Heavy-duty Aluminum Contractor Top Side Tool Boxes with Two Lower Drawers, 60''X13.3''X21''", + "images": [ + "data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' height='100%' width='100%' preserveAspectRatio='none' \/%3E", + "https:\/\/m.media-amazon.com\/images\/I\/31dGP4Bv46L._AC_SR100,100_QL65_.jpg" + ], + "rating": 0, + "location": "carousel", + "price_upper": 738.99, + "reviews_count": 0, + "is_prime_eligible": false + } + ], + "url": "https:\/\/www.amazon.com\/dp\/B084Y7CHX5?th=1&psc=1&language=en_US", + "asin": "B084Y7CHX5", + "page": 1, + "brand": "JOBOX", + "price": 0, + "stock": "", + "title": "Crescent JOBOX 36\" Site-Vault Heavy-Duty Chest - 2-652990", + "coupon": "", + "images": [ + "https:\/\/m.media-amazon.com\/images\/I\/61aBCsKc6CL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/71ZKcGpihHL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/71SpvaqqVtL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/710Wtj95vFL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/61TdSGzI6zL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/716IyNJGt2L._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/616Mn8OeRNL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/6181A4vJv4L._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/612mgUyrRyL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/61e59bzI3VL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/61Rd3nKho4L._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/61b9RSokuoL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/71J+pe2Z8AL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/71jlEWYRO1L._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/71LjLd6fMXL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/71LjLd6fMXL._AC_SL1500_.jpg", + "https:\/\/m.media-amazon.com\/images\/I\/6150cjjXv0L._AC_SL1500_.jpg" + ], + "rating": 4.3, + "reviews": [], + "category": [ + { + "ladder": [ + { + "url": "\/Tools-and-Home-Improvement\/b\/ref=dp_bc_1?ie=UTF8&node=228013", + "name": "Tools & Home Improvement" + }, + { + "url": "\/power-tools-hand-tools\/b\/ref=dp_bc_2?ie=UTF8&node=328182011", + "name": "Power & Hand Tools" + }, + { + "url": "\/tool-organizer\/b\/ref=dp_bc_3?ie=UTF8&node=13400691", + "name": "Tool Organizers" + }, + { + "url": "\/Tool-Boxes\/b\/ref=dp_bc_4?ie=UTF8&node=13400721", + "name": "Tool Boxes" + } + ] + } + ], + "currency": "USD", + "delivery": [], + "page_type": "Product", + "price_sns": 0, + "store_url": "\/stores\/Crescent+JOBOX\/page\/AE867E11-B7A1-440D-BE13-E57B19A5E27D?is_byline_deeplink=true&deeplink=72F53DBD-57A5-483D-8216-538240B5F05C&redirect_store_id=AE867E11-B7A1-440D-BE13-E57B19A5E27D&lp_asin=B084Y7CHX5&ref_=ast_bln&store_ref=bl_ast_dp_brandLogo_sto", + "variation": [ + { + "asin": "B084Y7X5RG", + "selected": false, + "dimensions": { + "Size": "48\"", + "Style": "Heavy-Duty High Capacity" + } + }, + { + "asin": "B084Y3VWTC", + "selected": false, + "dimensions": { + "Size": "42\"", + "Style": "Heavy-Duty" + } + }, + { + "asin": "B084Y7P6D7", + "selected": false, + "dimensions": { + "Size": "48\"", + "Style": "Heavy-Duty" + } + }, + { + "asin": "B084Y7CHX5", + "selected": true, + "dimensions": { + "Size": "36\"", + "Style": "Heavy-Duty" + } + }, + { + "asin": "B084Y6M494", + "selected": false, + "dimensions": { + "Size": "60\"", + "Style": "Heavy-Duty" + } + }, + { + "asin": "B084Y3N14N", + "selected": false, + "dimensions": { + "Size": "30\"", + "Style": "Heavy-Duty" + } + } + ], + "has_videos": true, + "sales_rank": [ + { + "rank": 517300, + "ladder": [ + { + "url": "\/gp\/bestsellers\/hi\/ref=pd_zg_ts_hi", + "name": "Tools & Home Improvement " + } + ] + }, + { + "rank": 1515, + "ladder": [ + { + "url": "\/gp\/bestsellers\/hi\/13400721\/ref=pd_zg_hrsr_hi", + "name": "Tool Boxes" + } + ] + } + ], + "asin_in_url": "B084Y7CHX5", + "description": "Crescent JOBOX is the leader in on-site storage security, organization, and safety with its high-strength, chamfered lid, commercial gas springs* for lid control and its Site-Vault\u2122 security system. 4-way EZ-Loader skid bolsters and slip-resistant textured powder paint help provide safety and handling ease (* On 48 and longer chests)\"", + "parent_asin": "B08TNZFC6Q", + "price_upper": 0, + "pricing_str": "", + "pricing_url": "https:\/\/www.amazon.com\/gp\/offer-listing\/B084Y7CHX5?startIndex=0", + "manufacturer": "Crescent JOBOX", + "price_buybox": -1, + "product_name": "Crescent JOBOX 36\" Site-Vault Heavy-Duty Chest - 2-652990", + "bullet_points": "Site-Vault security system is a patented system with 3 locking points and a reflective orange lock indicator that shows when the box is locked or unlocked\nHeavy-duty beveled edge lid eliminates sharp front lid corners and enhances lid strength\nDual built-in lid storage with integrated cargo nets is perfect for rain gear, PPE or small tools\nEZ-Lock support arm secures and locks lid in open position for safe use\nUpgraded lock system now allows usage of padlock models Master #1, #5, #175 and American #50\nIntegrated sidewall cargo bin provides convenient storage for small tools and parts\nErgonomic, lifting handles are protected inside deep recesses to prevent pinched knuckles and damage\nHeavy-duty EZ-Loader 4-way skid bolsters allow fork lift loading from any side for quicker and easier transport\nSkid bolsters are compatible with casters for easy mobility (1-320993 not included)\nHeavy-duty hinge is staked and welded to prevent the pin from being forced out for break in", + "price_initial": 0, + "pricing_count": 1, + "reviews_count": 16, + "sns_discounts": [], + "developer_info": [], + "price_shipping": 0, + "product_details": { + "upc": "\u200e043419957480", + "asin": "B084Y7CHX5", + "size": "\u200e36\"", + "brand": "\u200eJOBOX", + "color": "\u200eOrange", + "shape": "\u200eRectangular", + "style": "\u200eHeavy-Duty", + "usage": "\u200eProfessional", + "pattern": "\u200eSolid", + "material": "\u200eAlloy Steel", + "item_weight": "\u200e98 pounds", + "part_number": "\u200e2-652990", + "manufacturer": "\u200eApex Tool Group", + "customer_reviews": "4.3 4.3 out of 5 stars 16 ratings 4.3 out of 5 stars", + "number_of_pieces": "\u200e1", + "best_sellers_rank": "#517,300 in Tools & Home Improvement (See Top 100 in Tools & Home Improvement) #1,515 in Tool Boxes", + "item_model_number": "\u200e2-652990", + "batteries_required": "\u200eNo", + "product_dimensions": "\u200e35.91\"L x 19.97\"W x 27.44\"H", + "included_components": "\u200e(1) 36\" Site-Vault\u2122 Heavy-Duty Chest", + "date_first_available": "February 18, 2020", + "warranty_description": "\u200e1 year limited warranty", + "item_package_quantity": "\u200e1", + "number_of_compartments": "\u200e1", + "water_resistance_level": "\u200eWater Resistant" + }, + "featured_merchant": [], + "is_prime_eligible": true, + "parse_status_code": 12000, + "technical_details": [ + { + "name": "Brand", + "value": "JOBOX" + }, + { + "name": "Material", + "value": "Alloy Steel" + }, + { + "name": "Color", + "value": "Orange" + }, + { + "name": "Product Dimensions", + "value": "35.91\"L x 19.97\"W x 27.44\"H" + }, + { + "name": "Water Resistance Level", + "value": "Water Resistant" + }, + { + "name": "Number of Compartments", + "value": "1" + }, + { + "name": "UPC", + "value": "043419957480" + }, + { + "name": "Manufacturer", + "value": "Apex Tool Group" + }, + { + "name": "Part Number", + "value": "2-652990" + }, + { + "name": "Item Weight", + "value": "98 pounds" + }, + { + "name": "Item model number", + "value": "2-652990" + }, + { + "name": "Size", + "value": "36\"" + }, + { + "name": "Style", + "value": "Heavy-Duty" + }, + { + "name": "Pattern", + "value": "Solid" + }, + { + "name": "Shape", + "value": "Rectangular" + }, + { + "name": "Item Package Quantity", + "value": "1" + }, + { + "name": "Number Of Pieces", + "value": "1" + }, + { + "name": "Usage", + "value": "Professional" + }, + { + "name": "Included Components", + "value": "(1) 36\" Site-Vault\u2122 Heavy-Duty Chest" + }, + { + "name": "Batteries Required?", + "value": "No" + }, + { + "name": "Warranty Description", + "value": "1 year limited warranty" + } + ], + "product_dimensions": "\u200e35.91\"L x 19.97\"W x 27.44\"H", + "warranty_and_support": { + "links": [ + { + "url": "\/gp\/help\/customer\/display.html\/ref=orc_hp_s_retpol?ie=UTF8&nodeId=201819200", + "title": "here" + } + ], + "description": "Amazon.com Return Policy: Amazon.com Voluntary 30-Day Return Guarantee: You can return many items you have purchased within 30 days following delivery of the item to you. Our Voluntary 30-Day Return Guarantee does not affect your legal right of withdrawal in any way. You can find out more about the exceptions and conditions here." + }, + "answered_questions_count": 0, + "rating_stars_distribution": [ + { + "rating": 5, + "percentage": 75 + }, + { + "rating": 4, + "percentage": 8 + }, + { + "rating": 3, + "percentage": 0 + }, + { + "rating": 2, + "percentage": 8 + }, + { + "rating": 1, + "percentage": 9 + } + ] + }, + "created_at": "2025-08-15 15:44:04", + "updated_at": "2025-08-15 15:44:07", + "page": 1, + "url": "https:\/\/www.amazon.com\/dp\/B084Y7CHX5?th=1&psc=1&language=en_US", + "job_id": "7362147074835487745", + "is_render_forced": false, + "status_code": 200, + "type": "parsed", + "parser_type": "", + "parser_preset": null + } + ], + "job": { + "callback_url": null, + "client_id": 83306, + "context": [ + { + "key": "force_headers", + "value": false + }, + { + "key": "force_cookies", + "value": false + }, + { + "key": "hc_policy", + "value": true + }, + { + "key": "autoselect_variant", + "value": true + }, + { + "key": "check_empty_geo", + "value": null + }, + { + "key": "safe_search", + "value": true + }, + { + "key": "currency", + "value": "USD" + } + ], + "created_at": "2025-08-15 15:44:04", + "domain": "com", + "geo_location": null, + "id": "7362147074835487745", + "limit": 10, + "locale": null, + "pages": 1, + "parse": true, + "parser_type": null, + "parser_preset": null, + "parsing_instructions": null, + "browser_instructions": null, + "render": null, + "xhr": false, + "markdown": false, + "url": null, + "query": "B084Y7CHX5", + "source": "amazon_product", + "start_page": 1, + "status": "done", + "storage_type": null, + "storage_url": null, + "subdomain": "www", + "content_encoding": "utf-8", + "updated_at": "2025-08-15 15:44:07", + "user_agent_type": "desktop", + "session_info": null, + "statuses": [], + "client_notes": null, + "_links": [ + { + "rel": "self", + "href": "http:\/\/data.oxylabs.io\/v1\/queries\/7362147074835487745", + "method": "GET" + }, + { + "rel": "results", + "href": "http:\/\/data.oxylabs.io\/v1\/queries\/7362147074835487745\/results", + "method": "GET" + }, + { + "rel": "results-content", + "href_list": [ + "http:\/\/data.oxylabs.io\/v1\/queries\/7362147074835487745\/results\/1\/content" + ], + "method": "GET" + }, + { + "rel": "results-html", + "href": "http:\/\/data.oxylabs.io\/v1\/queries\/7362147074835487745\/results?type=raw", + "method": "GET" + }, + { + "rel": "results-content-html", + "href_list": [ + "http:\/\/data.oxylabs.io\/v1\/queries\/7362147074835487745\/results\/1\/content?type=raw" + ], + "method": "GET" + }, + { + "rel": "results-parsed", + "href": "http:\/\/data.oxylabs.io\/v1\/queries\/7362147074835487745\/results?type=parsed", + "method": "GET" + }, + { + "rel": "results-content-parsed", + "href_list": [ + "http:\/\/data.oxylabs.io\/v1\/queries\/7362147074835487745\/results\/1\/content?type=parsed" + ], + "method": "GET" + } + ] + } +} \ No newline at end of file diff --git a/tests/Fixtures/amazon_product_listing_faulted2.json b/tests/Fixtures/amazon_product_listing_faulted2.json new file mode 100644 index 0000000..2ac165d --- /dev/null +++ b/tests/Fixtures/amazon_product_listing_faulted2.json @@ -0,0 +1,123 @@ +{ + "results": [ + { + "content": "", + "created_at": "2025-09-01 12:20:03", + "updated_at": "2025-09-01 12:20:04", + "page": 1, + "url": "https://www.amazon.com/gp/aod/ajax/ref=dp_aod_unknown_mbc?asin=B0DY98N6BR&pageno=1&language=en_US", + "job_id": "7368256325517601793", + "is_render_forced": false, + "status_code": 404, + "type": "parsed", + "parser_type": "", + "parser_preset": null + } + ], + "job": { + "callback_url": null, + "client_id": 83306, + "context": [ + { + "key": "force_headers", + "value": false + }, + { + "key": "force_cookies", + "value": false + }, + { + "key": "hc_policy", + "value": true + }, + { + "key": "condition", + "value": null + }, + { + "key": "check_empty_geo", + "value": null + }, + { + "key": "safe_search", + "value": true + }, + { + "key": "currency", + "value": "USD" + } + ], + "created_at": "2025-09-01 12:20:03", + "domain": "com", + "geo_location": null, + "id": "7368256325517601793", + "limit": 10, + "locale": null, + "pages": 1, + "parse": true, + "parser_type": null, + "parser_preset": null, + "parsing_instructions": null, + "browser_instructions": null, + "render": null, + "xhr": false, + "markdown": false, + "url": null, + "query": "B0DY98N6BR", + "source": "amazon_pricing", + "start_page": 1, + "status": "faulted", + "storage_type": null, + "storage_url": null, + "subdomain": "www", + "content_encoding": "utf-8", + "updated_at": "2025-09-01 12:20:04", + "user_agent_type": "desktop", + "session_info": null, + "statuses": [], + "client_notes": null, + "_links": [ + { + "rel": "self", + "href": "http://data.oxylabs.io/v1/queries/7368256325517601793", + "method": "GET" + }, + { + "rel": "results", + "href": "http://data.oxylabs.io/v1/queries/7368256325517601793/results", + "method": "GET" + }, + { + "rel": "results-content", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7368256325517601793/results/1/content" + ], + "method": "GET" + }, + { + "rel": "results-html", + "href": "http://data.oxylabs.io/v1/queries/7368256325517601793/results?type=raw", + "method": "GET" + }, + { + "rel": "results-content-html", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7368256325517601793/results/1/content?type=raw" + ], + "method": "GET" + }, + { + "rel": "results-parsed", + "href": "http://data.oxylabs.io/v1/queries/7368256325517601793/results?type=parsed", + "method": "GET" + }, + { + "rel": "results-content-parsed", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7368256325517601793/results/1/content?type=parsed" + ], + "method": "GET" + } + ] + } +} \ No newline at end of file diff --git a/tests/Fixtures/google_shopping_faulted_result.json b/tests/Fixtures/google_shopping_faulted_result.json new file mode 100644 index 0000000..f62ff8f --- /dev/null +++ b/tests/Fixtures/google_shopping_faulted_result.json @@ -0,0 +1,135 @@ +{ + "results": [ + { + "content": "", + "created_at": "2025-09-01 12:55:43", + "updated_at": "2025-09-01 12:56:01", + "page": 1, + "url": "https://www.google.com/shopping/product/9949842269271681420/offers?gl=us&hl=en", + "job_id": "7368265301332333570", + "is_render_forced": false, + "status_code": 200, + "type": "parsed", + "parser_type": "", + "parser_preset": null + } + ], + "job": { + "callback_url": null, + "client_id": 83306, + "context": [ + { + "key": "force_headers", + "value": false + }, + { + "key": "force_cookies", + "value": false + }, + { + "key": "hc_policy", + "value": true + } + ], + "created_at": "2025-09-01 12:55:43", + "domain": "com", + "geo_location": "United States", + "id": "7368265301332333570", + "limit": 10, + "locale": null, + "pages": 5, + "parse": true, + "parser_type": null, + "parser_preset": null, + "parsing_instructions": null, + "browser_instructions": null, + "render": "png", + "xhr": false, + "markdown": false, + "url": null, + "query": "9949842269271681420", + "source": "google_shopping_pricing", + "start_page": 1, + "status": "faulted", + "storage_type": null, + "storage_url": null, + "subdomain": "www", + "content_encoding": "utf-8", + "updated_at": "2025-09-01 12:56:01", + "user_agent_type": "desktop", + "session_info": null, + "statuses": [], + "client_notes": null, + "_links": [ + { + "rel": "self", + "href": "http://data.oxylabs.io/v1/queries/7368265301332333570", + "method": "GET" + }, + { + "rel": "results", + "href": "http://data.oxylabs.io/v1/queries/7368265301332333570/results", + "method": "GET" + }, + { + "rel": "results-content", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/1/content", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/2/content", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/3/content", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/4/content", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/5/content" + ], + "method": "GET" + }, + { + "rel": "results-html", + "href": "http://data.oxylabs.io/v1/queries/7368265301332333570/results?type=raw", + "method": "GET" + }, + { + "rel": "results-content-html", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/1/content?type=raw", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/2/content?type=raw", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/3/content?type=raw", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/4/content?type=raw", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/5/content?type=raw" + ], + "method": "GET" + }, + { + "rel": "results-parsed", + "href": "http://data.oxylabs.io/v1/queries/7368265301332333570/results?type=parsed", + "method": "GET" + }, + { + "rel": "results-content-parsed", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/1/content?type=parsed", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/2/content?type=parsed", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/3/content?type=parsed", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/4/content?type=parsed", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/5/content?type=parsed" + ], + "method": "GET" + }, + { + "rel": "results-png", + "href": "http://data.oxylabs.io/v1/queries/7368265301332333570/results?type=png", + "method": "GET" + }, + { + "rel": "results-content-png", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/1/content?type=png", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/2/content?type=png", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/3/content?type=png", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/4/content?type=png", + "http://data.oxylabs.io/v1/queries/7368265301332333570/results/5/content?type=png" + ], + "method": "GET" + } + ] + } +} \ No newline at end of file diff --git a/tests/Fixtures/universal_failed_result.json b/tests/Fixtures/universal_failed_result.json new file mode 100644 index 0000000..5b77d01 --- /dev/null +++ b/tests/Fixtures/universal_failed_result.json @@ -0,0 +1,743 @@ +{ + "results": [ + { + "content": { + "parse_status_code": 12002 + }, + "created_at": "2025-08-31 14:26:03", + "updated_at": "2025-08-31 14:29:28", + "page": 1, + "url": "https://www.homedepot.com/p/MOEN-Adler-Single-Handle-Pull-Down-Sprayer-Kitchen-Faucet-with-Reflex-and-Power-Clean-in-Matte-Black-87233BL/311933284?source=shoppingads&locale=en-US", + "job_id": "7367925645617159169", + "is_render_forced": false, + "status_code": 200, + "type": "parsed", + "parser_type": "ecommerce_product", + "parser_preset": null, + "_request": { + "cookies": [], + "headers": { + "Accept": "*/*", + "Referer": "https://www.homedepot.com/", + "Sec-Ch-Ua": "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Brave\";v=\"122\"", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.8", + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": "\"Linux\"" + } + }, + "_response": { + "cookies": [ + { + "key": "HD_DC", + "path": "/", + "value": "origin", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "AKA_A2", + "path": "/", + "value": "A", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1756654132.804429, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "akavpau_prod", + "path": "/", + "value": "1756650832~id=136376d5f21c3dc44d76c7df590109ff", + "domain": "www.homedepot.com", + "secure": true, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "akacd_usbeta", + "path": "/", + "value": "3934103331~rv=38~id=bed9d701f0cc5ed54bc1e38bf1696b84", + "domain": "www.homedepot.com", + "secure": true, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "bm_ss", + "path": "/", + "value": "ab8e18ef4e", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1756654132.804471, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "_bman", + "path": "/", + "value": "48d89825d0d0137a9354be80f9f3832b", + "domain": ".www.homedepot.com", + "secure": true, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "bm_so", + "path": "/", + "value": "0A581902335F26B8EDCC2699B9FE988796B7B9404DF5C82EA73374A0BC21876E~YAAQtP1IF1btlfGYAQAAm++HAAQJRl0I/cprJjXnHCyb0nGnbv3V4Nve41OQalP0LWwH9/XWWGrOhTu2QjQm28lns2L2maUGEfA8QZaN5w15KhROtQJgmOBsUTkp3EiDVZnhGYjZEu3oB7QKV53d7hW0naJ6cwor/tAn4gLUelgKuXpZWzW2dpXR4olKeTxgcsx4LahGLtac2PDL8dA/PkHAAnY2t4ajFJM2H1+SsHDnXmrjvC0i+3Qw5HTKGl+eL67G6t7Bzl/DaGtBNcEeeYzwvO+ux349VmXf6AiBdx05wCd24chDj1qkJPhUlTJqUohWRlehcuGiI+USPey3cmEleJMknKa3HAT53J8XeZoAlobP2/rpWQ1N36/9J8C9M1BmDmh1UMJT1m6SdwYVoDbUDh9wABnGp3NpOA/v7Fxn+SqdddisXE4uchimRQBIFiwsuTnRf+48WB7luaxv", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1756736932.804505, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "bm_sz", + "path": "/", + "value": "14CAEEE198D0C08E6ADA08E3B7C29F2E~YAAQtP1IF1ftlfGYAQAAm++HAByKgOxcUm1xGZNg9z8BpyjSf3/vDup4oiEjxj6eh0uW7QNiDjtiXs3yXqeQx0EnQ7sMGJczmiz/eSWAi+lUAuXVlw9vDtQv15xLEUcvHRAPTNELJzXSfRvOAcCnwpkgQxgdqUuxgzoylAmtcpyMettFMEStWD1Pty4/GmtseB49I/NaYft5im6nhblJdCetwkiqVewSainYkiy7QEJm3BUSKmu9prDfhpSAVCMb5w468D7tVp+muehAaI7uFtfreKnuXW5rRFc+G7AoE66PbmSyK7suHdZKqfecdFFfD28drcTks8DpjMKMw8oUCXQvhwHtKRPFJVl1IPy+ZIJjf1XPnl/rjKTBOIkIuZGftOB60kacWhqO8U+L+Ws=~3748165~4403768", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1756664932.804515, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "THD_NR", + "path": "/", + "value": "0", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1756736932, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "kndctr_F6421253512D2C100A490D45_AdobeOrg_cluster", + "path": "/", + "value": "irl1", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1756652335.019147, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "AMCV_F6421253512D2C100A490D45%40AdobeOrg", + "path": "/", + "value": "MCMID|69413148745427377631303856554730238980", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1757255334.089156, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "THD_PERSIST", + "path": "/", + "value": "", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255334.23317, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "THD_SESSION", + "path": "/", + "value": "", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1756736934, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "THD_CACHE_NAV_SESSION", + "path": "/", + "value": "", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1756736934, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "THD_CACHE_NAV_PERSIST", + "path": "/", + "value": "", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255334.233593, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "IN_STORE_API_SESSION", + "path": "/", + "value": "TRUE", + "domain": "www.homedepot.com", + "secure": false, + "comment": "", + "expires": 1756650834, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "ak_bmsc", + "path": "/", + "value": "2E64E9412B1089CDD77CA4946A28913B~000000000000000000000000000000~YAAQtP1IF2ftlfGYAQAAvveHAByPUeLRz+UlWNhEoE4biHcpaSSGNifopja9yFHeZhnXxVzKKJK3aHBGLsQNVt/J4st8DUBAqvkl8lxw66KGOPAb4CPfN66f+IeIXUGiwnhL9Z4POeJYyUgXlxV8eCgoeNdMKHl37/eOQsb6AInG5a2w86voJsG50Uo2uq7afAsFUpWl1DTBUgZWTV1tL+pqAhIXLdJadnzcjjQLyt6ZMRBKPtDkIoMHqgH1SjITMFORz8H/MdRsNmcsatvEBD12nPMMQORwRlMn74MevIXH9rOke8kNIWuh5d5QzBq9dEb9DkDzi1k1sAg7b4BGkXK0ZTmwos4RITwqGCne85OQIJL9lAm+U/nD+l58SnUnuec8uHbh22pufM/Jl00vZT4wUQT2OnwwEPqrb8rgT+Uzk+/R6W7l5wsBlw6xYJYEBAatorcao4jfrTVWqqc=", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1756657733.911273, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "kndctr_F6421253512D2C100A490D45_AdobeOrg_identity", + "path": "/", + "value": "CiY2OTQxMzE0ODc0NTQyNzM3NzYzMTMwMzg1NjU1NDczMDIzODk4MFITCKnon4SQMxABGAEqBElSTDEwAPABmvCfhJAz", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1772202535.019199, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "x-tt-maa", + "path": "/", + "value": "flash25-0", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1756736935, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "x-ttsearch", + "path": "/", + "value": "plp_speed", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1756654135, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "THD_LOCALIZER", + "path": "/", + "value": "%7B%22WORKFLOW%22%3A%22LOCALIZED_BY_DEFAULT%22%2C%22THD_FORCE_LOC%22%3A%222%22%2C%22THD_LOCSTORE%22%3A%22121%2BCumberland%20-%20Atlanta%2C%20GA%2B%22%2C%22THD_STRFINDERZIP%22%3A%2230339%22%2C%22THD_STORE_HOURS%22%3A%221%3B8%3A00-19%3A00%3B2%3B6%3A00-21%3A00%3B3%3B6%3A00-21%3A00%3B4%3B6%3A00-21%3A00%3B5%3B6%3A00-21%3A00%3B6%3B6%3A00-21%3A00%3B7%3B6%3A00-21%3A00%22%2C%22THD_STORE_HOURS_EXPIRY%22%3A1756654135%7D", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "DELIVERY_ZIP", + "path": "/", + "value": "30339", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255335.040024, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "DELIVERY_ZIP_TYPE", + "path": "/", + "value": "DEFAULT", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255335.040042, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "_abck", + "path": "/", + "value": "CC83132CEA9DCC246AD6F862827DDB08~-1~YAAQtP1IF2rtlfGYAQAAZviHAA6PVwtmWpokK7OprS5EssgapAlHFmjgP4kMtLfQz1jNwz/vIvCF9SzKy63mP88GXEZPBfhkZHS1EQ0A4jJbnESLRy5aylWhNJz0wa32Eb4euZnrtcdQKuym1ngbjLw5k0i8ZzGICf6Rlh7Gf6DOeqY4y5vW5HtQ7XPwOOEbBECSx86sJa+EtJr0/sQ/rncASGkWLQeAHevFYLCKK43nswWJbxlP8vF6/q4Oi+mnSMckCdVTOJ1d/p7mTCxsJjToKD6oHiwlFZwddG++y7k3rQMY6+c4nEhegF2nqzzX2Gr3yB03ogSVLGqNsl24HagXMqsM6Soi/G13/ukD5lroG2lqMG/cUX3+1SXbs9PeKj9NBpLMNF9/jyMl4d7yX66eBQiFyQiS8+rBATwQYLz+rcEvqVvQwx1FF1cPBz2KXckf718zAkYSUdaYtwNNLYkRRXFiDHVCTl8fEXZlYcGu4H36VY+h2E7x/D6diqRMlD0UYDhfEJOfLw18E96LGNlA7OkW5zxMqdu9h7zhyk8Exgsp3FuS3JBAiw6DphOUwPsgGK+kJjLQtuzu9htBvEoRRtUriij23Q==~-1~-1~-1~~", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1772202535.054271, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "pxcts", + "path": "/", + "value": "d2f3761e-8676-11f0-bd50-96c059590e9e", + "domain": "www.homedepot.com", + "secure": false, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "_pxvid", + "path": "/", + "value": "d2f36aaa-8676-11f0-bd4f-85b659faacbe", + "domain": "www.homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255335.35562, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "bm_lso", + "path": "/", + "value": "0A581902335F26B8EDCC2699B9FE988796B7B9404DF5C82EA73374A0BC21876E~YAAQtP1IF1btlfGYAQAAm++HAAQJRl0I/cprJjXnHCyb0nGnbv3V4Nve41OQalP0LWwH9/XWWGrOhTu2QjQm28lns2L2maUGEfA8QZaN5w15KhROtQJgmOBsUTkp3EiDVZnhGYjZEu3oB7QKV53d7hW0naJ6cwor/tAn4gLUelgKuXpZWzW2dpXR4olKeTxgcsx4LahGLtac2PDL8dA/PkHAAnY2t4ajFJM2H1+SsHDnXmrjvC0i+3Qw5HTKGl+eL67G6t7Bzl/DaGtBNcEeeYzwvO+ux349VmXf6AiBdx05wCd24chDj1qkJPhUlTJqUohWRlehcuGiI+USPey3cmEleJMknKa3HAT53J8XeZoAlobP2/rpWQ1N36/9J8C9M1BmDmh1UMJT1m6SdwYVoDbUDh9wABnGp3NpOA/v7Fxn+SqdddisXE4uchimRQBIFiwsuTnRf+48WB7luaxv^1756650535517", + "domain": ".www.homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255335.517132, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "thda.s", + "path": "/", + "value": "5fb535f1-d005-4453-619b-46733df5764e", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "thda.u", + "path": "/", + "value": "0ca8b503-bc91-6740-b152-32faf83979ad", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255336.310845, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "_bman_api", + "path": "/", + "value": "bd5187e6b56676c71d7d5cbdd2b35def", + "domain": ".apionline.homedepot.com", + "secure": true, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "_px_f394gi7Fvmc43dfg_user_id", + "path": "/", + "value": "ZDQxNGEwZjEtODY3Ni0xMWYwLWIxMWEtNTU2NDljMjAyZGYy", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255337.087958, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "RT", + "path": "/", + "value": "\"z=1&dm=www.homedepot.com&si=fe224421-aad3-418c-b51b-1252a1d19c20&ss=mezsdd2d&sl=1&tt=46l&rl=1&ld=46m\"", + "domain": ".www.homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255337, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "QSI_HistorySession", + "path": "/", + "value": "https%3A%2F%2Fwww.homedepot.com%2Fp%2FMOEN-Adler-Single-Handle-Pull-Down-Sprayer-Kitchen-Faucet-with-Reflex-and-Power-Clean-in-Matte-Black-87233BL%2F311933284%3Fsource%3Dshoppingads%26locale%3Den-US~1756650538668", + "domain": "www.homedepot.com", + "secure": true, + "comment": "", + "expires": -1, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "forterToken", + "path": "/", + "value": "53899ea4a3a04e26b5a44a76fe38398e_1756650537090__UDF43b-m4_23ck_", + "domain": ".homedepot.com", + "secure": false, + "comment": "", + "expires": 1757255339.906151, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "bm_s", + "path": "/", + "value": "YAAQtP1IF33tlfGYAQAA6QuIAAM9pMr63Hvew+wLXe9D5hmNTj/fgoE+Ul9K5BETWuHE6OU0A0emLJWJIEMK6Z6gKW57Eod8qJbEv55CyrFfi1JsWdjlx5VgQk+b518Eeqe+LPmSJ5RqBZBjOzpBtvUfy1fl3X/VXiOt+sSRin/TUUYK0uR82wOJJdhuKpnOjlj+ZBVOnAJHQMRJ4E5myauoKCK8M35HfEBPVim+fNwf5l3qKt6oGGowEeS+2jPDgtmX+3G9Vs2EdA87Eq+TfynOvT/e2fSebMcTFv0HM2a8SQABbloioj4TDLHHeIcADFnVRCAYFGzojE4Ln2RRME8QjLMpDdMrt4dzD+AMKAwMjvMTUhhHi9T49u4NEXABuaOeTz/HMw9NjKxJ+bAuNjR2BSk2wpiDE6/3DG3shpcnwL2ss/arpGPHqXjL+M84IFhN2PpH9D6B/vVZj0DQBWz0p3KhKqLne24vBUR18Ipzpoksb4wHsbuBtI/zEQIH104ddci/uhZrbzHER84u5efPVhvvaZxU4POUVINZ+ZZqK5Wp5PC+", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1759328940.058189, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "QuantumMetricSessionID", + "path": "/", + "value": "401a3d2fcd34ee4f1128eb4661b47a03", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1756652354, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "QuantumMetricUserID", + "path": "/", + "value": "15d5edf9507141cc2c8bc9379920449b", + "domain": ".homedepot.com", + "secure": true, + "comment": "", + "expires": 1757255340.099928, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "_px", + "path": "/", + "value": "WrI53+zAOIOptz8CFPwxjQuPJw6v4Ij4D2FwrTS+BREXBIXWR4T+Jp+vHCs6nXrYDABPkR7fLBY/GC6oqqa3MA==:1000:qtxcKUjUAsCph6flPpxeyLpgP+FURctsxDk/GGQCoSNO/bFaWl1Xo0PBK2lmUUlOzmJ1mBin4zKplUJubqpCY7W1nLaklVeSuf8GCZ/b3FHDMKb7TOu28wCOLPQy0kqVHVELvtqBeJJLQ5bt/KibyJwN9uIS4TfjY5pguOeefwuwjdvLATpNdTXcz9kRv9s+n+UG+WG+G3xUJyZsolJMf8Mh/oC1HhOFYOvKOaY0KoYkFkEztOq/tI129c3efr26NLUOfolSMjXFb3q7f3g2bA==", + "domain": "www.homedepot.com", + "secure": false, + "comment": "", + "expires": 1756823359, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + }, + { + "key": "_pxde", + "path": "/", + "value": "2695944b12f6f0e06ec9cc754e5d999f8c4c000beeb8f74d4b5b58c00c92bbb1:eyJ0aW1lc3RhbXAiOjE3NTY2NTA1NTkxMDMsImluY19pZCI6WyJkZGE4ZTliYzMyNjI1MjhlMmVkZmIzYjAyNzg0MjZmMiIsIjVjZjdiMmFmZDQwYTBlNGFjMWRhMDMzMzZkYTc2YjRmIiwiYjhlZGY4Y2M4MmMxN2RmM2Q3YzFhZWFmYjAzZWNjYjQiXX0=", + "domain": "www.homedepot.com", + "secure": false, + "comment": "", + "expires": 1756823359, + "max-age": "", + "version": "", + "httponly": "", + "samesite": "" + } + ], + "headers": { + "date": "Sun, 31 Aug 2025 14:28:52 GMT", + "host": "www.homedepot.com", + "link": ";rel=\"preload\";as=\"font\";type=\"font/woff2\";crossorigin,;rel=\"preload\";as=\"font\";type=\"font/woff2\";crossorigin,;rel=\"preload\";as=\"font\";type=\"font/woff2\";crossorigin,;rel=\"preload\";as=\"font\";type=\"font/woff2\";crossorigin, ;rel=\"preconnect\"", + "vary": "Accept-Encoding", + "grace": "none", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", + "pragma": "no-cache", + "server": "nginx", + "expires": "Sun, 31 Aug 2025 14:28:52 GMT", + "sec-gpc": "1", + "x-proto": "secure", + "priority": "u=0, i", + "expect-ct": "max-age=0", + "sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Brave\";v=\"139\", \"Chromium\";v=\"139\"", + "x-varnish": "1020963198 1023945492", + "akamai-grn": "0.b4fd4817.1756650532.4061345", + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "content-type": "text/html; charset=utf-8", + "x-experiment": "{\"decision_engine\":{},\"adobe\":{}}", + "accept-ranges": "bytes", + "cache-control": "no-cache", + "server-timing": "cdn-cache; desc=REVALIDATE, edge; dur=137, origin; dur=44, ak_p; desc=\"1756650532220_390659508_67507013_18105_50926_15_291_255\";dur=1", + "x-device-type": "desktop", + "x-varnish-esi": "true", + "sec-fetch-dest": "document", + "sec-fetch-mode": "cors", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "accept-encoding": "gzip, deflate, br", + "accept-language": "en-US,en;q=0.8;q=0.9", + "x-varnish-cache": "HIT(1)@vdir", + "content-encoding": "br", + "sec-ch-ua-mobile": "?0", + "x-xss-protection": "0", + "sec-ch-ua-platform": "\"Linux\"", + "x-download-options": "noopen", + "x-akamai-transformed": "9 - 0 pmb=mTOE,2mRUM,2", + "x-application-context": "render-c2-workers:undefined:fusion-hdh-pip-desktop:kitchen-faucets", + "x-content-type-options": "nosniff", + "x-dns-prefetch-control": "on", + "strict-transport-security": "max-age=63072000; includeSubDomains", + "upgrade-insecure-requests": "1", + "x-permitted-cross-domain-policies": "none", + "remove-dup-edge-ctrl-headers-rollout-enabled": "1" + } + }, + "session_info": { + "id": null, + "expires_at": null, + "remaining": null + } + } + ], + "job": { + "callback_url": null, + "client_id": 83306, + "context": [ + { + "key": "force_headers", + "value": false + }, + { + "key": "force_cookies", + "value": false + }, + { + "key": "hc_policy", + "value": true + }, + { + "key": "successful_status_codes", + "value": [] + }, + { + "key": "follow_redirects", + "value": null + }, + { + "key": "cookies", + "value": [] + }, + { + "key": "headers", + "value": [] + }, + { + "key": "session_id", + "value": null + }, + { + "key": "http_method", + "value": "get" + }, + { + "key": "content", + "value": null + }, + { + "key": "store_id", + "value": null + }, + { + "key": "proxy_location", + "value": null + }, + { + "key": "delivery_location", + "value": null + }, + { + "key": "fulfillment_type", + "value": null + } + ], + "created_at": "2025-08-31 14:26:03", + "domain": "com", + "geo_location": "United States", + "id": "7367925645617159169", + "limit": 10, + "locale": null, + "pages": 1, + "parse": true, + "parser_type": "ecommerce_product", + "parser_preset": null, + "parsing_instructions": null, + "browser_instructions": null, + "render": "png", + "xhr": false, + "markdown": false, + "url": "https://www.homedepot.com/p/MOEN-Adler-Single-Handle-Pull-Down-Sprayer-Kitchen-Faucet-with-Reflex-and-Power-Clean-in-Matte-Black-87233BL/311933284?source=shoppingads&locale=en-US&srsltid=AfmBOorXn9kENeFYSiVY1MuebuKYlSS4NTCiFb0gxtHHhzJioIbQiSmgy88", + "query": "", + "source": "universal", + "start_page": 1, + "status": "done", + "storage_type": null, + "storage_url": null, + "subdomain": "www", + "content_encoding": "utf-8", + "updated_at": "2025-08-31 14:29:28", + "user_agent_type": "desktop", + "session_info": null, + "statuses": [], + "client_notes": null, + "_links": [ + { + "rel": "self", + "href": "http://data.oxylabs.io/v1/queries/7367925645617159169", + "method": "GET" + }, + { + "rel": "results", + "href": "http://data.oxylabs.io/v1/queries/7367925645617159169/results", + "method": "GET" + }, + { + "rel": "results-content", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7367925645617159169/results/1/content" + ], + "method": "GET" + }, + { + "rel": "results-html", + "href": "http://data.oxylabs.io/v1/queries/7367925645617159169/results?type=raw", + "method": "GET" + }, + { + "rel": "results-content-html", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7367925645617159169/results/1/content?type=raw" + ], + "method": "GET" + }, + { + "rel": "results-parsed", + "href": "http://data.oxylabs.io/v1/queries/7367925645617159169/results?type=parsed", + "method": "GET" + }, + { + "rel": "results-content-parsed", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7367925645617159169/results/1/content?type=parsed" + ], + "method": "GET" + }, + { + "rel": "results-png", + "href": "http://data.oxylabs.io/v1/queries/7367925645617159169/results?type=png", + "method": "GET" + }, + { + "rel": "results-content-png", + "href_list": [ + "http://data.oxylabs.io/v1/queries/7367925645617159169/results/1/content?type=png" + ], + "method": "GET" + } + ] + } +} \ No newline at end of file