bottleneckcrm/backend/common/signup_source.py

41 lines
1.6 KiB
Python

"""What an org's creation records about where it came from.
The web app keeps the first visit's campaign tags and referring host in a
cookie and sends them with the create; the API cleans them here and adds which
client created the org. A bad value is dropped rather than refused: this is
analytics, and it must never stop an org from being created.
"""
import re
TAG_KEYS = ("utm_source", "utm_medium", "utm_campaign")
MAX_TAG_LENGTH = 100
_HOST = re.compile(
r"^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$"
)
# The Flutter app's user agent. Web creates go through the SvelteKit server.
PHONE_USER_AGENT_PREFIX = "Dart/"
def clean_signup_source(raw, user_agent):
"""Return the dict to store on `Org.signup_source`.
Always has `client` ("phone" or "web"); adds each `utm_*` tag that is a
non-blank string (printable characters only, at most MAX_TAG_LENGTH) and
`referrer` when it is a bare host name. Anything else in `raw` is ignored.
"""
phone = (user_agent or "").startswith(PHONE_USER_AGENT_PREFIX)
source = {"client": "phone" if phone else "web"}
if not isinstance(raw, dict):
return source
for key in TAG_KEYS:
value = raw.get(key)
if isinstance(value, str):
value = "".join(ch for ch in value if ch.isprintable()).strip()
if value:
source[key] = value[:MAX_TAG_LENGTH]
referrer = raw.get("referrer")
if isinstance(referrer, str) and _HOST.match(referrer.strip().lower()):
source["referrer"] = referrer.strip().lower()
return source