From d2aac6ba79b013abfca7603260d520471006d6b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:55:32 +0000 Subject: [PATCH 1/9] Initial plan From 24e26620f17a71962d50d20d6160eb15a8bd69a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:05:42 +0000 Subject: [PATCH 2/9] Add docs CI gates: frontmatter, code samples, links, redirects Co-authored-by: tylergoerzen-mxp <259741734+tylergoerzen-mxp@users.noreply.github.com> --- .github/workflows/docs-ci.yml | 66 ++++++++++++ docs.json | 74 +++++++------- docs/mcp.mdx | 2 +- reference/event-deduplication.mdx | 2 +- scripts/check_code_samples.py | 86 ++++++++++++++++ scripts/check_frontmatter.py | 67 ++++++++++++ scripts/check_links.py | 165 ++++++++++++++++++++++++++++++ scripts/check_redirects.py | 101 ++++++++++++++++++ 8 files changed, 524 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/docs-ci.yml create mode 100644 scripts/check_code_samples.py create mode 100644 scripts/check_frontmatter.py create mode 100644 scripts/check_links.py create mode 100644 scripts/check_redirects.py diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml new file mode 100644 index 000000000..25cf95b0a --- /dev/null +++ b/.github/workflows/docs-ci.yml @@ -0,0 +1,66 @@ +name: Docs CI + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + frontmatter: + name: Frontmatter check + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Check frontmatter + run: python scripts/check_frontmatter.py + + code-samples: + name: Code-sample check + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Check code samples + run: python scripts/check_code_samples.py + + links: + name: Internal-links check + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Check internal links + run: python scripts/check_links.py + + redirects: + name: Redirects check + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Check redirects + run: python scripts/check_redirects.py diff --git a/docs.json b/docs.json index f7308876a..e086cc793 100644 --- a/docs.json +++ b/docs.json @@ -409,7 +409,7 @@ "docs/data-governance/data-volume-monitoring", "docs/data-governance/warehouse-sync-monitoring", "docs/data-governance/data-clean-up", - "docs/data-governance/ai-powered-data-governance" + "docs/data-governance/ai-powered-data-governance" ] }, { @@ -705,7 +705,7 @@ "guides/strategic-playbooks/product-analytics-strategy/finale" ] }, - "guides/strategic-playbooks/onboarding-playbook", + "guides/strategic-playbooks/onboarding-playbook", "guides/strategic-playbooks/project-migration", "guides/strategic-playbooks/feature-flag-migration-playbook" ] @@ -1879,7 +1879,7 @@ }, { "source": "/docs/formulas", - "destination": "/changelogs/2023-09-19-formulas" + "destination": "/changelogs" }, { "source": "/docs/funnels", @@ -2383,7 +2383,7 @@ }, { "source": "/docs/tracking/data-warehouse/groups", - "destination": "/docs/tracking-methods/data-warehouse/sending-group-profiles" + "destination": "/docs/tracking-methods/warehouse-connectors" }, { "source": "/docs/tracking/data-warehouse/redshift", @@ -2747,27 +2747,27 @@ }, { "source": "/guides/plan/setup", - "destination": "/guides/strategic-playbooks/onboarding-playbook/plan/setup" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/plan/framework", - "destination": "/guides/strategic-playbooks/onboarding-playbook/plan/framework" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/plan/tracking-strategy", - "destination": "/guides/strategic-playbooks/onboarding-playbook/plan/tracking-strategy" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/implement/send-your-data", - "destination": "/guides/strategic-playbooks/onboarding-playbook/implement/send-your-data" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/implement/qa-data-audit", - "destination": "/guides/strategic-playbooks/onboarding-playbook/implement/qa-data-audit" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/implement/establish-governance", - "destination": "/guides/strategic-playbooks/onboarding-playbook/implement/establish-governance" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/launch/create-boards", @@ -2855,7 +2855,7 @@ }, { "source": "/hc", - "destination": "docs/getting-started/what-is-mixpanel" + "destination": "/docs/what-is-mixpanel" }, { "source": "/hc/admin/general_settings", @@ -3227,23 +3227,23 @@ }, { "source": "/hc/en-us/articles/13174988269844*", - "destination": "/changelogs/2022-12-01-improvements" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13175102961428*", - "destination": "/changelogs/2022-12-13-boards" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13175146659732*", - "destination": "/changelogs/2023-01-18-table-boards" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13175184938260*", - "destination": "/changelogs/2023-01-23-users-flows" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13175224500628*", - "destination": "/changelogs/2023-01-31-embed" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13271005313556*", @@ -3251,39 +3251,39 @@ }, { "source": "/hc/en-us/articles/13395179786644*", - "destination": "/changelogs/2022-07-08-reorient" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395198918804*", - "destination": "/changelogs/2022-11-03-session-improvements" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395199893652*", - "destination": "/changelogs/2022-11-07-millisecond" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395205961236*", - "destination": "/changelogs/2022-07-01-view-users" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395233184916*", - "destination": "/changelogs/2022-06-16-faster-workflow" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395235568660*", - "destination": "/changelogs/2022-05-31-improve-conversion-flow" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395324158868*", - "destination": "/changelogs/2022-05-24-lexicon-context" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395325754772*", - "destination": "/changelogs/2022-04-18-relative-comparison" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13395328448404*", - "destination": "/changelogs/2022-03-29-text-boards" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/13530857626132*", @@ -3291,7 +3291,7 @@ }, { "source": "/hc/en-us/articles/13756463065492*", - "destination": "/changelogs/2023-02-28-retention-calendar-interval" + "destination": "/changelogs" }, { "source": "/hc/en-us/articles/14202292561172*", @@ -3635,7 +3635,7 @@ }, { "source": "/hc/en-us/articles/360041995352*", - "destination": "/docs/tracking/how-tos/events-properties" + "destination": "/docs/data-structure/events-and-properties" }, { "source": "/hc/en-us/articles/360042412051*", @@ -4719,43 +4719,43 @@ }, { "source": "/guides/guides-by-topic/core-reports/create-boards", - "destination":"/guides/guides-by-topic/core-reports" + "destination": "/guides/guides-by-topic/core-reports" }, { "source": "/guides/guides-by-topic/core-reports/discover-insights", - "destination":"/guides/guides-by-topic/core-reports" + "destination": "/guides/guides-by-topic/core-reports" }, { "source": "/guides/guides-by-topic/core-reports/analyze-conversions", - "destination":"/guides/guides-by-topic/core-reports" + "destination": "/guides/guides-by-topic/core-reports" }, { "source": "/guides/guides-by-topic/core-reports/build-user-flows", - "destination":"/guides/guides-by-topic/core-reports" + "destination": "/guides/guides-by-topic/core-reports" }, { "source": "/guides/guides-by-topic/core-reports/track-user-retention", - "destination":"/guides/guides-by-topic/core-reports" + "destination": "/guides/guides-by-topic/core-reports" }, { "source": "/guides/guides-by-topic/core-reports/define-cohorts", - "destination":"/guides/guides-by-topic/core-reports" + "destination": "/guides/guides-by-topic/core-reports" }, { "source": "/guides/strategic-playbooks/onboarding-playbook/plan/*", - "destination":"/guides/strategic-playbooks/onboarding-playbook" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/strategic-playbooks/onboarding-playbook/implement/*", - "destination":"/guides/strategic-playbooks/onboarding-playbook" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/strategic-playbooks/onboarding-playbook/launch/*", - "destination":"/guides/strategic-playbooks/onboarding-playbook" + "destination": "/guides/strategic-playbooks/onboarding-playbook" }, { "source": "/guides/strategic-playbooks/onboarding-playbook/beyond-onboarding", - "destination":"/guides/strategic-playbooks/onboarding-playbook" + "destination": "/guides/strategic-playbooks/onboarding-playbook" } ] } diff --git a/docs/mcp.mdx b/docs/mcp.mdx index 29b000df9..60c847393 100644 --- a/docs/mcp.mdx +++ b/docs/mcp.mdx @@ -222,7 +222,7 @@ Any client that supports the MCP JSON config format, including Microsoft Copilot **Beta.** Service account authentication for MCP is in beta. The interface may change. -[Service accounts](/reference/Mixpanel%20APIs/authentication/service-accounts) are non-human Mixpanel users designed for scripts, back-end services, and automated workflows. They authenticate via a static header — no browser-based login is required. +[Service accounts](/reference/service-accounts) are non-human Mixpanel users designed for scripts, back-end services, and automated workflows. They authenticate via a static header — no browser-based login is required. Use service accounts when you need a headless MCP connection, such as CI/CD pipelines, automated agents, or shared team setups. The service account's project permissions apply: it can only access projects it has been added to. diff --git a/reference/event-deduplication.mdx b/reference/event-deduplication.mdx index 8cfd3d3ad..aa6db007b 100644 --- a/reference/event-deduplication.mdx +++ b/reference/event-deduplication.mdx @@ -23,7 +23,7 @@ Only the four key event properties listed above are used for deduplication. Addi Deduplication occurs when a subset of the event data (event name, distinct\_id, timestamp, \$insert\_id) is identical. Other event properties are not considered. -**Required [Event Object](/docs/data-model#anatomy-of-an-event) attributes** +**Required [Event Object](/docs/data-structure/events-and-properties) attributes** diff --git a/scripts/check_code_samples.py b/scripts/check_code_samples.py new file mode 100644 index 000000000..f6bc8e020 --- /dev/null +++ b/scripts/check_code_samples.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +CI gate: every fenced code block in MDX files must declare a language. + +A fenced block opening looks like: + ```python + ```javascript + ```bash + +A block with no language identifier: + ``` + +will cause this check to fail. + +Excluded directories (same as other checks): + - snippets/ + - openapi/ +""" + +import sys +import glob +import os +import re + +EXCLUDED_DIRS = {"snippets", "openapi"} + +# Matches the opening fence of a code block; captures the language (may be empty). +# The fence may be indented (e.g. inside a ). +FENCE_OPEN_RE = re.compile(r"^[ \t]*`{3,}([^\n`]*)$", re.MULTILINE) + + +def check_file(path: str) -> list[str]: + errors = [] + with open(path, encoding="utf-8") as fh: + content = fh.read() + + in_block = False + for lineno, line in enumerate(content.splitlines(), 1): + stripped = line.strip() + if stripped.startswith("```"): + if in_block: + # Closing fence + in_block = False + else: + # Opening fence — extract language token + rest = stripped[3:].strip() + lang = rest.split()[0] if rest else "" + if not lang: + errors.append( + f"{path}:{lineno}: code block is missing a language identifier" + ) + in_block = True + + return errors + + +def is_excluded(path: str) -> bool: + parts = path.replace(os.sep, "/").split("/") + return any(part in EXCLUDED_DIRS for part in parts) + + +def main() -> int: + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + mdx_files = glob.glob(os.path.join(root, "**", "*.mdx"), recursive=True) + + checked = 0 + all_errors: list[str] = [] + for path in sorted(mdx_files): + rel = os.path.relpath(path, root) + if is_excluded(rel): + continue + all_errors.extend(check_file(path)) + checked += 1 + + if all_errors: + print("Code-sample check FAILED:") + for err in all_errors: + print(f" {err}") + return 1 + + print(f"Code-sample check PASSED ({checked} files checked).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_frontmatter.py b/scripts/check_frontmatter.py new file mode 100644 index 000000000..5bf0567d4 --- /dev/null +++ b/scripts/check_frontmatter.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +CI gate: every MDX page must have a 'title' field in its YAML front-matter. + +Directories that are intentionally excluded from the check: + - snippets/ (reusable MDX components, not standalone pages) + - links/ (external-link stubs that use 'url' instead of a body) + - openapi/ (OpenAPI spec files, not MDX pages) +""" + +import re +import sys +import glob +import os + +EXCLUDED_DIRS = {"snippets", "links", "openapi"} + +FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL) + + +def check_file(path: str) -> list[str]: + errors = [] + with open(path, encoding="utf-8") as fh: + content = fh.read() + + m = FRONTMATTER_RE.match(content) + if not m: + errors.append(f"{path}: missing front-matter block") + return errors + + fm = m.group(1) + if not re.search(r"^\s*title\s*:", fm, re.MULTILINE): + errors.append(f"{path}: front-matter is missing required 'title' field") + + return errors + + +def is_excluded(path: str) -> bool: + parts = path.replace(os.sep, "/").split("/") + return any(part in EXCLUDED_DIRS for part in parts) + + +def main() -> int: + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + mdx_files = glob.glob(os.path.join(root, "**", "*.mdx"), recursive=True) + + checked = 0 + all_errors: list[str] = [] + for path in sorted(mdx_files): + rel = os.path.relpath(path, root) + if is_excluded(rel): + continue + all_errors.extend(check_file(path)) + checked += 1 + + if all_errors: + print("Frontmatter check FAILED:") + for err in all_errors: + print(f" {err}") + return 1 + + print(f"Frontmatter check PASSED ({checked} files checked).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_links.py b/scripts/check_links.py new file mode 100644 index 000000000..3b7b2865f --- /dev/null +++ b/scripts/check_links.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +""" +CI gate: internal links in MDX files must resolve to an existing page. + +Rules: + - Only links whose path starts with '/' are checked (relative links are + ignored because they are rare and context-dependent). + - External URLs (http/https), anchor-only links (#…), and mailto links + are skipped. + - Query-string parameters and fragment anchors are stripped before + looking up the target file. + - A link target is considered valid when one of the following is true: + 1. /.mdx exists + 2. //index.mdx exists (index pages) + 3. is the source of a redirect declared in docs.json + (redirect sources are valid inbound paths even without a backing file) + +Source locations checked: + - href="…" attributes (JSX / HTML in MDX) + - [text](…) Markdown links + +Excluded directories: + - snippets/ + - openapi/ +""" + +import json +import os +import re +import sys +import glob +from urllib.parse import urlparse, unquote + +EXCLUDED_DIRS = {"snippets", "openapi"} + +# Path prefixes that point to static assets, not pages — skip these. +NON_PAGE_PREFIXES = ("/images/", "/icons/", "/logo/", "/favicon") + +# href="..." or href='...' +HREF_RE = re.compile(r"""href=["']([^"']+)["']""") +# [label](url) – skip image links starting with ! +MD_LINK_RE = re.compile(r"(? list[str]: + links: list[str] = [] + for m in HREF_RE.finditer(content): + links.append(m.group(1)) + for m in MD_LINK_RE.finditer(content): + links.append(m.group(1)) + return links + + +def is_internal(link: str) -> bool: + if link.startswith(("http://", "https://", "mailto:", "#")): + return False + if not link.startswith("/"): + return False + if link.startswith(NON_PAGE_PREFIXES): + return False + return True + + +def normalise(link: str) -> str: + """Strip fragment and query-string, then decode percent-encoding.""" + parsed = urlparse(link) + path = parsed.path + return unquote(path).rstrip("/") + + +def build_valid_paths(root: str) -> tuple[set[str], list[str]]: + """Return (exact_paths, wildcard_prefixes) for valid root-relative paths. + + exact_paths – full paths that must match exactly (O(1) lookup via set). + wildcard_prefixes – path prefixes derived from wildcard redirect sources + (e.g. '/changelogs' from '/changelogs/*'). A link + target is valid if it starts with one of these prefixes + followed by '/'. + """ + exact: set[str] = set() + prefixes: list[str] = [] + + # Every .mdx file contributes its path (without extension) and with extension + for mdx in glob.glob(os.path.join(root, "**", "*.mdx"), recursive=True): + rel = os.path.relpath(mdx, root).replace(os.sep, "/") + # e.g. "docs/what-is-mixpanel.mdx" + exact.add("/" + rel) # with .mdx + exact.add("/" + rel[:-4]) # without .mdx + # index pages: "docs/foo/index.mdx" → "/docs/foo" + if rel.endswith("/index.mdx"): + exact.add("/" + rel[: -len("/index.mdx")]) + + # Redirect sources are also valid inbound paths + docs_json = os.path.join(root, "docs.json") + if os.path.exists(docs_json): + with open(docs_json, encoding="utf-8") as fh: + data = json.load(fh) + for redir in data.get("redirects", []): + src = redir.get("source", "") + if src.endswith("/*"): + # Wildcard source: store the prefix for prefix matching + prefix = src[:-2] # e.g. "/changelogs" + exact.add(prefix) + prefixes.append(prefix) + else: + exact.add(src) + + return exact, prefixes + + +def is_excluded(path: str) -> bool: + parts = path.replace(os.sep, "/").split("/") + return any(part in EXCLUDED_DIRS for part in parts) + + +def main() -> int: + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + valid_paths, wildcard_prefixes = build_valid_paths(root) + + mdx_files = glob.glob(os.path.join(root, "**", "*.mdx"), recursive=True) + + checked = 0 + all_errors: list[str] = [] + + for path in sorted(mdx_files): + rel = os.path.relpath(path, root) + if is_excluded(rel): + continue + checked += 1 + + with open(path, encoding="utf-8") as fh: + content = fh.read() + + for raw_link in extract_links(content): + if not is_internal(raw_link): + continue + target = normalise(raw_link) + if not target: + continue + + # Direct match (O(1)) + if target in valid_paths: + continue + + # Wildcard-redirect prefix match: check only the small list of + # known wildcard prefixes rather than iterating all valid_paths. + if any(target.startswith(p + "/") for p in wildcard_prefixes): + continue + + all_errors.append( + f"{rel}: broken internal link '{raw_link}' → '{target}'" + ) + + if all_errors: + print("Links check FAILED:") + for err in all_errors: + print(f" {err}") + return 1 + + print(f"Links check PASSED ({checked} files, {len(valid_paths)} valid paths indexed).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_redirects.py b/scripts/check_redirects.py new file mode 100644 index 000000000..446af1a80 --- /dev/null +++ b/scripts/check_redirects.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +CI gate: validate the 'redirects' section of docs.json. + +Checks performed: + 1. No duplicate redirect source paths. + 2. Every redirect destination resolves to an existing page OR is itself + the source of another redirect (chained redirects are allowed). + Wildcard destinations (containing '*') are skipped because their + validity is structural rather than path-based. +""" + +import json +import os +import sys +import glob + + +def build_file_paths(root: str) -> set[str]: + """Return all root-relative page paths derived from .mdx files.""" + paths: set[str] = set() + for mdx in glob.glob(os.path.join(root, "**", "*.mdx"), recursive=True): + rel = os.path.relpath(mdx, root).replace(os.sep, "/") + paths.add("/" + rel) # with extension + paths.add("/" + rel[:-4]) # without extension + if rel.endswith("/index.mdx"): + paths.add("/" + rel[: -len("/index.mdx")]) + return paths + + +def normalise(path: str) -> str: + """Strip trailing slash and query string.""" + return path.split("?")[0].rstrip("/") or "/" + + +def main() -> int: + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + docs_json_path = os.path.join(root, "docs.json") + + if not os.path.exists(docs_json_path): + print("Redirects check SKIPPED (docs.json not found).") + return 0 + + with open(docs_json_path, encoding="utf-8") as fh: + data = json.load(fh) + + redirects = data.get("redirects", []) + all_errors: list[str] = [] + + # ── 1. Duplicate sources ──────────────────────────────────────────────── + sources: list[str] = [r.get("source", "") for r in redirects] + seen: set[str] = set() + duplicates: set[str] = set() + for src in sources: + if src in seen: + duplicates.add(src) + seen.add(src) + + for dup in sorted(duplicates): + all_errors.append(f"docs.json: duplicate redirect source '{dup}'") + + # ── 2. Destinations resolve to a known page or another redirect source ── + file_paths = build_file_paths(root) + source_set = set(sources) # redirect sources are also valid destinations + + for redir in redirects: + dest = redir.get("destination", "") + if not dest: + all_errors.append( + f"docs.json: redirect from '{redir.get('source')}' has an empty destination" + ) + continue + + # Skip wildcards / external URLs – structural validity only + if "*" in dest or dest.startswith("http"): + continue + + # Strip anchors and query strings from destination + dest_path = normalise(dest.split("#")[0]) + + if dest_path not in file_paths and dest_path not in source_set: + all_errors.append( + f"docs.json: redirect destination '{dest}' does not resolve " + f"to a known page (source: '{redir.get('source')}')" + ) + + if all_errors: + print("Redirects check FAILED:") + for err in all_errors: + print(f" {err}") + return 1 + + print( + f"Redirects check PASSED ({len(redirects)} redirects validated, " + f"{len(file_paths)} pages indexed)." + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 37a61ccf8f06c0e947f1091b49856be772bafdb5 Mon Sep 17 00:00:00 2001 From: Tyler Goerzen Date: Tue, 18 Aug 2026 13:34:34 -0700 Subject: [PATCH 3/9] Fix false positives and false negatives in the docs CI gates check_redirects.py - Honour wildcard redirect sources when resolving a destination. The docstring says chained redirects are allowed, but only exact sources were matched, so the 461 wildcard sources were ignored. That produced 25 errors on main of which only 3 were real: an 88% false-positive rate that would have fired again on the next redirect anyone added. - Detect loops. Every node in a cycle is also a source, so the chained-redirect rule silently swallowed /self -> /self and /a -> /b -> /a. check_links.py - Blank out fenced and inline code before extracting links, so a page documenting an example does not fail CI. Line numbers are preserved. check_code_samples.py - Track fence length so a ```python block nested in a ````mdx block does not close the outer block early. Drop the unused FENCE_OPEN_RE and report repo-relative paths instead of absolute ones. check_frontmatter.py - An empty title no longer passes. Verified: all four pass on this branch, and each rejects a deliberate bad fixture (empty title, bare fence, dead link, redirect loop). Co-Authored-By: Claude Opus 5 --- scripts/check_code_samples.py | 39 ++++++++++++++++------------------- scripts/check_frontmatter.py | 5 ++++- scripts/check_links.py | 26 ++++++++++++++++++++++- scripts/check_redirects.py | 39 ++++++++++++++++++++++++++++++----- 4 files changed, 81 insertions(+), 28 deletions(-) diff --git a/scripts/check_code_samples.py b/scripts/check_code_samples.py index f6bc8e020..2c948b6fb 100644 --- a/scripts/check_code_samples.py +++ b/scripts/check_code_samples.py @@ -24,32 +24,29 @@ EXCLUDED_DIRS = {"snippets", "openapi"} -# Matches the opening fence of a code block; captures the language (may be empty). -# The fence may be indented (e.g. inside a ). -FENCE_OPEN_RE = re.compile(r"^[ \t]*`{3,}([^\n`]*)$", re.MULTILINE) - - -def check_file(path: str) -> list[str]: +def check_file(path: str, display: str) -> list[str]: errors = [] with open(path, encoding="utf-8") as fh: content = fh.read() - in_block = False + fence_len = 0 # 0 = outside a block; otherwise the opening fence's length for lineno, line in enumerate(content.splitlines(), 1): stripped = line.strip() - if stripped.startswith("```"): - if in_block: - # Closing fence - in_block = False - else: - # Opening fence — extract language token - rest = stripped[3:].strip() - lang = rest.split()[0] if rest else "" - if not lang: - errors.append( - f"{path}:{lineno}: code block is missing a language identifier" - ) - in_block = True + if not stripped.startswith("```"): + continue + ticks = len(stripped) - len(stripped.lstrip("`")) + rest = stripped[ticks:].strip() + if fence_len: + # Only a bare fence at least as long as the opener closes the block, + # so a ```python block nested inside ````mdx does not end it early. + if ticks >= fence_len and not rest: + fence_len = 0 + continue + if not rest: + errors.append( + f"{display}:{lineno}: code block is missing a language identifier" + ) + fence_len = ticks return errors @@ -69,7 +66,7 @@ def main() -> int: rel = os.path.relpath(path, root) if is_excluded(rel): continue - all_errors.extend(check_file(path)) + all_errors.extend(check_file(path, rel)) checked += 1 if all_errors: diff --git a/scripts/check_frontmatter.py b/scripts/check_frontmatter.py index 5bf0567d4..acc6626cd 100644 --- a/scripts/check_frontmatter.py +++ b/scripts/check_frontmatter.py @@ -29,8 +29,11 @@ def check_file(path: str) -> list[str]: return errors fm = m.group(1) - if not re.search(r"^\s*title\s*:", fm, re.MULTILINE): + title_match = re.search(r"^\s*title\s*:\s*(.*)$", fm, re.MULTILINE) + if not title_match: errors.append(f"{path}: front-matter is missing required 'title' field") + elif not title_match.group(1).strip().strip("\"'"): + errors.append(f"{path}: front-matter 'title' is empty") return errors diff --git a/scripts/check_links.py b/scripts/check_links.py index 3b7b2865f..cf7f7b58e 100644 --- a/scripts/check_links.py +++ b/scripts/check_links.py @@ -42,6 +42,30 @@ MD_LINK_RE = re.compile(r"(? str: + """Blank out fenced code blocks and inline code spans so that example + links inside them are not treated as real links. Line count is preserved + so reported line numbers stay accurate.""" + out: list[str] = [] + fence_len = 0 + for line in content.splitlines(): + stripped = line.strip() + if stripped.startswith("```"): + ticks = len(stripped) - len(stripped.lstrip("`")) + if fence_len: + if ticks >= fence_len and not stripped[ticks:].strip(): + fence_len = 0 + else: + fence_len = ticks + out.append("") + continue + out.append("" if fence_len else INLINE_CODE_RE.sub("", line)) + return "\n".join(out) + + def extract_links(content: str) -> list[str]: links: list[str] = [] for m in HREF_RE.finditer(content): @@ -129,7 +153,7 @@ def main() -> int: checked += 1 with open(path, encoding="utf-8") as fh: - content = fh.read() + content = strip_code(fh.read()) for raw_link in extract_links(content): if not is_internal(raw_link): diff --git a/scripts/check_redirects.py b/scripts/check_redirects.py index 446af1a80..b5a0b461f 100644 --- a/scripts/check_redirects.py +++ b/scripts/check_redirects.py @@ -4,12 +4,15 @@ Checks performed: 1. No duplicate redirect source paths. - 2. Every redirect destination resolves to an existing page OR is itself - the source of another redirect (chained redirects are allowed). + 2. No redirect loops (a self-redirect or a cycle). + 3. Every redirect destination resolves to an existing page OR is itself + the source of another redirect, including a wildcard one (chained + redirects are allowed). Wildcard destinations (containing '*') are skipped because their validity is structural rather than path-based. """ +import fnmatch import json import os import sys @@ -59,9 +62,35 @@ def main() -> int: for dup in sorted(duplicates): all_errors.append(f"docs.json: duplicate redirect source '{dup}'") - # ── 2. Destinations resolve to a known page or another redirect source ── + # ── 2. Redirect loops (self-redirects and cycles) ─────────────────────── + dest_by_source = {r.get("source", ""): r.get("destination", "") for r in redirects} + reported_loops: set[str] = set() + for start in dest_by_source: + hops = {start} + node = dest_by_source[start] + while node in dest_by_source: + if node in hops: + if node not in reported_loops: + reported_loops.add(node) + all_errors.append( + f"docs.json: redirect loop starting at '{start}' revisits '{node}'" + ) + break + hops.add(node) + node = dest_by_source[node] + + # ── 3. Destinations resolve to a known page or another redirect source ── file_paths = build_file_paths(root) - source_set = set(sources) # redirect sources are also valid destinations + exact_sources = {s for s in sources if "*" not in s} + wildcard_sources = [s for s in sources if "*" in s] + + def resolves(path: str) -> bool: + """A destination is valid if it is a real page, an exact redirect + source, or matched by a wildcard redirect source. The last case is a + chained redirect, which the CDN follows to a final landing page.""" + if path in file_paths or path in exact_sources: + return True + return any(fnmatch.fnmatch(path, pat) for pat in wildcard_sources) for redir in redirects: dest = redir.get("destination", "") @@ -78,7 +107,7 @@ def main() -> int: # Strip anchors and query strings from destination dest_path = normalise(dest.split("#")[0]) - if dest_path not in file_paths and dest_path not in source_set: + if not resolves(dest_path): all_errors.append( f"docs.json: redirect destination '{dest}' does not resolve " f"to a known page (source: '{redir.get('source')}')" From f3f1fb20ab733f8bd9b7aa77fc9bffee89b165ec Mon Sep 17 00:00:00 2001 From: Tyler Goerzen Date: Wed, 19 Aug 2026 23:35:31 -0700 Subject: [PATCH 4/9] Add OpenAPI gate, enforce description and unique titles, pin actions Completes rec #20 Part B's gate list. - New check_openapi.py: parses all 14 specs, requires openapi/info/paths, and resolves every local $ref. Uses openapi-spec-validator for full schema validation when installed, and still runs structurally without it. Passes on all 14 specs today; rejects a spec with a dangling $ref. - check_frontmatter.py now requires a non-empty description and fails on duplicate rendered titles, which rec #11 asked for. - Pin actions/checkout and actions/setup-python to commit SHAs, matching stale.yml. Collapse four near-identical jobs into one with ordered steps, and add a concurrency group. MERGE ORDER: the frontmatter gate is red until #172 (description backfill) and #178 (title dedupe) land. Verified against the #172 tree: all description errors clear, leaving only the duplicate titles that #178 resolves. Merge this last, as the plan intends. Co-Authored-By: Claude Opus 5 --- .github/workflows/docs-ci.yml | 58 +++++--------- scripts/check_frontmatter.py | 40 ++++++++-- scripts/check_openapi.py | 143 ++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 48 deletions(-) create mode 100644 scripts/check_openapi.py diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml index 25cf95b0a..05a6a5767 100644 --- a/.github/workflows/docs-ci.yml +++ b/.github/workflows/docs-ci.yml @@ -8,59 +8,37 @@ on: branches: - main +concurrency: + group: docs-ci-${{ github.ref }} + cancel-in-progress: true + jobs: - frontmatter: - name: Frontmatter check + checks: + name: Docs checks runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" + - name: Install OpenAPI validator + run: pip install --quiet pyyaml openapi-spec-validator + # continue-on-error is deliberately absent: each check is a hard gate. + # Steps run in order and the job reports the first failure. - name: Check frontmatter run: python scripts/check_frontmatter.py - - code-samples: - name: Code-sample check - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - name: Check code samples + if: '!cancelled()' run: python scripts/check_code_samples.py - - links: - name: Internal-links check - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - name: Check internal links + if: '!cancelled()' run: python scripts/check_links.py - - redirects: - name: Redirects check - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - name: Check redirects + if: '!cancelled()' run: python scripts/check_redirects.py + - name: Check OpenAPI specs + if: '!cancelled()' + run: python scripts/check_openapi.py diff --git a/scripts/check_frontmatter.py b/scripts/check_frontmatter.py index acc6626cd..9dbf32139 100644 --- a/scripts/check_frontmatter.py +++ b/scripts/check_frontmatter.py @@ -18,24 +18,37 @@ FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL) -def check_file(path: str) -> list[str]: +def check_file(path: str, display: str) -> tuple[list[str], str | None]: + """Return (errors, title). Title is None when absent or empty.""" errors = [] with open(path, encoding="utf-8") as fh: content = fh.read() m = FRONTMATTER_RE.match(content) if not m: - errors.append(f"{path}: missing front-matter block") - return errors + errors.append(f"{display}: missing front-matter block") + return errors, None fm = m.group(1) + title = None title_match = re.search(r"^\s*title\s*:\s*(.*)$", fm, re.MULTILINE) if not title_match: - errors.append(f"{path}: front-matter is missing required 'title' field") - elif not title_match.group(1).strip().strip("\"'"): - errors.append(f"{path}: front-matter 'title' is empty") + errors.append(f"{display}: front-matter is missing required 'title' field") + else: + title = title_match.group(1).strip().strip("\"'") + if not title: + errors.append(f"{display}: front-matter 'title' is empty") + title = None - return errors + # A description is what search results and llms.txt entries render, so a + # page without one is invisible to both. + desc_match = re.search(r"^\s*description\s*:\s*(.*)$", fm, re.MULTILINE) + if not desc_match: + errors.append(f"{display}: front-matter is missing required 'description' field") + elif not desc_match.group(1).strip().strip("\"'"): + errors.append(f"{display}: front-matter 'description' is empty") + + return errors, title def is_excluded(path: str) -> bool: @@ -49,13 +62,24 @@ def main() -> int: checked = 0 all_errors: list[str] = [] + titles: dict[str, list[str]] = {} for path in sorted(mdx_files): rel = os.path.relpath(path, root) if is_excluded(rel): continue - all_errors.extend(check_file(path)) + errors, title = check_file(path, rel) + all_errors.extend(errors) + if title: + titles.setdefault(title, []).append(rel) checked += 1 + # Two pages sharing a rendered title are indistinguishable in search + # results and to answer engines. + for title, pages in sorted(titles.items()): + if len(pages) > 1: + joined = ", ".join(sorted(pages)) + all_errors.append(f'duplicate title "{title}" on {len(pages)} pages: {joined}') + if all_errors: print("Frontmatter check FAILED:") for err in all_errors: diff --git a/scripts/check_openapi.py b/scripts/check_openapi.py new file mode 100644 index 000000000..d493fb5cb --- /dev/null +++ b/scripts/check_openapi.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +""" +CI gate: validate every OpenAPI specification under openapi/. + +Checks performed: + 1. The file parses as YAML or JSON. + 2. Required top-level structure is present (openapi/swagger version, info + with title and version, and paths). + 3. Every local $ref ("#/...") resolves to a node that exists in the document. + 4. If openapi-spec-validator is installed, the full spec is validated against + the OpenAPI schema. Without it, checks 1-3 still run. +""" + +import glob +import json +import os +import sys + +import yaml + + +def load(path: str): + with open(path, encoding="utf-8") as fh: + text = fh.read() + if path.endswith(".json"): + return json.loads(text) + return yaml.safe_load(text) + + +def iter_refs(node, trail="#"): + """Yield every ($ref value, location) pair in the document.""" + if isinstance(node, dict): + for key, value in node.items(): + if key == "$ref" and isinstance(value, str): + yield value, trail + else: + yield from iter_refs(value, f"{trail}/{key}") + elif isinstance(node, list): + for index, value in enumerate(node): + yield from iter_refs(value, f"{trail}/{index}") + + +def resolves(doc, ref: str) -> bool: + """Walk a local JSON pointer ("#/components/schemas/Foo") through the doc.""" + node = doc + for part in ref.lstrip("#/").split("/"): + # JSON pointer escapes, per RFC 6901. + part = part.replace("~1", "/").replace("~0", "~") + if isinstance(node, list): + if not part.isdigit() or int(part) >= len(node): + return False + node = node[int(part)] + elif isinstance(node, dict): + if part not in node: + return False + node = node[part] + else: + return False + return True + + +def check_file(path: str, display: str) -> list[str]: + errors = [] + try: + doc = load(path) + except Exception as exc: # noqa: BLE001 - report any parse failure verbatim + return [f"{display}: does not parse ({type(exc).__name__}: {exc})"] + + if not isinstance(doc, dict): + return [f"{display}: top level is not a mapping"] + + if not (doc.get("openapi") or doc.get("swagger")): + errors.append(f"{display}: missing 'openapi' (or 'swagger') version field") + + info = doc.get("info") + if not isinstance(info, dict): + errors.append(f"{display}: missing 'info' object") + else: + for field in ("title", "version"): + if not info.get(field): + errors.append(f"{display}: 'info.{field}' is missing or empty") + + if "paths" not in doc and "webhooks" not in doc: + errors.append(f"{display}: missing 'paths'") + + for ref, where in iter_refs(doc): + if ref.startswith("#"): + if not resolves(doc, ref): + errors.append(f"{display}: unresolved local $ref '{ref}' at {where}") + elif not ref.startswith(("http://", "https://")): + target = os.path.join(os.path.dirname(path), ref.split("#")[0]) + if not os.path.exists(target): + errors.append(f"{display}: $ref points at a missing file '{ref}'") + + return errors + + +def main() -> int: + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + spec_dir = os.path.join(root, "openapi") + if not os.path.isdir(spec_dir): + print("OpenAPI check SKIPPED (no openapi/ directory).") + return 0 + + specs = sorted( + glob.glob(os.path.join(spec_dir, "**", "*.yaml"), recursive=True) + + glob.glob(os.path.join(spec_dir, "**", "*.yml"), recursive=True) + + glob.glob(os.path.join(spec_dir, "**", "*.json"), recursive=True) + ) + + try: + from openapi_spec_validator import validate as spec_validate + + deep = True + except ImportError: + spec_validate = None + deep = False + + all_errors: list[str] = [] + for path in specs: + rel = os.path.relpath(path, root) + errors = check_file(path, rel) + if not errors and spec_validate is not None: + try: + spec_validate(load(path)) + except Exception as exc: # noqa: BLE001 - surface the validator's message + first = str(exc).split("\n")[0] + errors.append(f"{rel}: failed OpenAPI schema validation: {first}") + all_errors.extend(errors) + + if all_errors: + print("OpenAPI check FAILED:") + for err in all_errors: + print(f" {err}") + return 1 + + depth = "structure + schema" if deep else "structure only (validator not installed)" + print(f"OpenAPI check PASSED ({len(specs)} specs validated, {depth}).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 2df35722674e2561aa8a08990e1144c0e64bba75 Mon Sep 17 00:00:00 2001 From: Tyler Goerzen Date: Fri, 25 Sep 2026 12:52:31 -0700 Subject: [PATCH 5/9] Baseline existing violations so the gates only block new ones; reject redirect chains; validate OpenAPI examples The frontmatter gate failed on main content (362 pages missing a description, 14 duplicate-title groups), so it would have blocked every unrelated PR. Known violations now live in scripts/docs-ci-baseline.json and only new ones fail. Entries that get fixed are reported, never fatal, so TOF-439 (#172) and TOF-441 (#174) can land without touching it. - check_redirects: reject chains (destination is itself redirected); 229 existing chains baselined for #174. Loops reported once per cycle. - check_openapi: validate media-type, parameter and schema-level examples against their schemas; fix the one invalid GDPR example. - check_code_samples: tag two bare fences that landed on main. Co-Authored-By: Claude Opus 5.5 --- docs/mcp.mdx | 2 +- docs/tracking-methods/first-party-domains.mdx | 2 +- openapi/gdpr.openapi.yaml | 2 +- .../__pycache__/check_openapi.cpython-313.pyc | Bin 0 -> 12857 bytes .../__pycache__/ci_baseline.cpython-313.pyc | Bin 0 -> 4391 bytes scripts/check_frontmatter.py | 24 +- scripts/check_openapi.py | 90 ++- scripts/check_redirects.py | 90 +-- scripts/ci_baseline.py | 74 ++ scripts/docs-ci-baseline.json | 642 ++++++++++++++++++ 10 files changed, 871 insertions(+), 55 deletions(-) create mode 100644 scripts/__pycache__/check_openapi.cpython-313.pyc create mode 100644 scripts/__pycache__/ci_baseline.cpython-313.pyc create mode 100644 scripts/ci_baseline.py create mode 100644 scripts/docs-ci-baseline.json diff --git a/docs/mcp.mdx b/docs/mcp.mdx index 137a4cbb6..ab924dfdf 100644 --- a/docs/mcp.mdx +++ b/docs/mcp.mdx @@ -406,7 +406,7 @@ claude plugin install mixpanel Then, inside Claude Code: -``` +```text /mixpanel:install ``` diff --git a/docs/tracking-methods/first-party-domains.mdx b/docs/tracking-methods/first-party-domains.mdx index 5241c839b..31a726d96 100644 --- a/docs/tracking-methods/first-party-domains.mdx +++ b/docs/tracking-methods/first-party-domains.mdx @@ -40,7 +40,7 @@ Each organization can have up to **3** first-party domains. ## How it works -``` +```text Your app ──► track.yourcompany.com ──► Mixpanel ingestion API (CNAME to Mixpanel) ``` diff --git a/openapi/gdpr.openapi.yaml b/openapi/gdpr.openapi.yaml index 87bb8a949..1ebbd24d4 100644 --- a/openapi/gdpr.openapi.yaml +++ b/openapi/gdpr.openapi.yaml @@ -400,7 +400,7 @@ components: status: SUCCESS requesting_user: user@mail.com compliance_type: GDPR - project_id: your project ID + project_id: 12345 date_requested: YYYY-MM-DDTHH:MM:SS distinct_ids: - distinct_id_1 diff --git a/scripts/__pycache__/check_openapi.cpython-313.pyc b/scripts/__pycache__/check_openapi.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bf9de76dee76af1ec31172c705adc01230c2c4a GIT binary patch literal 12857 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/ci_baseline.cpython-313.pyc b/scripts/__pycache__/ci_baseline.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10c7c545cbae1f8fa221e85b45d6fef2f0eba1f7 GIT binary patch literal 4391 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 M@-2H$p9E|F2W(mt+W-In literal 0 HcmV?d00001 diff --git a/scripts/check_frontmatter.py b/scripts/check_frontmatter.py index 9dbf32139..f272ef583 100644 --- a/scripts/check_frontmatter.py +++ b/scripts/check_frontmatter.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 """ -CI gate: every MDX page must have a 'title' field in its YAML front-matter. +CI gate: every MDX page needs a non-empty title and description in its YAML +front-matter, and no two pages may share a title. + +Known violations on main are listed in docs-ci-baseline.json (see +ci_baseline.py); only new violations fail the check. Directories that are intentionally excluded from the check: - snippets/ (reusable MDX components, not standalone pages) @@ -13,6 +17,8 @@ import glob import os +from ci_baseline import report + EXCLUDED_DIRS = {"snippets", "links", "openapi"} FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL) @@ -74,20 +80,14 @@ def main() -> int: checked += 1 # Two pages sharing a rendered title are indistinguishable in search - # results and to answer engines. + # results and to answer engines. One entry per page (not per group) keeps + # baseline entries stable when a group shrinks during cleanup. for title, pages in sorted(titles.items()): if len(pages) > 1: - joined = ", ".join(sorted(pages)) - all_errors.append(f'duplicate title "{title}" on {len(pages)} pages: {joined}') - - if all_errors: - print("Frontmatter check FAILED:") - for err in all_errors: - print(f" {err}") - return 1 + for page in sorted(pages): + all_errors.append(f'{page}: title "{title}" is also used by another page') - print(f"Frontmatter check PASSED ({checked} files checked).") - return 0 + return report("frontmatter", "Frontmatter check", all_errors, f"{checked} files checked") if __name__ == "__main__": diff --git a/scripts/check_openapi.py b/scripts/check_openapi.py index d493fb5cb..7ea799903 100644 --- a/scripts/check_openapi.py +++ b/scripts/check_openapi.py @@ -8,7 +8,9 @@ with title and version, and paths). 3. Every local $ref ("#/...") resolves to a node that exists in the document. 4. If openapi-spec-validator is installed, the full spec is validated against - the OpenAPI schema. Without it, checks 1-3 still run. + the OpenAPI schema, and every example is validated against the schema it + illustrates (media-type and parameter `example`/`examples`, plus + schema-level `example`). Without it, checks 1-3 still run. """ import glob @@ -58,6 +60,88 @@ def resolves(doc, ref: str) -> bool: return False return True +# Keywords that mark a mapping as a Schema Object, so a schema-level `example` +# can be told apart from an arbitrary mapping that happens to have that key. +SCHEMA_KEYWORDS = {"type", "properties", "items", "allOf", "oneOf", "anyOf", "enum", "format", "$ref"} + + +def escape_pointer(key) -> str: + return str(key).replace("~", "~0").replace("/", "~1") + + +def deref(doc, node): + """Follow local $refs (e.g. to components/examples) to the target node.""" + for _ in range(20): + if not (isinstance(node, dict) and str(node.get("$ref", "")).startswith("#")): + return node + if not resolves(doc, node["$ref"]): + return None + target = doc + for part in node["$ref"][2:].split("/"): + part = part.replace("~1", "/").replace("~0", "~") + target = target[int(part)] if isinstance(target, list) else target[part] + node = target + return node + + +def iter_examples(doc, node, pointer=""): + """Yield (schema pointer, example location, example value) triples. + + Example Objects from an `examples` map are unwrapped to their `value` + (following $refs); ones using externalValue are skipped. + """ + if isinstance(node, list): + for index, value in enumerate(node): + yield from iter_examples(doc, value, f"{pointer}/{index}") + return + if not isinstance(node, dict): + return + + if isinstance(node.get("schema"), dict): + # Media Type or Parameter Object: examples illustrate node["schema"]. + if "example" in node: + yield f"{pointer}/schema", f"{pointer}/example", node["example"] + for name, example in (node.get("examples") or {}).items(): + example = deref(doc, example) + if isinstance(example, dict) and "value" in example: + yield f"{pointer}/schema", f"{pointer}/examples/{escape_pointer(name)}", example["value"] + elif "example" in node and SCHEMA_KEYWORDS & node.keys(): + yield pointer, f"{pointer}/example", node["example"] + + for key, value in node.items(): + # Example payloads are data, not schema; don't mistake their keys for + # keywords. + if key in ("example", "examples"): + continue + child = f"{pointer}/{escape_pointer(key)}" + if key == "properties" and isinstance(value, dict): + # Keys here are property names, so a property literally called + # "example" or "type" must not make the map look like a schema. + for name, prop in value.items(): + yield from iter_examples(doc, prop, f"{child}/{escape_pointer(name)}") + continue + yield from iter_examples(doc, value, child) + + +def check_examples(doc, display: str) -> list[str]: + """Validate every example against its schema, resolving $refs in the doc.""" + from openapi_schema_validator import OAS30Validator, OAS31Validator + from referencing import Registry, Resource + from referencing.jsonschema import DRAFT4, DRAFT202012 + + is_31 = str(doc.get("openapi", "")).startswith("3.1") + validator_cls = OAS31Validator if is_31 else OAS30Validator + resource = Resource.from_contents(doc, default_specification=DRAFT202012 if is_31 else DRAFT4) + registry = Registry().with_resource("urn:spec", resource) + + errors = [] + for schema_pointer, where, example in iter_examples(doc, doc): + validator = validator_cls({"$ref": f"urn:spec#{schema_pointer}"}, registry=registry) + first = next(iter(validator.iter_errors(example)), None) + if first is not None: + errors.append(f"{display}: example at {where} does not match its schema: {first.message}") + return errors + def check_file(path: str, display: str) -> list[str]: errors = [] @@ -126,6 +210,8 @@ def main() -> int: except Exception as exc: # noqa: BLE001 - surface the validator's message first = str(exc).split("\n")[0] errors.append(f"{rel}: failed OpenAPI schema validation: {first}") + else: + errors.extend(check_examples(load(path), rel)) all_errors.extend(errors) if all_errors: @@ -134,7 +220,7 @@ def main() -> int: print(f" {err}") return 1 - depth = "structure + schema" if deep else "structure only (validator not installed)" + depth = "structure + schema + examples" if deep else "structure only (validator not installed)" print(f"OpenAPI check PASSED ({len(specs)} specs validated, {depth}).") return 0 diff --git a/scripts/check_redirects.py b/scripts/check_redirects.py index b5a0b461f..d7a9f4e23 100644 --- a/scripts/check_redirects.py +++ b/scripts/check_redirects.py @@ -5,11 +5,16 @@ Checks performed: 1. No duplicate redirect source paths. 2. No redirect loops (a self-redirect or a cycle). - 3. Every redirect destination resolves to an existing page OR is itself - the source of another redirect, including a wildcard one (chained - redirects are allowed). - Wildcard destinations (containing '*') are skipped because their - validity is structural rather than path-based. + 3. No redirect chains: a destination must not itself be the source of + another redirect (exact or wildcard). Each hop costs crawlers and answer + engines a round trip, and chains decay into loops. Point the redirect at + the final page instead. + 4. Every destination resolves to an existing page. + Wildcard destinations (containing '*') are only checked for chains, + because their validity is structural rather than path-based. + +Known violations on main are listed in docs-ci-baseline.json (see +ci_baseline.py); only new violations fail the check. """ import fnmatch @@ -18,6 +23,8 @@ import sys import glob +from ci_baseline import report + def build_file_paths(root: str) -> set[str]: """Return all root-relative page paths derived from .mdx files.""" @@ -64,67 +71,74 @@ def main() -> int: # ── 2. Redirect loops (self-redirects and cycles) ─────────────────────── dest_by_source = {r.get("source", ""): r.get("destination", "") for r in redirects} - reported_loops: set[str] = set() + reported_cycles: set[frozenset[str]] = set() + reported_loop_sources: set[str] = set() for start in dest_by_source: - hops = {start} + path = [start] node = dest_by_source[start] while node in dest_by_source: - if node in hops: - if node not in reported_loops: - reported_loops.add(node) - all_errors.append( - f"docs.json: redirect loop starting at '{start}' revisits '{node}'" - ) + if node in path: + reported_loop_sources.add(start) + cycle = path[path.index(node):] + if frozenset(cycle) not in reported_cycles: + reported_cycles.add(frozenset(cycle)) + # Rotate to the smallest member so the message is stable. + pivot = cycle.index(min(cycle)) + ordered = cycle[pivot:] + cycle[:pivot] + hops = " -> ".join(ordered + [ordered[0]]) + all_errors.append(f"docs.json: redirect loop {hops}") break - hops.add(node) + path.append(node) node = dest_by_source[node] - # ── 3. Destinations resolve to a known page or another redirect source ── + # ── 3 + 4. Destinations are final pages, not further redirects ───────── file_paths = build_file_paths(root) exact_sources = {s for s in sources if "*" not in s} wildcard_sources = [s for s in sources if "*" in s] - def resolves(path: str) -> bool: - """A destination is valid if it is a real page, an exact redirect - source, or matched by a wildcard redirect source. The last case is a - chained redirect, which the CDN follows to a final landing page.""" - if path in file_paths or path in exact_sources: + def is_redirected(path: str) -> bool: + """True when the CDN would redirect `path` again (a chain).""" + if path in exact_sources: return True return any(fnmatch.fnmatch(path, pat) for pat in wildcard_sources) for redir in redirects: + src = redir.get("source", "") dest = redir.get("destination", "") if not dest: - all_errors.append( - f"docs.json: redirect from '{redir.get('source')}' has an empty destination" - ) + all_errors.append(f"docs.json: redirect from '{src}' has an empty destination") continue - # Skip wildcards / external URLs – structural validity only - if "*" in dest or dest.startswith("http"): + if dest.startswith("http"): continue # Strip anchors and query strings from destination dest_path = normalise(dest.split("#")[0]) - if not resolves(dest_path): + # Loops were reported above; don't double-report them as chains. + if src in reported_loop_sources: + continue + + if "*" in dest: + # A wildcard destination chains only if it is itself a source. + if dest_path in sources: + all_errors.append(f"docs.json: redirect chain '{src}' -> '{dest}' (destination is redirected again)") + continue + + if is_redirected(dest_path): + all_errors.append(f"docs.json: redirect chain '{src}' -> '{dest}' (destination is redirected again)") + elif dest_path not in file_paths: all_errors.append( f"docs.json: redirect destination '{dest}' does not resolve " - f"to a known page (source: '{redir.get('source')}')" + f"to a known page (source: '{src}')" ) - if all_errors: - print("Redirects check FAILED:") - for err in all_errors: - print(f" {err}") - return 1 - - print( - f"Redirects check PASSED ({len(redirects)} redirects validated, " - f"{len(file_paths)} pages indexed)." + return report( + "redirects", + "Redirects check", + all_errors, + f"{len(redirects)} redirects validated, {len(file_paths)} pages indexed", ) - return 0 - if __name__ == "__main__": sys.exit(main()) diff --git a/scripts/ci_baseline.py b/scripts/ci_baseline.py new file mode 100644 index 000000000..b318102b1 --- /dev/null +++ b/scripts/ci_baseline.py @@ -0,0 +1,74 @@ +""" +Shared baseline handling for the docs CI gates. + +A new gate should not block every unrelated PR on day one because of +violations that already exist on main. Each check can therefore list its +known, pre-existing violations in scripts/docs-ci-baseline.json. The check +fails only on violations that are NOT in the baseline, so new content is held +to the full standard while old content is cleaned up separately. + +Baseline entries that no longer occur are reported but never fail the build, +so a cleanup PR (for example TOF-439 descriptions or TOF-441 redirect chains) +can land without touching the baseline. Prune them with --update-baseline. + +To regenerate one check's entries from the current tree: + python scripts/check_frontmatter.py --update-baseline +""" + +import json +import os +import sys + +BASELINE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "docs-ci-baseline.json") + + +def _load() -> dict[str, list[str]]: + if not os.path.exists(BASELINE_PATH): + return {} + with open(BASELINE_PATH, encoding="utf-8") as fh: + return json.load(fh) + + +def _save(baseline: dict[str, list[str]]) -> None: + with open(BASELINE_PATH, "w", encoding="utf-8") as fh: + json.dump(baseline, fh, indent=2, sort_keys=True) + fh.write("\n") + + +def report(check: str, label: str, errors: list[str], passed_summary: str) -> int: + """Print results for one check and return its exit code. + + `errors` must be stable strings (no line numbers) so they can be matched + against the baseline across unrelated edits. + """ + if "--update-baseline" in sys.argv: + baseline = _load() + if errors: + baseline[check] = sorted(set(errors)) + else: + baseline.pop(check, None) + _save(baseline) + print(f"{label} baseline updated: {len(set(errors))} known violation(s) recorded.") + return 0 + + known = set(_load().get(check, [])) + new_errors = [err for err in errors if err not in known] + fixed = sorted(known - set(errors)) + baselined = len(errors) - len(new_errors) + + if fixed: + script = os.path.relpath(os.path.abspath(sys.argv[0]), os.path.dirname(os.path.dirname(BASELINE_PATH))) + print( + f"{label}: {len(fixed)} baseline entry(ies) no longer occur (fixed). " + f"Run `python {script} --update-baseline` to prune them." + ) + + if new_errors: + print(f"{label} FAILED ({len(new_errors)} new violation(s); {baselined} known and baselined):") + for err in new_errors: + print(f" {err}") + return 1 + + suffix = f"; {baselined} known violation(s) baselined" if baselined else "" + print(f"{label} PASSED ({passed_summary}{suffix}).") + return 0 diff --git a/scripts/docs-ci-baseline.json b/scripts/docs-ci-baseline.json new file mode 100644 index 000000000..e2b14623d --- /dev/null +++ b/scripts/docs-ci-baseline.json @@ -0,0 +1,642 @@ +{ + "frontmatter": [ + "changelogs.mdx: front-matter is missing required 'description' field", + "docs/access-security.mdx: front-matter is missing required 'description' field", + "docs/access-security/audit-log-reference.mdx: front-matter is missing required 'description' field", + "docs/access-security/audit-log.mdx: front-matter is missing required 'description' field", + "docs/access-security/login-methods.mdx: front-matter is missing required 'description' field", + "docs/access-security/single-sign-on/azure.mdx: front-matter is missing required 'description' field", + "docs/access-security/single-sign-on/google.mdx: front-matter is missing required 'description' field", + "docs/access-security/single-sign-on/jumpcloud.mdx: front-matter is missing required 'description' field", + "docs/access-security/single-sign-on/okta.mdx: front-matter is missing required 'description' field", + "docs/access-security/two-factor-authentication.mdx: front-matter is missing required 'description' field", + "docs/agentic-automations.mdx: front-matter is missing required 'description' field", + "docs/boards/sharing-and-permission.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/build-an-integration.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/abtasty.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/airship.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/appcues.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/apptimize.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/braze.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/chameleon.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/clevertap.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/facebook-ads.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/google-ads.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/insider.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/iterable.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/kameleoon.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/leanplum.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/mailchimp.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/marketo.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/moengage.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/mparticle.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/mparticle.mdx: title \"mParticle\" is also used by another page", + "docs/cohort-sync/integrations/onesignal.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/salesforce-marketing-cloud.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/segment.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/segment.mdx: title \"Segment\" is also used by another page", + "docs/cohort-sync/integrations/taplytics.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/vwo.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/webengage.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/integrations/xtremepush.mdx: front-matter is missing required 'description' field", + "docs/cohort-sync/webhooks.mdx: front-matter is missing required 'description' field", + "docs/community.mdx: front-matter is missing required 'description' field", + "docs/community/guidelines.mdx: front-matter is missing required 'description' field", + "docs/data-governance.mdx: front-matter is missing required 'description' field", + "docs/data-governance/ai-powered-data-governance.mdx: front-matter is missing required 'description' field", + "docs/data-governance/data-clean-up.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/common-sql-queries.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/integrations.mdx: title \"Data Pipeline Integrations\" is also used by another page", + "docs/data-pipelines/integrations/aws-s3.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/integrations/azure-blob-storage.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/integrations/bigquery.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/integrations/databricks.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/integrations/gcp-gcs.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/integrations/gcp-gcs.mdx: title \"Google Cloud Storage\" is also used by another page", + "docs/data-pipelines/integrations/redshift-spectrum.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/integrations/snowflake.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/json-pipelines.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations.mdx: title \"Data Pipeline Integrations\" is also used by another page", + "docs/data-pipelines/old-pipelines/integrations/raw-aws-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations/raw-azure-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations/raw-gcs-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations/schematized-aws-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations/schematized-azure-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations/schematized-bigquery-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations/schematized-gcs-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/integrations/schematized-snowflake-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-pipelines/old-pipelines/schematized-export-pipeline.mdx: front-matter is missing required 'description' field", + "docs/data-structure/lookup-tables.mdx: title \"Lookup Tables\" is also used by another page", + "docs/features.mdx: front-matter is missing required 'description' field", + "docs/features.mdx: title \"Features\" is also used by another page", + "docs/features/annotations.mdx: front-matter is missing required 'description' field", + "docs/features/comments.mdx: front-matter is missing required 'description' field", + "docs/features/computed-properties.mdx: front-matter is missing required 'description' field", + "docs/features/custom-buckets.mdx: front-matter is missing required 'description' field", + "docs/features/revenue-analytics.mdx: front-matter is missing required 'description' field", + "docs/features/saved-metrics-and-behaviors.mdx: front-matter is missing required 'description' field", + "docs/features/slack-integration.mdx: front-matter is missing required 'description' field", + "docs/mcp.mdx: front-matter is missing required 'description' field", + "docs/metric_tree.mdx: front-matter is missing required 'description' field", + "docs/migration.mdx: front-matter is missing required 'description' field", + "docs/migration/adobe-analytics.mdx: front-matter is missing required 'description' field", + "docs/migration/amplitude.mdx: front-matter is missing required 'description' field", + "docs/migration/google-analytics.mdx: front-matter is missing required 'description' field", + "docs/mixpanel-agent.mdx: front-matter is missing required 'description' field", + "docs/mixpanel-headless.mdx: front-matter is missing required 'description' field", + "docs/mixpanel-headless.mdx: title \"Mixpanel Headless\" is also used by another page", + "docs/orgs-and-projects.mdx: front-matter is missing required 'description' field", + "docs/orgs-and-projects/managing-projects.mdx: front-matter is missing required 'description' field", + "docs/orgs-and-projects/organizations.mdx: front-matter is missing required 'description' field", + "docs/orgs-and-projects/roles-and-permissions.mdx: front-matter is missing required 'description' field", + "docs/pricing/legacy-mtu-billing.mdx: front-matter is missing required 'description' field", + "docs/privacy.mdx: front-matter is missing required 'description' field", + "docs/privacy/eu-residency.mdx: front-matter is missing required 'description' field", + "docs/privacy/gdpr-compliance.mdx: front-matter is missing required 'description' field", + "docs/privacy/in-residency.mdx: front-matter is missing required 'description' field", + "docs/quickstart.mdx: front-matter is missing required 'description' field", + "docs/quickstart/capture-events.mdx: front-matter is missing required 'description' field", + "docs/quickstart/capture-events/autocapture.mdx: front-matter is missing required 'description' field", + "docs/quickstart/capture-events/autocapture.mdx: title \"Autocapture\" is also used by another page", + "docs/quickstart/capture-events/track-events.mdx: front-matter is missing required 'description' field", + "docs/quickstart/capture-events/track-events.mdx: title \"Track Events\" is also used by another page", + "docs/quickstart/company-analytics.mdx: front-matter is missing required 'description' field", + "docs/quickstart/connect-your-data.mdx: front-matter is missing required 'description' field", + "docs/quickstart/identify-users.mdx: front-matter is missing required 'description' field", + "docs/quickstart/install-mixpanel.mdx: front-matter is missing required 'description' field", + "docs/quickstart/install-with-ai.mdx: front-matter is missing required 'description' field", + "docs/quickstart/tips-and-tricks.mdx: front-matter is missing required 'description' field", + "docs/reports.mdx: front-matter is missing required 'description' field", + "docs/reports/apps.mdx: front-matter is missing required 'description' field", + "docs/reports/funnels.mdx: title \"Funnels\" is also used by another page", + "docs/reports/funnels/funnels-advanced.mdx: front-matter is missing required 'description' field", + "docs/reports/funnels/funnels-faq.mdx: front-matter is missing required 'description' field", + "docs/reports/funnels/funnels-overview.mdx: title \"Funnels\" is also used by another page", + "docs/reports/funnels/funnels-quickstart.mdx: front-matter is missing required 'description' field", + "docs/response-times.mdx: front-matter is missing required 'description' field", + "docs/session-replay/heatmaps.mdx: front-matter is missing required 'description' field", + "docs/session-replay/session-replay-privacy-controls.mdx: front-matter is missing required 'description' field", + "docs/tracking-best-practices.mdx: front-matter is missing required 'description' field", + "docs/tracking-best-practices/bot-traffic.mdx: front-matter is missing required 'description' field", + "docs/tracking-best-practices/developer-environments.mdx: front-matter is missing required 'description' field", + "docs/tracking-best-practices/hot-shard-limits.mdx: front-matter is missing required 'description' field", + "docs/tracking-best-practices/server-side-best-practices.mdx: front-matter is missing required 'description' field", + "docs/tracking-best-practices/tracking-plan.mdx: front-matter is missing required 'description' field", + "docs/tracking-best-practices/warehouse-best-practices.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/autocapture.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/autocapture.mdx: title \"Autocapture\" is also used by another page", + "docs/tracking-methods/choosing-the-right-method.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/data-inspector.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/id-management.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/id-management/identifying-users-original.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/id-management/identifying-users-simplified.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/id-management/migrating-to-simplified-id-merge-system.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/ad-spend.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/amazon-s3.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/aws-kafka.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/cms-ecommerce.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/freshpaint.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/google-cloud-storage.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/google-cloud-storage.mdx: title \"Google Cloud Storage\" is also used by another page", + "docs/tracking-methods/integrations/google-pubsub.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/google-sheets.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/google-tag-manager.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/langfuse.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/launchdarkly.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/mobile-attribution-tracking.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/mparticle.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/mparticle.mdx: title \"mParticle\" is also used by another page", + "docs/tracking-methods/integrations/nextjs.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/rudderstack.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/segment.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/segment.mdx: title \"Segment\" is also used by another page", + "docs/tracking-methods/integrations/shopify.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/snowplow.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/stripe.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/tealium.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/integrations/vendo.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/android.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/android/android-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/android/android-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/android/android-replay.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/flutter.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/flutter/flutter-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/flutter/flutter-replay.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/go.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/go/go-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/go/go-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/ios.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/java/index.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/java/java-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/java/java-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/javascript.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/javascript/javascript-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/javascript/javascript-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/javascript/javascript-replay.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/nodejs.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/nodejs/nodejs-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/nodejs/nodejs-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/php.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/python.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/python/python-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/python/python-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/react-native.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/react-native/react-native-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/react-native/react-native-replay.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/ruby.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/ruby/ruby-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/ruby/ruby-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/swift.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/swift/swift-flags.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/swift/swift-openfeature.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/swift/swift-replay.mdx: front-matter is missing required 'description' field", + "docs/tracking-methods/sdks/unity.mdx: front-matter is missing required 'description' field", + "guides/benchmarks.mdx: front-matter is missing required 'description' field", + "guides/board-templates.mdx: front-matter is missing required 'description' field", + "guides/glossary.mdx: front-matter is missing required 'description' field", + "guides/guides-by-topic/continuous-innovation.mdx: front-matter is missing required 'description' field", + "guides/guides-by-topic/core-reports.mdx: front-matter is missing required 'description' field", + "guides/guides-by-topic/features.mdx: front-matter is missing required 'description' field", + "guides/guides-by-topic/features.mdx: title \"Features\" is also used by another page", + "guides/guides-by-topic/govern-data.mdx: front-matter is missing required 'description' field", + "guides/guides-by-use-case.mdx: front-matter is missing required 'description' field", + "guides/guides-by-use-case/empower-your-team/close-strategy-execution-gap.mdx: front-matter is missing required 'description' field", + "guides/guides-by-use-case/empower-your-team/see-replays.mdx: front-matter is missing required 'description' field", + "guides/guides-by-use-case/engage-your-users/drive-product-innovation.mdx: front-matter is missing required 'description' field", + "guides/guides-by-use-case/engage-your-users/ship-features.mdx: front-matter is missing required 'description' field", + "guides/guides-by-use-case/grow-your-usership/grow-revenue.mdx: front-matter is missing required 'description' field", + "guides/guides-by-workflow/build-tracking-strategy.mdx: front-matter is missing required 'description' field", + "guides/guides-by-workflow/data-privacy.mdx: front-matter is missing required 'description' field", + "guides/guides-by-workflow/embed-data.mdx: front-matter is missing required 'description' field", + "guides/guides-by-workflow/ensure-data-quality.mdx: front-matter is missing required 'description' field", + "guides/guides-by-workflow/sync-with-mirror.mdx: front-matter is missing required 'description' field", + "guides/headless.mdx: title \"Mixpanel Headless\" is also used by another page", + "guides/mcp.mdx: front-matter is missing required 'description' field", + "guides/mcp/integrations.mdx: front-matter is missing required 'description' field", + "guides/mcp/mcp-by-industry.mdx: front-matter is missing required 'description' field", + "guides/mixpanel-agent.mdx: front-matter is missing required 'description' field", + "guides/mixpanel-agent/use-cases.mdx: front-matter is missing required 'description' field", + "guides/mixpanel-introduction.mdx: front-matter is missing required 'description' field", + "guides/self-guided-tours.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/feature-flag-migration-playbook.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/guide-to-product-analytics/conclusion.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/guide-to-product-analytics/get-to-know-your-users.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/guide-to-product-analytics/grow-and-scale.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/guide-to-product-analytics/intro.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/guide-to-product-analytics/retain-your-users.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/onboarding-playbook.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/product-analytics-strategy/advanced.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/product-analytics-strategy/expert.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/product-analytics-strategy/finale.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/product-analytics-strategy/get-started.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/product-analytics-strategy/intermediate.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/product-analytics-strategy/non-existent.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/product-analytics-strategy/novice.mdx: front-matter is missing required 'description' field", + "guides/strategic-playbooks/project-migration.mdx: front-matter is missing required 'description' field", + "guides/use-mixpanel-ai/trust-ai-answers.mdx: front-matter is missing required 'description' field", + "guides/use-mixpanel-ai/write-business-context.mdx: front-matter is missing required 'description' field", + "reference/activity-stream-query.mdx: front-matter is missing required 'description' field", + "reference/add-service-accounts-to-projects.mdx: front-matter is missing required 'description' field", + "reference/archive-experiment.mdx: front-matter is missing required 'description' field", + "reference/authentication-2.mdx: front-matter is missing required 'description' field", + "reference/authentication-2.mdx: title \"Authentication\" is also used by another page", + "reference/authentication.mdx: front-matter is missing required 'description' field", + "reference/cancel-warehouse-pipeline.mdx: front-matter is missing required 'description' field", + "reference/cohorts-list.mdx: front-matter is missing required 'description' field", + "reference/create-annotation-tag-1.mdx: front-matter is missing required 'description' field", + "reference/create-annotation.mdx: front-matter is missing required 'description' field", + "reference/create-deletion.mdx: front-matter is missing required 'description' field", + "reference/create-event-stream-import.mdx: front-matter is missing required 'description' field", + "reference/create-experiment.mdx: front-matter is missing required 'description' field", + "reference/create-feature-flag-1.mdx: front-matter is missing required 'description' field", + "reference/create-groups-import.mdx: front-matter is missing required 'description' field", + "reference/create-identity.mdx: front-matter is missing required 'description' field", + "reference/create-lookup-table-import.mdx: front-matter is missing required 'description' field", + "reference/create-people-import.mdx: front-matter is missing required 'description' field", + "reference/create-retrieval-1.mdx: front-matter is missing required 'description' field", + "reference/create-service-account.mdx: front-matter is missing required 'description' field", + "reference/create-warehouse-pipeline.mdx: front-matter is missing required 'description' field", + "reference/decide-experiment.mdx: front-matter is missing required 'description' field", + "reference/delete-all-schemas-in-project.mdx: front-matter is missing required 'description' field", + "reference/delete-annotation-1.mdx: front-matter is missing required 'description' field", + "reference/delete-deletion.mdx: front-matter is missing required 'description' field", + "reference/delete-experiment.mdx: front-matter is missing required 'description' field", + "reference/delete-feature-flag-1.mdx: front-matter is missing required 'description' field", + "reference/delete-group.mdx: front-matter is missing required 'description' field", + "reference/delete-profile.mdx: front-matter is missing required 'description' field", + "reference/delete-schema-by-entity-and-name.mdx: front-matter is missing required 'description' field", + "reference/delete-schemas-for-entity.mdx: front-matter is missing required 'description' field", + "reference/delete-service-account.mdx: front-matter is missing required 'description' field", + "reference/delete-warehouse-import.mdx: front-matter is missing required 'description' field", + "reference/edit-warehouse-pipeline.mdx: front-matter is missing required 'description' field", + "reference/engage-query.mdx: front-matter is missing required 'description' field", + "reference/event-deduplication.mdx: front-matter is missing required 'description' field", + "reference/feature-flags-api.mdx: front-matter is missing required 'description' field", + "reference/feature-flags-api.mdx: title \"Overview\" is also used by another page", + "reference/feature-flags-management-api.mdx: front-matter is missing required 'description' field", + "reference/feature-flags-management-api.mdx: title \"Overview\" is also used by another page", + "reference/force-conclude-experiment.mdx: front-matter is missing required 'description' field", + "reference/funnels-list-saved.mdx: front-matter is missing required 'description' field", + "reference/funnels-query.mdx: front-matter is missing required 'description' field", + "reference/funnels-query.mdx: title \"Query Saved Report\" is also used by another page", + "reference/gdpr-api.mdx: front-matter is missing required 'description' field", + "reference/gdpr-api.mdx: title \"Overview\" is also used by another page", + "reference/get-annotation-1.mdx: front-matter is missing required 'description' field", + "reference/get-annotation-tags-1.mdx: front-matter is missing required 'description' field", + "reference/get-deletion.mdx: front-matter is missing required 'description' field", + "reference/get-experiment.mdx: front-matter is missing required 'description' field", + "reference/get-feature-flag-1.mdx: front-matter is missing required 'description' field", + "reference/get-flag-definitions.mdx: front-matter is missing required 'description' field", + "reference/get-import-history.mdx: front-matter is missing required 'description' field", + "reference/get-retrieval.mdx: front-matter is missing required 'description' field", + "reference/get-service-account.mdx: front-matter is missing required 'description' field", + "reference/get-variant-assignments.mdx: front-matter is missing required 'description' field", + "reference/get-warehouse-import.mdx: front-matter is missing required 'description' field", + "reference/get-warehouse-pipeline-status.mdx: front-matter is missing required 'description' field", + "reference/group-batch-update.mdx: front-matter is missing required 'description' field", + "reference/group-delete-property.mdx: front-matter is missing required 'description' field", + "reference/group-remove-from-list-property.mdx: front-matter is missing required 'description' field", + "reference/group-set-property-once.mdx: front-matter is missing required 'description' field", + "reference/group-set-property.mdx: front-matter is missing required 'description' field", + "reference/group-union.mdx: front-matter is missing required 'description' field", + "reference/identity-create-alias.mdx: front-matter is missing required 'description' field", + "reference/identity-merge.mdx: front-matter is missing required 'description' field", + "reference/import-events.mdx: front-matter is missing required 'description' field", + "reference/ingestion-api-authentication.mdx: front-matter is missing required 'description' field", + "reference/ingestion-api-authentication.mdx: title \"Authentication\" is also used by another page", + "reference/ingestion-api.mdx: front-matter is missing required 'description' field", + "reference/ingestion-api.mdx: title \"Overview\" is also used by another page", + "reference/insights-query.mdx: front-matter is missing required 'description' field", + "reference/insights-query.mdx: title \"Query Saved Report\" is also used by another page", + "reference/launch-experiment.mdx: front-matter is missing required 'description' field", + "reference/lexicon-schemas-api-authentication.mdx: front-matter is missing required 'description' field", + "reference/lexicon-schemas-api-authentication.mdx: title \"Authentication\" is also used by another page", + "reference/lexicon-schemas-api.mdx: front-matter is missing required 'description' field", + "reference/lexicon-schemas-api.mdx: title \"Overview\" is also used by another page", + "reference/limits-1.mdx: front-matter is missing required 'description' field", + "reference/limits-1.mdx: title \"Limits\" is also used by another page", + "reference/limits.mdx: front-matter is missing required 'description' field", + "reference/limits.mdx: title \"Limits\" is also used by another page", + "reference/list-all-annotations-for-project.mdx: front-matter is missing required 'description' field", + "reference/list-all-schemas-for-project.mdx: front-matter is missing required 'description' field", + "reference/list-experiments.mdx: front-matter is missing required 'description' field", + "reference/list-feature-flags-1.mdx: front-matter is missing required 'description' field", + "reference/list-lookup-tables.mdx: front-matter is missing required 'description' field", + "reference/list-project-service-accounts.mdx: front-matter is missing required 'description' field", + "reference/list-recent-events.mdx: front-matter is missing required 'description' field", + "reference/list-schemas-by-entity-and-name.mdx: front-matter is missing required 'description' field", + "reference/list-schemas-for-entity.mdx: front-matter is missing required 'description' field", + "reference/list-service-accounts.mdx: front-matter is missing required 'description' field", + "reference/list-warehouse-imports.mdx: front-matter is missing required 'description' field", + "reference/list-warehouse-pipeline-jobs.mdx: front-matter is missing required 'description' field", + "reference/list-warehouse-pipeline-sync-dates.mdx: front-matter is missing required 'description' field", + "reference/lookup-tables.mdx: front-matter is missing required 'description' field", + "reference/lookup-tables.mdx: title \"Lookup Tables\" is also used by another page", + "reference/overview-1.mdx: front-matter is missing required 'description' field", + "reference/overview-1.mdx: title \"Overview\" is also used by another page", + "reference/overview-2.mdx: front-matter is missing required 'description' field", + "reference/overview-2.mdx: title \"Overview\" is also used by another page", + "reference/overview.mdx: front-matter is missing required 'description' field", + "reference/overview.mdx: title \"Overview\" is also used by another page", + "reference/patch-annotation-1.mdx: front-matter is missing required 'description' field", + "reference/pause-warehouse-pipeline.mdx: front-matter is missing required 'description' field", + "reference/permissions.mdx: front-matter is missing required 'description' field", + "reference/platform-api.mdx: front-matter is missing required 'description' field", + "reference/platform-api.mdx: title \"Overview\" is also used by another page", + "reference/profile-append-to-list-property.mdx: front-matter is missing required 'description' field", + "reference/profile-batch-update.mdx: front-matter is missing required 'description' field", + "reference/profile-delete-property.mdx: front-matter is missing required 'description' field", + "reference/profile-numerical-add.mdx: front-matter is missing required 'description' field", + "reference/profile-remove-from-list-property.mdx: front-matter is missing required 'description' field", + "reference/profile-set-property-once.mdx: front-matter is missing required 'description' field", + "reference/profile-set.mdx: front-matter is missing required 'description' field", + "reference/project-secret.mdx: front-matter is missing required 'description' field", + "reference/project-token.mdx: front-matter is missing required 'description' field", + "reference/query-api-authentication.mdx: front-matter is missing required 'description' field", + "reference/query-api-authentication.mdx: title \"Authentication\" is also used by another page", + "reference/query-api.mdx: front-matter is missing required 'description' field", + "reference/query-api.mdx: title \"Overview\" is also used by another page", + "reference/query-event-properties.mdx: front-matter is missing required 'description' field", + "reference/query-events-top-properties.mdx: front-matter is missing required 'description' field", + "reference/query-events-top-property-values.mdx: front-matter is missing required 'description' field", + "reference/query-jql.mdx: front-matter is missing required 'description' field", + "reference/query-months-top-event-names.mdx: front-matter is missing required 'description' field", + "reference/query-top-events.mdx: front-matter is missing required 'description' field", + "reference/rate-limits.mdx: front-matter is missing required 'description' field", + "reference/raw-data-export-api-authentication.mdx: front-matter is missing required 'description' field", + "reference/raw-data-export-api-authentication.mdx: title \"Authentication\" is also used by another page", + "reference/raw-data-export-api.mdx: front-matter is missing required 'description' field", + "reference/raw-data-export-api.mdx: title \"Overview\" is also used by another page", + "reference/raw-event-export.mdx: front-matter is missing required 'description' field", + "reference/remove-service-accounts-from-projects.mdx: front-matter is missing required 'description' field", + "reference/replace-lookup-table.mdx: front-matter is missing required 'description' field", + "reference/request-signature.mdx: front-matter is missing required 'description' field", + "reference/resume-warehouse-pipeline.mdx: front-matter is missing required 'description' field", + "reference/retention-frequency-query.mdx: front-matter is missing required 'description' field", + "reference/retention-query.mdx: front-matter is missing required 'description' field", + "reference/run-an-import-1.mdx: front-matter is missing required 'description' field", + "reference/run-an-import.mdx: front-matter is missing required 'description' field", + "reference/segmentation-expressions.mdx: front-matter is missing required 'description' field", + "reference/segmentation-numeric-query.mdx: front-matter is missing required 'description' field", + "reference/segmentation-query-average.mdx: front-matter is missing required 'description' field", + "reference/segmentation-query.mdx: front-matter is missing required 'description' field", + "reference/segmentation-sum-query.mdx: front-matter is missing required 'description' field", + "reference/service-accounts-api-authentication.mdx: front-matter is missing required 'description' field", + "reference/service-accounts-api-authentication.mdx: title \"Authentication\" is also used by another page", + "reference/service-accounts-api.mdx: front-matter is missing required 'description' field", + "reference/service-accounts-api.mdx: title \"Overview\" is also used by another page", + "reference/service-accounts.mdx: front-matter is missing required 'description' field", + "reference/track-event.mdx: front-matter is missing required 'description' field", + "reference/track-event.mdx: title \"Track Events\" is also used by another page", + "reference/unarchive-experiment.mdx: front-matter is missing required 'description' field", + "reference/update-experiment.mdx: front-matter is missing required 'description' field", + "reference/update-feature-flag-1.mdx: front-matter is missing required 'description' field", + "reference/update-warehouse-import.mdx: front-matter is missing required 'description' field", + "reference/upload-schema-by-entity-and-name.mdx: front-matter is missing required 'description' field", + "reference/upload-schemas-for-project.mdx: front-matter is missing required 'description' field", + "reference/user-profile-limits.mdx: front-matter is missing required 'description' field", + "reference/user-profile-limits.mdx: title \"Limits\" is also used by another page", + "reference/user-profile-union.mdx: front-matter is missing required 'description' field", + "reference/warehouse-connectors-api-authentication.mdx: front-matter is missing required 'description' field", + "reference/warehouse-connectors-api-authentication.mdx: title \"Authentication\" is also used by another page", + "reference/warehouse-connectors-api.mdx: front-matter is missing required 'description' field", + "reference/warehouse-connectors-api.mdx: title \"Overview\" is also used by another page", + "troubleshooting/faqs.mdx: front-matter is missing required 'description' field" + ], + "redirects": [ + "docs.json: redirect chain '/developer-docs-redirect/android-push-notifications' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/aws-raw-pipeline' -> '/docs/data-pipelines/integrations/aws-raw-pipeline' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/azure-raw-pipeline' -> '/docs/data-pipelines/integrations/azure-raw-pipeline' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/client-side-vs-server-side-tracking' -> '/docs/getting-started/plan-your-implementation#need-to-start-tracking-product-data' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/cloud-ingestion' -> '/docs/quickstart/track-events?sdk=httpapi' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/data-structure-deep-dive' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/gcs' -> '/docs/data-pipelines/integrations/google-cloud-storage' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/gcs-raw-pipeline' -> '/docs/data-pipelines/integrations/google-cloud-storage-raw-pipeline' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/plan-your-implementation' -> '/docs/getting-started/plan-your-implementation' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/sending-events' -> '/docs/quickstart/track-events' (destination is redirected again)", + "docs.json: redirect chain '/developer-docs-redirect/under-the-hood' -> '/docs/how-it-works/infrastructure' (destination is redirected again)", + "docs.json: redirect chain '/docs/analysis/advanced/group-analytics' -> '/docs/data-structure/advanced/group-analytics' (destination is redirected again)", + "docs.json: redirect chain '/docs/analysis/advanced/other-advanced-features' -> '/docs/features/advanced' (destination is redirected again)", + "docs.json: redirect chain '/docs/analysis/advanced/other-advanced-features.md' -> '/docs/features/advanced' (destination is redirected again)", + "docs.json: redirect chain '/docs/analysis/boards/advanced' -> '/docs/boards/advanced' (destination is redirected again)", + "docs.json: redirect chain '/docs/android-push-notifications' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/docs/best-practices/analytics-strategy' -> '/guides/plan/framework' (destination is redirected again)", + "docs.json: redirect chain '/docs/best-practices/analytics-strategy/overview' -> '/guides/plan/framework' (destination is redirected again)", + "docs.json: redirect chain '/docs/best-practices/analytics-strategy/retention' -> '/guides/launch/track-user-retention' (destination is redirected again)", + "docs.json: redirect chain '/docs/data-pipelines/integrations/aws-raw-pipeline' -> '/docs/data-pipelines/integrations/raw-aws-pipeline' (destination is redirected again)", + "docs.json: redirect chain '/docs/features/revenue_analytics' -> '/guides/launch/revenue-analytics' (destination is redirected again)", + "docs.json: redirect chain '/docs/ios-push-notifications' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/data-pipelines/aws-raw-pipeline' -> '/docs/data-pipelines/integrations/aws-raw-pipeline' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/data-pipelines/azure-raw-pipeline' -> '/docs/data-pipelines/integrations/azure-raw-pipeline' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/data-pipelines/gcs' -> '/docs/data-pipelines/integrations/google-cloud-storage' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/data-pipelines/gcs-raw-pipeline' -> '/docs/data-pipelines/integrations/google-cloud-storage-raw-pipeline' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/adoption-reference-guide' -> '/docs/best-practices/adoption' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/analytics-strategy' -> '/docs/best-practices/analytics-strategy/overview' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/creating-a-tracking-plan' -> '/docs/best-practices/create-a-tracking-plan' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/developers/mixpanel-for-developers-commonly-asked-questions' -> '/docs/debugging/overview' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/developers/mixpanel-for-developers-fundamentals' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/developers/mixpanel-for-developers-id-management' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/mixpanel-analysis' -> '/docs/how-it-works/concepts#overview' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/mixpanel-analysis/data-model' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/mixpanel-analysis/overview' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/tutorials/setting-up-mixpanel' -> '/docs/best-practices/project-setup' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/under-the-hoo' -> '/docs/how-it-works/infrastructure' (destination is redirected again)", + "docs.json: redirect chain '/docs/other-bits/under-the-hood' -> '/docs/how-it-works/infrastructure' (destination is redirected again)", + "docs.json: redirect chain '/docs/session-replay/session-replay-android' -> '/docs/session-replay/implement-session-replay/session-replay-android' (destination is redirected again)", + "docs.json: redirect chain '/docs/session-replay/session-replay-ios' -> '/docs/session-replay/implement-session-replay/session-replay-ios' (destination is redirected again)", + "docs.json: redirect chain '/docs/session-replay/session-replay-web' -> '/docs/session-replay/implement-session-replay/session-replay-web' (destination is redirected again)", + "docs.json: redirect chain '/docs/sms-notifications' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking-methods/identifying-users' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/bigquery' -> '/docs/tracking-methods/data-warehouse/bigquery' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/data-warehouse' -> '/docs/tracking-methods/data-warehouse/overview' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/data-warehouse/bigquery' -> '/docs/tracking-methods/data-warehouse/bigquery' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/data-warehouse/databricks' -> '/docs/tracking-methods/data-warehouse/databricks' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/data-warehouse/events' -> '/docs/tracking-methods/data-warehouse/sending-events' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/data-warehouse/redshift' -> '/docs/tracking-methods/data-warehouse/redshift' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/data-warehouse/snowflake' -> '/docs/tracking-methods/data-warehouse/snowflake' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/data-warehouse/users' -> '/docs/tracking-methods/data-warehouse/sending-user-profiles' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/ad-spend' -> '/docs/data-structure/advanced/ad-spend' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/debugging' -> '/docs/debugging/overview' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/effective-server' -> '/docs/best-practices/server-side-best-practices' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/effective-server-side-tracking' -> '/docs/best-practices/server-side-best-practices' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/identifying-users' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/set-up-projects' -> '/docs/best-practices/developer-environments' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/tracking-utm-tags' -> '/docs/data-structure/advanced/utm-tags' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/how-tos/utm' -> '/docs/data-structure/advanced/utm-tags' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/http-api' -> '/docs/quickstart/track-events?sdk=httpapi' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/integrations/bigquery' -> '/docs/tracking-methods/data-warehouse/bigquery' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/integrations/snowflake' -> '/docs/tracking-methods/data-warehouse/snowflake' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/mobile' -> '/docs/tracking/reference/mobile-sdk' (destination is redirected again)", + "docs.json: redirect chain '/docs/tracking/reference/data-model' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/analytics-strategy' -> '/guides/analytics-strategy' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/beyond-onboarding' -> '/guides/beyond-onboarding' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/developers' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/implement/establish-governance' -> '/guides/implement/establish-governance' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/implement/qa-data-audit' -> '/guides/implement/qa-data-audit' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/implement/send-your-data' -> '/guides/implement/send-your-data' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/launch/analyze-conversions' -> '/guides/launch/analyze-conversions' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/launch/build-user-flows' -> '/guides/launch/build-user-flows' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/launch/create-boards' -> '/guides/launch/create-boards' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/launch/define-cohorts' -> '/guides/launch/define-cohorts' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/launch/discover-insights' -> '/guides/launch/discover-insights' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/launch/track-user-retention' -> '/guides/launch/track-user-retention' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/mixpanel-analysis' -> '/docs/how-it-works/concepts#overview' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/onboarding-overview' -> '/guides/onboarding-overview' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/plan/framework' -> '/guides/plan/framework' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/plan/setup' -> '/guides/plan/setup' (destination is redirected again)", + "docs.json: redirect chain '/docs/tutorials/plan/tracking-strategy' -> '/guides/plan/tracking-strategy' (destination is redirected again)", + "docs.json: redirect chain '/guides/analytics-strategy' -> '/guides/plan/framework' (destination is redirected again)", + "docs.json: redirect chain '/guides/beyond-onboarding' -> '/guides/strategic-playbooks/onboarding-playbook/beyond-onboarding' (destination is redirected again)", + "docs.json: redirect chain '/guides/launch/analyze-conversions' -> '/guides/strategic-playbooks/onboarding-playbook/launch/analyze-conversions' (destination is redirected again)", + "docs.json: redirect chain '/guides/launch/build-user-flows' -> '/guides/strategic-playbooks/onboarding-playbook/launch/build-user-flows' (destination is redirected again)", + "docs.json: redirect chain '/guides/launch/create-boards' -> '/guides/strategic-playbooks/onboarding-playbook/launch/create-boards' (destination is redirected again)", + "docs.json: redirect chain '/guides/launch/define-cohorts' -> '/guides/strategic-playbooks/onboarding-playbook/launch/define-cohorts' (destination is redirected again)", + "docs.json: redirect chain '/guides/launch/discover-insights' -> '/guides/strategic-playbooks/onboarding-playbook/launch/discover-insights' (destination is redirected again)", + "docs.json: redirect chain '/guides/launch/revenue-analytics' -> '/guides/strategic-playbooks/onboarding-playbook/launch/revenue-analytics' (destination is redirected again)", + "docs.json: redirect chain '/guides/launch/track-user-retention' -> '/guides/strategic-playbooks/onboarding-playbook/launch/track-user-retention' (destination is redirected again)", + "docs.json: redirect chain '/guides/strategic-playbooks/onboarding-playbook/launch/analyze-conversions' -> '/guides/guides-by-topic/core-reports/analyze-conversions' (destination is redirected again)", + "docs.json: redirect chain '/guides/strategic-playbooks/onboarding-playbook/launch/build-user-flows' -> '/guides/guides-by-topic/core-reports/build-user-flows' (destination is redirected again)", + "docs.json: redirect chain '/guides/strategic-playbooks/onboarding-playbook/launch/create-boards' -> '/guides/guides-by-topic/core-reports/create-boards' (destination is redirected again)", + "docs.json: redirect chain '/guides/strategic-playbooks/onboarding-playbook/launch/define-cohorts' -> '/guides/guides-by-topic/core-reports/define-cohorts' (destination is redirected again)", + "docs.json: redirect chain '/guides/strategic-playbooks/onboarding-playbook/launch/discover-insights' -> '/guides/guides-by-topic/core-reports/discover-insights' (destination is redirected again)", + "docs.json: redirect chain '/guides/strategic-playbooks/onboarding-playbook/launch/track-user-retention' -> '/guides/guides-by-topic/core-reports/track-user-retention' (destination is redirected again)", + "docs.json: redirect chain '/hc/admin/general_settings' -> '/docs/admin/organizations-projects' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us' -> '/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/10147279357076*' -> '/docs/debugging/overview#data-discrepancies' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004491523*' -> '/docs/best-practices/developer-environments#send-data-to-multiple-projects' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004491683*' -> '/docs/best-practices/developer-environments#when-to-use-multiple-production-projects' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004495783*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004497803*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004499323*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004499343*' -> '/docs/best-practices/server-side-best-practices#tracking-geolocation' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004499403*' -> '/docs/features/advanced#undefined-and-null-properties' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004509406*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004509426*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004510946*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004511086*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004511126*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004511246*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004519886*' -> '/docs/best-practices/create-a-tracking-plan' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004545603*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004547063*' -> '/docs/how-it-works/concepts#supported-data-types' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004551583*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004561766*' -> '/docs/tracking/how-tos/utm' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004561786*' -> '/docs/tracking/how-tos/utm' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004562563*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004563123*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004565766*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004567026*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004580746*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004581743*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004581763*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004582646*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004582706*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004582746*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004600343*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004600723*' -> '/docs/data-structure/advanced/utm-tags#mobile-attribution' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004602143*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004602503*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004602603*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004602683*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004615466*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004615486*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004615526*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004615566*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004616406*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004616486*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617246*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617306*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617366*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617386*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617426*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617486*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617526*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617566*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004617706*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004670543*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004670563*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004670603*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004670703*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004670743*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004670763*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004686363*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004686443*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004686503*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004686543*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004687563*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004687583*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004687743*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004688423*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004688623*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004690006*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004690046*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004690066*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004690086*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004690106*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004690126*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004690166*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004695303*' -> '/docs/' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004702823*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004706666*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004706906*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004706966*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004707726*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004707786*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004707946*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004734463*' -> '/docs/quickstart/track-events#http-api' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115004958823*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115005593506*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115005641266*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115005641306*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115005664806*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115005665706*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/115005717546*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/14377628688788*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/14383975110292*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/15235786397716*' -> '/docs/data-structure/advanced/ad-spend#gathering-data-from-ad-networks' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/15271983688212*' -> '/docs/features/attribution#overview' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360000857366*' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360000865566*' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360000894623*' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360000953003*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001131323*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001321243*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001337043*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001337103*' -> '/docs/tracking/how-tos/utm' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001354886*' -> '/docs/best-practices/developer-environments#separate-development-data' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001355146*' -> '/docs/best-practices/server-side-best-practices#tracking-browser-device-and-os' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001355206*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001355466*' -> '/docs/best-practices/server-side-best-practices#identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001360643*' -> '/docs/features/advanced#top-segments-logic' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360001361023*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360020461952*' -> '/docs/admin/organizations-projects' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360021085271*' -> '/docs/admin/organizations-projects' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360021916032*' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360024910951*' -> '/docs/admin/organizations-projects' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360025333632*' -> '/docs/data-structure/advanced/group-analytics' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360027754631*' -> '/docs/features/advanced#query-result-caching' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360028380611*' -> '/docs/how-it-works/concepts' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360035109991*' -> '/docs/best-practices/create-a-tracking-plan' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360039133851*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360041039771*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360047094171*' -> '/docs/features/advanced#list-property-support' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360048486671*' -> '/docs/debugging/overview#inactive-events-and-properties' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/360061218812*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/4402828400020*' -> '/docs/features/advanced#limits-and-ordering' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/4402837164948*' -> '/docs/debugging/overview' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/4409841288724*' -> '/docs/boards/advanced#faq' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/4409850288276*' -> '/docs/boards/advanced' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/7715039510548*' -> '/docs/features/advanced#query-builder-features' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/articles/9648680824852*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/categories/115000963103*' -> '/docs/admin/organizations-projects' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/categories/115001031023*' -> '/docs/how-it-works/concepts#overview' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/categories/115001197226*' -> '/docs' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/sections/115001299203*' -> '/docs/tracking-methods/id-management/identifying-users' (destination is redirected again)", + "docs.json: redirect chain '/hc/en-us/sections/115001308546*' -> '/docs/quickstart/track-events?sdk=httpapi' (destination is redirected again)" + ] +} From ec6590c6e76d7847891408c149837796fc611c71 Mon Sep 17 00:00:00 2001 From: Tyler Goerzen Date: Fri, 25 Sep 2026 12:52:56 -0700 Subject: [PATCH 6/9] Stop tracking Python bytecode Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + .../__pycache__/check_openapi.cpython-313.pyc | Bin 12857 -> 0 bytes scripts/__pycache__/ci_baseline.cpython-313.pyc | Bin 4391 -> 0 bytes 3 files changed, 1 insertion(+) delete mode 100644 scripts/__pycache__/check_openapi.cpython-313.pyc delete mode 100644 scripts/__pycache__/ci_baseline.cpython-313.pyc diff --git a/.gitignore b/.gitignore index 94f1119ef..68a281d60 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store .vscode +__pycache__/ diff --git a/scripts/__pycache__/check_openapi.cpython-313.pyc b/scripts/__pycache__/check_openapi.cpython-313.pyc deleted file mode 100644 index 3bf9de76dee76af1ec31172c705adc01230c2c4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12857 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 diff --git a/scripts/__pycache__/ci_baseline.cpython-313.pyc b/scripts/__pycache__/ci_baseline.cpython-313.pyc deleted file mode 100644 index 10c7c545cbae1f8fa221e85b45d6fef2f0eba1f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4391 zcma)9Uu+vi8lT|Hav zZfoD2l($QJNHyY6T~UeL!9$|sMDFDT0_h4*JUXPJy(Ky!-3f`eaD_w1(|xnf+JP2y zlXzzR?Kj{5`R3<4p^%7R{I>DebJOtjPxi$b_8a(e2XhF$jwD2p#7X?K9Odklr@Xxi zRG8)yK4)8dO$6G{&eKnDhMf29^(Q#PZ4T#Y+P4=hWW!k{VK3jLP`~7w7N-N#!Rde- zNW*+{mfZKFb1oW^Ja@m9ToOO*lDsxYhvb_MNPhT;jtnUP-(1t-VNnX&Je^rS8j`;S zm7+};BRD5(Dm-LyMyJ@!5v=NpfuBB(v$9Ey0Z|;s8o6M9!$wXo7gVh2 zCY~wi%1fA>C$xgg8YKnbR`IC>)-|lk6<7$KA&Oi!2-Y*=`MeGsd0jI=hiqcGK#8nY zFu9mFOtz;e=d}SmAuBnoNa1dTqt0}grLe)#TRn<0{9Ox+rX|Ah^#@d$|Y=&l1w3Tg$e}v zkdqP+OY>wsQksqnx|Ssr>xxpQ7_x^cDe08Od!}rHF^~kzoM{?b&KJ~v5v-K4tqIgX zTKZUe$wj$X0)wVb9*GUdN3lwp!LsuL?``_<5Xe{alqeL_=GRyO{f7A zn|fKvvDj~n%>X_{%aC`V6>Ux&i+ zzm&z;q=f7jF^p0L^tW?t?@R-~W)@}BBy^xuxm&6D6+3E^)lK&-3Clx?S*DuOJde%U z1B(EkJ2zo^9i^I7m=o*Awnbnxc4B`tQcQw9oFm`2{#^wLd(guujjCw6zH!N34HglH8K469wIy5d#oH#x?kv=s(bbwaB|8ywm< zJUmK9hhsw{k1Mg^k$p3;E8S?nZNSu&CC{fu|awS;>(L?+8FoJ!+m3-Pc zm5_GJoo2?+E-+&=e2gbxnnkOD(3O!(BUcVyI(R*HE6}$b=(`>6n)9#vL$4I)v!D70 zmbR~kBUdVyEA{T*{pLFXiG#QN1MvU7!ET55d&f5mA8i)LcX=(JqfTMII)mLScB#A# z4^S$tcd1sXu(V3Tr~rxOQZ8p>qE#Xe1)=Te3a+h(>V14(%vI3+m7?jHU3vzSgE8%7OzyNHLtlD;k>F#Dzy#S~QLjr2BcJ-?OLWy2yyCy? zul3Epyma_e-{@_hc*S?gSL=DD<2yGJhj01gpZVj>Jkgi@`8y%#8KXs#sh z94z@h;P5u-@YKpwl6UgstY@Cu1#Bm~ldgLqjwF)!_Tf#nkVeww?8hk8LKR7_14%-P zmAO5BPA_n};v1x7-EHo>eM>|fne8=+23w8Au|{(z`3QR6WAKBJ^9VWy(bY3a_W`JU z*4&};-Bk4C;-{lyqc#Sr{o43+L4OEo|b%H+q*W$23o8B*)GkW^tCdQhCKW* zQl1U_S||+S;=LDL^6ftZnFk*v|4B{~fiIBiaiWpvb)MR5g0-3-ji0YA$<2z&@f9=# z<1M@3#Cu!5K*{~cm(T{ATQL9$9Z5gT;kd_<%H%*^phh6_t$0YCNlyogx2hAP=OE)O zLs?(a-_i>$LF%$4cE<&XLPQhWGVJQ>NeUe($)d9CE39ceLI2o#(emn2&OA03E>EWv;)!6nn|dHb5RJnW(%D$t2( z2_?N`_j}6?5CEPPXYv<8o4;+N$0%ojXy4R;4xQ1wf&kYsm`sO3<|FVi<{$;LXf23*4_sH? z%w5Z^cpq7f{NlBdIp045U8`Fkep_1TeJi!Hb!hI`<&$3od#;?gbfUg#-gPs$bJZ7` z%e@kYrM9W&ZQQr}o%lP((&K;n)yks>8oS3DV=pv1UcBi`-41vDD;WO0`!(;?z^j4! z#Qb9`osX;pcg;@x{h?heU1NXjt?yVO%a0zoy5kSMv(GfT##V#fwY}Hl_498WSdKJV zS34uMBd-OnEAzYG(3d-7v&X;m20``haL-(}mbfm|C*BZizn(w%VdTB!2lwB2`GakL z+3^W!3{9;eF3I(;_7A*!bn)n|{;}o$u@9xcoq7Mv4f*}lO8@c3qt7&+DXa-xNaMZ{ z1i$zn5&62;y{c8-j?BOK*0u&7Y78B|@xqN~8z)JlBXiT2{XDW|?&;NFSM5;EsFB+4 z&w^XN7EsUDFT$N)9y~fnYth;RjR!}UL;F8T%pO^F`R9^zM{A??Up5}>UwD4u*wUef zIC9gq?{*;g-+%8!-A@?oO8$7K|47XB@nasCqg|Faod$%SPTMe}Su{I=t64d&b9!F0 zyzs+>m5=3>XAB#q`qJqP{K$e`450ux$tzHL&@2F)!jDF~P@82bKvCMou!!yeD{5AW zZ>*@RKdBFQ(r+vl2V9vgK&w_}P_Cc{a4>adBJb%-YlSp1N{vb8T`tyyF`L zw6zX_yRUY*9;vJI@oV||*5#hQH3YPUsc+b_7Tm#wYCF~tEOo_R7KWFE#Zmk5Mw9Z1 M@-2H$p9E|F2W(mt+W-In From 92e9f36a718f88c6918136c7f5d2a32893d2627c Mon Sep 17 00:00:00 2001 From: Tyler Goerzen Date: Fri, 25 Sep 2026 12:53:03 -0700 Subject: [PATCH 7/9] Note the baseline in the workflow Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml index 05a6a5767..ea1ec5051 100644 --- a/.github/workflows/docs-ci.yml +++ b/.github/workflows/docs-ci.yml @@ -27,6 +27,8 @@ jobs: - name: Install OpenAPI validator run: pip install --quiet pyyaml openapi-spec-validator # continue-on-error is deliberately absent: each check is a hard gate. + # Violations that already existed on main are listed in + # scripts/docs-ci-baseline.json and don't fail; only new ones do. # Steps run in order and the job reports the first failure. - name: Check frontmatter run: python scripts/check_frontmatter.py From 09a6745b1799e01d70f750c6617769953972c1e3 Mon Sep 17 00:00:00 2001 From: Tyler Goerzen Date: Fri, 25 Sep 2026 13:00:56 -0700 Subject: [PATCH 8/9] Fail wildcard redirect destinations that match no page Co-Authored-By: Claude Opus 5.5 --- scripts/check_redirects.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/check_redirects.py b/scripts/check_redirects.py index d7a9f4e23..78d45334d 100644 --- a/scripts/check_redirects.py +++ b/scripts/check_redirects.py @@ -9,9 +9,10 @@ another redirect (exact or wildcard). Each hop costs crawlers and answer engines a round trip, and chains decay into loops. Point the redirect at the final page instead. - 4. Every destination resolves to an existing page. - Wildcard destinations (containing '*') are only checked for chains, - because their validity is structural rather than path-based. + 4. Every destination resolves to an existing page. A wildcard destination + (e.g. '/guides/mcp/*') can't be checked child by child, since the + incoming URLs are unknown, so it must at least match one existing page: + removing or renaming the whole target section fails the check. Known violations on main are listed in docs-ci-baseline.json (see ci_baseline.py); only new violations fail the check. @@ -123,6 +124,11 @@ def is_redirected(path: str) -> bool: # A wildcard destination chains only if it is itself a source. if dest_path in sources: all_errors.append(f"docs.json: redirect chain '{src}' -> '{dest}' (destination is redirected again)") + elif not any(fnmatch.fnmatch(page, dest_path) for page in file_paths): + all_errors.append( + f"docs.json: wildcard redirect destination '{dest}' matches no existing page " + f"(source: '{src}')" + ) continue if is_redirected(dest_path): From 47c446a11cdf31fdfea39bbd1a5da7e034d27616 Mon Sep 17 00:00:00 2001 From: no value <270450525+hywel-mixpanel@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:13:22 +0000 Subject: [PATCH 9/9] Fix docs CI violations on pages added since the gates were written Add missing descriptions to agent-intelligence and wingify, and tag three bare code fences in audit-log-streaming as text. Co-Authored-By: Claude Fable 5.1 --- docs/access-security/audit-log-streaming.mdx | 6 +++--- docs/agent-intelligence.mdx | 1 + docs/cohort-sync/integrations/wingify.mdx | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/access-security/audit-log-streaming.mdx b/docs/access-security/audit-log-streaming.mdx index 32033b9de..6a842d83e 100644 --- a/docs/access-security/audit-log-streaming.mdx +++ b/docs/access-security/audit-log-streaming.mdx @@ -59,7 +59,7 @@ Each line contains one complete audit log entry. See the [Audit Log Reference](/ Files use this path: -``` +```text [/]YYYY/MM/DD/HH/MM/.ndjson.gz ``` @@ -73,7 +73,7 @@ Mixpanel delivers each batch of audit log entries as a gzipped NDJSON object int The external ID is unique to your Mixpanel organization and has this format: -``` +```text mixpanel-audit-log-streaming- ``` @@ -177,7 +177,7 @@ Mixpanel delivers each batch of audit log entries as a gzipped NDJSON object int Grant this service account: -``` +```text audit-log-streaming@mixpanel-prod-1.iam.gserviceaccount.com ``` diff --git a/docs/agent-intelligence.mdx b/docs/agent-intelligence.mdx index d60272898..7e6d26ab3 100644 --- a/docs/agent-intelligence.mdx +++ b/docs/agent-intelligence.mdx @@ -1,5 +1,6 @@ --- title: "Agent Intelligence" +description: "Send your AI agent traces to Mixpanel over OpenTelemetry and analyze usage, cost, errors, and conversations alongside your product data" --- Agent Intelligence brings your AI agent's traces into Mixpanel, next to the product data you already track. diff --git a/docs/cohort-sync/integrations/wingify.mdx b/docs/cohort-sync/integrations/wingify.mdx index 43c1e860e..0caba6e5f 100644 --- a/docs/cohort-sync/integrations/wingify.mdx +++ b/docs/cohort-sync/integrations/wingify.mdx @@ -1,5 +1,6 @@ --- title: Wingify +description: "Export Mixpanel cohorts to Wingify (formerly VWO) as custom segments for your experiments" --- ## Overview