Skip to content

Commit fdff729

Browse files
committed
feat(telemetry): gate ssh.network.sampled changes behind a 15s cooldown
Latency changes now must clear both the 25 ms absolute and 20 % relative thresholds (previously either), and all change triggers (p2p flip, DERP change, latency swing) share a 15 s cooldown since the last emission. Suppressed changes coalesce into the next eligible emission instead of being lost; the 60 s heartbeat is unchanged. Worst-case emission rate drops from 28.8k to 5.76k events/day per connection.
1 parent 32dbc85 commit fdff729

3 files changed

Lines changed: 60 additions & 16 deletions

File tree

src/instrumentation/EVENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,10 @@ Emitted by `SshTelemetry`.
382382

383383
#### `ssh.network.sampled`
384384

385-
Tunnel network sample. Emitted on a p2p flip, a preferred-DERP change, a
386-
meaningful latency change (at least 25 ms or 20 %), or a roughly 60 s
387-
heartbeat.
385+
Tunnel network sample. Emitted on a roughly 60 s heartbeat, or on a p2p
386+
flip, a preferred-DERP change, or a meaningful latency change (at least
387+
25 ms and at least 20 %). Change-triggered emissions are limited to one per
388+
15 s; a change that persists past that cooldown is emitted when it expires.
388389

389390
| Attribute | Values |
390391
| ------------------------------ | ------------------------------------------------------------------ |

src/instrumentation/ssh.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NetworkInfo } from "../remote/sshProcess";
22
import type { TelemetryReporter } from "../telemetry/reporter";
33

44
const NETWORK_SAMPLE_INTERVAL_MS = 60_000;
5+
const NETWORK_CHANGE_COOLDOWN_MS = 15_000;
56
const NETWORK_LATENCY_CHANGE_RATIO = 0.2;
67
const NETWORK_LATENCY_MIN_ABSOLUTE_CHANGE_MS = 25;
78

@@ -141,15 +142,22 @@ export class SshTelemetry {
141142
}
142143
}
143144

144-
/** Emit on p2p flip, DERP change, large latency swing, or heartbeat interval. */
145+
/** Emit on the heartbeat interval, or on a p2p flip, DERP change, or large
146+
* latency swing once the change cooldown has elapsed. Suppression leaves the
147+
* last emitted sample in place, so a change that persists through the
148+
* cooldown is emitted when it expires rather than lost. */
145149
function shouldEmitSample(
146150
previous: NetworkSample,
147151
current: NetworkInfo,
148152
now: number,
149153
): boolean {
150-
if (now - previous.emittedAtMs >= NETWORK_SAMPLE_INTERVAL_MS) {
154+
const sinceLastEmit = now - previous.emittedAtMs;
155+
if (sinceLastEmit >= NETWORK_SAMPLE_INTERVAL_MS) {
151156
return true;
152157
}
158+
if (sinceLastEmit < NETWORK_CHANGE_COOLDOWN_MS) {
159+
return false;
160+
}
153161
if (current.p2p !== previous.p2p) {
154162
return true;
155163
}
@@ -168,7 +176,7 @@ function hasMeaningfulLatencyChange(
168176
}
169177
const absoluteChange = Math.abs(current - previous);
170178
return (
171-
absoluteChange >= NETWORK_LATENCY_MIN_ABSOLUTE_CHANGE_MS ||
179+
absoluteChange >= NETWORK_LATENCY_MIN_ABSOLUTE_CHANGE_MS &&
172180
absoluteChange / Math.abs(previous) >= NETWORK_LATENCY_CHANGE_RATIO
173181
);
174182
}

test/unit/instrumentation/ssh.test.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,35 +187,56 @@ describe("SshTelemetry", () => {
187187
name: "latency change below both thresholds (50 -> 51)",
188188
prev: {},
189189
next: { latency: 51 },
190-
advanceMs: 1_000,
190+
advanceMs: 15_000,
191191
expected: 1,
192192
},
193193
{
194-
name: "ratio branch only (50 -> 70)",
194+
name: "latency change meeting only the ratio threshold (50 -> 70)",
195195
prev: {},
196196
next: { latency: 70 },
197-
advanceMs: 1_000,
198-
expected: 2,
197+
advanceMs: 15_000,
198+
expected: 1,
199199
},
200200
{
201-
name: "absolute branch only (200 -> 230)",
201+
name: "latency change meeting only the absolute threshold (200 -> 230)",
202202
prev: { latency: 200 },
203203
next: { latency: 230 },
204+
advanceMs: 15_000,
205+
expected: 1,
206+
},
207+
{
208+
name: "latency change meeting both thresholds (50 -> 80)",
209+
prev: {},
210+
next: { latency: 80 },
211+
advanceMs: 15_000,
212+
expected: 2,
213+
},
214+
{
215+
name: "latency change within the cooldown (50 -> 80)",
216+
prev: {},
217+
next: { latency: 80 },
204218
advanceMs: 1_000,
219+
expected: 1,
220+
},
221+
{
222+
name: "p2p flip after the cooldown",
223+
prev: {},
224+
next: { p2p: false },
225+
advanceMs: 15_000,
205226
expected: 2,
206227
},
207228
{
208-
name: "p2p flip",
229+
name: "p2p flip within the cooldown",
209230
prev: {},
210231
next: { p2p: false },
211232
advanceMs: 1_000,
212-
expected: 2,
233+
expected: 1,
213234
},
214235
{
215-
name: "DERP region change",
236+
name: "DERP region change after the cooldown",
216237
prev: {},
217238
next: { preferred_derp: "SFO" },
218-
advanceMs: 1_000,
239+
advanceMs: 15_000,
219240
expected: 2,
220241
},
221242
{
@@ -229,7 +250,7 @@ describe("SshTelemetry", () => {
229250
name: "baseline established from zero-latency placeholder (0 -> 5)",
230251
prev: { latency: 0 },
231252
next: { latency: 5 },
232-
advanceMs: 1_000,
253+
advanceMs: 15_000,
233254
expected: 2,
234255
},
235256
])(
@@ -245,6 +266,20 @@ describe("SshTelemetry", () => {
245266
},
246267
);
247268

269+
it("coalesces a change suppressed by the cooldown into a later emission", () => {
270+
const { ssh, sink } = setup();
271+
272+
ssh.networkSampled(makeNetworkInfo());
273+
vi.advanceTimersByTime(3_000);
274+
ssh.networkSampled(makeNetworkInfo({ p2p: false }));
275+
vi.advanceTimersByTime(12_000);
276+
ssh.networkSampled(makeNetworkInfo({ p2p: false }));
277+
278+
const samples = sink.eventsNamed("ssh.network.sampled");
279+
expect(samples).toHaveLength(2);
280+
expect(samples[1].properties.p2p).toBe("false");
281+
});
282+
248283
it("includes p2p, preferred_derp, latency, and bandwidth in the emitted sample", () => {
249284
const { ssh, sink } = setup();
250285

0 commit comments

Comments
 (0)