Skip to content

Commit

Permalink
fix: update deprecated SDK references and remove unused resources (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
wax911 authored May 14, 2024
1 parent aca26cb commit 84cfff9
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 133 deletions.
1 change: 0 additions & 1 deletion .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ GROWTH_BOOK_HOST=GROWTH_BOOK_HOST
GROWTH_BOOK_KEY=GROWTH_BOOK_KEY
GROWTH_BOOK_TTL=600
EDGE_HOST=http://localhost:8888
ROLLBAR_TOKEN=ROLLBAR_TOKEN
LOGTAIL_SOURCE_TOKEN=LOGTAIL_SOURCE_TOKEN
DJANGO_Q_NAME=queue-service
DJANGO_Q_ORM=default
Expand Down
18 changes: 2 additions & 16 deletions app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ def __get_base_dir() -> str:
"corsheaders.middleware.CorsMiddleware",
"core.middleware.HeaderMiddleware",
"core.middleware.FeatureFlagMiddleware",
'core.middleware.CustomRollbarNotifierMiddleware',
]

ROOT_URLCONF = "app.urls"
Expand Down Expand Up @@ -184,15 +183,9 @@ def __get_base_dir() -> str:
"class": "logging.StreamHandler",
"formatter": "standard",
},
"rollbar": {
"level": "WARNING",
"filters": ["require_debug_false"],
"access_token": config('ROLLBAR_TOKEN', cast=str),
"environment": "development" if DEBUG else "production",
"class": "rollbar.logger.RollbarHandler"
},
"logtail": {
"level": "INFO",
"filters": ["require_debug_false"],
"class": "logtail.LogtailHandler",
"formatter": "simple",
"source_token": config("LOGTAIL_SOURCE_TOKEN", cast=str)
Expand All @@ -204,7 +197,7 @@ def __get_base_dir() -> str:
"propagate": True,
},
"root": {
"handlers": ["rollbar", "logtail"],
"handlers": ["logtail"],
"propagate": True,
},
},
Expand All @@ -216,13 +209,6 @@ def __get_base_dir() -> str:
"ttl": config("GROWTH_BOOK_TTL", cast=int),
}

ROLLBAR = {
'access_token': config("ROLLBAR_TOKEN", cast=str),
'environment': 'development' if DEBUG else 'production',
'code_version': '1.0',
'root': BASE_DIR,
}

ON_THE_EDGE = {
"host": config("EDGE_HOST", cast=str)
}
Expand Down
10 changes: 3 additions & 7 deletions config/data/repositories.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
from json import JSONDecodeError
from typing import Optional, Any

from uplink import Consumer

from core.errors import NoDataError
from core.repositories import DataRepository
from .schemas import ConfigurationSchema
from ..data.sources import RemoteSource
from ..domain.entities import (ConfigurationModel, SettingsModel, ImageModel, NavigationModel,
NavigationGroupModel, GenreModel)
from pyxtension.streams import stream
from ..domain.entities import (ConfigurationModel)


class Repository(DataRepository):
Expand All @@ -20,7 +15,8 @@ def __init__(self, remote_source: Consumer) -> None:

def invoke(self, **kwargs) -> ConfigurationModel:
try:
data = self._remote_source.get_config()
headers = kwargs.get('headers')
data = self._remote_source.get_config(headers)
return data
except JSONDecodeError as e:
self._logger.error(f"Malformed response with error message `{e.doc}`", exc_info=e)
Expand Down
2 changes: 0 additions & 2 deletions config/data/schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Any

from marshmallow import fields, post_load

from config.domain.entities import ConfigurationModel
Expand Down
10 changes: 5 additions & 5 deletions config/data/sources.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from marshmallow import EXCLUDE
from uplink import get, timeout, retry, ratelimit, Consumer
from uplink import get, timeout, retry, ratelimit, Consumer, HeaderMap

from core.decorators import raise_api_error
from core import __TIME_OUT__, __MAX_ATTEMPTS__, __RATE_LIMIT_CALLS__, __RATE_LIMIT_PERIOD_CALLS__
from ..data.schemas import ConfigurationSchema


