Skip to content

StackSight v2.0.0

Choose a tag to compare

@github-actions github-actions released this 06 Aug 07:46
· 5 commits to main since this release

A rewrite of the foundations. Everything the extension did, it still does. What changed
is that the numbers it shows are now correct, the claims it makes are now enforced, and
the tests now test the code that ships.

Your 1.x data migrates automatically on first run. The old storage keys are left in
place as a rollback copy rather than deleted.

Fixed

  • Lost updates. Every context did read-modify-write on the whole items map. Two
    tabs finishing a load in the same tick both read the same snapshot and the second
    write erased the first; a popup edit landing during a background visit clobbered it.
    The service worker is now the only writer and applies every mutation through one
    serialised queue. chrome.storage has no compare-and-swap, so this needed a
    structural fix rather than a wrapper around set().
  • Renewal dates on month ends. setMonth() overflowed January 31 to March 3,
    skipping February. Dates now anchor on the original day of month and clamp to the
    target month's length, so a subscription on the 31st goes Jan 31, Feb 28, Mar 31
    instead of drifting or overflowing. Yearly renewals on Feb 29 clamp in common years
    and return to the 29th in leap years.
  • Cost per visit was badly wrong for sites you use a lot. recentVisits held at
    most 30 raw timestamps but was read as "visits in the last 30 days". A site opened
    100 times a month reported 30, overstating its cost per visit by more than three
    times. Replaced with per-day counts, which are unbounded in magnitude and smaller on
    disk.
  • Failed writes were reported as success. A full storage area silently dropped
    data while the UI said it had saved, and dismissSite() returned ok after a failed
    write. Every command now returns a real result and the interface says what happened.
  • Adding a site by hand invented a visit. It set visitCount: 1 and
    lastVisited: now for a site you had not opened, skewing cost per visit from the
    first render.
  • Export and import lost data. Export omitted dismissed sites; import ignored the
    currency that export wrote. The full state now round trips.
  • "Replace everything" did not. It replaced items and stacks only, leaving old
    dismissals in place and still suppressing sites.
  • Import crashed on some valid JSON. {"items": null} passed the shape check and
    then threw inside Object.entries.
  • Dismissed sites grew forever. They now expire on a configurable TTL and are
    capped at 500.
  • A brand new site was immediately flagged stale, since it had no visits yet. New
    sites are exempt until they have existed for a full stale window.
  • The popup squashed its own header and tabs when the list was long, because every
    child of the column flexbox could shrink.

Security

  • connect-src 'none' in an explicit manifest CSP. The no-network claim used to
    rest on a source audit; Chrome now refuses to let the extension open a connection at
    all. The policy also pins script-src, object-src, img-src, style-src,
    frame-src, form-action and base-uri.
  • The prompt is injected on demand rather than declared as a static content script
    on every http(s) page. On a site you already track, or dismissed, or with prompting
    off, no StackSight code touches the page. This reduces runtime exposure, not the
    <all_urls> permission, which is still needed to observe navigations.
  • The prompt renders in a closed shadow root with its own styles. The page it sits
    on can no longer restyle it, read it through element.shadowRoot, or rewrite what it
    is asking.
  • No innerHTML anywhere. The prompt and the popup's dynamic rendering build every
    node with createElement and textContent, so escaping cannot be missed at a call
    site rather than merely being remembered at all of them.
  • The currency symbol is allowlisted. It reached innerHTML unescaped in 1.3.
    Unreachable then, but it would have become live the moment import started reading
    settings, which 2.0 does.
  • Control characters and bidi overrides are stripped from stored text, so a crafted
    page title cannot render back-to-front and impersonate another site.
  • The payload reaches the prompt as an executeScript argument instead of a
    follow-up message, removing the window where the script existed but its listener was
    not registered.

Added

  • Insights view. Monthly and yearly spend, how much is going to stale sites, a 30
    day visit sparkline, spend by stack, and most expensive per visit.
  • Settings page. Preferences, a summary of stored data, export and import,
    management of dismissed sites, and the full disclaimer.
  • Configurable thresholds. Stale window, renewal warning window and dismissal TTL
    were constants you had to edit in source. They are settings now.
  • Light, dark and match-system themes, applied to the popup, the settings page and
    the injected prompt.
  • Favicons from Chrome's local icon cache, with deterministic letter avatars when
    there is no cached icon.
  • Fuzzy ranked search, so gh finds GitHub. Matches across name, host, stack and
    note, weighted by field.
  • Pinned sites, which stay above everything including the last launched site.
  • A toolbar badge counting paid sites you have stopped opening.
  • Restore dismissed sites individually or all at once.
  • Erase everything, with confirmation.

Changed

  • alert, confirm and prompt are gone. They blocked the extension process and
    could dismiss the popup mid-action. Replaced by in-popup dialogs and toasts.
  • Modals trap focus, close on Escape, and restore focus to whatever opened them.
    Interactive elements have ARIA roles and labels.
  • Insights replaces the list rather than stacking below it, which pushed the popup
    past the height Chrome allows.
  • Storage is one key. chrome.storage.local.set guarantees nothing across multiple
    keys, so a multi-key write could half-apply. One key means one atomic write.
  • Visit history is per calendar day, in local time. UTC keys would put an 8pm visit
    in UTC-5 on the following day, so the stale flag and "visits this month" would
    disagree with the calendar the user is reading.
  • Import and export moved to the settings page, where there is room for the summary
    and the merge-or-replace choice.

Internal

  • lib/core.js: all pure logic, with no DOM, no chrome.*, no I/O and no implicit
    clock. Every time-dependent function takes now, so tests pin it instead of sleeping.
  • lib/store.js: the serialised command queue, over an injectable storage adapter.
  • Both are classic scripts, loaded by the service worker via importScripts, by the
    content script via executeScript, by the pages via <script src>, and by the tests
    via require. Still no build step and no dependencies.
  • Tests rewritten. The old suite copy-pasted the logic it tested, so it could pass
    while the extension was broken. 57 tests now run against the shipped modules on
    node --test.
  • dev/: a dependency-free harness that runs the real pages in an ordinary tab
    against the real store, used for development and for every screenshot in the README.

Install

Download stacksight-2.0.0.zip below and unzip it somewhere stable, not Downloads or the Desktop: Chrome reads the folder live.

  1. Open chrome://extensions and turn on Developer mode.
  2. Click Load unpacked and select the unzipped stacksight folder.
  3. Pin it from the puzzle-piece menu.

Requires Chrome 111 or newer. Not on the Chrome Web Store, and not planned for it.

Full notes: INSTALL.txt · Architecture · Security model