Skill Tracer
Skills › Marketing Skills › conversion-tracking › check_recipes.py

check_recipes.py

89 lines · skills/conversion-tracking/scripts/check_recipes.py at 88c79f6

1#!/usr/bin/env python3
2"""Check that every GTM detect recipe embeds its snippet verbatim.
3
4Part of the conversion-tracking skill in Marketing Skills.
5
6Each assets/gtm-recipes/detect/detect-<tool>.json carries one Custom HTML
7listener tag. Its "html" parameter must equal the matching
8assets/snippets/<tool>.js wrapped like this:
9
10 "<script>\\n" + <snippet body> + "\\n</script>"
12where <snippet body> is the .js file with its leading /*! ... */ license
13header removed and trailing newlines stripped. The header is dropped because
14the GTM tag name already identifies the recipe.
16Usage:
17 python3 scripts/check_recipes.py # exit 1 and list any drift
18 python3 scripts/check_recipes.py --write # regenerate the embedded HTML
20Uses only the Python standard library (3.8+).
21"""
22import json
23import re
24import sys
25from pathlib import Path
27ROOT = Path(__file__).resolve().parent.parent
28DETECT_DIR = ROOT / "assets" / "gtm-recipes" / "detect"
29SNIPPET_DIR = ROOT / "assets" / "snippets"
30HEADER_RE = re.compile(r"\A/\*!.*?\*/\n", re.S)
33def expected_html(snippet_path):
34 body = HEADER_RE.sub("", snippet_path.read_text(encoding="utf-8"), count=1)
35 return "<script>\n" + body.rstrip("\n") + "\n</script>"
38def html_param(recipe):
39 tags = [t for t in recipe["containerVersion"]["tag"] if t.get("type") == "html"]
40 if len(tags) != 1:
41 raise ValueError(f"expected exactly 1 Custom HTML tag, found {len(tags)}")
42 params = [p for p in tags[0]["parameter"] if p.get("key") == "html"]
43 if len(params) != 1:
44 raise ValueError("Custom HTML tag has no html parameter")
45 return params[0]
48def main():
49 write = "--write" in sys.argv[1:]
50 recipes = sorted(DETECT_DIR.glob("detect-*.json"))
51 problems = []
52 for recipe_path in recipes:
53 tool = recipe_path.stem[len("detect-"):]
54 snippet_path = SNIPPET_DIR / f"{tool}.js"
55 if not snippet_path.exists():
56 problems.append(f"{recipe_path.name}: no snippet {snippet_path.name}")
57 continue
58 try:
59 recipe = json.loads(recipe_path.read_text(encoding="utf-8"))
60 param = html_param(recipe)
61 except (ValueError, KeyError) as e:
62 problems.append(f"{recipe_path.name}: {e}")
63 continue
64 want = expected_html(snippet_path)
65 if param["value"] == want:
66 continue
67 if write:
68 param["value"] = want
69 recipe_path.write_text(json.dumps(recipe, indent=2, ensure_ascii=False) + "\n",
70 encoding="utf-8")
71 print(f"rewrote {recipe_path.name}")
72 else:
73 problems.append(f"{recipe_path.name}: embedded HTML differs from {snippet_path.name}")
75 orphans = {p.stem for p in SNIPPET_DIR.glob("*.js")} - {
76 p.stem[len("detect-"):] for p in recipes}
77 for name in sorted(orphans):
78 problems.append(f"{name}.js: no detect recipe detect-{name}.json")
80 if problems:
81 print("\n".join(problems), file=sys.stderr)
82 if not write:
83 print("run with --write to regenerate the embedded HTML", file=sys.stderr)
84 sys.exit(1)
85 print(f"ok: {len(recipes)} detect recipes match their snippets")
88if __name__ == "__main__":
89 main()