@timeout(seconds=__TIME_OUT__)
@timeout(seconds=5)
@retry(
max_attempts=__MAX_ATTEMPTS__,
max_attempts=3,
when=retry.when.raises(Exception),
stop=retry.stop.after_attempt(__MAX_ATTEMPTS__) | retry.stop.after_delay(__RATE_LIMIT_PERIOD_CALLS__),
stop=retry.stop.after_attempt(3) | retry.stop.after_delay(2),
backoff=retry.backoff.jittered(multiplier=0.5)
)
@ratelimit(
Expand All @@ -21,7 +21,7 @@ class RemoteSource(Consumer):

@raise_api_error
@get("config")
def get_config(self) -> ConfigurationSchema(unknown=EXCLUDE):
def get_config(self, headers: HeaderMap) -> ConfigurationSchema(unknown=EXCLUDE):
"""
:return: ConfigurationSchema
"""
Expand Down
6 changes: 3 additions & 3 deletions config/domain/usecases.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Any, Optional
from typing import Optional, Mapping

from .entities import ConfigurationModel
from core.usecases import CommonUseCase


class ConfigUseCase(CommonUseCase):

def fetch_configuration(self) -> Optional[ConfigurationModel]:
def fetch_configuration(self, headers: Mapping) -> Optional[ConfigurationModel]:
try:
data = self._repository.invoke()
data = self._repository.invoke(headers=headers)
return data
except Exception as e:
self._logger.error(f"Uncaught exception", exc_info=e)
Expand Down
7 changes: 4 additions & 3 deletions config/graphql/resolvers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Optional
from typing import Optional, Dict

# noinspection PyPackageRequirements
from graphql import GraphQLResolveInfo

from core.utils import get_forwarded_headers
from ..di.containers import UseCaseContainer
from ..domain.entities import ConfigurationModel
from ..domain.usecases import ConfigUseCase
Expand All @@ -19,6 +20,6 @@ def resolve_config(
:return: Instance of Config
"""
use_case: ConfigUseCase = use_case_provider()
enabled_analytics = info.context.feature.isOn("enable-analytics")
result = use_case.fetch_configuration()
forwarded_headers = get_forwarded_headers(info.context)
result = use_case.fetch_configuration(forwarded_headers)
return result
46 changes: 7 additions & 39 deletions core/middleware.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import logging
from typing import Optional, Dict

from django.http import HttpRequest, JsonResponse, HttpResponse
from django.utils.deprecation import MiddlewareMixin
from growthbook import GrowthBook
from rollbar.contrib.django.middleware import RollbarNotifierMiddleware

from .mixin import GrowthBookMixin, LoggerMixin
from .models import ContextHeader, Application
from .utils import UAParser


class FeatureFlagMiddleware(MiddlewareMixin, GrowthBookMixin):
Expand All @@ -32,63 +32,31 @@ def process_request(
request: HttpRequest,
) -> Optional[HttpResponse]:
headers = request.META
ua_parser = UAParser(user_agent=headers.get('HTTP_USER_AGENT'))

request.context_header = ContextHeader(
authorization=headers.get('HTTP_AUTHORIZATION'),
accepts=headers.get('HTTP_ACCEPT'),
agent=headers.get('HTTP_USER_AGENT'),
contentType=headers.get('CONTENT_TYPE'),
acceptEncoding=headers.get('HTTP_ACCEPT_ENCODING'),
language=headers.get('HTTP_ACCEPT_LANGUAGE'),
content_type=headers.get('CONTENT_TYPE'),
accept_encoding=headers.get('HTTP_ACCEPT_ENCODING'),
application=Application(
locale=headers.get('HTTP_X_APP_LOCALE'),
version=headers.get('HTTP_X_APP_VERSION'),
source=headers.get('HTTP_X_APP_SOURCE'),
code=headers.get('HTTP_X_APP_CODE'),
label=headers.get('HTTP_X_APP_NAME'),
buildType=headers.get('HTTP_X_APP_BUILD_TYPE'),
)
),
user_agent_info=ua_parser.get_result(),
)

enforced = [
'HTTP_HOST',
'HTTP_ACCEPT',
'HTTP_ACCEPT_ENCODING',
'HTTP_ACCEPT_LANGUAGE',
'HTTP_USER_AGENT',
]

for header in enforced:
if header not in headers:
return self.__fail(header)


class CustomRollbarNotifierMiddleware(RollbarNotifierMiddleware):

def get_extra_data(self, request, exc) -> Dict:
feature: GrowthBook = request.feature
context: ContextHeader = request.context_header

# Example of adding arbitrary metadata (optional)
extra_data: Dict = {
'user_agent': context.agent,
'feature_flags': feature.get_features()
}

return extra_data

def get_payload_data(self, request, exc) -> Dict:
payload_data: Dict = dict()

if not request.user.is_anonymous:
# Adding info about the user affected by this event (optional)
# The 'id' field is required, anything else is optional
payload_data = {
'person': {
'id': request.user.id,
'username': request.user.username,
'email': request.user.email,
},
}

return payload_data
48 changes: 40 additions & 8 deletions core/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from typing import NoReturn

from dependency_injector.wiring import Provide, inject
from django.http import HttpRequest
from django.http import HttpRequest, HttpResponse
from growthbook import GrowthBook

from core.models import ContextHeader
from di import CoreContainer


Expand All @@ -14,16 +15,47 @@ def __init__(self) -> None:
self._logger = logging.getLogger('django')


class GrowthBookMixin:
class GrowthBookMixin(LoggerMixin):

@inject
def __init__(self, growth_book=Provide[CoreContainer.growth_book]) -> None:
super().__init__()
self.__growth_book: GrowthBook = growth_book

def process_request(
self,
request: HttpRequest,
) -> NoReturn:
def set_request_attributes(self, context_header: ContextHeader):
user_agent_info = context_header.user_agent_info
application = context_header.application
self.__growth_book.set_attributes({
'app_locale': application.locale,
'app_version': application.version,
'app_source': application.source,
'app_code': application.code,
'app_name': application.label,
'app_build_type': application.buildType,
'browser_name': user_agent_info.user_agent.family,
'browser_version': user_agent_info.user_agent.version,
'cpu_architecture': user_agent_info.cpu.architecture,
'device_model': user_agent_info.device.model,
'device_vendor': user_agent_info.device.brand,
'device_type': user_agent_info.device.family,
'engine_name': user_agent_info.engine.family,
'engine_version': user_agent_info.engine.version,
'os_name': user_agent_info.os.family,
'os_version': user_agent_info.os.version,
})

def process_request(self, request: HttpRequest) -> NoReturn:
self._logger.info('loading growth_book features')
self.__growth_book.load_features()
if hasattr(request, 'context_header'):
self.set_request_attributes(
context_header=request.context_header
)
else:
self._logger.warning('context_header is not defined in the current request context')
request.feature = self.__growth_book
request.feature.load_features()
request.feature.destroy()

def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
self._logger.info('destroying growth_book instance')
self.__growth_book.destroy()
return response
57 changes: 47 additions & 10 deletions core/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Optional

from django.db import models
Expand All @@ -12,13 +12,51 @@ class Meta:
abstract = True


@dataclass
class UserAgent:
family: Optional[str] = None
major: Optional[str] = None
minor: Optional[str] = None
patch: Optional[str] = None

@property
def version(self):
return f'{self.major}{self.minor}{self.patch}'


@dataclass
class CPU:
architecture: Optional[str] = None


@dataclass
class Device:
browser: Optional[str]
cpu: Optional[str]
device: Optional[str]
engine: Optional[str]
os: Optional[str]
family: Optional[str] = None
brand: Optional[str] = None
model: Optional[str] = None


@dataclass
class OS:
family: Optional[str] = None
major: Optional[str] = None
minor: Optional[str] = None
patch: Optional[str] = None
patch_minor: Optional[str] = None

@property
def version(self):
return f'{self.major}{self.minor}{self.patch}${self.patch_minor}'


@dataclass
class UserAgentInfo:
raw: str
user_agent: UserAgent = field(default_factory=UserAgent)
cpu: CPU = field(default_factory=CPU)
device: Device = field(default_factory=Device)
engine: UserAgent = field(default_factory=UserAgent)
os: OS = field(default_factory=OS)


@dataclass
Expand All @@ -35,8 +73,7 @@ class Application:
class ContextHeader:
authorization: Optional[str]
accepts: Optional[str]
agent: str
contentType: Optional[str]
acceptEncoding: Optional[str]
language: Optional[str]
content_type: Optional[str]
accept_encoding: Optional[str]
application: Application
user_agent_info: UserAgentInfo
Loading

0 comments on commit 84cfff9

Please sign in to comment.