ClipDeck Visit Counter — Storage + Messages + Tabs in One Loop
~15 min · tabs, onUpdated, storage, messaging, service-worker, permissions
Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Lesson 4 was the noun. Lesson 5 was the verbs. Lesson 6 is the small, real thing that uses both — a visit counter you can watch climb in real time. Track 2 ends here, with one feature that runs even when the popup is closed and the laptop is asleep."
What You're Building
The visit counter is the simplest possible R in ClipDeck — read state from storage, render it. Every time you finish loading any tab, the service worker increments a counter. The popup, when opened, displays the total and a per-URL breakdown. Closing the popup, sleeping the SW, even restarting Chrome — the count survives, because all of it lives in chrome.storage.local.
This is the final ClipDeck slice for Track 2. From Track 3 onward, the same stack — storage as state, messages as actions, events as triggers — will host the clip CRUD itself. Lesson 6 is the worked example that proves the stack is wired correctly end-to-end.
The chrome.tabs.onUpdated Event
Tabs fire onUpdated at every state change — favicon load, title swap, navigation, completion. You only care about completion, so guard on changeInfo.status === "complete" and a defined tab.url:
tabId — the id of the tab that changed.
changeInfo — what changed this fire. Includes status (loading / complete) and sometimes url.
tab — the full tab object after the change. Use tab.url over changeInfo.url; the latter is only present on actual URL transitions.
Listening to onUpdated requires the "tabs" permission (to read the URL) OR the "activeTab" permission (only when the user invokes the action). For an always-on visit counter you need "tabs" — add it to manifest.json alongside "storage".
Filtering System Pages
You'll quickly notice chrome://, about:, chrome-extension:// URLs polluting the count. Skip them — the user did not browse to a page in any meaningful sense. A one-line guard handles 99% of the noise:
Also skip empty / null URLs (they appear on new-tab transitions before the actual navigation lands). After those guards, every increment maps to a real page load the user initiated.
The Schema
Two keys in chrome.storage.local:
totalVisits: number — monotonic counter, never reset except on user action.
urlCounts: Record<string, number> — per-URL count. Lets the popup show a top-N list.
This schema is the smallest thing that demonstrates both a flat scalar and a structured object in storage. ClipDeck's clip list (Track 3) will follow the same pattern — one key for the array, one key for derived counts/metadata.
The Popup Side
The popup loads, reads both keys, renders. It also subscribes to chrome.storage.onChanged; when the SW writes new counts, the popup re-renders without any message passing. A "Reset counters" button sends { type: "resetVisitCounts" } to the SW; the SW writes zeros to storage and responds {ok:true}. Reset is an action, so it goes through messages (Lesson 5's verb/noun split).
The full Track 2 stack in one feature: events trigger the SW, the SW mutates storage, the popup re-renders via onChanged, user actions flow back through messages. This is the loop ClipDeck rides for every feature from here on.
Why the visit counter, of all things? It is the smallest feature that exercises every Track 2 concept without inventing a new domain. You will throw the counter away in Track 3 if you want — the point is the loop, not the count. Some students keep it as a permanent ClipDeck "how often did I hit this site?" widget; either choice is fine.
Watch it climb live. chrome://extensions → ClipDeck → 'Inspect views: service worker' to open the SW DevTools. Application tab → Storage → Extension Storage → 'local'. Now navigate to a few pages. Watch urlCounts grow in real time, no refresh needed. This is the fastest sanity check that the whole pipeline is wired correctly.
Bump clipdeck/manifest.json to version 0.3.0, set "permissions": ["storage", "tabs"]. Add the chrome.tabs.onUpdated listener from the first code block to clipdeck/background.js (alongside the existing message listeners). In clipdeck/popup.html, add <div>Total visits: <span id="totalVisits">0</span></div>, <ol id="topUrls"></ol>, and <button id="resetBtn">Reset counters</button>. In clipdeck/popup.js, replace the body with the second code block. Reload the extension. Open the popup — totals should be zero. Close the popup. Open four or five real-web pages (any sites you use). Re-open the popup — totalVisits should match the page loads, and topUrls should list them. Click Reset — both counters zero, top-URL list empties, no popup re-open needed.
Hint
If the count never moves, the most likely cause is missing the "tabs" permission — without it, tab.url is undefined and isSkippableUrl returns true. Reload the extension after editing the manifest. If the popup renders zeros but the SW DevTools' Application → Extension Storage shows real counts, the popup's chrome.storage.local.get call is racing the storage write — check the destructured key names match exactly (typos like totalVisit vs totalVisits will silently default to 0). If onChanged fires but the popup does not re-render, your listener is registered before the popup DOM is ready — register onChanged first, then call render() last in popup.js.
Progress
Progress is local-only — sign in to sync across devices.