diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/util/LogDump.kt b/strawApp/src/main/kotlin/com/sulkta/straw/util/LogDump.kt index a34d1ccf4..cacb6e3f9 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/util/LogDump.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/util/LogDump.kt @@ -211,9 +211,24 @@ object LogDump { } } // Long high-entropy runs (hashes, visitor data, unlabeled - // tokens): 20+ [A-Za-z0-9_-] chars containing a digit. Scrubbed - // in BOTH profiles — visitorData & friends are creds, not signal. - s = LONG_TOKEN_RE.replace(s, "") + // tokens): 20+ [A-Za-z0-9_-] chars containing a digit. Scrubbed in + // BOTH profiles — visitorData & friends are creds, not signal. On the + // dogfood profile a token that is exactly a channel/playlist id + // (UC…, PL…/UU…/LL…/RD…/OLAK5uy_…) is KEPT — those ride past the + // skipped id passes above but LONG_TOKEN would otherwise redact them, + // and they're the same triage signal as the video id we keep. Every + // other high-entropy run (visitorData, hashes) still redacts. + s = if (keepIdentifiers) { + LONG_TOKEN_RE.replace(s) { m -> + if (CHANNEL_ID_RE.matches(m.value) || PLAYLIST_ID_RE.matches(m.value)) { + m.value + } else { + "" + } + } + } else { + LONG_TOKEN_RE.replace(s, "") + } // IP addresses (v4 + v6 — v6 patterns are shaped so threadtime // HH:MM:SS timestamps can never match). s = IPV4_RE.replace(s, "") diff --git a/strawApp/src/test/kotlin/com/sulkta/straw/util/LogDumpScrubTest.kt b/strawApp/src/test/kotlin/com/sulkta/straw/util/LogDumpScrubTest.kt index 1a3b7d0b1..a59a667d8 100644 --- a/strawApp/src/test/kotlin/com/sulkta/straw/util/LogDumpScrubTest.kt +++ b/strawApp/src/test/kotlin/com/sulkta/straw/util/LogDumpScrubTest.kt @@ -72,6 +72,28 @@ class LogDumpScrubTest { assertFalse(LogDump.scrubLine(line).contains("dQw4w9WgXcQ")) } + @Test + fun dogfoodKeepsChannelAndPlaylistIdFullScrubs() { + // These are ≥20 chars so LONG_TOKEN would otherwise redact them even + // on the dogfood profile; the channel/playlist exemption keeps them. + val channelId = "UCuAXFkgsw1L7xaCfnd5JJOw" // UC + 22, has digits + val playlistId = "PLbpi6ZahtOH6Blw3RGYpWkSByi_T7Rygb" + assertEquals(channelId, LogDump.scrubLineDogfood(channelId)) + assertEquals(playlistId, LogDump.scrubLineDogfood(playlistId)) + // Full profile redacts them to their labelled placeholders. + assertEquals("", LogDump.scrubLine(channelId)) + assertEquals("", LogDump.scrubLine(playlistId)) + } + + @Test + fun dogfoodStillScrubsGenericHighEntropyToken() { + // visitorData-shaped: ≥20 chars with a digit, NOT a channel/playlist + // prefix → still `` on the dogfood profile (it's a cred). + val visitorData = "CgtVQzEyMzQ1Njc4OTBhYg" + assertEquals("", LogDump.scrubLineDogfood(visitorData)) + assertEquals("", LogDump.scrubLine(visitorData)) + } + // ---- B2: dogfood profile STILL scrubs real credentials / PII --------- @Test