google-ads-php icon indicating copy to clipboard operation
google-ads-php copied to clipboard

`grpc` extension changes exceptions being thrown.

Open lezhnev74 opened this issue 5 years ago • 8 comments

I was testing campaign creation and particular case: name duplication error. I found out that different exceptions are thrown in the runtime:

  • when grpc is enabled I get \Google\Ads\GoogleAds\Lib\V2\GoogleAdsException
  • when grpc is disabled I get \Google\ApiCore\ApiException

Not to say, that first case is much more preferable, as the exception contains useful extra information about the problem.

lezhnev74 avatar Oct 30 '19 10:10 lezhnev74

Good feedback. In case of an error, REST and gRPC transports do not return the metadata in the exact same format:

  • gRPC

image

  • REST

image

The current implementation of the GoogleAdsExceptionMiddleware which translates the response including the metadata content into GoogleAdsException is able to process the gRPC format only.

This is not a bug per se, flagging this issue as an enhancement request.

You are saying that there is more information about the error in the GoogleAdsException (gRPC) compared to the ApiException (REST): could you please provide concrete examples of this?

PierrickVoulet avatar Oct 30 '19 20:10 PierrickVoulet

You are saying that there is more information about the error in the GoogleAdsException (gRPC) compared to the ApiException (REST): could you please provide concrete examples of this?

Yes, to get details about the error I do this:

/** @var \Google\Ads\GoogleAds\Lib\V2\GoogleAdsException $e */
$errorArray = json_decode($e->getGoogleAdsFailure()->serializeToJsonString(), true);

# ApiException won't let me do that

That gives me roughly this data which I can work with:

[{"message": "AdGroup with the same name already exists for the campaign.", "trigger": {"stringValue": "Optimized AdGroup #5db9e837828d3"}, "location": {"fieldPathElements": [{"index": "6", "fieldName": "operations"}, {"fieldName": "create"}, {"fieldName": "name"}]}, "errorCode": {"adGroupError": "DUPLICATE_ADGROUP_NAME"}}]

Why do I serialize errors to JSON and deserialize it back? Because this is the only way I figured out to get the actual error code and details about the error. Is there a better way?

lezhnev74 avatar Oct 31 '19 06:10 lezhnev74

Thanks for sharing, ApiException still provides methods like getMetadata and gives access to raw response data. I believe it contains the same information about the error(s) even if not as well structured as you have with a GoogleAdsException.

To be sure, do you mind executing a var_dump of both exception types that return with the same error(s)? I would be curious to see if you can spot any difference of information (and not only structural).

PierrickVoulet avatar Oct 31 '19 12:10 PierrickVoulet

Nice! Can you show an example I can use, say, for getting errorCode value of the error?

lezhnev74 avatar Oct 31 '19 12:10 lezhnev74

Sure, you could print the error codes using something like this:

try {
    // Does some API  call(s)
} catch (ApiException $apiException) {
    foreach($apiException->getMetadata() as $metadata) {
        if(
            array_key_exists('@type', $metadata) &&
            $metadata['@type'] == 'type.googleapis.com/google.ads.googleads.v2.errors.GoogleAdsFailure' &&
            array_key_exists('errors', $metadata)
        ) {
            foreach($metadata['errors'] as $error) {
                if(array_key_exists('errorCode', $error)) {
                    foreach($error['errorCode'] as $errorCode) {
                        print("Error Code: " . $errorCode);
                    }
                }
            }
        }
    }
}

HIH

PierrickVoulet avatar Nov 04 '19 19:11 PierrickVoulet

Thank you! Let me play with that example. Is there plans to develop a more intuitive way to access error?

lezhnev74 avatar Nov 05 '19 05:11 lezhnev74

Tried this test, but it gave me no error codes.

$budget = new CampaignBudget([
    'name' => new StringValue([
        'value' => sprintf('Budget for campaign [%s]', uniqid())
    ]),
    'delivery_method' => BudgetDeliveryMethod::STANDARD,
    'amount_micros' => new Int64Value(['value' => -100]) // wrong value cuases failure
]);
$op = new CampaignBudgetOperation();
$op->setCreate($budget);

$client = $this->app->make(GoogleAdsClientBuilder::class)->build($userId, $googleClientId);
$campaignBudgetServiceClient = $client->getCampaignBudgetServiceClient();
try {
    $campaignBudgetServiceClient->mutateCampaignBudgets($googleClientId, [$op]);
} catch (ApiException $e) {
    foreach($e->getMetadata() as $metadata) {
        if(
            array_key_exists('@type', $metadata) &&
            $metadata['@type'] == 'type.googleapis.com/google.ads.googleads.v2.errors.GoogleAdsFailure' &&
            array_key_exists('errors', $metadata)
        ) {
            foreach($metadata['errors'] as $error) {
                if(array_key_exists('errorCode', $error)) {
                    foreach($error['errorCode'] as $errorCode) {
                        print("Error Code: " . $errorCode);
                    }
                }
            }
        }
    }
}

The API call logged:

Request
-------
Method Name: /google.ads.googleads.v2.services.CampaignBudgetService/MutateCampaignBudgets
Host: googleads.googleapis.com
Headers: {
    "x-goog-api-client": "gl-php\/7.2.23 gapic\/ gax\/1.2.0 grpc\/1.23.1",
    "x-goog-request-params": "customer_id=8530332597",
    "developer-token": "REDACTED"
}
Request: {"customerId":"8530332597","operations":[{"create":{"name":"Budget for campaign [5dc173920a363]","amountMicros":"-100","deliveryMethod":"STANDARD"}}]}

Response
-------
Headers: {
    "request-id": "6OL1RE-W0C1FZgRWStM1eQ",
    "date": "Tue, 05 Nov 2019 13:05:24 GMT",
    "alt-svc": "quic=\":443\"; ma=2592000; v=\"46,43\",h3-Q049=\":443\"; ma=2592000,h3-Q048=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000"
}

Fault
-------
Status code: 3
Details: Request contains an invalid argument.
Failure: {"errors":[{"errorCode":{"rangeError":"TOO_LOW"},"message":"Too low.","location":{"fieldPathElements":[{"fieldName":"operations","index":"0"},{"fieldName":"create"},{"fieldName":"amount_micros"}]}}]}

PHP extensions:

php -m
Starting instaon_mysql ... done
[PHP Modules]
bcmath
Core
ctype
curl
date
dom
exif
fileinfo
filter
ftp
gd
grpc
hash
iconv
imap
json
libxml
mbstring
mysqlnd
openssl
pcntl
pcre
PDO
pdo_mysql
pdo_sqlite
Phar
posix
protobuf
readline
Reflection
session
SimpleXML
soap
sockets
sodium
SPL
sqlite3
standard
tokenizer
xdebug
xml
xmlreader
xmlwriter
zip
zlib

[Zend Modules]
Xdebug

lezhnev74 avatar Nov 05 '19 13:11 lezhnev74

The main condition I drafted might be overkill, did you try to simplify keeping only:

array_key_exists('errors', $metadata)

PierrickVoulet avatar Nov 11 '19 22:11 PierrickVoulet