Suggesting path-cleaning rules
Many teams never configure path cleaning, so their Web analytics breakdowns fragment across
thousands of near-identical URLs. This feature proactively suggests cleaning rules for the
web-analytics precompute cohort: weekly, for each team, it samples real paths, asks the LLM for
{regex, alias} rules, validates them against the team's own paths, and stores them for review.
It only suggests — it never auto-applies. Applying rewrites historical numbers in every cleaned
chart, so that stays a human decision (the existing settings UI, or the --apply flag below after
review). To hand-author or directly apply rules, use the managing-path-cleaning-rules skill.
Architecture
- Core:
products/web_analytics/backend/path_cleaning_suggestions/service.py
sample_pathnames / count_distinct_pathnames — top $pathname by views via HogQL.
call_llm_for_rules — one-shot call through the LLM gateway
(get_llm_client(product="web_analytics", team_id=...), model
WEB_ANALYTICS_PATH_CLEANING_SUGGESTIONS_MODEL, default claude-haiku-4-5).
validate_and_annotate_rules — compiles each regex with re2 (the engine ClickHouse
replaceRegexpAll uses) and test-applies it to the sampled paths. Rules that don't compile or
match nothing are dropped; survivors get a dense order, a match_count, and in-memory
before/after examples (printed by the management command, never stored — health-issue
payloads are readable with just health_issue:read and must not leak real paths). This is
the skill's "test before saving" step, automated.
generate_suggestions_for_team — orchestrates the above with gating (see below); pure
generation, no storage.
apply_suggestions_to_team — merges rules into path_cleaning_filters, never overwrites
(dedupes by regex, continues order).
- Storage: a
path_cleaning_suggestions health issue (HealthIssue, severity info) — no
dedicated model. One active issue per team (hash_keys=[]); payload carries rules, model,
sampled_path_count, distinct_path_count. Applying (or hand-configuring rules) resolves the
issue on the next check run; dismissal is the health-issue dismissed flag.
- Schedule:
PathCleaningSuggestionsCheck
(products/web_analytics/backend/temporal/health_checks/path_cleaning_suggestions.py), a health
check on the shared health-check framework, weekly (Mon 06:23 UTC), small sequential batches
because each eligible team costs an LLM call. Teams with an existing active suggestion are
re-emitted without a fresh LLM round trip.
- Cohort:
WEB_ANALYTICS_PATH_CLEANING_SUGGESTIONS_TEAM_IDS, defaulting to the precompute
enrollment list WEB_ANALYTICS_LAZY_PRECOMPUTE_TEAM_IDS.
Gating (why a team is skipped)
generate_suggestions_for_team returns a status:
skipped_inactive — team sent no $pageview within visited_within_days (default 30); we only
suggest for teams actively using web analytics. Bypass with --ignore-visit-gate.
skipped_configured — team already has path cleaning rules (override with include_configured).
skipped_low_cardinality — fewer distinct paths than min_distinct_paths (default 50); cleaning
adds no value, so we don't spend tokens.
skipped_no_paths — no pageviews in the window.
generated — rules produced (may be an empty list if paths are already clean; empty generations
are never stored, so they can't shadow an actionable suggestion).
error — sampling/LLM failed; captured per-team, never aborts the cohort sweep.
How users see and apply suggestions
- Settings banner:
PathCleaningSuggestionsBanner on /settings/project#path_cleaning shows the
latest suggested row as regex → alias previews with match counts; "Apply all" (project admins
only) merges the rules, the close button dismisses. Driven by pathCleaningSuggestionsLogic.
- Onboarding step:
OnboardingWebAnalyticsPathCleaningStep (stepKey path_cleaning) surfaces the
same banner during Web analytics onboarding.
- API (
products/web_analytics/backend/api/web_analytics_path_cleaning_suggestions.py):
POST /api/projects/:id/web_analytics_path_cleaning_suggestions/generate/ produces and stores a
fresh suggestion on demand; GET .../{issue_id}/preview/ applies the rules to a fresh sample of
the team's top paths and returns before/after pairs (read scope, computed on demand, never
stored — this backs the banner's "Preview on your paths" modal); POST .../{issue_id}/apply/
merges the rules and resolves the issue (project admin only — the same gate the team API puts on
path_cleaning_filters).
Listing and dismissing go through the generic health-issues API
(GET /api/projects/:id/health_issues/?kind=path_cleaning_suggestions&status=active&dismissed=false,
PATCH .../health_issues/{id}/ with {"dismissed": true}).
- Health page: the check renders on
/web/health alongside the other web-analytics checks, with
remediation guidance for humans and agents.
- PostHog AI (Max): generate/apply are exposed as MCP tools in
products/web_analytics/mcp/tools.yaml (web-analytics-path-cleaning-suggestions-{generate,apply}),
so a user can ask Max to suggest path-cleaning rules and apply them conversationally. Apply is
destructive (it changes historical chart numbers), so the MCP confirmation gate applies.
Running it
# Default cohort, print suggestions, store health issues:
python manage.py suggest_path_cleaning_rules
# Specific teams, dry run (nothing stored):
python manage.py suggest_path_cleaning_rules --teams 2,19279 --no-store
# Generate AND apply for one reviewed team (merges, never overwrites):
python manage.py suggest_path_cleaning_rules --teams 2 --apply
Useful flags: --days (lookback), --limit (top-N paths sampled), --min-distinct-paths,
--include-configured, --no-store, --apply.
The health check can also be triggered per team from the health-issues refresh endpoint or the
admin UI, like any other health check.
Reviewing suggestions
Read a team's active suggestion:
HealthIssue.objects.filter(team_id=team_id, kind="path_cleaning_suggestions", status="active").first()
Each rule in payload["rules"] carries regex, alias, order, reason, and match_count —
that's what to show a human deciding whether to apply. Before/after examples on real paths are only
printed by the management command at generation time; they are deliberately kept out of the stored
payload.
Extending
- Adding a surfacing channel (in-app notification, settings banner, onboarding wizard step): read the
team's active
path_cleaning_suggestions health issue and render its payload["rules"]. Keep
apply manual.
- Changing the model: it must be allowlisted for the
web_analytics product in
services/llm-gateway/src/llm_gateway/products/config.py.
- The agentic alternative — a
signals-scout-web-analytics-path-cleaning scout — is sketched in the
design notes; prefer the dedicated job for the precompute cohort because it targets that exact
cohort and surfaces structured, validated rows rather than Signals-inbox findings.
1---2name: suggesting-path-cleaning-rules3description: Runs and reasons about the automated AI health check that suggests path-cleaning rules for web-analytics teams. Use when asked to generate path-cleaning suggestions for a team or cohort, to run the suggestion check, to review/apply AI-suggested rules, to inspect path_cleaning_suggestions health issues, or to extend the suggestion pipeline. Covers the suggest_path_cleaning_rules management command, the path_cleaning_suggestions health check, the cohort gating (precompute teams), and how suggestions are validated against real paths before storage. For hand-authoring or applying rules directly, use managing-path-cleaning-rules instead.4---5
6# Suggesting path-cleaning rules
7
8Many teams never configure path cleaning, so their Web analytics breakdowns fragment across
9thousands of near-identical URLs. This feature **proactively suggests** cleaning rules for the
10web-analytics precompute cohort: weekly, for each team, it samples real paths, asks the LLM for
11`{regex, alias}` rules, validates them against the team's own paths, and stores them for review.
12
13It **only suggests** — it never auto-applies. Applying rewrites historical numbers in every cleaned
14chart, so that stays a human decision (the existing settings UI, or the `--apply` flag below after
15review). To hand-author or directly apply rules, use the `managing-path-cleaning-rules` skill.
16
17## Architecture
18
19- **Core**: `products/web_analytics/backend/path_cleaning_suggestions/service.py`
20 - `sample_pathnames` / `count_distinct_pathnames` — top `$pathname` by views via HogQL.
21 - `call_llm_for_rules` — one-shot call through the LLM gateway
22 (`get_llm_client(product="web_analytics", team_id=...)`, model
23 `WEB_ANALYTICS_PATH_CLEANING_SUGGESTIONS_MODEL`, default `claude-haiku-4-5`).
24 - `validate_and_annotate_rules` — compiles each regex with **re2** (the engine ClickHouse
25 `replaceRegexpAll` uses) and test-applies it to the sampled paths. Rules that don't compile or
26 match nothing are dropped; survivors get a dense `order`, a `match_count`, and in-memory
27 before/after `examples` (printed by the management command, never stored — health-issue
28 payloads are readable with just `health_issue:read` and must not leak real paths). This is
29 the skill's "test before saving" step, automated.
30 - `generate_suggestions_for_team` — orchestrates the above with gating (see below); pure
31 generation, no storage.
32 - `apply_suggestions_to_team` — **merges** rules into `path_cleaning_filters`, never overwrites
33 (dedupes by regex, continues `order`).
34- **Storage**: a `path_cleaning_suggestions` **health issue** (`HealthIssue`, severity `info`) — no
35 dedicated model. One active issue per team (`hash_keys=[]`); `payload` carries `rules`, `model`,
36 `sampled_path_count`, `distinct_path_count`. Applying (or hand-configuring rules) resolves the
37 issue on the next check run; dismissal is the health-issue `dismissed` flag.
38- **Schedule**: `PathCleaningSuggestionsCheck`
39 (`products/web_analytics/backend/temporal/health_checks/path_cleaning_suggestions.py`), a health
40 check on the shared health-check framework, weekly (Mon 06:23 UTC), small sequential batches
41 because each eligible team costs an LLM call. Teams with an existing active suggestion are
42 re-emitted without a fresh LLM round trip.
43- **Cohort**: `WEB_ANALYTICS_PATH_CLEANING_SUGGESTIONS_TEAM_IDS`, defaulting to the precompute
44 enrollment list `WEB_ANALYTICS_LAZY_PRECOMPUTE_TEAM_IDS`.
45
46## Gating (why a team is skipped)
47
48`generate_suggestions_for_team` returns a status:
49
50- `skipped_inactive` — team sent no `$pageview` within `visited_within_days` (default 30); we only
51 suggest for teams actively using web analytics. Bypass with `--ignore-visit-gate`.
52- `skipped_configured` — team already has path cleaning rules (override with `include_configured`).
53- `skipped_low_cardinality` — fewer distinct paths than `min_distinct_paths` (default 50); cleaning
54 adds no value, so we don't spend tokens.
55- `skipped_no_paths` — no pageviews in the window.
56- `generated` — rules produced (may be an empty list if paths are already clean; empty generations
57 are never stored, so they can't shadow an actionable suggestion).
58- `error` — sampling/LLM failed; captured per-team, never aborts the cohort sweep.
59
60## How users see and apply suggestions
61
62- **Settings banner**: `PathCleaningSuggestionsBanner` on `/settings/project#path_cleaning` shows the
63 latest `suggested` row as regex → alias previews with match counts; "Apply all" (project admins
64 only) merges the rules, the close button dismisses. Driven by `pathCleaningSuggestionsLogic`.
65- **Onboarding step**: `OnboardingWebAnalyticsPathCleaningStep` (stepKey `path_cleaning`) surfaces the
66 same banner during Web analytics onboarding.
67- **API** (`products/web_analytics/backend/api/web_analytics_path_cleaning_suggestions.py`):
68 `POST /api/projects/:id/web_analytics_path_cleaning_suggestions/generate/` produces and stores a
69 fresh suggestion on demand; `GET .../{issue_id}/preview/` applies the rules to a fresh sample of
70 the team's top paths and returns before/after pairs (read scope, computed on demand, never
71 stored — this backs the banner's "Preview on your paths" modal); `POST .../{issue_id}/apply/`
72 merges the rules and resolves the issue (project admin only — the same gate the team API puts on
73 `path_cleaning_filters`).
74 Listing and dismissing go through the generic health-issues API
75 (`GET /api/projects/:id/health_issues/?kind=path_cleaning_suggestions&status=active&dismissed=false`,
76 `PATCH .../health_issues/{id}/` with `{"dismissed": true}`).
77- **Health page**: the check renders on `/web/health` alongside the other web-analytics checks, with
78 remediation guidance for humans and agents.
79- **PostHog AI (Max)**: generate/apply are exposed as MCP tools in
80 `products/web_analytics/mcp/tools.yaml` (`web-analytics-path-cleaning-suggestions-{generate,apply}`),
81 so a user can ask Max to suggest path-cleaning rules and apply them conversationally. Apply is
82 `destructive` (it changes historical chart numbers), so the MCP confirmation gate applies.
83
84## Running it
85
86```sh
87# Default cohort, print suggestions, store health issues:
88python manage.py suggest_path_cleaning_rules
89
90# Specific teams, dry run (nothing stored):
91python manage.py suggest_path_cleaning_rules --teams 2,19279 --no-store
92
93# Generate AND apply for one reviewed team (merges, never overwrites):
94python manage.py suggest_path_cleaning_rules --teams 2 --apply
95```
96
97Useful flags: `--days` (lookback), `--limit` (top-N paths sampled), `--min-distinct-paths`,
98`--include-configured`, `--no-store`, `--apply`.
99
100The health check can also be triggered per team from the health-issues `refresh` endpoint or the
101admin UI, like any other health check.
102
103## Reviewing suggestions
104
105Read a team's active suggestion:
106
107```python
108HealthIssue.objects.filter(team_id=team_id, kind="path_cleaning_suggestions", status="active").first()
109```
110
111Each rule in `payload["rules"]` carries `regex`, `alias`, `order`, `reason`, and `match_count` —
112that's what to show a human deciding whether to apply. Before/after examples on real paths are only
113printed by the management command at generation time; they are deliberately kept out of the stored
114payload.
115
116## Extending
117
118- Adding a surfacing channel (in-app notification, settings banner, onboarding wizard step): read the
119 team's active `path_cleaning_suggestions` health issue and render its `payload["rules"]`. Keep
120 apply manual.
121- Changing the model: it must be allowlisted for the `web_analytics` product in
122 `services/llm-gateway/src/llm_gateway/products/config.py`.
123- The agentic alternative — a `signals-scout-web-analytics-path-cleaning` scout — is sketched in the
124 design notes; prefer the dedicated job for the precompute cohort because it targets that exact
125 cohort and surfaces structured, validated rows rather than Signals-inbox findings.