Skip to content

Commit fe29826

Browse files
committed
perf(clicks): omit None fields from time-series inserts
Explicit nulls flip a bucket field's BSON type and force the bucket to close early, fragmenting the collection into a few measurements per bucket. Absent fields pack fine and queries are unaffected: MongoDB treats missing like null in $eq/$group and the stats sentinels already map both.
1 parent 6633971 commit fe29826

3 files changed

Lines changed: 36 additions & 7 deletions

File tree

schemas/models/click.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,14 @@ class ClickDoc(MongoBaseModel):
6060
utm_source: str | None = None
6161
utm_medium: str | None = None
6262
utm_campaign: str | None = None
63+
64+
def to_mongo(self) -> dict:
65+
"""Serialise for insertion, omitting None fields entirely.
66+
67+
Time-series buckets track one BSON type per field. An explicit
68+
null following a string value (or vice versa) is a type conflict
69+
that closes the bucket early, while an absent field packs fine.
70+
Queries are unaffected: MongoDB treats missing and null the same
71+
in $eq/$group, and the stats sentinels already map both.
72+
"""
73+
return self.model_dump(by_alias=True, exclude_none=True)

tests/unit/schemas/models/test_click.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,19 @@ def test_to_mongo_round_trip(self):
5050
restored = ClickDoc.from_mongo(doc.to_mongo())
5151
assert restored.referrer == "google.com"
5252
assert restored.redirect_ms == doc.redirect_ms
53+
54+
def test_to_mongo_omits_none_fields(self):
55+
# Explicit nulls flip the per-field BSON type and close time-series
56+
# buckets; None fields must be absent from the insert document.
57+
data = self._make(referrer=None, bot_name=None).to_mongo()
58+
assert "referrer" not in data
59+
assert "bot_name" not in data
60+
assert "device" not in data
61+
assert "utm_source" not in data
62+
assert "domain" not in data["meta"]
63+
64+
def test_to_mongo_keeps_set_fields(self):
65+
data = self._make(referrer="google.com", device="mobile").to_mongo()
66+
assert data["referrer"] == "google.com"
67+
assert data["device"] == "mobile"
68+
assert data["country"] == "Unknown"

tests/unit/services/test_click_service.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,11 @@ async def test_utm_tags_default_to_none(self):
303303

304304
await handler.handle(make_context(url_data))
305305

306+
# Absent, not null: explicit nulls close time-series buckets.
306307
doc = d.click_repo.insert.call_args[0][0]
307-
assert doc["utm_source"] is None
308-
assert doc["utm_medium"] is None
309-
assert doc["utm_campaign"] is None
308+
assert "utm_source" not in doc
309+
assert "utm_medium" not in doc
310+
assert "utm_campaign" not in doc
310311

311312
@pytest.mark.asyncio
312313
async def test_meta_carries_domain_from_cache(self):
@@ -320,17 +321,18 @@ async def test_meta_carries_domain_from_cache(self):
320321
assert doc["meta"]["domain"] == "links.acme.com"
321322

322323
@pytest.mark.asyncio
323-
async def test_meta_domain_none_when_cache_empty_string(self):
324-
# Older cached entries pre-PR1 have domain="". Coerce to None so
325-
# per-domain queries can distinguish "unknown" from a real value.
324+
async def test_meta_domain_omitted_when_cache_empty_string(self):
325+
# Older cached entries pre-PR1 have domain="". Coerced to None and
326+
# then omitted from the insert doc ($eq: null matches missing, so
327+
# per-domain queries still distinguish "unknown" from a real value).
326328
d = make_deps()
327329
handler = make_v2_handler(d.click_repo, d.url_repo, d.geoip, d.url_cache)
328330
url_data = make_v2_cache(domain="")
329331

330332
await handler.handle(make_context(url_data))
331333

332334
doc = d.click_repo.insert.call_args[0][0]
333-
assert doc["meta"]["domain"] is None
335+
assert "domain" not in doc["meta"]
334336

335337
@pytest.mark.asyncio
336338
async def test_blocked_bot_skips_analytics_no_error(self):

0 commit comments

Comments
 (0)