- Go 100%
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. |
||
|---|---|---|
| .gitignore | ||
| config.go | ||
| firefox.go | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
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,
collectexits non-zero so gosh stores nothing rather than leaving a quiet gap. - A newer
user_versionthan tested is fine while the columns check out;doctormentions it. - Without
moz_places_metadata(Firefox before 96, or if it is renamed) you still getvisitevents. - 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 withsource = 3appear to be the synced ones (they never coincide with this machine's own view-time records), so they are skipped;doctorshows how many. That meaning is inferred, not documented by Mozilla, and the column is missing from older Firefox, where synced visits are reported.viewevents come from local-only interaction data and don't have this problem. viewevents 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_timeonly 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.