From 44e854e0d2c86a720efbb89f704627f7d2f9816a Mon Sep 17 00:00:00 2001 From: Aldo Gonzalez <168454423+aldogonzalez8@users.noreply.github.com> Date: Fri, 26 Jul 2024 08:15:10 -0600 Subject: [PATCH] Source facebook-marketing: add friendly messages and tests (#42562) --- .../source-facebook-marketing/metadata.yaml | 2 +- .../source-facebook-marketing/pyproject.toml | 2 +- .../streams/common.py | 10 +- .../unit_tests/test_errors.py | 110 ++++++++++++++---- .../sources/facebook-marketing.md | 1 + 5 files changed, 99 insertions(+), 26 deletions(-) diff --git a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml index c1e0db44bf3e..24388805cd75 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml +++ b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml @@ -10,7 +10,7 @@ data: connectorSubtype: api connectorType: source definitionId: e7778cfc-e97c-4458-9ecb-b4f2bba8946c - dockerImageTag: 3.3.14 + dockerImageTag: 3.3.15 dockerRepository: airbyte/source-facebook-marketing documentationUrl: https://docs.airbyte.com/integrations/sources/facebook-marketing githubIssueLabel: source-facebook-marketing diff --git a/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml b/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml index 79c09a6fff46..2c2d02cdd729 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml +++ b/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",] build-backend = "poetry.core.masonry.api" [tool.poetry] -version = "3.3.14" +version = "3.3.15" name = "source-facebook-marketing" description = "Source implementation for Facebook Marketing." authors = [ "Airbyte ",] diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/common.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/common.py index 3a08726ffbe4..c3446cdfea01 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/common.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/streams/common.py @@ -181,7 +181,15 @@ def traced_exception(fb_exception: FacebookRequestError): "Ad Account Id is used (as in Ads Manager), re-authenticate if FB oauth is used or refresh " "access token with all required permissions." ) - + elif "reduce the amount of data" in msg: + failure_type = FailureType.config_error + friendly_msg = ( + "Please reduce the number of fields requested. Go to the schema tab, select your source, " + "and unselect the fields you do not need." + ) + elif "The start date of the time range cannot be beyond 37 months from the current date" in msg: + failure_type = FailureType.config_error + friendly_msg = "Please set the start date of your sync to be within the last 3 years." elif fb_exception.api_error_code() in FACEBOOK_RATE_LIMIT_ERROR_CODES: return AirbyteTracedException( message="The maximum number of requests on the Facebook API has been reached. See https://developers.facebook.com/docs/graph-api/overview/rate-limiting/ for more information", diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py index e8a12f284905..b593e7e1b1f3 100644 --- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py +++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_errors.py @@ -205,6 +205,21 @@ }, }, ), + ( + "error_400_start_date_not_within_three_years", + "Please set the start date of your sync to be within the last 3 years.", + { + "status_code": 400, + "json": { + "error": { + "message": "(#3018) The start date of the time range cannot be beyond 37 months from the current date", + "type": "OAuthException", + "code": 3018, + "fbtrace_id": "Ag-P22y80OSEXM4qsGk2T9P" + } + }, + }, + ), # ("error_400_unsupported request", # "Re-authenticate because current credential missing permissions", # { @@ -235,6 +250,29 @@ # ), ] +SERVICE_TEMPORARILY_UNAVAILABLE_TEST_NAME = "error_400_service_temporarily_unavailable" +SERVICE_TEMPORARILY_UNAVAILABLE_RESPONSE = { + "status_code": 503, + "json": { + "error": { + "message": "(#2) Service temporarily unavailable", + "type": "OAuthException", + "is_transient": True, + "code": 2, + "fbtrace_id": "AnUyGZoFqN2m50GHVpOQEqr", + } + }, + } +REDUCE_FIELDS_ERROR_TEST_NAME = "error_500_reduce_the_amount_of_data" +REDUCE_FIELDS_ERROR_RESPONSE = { + "status_code": 500, + "json": { + "error": { + "message": "Please reduce the amount of data you're asking for, then retry your request", + "code": 1, + } + }, + } class TestRealErrors: @pytest.mark.parametrize( @@ -273,31 +311,12 @@ class TestRealErrors: }, ), ( - "error_400_service_temporarily_unavailable", - { - "status_code": 400, - "json": { - "error": { - "message": "(#2) Service temporarily unavailable", - "type": "OAuthException", - "is_transient": True, - "code": 2, - "fbtrace_id": "AnUyGZoFqN2m50GHVpOQEqr", - } - }, - }, + SERVICE_TEMPORARILY_UNAVAILABLE_TEST_NAME, + SERVICE_TEMPORARILY_UNAVAILABLE_RESPONSE, ), ( - "error_500_reduce_the_amount_of_data", - { - "status_code": 500, - "json": { - "error": { - "message": "Please reduce the amount of data you're asking for, then retry your request", - "code": 1, - } - }, - } + REDUCE_FIELDS_ERROR_TEST_NAME, + REDUCE_FIELDS_ERROR_RESPONSE, # It can be a temporal problem: # Happened during 'ad_account' stream sync which always returns only 1 record. # Potentially could be caused by some particular field (list of requested fields is constant). @@ -387,6 +406,49 @@ def test_config_error_during_actual_nodes_read(self, requests_mock, name, friend assert error.failure_type == FailureType.config_error assert friendly_msg in error.message + @pytest.mark.parametrize("name, friendly_msg, config_error_response, failure_type", + [ + ( + REDUCE_FIELDS_ERROR_TEST_NAME, + "Please reduce the number of fields requested. Go to the schema tab, " + "select your source, and unselect the fields you do not need.", + REDUCE_FIELDS_ERROR_RESPONSE, + FailureType.config_error + ), + ( + SERVICE_TEMPORARILY_UNAVAILABLE_TEST_NAME, + "The Facebook API service is temporarily unavailable. This issue should resolve itself, and does not require further action.", + SERVICE_TEMPORARILY_UNAVAILABLE_RESPONSE, + FailureType.transient_error + ) + ] + ) + def test_config_error_that_was_retried_when_reading_nodes(self, requests_mock, name, friendly_msg, config_error_response, failure_type): + """This test covers errors that have been resolved in the past with a retry strategy, but it could also can fail after retries, + then, we need to provide the user with a humanized error explaining what just happened""" + api = API(access_token=some_config["access_token"], page_size=100) + stream = AdCreatives(api=api, account_ids=some_config["account_ids"]) + + requests_mock.register_uri("GET", f"{act_url}", [ad_account_response]) + requests_mock.register_uri( + "GET", + f"{act_url}adcreatives", + [config_error_response], + ) + try: + list( + stream.read_records( + sync_mode=SyncMode.full_refresh, + stream_state={}, + stream_slice={"account_id": account_id}, + ) + ) + assert False + except Exception as error: + assert isinstance(error, AirbyteTracedException) + assert error.failure_type == failure_type + assert (friendly_msg) in error.message + @pytest.mark.parametrize("name, friendly_msg, config_error_response", CONFIG_ERRORS) def test_config_error_insights_account_info_read(self, requests_mock, name, friendly_msg, config_error_response): """Error raised during actual nodes read""" @@ -525,6 +587,7 @@ def test_adaccount_list_objects_retry(self, requests_mock, failure_response): ) assert list(record_gen) == [{"account_id": "unknown_account", "id": "act_unknown_account"}] + def test_traced_exception_with_api_error(): error = FacebookRequestError( message="Some error occurred", @@ -541,6 +604,7 @@ def test_traced_exception_with_api_error(): assert result.message == "Invalid access token. Re-authenticate if FB oauth is used or refresh access token with all required permissions" assert result.failure_type == FailureType.config_error + def test_traced_exception_without_api_error(): error = FacebookRequestError( message="Call was unsuccessful. The Facebook API has imploded", diff --git a/docs/integrations/sources/facebook-marketing.md b/docs/integrations/sources/facebook-marketing.md index d22d396d87bc..a377d825947a 100644 --- a/docs/integrations/sources/facebook-marketing.md +++ b/docs/integrations/sources/facebook-marketing.md @@ -215,6 +215,7 @@ This response indicates that the Facebook Graph API requires you to reduce the f | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 3.3.15 | 2024-07-15 | [42562](https://github.com/airbytehq/airbyte/pull/42562) | Add friendly messages for "reduce fields" and "start date" errors | | 3.3.14 | 2024-07-15 | [41958](https://github.com/airbytehq/airbyte/pull/41958) | Update cdk to filter invalid fields from configured catalog | | 3.3.13 | 2024-07-13 | [41732](https://github.com/airbytehq/airbyte/pull/41732) | Update dependencies | | 3.3.12 | 2024-07-11 | [41644](https://github.com/airbytehq/airbyte/pull/41644) | Remove discriminator with missing schemas |