Feature/browser activity classifier#1354
Conversation
| classified_event = { | ||
| "timestamp": event.timestamp, | ||
| "duration": event.duration.total_seconds() if event.duration else 0, | ||
| "data": {"url": url, "title": title, "category": category}, | ||
| } | ||
| client.heartbeat( | ||
| output_bucket, classified_event, | ||
| pulsetime=POLL_INTERVAL_SECONDS + 5, queued=True, | ||
| ) |
There was a problem hiding this comment.
heartbeat receives a plain dict instead of an Event object
The official aw-client API signature is heartbeat(bucket_id: str, event: Event, pulsetime: float, ...) where event must be an aw_core.models.Event instance. Passing a plain dict here will raise an AttributeError at runtime when the client internally accesses attributes like .timestamp and .data on the object.
| while True: | ||
| since = datetime.now(timezone.utc) - timedelta(seconds=LOOKBACK_SECONDS) | ||
| recent_events = client.get_events(source_bucket, start=since, limit=50) | ||
|
|
||
| for event in recent_events: | ||
| url = event.data.get("url", "") | ||
| title = event.data.get("title", "") | ||
| category = classify_url(url) | ||
|
|
||
| classified_event = { | ||
| "timestamp": event.timestamp, | ||
| "duration": event.duration.total_seconds() if event.duration else 0, | ||
| "data": {"url": url, "title": title, "category": category}, | ||
| } | ||
| client.heartbeat( | ||
| output_bucket, classified_event, | ||
| pulsetime=POLL_INTERVAL_SECONDS + 5, queued=True, | ||
| ) | ||
| log.info("Classified [%s] -> %s", category, url or title) | ||
|
|
||
| time.sleep(POLL_INTERVAL_SECONDS) |
There was a problem hiding this comment.
Overlapping lookback window causes duplicate events in the output bucket
LOOKBACK_SECONDS = 30 but the loop sleeps only POLL_INTERVAL_SECONDS = 10 seconds between runs. Each poll fetches the last 30 s of source events, so roughly 20 s worth of events from the previous cycle are re-fetched and re-heartbeated every iteration — meaning each source event is written ~3 times to the output bucket. There is no deduplication guard (e.g., tracking the last-processed timestamp or event ID).
| for category, domains in CATEGORY_RULES.items(): | ||
| if any(known_domain in domain for known_domain in domains): | ||
| return category |
There was a problem hiding this comment.
Substring domain check causes false positives
known_domain in domain checks whether the rule string is a substring of the parsed netloc. For example, "x.com" would match "notx.com" and "neetcode.io" would match "myneetcode.io". Anchoring the check (e.g., domain == known_domain or domain.endswith("." + known_domain)) would prevent these misclassifications.
| @@ -0,0 +1 @@ | |||
| Update: Mon Mar 23 03:20:19 EDT 2026 | |||
There was a problem hiding this comment.
Personal progress note should not be committed to the repository
progress.txt is a personal bookkeeping file with no relevance to the codebase. It should be removed from the PR and added to .gitignore.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Classifies browser tabs into appropriate activity.