Skip to content

🐛 Source google-analytics-data-api: fix max time #43929

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: 3cc2eafd-84aa-4dca-93af-322d9dfeec1a
dockerImageTag: 2.5.1
dockerImageTag: 2.5.2
dockerRepository: airbyte/source-google-analytics-data-api
documentationUrl: https://docs.airbyte.com/integrations/sources/google-analytics-data-api
githubIssueLabel: source-google-analytics-data-api
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
version = "2.5.1"
version = "2.5.2"
name = "source-google-analytics-data-api"
description = "Source implementation for Google Analytics Data Api."
authors = [ "Airbyte <[email protected]>",]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import uuid
from abc import ABC
from datetime import timedelta
from http import HTTPStatus
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Set, Tuple, Type, Union

Expand Down Expand Up @@ -101,6 +102,20 @@ def backoff_time(


class GoogleAnalyticsDatApiErrorHandler(HttpStatusErrorHandler):
QUOTA_RECOVERY_TIME = 3600

def __init__(
self,
logger: logging.Logger,
error_mapping: Optional[Mapping[Union[int, str, type[Exception]], ErrorResolution]] = None,
) -> None:
super().__init__(
logger=logger,
error_mapping=error_mapping,
max_retries=5,
max_time=timedelta(seconds=GoogleAnalyticsDatApiErrorHandler.QUOTA_RECOVERY_TIME),
)

@GoogleAnalyticsQuotaHandler.handle_quota()
def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
if not isinstance(response_or_exception, Exception) and response_or_exception.status_code == requests.codes.too_many_requests:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from airbyte_cdk.utils import AirbyteTracedException
from airbyte_protocol.models import Status
from source_google_analytics_data_api import SourceGoogleAnalyticsDataApi
from source_google_analytics_data_api.source import MetadataDescriptor
from source_google_analytics_data_api.api_quota import GoogleAnalyticsApiQuotaBase
from source_google_analytics_data_api.source import GoogleAnalyticsDatApiErrorHandler, MetadataDescriptor
from source_google_analytics_data_api.utils import NO_DIMENSIONS, NO_METRICS, NO_NAME, WRONG_CUSTOM_REPORT_CONFIG, WRONG_JSON_SYNTAX


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

@pytest.mark.parametrize("error_code", (402,404, 405))

def test_exhausted_quota_recovers_after_two_retries(requests_mock, config_gen):
"""
If the account runs out of quota the api will return a message asking us to back off for one hour.
We have set backoff time for this scenario to 30 minutes to check if quota is already recovered, if not
it will backoff again 30 minutes and quote should be reestablished by then.
Now, we don't want wait one hour to test out this retry behavior so we will fix time dividing by 600 the quota
recovery time and also the backoff time.
"""
requests_mock.register_uri(
"POST", "https://oauth2.googleapis.com/token", json={"access_token": "access_token", "expires_in": 3600, "token_type": "Bearer"}
)
error_response = {"error": {"message":"Exhausted potentially thresholded requests quota. This quota will refresh in under an hour. To learn more, see"}}
requests_mock.register_uri(
"GET",
"https://analyticsdata.googleapis.com/v1beta/properties/UA-11111111/metadata",
# first try we get 429 t=~0
[{"json": error_response, "status_code": 429},
# first retry we get 429 t=~1800
{"json": error_response, "status_code": 429},
# second retry quota is recovered, t=~3600
{"json": {
"dimensions": [{"apiName": "date"}, {"apiName": "country"}, {"apiName": "language"}, {"apiName": "browser"}],
"metrics": [{"apiName": "totalUsers"}, {"apiName": "screenPageViews"}, {"apiName": "sessions"}],
}, "status_code": 200}
]
)
def fix_time(time):
return int(time / 600 )
source = SourceGoogleAnalyticsDataApi()
logger = MagicMock()
max_time_fixed = fix_time(GoogleAnalyticsDatApiErrorHandler.QUOTA_RECOVERY_TIME)
potentially_thresholded_requests_per_hour_mapping = GoogleAnalyticsApiQuotaBase.quota_mapping["potentiallyThresholdedRequestsPerHour"]
threshold_backoff_time = potentially_thresholded_requests_per_hour_mapping["backoff"]
fixed_threshold_backoff_time = fix_time(threshold_backoff_time)
potentially_thresholded_requests_per_hour_mapping_fixed = {
**potentially_thresholded_requests_per_hour_mapping,
"backoff": fixed_threshold_backoff_time,
}
with (
patch.object(GoogleAnalyticsDatApiErrorHandler, 'QUOTA_RECOVERY_TIME', new=max_time_fixed),
patch.object(GoogleAnalyticsApiQuotaBase, 'quota_mapping', new={**GoogleAnalyticsApiQuotaBase.quota_mapping,"potentiallyThresholdedRequestsPerHour": potentially_thresholded_requests_per_hour_mapping_fixed})):
output = source.check(logger, config_gen(property_ids=["UA-11111111"]))
assert output == AirbyteConnectionStatus(status=Status.SUCCEEDED, message=None)


@pytest.mark.parametrize("error_code", (402, 404, 405))
def test_check_failure(requests_mock, config_gen, error_code):
requests_mock.register_uri(
"POST", "https://oauth2.googleapis.com/token", json={"access_token": "access_token", "expires_in": 3600, "token_type": "Bearer"}
Expand Down
3 changes: 2 additions & 1 deletion docs/integrations/sources/google-analytics-data-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ The Google Analytics connector is subject to Google Analytics Data API quotas. P
<summary>Expand to review</summary>

| Version | Date | Pull Request | Subject |
|:--------|:-----------| :------------------------------------------------------- |:---------------------------------------------------------------------------------------|
|:--------|:-----------|:---------------------------------------------------------|:---------------------------------------------------------------------------------------|
| 2.5.2 | 2024-08-12 | [43929](https://github.com/airbytehq/airbyte/pull/43929) | Increase streams max_time to backoff |
| 2.5.1 | 2024-08-10 | [43289](https://github.com/airbytehq/airbyte/pull/43289) | Update dependencies |
| 2.5.0 | 2024-08-07 | [42841](https://github.com/airbytehq/airbyte/pull/42841) | Upgrade to CDK 3 |
| 2.4.14 | 2024-07-27 | [42746](https://github.com/airbytehq/airbyte/pull/42746) | Update dependencies |
Expand Down
Loading