Skip to content

Commit e4623e8

Browse files
🐛 Source google-analytics-data-api: fix max time (#43929)
1 parent 476bdea commit e4623e8

File tree

5 files changed

+67
-4
lines changed

5 files changed

+67
-4
lines changed

airbyte-integrations/connectors/source-google-analytics-data-api/metadata.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ data:
1212
connectorSubtype: api
1313
connectorType: source
1414
definitionId: 3cc2eafd-84aa-4dca-93af-322d9dfeec1a
15-
dockerImageTag: 2.5.2
15+
dockerImageTag: 2.5.3
1616
dockerRepository: airbyte/source-google-analytics-data-api
1717
documentationUrl: https://docs.airbyte.com/integrations/sources/google-analytics-data-api
1818
githubIssueLabel: source-google-analytics-data-api

airbyte-integrations/connectors/source-google-analytics-data-api/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
33
build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
6-
version = "2.5.2"
6+
version = "2.5.3"
77
name = "source-google-analytics-data-api"
88
description = "Source implementation for Google Analytics Data Api."
99
authors = [ "Airbyte <[email protected]>",]

airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py

+15
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import re
1010
import uuid
1111
from abc import ABC
12+
from datetime import timedelta
1213
from http import HTTPStatus
1314
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Set, Tuple, Type, Union
1415

@@ -101,6 +102,20 @@ def backoff_time(
101102

102103

103104
class GoogleAnalyticsDatApiErrorHandler(HttpStatusErrorHandler):
105+
QUOTA_RECOVERY_TIME = 3600
106+
107+
def __init__(
108+
self,
109+
logger: logging.Logger,
110+
error_mapping: Optional[Mapping[Union[int, str, type[Exception]], ErrorResolution]] = None,
111+
) -> None:
112+
super().__init__(
113+
logger=logger,
114+
error_mapping=error_mapping,
115+
max_retries=5,
116+
max_time=timedelta(seconds=GoogleAnalyticsDatApiErrorHandler.QUOTA_RECOVERY_TIME),
117+
)
118+
104119
@GoogleAnalyticsQuotaHandler.handle_quota()
105120
def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
106121
if not isinstance(response_or_exception, Exception) and response_or_exception.status_code == requests.codes.too_many_requests:

airbyte-integrations/connectors/source-google-analytics-data-api/unit_tests/test_source.py

+49-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from airbyte_cdk.utils import AirbyteTracedException
1111
from airbyte_protocol.models import Status
1212
from source_google_analytics_data_api import SourceGoogleAnalyticsDataApi
13-
from source_google_analytics_data_api.source import MetadataDescriptor
13+
from source_google_analytics_data_api.api_quota import GoogleAnalyticsApiQuotaBase
14+
from source_google_analytics_data_api.source import GoogleAnalyticsDatApiErrorHandler, MetadataDescriptor
1415
from source_google_analytics_data_api.utils import NO_DIMENSIONS, NO_METRICS, NO_NAME, WRONG_CUSTOM_REPORT_CONFIG, WRONG_JSON_SYNTAX
1516

1617

@@ -108,7 +109,53 @@ def test_check_failure_throws_exception(requests_mock, config_gen, error_code):
108109
assert e.value.failure_type == FailureType.config_error
109110
assert "Access was denied to the property ID entered." in e.value.message
110111

111-
@pytest.mark.parametrize("error_code", (402,404, 405))
112+
113+
def test_exhausted_quota_recovers_after_two_retries(requests_mock, config_gen):
114+
"""
115+
If the account runs out of quota the api will return a message asking us to back off for one hour.
116+
We have set backoff time for this scenario to 30 minutes to check if quota is already recovered, if not
117+
it will backoff again 30 minutes and quote should be reestablished by then.
118+
Now, we don't want wait one hour to test out this retry behavior so we will fix time dividing by 600 the quota
119+
recovery time and also the backoff time.
120+
"""
121+
requests_mock.register_uri(
122+
"POST", "https://oauth2.googleapis.com/token", json={"access_token": "access_token", "expires_in": 3600, "token_type": "Bearer"}
123+
)
124+
error_response = {"error": {"message":"Exhausted potentially thresholded requests quota. This quota will refresh in under an hour. To learn more, see"}}
125+
requests_mock.register_uri(
126+
"GET",
127+
"https://analyticsdata.googleapis.com/v1beta/properties/UA-11111111/metadata",
128+
# first try we get 429 t=~0
129+
[{"json": error_response, "status_code": 429},
130+
# first retry we get 429 t=~1800
131+
{"json": error_response, "status_code": 429},
132+
# second retry quota is recovered, t=~3600
133+
{"json": {
134+
"dimensions": [{"apiName": "date"}, {"apiName": "country"}, {"apiName": "language"}, {"apiName": "browser"}],
135+
"metrics": [{"apiName": "totalUsers"}, {"apiName": "screenPageViews"}, {"apiName": "sessions"}],
136+
}, "status_code": 200}
137+
]
138+
)
139+
def fix_time(time):
140+
return int(time / 600 )
141+
source = SourceGoogleAnalyticsDataApi()
142+
logger = MagicMock()
143+
max_time_fixed = fix_time(GoogleAnalyticsDatApiErrorHandler.QUOTA_RECOVERY_TIME)
144+
potentially_thresholded_requests_per_hour_mapping = GoogleAnalyticsApiQuotaBase.quota_mapping["potentiallyThresholdedRequestsPerHour"]
145+
threshold_backoff_time = potentially_thresholded_requests_per_hour_mapping["backoff"]
146+
fixed_threshold_backoff_time = fix_time(threshold_backoff_time)
147+
potentially_thresholded_requests_per_hour_mapping_fixed = {
148+
**potentially_thresholded_requests_per_hour_mapping,
149+
"backoff": fixed_threshold_backoff_time,
150+
}
151+
with (
152+
patch.object(GoogleAnalyticsDatApiErrorHandler, 'QUOTA_RECOVERY_TIME', new=max_time_fixed),
153+
patch.object(GoogleAnalyticsApiQuotaBase, 'quota_mapping', new={**GoogleAnalyticsApiQuotaBase.quota_mapping,"potentiallyThresholdedRequestsPerHour": potentially_thresholded_requests_per_hour_mapping_fixed})):
154+
output = source.check(logger, config_gen(property_ids=["UA-11111111"]))
155+
assert output == AirbyteConnectionStatus(status=Status.SUCCEEDED, message=None)
156+
157+
158+
@pytest.mark.parametrize("error_code", (402, 404, 405))
112159
def test_check_failure(requests_mock, config_gen, error_code):
113160
requests_mock.register_uri(
114161
"POST", "https://oauth2.googleapis.com/token", json={"access_token": "access_token", "expires_in": 3600, "token_type": "Bearer"}

docs/integrations/sources/google-analytics-data-api.md

+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ The Google Analytics connector is subject to Google Analytics Data API quotas. P
268268

269269
| Version | Date | Pull Request | Subject |
270270
|:--------|:-----------| :------------------------------------------------------- |:---------------------------------------------------------------------------------------|
271+
| 2.5.3 | 2024-08-13 | [43929](https://github.com/airbytehq/airbyte/pull/43929) | Increase streams max_time to backoff |
271272
| 2.5.2 | 2024-08-12 | [43909](https://github.com/airbytehq/airbyte/pull/43909) | Update dependencies |
272273
| 2.5.1 | 2024-08-10 | [43289](https://github.com/airbytehq/airbyte/pull/43289) | Update dependencies |
273274
| 2.5.0 | 2024-08-07 | [42841](https://github.com/airbytehq/airbyte/pull/42841) | Upgrade to CDK 3 |

0 commit comments

Comments
 (0)