Metadata-Version: 2.4
Name: detrack
Version: 0.4.0
Summary: Strip tracking parameters from URLs. Deterministically. Zero dependencies.
Author-email: Emiliano Gandini Outeda <emiliano.gandini@protonmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/emiliano-go/detrack
Project-URL: Source, https://github.com/emiliano-go/detrack
Project-URL: Issues, https://github.com/emiliano-go/detrack/issues
Keywords: seo,tracking,privacy,utm,url-cleaner,url,clean,analytics
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Dynamic: license-file

<h1 align="center">detrack</h1>

<p align="center">
  <strong>Strip tracking parameters from URLs. Deterministically. Zero dependencies.</strong>
</p>

<p align="center">
  <a href="https://pypi.org/project/detrack/">
    <img src="https://img.shields.io/pypi/v/detrack?logo=pypi&logoColor=white&style=for-the-badge" alt="PyPI">
  </a>
  <a href="https://www.python.org/downloads/">
    <img src="https://img.shields.io/pypi/pyversions/detrack?logo=python&logoColor=white&style=for-the-badge" alt="Python">
  </a>
  <a href="LICENSE">
    <img src="https://img.shields.io/pypi/l/detrack?logo=opensourceinitiative&logoColor=white&style=for-the-badge" alt="License">
  </a>
  <a href="https://github.com/emiliano-go/detrack">
    <img src="https://img.shields.io/badge/dependencies-none-brightgreen?style=for-the-badge" alt="no dependencies">
  </a>
</p>

## Install

```bash
pip install detrack
```

## Quick start

```python
import detrack

url = "https://example.com/post?utm_source=twitter&q=python&fbclid=123"
result = detrack.clean(url)

print(result.url)
# "https://example.com/post?q=python"

print(result.removed_params)
# {"utm_source": "twitter", "fbclid": "123"}

print(result.cleaned_params)
# {"q": "python"}

# Quick check if tracking was found
if result.has_tracking:
    print(f"Stripped: {list(result.removed_params.keys())}")
    # Stripped: ['utm_source', 'fbclid']
```

## Why detrack?

Other URL cleaners do too much (host remapping, site-specific rules, semantic rewriting), while `detrack` does one thing and does it well: remove tracking parameters.

This makes `detrack` predictable, testable, and trivial to integrate.

## Ecosystem

