- Go 100%
Read the profile name from the browser's Local State and report it as "<name> (<browser>)", falling back to the directory name. The name comes first because gosh's timeline shows only the first 8 bytes of a session. Refs keep using the directory name so renaming a profile does not re-report its history. Only the name is read, not the account name or email stored beside it. |
||
|---|---|---|
| .gitignore | ||
| chrome.go | ||
| config.go | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
gosh-chrome
A gosh plugin that reports Chrome browsing activity: when you were on which pages, and for how long. It reads every Chromium-family profile it finds (Chrome, Chromium, Brave, Edge, Vivaldi), since they share one history format. A sibling of gosh-firefox.
Each profile keeps history in a SQLite file called History. gosh-chrome collect turns it into gosh events:
| kind | one event per |
|---|---|
visit |
page visit (visits) |
view |
5 minutes the page was in the foreground (context_annotations.total_foreground_duration, else visits.visit_duration) |
A visit is a single moment, but gosh infers time from the spacing of events. The view events fill in the
time Chrome measured on the page, so a 40 minute read counts as 40 minutes and not 5. Subframes and internal
pages (chrome:, chrome-extension:) are skipped.
Install
go install git.batts.cloud/vbatts/gosh-chrome@latest
gosh plugin add chrome
gosh-chrome doctor # are my profiles readable?
gosh collect
Profiles are found under ~/.config/{google-chrome,google-chrome-beta,google-chrome-unstable,chromium, BraveSoftware/Brave-Browser,microsoft-edge,vivaldi} (honouring XDG_CONFIG_HOME) and the snap and flatpak
locations. --dir limits collect and doctor to one profile or one user data directory.
How time is decided
Chrome records two durations per visit, and neither means you were there:
total_foreground_durationis time the page was in the foreground.0means it never was (a tab opened in the background);-1means it was never finalized.visit_durationis time until the tab navigated or closed. For a background tab, that is how long the tab stayed open: in one real profile 883 visits claim over an hour, up to 96 hours.
So the plugin trusts the foreground duration when it is known (including 0: no time), and falls back to
visit_duration only when it isn't. Then every visit is limited by a cap, 30 minutes by default:
foreground time over the cap is clamped to it, but a fallback visit_duration over the cap is dropped,
because it is almost always a tab left open, and clamping it would still invent attention. On one profile this
is the difference between crediting 352 hours and 160. Set cap in the config to change it. doctor shows
how much time was found and how many visits hit the cap.
The foreground duration is still the time a page was active in its window, not proof you were at the keyboard, but gosh only credits about 5 minutes per event and ends a session after a 30 minute gap.
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:
gosh report -by path -only chrome -since 7d
gosh report -by path -only chrome -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-chrome/config (or
$XDG_CONFIG_HOME/gosh-chrome/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
cap 30m # longest time credited to one visit
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.
Profiles
Every profile is read, and each event's session is <profile name> (<browser>), for example
Aspen (google-chrome), using the name you gave the profile in the browser (from Local State; the
directory name, like Profile 2, is the fallback). gosh timeline shows the first 8 bytes of the session, so
you can tell work from personal browsing; the name comes first for that reason. Event refs use the directory name, which doesn't change when you rename a profile. Only the
profile's name is read from Local State, not the account name or email stored next to it.
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
document id, a mail message id). detail title adds page titles; ignore keeps a site out entirely. Profile names appear as the session, so
name them with that in mind.
Synced history
Chrome Sync merges history from your other devices into visits. Unlike Firefox, Chrome marks these:
visits.originator_cache_guid is set to the originating device and empty for visits made here. Synced
visits are skipped, so visit events mean "browsed on this machine"; doctor shows how many there are. The
column is missing from older Chrome, where synced visits are reported.
When Chrome changes the format
The History schema is internal to Chrome and not a stable API (meta.version; this was tested with 70,
Chrome 155). 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 version than tested is fine while the columns check out;
doctormentions it. - Without
originator_cache_guidorcontext_annotations, you still get visits, with the fallbacks above. - Timestamps are range-checked (2000 to tomorrow). If the epoch or unit changes, the rows are dropped with a
warning instead of being reported at the wrong time. A full run (
--since 0) applies no lower bound in SQL precisely so such rows are seen and flagged, not silently filtered out.
gosh-chrome doctor shows each profile's path, browser 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
Chrome keeps History open and locked while it runs, with recent changes possibly in History-journal.
Each run copies the database and its journal to a temporary directory, reads the copy, and deletes it.
Profiles not written since --since are skipped, so frequent runs are cheap. A copy taken in the middle of a
write can be inconsistent; if so that profile fails with a warning and the next run reads it normally.
Limitations
viewevents are spread from the visit's start onward, but Chrome only records a total, so a page you left and returned to hours later is credited as if you stayed.- Incognito and guest browsing is not recorded in
History. - Only Linux locations are known.
Protocol
gosh-chrome describe
gosh-chrome collect --since <unix seconds> [--dir <profile or user data dir>] [--config <file>]
gosh-chrome doctor [--dir <profile or user data dir>] [--config <file>]
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 browser, profile and visit, so collecting twice is harmless. The only dependency is the pure-Go SQLite driver modernc.org/sqlite (no cgo).
License
MIT, see LICENSE.