{"id":50991141,"url":"https://github.com/franzos/enrichr","last_synced_at":"2026-06-20T03:05:43.818Z","repository":{"id":363661888,"uuid":"1264414782","full_name":"franzos/enrichr","owner":"franzos","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-09T21:53:43.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-09T22:05:05.020Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/franzos.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-09T21:34:37.000Z","updated_at":"2026-06-09T21:53:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/franzos/enrichr","commit_stats":null,"previous_names":["franzos/enrichr"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/franzos/enrichr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fenrichr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fenrichr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fenrichr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fenrichr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/franzos","download_url":"https://codeload.github.com/franzos/enrichr/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franzos%2Fenrichr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34555508,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-20T03:05:42.169Z","updated_at":"2026-06-20T03:05:43.810Z","avatar_url":"https://github.com/franzos.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# enrichr\n\nDatabase-independent analytics event enrichment library. Takes a `RawEvent` (URL, IP, user-agent, referrer, UTM params) and produces a clean `Event` with visitor id, location, device/browser/OS info, and traffic source — with no storage, no HTTP, no async. You own the database and the HTTP layer; this crate just does the enrichment pipeline. It replaces `amplyco-analytics`, inspired by [liwan](https://liwan.dev).\n\n## Privacy\n\n**A secret, high-entropy salt is not optional.** IPv4 has only 2³² addresses — a hash without a salt is a lookup table. Pass at least 128 random bits (16 bytes) of binary salt to `StaticSalt::new` or `ArcSwapSalt::new`.\n\n`MaskedHashedStrategy` is the recommended choice over `SaltedHasher`: it zeroes the last octet(s) before hashing (`IpMaskMode::Balanced` → /24 for IPv4, /56 for IPv6), so the hash never encodes a specific host address. The hash algorithm (sha256 vs blake3) is a performance/standardization choice, not a privacy one — the salt is what protects users.\n\n## Install\n\n```toml\n[dependencies]\nenrichr = \"0.1\"\n```\n\n## Usage\n\n```rust\nuse enrichr::{\n    Processor, RawEvent, EventKind, MaskedHashedStrategy, StaticSalt, IpMaskMode,\n};\nuse enrichr::hash::blake3::Blake3Hasher;\nuse enrichr::useragent::UaParserBuiltin;\nuse enrichr::classify::ReferrerListClassifier;\nuse chrono::Utc;\nuse std::net::{IpAddr, Ipv4Addr};\n\n// Build once, share behind an Arc — Processor is Send + Sync.\nlet processor = Processor::builder()\n    .visitor_id_strategy(MaskedHashedStrategy::new(\n        Blake3Hasher,\n        StaticSalt::new(vec![/* 16+ random bytes */]),\n        IpMaskMode::Balanced,\n    ))\n    .ua_parser(UaParserBuiltin::new())\n    .classifier(ReferrerListClassifier::new())\n    .keep_raw_referrer(false)   // true to preserve full referrer URL\n    .build();\n\nlet mut raw = RawEvent::new(\n    EventKind::PageView,\n    \"https://example.com/post?utm_source=newsletter\".into(),\n    Utc::now(),\n);\nraw.ip = Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5)));\nraw.user_agent = Some(\"Mozilla/5.0 (Macintosh; ...)\".into());\nraw.referrer = Some(\"https://www.google.com/\".into());\n\nlet event = processor.process(raw)?;\n// event.visitor_id  — base62 of the full digest (~43 chars), stable per (masked-ip, ua, entity)\n// event.referrer    — eTLD+1 (\"google.com\"), or None\n// event.traffic_source — category + source_name + medium (organic/social/referral/cpc)\n// event.device / .browser / .os / .bot   — device.device_type: mobile/tablet/desktop/bot\n// event.location    — None unless geoip feature + GeoIpDb configured\n```\n\nThe `visitor_id` field on `RawEvent` is an escape hatch: if you set it yourself, `Processor` passes it through unchanged — useful when you've already computed a hash upstream.\n\n## Features\n\nEverything beyond the core pipeline (`Processor`, `RawEvent`/`Event`, the `Hasher`/`SaltProvider`/`VisitorIdStrategy`/`UaParser`/`Classifier` traits, `mask_ip`) is feature-gated, so you only pull the dependencies you use.\n\n`default = [\"serde\", \"blake3\", \"useragent\", \"referrer-list\"]` — the batteries-included set: it hashes visitor ids, parses user agents, classifies referrers, and (de)serializes the output. `full` turns on everything.\n\n### Hashing\n\n| Feature | Adds | Pulls | Default |\n|---|---|---|---|\n| `blake3` | `Blake3Hasher` (32-byte BLAKE3 digest) — fast, recommended | `blake3` | yes |\n| `sha256` | `Sha256Hasher` (32-byte SHA-256) — standardized, pick it if an audit/compliance regime expects SHA-2 | `sha2` | no |\n\nThe built-in `SaltedHasher` / `MaskedHashedStrategy` are generic over `Hasher`, so **you need at least one of these two features** to use them out of the box — or implement `Hasher` (or the whole `VisitorIdStrategy`) yourself. The choice between BLAKE3 and SHA-256 is performance/standardization; neither protects users without a secret salt (see [Privacy](#privacy)).\n\n### Enrichment\n\n| Feature | Adds | Pulls | Default |\n|---|---|---|---|\n| `useragent` | `UaParserBuiltin` — device/browser/OS, a `device_type` bucket (mobile/tablet/desktop/bot), and a best-effort `is_bot` (uap-core spiders + self-identifying agents like GPTBot/curl), via `ua-parser` with a regex DB embedded at compile time (one parse per event) | `ua-parser`, `yaml_serde` | yes |\n| `referrer-list` | `ReferrerListClassifier` (built-in domain→category/source table; derives `medium`, and detects paid clicks via `gclid`/`msclkid`) and the `referrer` utils: `registrable_domain` (eTLD+1 via the Public Suffix List), `extract_utm`, and `paid_click` | `psl`, `url` | yes |\n| `geoip` | `GeoIpDb` — hot-reloadable MaxMind `.mmdb` city reader (see [GeoIP](#geoip)); `lookup` returns a `Location` | `maxminddb` | no |\n\n**Caveat for `referrer-list`:** `Event.referrer` (the eTLD+1) is computed by `registrable_domain`, which lives behind this feature. With the feature **off**, `Event.referrer` and `Event.traffic_source` are always `None` regardless of the incoming referrer — the pipeline simply doesn't parse it. `keep_raw_referrer(true)` still preserves the full URL in `Event.raw_referrer` either way.\n\n### Serialization \u0026 codegen\n\nThese are additive derives on the public output types (`Event`, `Location`, `Context`, `Utm`, `DeviceInfo`, `BrowserInfo`, `OperatingSystemInfo`, `ParsedUa`, `TrafficSource`, plus `EventKind`/`VisitorId`). `RawEvent` is deliberately **never** `Serialize` — it carries the raw IP/UA/referrer.\n\n| Feature | Adds | Pulls | Default |\n|---|---|---|---|\n| `serde` | `Serialize`/`Deserialize`; also enables `chrono/serde` for the timestamp. `EventKind`/`VisitorId` deserialize through their validating constructors | `serde` | yes |\n| `utoipa` | `utoipa::ToSchema` (OpenAPI); `EventKind`/`VisitorId` render as `string` | `utoipa` | no |\n| `schemars` | `schemars::JsonSchema` | `schemars` | no |\n| `typeshare` | `#[typeshare]` annotations for TypeScript type generation | `typeshare` | no |\n\n### Helpers\n\n| Feature | Adds | Pulls | Default |\n|---|---|---|---|\n| `http-headers` | `headers::client_ip(getter)` — framework-agnostic client-IP extraction from forwarding headers (`X-Forwarded-For`, `CF-Connecting-IP`, …). You supply proxy trust | — (std only) | no |\n\n### No-default build\n\nWith `default-features = false` and nothing else, `enrichr` is a near-identity pipeline: it validates and passes fields through but does no hashing, UA parsing, referrer classification, or geo lookup. It's only useful in that mode if you wire in your own `VisitorIdStrategy` / `Classifier` / `UaParser` implementations.\n\n## GeoIP\n\nThe library doesn't download databases. `GeoIpDb::from_path` loads a MaxMind-format city MMDB at startup; call `reload_from_path` on whatever schedule you like (e.g. a 24 h timer). Reloads are integrity-gated: the candidate must parse, its `build_epoch` must be ≥ the current one, and its file size must be ≥ 80% of the current — a failed reload leaves the existing database in place.\n\n```rust\n#[cfg(feature = \"geoip\")]\n{\n    use enrichr::geoip::GeoIpDb;\n    let db = GeoIpDb::from_path(\"GeoLite2-City.mmdb\".as_ref())?;\n    let processor = Processor::builder().geoip(db).build();\n\n    // On your own schedule (e.g. every 24h), reload the GeoIP DB in place:\n    if let Some(geoip) = processor.geoip() {\n        geoip.reload_from_path(\"GeoLite2-City.mmdb\".as_ref())?;\n    }\n}\n```\n\n## License\n\nMIT OR Apache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffranzos%2Fenrichr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffranzos%2Fenrichr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffranzos%2Fenrichr/lists"}