Skip to content

Feature/browser activity classifier#1354

Open
AryanSingh103 wants to merge 3 commits into
ActivityWatch:masterfrom
AryanSingh103:feature/browser-activity-classifier
Open

Feature/browser activity classifier#1354
AryanSingh103 wants to merge 3 commits into
ActivityWatch:masterfrom
AryanSingh103:feature/browser-activity-classifier

Conversation

@AryanSingh103

Copy link
Copy Markdown

Classifies browser tabs into appropriate activity.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a new standalone aw-watcher-browser-classifier that periodically reads browser tab events from the existing aw-watcher-web bucket, classifies each URL against a hard-coded category ruleset, and writes classified events to a new output bucket via heartbeat.

  • The heartbeat call passes a plain dict instead of the required aw_core.models.Event object, causing an AttributeError crash on every classification cycle.
  • The 30-second lookback window with a 10-second poll interval means every source event is re-submitted approximately three times per cycle with no deduplication, inflating the output bucket.
  • progress.txt (a personal timestamp note) is included in the commit and should be removed.

Confidence Score: 2/5

Not ready to merge — the watcher crashes on its first heartbeat call and, once that is fixed, will silently write triplicate events to the output bucket on every poll cycle.

The heartbeat call passes a plain dict where aw_core.models.Event is required, causing an immediate crash on every classification cycle. Independently, the 30s lookback with a 10s sleep means every source event is reprocessed ~3 times with no deduplication. Both defects are on the hot path of every run.

aw-watcher-browser-classifier/classifier.py — the Event type mismatch and overlapping poll window both need to be addressed before the watcher produces correct output.

Important Files Changed

Filename Overview
aw-watcher-browser-classifier/classifier.py New browser-activity classifier watcher with two runtime bugs: heartbeat receives a plain dict instead of aw_core.models.Event (crashes at runtime), and the 30s lookback with a 10s poll interval causes every source event to be written ~3x to the output bucket.
progress.txt Personal progress note with a single timestamp; should not be committed to the repository.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Main
    participant AW_Server as ActivityWatch Server
    participant SourceBucket as aw-watcher-web bucket
    participant OutputBucket as classifier output bucket

    Main->>AW_Server: get_buckets()
    AW_Server-->>Main: bucket list
    Main->>AW_Server: create_bucket(output_bucket)

    loop Every POLL_INTERVAL_SECONDS (10s)
        Main->>AW_Server: "get_events(source_bucket, start=now-30s, limit=50)"
        AW_Server-->>SourceBucket: query last 30s events
        SourceBucket-->>Main: recent_events

        loop For each event
            Main->>Main: "classify_url(url) -> category"
            Main->>AW_Server: "heartbeat(output_bucket, dict, pulsetime=15)"
            Note over Main,AW_Server: dict passed instead of Event object
            AW_Server-->>OutputBucket: write classified event
        end

        Main->>Main: sleep(10s)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Main
    participant AW_Server as ActivityWatch Server
    participant SourceBucket as aw-watcher-web bucket
    participant OutputBucket as classifier output bucket

    Main->>AW_Server: get_buckets()
    AW_Server-->>Main: bucket list
    Main->>AW_Server: create_bucket(output_bucket)

    loop Every POLL_INTERVAL_SECONDS (10s)
        Main->>AW_Server: "get_events(source_bucket, start=now-30s, limit=50)"
        AW_Server-->>SourceBucket: query last 30s events
        SourceBucket-->>Main: recent_events

        loop For each event
            Main->>Main: "classify_url(url) -> category"
            Main->>AW_Server: "heartbeat(output_bucket, dict, pulsetime=15)"
            Note over Main,AW_Server: dict passed instead of Event object
            AW_Server-->>OutputBucket: write classified event
        end

        Main->>Main: sleep(10s)
    end
Loading

Reviews (1): Last reviewed commit: "Add custom browser-activity classifier w..." | Re-trigger Greptile

Comment on lines +93 to +101
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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +84 to +104
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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).

Comment on lines +53 to +55
for category, domains in CATEGORY_RULES.items():
if any(known_domain in domain for known_domain in domains):
return category

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment thread progress.txt
@@ -0,0 +1 @@
Update: Mon Mar 23 03:20:19 EDT 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant