Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,16 @@ What the numbers are, stated permanently on screen rather than in a footnote:
The opening two rounds and each destination's first answer pay for DNS, TCP and TLS, so they are
counted but kept out of every timing statistic and every chart. Left in, they pinned the top of the
y-axis near a second for the rest of the session and flattened every real measurement into a line
along the bottom. Rounds fire every 1, 2, 3, 5 or 10 seconds; the interval is the pause *between*
rounds, and a round waits for all ten destinations to answer or time out first, so a struggling link
stretches the gap rather than piling requests up. Spots are compared by the **median of each destination's own median**, over only
along the bottom. Rounds fire every 0.25, 0.5, 1, 2, 3, 5 or 10 seconds; the interval is the
pause *between* rounds, and a round waits for all ten destinations to answer or time out first, so a
struggling link stretches the gap rather than piling requests up. That back-pressure is what makes
the sub-second settings safe for the browser: measured at **0.306 s per round** at a 250 ms gap, flat
from the first round to the four-thousandth sample.

Safe for the browser is not the same as polite to ten other people's servers, so choosing 0.25 s or
0.5 s raises a notice saying what it costs — up to roughly 2,000 requests to each destination over
ten minutes, and a destination whose bot protection starts refusing will show up as a dead card when
it is really a rate limit. It is for a short sweep of a problem area, not a whole building. Spots are compared by the **median of each destination's own median**, over only
the destinations that produced a median at *every* spot — pooling raw samples instead would make the
figure lurch when a destination dropped out, reporting a change in which destinations answered as
though it were a change in latency.
Expand Down
5 changes: 3 additions & 2 deletions src/components/PrivacySafetyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ export const THIRD_PARTY_DISCLOSURES: { host: string; receives: string }[] = [
'www.atlassian.com',
receives:
'Your IP, repeatedly, for as long as a Walk & Test run lasts — every one of them is probed ' +
'once per round, so a ten-minute walk is a few hundred requests to each, up to roughly six ' +
'hundred at the one-second interval. Each request is a HEAD for one small public file and ' +
'once per round, so a ten-minute walk is a few hundred requests to each, and closer to two ' +
'thousand at the quarter-second interval. Each request is a HEAD for one small public file ' +
'and ' +
'carries no cookies (credentials: omit), so these hosts see an address and a TLS handshake ' +
'rather than a logged-in user. Several of them are advertising businesses; the IP and the ' +
'timing pattern are still theirs to log.',
Expand Down
19 changes: 19 additions & 0 deletions src/components/WalkTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
INTERVAL_CHOICES,
MAX_STORED_SAMPLES,
PROBE_TIMEOUT_MS,
SUB_SECOND_INTERVAL_MS,
WALK_TARGETS,
WARMUP_ROUNDS,
buildWalkConclusions,
Expand Down Expand Up @@ -619,6 +620,24 @@ export const WalkTest: React.FC<WalkTestProps> = ({ onHistoryUpdate }) => {
)}
</div>

{/* Not a warning about the browser, which copes. A warning about what
this sends to ten other people's servers. */}
{intervalMs <= SUB_SECOND_INTERVAL_MS && (
<div className="flex items-start gap-2 bg-amber-500/10 border border-amber-500/25 rounded-xl p-3 text-[11px] text-amber-100 leading-relaxed">
<Info className="w-3.5 h-3.5 shrink-0 mt-0.5" />
<span>
At {intervalMs / 1000}s this stops sampling these ten services and starts putting
sustained traffic on them: up to about{' '}
{(Math.round(600 / ((intervalMs + 50) / 1000) / 100) * 100).toLocaleString()} requests
to each over ten minutes, fewer on a slow link because each round waits for the
previous one. The browser copes, and the loop waits for each round to finish so it
cannot pile requests up. Their bot protection is the thing to watch — a destination
that starts refusing will show here as a dead card when it is really a rate limit.
Use it for a short sweep of a problem area, not for a whole building.
</span>
</div>
)}

