Skip to content

Commit d9412c5

Browse files
ErikChevalierFlintWaveclaude
authored
fix(engine): strip tracking params from the links results point to (#32)
UrlNormalizer stripped utm_*/fbclid/etc. but only for the dedup key; the aggregator stored the raw upstream URL, so the link a user clicks still carried trackers. Add UrlNormalizer.stripTracking (lossless apart from the tracker params: keeps scheme/host case, path, trailing slash, fragment, param order) and surface it as the result URL. Covers the in-app results and the served web page. Co-authored-by: FlintWave <flintwave@tuta.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2c52dc7 commit d9412c5

3 files changed

Lines changed: 65 additions & 8 deletions

File tree

app/src/main/java/org/searchmob/engine/aggregate/Aggregator.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ class Aggregator(
9191
buckets[key] =
9292
MutableBucket(
9393
title = item.title,
94-
url = item.url,
94+
// Surface a tracker-stripped URL so the clicked link drops utm_*/fbclid/etc;
95+
// `key` above is still the lossy normalized form used only for dedup.
96+
url = UrlNormalizer.stripTracking(item.url),
9597
snippet = item.snippet,
9698
engines = linkedSetOf(item.engineId),
9799
score = contribution,

app/src/main/java/org/searchmob/engine/aggregate/UrlNormalizer.kt

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,45 @@ package org.searchmob.engine.aggregate
33
import java.net.URI
44

55
/**
6-
* Normalizes a URL for dedup: lowercase scheme/host, drop a leading `www.`, strip a trailing slash,
7-
* and remove common tracking query parameters (`utm_*`, `fbclid`, `gclid`, ...). Remaining query
8-
* params are sorted so equivalent URLs collapse deterministically.
6+
* URL helpers for the aggregator. [normalize] builds a lossy dedup key (lowercase scheme/host, drop
7+
* a leading `www.`, strip a trailing slash, remove tracking params, sort the rest). [stripTracking]
8+
* removes the same tracking params but otherwise keeps the URL faithful, for the link shown/clicked.
99
*/
1010
object UrlNormalizer {
1111
private val trackingPrefixes = listOf("utm_")
1212
private val trackingKeys = setOf("fbclid", "gclid", "gclsrc", "dclid", "msclkid", "mc_eid", "igshid", "ref")
1313

14+
private fun isTracking(param: String): Boolean {
15+
val key = param.substringBefore("=").lowercase()
16+
return trackingKeys.contains(key) || trackingPrefixes.any { key.startsWith(it) }
17+
}
18+
19+
/**
20+
* Returns [rawUrl] with known tracking params removed, preserving everything else for display:
21+
* scheme/host case, path, trailing slash, fragment, and the order of the surviving params. This
22+
* is the link the user actually clicks, so unlike [normalize] (a lossy dedup key) it is kept
23+
* faithful apart from the trackers. Falls back to the trimmed input if it does not parse.
24+
*/
25+
fun stripTracking(rawUrl: String): String {
26+
val trimmed = rawUrl.trim()
27+
return try {
28+
val uri = URI(trimmed)
29+
val query = uri.rawQuery ?: return trimmed
30+
val kept =
31+
query
32+
.split("&")
33+
.filter { it.isNotBlank() }
34+
.filterNot { isTracking(it) }
35+
buildString {
36+
append(trimmed.substringBefore("?"))
37+
if (kept.isNotEmpty()) append("?").append(kept.joinToString("&"))
38+
uri.rawFragment?.let { append("#").append(it) }
39+
}
40+
} catch (_: Exception) {
41+
trimmed
42+
}
43+
}
44+
1445
fun normalize(rawUrl: String): String {
1546
val trimmed = rawUrl.trim()
1647
return try {
@@ -23,10 +54,7 @@ object UrlNormalizer {
2354
uri.query
2455
?.split("&")
2556
?.filter { it.isNotBlank() }
26-
?.filterNot { param ->
27-
val key = param.substringBefore("=").lowercase()
28-
trackingKeys.contains(key) || trackingPrefixes.any { key.startsWith(it) }
29-
}
57+
?.filterNot { isTracking(it) }
3058
?.sorted()
3159
?.joinToString("&")
3260
?.takeIf { it.isNotEmpty() }

app/src/test/java/org/searchmob/engine/UrlNormalizerTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,31 @@ class UrlNormalizerTest {
2828
UrlNormalizer.normalize("http://www.example.com/x/"),
2929
)
3030
}
31+
32+
@Test
33+
fun stripTrackingRemovesTrackersButKeepsTheRestForDisplay() {
34+
// Host case, www, trailing slash, fragment, and the surviving param order are all kept.
35+
assertEquals(
36+
"https://WWW.Example.com/Some/Path/?id=42&q=hi#frag",
37+
UrlNormalizer.stripTracking(
38+
"https://WWW.Example.com/Some/Path/?id=42&utm_source=n&q=hi&fbclid=a#frag",
39+
),
40+
)
41+
}
42+
43+
@Test
44+
fun stripTrackingDropsQueryWhenOnlyTrackers() {
45+
assertEquals(
46+
"https://example.com/p",
47+
UrlNormalizer.stripTracking("https://example.com/p?utm_campaign=x&gclid=y"),
48+
)
49+
}
50+
51+
@Test
52+
fun stripTrackingLeavesCleanUrlsUntouched() {
53+
assertEquals(
54+
"https://example.com/p/?id=1",
55+
UrlNormalizer.stripTracking("https://example.com/p/?id=1"),
56+
)
57+
}
3158
}

0 commit comments

Comments
 (0)