No description
Find a file
Vincent Batts d07d0c13b3 Report page URLs as paths using gosh protocol 2
Declare protocol 2 and send each page's URL (without query, fragment or
credentials) as the event path, so gosh groups time by site and page
instead of everything landing under /firefox/<host>.

file:// pages are reported as plain file paths; files on other hosts are
skipped. Config rules still map sites to project directories, and detail
url is now only sent for mapped pages, where the path no longer shows it.
2026-10-17 08:51:35 -04:00
.gitignore Add gosh-firefox plugin reporting Firefox browsing activity 2026-10-17 08:15:38 -04:00
config.go Report page URLs as paths using gosh protocol 2 2026-10-17 08:51:35 -04:00
firefox.go Report page URLs as paths using gosh protocol 2 2026-10-17 08:51:35 -04:00
go.mod Add gosh-firefox plugin reporting Firefox browsing activity 2026-10-17 08:15:38 -04:00
go.sum Add gosh-firefox plugin reporting Firefox browsing activity 2026-10-17 08:15:38 -04:00
LICENSE Add gosh-firefox plugin reporting Firefox browsing activity 2026-10-17 08:15:38 -04:00
main.go Report page URLs as paths using gosh protocol 2 2026-10-17 08:51:35 -04:00
main_test.go Report page URLs as paths using gosh protocol 2 2026-10-17 08:51:35 -04:00
README.md Report page URLs as paths using gosh protocol 2 2026-10-17 08:51:35 -04:00

gosh-firefox

A gosh plugin that reports Firefox browsing activity: when you were on which sites, and for how long.

Firefox keeps history in each profile's places.sqlite. gosh-firefox collect turns it into gosh events:

kind one event per
visit page visit (moz_historyvisits)
view 5 minutes a page was actually in front of you (moz_places_metadata.total_view_time)

A visit is a single moment, but gosh infers time from the spacing of events. The view events fill in the time Firefox measured you spent on a page, so a 40 minute read counts as 40 minutes and not 5. Embedded content, framed links and other internal pages (about:, moz-extension:) are skipped.

Install

go install git.batts.cloud/vbatts/gosh-firefox@latest
gosh plugin add firefox
gosh-firefox doctor        # are my profiles readable?
gosh collect

All profiles are read, from ~/.mozilla/firefox, ~/.config/mozilla/firefox, and the snap and flatpak locations. --dir limits collect and doctor to one profile or a directory of profiles.

Where activity is reported

gosh plugin protocol 2 lets an event's path be a URI, so each event's path is simply the page: https://git.batts.cloud/vbatts/gosh/issues. gosh drops the query, fragment and credentials, lowercases the host and groups everything in a tree by site and page, so you can see time per website or per page:

gosh report -by path -only firefox -since 7d
gosh report -by path -only firefox -prefix https://github.com -depth 2

Pages opened from disk (file:///...) are reported as plain file paths, so they land in the git project that contains them. Files on another host are skipped.

Browsing has no git project, so gosh report -by project shows web pages under (no repo). To count browsing toward real projects, map sites to directories in ~/.config/gosh-firefox/config (or $XDG_CONFIG_HOME/gosh-firefox/config):

# host, host/path-prefix, or *.suffix, then the directory
git.batts.cloud/vbatts/gosh   ~/src/git.batts.cloud/vbatts/gosh
docs.example.com              ~/work/example
*.corp.example.com            ~/work/corp

ignore mail.example.com       # never reported
detail url                    # url (default), title, host or none

The most specific match wins (longest path prefix, then exact host over wildcard). A mapped page is reported at the directory, so it leaves the per-page tree; with detail url its URL is kept in the event's detail and shows in gosh timeline. Unmapped pages already carry the URL as their path, so they get no detail by default. gosh passes plugins no options, so this file is the only configuration.

Privacy

The URL is the event's path and is shown in reports you may share. The plugin sends it without its query string, fragment or credentials, which is where tokens usually live, but paths can still hold ids (a mail message id, a chat conversation id). detail title adds page titles; ignore keeps a site out entirely.

When Firefox changes the format

places.sqlite is internal to Firefox and not a stable API; the schema has gone through dozens of versions (PRAGMA user_version; this was tested against 57 through 87, Firefox 91 to 158). So the plugin does not trust the version number:

  • Every run checks that the tables and columns it queries exist. If they don't, that profile is skipped with the missing names on stderr, and if no profile could be read, collect exits non-zero so gosh stores nothing rather than leaving a quiet gap.
  • A newer user_version than tested is fine while the columns check out; doctor mentions it.
  • Without moz_places_metadata (Firefox before 96, or if it is renamed) you still get visit events.
  • Timestamps are range-checked (2000 to tomorrow). If a unit changes, the rows are dropped with a warning instead of being reported at the wrong time.

gosh-firefox doctor shows each profile's path, Firefox version, schema version, what was found and status: ok or what is wrong; it exits non-zero if any profile is unusable.

How it reads a live database

Firefox keeps places.sqlite open and locked, with recent changes in places.sqlite-wal. Opening it directly fails with "database is locked", and opening it read-only/immutable silently misses the newest history. So each run copies the database and WAL to a temporary directory, reads the copy, and deletes it. Profiles not written since --since are skipped, so frequent runs are cheap.

Limitations

  • Firefox Sync merges history from your other devices into moz_historyvisits, and no device name is stored. Visits with source = 3 appear to be the synced ones (they never coincide with this machine's own view-time records), so they are skipped; doctor shows how many. That meaning is inferred, not documented by Mozilla, and the column is missing from older Firefox, where synced visits are reported. view events come from local-only interaction data and don't have this problem.
  • view events are spread from the page's first view onward, but Firefox only records a total, so a page you left and returned to hours later is credited as if you stayed.
  • total_view_time only counts while the window is focused, and private browsing is not recorded at all.

Protocol

gosh-firefox describe
gosh-firefox collect --since <unix seconds> [--dir <profile or profiles dir>] [--config <file>]
gosh-firefox doctor [--dir <profile or profiles dir>]

The plugin declares protocol 2 (paths may be URIs), so it needs a gosh that supports it. Output is JSON lines; see gosh's docs/PLUGINS.md. Events are keyed by profile, page and timestamp, so collecting twice is harmless. The only dependency is the pure-Go SQLite driver modernc.org/sqlite (no cgo).

License

MIT, see LICENSE.