Repository navigation
Decisions
github-actions[bot] edited this page Nov 1, 2026
·
15 revisions
Why NetworkMonitor works the way it does. Read this before changing something that looks odd: most odd-looking things were the second or third attempt.
- Under-reporting is the one error it can't afford. Anything ambiguous counts as internet traffic. A total that's slightly high is an annoyance; one that hides real usage defeats the app.
- Few permission prompts, and each one optional. Networks are identified without any permission. The welcome window offers the two that add something, Wi-Fi names (Location, #85) and website names (the content filter, #128), each said as what you get (#134). The only password ever asked for is optional, for Hotspot mode's helper.
- No network access of its own, but four: a speed test you start; once a month the country and network data, the same signed file for everyone (#121); once a week, while they're switched on, the ad and malware lists, the same way (#176); and once a day a check for a new version (#132). None of the last three runs by itself on a hotspot, and each has a switch to turn it off.
- Nothing to configure where the app can decide. Sampling rates follow the power source; Hotspot mode is one switch with no list to maintain. A switch that needs configuring to work has failed.
- Never lose a byte to save power. Sample less often, never count less.
- Say what it decided. Settings states the sampling mode, why a connection counts as a hotspot, what was paused, what wasn't, and what got through anyway.
- Real numbers. Measurements are measured, screenshots are real captures, and limits are stated rather than hidden.
Newest last. Links go to the commit or pull request.
| Date | Decision | Why |
|---|---|---|
| 2026-07-31 | Kernel counters via sysctl(NET_RT_IFLIST2) (8f5823a) |
getifaddrs() counters are 32-bit and had already wrapped: 1.23 GB shown for a true 5.52 GB |
| 2026-07-31 |
nettop in delta mode, on a pseudo-terminal (8f5823a) |
Cumulative totals fall when sockets close; through a pipe it block-buffers and delivers nothing for ~30 s |
| 2026-07-31 | Networks identified by the gateway's hardware address (8f5823a) | The Wi-Fi name needs Location permission; the gateway address separates routers sharing a name and survives roaming |
| 2026-07-31 | Apps identified by the outermost .app in a process's path (8f5823a) |
Collapses helper processes into their app; process names are truncated to 15 characters |
| 2026-07-31 | The test suite is a plain executable (8f5823a) | XCTest and swift-testing ship only with Xcode |
| 2026-07-31 | The readout is two lines drawn into an image at 8.5 pt, at a fixed width (792641a) |
NSButton clipped multi-line titles at every size; nothing beside it should move |
| 2026-07-31 | Start at login through SMAppService.mainApp, after briefly using a LaunchAgent (c2e08e1) |
The LaunchAgent showed as a generic "exec" tile; SMAppService was proven to work with an ad-hoc signature |
| 2026-07-31 | One instance only; no KeepAlive (390c646) |
Two copies double-count every byte; KeepAlive would make Quit a no-op |
| 2026-07-31 | Versions stamped from git; releases built from tags; a smoke test in CI (859c607) | A hardcoded build number would block every future update; a crash on launch is the worst bug an updater can ship |
| 2026-08-01 |
nettop's stdin is a pipe nobody writes to, and per-app tracking always runs (4c4eb9c) |
With /dev/null it spun at ~140% of a core; with a pipe, 0.55%. The battery modes existed only because of that cost, and switched attribution off on hotspots, where it matters most |
| 2026-08-01 | A total per connection: joining a different network starts over; offline is a gap, not a switch (4c4eb9c) | The question is what this connection cost; a flaky link mustn't wipe the count |
| 2026-08-01 | Sampling rates follow the power source, with no setting (4c4eb9c) | Deltas lose no bytes, only freshness |
| 2026-08-01 | The readout is published only when its text changes (54506ce) | Every publish re-laid out the hidden popover twice a second |
| 2026-08-01 | Internet only: classify each connection's remote address, subtract LAN unicast, clamp (4e64478) | AirPlay, Time Machine and SMB counted as internet; -n avoids the monitor making DNS traffic |
| 2026-08-01 | Multicast is never subtracted (30a8d3c) |
nettop's multicast figure exceeded what the interface could physically carry by 12.6× or more |
| 2026-08-01 | Processes outside an app are adopted by the app that launched them (a4c5a4b) | Tools started from editors and terminals were the top consumer, hidden under System |
| 2026-08-01 | The popover's height is computed from its rows; it's anchored to an invisible window (09f627e) | Measured heights start at zero; with an auto-hidden menu bar the popover jumped to the screen's left edge |
| 2026-08-04 | Hotspot mode: six ranked signals; per-app settings, a config-file edit, and a root helper for the rest (be754a8) | Each app exposes a different mechanism; blocking update hosts would break VS Code Remote-SSH and take Claude offline |
| 2026-08-04 | Every change is journalled before it's made (be754a8) | The failure to prevent is a Mac that silently stopped updating months ago |
| 2026-08-04 | The helper is a sudoers-gated root binary with fixed verbs, not an XPC daemon (be754a8) | XPC client checks need a Developer ID team; a tiny argument-free vocabulary limits what root can be asked |
| 2026-09-08 | The app list re-sorts live (f740eb7) | Freezing the order hid a large download under tiny apps; samples now close after 120 ms of quiet |
| 2026-09-29 | Builds with Command Line Tools 27: no @State, native build system, per-architecture builds joined with lipo (4eb8f8c) |
@State is an Xcode-only macro; swiftbuild fails; --arch twice needs Xcode |
| 2026-09-29 | A drag-to-Applications disk image with a versionless name (d91ca04) | A stable "latest download" link for the website |
| 2026-10-20 |
Speed test with macOS's networkQuality, sequential, plus a ping-only pass; no RPM (6993190) |
Nothing to install or maintain; sequential like other tests; one batch of idle pings is too noisy |
| 2026-10-22 | Popover semi-transient; Settings makes the app regular while open (7d44ecd, 76bfdb5) | Revealing an auto-hidden menu bar activated another app, closed the popover and buried Settings (#6) |
| 2026-10-22 | Low Data Mode through interface flags (e2415b9) | One mechanism for Wi-Fi and tethers, nothing on disk, cleared by a restart |
| 2026-10-22 | "Not covered" found by updater shape; "got through" watched (e2415b9) | A list of names can't reach every Mac; the gaps should be visible |
| 2026-10-22 | No Terminal: the helper is installed from the app through the administrator dialog (e2415b9) | The dialog needs no Developer ID, and users shouldn't need Terminal |
| 2026-10-22 | The release workflow publishes the disk image (fbcd80c) | Older release notes sent people to clone, build and clear quarantine by hand |
| 2026-10-23 | Usage History in SQLite, hourly per app per visit, never reset by Reset Now or midnight (ddd2423, #3) | Yesterday's usage was lost by design; about 5 MB a year, a year's query in ~30 ms |
| 2026-10-23 | One "Show speeds in" choice for the menu bar and the speed test; bytes by default (ebde6c7, #4) | Two places disagreed; upgrading mustn't change the menu bar |
| 2026-10-23 | The speed test as a Card or a Compact strip; the menu bar always shows a running test (e6cef5e) | The popover's tallest size never changes; the old switch changed nothing visible |
| 2026-10-23 | Every change goes issue → pull request → release (#9) | A written workflow every contributor follows |
| 2026-10-23 | Settings as toolbar panes, following Apple's guidelines; Usage History moves in (#11) | One long, cramped column; features hidden behind unlabelled icons |
| 2026-10-23 | Battery: nothing drawn while nobody can see the menu bar; a Low Power Mode profile (#22, #1) | What remained after the nettop fix was drawing the readout: 1.19% of a core on battery, now 0.47% with the screen off and 0.53% in Low Power Mode |
| 2026-10-23 | Hotspot lists say how each app is paused, or why it can't be, in plain words (#40) | "Apps paused" read as happening already, and "electron-updater" or "Sparkle, sandboxed" explained nothing to someone new |
| 2026-10-24 | Menu bar colors can be changed; a speed test's colours follow, kept clear of them (#45) | Colour is the only thing that tells a test's figures from the live rates, so a live colour that matched the test's would hide which is which |
| 2026-10-24 | Settings is never taller than the screen; a taller pane scrolls (#46) | With both Hotspot lists open, the window ran off the bottom of the screen |
| 2026-10-24 | Settings' toolbar takes clicks while a pane is still changing (#55), and the window keeps its centre when it does (#57) |
NSTabViewController ignored those clicks: a tab clicked within about 0.3 s of the last did nothing. Allowing them let Auto Layout resize the window first, from its left edge, walking it 60 pt left per switch around History |
| 2026-10-25 | Settings › Hotspot loads in the background, marks only changes that are really waiting for an app to quit, and gives a pane a scroll bar only while it scrolls (#65) | Opening it asked the helper twice on the main thread (about 260 ms) and rescanned the apps; quit it to apply showed whenever the app was open, on any network; a scroll bar that came and went left a wrapped line's height behind |
| 2026-10-25 | Settings is one width, 720 pt, for every pane, and changes panes at once, with no crossfade (#61) | The crossfade ran about 0.7 s per switch and let a scroll bar flash on the way out of a taller pane; History at 768 pt and the rest at 648 pt made the window change width |
| 2026-10-25 |
The gateway's address is read from configd, then arp (issue #70) |
macOS 27 hides the ARP table from apps without Local Network permission, so every Wi-Fi network had become one. configd needs no permission, and asking for it would mean a prompt after every ad-hoc-signed update |
| 2026-10-25 | History's All / Wi-Fi / Hotspot switch replaces Metered only, opens on Hotspot while you're on one, and app bars are split by network type (issue #70) | A day on Wi-Fi and a hotspot should read as two parts, everywhere in the pane. Hotspot is a kind of network, so it filters by kind and matches the chart's colours; Metered only was nearly the same set under another name |
| 2026-10-25 | Wi-Fi names, with Location, always on (issue #85) | "Wi-Fi · 9493" told people nothing. macOS 27 shares the name only with Location, so the app asks a few seconds after launch, with no setting. The name is only a label: the router's address stays the identity, and a router's two band names are both kept |
| 2026-10-25 | Every build signed with one self-signed certificate (issue #86) | Ad-hoc signatures made each update a new app to macOS, which asked for Location again. A fixed certificate gives every build the same designated requirement; checked, an update with it wasn't asked. The key stays out of the repo: in ~/.config/networkmonitor-signing/ and two repository secrets |
| 2026-10-29 |
Connections keeps each app's servers: nettop's internet connection rows, by address, hourly, per visit (#111) |
The rows were already parsed for the LAN split and thrown away. Keeping them needs no new permission and no new process. Each app and server pair is stored once, so an hourly row holds a small number rather than an app's path |
| 2026-10-29 | Servers are named from DB-IP Lite, shipped in the app (#111) | No lookups online: asking a service about each address would tell it every server the Mac talks to. DB-IP's free data is CC BY 4.0. Converted to sorted range starts with the narrowest label width that fits, and IPv6 keyed on its first 64 bits, it's 13 MB, from 60 MB of CSV |
| 2026-10-29 | The converted data is committed, not downloaded at build time (#111) | DB-IP removes each month's files once the next is out (a 2025 month already returns 404), so a pinned download would break, and an old tag could never be rebuilt |
| 2026-10-29 | Networks that host other companies are marked as hosts (#111) | Until server names arrive, the owner of a network is the only clue. That's right for Google or Microsoft, and wrong for anything on Amazon's or Cloudflare's network, which would otherwise all be blamed on them |
| 2026-10-29 | Connections is a window of its own, opened from the popover's globe and ⌘K (#112) | It's read beside other work and wants room: resizable, which Settings' fixed 720 pt can't be, and wider again once the map sits beside the list (#113) |
| 2026-10-29 | Grouped by company first; a company opens into its apps, countries and servers (#112) | Which companies get your data is the question. Apps and Countries are the same hierarchy turned around |
| 2026-10-29 | Hosted traffic reads Hosted on Amazon, marked HOST, and never counts as talking to Amazon (#112) | The network's owner is the only clue until server names arrive; for clouds and CDNs it names the landlord, not the tenant |
| 2026-10-29 | Each app shows its maker from its code signature (#112) | Reading a signature's certificates is quick, offline and needs no permission, and "by Microsoft, also talks to Google" is the line worth reading |
| 2026-10-29 | Signed with the Developer ID and notarized; the app first, stapled, then the disk image (#119) | Ends Open Anyway. Stapling both means the first open works offline. The self-signed certificate stays as the fallback for builds without the Developer ID, so contributors can still build |
| 2026-10-30 | The map is drawn by the app from Natural Earth's outlines, projected with Equal Earth and simplified at build time to 99 KB (#113) | No map tiles, so no map service learns where you look, and nothing to download. Equal Earth keeps areas true, so a big shaded country is big |
| 2026-10-30 | Anycast networks (Cloudflare, Fastly and others) are listed beside the map, not painted (#113) | DB-IP files their addresses under one country, but the answer came from the nearest data centre: on a Mac in Lebanon, Cloudflare read as Canada |
| 2026-10-30 | The map redraws on its own only while Live is on, and Live switches off when the window closes (#113) | A still map costs nothing; animation is opted into, and never runs where nobody can see it |
| 2026-10-30 | Connections' controls are a window toolbar plus one labelled row, as Calendar and Finder lay theirs out (#123) | Six controls in four sizes across three rows read as all over the place. The view, the period and search are the window's; what the list is grouped by and narrowed to belongs to the list |
| 2026-10-30 | Usage History reads as Connections does: the period and ‹ Today › side by side with All / Wi-Fi / Hotspot in one row, then the heading, with the chips beside it (#125) | The same controls were scattered there too. The parts are shared (PeriodControls.swift), so the two windows can't drift apart |
| 2026-10-30 | Settings › Connections, a pane like every other feature's (#126) | People look for a feature in Settings. It says what Connections is, opens it, shows today's top companies, sets the layout it opens with, and says where the names come from |
| 2026-10-30 | The content filter is a system extension that allows everything and reports each flow's name over XPC, turned on only from Settings › Connections (#128) | Server names turn Hosted on Amazon into the company behind it, and it's the base the firewall's rules are enforced through. It fails open, and both ends check the other's signature by team |
| 2026-10-30 | Companies come from a server's name before its network's owner (#128) | The owner names the landlord; the name names the tenant. A host's own domains (cloudfront.net, akamaiedge.net) still defer to the owner |
| 2026-10-30 | Permissions are set up in a welcome window, each as what you get, not a switch in Settings that says how it works (#134) | "Server names" and "a content filter" didn't tell anyone why to turn it on. On first launch the welcome window offers website names and Wi-Fi names, both optional, with the steps macOS needs shown while it waits. Said no, each has a Turn On… where it's used. Location is no longer asked for by itself at launch, and Stop Filtering left the menu: it blocked nothing |
| 2026-10-30 | Blocking by rules made in Connections, enforced by the content filter (#114) | One rule per company, app, country or server: Block, Block on Hotspots or Always Allow, the narrowest winning. "Monitor" and "Custom" were the same once your rules always win, so the modes are one choice, Anywhere or Only where I allow, the second coming with blocked attempts in the window. The filter keys connections as Connections does, so a rule means the row it was made on |
| 2026-10-30 | Hotspot mode stops every app's updater at the network (#98) | The settings and hosts it changed reached only apps someone had researched. With the content filter, any program inside an app named like an updater, Sparkle's downloader, or an updater of its own is blocked on a hotspot, and so are the helper's update-only servers, from one list in HostsFile. Apple's code is left to the system setting, so an update started by hand goes, and your rules win |
| 2026-10-30 | Ask Me asks once per app and company, in a panel, and lets it go after 30 seconds (#129) | A question per connection would be dozens a minute. Holding every flow between an app and a company on one question keeps it to the first time. The answers are rules for that app, so rules gained an optional app; Allow Once lasts an hour. A panel that never takes the focus, and a connection that goes ahead unanswered, keep a missed question from breaking anything |
| 2026-10-30 | The country and network data refreshes itself monthly, on by default, signed (#121) | Connections is only as right as its data, and DB-IP's changes every month. The whole file, the same for everyone, from the repository's geodata release: nothing about which servers this Mac talks to. An Ed25519 signature over a manifest of SHA-256s, with the public half in the app, so a tampered file is refused. On by default, with a switch, since it's the app's one network access of its own. Never on a hotspot, metered or in Low Data Mode |
| 2026-10-30 | The helper is a launch daemon the app registers, answering over XPC (#131) | With a Developer ID there's a team to check callers by, so SMAppService and XPC replace the sudoers rule: allowed with a switch in System Settings, listed under NetworkMonitor, updated with the app. Each request runs the same binary with one verb, so verbs behave as they did. The hourly self-heal job stays, installed by the daemon from a copy outside the app, because macOS doesn't unregister a daemon whose app was dragged to the Trash, and its program goes with the app. Builds without a Developer ID keep sudo
|
| 2026-10-30 | It updates itself with Sparkle 2 (#132) | People watched the repository for releases and dragged in a new disk image by hand. Sparkle 2.10.0, pinned, checks the newest release's appcast daily (releases/latest/download/appcast.xml, published with each release, so no extra hosting), never on a metered connection unless asked, and installs only with the person's OK and an EdDSA signature from a key whose public half is in the app. Its XPC services are left out, as the app isn't sandboxed. A menu bar app uses Sparkle's gentle reminders: in the Dock while an update is shown |
| 2026-10-30 | An Android phone is recognised by what it says, and any network can be marked by hand (#109) | A Samsung hotspot came up as Wi-Fi: Android now picks a random subnet, so the 192.168.42/43 convention missed it, and macOS ignores Android's own metered flag. Android's tethering marks every DHCP reply ANDROID_METERED (option 43), which configd keeps where any app can read it, with no permission. Ranked just after macOS's own flag. This network in Settings › Hotspot settles whatever is still missed, or wrongly caught |
| 2026-10-30 | An update that got through is reported by the data its app used while it downloaded (#95) | The size of the staged update is what's left on disk, unpacked: Slack's read 271.6 MB for about 120 MB of hotspot. Each framework creates its download folder or file first, so its creation and last change bound the download, and the app's traffic is counted by the minute in between. History's hourly totals are too coarse. When none was seen, the size on disk is shown as up to |
| 2026-10-30 |
What's new is written before tagging, in ReleaseNotes/, and shown in the update window (#152) |
The appcast linked to the GitHub release page, about 160 KB of GitHub's own page in Sparkle's small notes pane, and its notes were written after publishing. Now each version's Markdown goes in the appcast's <description>, which Sparkle renders since 2.9, and the same file becomes the release's notes, so the two can't differ. The workflow fails without it |
| 2026-10-30 | The update window shows every version since the one installed (#163) | One version's notes left someone going from 1.15.0 to 1.16.1 with only 1.16.1's line. The appcast now carries the ten newest versions' notes as HTML, each in a div tagged with its build. Sparkle 2.5 and later marks the installed one, and CSS hides it and everything older. That runs in the copy already installed, so it works from 1.15 on. Markdown would need a delegate in the app, which only later versions could have |
| 2026-10-30 | Every update that gets through is noticed, and a pause that didn't hold says so (#94) | On 27 October Claude Code's update went unseen, and Settings went on listing Slack and Chrome as paused after both had updated. Now Claude Code's staging and versions folders are watched as download places, measured by the traffic of the folder it runs from. An app in Applications whose version changes on the connection is reported, joined to its download if one was seen. Updaters with an app of their own are named for the app they update, from a short table, only when that app is installed. A paused app that got through reads updated anyway |
| 2026-10-30 | Updates that got through are kept in Usage History (#96) | The Got through list holds one connection, and the next replaces it, while in History an update was lost inside its app's normal traffic. Each is kept in history.sqlite once per report, the watch's list for one connection, with the visit it was first seen on, since History can split one connection into several visits. Sessions say how many got through and list them when chosen, Export CSV gains a column, and retention and Clear History take them too. The list saved before is moved in once |
| 2026-10-31 | Website names leave Settings once they're on (#181) | A permission that's been given is something to forget, not to manage: a green On and a Turn Off only said it was there. It's switched off where macOS keeps it, System Settings › Network › Filters, which the app follows, and Turn On… comes back |
| 2026-10-31 | The map is always live: it pulses to the countries answering right now, with no switch (#182) | Where your apps reach as it happens is what the map is for, and a switch was one more thing to turn on each time. Only the pulses redraw each frame, on a layer of their own, and only while something answers and someone can see the window. A past period or Blocked stays still: pulses about now would mix two times |
| 2026-10-31 | Settings › Hotspot's lists are one line per app, with how or why behind an ⓘ (#185) | A second line under every app, and a line per app under each tier, made 13 rows a wall of text where no switch was plainly whose. How an app is paused, or why it can't be, is asked about now and then, so it waits behind the ⓘ. What needs attention stays in view: updated anyway, paused once you quit it, and Paused once you allow the helper below |
| 2026-10-31 | Settings › General explains its rows behind an ⓘ, and Settings › Connections drops Today and the Map row (#189) | A paragraph under every section was more to read than to use, as in Hotspot (#185). Today's companies are what the Connections window opens on, one click away, and which outlines the map is drawn from is nothing to choose or act on. DB-IP's credit stays: its licence asks for it |
| 2026-10-31 | History's chips sit at the end of the Apps · Networks · Sessions row, not beside the heading (#192) | A chip is added by clicking a row in those lists, so it belongs beside them: at the heading it appeared far from the click, at the top of the pane |
| 2026-10-31 | Connections made by address are named from their handshake (#196) | Only 15% of Chrome's servers and 7% of Figma's had a name: Chromium connects by address. The TLS ClientHello and QUIC's Initial packets carry the site's name for the network to read, so the filter reads that and nothing after it. It holds the first bytes rather than passing them piecemeal, which means the same whichever way the data verdict's offsets count, and it's what blocking by name will need (#197) |
| 2026-10-31 | Rules by site name (#197) | An address rule stops working when a CDN moves the name, and a company rule on a host's network catches too much. A site rule covers a name and everything under it, between a server and an app in precedence. A nameless connection is decided again once its handshake names it, so the rule reaches Chrome too. The site offered is the registrable domain, from a short list of multi-label suffixes rather than the whole public suffix list: a rarer suffix offers a narrower site, never a wider one |
| 2026-11-01 | Ads, trackers and malware blocked by list, in every app (#176) | Neither PureSnitch nor Snitch does it system-wide on a Mac: one needs apps pointed at its DNS proxy by hand, the other blocks a list's addresses for every app through pf, at most 5,000. The content filter matches names, its own or the handshake's (#196), after your rules. Lists are StevenBlack's hosts (MIT) and URLhaus by malware-filter (CC0): tracker-only lists are nearly all GPL, so HaGeZi waits for a licence check. Stored as sorted 64-bit hashes, about 8 bytes a name; built and signed weekly by a workflow with the country data's key, refused if a list shrinks or catches a site people use every day. Off until switched on |
| 2026-11-01 | Block Everything, in the right-click menu (#198) | A way to stop everything at once, on a hotspot when a big download starts or when something looks wrong, as Snitch's Lockdown and Little Snitch's Deny All do. It comes before every rule, so no Always Allow opens a way round it, and leaves only the essentials and NetworkMonitor itself, so it can always be turned off. The readout turns red so it's never left on by mistake |
| 2026-11-01 | The filter's country data carries its month, and a copy without one is still used (#209) |
bundle.sh moved the tables into the filter without MONTH, and ServerDirectory.newest only used a copy whose month it could read, so until a newer month was downloaded the filter had no countries or network owners: country rules blocked nothing. Now the month travels with the tables, a copy without it is used rather than nothing, and the release workflow checks it's there |
| 2026-11-01 | Ask Me names the site, and can allow until the app quits (#199) | Figma wants to connect to Amazon hid what Figma reached. The panel now leads with the site, read from the handshake when the app connected by address, and the answer can cover the site or the whole company, once, until the app quits, or always. Questions stay one per app and company, and per site only on a host's network, so there are no more of them than before. "Until it quits" is a rule for that app that the app removes when it quits, and at launch for apps that quit meanwhile |
| 2026-11-01 | Rules for one network (#200) | Block on Hotspots was the only rule that depended on where the Mac is; Little Snitch's profiles switch with the network. A rule can carry the network it was made on, by the router's identity, and says nothing anywhere else, so the next rule decides there. That's the useful part of profiles without profiles to manage |
| 2026-11-01 | Rules export and import as JSON (#201) | Rules couldn't be moved to another Mac or backed up. The file says what it is (networkMonitorRules), so a stray JSON file is refused, and a plain list of rules is read too. Import never replaces a rule that's already there: the one on this Mac was chosen on this Mac |
| 2026-11-01 | Rules about an app follow the team that signed it (#202) | A rule named an app by its path, so anything put at that path inherited it. A rule now keeps the signing team from when it was made, read from the app's code signature, and the filter compares it with the team of the program connecting, read from its audit token. Apps without a team are matched by path, since there's nothing to compare |
| Approach | Why not |
|---|---|
getifaddrs() counters |
32-bit, wrap every 4 GiB |
nettop cumulative mode |
totals fall when sockets close, under-counting browsers |
A pipe for nettop's output |
block-buffered: data arrives in ~30-second bursts |
/dev/null or an inherited stdin for nettop
|
it spins at ~140% of a core |
FileHandle.availableData on the PTY |
raises an Objective-C exception Swift can't catch |
nettop -P |
one row per process, losing the remote address needed for the LAN split |
| Switching per-app tracking off on battery | the cost was a bug; it switched attribution off on hotspots |
| Subtracting multicast from the total | made the total smaller than its own rows |
| The Wi-Fi name (SSID) as identity | many networks share a name, and one router can have two; it's a label instead (#85) |
| The access point's address (BSSID) as identity | changes on every roam |
| Resetting on every path change | path changes fire several times per event; would wipe hotspot totals |
| A rolling 24-hour reset | drifts to arbitrary hours; midnight matches how carriers count |
| A digits-only monospaced font in the menu bar | width wandered between 97 and 126 points |
A two-line NSButton title |
clipped the top line at every size |
| A template image for the readout | recoloured to monochrome, losing the green |
| A LaunchAgent for start at login | generic "exec" tile in Login Items without a Team ID |
| Freezing the app list's order while open | big downloads sat under tiny apps |
| A variable-height popover | re-anchored many times per animation and jumped |
| A Network Extension content filter for Hotspot mode | blocking traffic is the wrong tool for stopping updates, and it needs a Developer ID. Reversed by #98, once the filter existed for Connections: it now stops the updaters no setting reaches |
| Unloading updaters' LaunchAgents | puts the app in the business of managing background items; churns Login Items |
| Blocking update hosts for every Electron app | breaks VS Code Remote-SSH and the Claude app |
Pausing (SIGSTOP) an update already downloading |
the download belongs to a system daemon; servers time out; a stopped process gets stranded |
| Writing settings while the app is running | the app writes its own settings back on quit |
| A built-in list of updater names | rots, and describes one machine |
| A per-app list or allowlist for Hotspot mode | a switch that needs configuring has failed |
| Telling security updates from feature updates | not reliably possible |
launchctl disable |
persists across reboots and outlives the app |
| System Settings' stored Low Data Mode setting | reaches wired connections only |
| Ookla's CLI, M-Lab NDT, our own speed test engine | licence; publishing users' IPs; worse than the one on every Mac |
networkQuality in parallel mode |
upload and download compete and both read low |
A time limit (-M) on the speed test |
a capped run drops figures that hadn't converged |
| Showing RPM / responsiveness | Apple's own unit, blending latency inside saturated connections with what other apps feel |
| JSON for Usage History | wrong shape for range queries and sums |
| Importing usage from before History existed | it has no hours; spreading it out would invent data |
| Four pop-up menus to filter History | the lists themselves are the filters |
@testable import |
compiled in debug but broke release builds |
| Reverse DNS to name servers | Sends a query for every address, so the monitor would make traffic, and the answers are often meaningless (1e100.net) |
| A geolocation service online | Tells a third party every server the Mac talks to |
| Reading DNS replies in the content filter to name servers | With the Mac's DNS at 1.1.1.1 or 8.8.8.8, Chrome, Figma and Teams look names up over HTTPS, so the replies never show them; and a filter doing it has been reported to deadlock mDNSResponder in the kernel (#196) |
| Downloading DB-IP's data at build time | Old months are removed, so builds would break and old tags couldn't be rebuilt |
| Map tiles from Apple Maps or OpenStreetMap | Every pan and zoom would tell a map service where you're looking; outlines drawn locally need nothing |
Using NetworkMonitor
- Installing, updating and removing
- Features
- Connections
- Usage History
- Speed test
- Hotspot mode
- Accuracy and battery
- Privacy
- Security policy
Working on NetworkMonitor