Skip to content

Commit

Permalink
EU workspace, api_key for authentication, render workflow, refactor t…
Browse files Browse the repository at this point in the history
…racking...
  • Loading branch information
jasonbryant84 committed Nov 18, 2024
1 parent 114e526 commit 94002e1
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 58 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# .github/workflows/deploy.yml
name: Deploy to Render

on:
push:
branches:
- master

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v2

- name: Install dependencies
run: |
python -m venv env
source env/bin/activate
pip install -r requirements.txt
- name: Deploy to Render
env:
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
run: |
render deploy service
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ Start the server with
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

Ngrok

To test with webhooks, etc which will forward to localhost and handle https requests.
```
ngrok http 8000
```

### Environment Variables (.env)

```
Expand Down
4 changes: 2 additions & 2 deletions app/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ def __init__(self):
tz: str = None # type: ignore
log_level: logging = logging.DEBUG
env: Environments = Environments.LOCAL
api_title: str = "Qdrant Analytics Api"
api_title: str = "Qdrant Analytics API"
base_domain: str = "localhost:8000"
segment_write_key: str = "iyIg4FyUqxwJysbSBwufFavC9u9QMNlT"
segment_write_key: str = "q4fET8II4UoGX7NWh8lVOQWu79pLvz7d"


config = {
Expand Down
2 changes: 1 addition & 1 deletion app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
API_AUTHENTICATION_KEY = os.getenv("API_AUTHENTICATION_KEY")


def authenticate(x_api_key: Optional[str] = Header(None)) -> None:
def authenticate(x_api_key: Optional[str] = Header(None, alias="x_api_key")) -> None:
if x_api_key != API_AUTHENTICATION_KEY:
raise HTTPException(status_code=401, detail="Unauthorized")
28 changes: 14 additions & 14 deletions app/routers/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from fastapi.responses import JSONResponse
from qdrant_analytics_events.SegmentIdentify import Model as SegmentIdentify
from qdrant_analytics_events.SegmentPage import Model as SegmentPage
from qdrant_analytics_events.SegmentTrack import Model as SegmentTrack

from app.config import get_app_config
from app.constants import NON_CONSENTED_USER_ID, QDRANT_ANONYMOUS_ID_KEY
Expand All @@ -22,7 +21,7 @@


@router.get("/healthcheck")
async def healthcheck(_: Request):
async def healthcheck(request: Request):
return {"message": "healthcheck successful"}


Expand Down Expand Up @@ -72,7 +71,7 @@ async def identify(

return {"message": "User identified successfully"}
except Exception as error:
logger.warning(f"Error (calling track_event): {error}")
logger.warning(f"Error (calling identify): {error}")
return {"Error": error}


Expand All @@ -84,20 +83,21 @@ async def track_event(
_: str = Depends(authenticate),
):
try:
user_id, anonymous_id, args, data = await create_args(request)

segment_service.track_event(
SegmentTrack(
user_id=user_id,
anonymous_id=anonymous_id,
event_name=data.get("event"),
**args,
)
data = await request.json()
args = await create_args(request, data)

success = segment_service.track_event(args)

return (
{"message": "Event tracked successfully"}
if success
else {
"error": f"Event track failed. Check logs for event: {args['event_name']}"
}
)

return {"message": "Event tracked successfully"}
except Exception as error:
logger.warning(f"Error (calling track_event): {error}")
logger.warning(f"Error (calling /track): {error}")
return {"Error": error}


Expand Down
67 changes: 35 additions & 32 deletions app/segment/service.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import logging
from datetime import datetime
from typing import Literal, TypeVar
from typing import Literal, TypeVar, Union

import segment.analytics as analytics
from fastapi import BackgroundTasks
from qdrant_analytics_events.index import QdrantAnalyticsEvents
from qdrant_analytics_events.Interaction import Model as InteractionEvent
from qdrant_analytics_events.SegmentIdentify import Model as SegmentIdentify
from qdrant_analytics_events.SegmentPage import Model as SegmentPage
from qdrant_analytics_events.SegmentTrack import Model as SegmentTrack

from app.constants import NON_CONSENTED_USER_ID
from app.utils import date_format

any_model = InteractionEvent(location="", label="")
EventType = Union[type(QdrantAnalyticsEvents(any_model).root)] # type: ignore - Type[] doesn't do it, but type() works!

logger = logging.getLogger(__name__)

Expand All @@ -28,8 +34,8 @@ def __init__(
write_key: str,
background_tasks: BackgroundTasks,
source_name: Literal[
"default", "cluster_api", "cloud_ui", "marketing"
] = "default",
"default", "server_side_analytics"
] = "server_side_analytics",
send: bool = True,
):
"""
Expand All @@ -41,12 +47,14 @@ def __init__(
"""
self.analytics = analytics.Client(
write_key=write_key,
debug=False,
sync_mode=True,
max_retries=2,
timeout=3,
upload_size=1, # 1 event per request, to avoid batching
send=send,
on_error=self._on_error,
host="https://events.eu1.segmentapis.com",
)
self.background_tasks = background_tasks
self.source_name = source_name
Expand Down Expand Up @@ -97,7 +105,7 @@ def _identify(

try:
event_props["timestamp"] = (
datetime.strptime(event_props["timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ")
datetime.strptime(event_props["timestamp"], date_format)
if event_props["timestamp"]
else None
)
Expand Down Expand Up @@ -157,7 +165,7 @@ def _page_viewed(

try:
event_props["timestamp"] = (
datetime.strptime(event_props["timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ")
datetime.strptime(event_props["timestamp"], date_format)
if event_props["timestamp"]
else None
)
Expand All @@ -178,59 +186,54 @@ def _page_viewed(
#########
# Track #
#########
def track_event(self, event: TSegmentTrack) -> None: # type: ignore
def track_event(self, event_payload: dict) -> bool | None: # type: ignore
"""
Track event in Segment in a background task, after the response is sent.
:param str user_id: User id
:param TSegmentTrack event: SegmentTrack event model
:return: None
"""

event = SegmentTrack.model_validate(event)
event_props = event.model_dump(mode="json")
user_id = event_props.pop("user_id", None)
anonymous_id = event_props.pop("anonymous_id", NON_CONSENTED_USER_ID)
event_name = event_props.pop("event_name", None)
try:
event = SegmentTrack.model_validate(event_payload)
event_props = event.model_dump(mode="json")

if (user_id is None and anonymous_id is None) or event_name is None:
raise ValueError(
"`user_id` (or `anonymous_id`) and `event_name` are required"
)
if event_props["user_id"] is None and event_props["anonymous_id"] is None:
raise ValueError(
"`user_id` (or `anonymous_id`) and `event_name` are required"
)

self.background_tasks.add_task(
self._track_event, user_id, anonymous_id, event_name, event_props
)
self.background_tasks.add_task(self._track_event, event_props)

def _track_event(
self,
user_id: str | None,
anonymous_id: str | None,
event_name: str,
event_props: dict,
) -> None:
except Exception as error:
logger.warning(f"Error (calling track_event): {error}")
return False

def _track_event(self, event_props: dict) -> None:
"""
Track event in Segment.
If an exception occurs, the background task will be dropped
and the event will not be tracked.
:param str user_id: User id
:param str anonymous_id: Anonymous id (set to "not_consented" if user hasn't opted in)
:param str event_name: Event name
:param dict event_props: { properties, context, integrations, timestamp }
:param dict event_props: { event_name, user_id, anonymous_id, properties, context, integrations, timestamp }
:return: None
"""

try:
event_props["timestamp"] = (
datetime.strptime(event_props["timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ")
datetime.strptime(event_props["timestamp"], date_format)
if event_props["timestamp"]
else None
)

event = event_props.pop("event_name")
user_id = event_props.pop("user_id")
anonymous_id = event_props.pop("anonymous_id")

success, msg = self.analytics.track(
user_id=user_id,
anonymous_id=anonymous_id,
event=event_name,
event=event,
**event_props,
)

Expand Down
20 changes: 12 additions & 8 deletions app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

load_dotenv()

date_format = "%Y-%m-%dT%H:%M:%S.%fZ"


def now_tz() -> datetime:
"""
Expand All @@ -33,8 +35,8 @@ def get_allowed_origins():


def get_ids(data: dict) -> Tuple[str, str]:
user_id = data.get("userId", None) if "userId" in data else None
anonymous_id = data.get("anonymousId", None) or NON_CONSENTED_USER_ID
user_id = data.get("user_id", None) if "user_id" in data else None
anonymous_id = data.get("anonymous_id", None) or NON_CONSENTED_USER_ID

return user_id, anonymous_id

Expand All @@ -53,7 +55,7 @@ def format_properties(request: Request, data: dict) -> dict:
properties["path"] = path
properties["search"] = search

properties["referrer"] = request.headers.get("referer")
properties["referrer"] = request.headers.get("referer", None)
properties["originalTimestamp"] = data.get("originalTimestamp", None)

if data.get("name"): # Page events have a name property
Expand Down Expand Up @@ -87,14 +89,16 @@ def format_context(


async def create_args(
request: Request, exclude_properties: bool = False
) -> Tuple[str, str, dict]:
data = await request.json()
request: Request, data: dict, exclude_properties: bool = False
) -> dict:
[user_id, anonymous_id] = get_ids(data)

args = {
"event_name": data.get("event_name", "no event name"),
"user_id": user_id,
"anonymous_id": anonymous_id,
"integrations": data.get("integrations", {}),
"timestamp": data.get("originalTimestamp", None),
"timestamp": data.get("originalTimestamp", utc_now().strftime(date_format)),
}

properties = format_properties(request, data)
Expand All @@ -104,4 +108,4 @@ async def create_args(

args["context"] = format_context(request, data, properties, anonymous_id)

return user_id, anonymous_id, args, data
return args
Binary file removed pypi/qdrant_analytics_events-1.0.13.tar.gz
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ segment-analytics-python
python-dotenv
starlette
envclasses
./pypi/qdrant_analytics_events-1.0.13.tar.gz
./pypi/qdrant_analytics_events-1.0.14.pre-release.tar.gz

0 comments on commit 94002e1

Please sign in to comment.