`detrack` is the shared cleaning layer for the [seoslug](https://github.com/emiliano-gandini-outeda/seoslug) (SEO metadata) and [tagurl](https://github.com/emiliano-gandini-outeda/tagurl) (semantic tagging) libraries.

## Configuration

`detrack` ships with sensible defaults. Override them globally with `configure()`, or per-call with a `Settings` object.

### Global configuration

```python
from detrack import configure

# Raise the query length limit to 16KB
configure(max_query_length=16384)
```

### Per-call override

```python
from detrack import Settings, clean_query

# This call uses a 2KB limit, ignoring the global setting
clean_query(query, settings=Settings(max_query_length=2048))
```

### `Settings`

```python
@dataclass
class Settings:
    max_query_length: int = 8192  # queries longer than this are returned unchanged
    use_prefixes: bool = True     # strip params matching known prefixes (utm_*, mtm_*, etc.)
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_query_length` | `int` | `8192` | Maximum query string length (in characters). Longer queries are returned unchanged to prevent abuse. |
| `use_prefixes` | `bool` | `True` | When `True`, any param starting with a known prefix (`utm_`, `mtm_`, `hsa_`, `pk_`, etc.) is stripped even if not listed explicitly. |

---

## Examples

### Basic

```python
>>> detrack.clean("https://example.com?utm_source=twitter&q=python")
DetrackResult(url="https://example.com?q=python", cleaned_params={"q": "python"},
              removed_params={"utm_source": "twitter"})
```

### Multiple trackers stripped

```python
>>> detrack.clean("https://example.com?a=1&utm_source=x&b=2&fbclid=y&c=3")
DetrackResult(url="https://example.com?a=1&b=2&c=3",
              cleaned_params={"a": "1", "b": "2", "c": "3"},
              removed_params={"utm_source": "x", "fbclid": "y"})
```

### All params stripped (query removed entirely)

```python
>>> detrack.clean("https://example.com?utm_source=x&fbclid=y")
DetrackResult(url="https://example.com",
              cleaned_params={},
              removed_params={"utm_source": "x", "fbclid": "y"})
```

### Custom patterns

```python
>>> detrack.clean("https://example.com?session=abc123&page=1", patterns=["session"])
DetrackResult(url="https://example.com?page=1",
              cleaned_params={"page": "1"},
              removed_params={"session": "abc123"})
```

### Query string only

```python
>>> detrack.clean_query("a=1&utm_source=x&b=2")
"a=1&b=2"

>>> detrack.clean_query("utm_source=x&fbclid=y")
""
```

## API

### `detrack.clean(url, patterns=None, settings=None)`

Strip tracking parameters from a full URL.

| Parameter | Type | Description |
|-----------|------|-------------|
| `url` | `str` | Any URL string |
| `patterns` | `Iterable[str] \| None` | Optional param names to strip (defaults to `DEFAULT_PATTERNS`) |
| `settings` | `Settings \| None` | Optional per-call settings override (defaults to `DEFAULT_SETTINGS`) |

**Returns:** [`DetrackResult`](#detrackresult) -> dataclass with cleaned URL and metadata.

**Raises:** Nothing -> pure function, no exceptions.
Malformed URLs pass through unchanged. Queries exceeding `max_query_length` are returned unchanged.

---

### `detrack.clean_query(query, patterns=None, settings=None)`

Strip tracking parameters from a query string only.

| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | `str` | URL query string, e.g. `"a=1&utm_source=x&b=2"` |
| `patterns` | `Iterable[str] \| None` | Optional param names to strip |
| `settings` | `Settings \| None` | Optional per-call settings override (defaults to `DEFAULT_SETTINGS`) |

**Returns:** `str` -> cleaned query string. Returns the input unchanged if it's malformed or exceeds `max_query_length`.

---

### `detrack.clean_url(url, patterns=None, settings=None)`

Convenience shorthand — returns just the cleaned URL string.

| Parameter | Type | Description |
|-----------|------|-------------|
| `url` | `str` | Any URL string |
| `patterns` | `Iterable[str] \| None` | Optional param names to strip |
| `settings` | `Settings \| None` | Optional per-call settings override |

**Returns:** `str` -> cleaned URL.

```python
>>> from detrack import clean_url
>>> clean_url("https://example.com?utm_source=twitter&q=python")
'https://example.com?q=python'
```

---

### `detrack.clean_batch(urls, patterns=None, settings=None)`

Strip tracking parameters from multiple URLs at once.

| Parameter | Type | Description |
|-----------|------|-------------|
| `urls` | `Iterable[str]` | URLs to clean |
| `patterns` | `Iterable[str] \| None` | Optional param names to strip |
| `settings` | `Settings \| None` | Optional per-call settings override |

**Returns:** `list[DetrackResult]` -> one result per input URL.

```python
>>> from detrack import clean_batch
>>> urls = [
...     "https://example.com?a=1&utm_source=x",
...     "https://example.com?fbclid=y&b=2",
... ]
>>> results = clean_batch(urls)
>>> [r.url for r in results]
['https://example.com?a=1', 'https://example.com?b=2']
>>> [r.has_tracking for r in results]
[True, True]
```

---

### `detrack.configure(**kwargs)`

Update global settings. Only specified fields are changed.

```python
from detrack import configure

configure(max_query_length=16384)
```

**Raises:** `TypeError` for unknown keyword arguments.

---

### `detrack.DEFAULT_PATTERNS`

```python
frozenset[str]  # 330+ common tracking parameters
```

Covers 20+ platforms: UTM (50+ variants), Google Ads/Analytics, Facebook/Meta,
TikTok, LinkedIn, Spotify, HubSpot (18 params), Matomo/Piwik, Adjust, AppsFlyer,
Branch.io, Yandex, Microsoft/Bing, Pinterest, Snapchat, Quora, AT Internet,
Adobe/Marketo, Coremetrics, MyTracker, email marketing (Mailchimp, Klaviyo, etc.),
affiliate networks (CJ, Awin, etc.), cache busters, session IDs, and redirect params.

Prefix matching is enabled by default: any param starting with `utm_`, `mtm_`,
`hsa_`, `pk_`, `af_`, `adj_`, `at_`, `cm_`, `bsft_`, `mc_`, `ir_`, `fb_`,
`hs_`, `piwik_`, `mt_`, `vgo_`, `sms_`, `eml_`, or `nb_` is also stripped.

Pass a custom `patterns` list to `clean()` to override entirely.

---

### `detrack.PREFIXES`

```python
tuple[str, ...]  # 19 prefixes: ("utm_", "mtm_", "pk_", "hsa_", ...)
```

The prefixes used for automatic param matching when `use_prefixes=True`. Useful for understanding what gets stripped or building custom logic.

---

### `detrack.DEFAULT_SETTINGS`

```python
Settings(max_query_length=8192, use_prefixes=True)
```

Global default settings instance. Modify with :func:`configure`.

---

### `detrack.__version__`

```python
"0.3.0"
```

Current library version string.

---

### `DetrackResult`

```python
@dataclass
class DetrackResult:
    url: str                       # Cleaned URL
    parsed_url: SplitResult        # urllib.parse result (for further processing)
    cleaned_params: dict[str, str] # Parameters that remain
    removed_params: dict[str, str] # Stripped parameters + their original values
    has_tracking: bool             # True if any tracking params were removed
```

**String behavior:** `str(result)` and f-strings return the cleaned URL directly.

```python
>>> result = clean("https://example.com?utm_source=x&q=1")
>>> f"{result}"
'https://example.com?q=1'
```

**Repr:** Compact, without `SplitResult` internals.

```python
>>> repr(result)
"DetrackResult(url='https://example.com?q=1', cleaned_params={'q': '1'}, removed_params={'utm_source': 'x'})"
```

**Unpacking:** Tuple unpacking yields `(url, cleaned_params, removed_params)`.

```python
>>> url, cleaned, removed = clean("https://example.com?utm_source=x&q=1")
>>> url
'https://example.com?q=1'
```

`removed_params` preserves the original values so you can log what was stripped
for analytics, debugging, or compliance.

## Features

- **330+ default patterns**: covers 20+ platforms — UTM, Google, Facebook, TikTok, LinkedIn, Spotify, HubSpot, Matomo, Adjust, AppsFlyer, and more
- **Prefix matching**: automatically strips params starting with `utm_`, `mtm_`, `hsa_`, `pk_`, etc. even if not listed explicitly
- **Case-insensitive matching**: `UTM_SOURCE`, `Utm_Source`, and `utm_source` are all stripped
- **Zero dependencies**: uses only `urllib.parse` from the Python standard library
- **Deterministic**: same input always yields the same output, across all systems
- **Pure functions**: no state, no I/O, no random numbers, no exceptions
- **Metadata returned**: `removed_params` tells you exactly what was stripped and its original value
- **`has_tracking`**: quick boolean check on the result — `if clean(url).has_tracking`
- **Batch cleaning**: process multiple URLs at once with `clean_batch(urls)`
- **Configurable**: query length guard, prefix matching, and pattern lists are all adjustable

---

See MIT [LICENSE](LICENSE).
