Skip to content

Commit

Permalink
chore: disable FeatureFlagMiddleware and update user-agent parser
Browse files Browse the repository at this point in the history
  • Loading branch information
wax911 committed May 12, 2024
1 parent 1082d9a commit 89de54a
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 34 deletions.
2 changes: 1 addition & 1 deletion app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def __get_base_dir() -> str:
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware",
"core.middleware.HeaderMiddleware",
"core.middleware.FeatureFlagMiddleware",
# "core.middleware.FeatureFlagMiddleware",
'core.middleware.CustomRollbarNotifierMiddleware',
]

Expand Down
10 changes: 6 additions & 4 deletions core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

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


class FeatureFlagMiddleware(MiddlewareMixin, GrowthBookMixin):
Expand All @@ -32,13 +33,13 @@ 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'),
content_type=headers.get('CONTENT_TYPE'),
accept_encoding=headers.get('HTTP_ACCEPT_ENCODING'),
language=headers.get('HTTP_ACCEPT_LANGUAGE'),
application=Application(
locale=headers.get('HTTP_X_APP_LOCALE'),
Expand All @@ -47,7 +48,8 @@ def process_request(
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 = [
Expand Down
2 changes: 2 additions & 0 deletions core/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ def process_request(
) -> NoReturn:
request.feature = self.__growth_book
request.feature.load_features()
request.feature.destroy()

48 changes: 39 additions & 9 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,43 @@ class Meta:
abstract = True


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


@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


@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 +65,8 @@ class Application:
class ContextHeader:
authorization: Optional[str]
accepts: Optional[str]
agent: str
contentType: Optional[str]
acceptEncoding: Optional[str]
content_type: Optional[str]
accept_encoding: Optional[str]
language: Optional[str]
application: Application
user_agent_info: UserAgentInfo
32 changes: 21 additions & 11 deletions core/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def test_process_request_with_headers(self):
headers = {
'HTTP_AUTHORIZATION': 'Bearer token',
'HTTP_ACCEPT': 'application/json',
'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
'HTTP_USER_AGENT': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, '
'like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
'CONTENT_TYPE': 'application/json',
'HTTP_ACCEPT_ENCODING': 'gzip, deflate',
'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.9',
Expand All @@ -87,16 +87,26 @@ def test_process_request_with_headers(self):

self.assertEqual(context_header.authorization, 'Bearer token')
self.assertEqual(context_header.accepts, 'application/json')
self.assertEqual(context_header.agent, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
' (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3')
self.assertEqual(context_header.contentType, 'application/json')
self.assertEqual(context_header.acceptEncoding, 'gzip, deflate')
self.assertEqual(context_header.user_agent_info.raw, 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0'
' Mobile Safari/537.36')
self.assertEqual(context_header.content_type, 'application/json')
self.assertEqual(context_header.accept_encoding, 'gzip, deflate')
self.assertEqual(context_header.language, 'en-US,en;q=0.9')
# self.assertEqual(context_header.device.browser, 'Chrome')
# self.assertEqual(context_header.device.cpu, None)
# self.assertEqual(context_header.device.device, None)
# self.assertEqual(context_header.device.engine, 'Chrome')
# self.assertEqual(context_header.device.os, 'Windows NT 10.0')
self.assertEqual(context_header.user_agent_info.user_agent.family, 'Chrome Mobile')
self.assertEqual(context_header.user_agent_info.user_agent.major, '124')
self.assertEqual(context_header.user_agent_info.user_agent.minor, '0')
self.assertEqual(context_header.user_agent_info.user_agent.patch, '0')
self.assertIsNone(context_header.user_agent_info.cpu.architecture)
self.assertEqual(context_header.user_agent_info.device.family, 'Nexus 5')
self.assertEqual(context_header.user_agent_info.device.brand, 'LG')
self.assertEqual(context_header.user_agent_info.device.model, 'Nexus 5')
self.assertIsNone(context_header.user_agent_info.engine.family)
self.assertEqual(context_header.user_agent_info.os.family, 'Android')
self.assertEqual(context_header.user_agent_info.os.major, '6')
self.assertEqual(context_header.user_agent_info.os.minor, '0')
self.assertIsNone(context_header.user_agent_info.os.patch)
self.assertIsNone(context_header.user_agent_info.os.patch_minor)
self.assertEqual(context_header.application.locale, 'en-US')
self.assertEqual(context_header.application.version, '1.0')
self.assertEqual(context_header.application.source, 'web')
Expand Down
40 changes: 31 additions & 9 deletions core/utils.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
from typing import Optional
from typing import Optional, Dict, Any

from ua_parser import user_agent_parser

from .models import Device
from .models import UserAgentInfo, UserAgent, CPU, Device, OS


def safe_get(dictionary: Dict[str, any], keys: str, default: Optional[any] = None) -> any:
"""
Safely get a nested value from a dictionary.
Args:
dictionary (Dict[str, any]): The dictionary to extract the value from.
keys (str): A string representing the keys separated by dots.
default (Optional[any], optional): Default value to return if the keys are not found. Defaults to None.
Returns:
any: The value found at the specified keys, or the default value if not found.
"""
keys_list = keys.split('.')
for key in keys_list:
if isinstance(dictionary, dict) and key in dictionary:
dictionary = dictionary[key]
else:
return default if default is not None else {}
return dictionary


class UAParser:
def __init__(self, user_agent: Optional[str] = None):
self.ua = user_agent_parser.Parse(user_agent or '')

def get_result(self) -> Device:
return Device(
browser=self.ua.get('user_agent', {}).get('family'),
cpu=self.ua('cpu', {}).get('architecture'),
device=self.ua('device', {}).get('family'),
engine=self.ua('user_agent', {}).get('family'),
os=self.ua('os', {}).get('family'),
def get_result(self) -> UserAgentInfo:
return UserAgentInfo(
raw=safe_get(self.ua, 'string'),
user_agent=UserAgent(**safe_get(self.ua, 'user_agent')),
cpu=CPU(**safe_get(self.ua, 'cpu')),
device=Device(**safe_get(self.ua, 'device')),
engine=UserAgent(**safe_get(self.ua, 'engine')),
os=OS(**safe_get(self.ua, 'os'))
)

0 comments on commit 89de54a

Please sign in to comment.