{/* The one control that has to be reachable one-handed, mid-walk. */}
{isWalking && (
<div className="flex flex-wrap items-center gap-3 border-t border-slate-800 pt-4">
Expand Down
18 changes: 15 additions & 3 deletions src/utils/walkTest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DEFAULT_INTERVAL_MS,
INTERVAL_CHOICES,
MAX_STORED_SAMPLES,
SUB_SECOND_INTERVAL_MS,
WALK_TARGETS,
WARMUP_ROUNDS,
buildWalkConclusions,
Expand Down Expand Up @@ -485,12 +486,23 @@ describe('WALK_TARGETS', () => {
});

describe('INTERVAL_CHOICES', () => {
it('offers a one-second tick', () => {
// Safe because the loop awaits a whole round before starting the timer, so
// the interval is a gap between rounds rather than a fixed cadence.
it('offers sub-second ticks', () => {
// Safe for the browser because the loop awaits a whole round before starting
// the timer, so the interval is a gap between rounds rather than a fixed
// cadence and requests cannot pile up. Measured at 0.306s per round at a
// 250ms gap, flat from the first round to the four-thousandth.
expect(INTERVAL_CHOICES).toContain(250);
expect(INTERVAL_CHOICES).toContain(500);
expect(INTERVAL_CHOICES).toContain(1000);
});

it('flags exactly the choices that generate sustained third-party traffic', () => {
// Safe for the browser is not the same as polite to ten other people's
// servers, and the UI warns on the second question, not the first.
const flagged = INTERVAL_CHOICES.filter((ms) => ms <= SUB_SECOND_INTERVAL_MS);
expect(flagged).toEqual([250, 500]);
});

it('is ordered fastest first and holds the default', () => {
expect([...INTERVAL_CHOICES]).toEqual([...INTERVAL_CHOICES].sort((a, b) => a - b));
expect(INTERVAL_CHOICES).toContain(DEFAULT_INTERVAL_MS);
Expand Down
22 changes: 20 additions & 2 deletions src/utils/walkTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,14 @@ export const DEFAULT_INTERVAL_MS = 3000;
* in a round to answer or time out before it starts the timer. A round in which
* everything times out therefore takes `PROBE_TIMEOUT_MS`, and the effective
* tick stretches to match rather than requests piling up on a struggling link.
* That is what makes the one-second option safe.
* That back-pressure is what makes the sub-second options safe for the browser.
*
* It does not make them polite. At a quarter-second gap a ten-minute walk is
* well over a thousand requests to each of ten third parties, which is a
* different kind of cost from the one the browser pays — see
* `SUB_SECOND_INTERVAL_MS` and the notice the UI shows when one is selected.
*/
export const INTERVAL_CHOICES = [1000, 2000, 3000, 5000, 10000] as const;
export const INTERVAL_CHOICES = [250, 500, 1000, 2000, 3000, 5000, 10000] as const;

/**
* Rounds discarded from every timing statistic and every chart at the start of
Expand All @@ -82,6 +87,19 @@ export const INTERVAL_CHOICES = [1000, 2000, 3000, 5000, 10000] as const;
*/
export const WARMUP_ROUNDS = 2;

/**
* At or below this gap, the walk is generating sustained traffic to ten third
* parties rather than sampling them, and the UI says so.
*
* A quarter-second gap resolves to roughly three rounds a second once each
* round's own duration is added, so ten minutes is on the order of 1,800
* requests per destination. Nothing breaks, and several of these operators run
* bot protection that may reasonably start refusing — which would then show up
* on the cards as a dead destination that is really a rate limit. Worth
* knowing before reading the result.
*/
export const SUB_SECOND_INTERVAL_MS = 500;

/**
* Rounds visible in the live charts. A single bad spike would otherwise hold the
* y-axis at its height for the rest of the walk. The tables and the saved record
Expand Down
Loading