diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f47c1f8..1e7561c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -31,7 +31,11 @@ "mcp__plugin_figma_figma__whoami", "mcp__plugin_figma_figma__get_metadata", "mcp__plugin_figma_figma__get_screenshot", - "WebSearch" + "WebSearch", + "Bash(git -C ~/Workspace/seo-workspace rm:*)", + "Bash(git -C ~/Workspace/seo-workspace check-ignore -q \".claude/commands\")", + "Bash(git pull:*)", + "Bash(bash:*)" ] } } diff --git a/custom-skills/61-gtm-manager/README.md b/custom-skills/61-gtm-manager/README.md deleted file mode 100644 index 2aea0e4..0000000 --- a/custom-skills/61-gtm-manager/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# OurDigital GTM Manager - -Comprehensive Google Tag Manager management toolkit powered by Playwright. - -> **Note**: For lightweight audit-only functionality, see [13-ourdigital-gtm-audit](../13-ourdigital-gtm-audit/). - -## Features - -- **Audit Mode**: Validate GTM installations, dataLayer events, forms, and checkout flows -- **Inject Mode**: Generate custom HTML tags for dataLayer pushes -- **Container Detection**: Verify GTM installation, position, and duplicates -- **DataLayer Validation**: Event structure, types, sequence checking -- **Form Tracking**: Form discovery, field analysis, event verification -- **E-commerce Checkout**: Full funnel flow simulation and validation -- **Multi-Platform**: GA4, Meta Pixel, LinkedIn, Google Ads, Kakao, Naver -- **Notion Integration**: Export audit results to Notion database - -## Installation - -```bash -cd ourdigital-gtm-manager - -# Install dependencies -pip install -r requirements.txt - -# Install Playwright browsers -playwright install chromium -``` - -## Audit Mode - -Analyze existing GTM implementations: - -```bash -# Full audit -python gtm_manager.py audit --url "https://yoursite.com" --journey full - -# Specific container validation -python gtm_manager.py audit --url "https://yoursite.com" --container "GTM-XXXXXX" - -# Form tracking only -python gtm_manager.py audit --url "https://yoursite.com/contact" --journey form - -# Audit and generate missing tags -python gtm_manager.py audit --url "https://yoursite.com" --generate-tags - -# Export to Notion -python gtm_manager.py audit --url "https://yoursite.com" --notion -``` - -### Audit Options - -| Option | Description | Default | -|--------|-------------|---------| -| `--url` | Target URL (required) | - | -| `--container` | Expected GTM container ID | None | -| `--journey` | pageview, scroll, form, checkout, datalayer, full | full | -| `--output` | Output file path | gtm_audit_report.json | -| `--generate-tags` | Generate missing dataLayer tags | False | -| `--notion` | Export to Notion database | False | - -## Inject Mode - -Generate GTM custom HTML tags when you can't modify source code directly: - -```bash -# List available event types -python gtm_manager.py inject --list-events - -# Generate all ecommerce tags -python gtm_manager.py inject --preset ecommerce --output ./tags - -# Generate specific events -python gtm_manager.py inject --event purchase --event add_to_cart - -# Generate from audit report -python gtm_manager.py inject --from-audit gtm_audit_report.json - -# Generate with DOM scraping code -python gtm_manager.py inject --event view_item --scrape -``` - -### Inject Options - -| Option | Description | Default | -|--------|-------------|---------| -| `--event` | Event type(s) to generate | - | -| `--preset` | ecommerce, engagement, all | - | -| `--from-audit` | Generate from audit report | - | -| `--output` | Output directory | ./gtm_tags | -| `--format` | html or json | html | -| `--currency` | Currency code | KRW | -| `--scrape` | Generate DOM scraping code | False | -| `--list-events` | Show available events | - | - -### Supported Events - -**Ecommerce**: view_item, add_to_cart, remove_from_cart, view_cart, begin_checkout, add_shipping_info, add_payment_info, purchase - -**Forms**: form_submit, form_start, generate_lead - -**Engagement**: scroll, file_download, search, outbound_click, share - -**Video**: video_start, video_progress, video_complete - -**User**: login, sign_up, page_view - -## Notion Integration - -```bash -# Set Notion API token -export NOTION_TOKEN="secret_xxxxx" - -# Export audit to Notion -python gtm_manager.py audit --url "https://yoursite.com" --notion --notion-detailed -``` - -## Using with Claude Code - -This project includes a `CLAUDE.md` file optimized for Claude Code. - -```bash -claude - -# Then ask Claude: -> Run a GTM audit on https://example.com -> Generate ecommerce dataLayer tags for my site -> Check the checkout flow and create missing tags -``` - -## Documentation - -See `docs/` folder for: -- GA4 event specifications -- E-commerce dataLayer schemas -- Form tracking patterns -- Checkout flow sequences -- Common issues and fixes - -## License - -MIT diff --git a/custom-skills/61-gtm-manager/code/CLAUDE.md b/custom-skills/61-gtm-manager/code/CLAUDE.md deleted file mode 100644 index 114ce72..0000000 --- a/custom-skills/61-gtm-manager/code/CLAUDE.md +++ /dev/null @@ -1,219 +0,0 @@ -# OurDigital GTM Manager - -Comprehensive Google Tag Manager management toolkit - audit, analyze, and generate dataLayer implementations. - -> **Note**: For lightweight audit-only functionality, see [13-ourdigital-gtm-audit](../13-ourdigital-gtm-audit/). - -## Project Overview - -This tool provides two main capabilities: -1. **Audit**: Validate GTM installations, analyze dataLayer events, test form/checkout tracking -2. **Inject**: Generate custom HTML tags for dataLayer pushes when direct code access is unavailable - -## Quick Commands - -```bash -# Install dependencies -pip install playwright -playwright install chromium - -# AUDIT MODE -# Run full audit -python gtm_manager.py audit --url "https://example.com" --journey full - -# Audit with Notion export -python gtm_manager.py audit --url "https://example.com" --notion - -# Audit and generate missing tags -python gtm_manager.py audit --url "https://example.com" --generate-tags - -# INJECT MODE -# List available event types -python gtm_manager.py inject --list-events - -# Generate all ecommerce tags -python gtm_manager.py inject --preset ecommerce --output ./tags - -# Generate specific event tags -python gtm_manager.py inject --event purchase --event add_to_cart - -# Generate from audit report -python gtm_manager.py inject --from-audit gtm_audit_report.json - -# Generate with DOM scraping -python gtm_manager.py inject --event view_item --scrape -``` - -## Audit Mode - -### Journey Types - -| Journey | Description | -|---------|-------------| -| `pageview` | Basic page load + scroll simulation | -| `scroll` | Scroll depth trigger testing (25%, 50%, 75%, 90%) | -| `form` | Form discovery, field analysis, interaction simulation | -| `checkout` | E-commerce flow: cart → checkout → shipping → payment → purchase | -| `datalayer` | Deep dataLayer validation and event sequence analysis | -| `full` | All of the above combined | - -### Audit Output - -Generates `gtm_audit_report.json` with: -- Container status (installed, position, duplicates) -- DataLayer analysis (events, validation issues, sequence errors) -- Form analysis (forms found, tracking readiness, missing events) -- Checkout analysis (elements detected, flow issues) -- Network requests (GA4, Meta, LinkedIn, etc.) -- Recommendations and checklist - -## Inject Mode - -Generate GTM custom HTML tags for dataLayer injection when you can't modify source code directly. - -### Event Categories - -**Ecommerce:** -- `view_item`, `add_to_cart`, `remove_from_cart`, `view_cart` -- `begin_checkout`, `add_shipping_info`, `add_payment_info`, `purchase` - -**Forms & Leads:** -- `form_submit`, `form_start`, `generate_lead` - -**Engagement:** -- `scroll`, `file_download`, `search`, `outbound_click`, `share` - -**Video:** -- `video_start`, `video_progress`, `video_complete` - -**User:** -- `login`, `sign_up`, `page_view` - -### Presets - -| Preset | Events Included | -|--------|-----------------| -| `ecommerce` | All 8 ecommerce events | -| `engagement` | Forms, scroll, downloads, video, search, sharing | -| `all` | Everything including page_view, login, sign_up | - -### Generated Tag Features - -- GA4-compliant dataLayer structure -- Ecommerce object clearing before pushes -- DOM scraping option for dynamic values -- Trigger recommendations and selectors -- Korean payment method support - -## Notion Integration - -Export audit results directly to Notion database for tracking and collaboration. - -```bash -# Export to default Notion database (OurDigital GTM Audit Log) -python gtm_manager.py audit --url "https://example.com" --notion - -# Export with detailed content -python gtm_manager.py audit --url "https://example.com" --notion --notion-detailed -``` - -### Environment Variables - -```bash -export NOTION_TOKEN="secret_xxxxx" -``` - -### Default Database - -Default Notion database: [OurDigital GTM Audit Log](https://www.notion.so/2cf581e58a1e8163997fccb387156a20) - -## Key Files - -- `gtm_manager.py` - Main script with audit and inject functionality -- `docs/ga4_events.md` - GA4 event specifications -- `docs/ecommerce_schema.md` - E-commerce dataLayer structures -- `docs/form_tracking.md` - Form event patterns -- `docs/checkout_flow.md` - Checkout funnel sequence -- `docs/datalayer_validation.md` - Validation rules -- `docs/common_issues.md` - Frequent problems and fixes - -## Coding Guidelines - -When modifying this tool: - -1. **Tag Destinations**: Add new platforms to `TAG_DESTINATIONS` dict -2. **Event Validation**: Add requirements to `GA4_EVENT_REQUIREMENTS` dict -3. **Event Templates**: Add new events to `DataLayerInjector.EVENT_TEMPLATES` -4. **Form Selectors**: Extend `FormAnalyzer.discover_forms()` for custom forms -5. **Checkout Elements**: Add selectors to `CheckoutFlowAnalyzer.detect_checkout_elements()` - -## Korean Market Considerations - -- Support Korean payment methods (카카오페이, 네이버페이, 토스) -- Handle KRW currency (no decimals) -- Include Kakao Pixel and Naver Analytics patterns -- Korean button text patterns (장바구니, 결제하기, 주문하기) - -## Common Tasks - -### Add support for new tag platform -```python -# In TAG_DESTINATIONS dict -"NewPlatform": [ - r"tracking\.newplatform\.com", - r"pixel\.newplatform\.com", -], -``` - -### Add new event type for injection -```python -# In DataLayerInjector.EVENT_TEMPLATES -"custom_event": { - "description": "Track custom action", - "params": { - "custom_param": "/* value */", - }, - "trigger": "Custom Trigger", -}, -``` - -### Extend checkout flow for specific platform -```python -# In CheckoutFlowAnalyzer.detect_checkout_elements() -# Add platform-specific selectors -``` - -## Notion Output (Required) - -**IMPORTANT**: All GTM audit reports MUST be saved to the OurDigital GTM Audit Archive database. - -### Database Configuration - -| Field | Value | -|-------|-------| -| Database ID | `2cf581e5-8a1e-8163-997f-ccb387156a20` | -| URL | https://www.notion.so/dintelligence/OurDigital-GTM-Audit-Archive-2cf581e58a1e80c8b358f1625356e931 | - -### Required Properties - -| Property | Type | Description | -|----------|------|-------------| -| Site | Title | Site name or domain | -| URL | URL | Audited page URL | -| Audit Date | Date | Audit date (YYYY-MM-DD) | -| Audit ID | Rich Text | Format: GTM-YYYYMMDD-NNN | -| Audit Status | Select | Pass, Warning, Fail | -| GTM Status | Select | Installed, Not Found, Multiple Containers | -| Journey Type | Select | full, pageview, scroll, form, checkout, datalayer | -| Tags Fired | Multi-select | GA4, Google Ads, Meta Pixel, LinkedIn, TikTok, Kakao, Naver | -| Container IDs | Rich Text | GTM container ID(s) | -| Issues Count | Number | Total issues found | -| Critical Issues | Number | Critical issues count | -| Summary | Rich Text | Brief audit summary | - -### Language Guidelines - -- Report content in Korean (한국어) -- Keep technical English terms as-is (e.g., GTM, dataLayer, GA4) -- URLs and code remain unchanged - diff --git a/custom-skills/61-gtm-manager/code/docs/checkout_flow.md b/custom-skills/61-gtm-manager/code/docs/checkout_flow.md deleted file mode 100644 index 3204807..0000000 --- a/custom-skills/61-gtm-manager/code/docs/checkout_flow.md +++ /dev/null @@ -1,237 +0,0 @@ -# E-commerce Checkout Flow Reference - -## Complete Checkout Event Sequence - -``` -view_cart → begin_checkout → add_shipping_info → add_payment_info → purchase -``` - -Each step must fire in order with consistent item data. - -## Event Details - -### 1. view_cart -When user views cart page. - -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "view_cart", - ecommerce: { - currency: "KRW", - value: 125000, - items: [{ - item_id: "SKU_001", - item_name: "Blue T-Shirt", - price: 45000, - quantity: 2, - item_brand: "Brand", - item_category: "Apparel" - }, { - item_id: "SKU_002", - item_name: "Black Jeans", - price: 35000, - quantity: 1 - }] - } -}); -``` - -### 2. begin_checkout -When user initiates checkout process. - -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "begin_checkout", - ecommerce: { - currency: "KRW", - value: 125000, - coupon: "SUMMER10", - items: [/* same items as view_cart */] - } -}); -``` - -### 3. add_shipping_info -When user completes shipping step. - -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "add_shipping_info", - ecommerce: { - currency: "KRW", - value: 125000, - coupon: "SUMMER10", - shipping_tier: "Express", // Required - items: [/* same items */] - } -}); -``` - -**shipping_tier values:** -- "Standard" / "일반배송" -- "Express" / "익일배송" -- "Same Day" / "당일배송" -- "Free" / "무료배송" -- "Store Pickup" / "매장픽업" - -### 4. add_payment_info -When user enters payment details. - -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "add_payment_info", - ecommerce: { - currency: "KRW", - value: 125000, - coupon: "SUMMER10", - payment_type: "Credit Card", // Required - items: [/* same items */] - } -}); -``` - -**payment_type values:** -- "Credit Card" / "신용카드" -- "Debit Card" / "체크카드" -- "Bank Transfer" / "계좌이체" -- "Virtual Account" / "가상계좌" -- "Mobile Payment" / "휴대폰결제" -- "Kakao Pay" / "카카오페이" -- "Naver Pay" / "네이버페이" -- "Toss" / "토스" -- "PayPal" - -### 5. purchase -When transaction completes successfully. - -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "purchase", - ecommerce: { - transaction_id: "T_20250115_001234", // Required, unique - value: 130500, // Required (total) - tax: 11863, - shipping: 5000, - currency: "KRW", // Required - coupon: "SUMMER10", - items: [{ - item_id: "SKU_001", - item_name: "Blue T-Shirt", - affiliation: "Online Store", - coupon: "SUMMER10", - discount: 4500, - price: 45000, - quantity: 2 - }] - } -}); -``` - -## Funnel Drop-off Analysis - -### Tracking Drop-offs -Monitor completion rate at each step: - -| Step | Event | Drop-off Indicator | -|------|-------|-------------------| -| Cart | view_cart | User leaves cart page | -| Checkout Start | begin_checkout | User doesn't proceed | -| Shipping | add_shipping_info | Address form abandoned | -| Payment | add_payment_info | Payment not completed | -| Complete | purchase | Transaction failed | - -### Implementing Drop-off Tracking - -```javascript -// Track checkout step viewed but not completed -let checkoutStep = 0; - -function trackCheckoutProgress(step) { - if (step > checkoutStep) { - checkoutStep = step; - } -} - -window.addEventListener('beforeunload', () => { - if (checkoutStep > 0 && checkoutStep < 5) { - dataLayer.push({ - event: 'checkout_abandon', - last_step: checkoutStep, - step_name: ['cart', 'checkout', 'shipping', 'payment', 'complete'][checkoutStep - 1] - }); - } -}); -``` - -## Value Consistency Check - -Ensure `value` matches across events: - -``` -view_cart.value = sum(items.price * items.quantity) -begin_checkout.value = view_cart.value -add_shipping_info.value = begin_checkout.value -add_payment_info.value = add_shipping_info.value -purchase.value = add_payment_info.value + shipping + tax - discount -``` - -## Common Issues - -### Duplicate Purchase Events -**Problem**: Same order tracked multiple times -**Solution**: -```javascript -// Check if already tracked -const txId = "T_12345"; -if (!sessionStorage.getItem('purchase_' + txId)) { - dataLayer.push({ event: 'purchase', ... }); - sessionStorage.setItem('purchase_' + txId, 'true'); -} -``` - -### Missing Items in Later Steps -**Problem**: Items present in view_cart but missing in purchase -**Solution**: Store cart data in session and reuse - -### Inconsistent Currency -**Problem**: Some events use USD, others KRW -**Solution**: Standardize currency across all events - -### Wrong Value Calculation -**Problem**: purchase.value doesn't include tax/shipping -**Solution**: -``` -purchase.value = subtotal + tax + shipping - discount -``` - -## Korean E-commerce Platforms - -### Cafe24 -Custom dataLayer variable names - check documentation - -### Shopify Korea -Standard GA4 format with `Shopify.checkout` object - -### WooCommerce -Use official GA4 plugin or custom implementation - -### Naver SmartStore -Separate Naver Analytics implementation required - -## Checkout Flow Checklist - -- [ ] view_cart fires on cart page load -- [ ] begin_checkout fires on checkout button click -- [ ] add_shipping_info includes shipping_tier -- [ ] add_payment_info includes payment_type -- [ ] purchase has unique transaction_id -- [ ] All events have consistent items array -- [ ] Currency is consistent across all events -- [ ] Value calculations are accurate -- [ ] ecommerce object cleared before each push -- [ ] Purchase event fires only once per order diff --git a/custom-skills/61-gtm-manager/code/docs/datalayer_validation.md b/custom-skills/61-gtm-manager/code/docs/datalayer_validation.md deleted file mode 100644 index 65336dc..0000000 --- a/custom-skills/61-gtm-manager/code/docs/datalayer_validation.md +++ /dev/null @@ -1,287 +0,0 @@ -# DataLayer Validation Reference - -## DataLayer Structure Basics - -### Proper Initialization -```javascript -// Must appear BEFORE GTM script - - -``` - -### Push Syntax -```javascript -// Correct -dataLayer.push({ event: "page_view", page_title: "Home" }); - -// Wrong - direct assignment -dataLayer = [{ event: "page_view" }]; // ❌ Overwrites array -``` - -## Validation Rules - -### Event Names - -| Rule | Valid | Invalid | -|------|-------|---------| -| Alphanumeric + underscore | `add_to_cart` | `add-to-cart` | -| Max 40 characters | `purchase` | (too long names) | -| Case sensitive | `addToCart` ≠ `addtocart` | - | -| No spaces | `form_submit` | `form submit` | -| No special chars | `click_cta` | `click@cta` | - -### Parameter Names - -| Rule | Valid | Invalid | -|------|-------|---------| -| Max 40 characters | `item_category` | (too long) | -| Alphanumeric + underscore | `user_id` | `user-id` | -| Cannot start with `_` | `custom_param` | `_private` | -| Cannot start with number | `step_1` | `1_step` | - -### Data Types - -| Parameter | Expected Type | Example | -|-----------|---------------|---------| -| value | number | `29.99` not `"29.99"` | -| currency | string (ISO 4217) | `"USD"`, `"KRW"` | -| transaction_id | string | `"T_12345"` | -| quantity | integer | `2` not `2.0` | -| price | number | `45000` | -| items | array | `[{...}, {...}]` | - -### Type Validation Code - -```javascript -function validateDataLayerPush(data) { - const issues = []; - - // Check value is number - if (data.ecommerce?.value !== undefined) { - if (typeof data.ecommerce.value !== 'number') { - issues.push(`value should be number, got ${typeof data.ecommerce.value}`); - } - } - - // Check currency format - if (data.ecommerce?.currency) { - if (!/^[A-Z]{3}$/.test(data.ecommerce.currency)) { - issues.push(`currency should be 3-letter ISO code`); - } - } - - // Check items array - if (data.ecommerce?.items) { - if (!Array.isArray(data.ecommerce.items)) { - issues.push(`items should be array`); - } else { - data.ecommerce.items.forEach((item, i) => { - if (!item.item_id) issues.push(`items[${i}] missing item_id`); - if (!item.item_name) issues.push(`items[${i}] missing item_name`); - if (item.price && typeof item.price !== 'number') { - issues.push(`items[${i}].price should be number`); - } - if (item.quantity && !Number.isInteger(item.quantity)) { - issues.push(`items[${i}].quantity should be integer`); - } - }); - } - } - - return issues; -} -``` - -## E-commerce Object Clearing - -### Why Clear? -GA4 may merge previous ecommerce data with new events. - -### Correct Pattern -```javascript -// Clear first -dataLayer.push({ ecommerce: null }); - -// Then push new event -dataLayer.push({ - event: "view_item", - ecommerce: { ... } -}); -``` - -### Validation Check -```javascript -function checkEcommerceClear(dataLayerArray) { - let lastHadEcommerce = false; - const issues = []; - - dataLayerArray.forEach((item, i) => { - const hasEcommerce = 'ecommerce' in item; - const isNull = item.ecommerce === null; - - if (hasEcommerce && !isNull && lastHadEcommerce) { - issues.push({ - index: i, - message: 'Missing ecommerce:null before this push' - }); - } - - lastHadEcommerce = hasEcommerce && !isNull; - }); - - return issues; -} -``` - -## Event Sequence Validation - -### Expected Sequences - -**E-commerce Purchase Flow:** -``` -view_item_list? → view_item → add_to_cart → view_cart → -begin_checkout → add_shipping_info → add_payment_info → purchase -``` - -**Form Submission:** -``` -form_start → form_submit → generate_lead? -``` - -**User Authentication:** -``` -login | sign_up -``` - -### Sequence Validator - -```javascript -function validateSequence(events, expectedOrder) { - const eventNames = events - .filter(e => e.event) - .map(e => e.event); - - let lastIndex = -1; - const issues = []; - - eventNames.forEach(event => { - const index = expectedOrder.indexOf(event); - if (index !== -1) { - if (index < lastIndex) { - issues.push(`${event} fired out of expected order`); - } - lastIndex = index; - } - }); - - return issues; -} -``` - -## Duplicate Event Detection - -### Common Duplicates -- Multiple `page_view` on single page load -- `purchase` firing on page refresh -- Click events on bubbling elements - -### Detection Code - -```javascript -function findDuplicates(events) { - const seen = {}; - const duplicates = []; - - events.forEach((event, i) => { - if (!event.event) return; - - const key = JSON.stringify(event); - if (seen[key]) { - duplicates.push({ - event: event.event, - firstIndex: seen[key], - duplicateIndex: i - }); - } else { - seen[key] = i; - } - }); - - return duplicates; -} -``` - -## Real-time Monitoring Setup - -### Console Monitoring - -```javascript -// Paste in browser console to monitor pushes -(function() { - const original = dataLayer.push; - dataLayer.push = function() { - console.group('📊 dataLayer.push'); - console.log('Data:', arguments[0]); - console.log('Time:', new Date().toISOString()); - console.groupEnd(); - return original.apply(this, arguments); - }; - console.log('✅ DataLayer monitoring active'); -})(); -``` - -### Export DataLayer - -```javascript -// Copy full dataLayer to clipboard -copy(JSON.stringify(dataLayer, null, 2)); -``` - -## Validation Checklist - -### Structure -- [ ] dataLayer initialized before GTM -- [ ] Using push() not assignment -- [ ] Event names follow conventions -- [ ] Parameter names follow conventions - -### Data Types -- [ ] value is number -- [ ] currency is 3-letter code -- [ ] quantity is integer -- [ ] items is array -- [ ] Required fields present - -### E-commerce -- [ ] ecommerce:null before each push -- [ ] items array has item_id and item_name -- [ ] transaction_id is unique -- [ ] Consistent currency across events - -### Sequence -- [ ] Events fire in logical order -- [ ] No duplicate events -- [ ] Purchase fires only once - -## Debug Tools - -### GTM Preview Mode -- Real-time event inspection -- Variable value checking -- Tag firing verification - -### GA4 DebugView -- Live event stream -- Parameter validation -- User property tracking - -### Browser Console -```javascript -// View current dataLayer -console.table(dataLayer); - -// Filter by event -dataLayer.filter(d => d.event === 'purchase'); -``` diff --git a/custom-skills/61-gtm-manager/code/docs/ecommerce_schema.md b/custom-skills/61-gtm-manager/code/docs/ecommerce_schema.md deleted file mode 100644 index 7499ad1..0000000 --- a/custom-skills/61-gtm-manager/code/docs/ecommerce_schema.md +++ /dev/null @@ -1,216 +0,0 @@ -# E-commerce DataLayer Schema Reference - -## GA4 E-commerce Structure - -### Items Array Schema -Every e-commerce event requires an `items` array: - -```javascript -items: [{ - // Required - item_id: "SKU_12345", - item_name: "Blue T-Shirt", - - // Recommended - affiliation: "Store Name", - coupon: "SUMMER_SALE", - discount: 5.00, - index: 0, - item_brand: "Brand Name", - item_category: "Apparel", - item_category2: "Men", - item_category3: "Shirts", - item_category4: "T-Shirts", - item_category5: "Short Sleeve", - item_list_id: "related_products", - item_list_name: "Related Products", - item_variant: "Blue/Large", - location_id: "ChIJIQBpAG2ahYAR_6128GcTUEo", - price: 29.99, - quantity: 1 -}] -``` - -### Clear Previous E-commerce Data -Always clear before new e-commerce event: - -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "view_item", - ecommerce: { - // new data - } -}); -``` - -## Complete Purchase Flow - -### 1. Product List View -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "view_item_list", - ecommerce: { - item_list_id: "category_results", - item_list_name: "Category Results", - items: [ - { item_id: "SKU_001", item_name: "Product 1", index: 0, price: 29.99 }, - { item_id: "SKU_002", item_name: "Product 2", index: 1, price: 39.99 } - ] - } -}); -``` - -### 2. Product Click -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "select_item", - ecommerce: { - item_list_id: "category_results", - item_list_name: "Category Results", - items: [{ - item_id: "SKU_001", - item_name: "Product 1", - price: 29.99 - }] - } -}); -``` - -### 3. Product Detail View -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "view_item", - ecommerce: { - currency: "USD", - value: 29.99, - items: [{ - item_id: "SKU_001", - item_name: "Product 1", - item_brand: "Brand", - item_category: "Category", - price: 29.99, - quantity: 1 - }] - } -}); -``` - -### 4. Add to Cart -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "add_to_cart", - ecommerce: { - currency: "USD", - value: 29.99, - items: [{ - item_id: "SKU_001", - item_name: "Product 1", - price: 29.99, - quantity: 1 - }] - } -}); -``` - -### 5. View Cart -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "view_cart", - ecommerce: { - currency: "USD", - value: 59.98, - items: [ - { item_id: "SKU_001", item_name: "Product 1", price: 29.99, quantity: 1 }, - { item_id: "SKU_002", item_name: "Product 2", price: 29.99, quantity: 1 } - ] - } -}); -``` - -### 6. Begin Checkout -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "begin_checkout", - ecommerce: { - currency: "USD", - value: 59.98, - coupon: "DISCOUNT10", - items: [...] - } -}); -``` - -### 7. Add Shipping Info -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "add_shipping_info", - ecommerce: { - currency: "USD", - value: 59.98, - shipping_tier: "Standard", - items: [...] - } -}); -``` - -### 8. Add Payment Info -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "add_payment_info", - ecommerce: { - currency: "USD", - value: 59.98, - payment_type: "Credit Card", - items: [...] - } -}); -``` - -### 9. Purchase -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - event: "purchase", - ecommerce: { - transaction_id: "T_12345", - value: 65.97, - tax: 4.99, - shipping: 5.99, - currency: "USD", - coupon: "DISCOUNT10", - items: [{ - item_id: "SKU_001", - item_name: "Product 1", - affiliation: "Online Store", - coupon: "DISCOUNT10", - discount: 3.00, - item_brand: "Brand", - item_category: "Category", - price: 29.99, - quantity: 1 - }] - } -}); -``` - -## Korean E-commerce Considerations - -### Currency -```javascript -currency: "KRW", -value: 35000 // No decimals for KRW -``` - -### Common Korean Platform Integrations -- Cafe24: Uses custom dataLayer structure -- Shopify Korea: Standard GA4 format -- Naver SmartStore: Custom pixel implementation diff --git a/custom-skills/61-gtm-manager/code/docs/form_tracking.md b/custom-skills/61-gtm-manager/code/docs/form_tracking.md deleted file mode 100644 index 7d9d7d4..0000000 --- a/custom-skills/61-gtm-manager/code/docs/form_tracking.md +++ /dev/null @@ -1,157 +0,0 @@ -# Form Tracking Reference - -## GA4 Form Events - -### form_start -Fires on first interaction with form field. - -```javascript -dataLayer.push({ - event: "form_start", - form_id: "contact-form", - form_name: "Contact Us", - form_destination: "/submit-contact" -}); -``` - -### form_submit -Fires on successful form submission. - -```javascript -dataLayer.push({ - event: "form_submit", - form_id: "contact-form", - form_name: "Contact Us", - form_destination: "/submit-contact", - form_submit_text: "Send Message" -}); -``` - -### generate_lead -Fires when form generates a qualified lead. - -```javascript -dataLayer.push({ - event: "generate_lead", - currency: "USD", - value: 100, // Estimated lead value - form_id: "quote-request" -}); -``` - -## Form Field Events (Custom) - -### field_focus -```javascript -dataLayer.push({ - event: "field_focus", - form_id: "signup-form", - field_name: "email", - field_type: "email" -}); -``` - -### field_complete -```javascript -dataLayer.push({ - event: "field_complete", - form_id: "signup-form", - field_name: "email", - field_type: "email", - is_valid: true -}); -``` - -### field_error -```javascript -dataLayer.push({ - event: "field_error", - form_id: "signup-form", - field_name: "email", - error_message: "Invalid email format" -}); -``` - -## Form Abandonment Tracking - -### Detecting Abandonment -Track when user leaves form without submitting: - -```javascript -// Track form start -let formStarted = false; -document.querySelectorAll('form input, form select, form textarea') - .forEach(field => { - field.addEventListener('focus', function() { - if (!formStarted) { - formStarted = true; - dataLayer.push({ event: 'form_start', form_id: this.form.id }); - } - }); - }); - -// Track abandonment on page leave -window.addEventListener('beforeunload', function() { - if (formStarted && !formSubmitted) { - dataLayer.push({ - event: 'form_abandon', - form_id: 'contact-form', - last_field: lastFocusedField, - fields_completed: completedFieldCount - }); - } -}); -``` - -## GTM Trigger Configuration - -### Form Submission Trigger -| Setting | Value | -|---------|-------| -| Trigger Type | Form Submission | -| Wait for Tags | Check (if AJAX form) | -| Check Validation | Check | -| Form ID | equals `contact-form` | - -### Form Start Trigger (Custom Event) -| Setting | Value | -|---------|-------| -| Trigger Type | Custom Event | -| Event Name | form_start | -| Fire On | All Custom Events | - -## Common Form Types & Tracking - -### Contact Forms -Events: `form_start`, `form_submit`, `generate_lead` - -### Newsletter Signup -Events: `form_start`, `form_submit`, `sign_up` - -### Login Forms -Events: `form_start`, `login` - -### Search Forms -Events: `search` (with search_term parameter) - -### Multi-Step Forms -Track each step: -```javascript -dataLayer.push({ - event: "form_step", - form_id: "checkout-form", - step_number: 2, - step_name: "Shipping Address" -}); -``` - -## Validation Checklist - -- [ ] Form has id or name attribute -- [ ] All required fields have names -- [ ] Submit button identifiable -- [ ] form_start fires on first interaction -- [ ] form_submit fires only on success -- [ ] generate_lead has value parameter -- [ ] Error events track validation failures -- [ ] Abandonment tracking implemented (optional) diff --git a/custom-skills/61-gtm-manager/code/docs/ga4_events.md b/custom-skills/61-gtm-manager/code/docs/ga4_events.md deleted file mode 100644 index e7a964f..0000000 --- a/custom-skills/61-gtm-manager/code/docs/ga4_events.md +++ /dev/null @@ -1,177 +0,0 @@ -# GA4 Recommended Events Reference - -## Automatically Collected Events -Events GA4 collects without configuration: -- `first_visit` - First time user visits -- `session_start` - Session begins -- `page_view` - Page loads (enhanced measurement) -- `scroll` - 90% scroll depth -- `click` - Outbound link clicks -- `file_download` - File download clicks -- `video_start`, `video_progress`, `video_complete` - YouTube embeds - -## E-commerce Events (Required Parameters) - -### view_item_list -```javascript -{ - event: "view_item_list", - ecommerce: { - item_list_id: "related_products", - item_list_name: "Related Products", - items: [{ - item_id: "SKU_12345", // required - item_name: "Product Name", // required - price: 29.99, - quantity: 1 - }] - } -} -``` - -### view_item -```javascript -{ - event: "view_item", - ecommerce: { - currency: "USD", - value: 29.99, - items: [{ - item_id: "SKU_12345", // required - item_name: "Product Name", // required - price: 29.99, - quantity: 1 - }] - } -} -``` - -### add_to_cart -```javascript -{ - event: "add_to_cart", - ecommerce: { - currency: "USD", - value: 29.99, - items: [{ - item_id: "SKU_12345", // required - item_name: "Product Name", // required - price: 29.99, - quantity: 1 - }] - } -} -``` - -### begin_checkout -```javascript -{ - event: "begin_checkout", - ecommerce: { - currency: "USD", - value: 99.99, - coupon: "SUMMER_SALE", - items: [...] - } -} -``` - -### add_payment_info -```javascript -{ - event: "add_payment_info", - ecommerce: { - currency: "USD", - value: 99.99, - payment_type: "credit_card", - items: [...] - } -} -``` - -### purchase -```javascript -{ - event: "purchase", - ecommerce: { - transaction_id: "T12345", // required, must be unique - value: 99.99, // required - currency: "USD", // required - tax: 4.99, - shipping: 5.99, - coupon: "SUMMER_SALE", - items: [{ - item_id: "SKU_12345", // required - item_name: "Product Name",// required - price: 29.99, - quantity: 2 - }] - } -} -``` - -## Lead Generation Events - -### generate_lead -```javascript -{ - event: "generate_lead", - currency: "USD", - value: 100 // estimated lead value -} -``` - -### sign_up -```javascript -{ - event: "sign_up", - method: "email" // or "google", "facebook", etc. -} -``` - -### login -```javascript -{ - event: "login", - method: "email" -} -``` - -## Engagement Events - -### search -```javascript -{ - event: "search", - search_term: "blue shoes" -} -``` - -### share -```javascript -{ - event: "share", - method: "twitter", - content_type: "article", - item_id: "article_123" -} -``` - -## Parameter Validation Rules - -| Parameter | Type | Max Length | Notes | -|-----------|------|------------|-------| -| event name | string | 40 chars | No spaces, alphanumeric + underscore | -| item_id | string | 100 chars | Required for e-commerce | -| item_name | string | 100 chars | Required for e-commerce | -| currency | string | 3 chars | ISO 4217 format (USD, KRW, etc.) | -| transaction_id | string | 100 chars | Must be unique per transaction | -| value | number | - | Numeric, no currency symbols | - -## Common Validation Errors - -1. **Missing required params**: `item_id` or `item_name` not in items array -2. **Wrong data type**: `value` as string instead of number -3. **Duplicate transaction_id**: Same ID used for multiple purchases -4. **Empty items array**: E-commerce event with no items -5. **Invalid currency**: Currency code not in ISO 4217 format diff --git a/custom-skills/61-gtm-manager/code/docs/report_template.md b/custom-skills/61-gtm-manager/code/docs/report_template.md deleted file mode 100644 index 3e187ef..0000000 --- a/custom-skills/61-gtm-manager/code/docs/report_template.md +++ /dev/null @@ -1,115 +0,0 @@ -# GTM Audit Report Template - -## Executive Summary - -| Metric | Status | -|--------|--------| -| Container Installed | ✅ / ❌ | -| Container Valid | ✅ / ❌ | -| DataLayer Active | ✅ / ❌ | -| Tags Firing | X of Y | -| Critical Issues | X | -| Warnings | X | - -## Container Status - -**Container ID**: GTM-XXXXXX -**Installation Position**: head / body -**Multiple Containers**: Yes / No -**Noscript Fallback**: Present / Missing - -### Issues Found -- [ ] Issue description - -## DataLayer Analysis - -### Events Captured -| Event Name | Count | Has Issues | -|------------|-------|------------| -| page_view | 1 | No | -| add_to_cart | 0 | - | - -### DataLayer Quality -- [ ] Initialized before GTM -- [ ] Standard event naming -- [ ] Correct data types -- [ ] E-commerce structure valid - -## Tag Firing Report - -### Tags Fired ✅ -| Destination | Events | Parameters | -|-------------|--------|------------| -| GA4 | page_view | page_location, page_title | -| Meta Pixel | PageView | - | - -### Tags Not Detected ⚠️ -| Expected Tag | Reason | Priority | -|--------------|--------|----------| -| GA4 purchase | Event not triggered | High | - -## Network Request Analysis - -Total requests captured: X - -### By Destination -| Destination | Requests | Status | -|-------------|----------|--------| -| GA4 | X | ✅ | -| Meta | X | ✅ | -| Google Ads | 0 | ⚠️ | - -## Issues & Recommendations - -### Critical 🔴 -1. **Issue Title** - - Description - - Impact - - Recommended Fix - -### Warning 🟡 -1. **Issue Title** - - Description - - Recommended Fix - -### Info 🔵 -1. **Issue Title** - - Description - -## Action Items Checklist - -### Immediate (Critical) -- [ ] Action item 1 -- [ ] Action item 2 - -### Short-term (This Week) -- [ ] Action item 3 - -### Long-term (This Month) -- [ ] Action item 4 - -## Technical Details - -### Environment -- URL Audited: https://example.com -- Audit Timestamp: YYYY-MM-DD HH:MM:SS -- Browser: Chromium (headless) -- Viewport: 1920x1080 - -### Raw Data -Full JSON report available at: `gtm_audit_report.json` - ---- - -## Appendix: Tag Destination Reference - -| Tag Type | Network Pattern | -|----------|-----------------| -| GA4 | google-analytics.com/g/collect | -| UA (Legacy) | google-analytics.com/collect | -| Google Ads | googleads.g.doubleclick.net | -| Meta Pixel | facebook.com/tr | -| LinkedIn | px.ads.linkedin.com | -| TikTok | analytics.tiktok.com | -| Kakao | pixel.kakao.com | -| Naver | wcs.naver.com | diff --git a/custom-skills/61-gtm-manager/code/scripts/gtm_manager.py b/custom-skills/61-gtm-manager/code/scripts/gtm_manager.py deleted file mode 100644 index 4523c2d..0000000 --- a/custom-skills/61-gtm-manager/code/scripts/gtm_manager.py +++ /dev/null @@ -1,2073 +0,0 @@ -#!/usr/bin/env python3 -""" -GTM Manager - Comprehensive Google Tag Manager management toolkit. - -Features: -- Audit: GTM container validation, dataLayer analysis, form/checkout tracking -- Inject: Generate custom HTML tags for dataLayer pushes when direct code access is unavailable -- Export: Send audit results to Notion database - -Usage: - # Audit mode - python gtm_manager.py audit --url "https://example.com" --journey full - - # Inject mode - generate dataLayer push tags - python gtm_manager.py inject --event purchase --output tags/ - - # Generate tags from audit report - python gtm_manager.py inject --from-audit gtm_audit_report.json - -Options: - audit Run GTM audit on a URL - inject Generate custom HTML tags for dataLayer injection - --url Target URL to audit (required for audit) - --container Expected GTM container ID (e.g., GTM-XXXXXX) - --journey Journey type: pageview, scroll, click, form, checkout, datalayer, full - --output Output file/directory path - --notion Export results to Notion database -""" - -import argparse -import json -import re -import sys -from datetime import datetime -from urllib.parse import urlparse, parse_qs -from playwright.sync_api import sync_playwright - -# Tag destination patterns -TAG_DESTINATIONS = { - "GA4": [ - r"google-analytics\.com/g/collect", - r"analytics\.google\.com/g/collect", - ], - "Universal Analytics": [ - r"google-analytics\.com/collect", - r"google-analytics\.com/r/collect", - ], - "Google Ads": [ - r"googleads\.g\.doubleclick\.net", - r"google\.com/pagead", - r"googleadservices\.com/pagead", - ], - "Meta Pixel": [ - r"facebook\.com/tr", - r"connect\.facebook\.net", - ], - "LinkedIn": [ - r"px\.ads\.linkedin\.com", - r"snap\.licdn\.com", - ], - "TikTok": [ - r"analytics\.tiktok\.com", - ], - "Twitter/X": [ - r"ads-twitter\.com", - r"t\.co/i/adsct", - ], - "Kakao": [ - r"pixel\.kakao\.com", - ], - "Naver": [ - r"wcs\.naver\.com", - ], -} - -# GA4 Required Parameters by Event -GA4_EVENT_REQUIREMENTS = { - "purchase": { - "required": ["transaction_id", "value", "currency"], - "items_required": ["item_id", "item_name"], - }, - "add_to_cart": { - "required": ["currency", "value"], - "items_required": ["item_id", "item_name"], - }, - "begin_checkout": { - "required": ["currency", "value"], - "items_required": ["item_id", "item_name"], - }, - "add_shipping_info": { - "required": ["currency", "value"], - "recommended": ["shipping_tier"], - }, - "add_payment_info": { - "required": ["currency", "value"], - "recommended": ["payment_type"], - }, - "view_item": { - "required": ["currency", "value"], - "items_required": ["item_id", "item_name"], - }, - "view_cart": { - "required": ["currency", "value"], - "items_required": ["item_id", "item_name"], - }, - "generate_lead": { - "recommended": ["currency", "value"], - }, - "form_submit": { - "recommended": ["form_id", "form_name"], - }, -} - -# Checkout flow sequence -CHECKOUT_SEQUENCE = [ - "view_cart", - "begin_checkout", - "add_shipping_info", - "add_payment_info", - "purchase", -] - - -class DataLayerValidator: - """Advanced dataLayer validation and monitoring.""" - - def __init__(self): - self.events = [] - self.issues = [] - self.snapshots = [] - - def validate_event(self, event_data): - """Validate a single dataLayer event against GA4 specs.""" - issues = [] - event_name = event_data.get("event") - - if not event_name: - return issues - - # Check if event has requirements - if event_name in GA4_EVENT_REQUIREMENTS: - reqs = GA4_EVENT_REQUIREMENTS[event_name] - ecommerce = event_data.get("ecommerce", {}) - - # Check required fields - for field in reqs.get("required", []): - if field not in ecommerce and field not in event_data: - issues.append({ - "type": "missing_required", - "event": event_name, - "field": field, - "message": f"Missing required field: {field}", - }) - - # Check items array - items = ecommerce.get("items", []) - if reqs.get("items_required") and not items: - issues.append({ - "type": "missing_items", - "event": event_name, - "message": "E-commerce event missing 'items' array", - }) - - # Validate items structure - for i, item in enumerate(items): - for field in reqs.get("items_required", []): - if field not in item: - issues.append({ - "type": "item_missing_field", - "event": event_name, - "item_index": i, - "field": field, - "message": f"Item {i} missing required field: {field}", - }) - - # Check data types - if "value" in ecommerce: - if not isinstance(ecommerce["value"], (int, float)): - issues.append({ - "type": "wrong_type", - "event": event_name, - "field": "value", - "message": f"'value' should be number, got {type(ecommerce['value']).__name__}", - }) - - # Check transaction_id uniqueness hint - if event_name == "purchase" and "transaction_id" in ecommerce: - tid = ecommerce["transaction_id"] - if not tid or tid == "" or tid == "undefined": - issues.append({ - "type": "invalid_transaction_id", - "event": event_name, - "message": "transaction_id is empty or invalid", - }) - - return issues - - def validate_sequence(self, events): - """Validate checkout event sequence.""" - issues = [] - event_names = [e.get("event") for e in events if e.get("event")] - - # Find checkout events in order - checkout_events = [e for e in event_names if e in CHECKOUT_SEQUENCE] - - # Check sequence - last_idx = -1 - for event in checkout_events: - idx = CHECKOUT_SEQUENCE.index(event) - if idx < last_idx: - issues.append({ - "type": "sequence_error", - "message": f"Event '{event}' fired out of order", - }) - last_idx = idx - - return issues - - def check_ecommerce_clear(self, events): - """Check if ecommerce object is cleared before new pushes.""" - issues = [] - last_had_ecommerce = False - - for i, event in enumerate(events): - has_ecommerce = "ecommerce" in event - is_clear = event.get("ecommerce") is None - - if has_ecommerce and last_had_ecommerce and not is_clear: - # Previous had ecommerce, this has ecommerce, but no clear - issues.append({ - "type": "missing_ecommerce_clear", - "index": i, - "event": event.get("event"), - "message": "E-commerce data should be cleared before new push", - }) - - if has_ecommerce and not is_clear: - last_had_ecommerce = True - elif is_clear: - last_had_ecommerce = False - - return issues - - -class FormAnalyzer: - """Form discovery, analysis, and interaction tracking.""" - - def __init__(self, page): - self.page = page - self.forms = [] - self.interactions = [] - self.issues = [] - - def discover_forms(self): - """Find and analyze all forms on the page.""" - forms_data = self.page.evaluate(""" - () => { - const forms = document.querySelectorAll('form'); - return Array.from(forms).map((form, idx) => { - const fields = Array.from(form.querySelectorAll('input, select, textarea')); - return { - index: idx, - id: form.id || null, - name: form.name || null, - action: form.action || null, - method: form.method || 'get', - className: form.className || null, - fieldCount: fields.length, - fields: fields.map(field => ({ - type: field.type || field.tagName.toLowerCase(), - name: field.name || null, - id: field.id || null, - required: field.required || false, - placeholder: field.placeholder || null, - validation: field.pattern || null, - maxLength: field.maxLength > 0 ? field.maxLength : null, - })), - hasSubmitButton: form.querySelector('button[type="submit"], input[type="submit"]') !== null, - }; - }); - } - """) - - self.forms = forms_data - return forms_data - - def analyze_form_tracking_readiness(self): - """Check if forms are ready for GTM tracking.""" - issues = [] - - for form in self.forms: - # Check for identifiers - if not form["id"] and not form["name"]: - issues.append({ - "type": "form_no_identifier", - "form_index": form["index"], - "message": f"Form {form['index']} has no id or name attribute", - "recommendation": "Add id or name attribute for reliable form tracking", - }) - - # Check fields for tracking - for field in form["fields"]: - if field["type"] in ["text", "email", "tel"] and not field["name"] and not field["id"]: - issues.append({ - "type": "field_no_identifier", - "form_index": form["index"], - "field_type": field["type"], - "message": "Input field missing name/id for tracking", - }) - - # Check for submit button - if not form["hasSubmitButton"]: - issues.append({ - "type": "form_no_submit", - "form_index": form["index"], - "message": "Form has no submit button - may use JS submission", - "recommendation": "Verify form submission triggers dataLayer push", - }) - - self.issues = issues - return issues - - def simulate_form_interaction(self, form_index=0): - """Simulate user interaction with a form.""" - if form_index >= len(self.forms): - return {"error": "Form index out of range"} - - form = self.forms[form_index] - interactions = [] - - # Find form element - form_selector = f"form:nth-of-type({form_index + 1})" - if form["id"]: - form_selector = f"#{form['id']}" - elif form["name"]: - form_selector = f"form[name='{form['name']}']" - - try: - form_element = self.page.locator(form_selector) - - # Interact with each field - for field in form["fields"]: - field_selector = None - if field["id"]: - field_selector = f"#{field['id']}" - elif field["name"]: - field_selector = f"[name='{field['name']}']" - - if not field_selector: - continue - - try: - field_element = self.page.locator(field_selector).first - - # Focus event - field_element.focus() - interactions.append({ - "action": "focus", - "field": field["name"] or field["id"], - "timestamp": datetime.now().isoformat(), - }) - self.page.wait_for_timeout(200) - - # Fill based on type - test_values = { - "text": "Test User", - "email": "test@example.com", - "tel": "010-1234-5678", - "number": "100", - "password": "TestPass123!", - } - - if field["type"] in test_values: - field_element.fill(test_values[field["type"]]) - interactions.append({ - "action": "input", - "field": field["name"] or field["id"], - "type": field["type"], - "timestamp": datetime.now().isoformat(), - }) - self.page.wait_for_timeout(200) - - # Blur event - field_element.blur() - interactions.append({ - "action": "blur", - "field": field["name"] or field["id"], - "timestamp": datetime.now().isoformat(), - }) - - except Exception as e: - interactions.append({ - "action": "error", - "field": field["name"] or field["id"], - "error": str(e), - }) - - self.interactions = interactions - return interactions - - except Exception as e: - return {"error": str(e)} - - def check_form_events(self, datalayer_events): - """Check if expected form events are in dataLayer.""" - expected_events = ["form_start", "form_submit", "generate_lead"] - found_events = [] - missing_events = [] - - event_names = [e.get("event") for e in datalayer_events] - - for expected in expected_events: - if expected in event_names: - found_events.append(expected) - else: - missing_events.append(expected) - - return { - "found": found_events, - "missing": missing_events, - "recommendation": "Consider implementing: " + ", ".join(missing_events) if missing_events else None, - } - - -class CheckoutFlowAnalyzer: - """E-commerce checkout flow simulation and validation.""" - - def __init__(self, page): - self.page = page - self.steps_completed = [] - self.events_captured = [] - self.issues = [] - - def detect_checkout_elements(self): - """Find checkout-related elements on page.""" - elements = self.page.evaluate(""" - () => { - const selectors = { - cart: [ - '[class*="cart"]', '[id*="cart"]', - '[class*="basket"]', '[id*="basket"]', - ], - checkout: [ - '[class*="checkout"]', '[id*="checkout"]', - 'button:has-text("Checkout")', 'a:has-text("Checkout")', - 'button:has-text("결제")', 'a:has-text("결제")', - ], - addToCart: [ - 'button:has-text("Add to Cart")', 'button:has-text("Add to Bag")', - 'button:has-text("장바구니")', 'button:has-text("담기")', - '[class*="add-to-cart"]', '[id*="add-to-cart"]', - ], - quantity: [ - '[class*="quantity"]', '[name*="quantity"]', - '[class*="qty"]', '[name*="qty"]', - ], - removeItem: [ - '[class*="remove"]', 'button:has-text("Remove")', - 'button:has-text("삭제")', '[class*="delete"]', - ], - promoCode: [ - '[name*="promo"]', '[name*="coupon"]', '[id*="coupon"]', - '[placeholder*="promo"]', '[placeholder*="coupon"]', - ], - }; - - const found = {}; - for (const [type, selectorList] of Object.entries(selectors)) { - found[type] = []; - for (const sel of selectorList) { - try { - const elements = document.querySelectorAll(sel); - elements.forEach(el => { - found[type].push({ - selector: sel, - tag: el.tagName.toLowerCase(), - text: el.textContent?.slice(0, 50) || null, - visible: el.offsetParent !== null, - }); - }); - } catch(e) {} - } - } - return found; - } - """) - return elements - - def simulate_add_to_cart(self): - """Attempt to simulate add-to-cart action.""" - try: - # Try common add-to-cart selectors - selectors = [ - 'button:has-text("Add to Cart")', - 'button:has-text("Add to Bag")', - 'button:has-text("장바구니")', - '[class*="add-to-cart"]:visible', - '[id*="add-to-cart"]:visible', - ] - - for selector in selectors: - try: - btn = self.page.locator(selector).first - if btn.is_visible(): - btn.click() - self.steps_completed.append({ - "step": "add_to_cart", - "selector": selector, - "timestamp": datetime.now().isoformat(), - }) - self.page.wait_for_timeout(1500) - return True - except: - continue - - return False - except Exception as e: - self.issues.append({"step": "add_to_cart", "error": str(e)}) - return False - - def simulate_begin_checkout(self): - """Attempt to click checkout button.""" - try: - selectors = [ - 'button:has-text("Checkout")', - 'a:has-text("Checkout")', - 'button:has-text("결제하기")', - 'button:has-text("주문하기")', - '[class*="checkout-btn"]:visible', - ] - - for selector in selectors: - try: - btn = self.page.locator(selector).first - if btn.is_visible(): - btn.click() - self.steps_completed.append({ - "step": "begin_checkout", - "selector": selector, - "timestamp": datetime.now().isoformat(), - }) - self.page.wait_for_timeout(2000) - return True - except: - continue - - return False - except Exception as e: - self.issues.append({"step": "begin_checkout", "error": str(e)}) - return False - - def validate_checkout_events(self, datalayer_events): - """Validate checkout-related events in dataLayer.""" - results = { - "events_found": [], - "events_missing": [], - "sequence_valid": True, - "issues": [], - } - - event_names = [e.get("event") for e in datalayer_events] - - # Check each checkout step - for step in CHECKOUT_SEQUENCE: - if step in event_names: - results["events_found"].append(step) - - # Validate event parameters - for event in datalayer_events: - if event.get("event") == step: - validator = DataLayerValidator() - issues = validator.validate_event(event) - results["issues"].extend(issues) - else: - results["events_missing"].append(step) - - # Check sequence - found_sequence = [e for e in event_names if e in CHECKOUT_SEQUENCE] - expected_order = [e for e in CHECKOUT_SEQUENCE if e in found_sequence] - - if found_sequence != expected_order: - results["sequence_valid"] = False - results["issues"].append({ - "type": "sequence_error", - "message": f"Events out of order. Found: {found_sequence}, Expected: {expected_order}", - }) - - return results - - -class GTMAuditor: - """Main GTM audit orchestrator.""" - - def __init__(self, url, container_id=None, timeout=30000, headless=True): - self.url = url - self.expected_container = container_id - self.timeout = timeout - self.headless = headless - self.report = { - "audit_metadata": { - "url": url, - "timestamp": datetime.now().isoformat(), - "expected_container": container_id, - }, - "container_status": {}, - "datalayer_analysis": { - "events": [], - "validation_issues": [], - "sequence_issues": [], - }, - "form_analysis": { - "forms_found": [], - "tracking_issues": [], - "events_status": {}, - }, - "checkout_analysis": { - "elements_found": {}, - "events_status": {}, - "flow_issues": [], - }, - "network_requests": [], - "tags_fired": [], - "issues": [], - "recommendations": [], - "checklist": {}, - } - self.network_requests = [] - self.datalayer_history = [] - self.page = None - - def _setup_network_monitoring(self, page): - """Intercept and log network requests to tag destinations.""" - def handle_request(request): - url = request.url - for destination, patterns in TAG_DESTINATIONS.items(): - for pattern in patterns: - if re.search(pattern, url): - parsed = urlparse(url) - params = parse_qs(parsed.query) - self.network_requests.append({ - "destination": destination, - "url": url[:200], - "method": request.method, - "params": {k: v[0] if len(v) == 1 else v for k, v in params.items()}, - "timestamp": datetime.now().isoformat(), - }) - break - - page.on("request", handle_request) - - def _setup_datalayer_monitoring(self, page): - """Inject dataLayer monitoring script.""" - page.evaluate(""" - () => { - window.__gtmAuditEvents = []; - const originalPush = window.dataLayer.push; - window.dataLayer.push = function() { - const result = originalPush.apply(this, arguments); - for (let i = 0; i < arguments.length; i++) { - window.__gtmAuditEvents.push({ - data: JSON.parse(JSON.stringify(arguments[i])), - timestamp: new Date().toISOString() - }); - } - return result; - }; - } - """) - - def _capture_datalayer(self, page): - """Capture current dataLayer state.""" - try: - datalayer = page.evaluate(""" - () => { - if (typeof window.dataLayer !== 'undefined') { - return JSON.parse(JSON.stringify(window.dataLayer)); - } - return null; - } - """) - return datalayer - except Exception as e: - return {"error": str(e)} - - def _capture_monitored_events(self, page): - """Capture events logged by our monitoring.""" - try: - events = page.evaluate(""" - () => window.__gtmAuditEvents || [] - """) - return events - except: - return [] - - def _check_gtm_container(self, page): - """Verify GTM container installation.""" - result = page.evaluate(""" - () => { - const scripts = document.querySelectorAll('script'); - const gtmInfo = { - installed: false, - containers: [], - position: null, - noscript: false, - dataLayerInit: false, - dataLayerInitBeforeGTM: false, - }; - - gtmInfo.dataLayerInit = typeof window.dataLayer !== 'undefined' && - Array.isArray(window.dataLayer); - - let gtmScriptIndex = -1; - let dataLayerInitIndex = -1; - - scripts.forEach((script, index) => { - const src = script.src || ''; - const innerHTML = script.innerHTML || ''; - - // Check for dataLayer init - if (innerHTML.includes('dataLayer') && innerHTML.includes('[]')) { - dataLayerInitIndex = index; - } - - const gtmMatch = src.match(/gtm\\.js\\?id=(GTM-[A-Z0-9]+)/); - if (gtmMatch) { - gtmInfo.installed = true; - gtmInfo.containers.push(gtmMatch[1]); - gtmInfo.position = script.closest('head') ? 'head' : 'body'; - gtmScriptIndex = index; - } - - const inlineMatch = innerHTML.match(/GTM-[A-Z0-9]+/g); - if (inlineMatch) { - gtmInfo.installed = true; - inlineMatch.forEach(id => { - if (!gtmInfo.containers.includes(id)) { - gtmInfo.containers.push(id); - } - }); - } - }); - - gtmInfo.dataLayerInitBeforeGTM = dataLayerInitIndex < gtmScriptIndex && dataLayerInitIndex !== -1; - - const noscripts = document.querySelectorAll('noscript'); - noscripts.forEach(ns => { - if (ns.innerHTML.includes('googletagmanager.com/ns.html')) { - gtmInfo.noscript = true; - } - }); - - return gtmInfo; - } - """) - - status = { - "installed": result["installed"], - "containers": result["containers"], - "position": result["position"], - "noscript_present": result["noscript"], - "datalayer_initialized": result["dataLayerInit"], - "datalayer_init_before_gtm": result["dataLayerInitBeforeGTM"], - "issues": [], - } - - if not result["installed"]: - status["issues"].append("GTM container not detected") - self.report["issues"].append({ - "severity": "critical", - "type": "container_missing", - "message": "GTM container script not found on page", - }) - - if len(result["containers"]) > 1: - status["issues"].append(f"Multiple containers: {result['containers']}") - self.report["issues"].append({ - "severity": "warning", - "type": "multiple_containers", - "message": f"Multiple GTM containers found: {', '.join(result['containers'])}", - }) - - if self.expected_container and self.expected_container not in result["containers"]: - self.report["issues"].append({ - "severity": "error", - "type": "container_mismatch", - "message": f"Expected {self.expected_container}, found {result['containers']}", - }) - - if result["position"] == "body": - self.report["issues"].append({ - "severity": "warning", - "type": "script_position", - "message": "GTM script in body - may delay tag firing", - }) - - if not result["dataLayerInitBeforeGTM"]: - self.report["issues"].append({ - "severity": "warning", - "type": "datalayer_order", - "message": "dataLayer should be initialized before GTM script", - }) - - self.report["container_status"] = status - return status - - def _simulate_scroll(self, page): - """Simulate scroll to trigger scroll-depth tags.""" - page.evaluate(""" - () => { - const heights = [0.25, 0.5, 0.75, 0.9, 1.0]; - const docHeight = document.documentElement.scrollHeight; - heights.forEach((pct, i) => { - setTimeout(() => { - window.scrollTo(0, docHeight * pct); - }, i * 500); - }); - } - """) - page.wait_for_timeout(3000) - - def _run_form_audit(self, page): - """Execute form analysis.""" - print("📝 Analyzing forms...") - - form_analyzer = FormAnalyzer(page) - forms = form_analyzer.discover_forms() - tracking_issues = form_analyzer.analyze_form_tracking_readiness() - - self.report["form_analysis"]["forms_found"] = forms - self.report["form_analysis"]["tracking_issues"] = tracking_issues - - if forms: - print(f" Found {len(forms)} form(s)") - # Simulate interaction with first form - interactions = form_analyzer.simulate_form_interaction(0) - self.report["form_analysis"]["interactions"] = interactions - - # Allow time for events - page.wait_for_timeout(2000) - - # Check form events - datalayer = self._capture_datalayer(page) - if datalayer: - events_status = form_analyzer.check_form_events(datalayer) - self.report["form_analysis"]["events_status"] = events_status - else: - print(" No forms found on page") - - def _run_checkout_audit(self, page): - """Execute e-commerce checkout flow analysis.""" - print("🛒 Analyzing checkout flow...") - - checkout_analyzer = CheckoutFlowAnalyzer(page) - elements = checkout_analyzer.detect_checkout_elements() - - self.report["checkout_analysis"]["elements_found"] = elements - - # Log what we found - for element_type, found in elements.items(): - if found: - print(f" Found {len(found)} {element_type} element(s)") - - def _run_datalayer_audit(self, page): - """Execute deep dataLayer analysis.""" - print("📊 Analyzing dataLayer...") - - datalayer = self._capture_datalayer(page) - monitored_events = self._capture_monitored_events(page) - - if not datalayer: - self.report["datalayer_analysis"]["issues"] = ["dataLayer not found"] - return - - validator = DataLayerValidator() - - # Validate each event - for event in datalayer: - if isinstance(event, dict): - issues = validator.validate_event(event) - if issues: - self.report["datalayer_analysis"]["validation_issues"].extend(issues) - - # Check sequence - sequence_issues = validator.validate_sequence(datalayer) - self.report["datalayer_analysis"]["sequence_issues"] = sequence_issues - - # Check ecommerce clearing - clear_issues = validator.check_ecommerce_clear(datalayer) - self.report["datalayer_analysis"]["validation_issues"].extend(clear_issues) - - # Store events - events = [] - for i, item in enumerate(datalayer): - if isinstance(item, dict) and item.get("event"): - events.append({ - "index": i, - "event": item.get("event"), - "has_ecommerce": "ecommerce" in item, - "params": list(item.keys()), - }) - - self.report["datalayer_analysis"]["events"] = events - print(f" Found {len(events)} events in dataLayer") - - def _generate_recommendations(self): - """Generate recommendations based on findings.""" - recs = [] - - for issue in self.report["issues"]: - if issue["type"] == "container_missing": - recs.append({ - "priority": "high", - "action": "Install GTM container", - "details": "Add GTM snippet to
section", - }) - elif issue["type"] == "datalayer_order": - recs.append({ - "priority": "medium", - "action": "Initialize dataLayer before GTM", - "details": "Add 'window.dataLayer = window.dataLayer || [];' before GTM", - }) - - # Form recommendations - if not self.report["form_analysis"]["forms_found"]: - pass # No forms to track - elif self.report["form_analysis"].get("events_status", {}).get("missing"): - missing = self.report["form_analysis"]["events_status"]["missing"] - recs.append({ - "priority": "medium", - "action": "Implement form tracking events", - "details": f"Missing events: {', '.join(missing)}", - }) - - # DataLayer recommendations - validation_issues = self.report["datalayer_analysis"].get("validation_issues", []) - if validation_issues: - recs.append({ - "priority": "high", - "action": "Fix dataLayer validation issues", - "details": f"{len(validation_issues)} issue(s) found in event structure", - }) - - # Tag coverage - destinations = set(r["destination"] for r in self.network_requests) - if "GA4" not in destinations: - recs.append({ - "priority": "high", - "action": "Verify GA4 implementation", - "details": "No GA4 requests detected", - }) - - self.report["recommendations"] = recs - - def _generate_checklist(self): - """Generate audit checklist.""" - self.report["checklist"] = { - "container_health": { - "gtm_installed": self.report["container_status"].get("installed", False), - "correct_container": self.expected_container in self.report["container_status"].get("containers", []) if self.expected_container else True, - "no_duplicates": len(self.report["container_status"].get("containers", [])) <= 1, - "correct_position": self.report["container_status"].get("position") == "head", - "datalayer_init_order": self.report["container_status"].get("datalayer_init_before_gtm", False), - }, - "datalayer_quality": { - "initialized": self.report["container_status"].get("datalayer_initialized", False), - "events_present": len(self.report["datalayer_analysis"].get("events", [])) > 0, - "no_validation_errors": len(self.report["datalayer_analysis"].get("validation_issues", [])) == 0, - "correct_sequence": len(self.report["datalayer_analysis"].get("sequence_issues", [])) == 0, - }, - "form_tracking": { - "forms_identifiable": all( - f.get("id") or f.get("name") - for f in self.report["form_analysis"].get("forms_found", []) - ) if self.report["form_analysis"].get("forms_found") else True, - "form_events_present": len( - self.report["form_analysis"].get("events_status", {}).get("found", []) - ) > 0 if self.report["form_analysis"].get("forms_found") else True, - }, - "tag_firing": { - "ga4_active": any(r["destination"] == "GA4" for r in self.network_requests), - "requests_captured": len(self.network_requests) > 0, - }, - } - - def run_audit(self, journey="pageview"): - """Execute the full audit workflow.""" - print(f"🔍 Starting GTM audit for: {self.url}") - print(f" Journey type: {journey}") - - with sync_playwright() as p: - browser = p.chromium.launch(headless=self.headless) - context = browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) GTMAudit/1.0" - ) - page = context.new_page() - self.page = page - - self._setup_network_monitoring(page) - - try: - print("📄 Loading page...") - page.goto(self.url, timeout=self.timeout, wait_until="networkidle") - page.wait_for_timeout(2000) - - # Setup dataLayer monitoring after page load - try: - self._setup_datalayer_monitoring(page) - except: - pass - - print("🏷️ Checking GTM container...") - self._check_gtm_container(page) - - # Run journey-specific audits - if journey in ["scroll", "pageview", "full"]: - print("📜 Simulating scroll...") - self._simulate_scroll(page) - - if journey in ["form", "full"]: - self._run_form_audit(page) - - if journey in ["checkout", "full"]: - self._run_checkout_audit(page) - - if journey in ["datalayer", "full"]: - self._run_datalayer_audit(page) - - # Always do basic dataLayer capture - page.wait_for_timeout(2000) - self._run_datalayer_audit(page) - - # Store network requests - self.report["network_requests"] = self.network_requests - self.report["tags_fired"] = list(set(r["destination"] for r in self.network_requests)) - - except Exception as e: - self.report["issues"].append({ - "severity": "critical", - "type": "audit_error", - "message": str(e), - }) - finally: - browser.close() - - self._generate_recommendations() - self._generate_checklist() - - print("✅ Audit complete!") - return self.report - - def save_report(self, filepath): - """Save report to JSON file.""" - with open(filepath, "w", encoding="utf-8") as f: - json.dump(self.report, f, indent=2, ensure_ascii=False) - print(f"📝 Report saved to: {filepath}") - - def print_summary(self): - """Print audit summary to console.""" - print("\n" + "="*60) - print("📋 GTM AUDIT SUMMARY") - print("="*60) - - # Container - cs = self.report["container_status"] - print(f"\n🏷️ Container: {'✅ Installed' if cs.get('installed') else '❌ Not Found'}") - if cs.get("containers"): - print(f" IDs: {', '.join(cs['containers'])}") - - # DataLayer - dl = self.report["datalayer_analysis"] - print(f"\n📊 DataLayer:") - print(f" Events found: {len(dl.get('events', []))}") - print(f" Validation issues: {len(dl.get('validation_issues', []))}") - - # Forms - fa = self.report["form_analysis"] - if fa.get("forms_found"): - print(f"\n📝 Forms:") - print(f" Forms found: {len(fa['forms_found'])}") - print(f" Tracking issues: {len(fa.get('tracking_issues', []))}") - - # Tags - print(f"\n🔥 Tags Fired: {', '.join(self.report['tags_fired']) if self.report['tags_fired'] else 'None detected'}") - - # Issues - print(f"\n⚠️ Total Issues: {len(self.report['issues'])}") - for issue in self.report["issues"][:5]: - print(f" - [{issue['severity'].upper()}] {issue['message']}") - - # Recommendations - print(f"\n💡 Recommendations: {len(self.report['recommendations'])}") - for rec in self.report["recommendations"][:3]: - print(f" - [{rec['priority'].upper()}] {rec['action']}") - - print("\n" + "="*60) - - -class NotionExporter: - """Export GTM audit reports to Notion database.""" - - # Default database ID - OurDigital GTM Audit Log - DEFAULT_DATABASE_ID = "2cf581e5-8a1e-8163-997f-ccb387156a20" - - def __init__(self, token=None, database_id=None): - import os - self.token = token or os.environ.get("NOTION_TOKEN") or os.environ.get("NOTION_API_KEY") - self.database_id = database_id or self.DEFAULT_DATABASE_ID - self.base_url = "https://api.notion.com/v1" - - if not self.token: - raise ValueError("Notion token not found. Set NOTION_TOKEN or NOTION_API_KEY environment variable.") - - def _headers(self): - return { - "Authorization": f"Bearer {self.token}", - "Content-Type": "application/json", - "Notion-Version": "2022-06-28", - } - - def _make_request(self, method, endpoint, data=None): - import urllib.request - import urllib.error - - url = f"{self.base_url}{endpoint}" - req = urllib.request.Request(url, method=method, headers=self._headers()) - - if data: - req.data = json.dumps(data).encode("utf-8") - - try: - with urllib.request.urlopen(req) as response: - return json.loads(response.read().decode("utf-8")) - except urllib.error.HTTPError as e: - error_body = e.read().decode("utf-8") - raise Exception(f"Notion API error: {e.code} - {error_body}") - - def _generate_audit_id(self, url, timestamp): - """Generate unique audit ID.""" - from urllib.parse import urlparse - import hashlib - - domain = urlparse(url).netloc.replace("www.", "") - date_str = timestamp[:10].replace("-", "") - hash_suffix = hashlib.md5(f"{url}{timestamp}".encode()).hexdigest()[:6] - return f"GTM-{domain[:20]}-{date_str}-{hash_suffix}" - - def _determine_audit_status(self, report): - """Determine overall audit status based on issues.""" - issues = report.get("issues", []) - critical_count = sum(1 for i in issues if i.get("severity") == "critical") - high_count = sum(1 for i in issues if i.get("severity") in ["critical", "error"]) - - if critical_count > 0: - return "Fail" - elif high_count > 0 or len(issues) > 5: - return "Warning" - return "Pass" - - def _determine_gtm_status(self, container_status): - """Determine GTM installation status.""" - if not container_status.get("installed"): - return "Not Found" - if len(container_status.get("containers", [])) > 1: - return "Multiple Containers" - return "Installed" - - def _generate_summary(self, report): - """Generate text summary of audit findings.""" - parts = [] - - # Container status - cs = report.get("container_status", {}) - if cs.get("installed"): - containers = cs.get("containers", []) - parts.append(f"GTM: {', '.join(containers)}") - else: - parts.append("GTM: Not installed") - - # DataLayer - dl = report.get("datalayer_analysis", {}) - event_count = len(dl.get("events", [])) - validation_issues = len(dl.get("validation_issues", [])) - parts.append(f"Events: {event_count}") - if validation_issues: - parts.append(f"Validation issues: {validation_issues}") - - # Forms - fa = report.get("form_analysis", {}) - form_count = len(fa.get("forms_found", [])) - if form_count: - parts.append(f"Forms: {form_count}") - - # Tags - tags = report.get("tags_fired", []) - if tags: - parts.append(f"Tags: {', '.join(tags)}") - - # Issues - issues = report.get("issues", []) - if issues: - parts.append(f"Total issues: {len(issues)}") - - return " | ".join(parts) - - def export_report(self, report, journey_type="full"): - """Export audit report to Notion database.""" - metadata = report.get("audit_metadata", {}) - url = metadata.get("url", "") - timestamp = metadata.get("timestamp", datetime.now().isoformat()) - - audit_id = self._generate_audit_id(url, timestamp) - container_status = report.get("container_status", {}) - issues = report.get("issues", []) - tags_fired = report.get("tags_fired", []) - - # Count critical issues - critical_count = sum(1 for i in issues if i.get("severity") in ["critical", "error"]) - - # Build Notion page properties - from urllib.parse import urlparse - site_name = urlparse(url).netloc if url else "Unknown" - - properties = { - "Site": { - "title": [{"text": {"content": site_name}}] - }, - "Audit ID": { - "rich_text": [{"text": {"content": audit_id}}] - }, - "URL": { - "url": url if url else None - }, - "Audit Date": { - "date": {"start": timestamp[:10]} if timestamp else None - }, - "Journey Type": { - "select": {"name": journey_type} - }, - "GTM Status": { - "select": {"name": self._determine_gtm_status(container_status)} - }, - "Container IDs": { - "rich_text": [{"text": {"content": ", ".join(container_status.get("containers", []))}}] - }, - "Tags Fired": { - "multi_select": [{"name": tag} for tag in tags_fired if tag in ["GA4", "Google Ads", "Meta Pixel", "LinkedIn", "TikTok", "Kakao", "Naver"]] - }, - "Issues Count": { - "number": len(issues) - }, - "Critical Issues": { - "number": critical_count - }, - "Audit Status": { - "select": {"name": self._determine_audit_status(report)} - }, - "Summary": { - "rich_text": [{"text": {"content": self._generate_summary(report)[:2000]}}] - }, - } - - # Create page in database - page_data = { - "parent": {"database_id": self.database_id}, - "properties": properties, - } - - result = self._make_request("POST", "/pages", page_data) - - page_url = result.get("url", "") - print(f"📤 Exported to Notion: {page_url}") - - return { - "success": True, - "page_id": result.get("id"), - "page_url": page_url, - "audit_id": audit_id, - } - - def add_detailed_content(self, page_id, report): - """Add detailed content blocks to the Notion page.""" - blocks = [] - - # Issues section - issues = report.get("issues", []) - if issues: - blocks.append({ - "type": "heading_2", - "heading_2": { - "rich_text": [{"type": "text", "text": {"content": "Issues Found"}}] - } - }) - - for issue in issues[:10]: # Limit to 10 issues - severity = issue.get("severity", "info").upper() - message = issue.get("message", "") - blocks.append({ - "type": "bulleted_list_item", - "bulleted_list_item": { - "rich_text": [{"type": "text", "text": {"content": f"[{severity}] {message}"}}] - } - }) - - # Recommendations section - recommendations = report.get("recommendations", []) - if recommendations: - blocks.append({ - "type": "heading_2", - "heading_2": { - "rich_text": [{"type": "text", "text": {"content": "Recommendations"}}] - } - }) - - for rec in recommendations: - priority = rec.get("priority", "").upper() - action = rec.get("action", "") - details = rec.get("details", "") - blocks.append({ - "type": "bulleted_list_item", - "bulleted_list_item": { - "rich_text": [{"type": "text", "text": {"content": f"[{priority}] {action}: {details}"}}] - } - }) - - # Checklist section - checklist = report.get("checklist", {}) - if checklist: - blocks.append({ - "type": "heading_2", - "heading_2": { - "rich_text": [{"type": "text", "text": {"content": "Checklist"}}] - } - }) - - for category, items in checklist.items(): - blocks.append({ - "type": "heading_3", - "heading_3": { - "rich_text": [{"type": "text", "text": {"content": category.replace("_", " ").title()}}] - } - }) - - for item, passed in items.items(): - status = "✅" if passed else "❌" - item_name = item.replace("_", " ").title() - blocks.append({ - "type": "bulleted_list_item", - "bulleted_list_item": { - "rich_text": [{"type": "text", "text": {"content": f"{status} {item_name}"}}] - } - }) - - if blocks: - self._make_request("PATCH", f"/blocks/{page_id}/children", {"children": blocks}) - - -class DataLayerInjector: - """Generate GTM custom HTML tags for dataLayer injection. - - Use when direct code access is unavailable and you need to push - dataLayer events through GTM custom HTML tags. - """ - - # Event templates with required/recommended parameters - EVENT_TEMPLATES = { - "page_view": { - "description": "Track page views with custom dimensions", - "params": { - "page_title": "document.title", - "page_location": "window.location.href", - "page_path": "window.location.pathname", - }, - "custom_params": ["content_group", "author", "publish_date"], - "trigger": "All Pages", - }, - "view_item": { - "description": "Track product detail page views", - "params": { - "currency": "'KRW'", - "value": "/* product price */", - }, - "items_params": ["item_id", "item_name", "item_brand", "item_category", "price", "quantity"], - "trigger": "Product Detail Page", - "scrape_selectors": { - "item_name": [".product-title", ".product-name", "h1.title", "[itemprop='name']"], - "price": [".product-price", ".price", "[itemprop='price']", ".sale-price"], - "item_id": ["[data-product-id]", "[data-sku]", ".product-sku"], - "item_brand": [".brand", "[itemprop='brand']", ".product-brand"], - "item_category": [".category", ".breadcrumb", "[itemprop='category']"], - }, - }, - "add_to_cart": { - "description": "Track add to cart actions", - "params": { - "currency": "'KRW'", - "value": "/* cart value */", - }, - "items_params": ["item_id", "item_name", "item_brand", "item_category", "price", "quantity"], - "trigger": "Add to Cart Button Click", - "trigger_selector": "button.add-to-cart, .btn-cart, [data-action='add-to-cart']", - }, - "remove_from_cart": { - "description": "Track remove from cart actions", - "params": { - "currency": "'KRW'", - "value": "/* removed item value */", - }, - "items_params": ["item_id", "item_name", "price", "quantity"], - "trigger": "Remove from Cart Click", - "trigger_selector": "button.remove, .btn-remove, [data-action='remove']", - }, - "view_cart": { - "description": "Track cart page views", - "params": { - "currency": "'KRW'", - "value": "/* total cart value */", - }, - "items_params": ["item_id", "item_name", "price", "quantity"], - "trigger": "Cart Page", - }, - "begin_checkout": { - "description": "Track checkout initiation", - "params": { - "currency": "'KRW'", - "value": "/* checkout value */", - "coupon": "/* coupon code if any */", - }, - "items_params": ["item_id", "item_name", "price", "quantity"], - "trigger": "Checkout Page", - }, - "add_shipping_info": { - "description": "Track shipping info submission", - "params": { - "currency": "'KRW'", - "value": "/* order value */", - "shipping_tier": "/* shipping method */", - }, - "items_params": ["item_id", "item_name", "price", "quantity"], - "trigger": "Shipping Form Submit", - }, - "add_payment_info": { - "description": "Track payment info submission", - "params": { - "currency": "'KRW'", - "value": "/* order value */", - "payment_type": "/* payment method */", - }, - "items_params": ["item_id", "item_name", "price", "quantity"], - "trigger": "Payment Form Submit", - "payment_types_kr": ["신용카드", "카카오페이", "네이버페이", "토스", "무통장입금", "휴대폰결제"], - }, - "purchase": { - "description": "Track completed purchases", - "params": { - "transaction_id": "/* unique order ID */", - "currency": "'KRW'", - "value": "/* total order value */", - "tax": "/* tax amount */", - "shipping": "/* shipping cost */", - "coupon": "/* coupon code if any */", - }, - "items_params": ["item_id", "item_name", "item_brand", "item_category", "price", "quantity"], - "trigger": "Order Confirmation Page", - "scrape_selectors": { - "transaction_id": [".order-number", ".order-id", "[data-order-id]"], - "value": [".order-total", ".total-price", ".grand-total"], - }, - }, - "generate_lead": { - "description": "Track lead form submissions", - "params": { - "currency": "'KRW'", - "value": "/* lead value if applicable */", - }, - "custom_params": ["form_id", "form_name", "lead_source"], - "trigger": "Lead Form Submit", - }, - "form_submit": { - "description": "Track general form submissions", - "params": {}, - "custom_params": ["form_id", "form_name", "form_destination"], - "trigger": "Form Submission", - }, - "form_start": { - "description": "Track form interaction start", - "params": {}, - "custom_params": ["form_id", "form_name"], - "trigger": "Form Field Focus", - }, - "scroll": { - "description": "Track scroll depth", - "params": { - "percent_scrolled": "/* 25, 50, 75, 90, 100 */", - }, - "trigger": "Scroll Depth", - }, - "file_download": { - "description": "Track file downloads", - "params": { - "file_name": "/* filename */", - "file_extension": "/* pdf, xlsx, etc */", - "link_url": "/* download URL */", - }, - "trigger": "File Download Click", - "trigger_selector": "a[href$='.pdf'], a[href$='.xlsx'], a[href$='.docx'], a[download]", - }, - "video_start": { - "description": "Track video play start", - "params": { - "video_title": "/* video title */", - "video_provider": "/* youtube, vimeo, etc */", - "video_url": "/* video URL */", - }, - "trigger": "Video Start", - }, - "video_progress": { - "description": "Track video progress milestones", - "params": { - "video_title": "/* video title */", - "video_percent": "/* 25, 50, 75, 100 */", - "video_current_time": "/* seconds watched */", - }, - "trigger": "Video Progress", - }, - "video_complete": { - "description": "Track video completion", - "params": { - "video_title": "/* video title */", - "video_duration": "/* total duration */", - }, - "trigger": "Video Complete", - }, - "search": { - "description": "Track site search", - "params": { - "search_term": "/* search query */", - }, - "custom_params": ["search_results_count"], - "trigger": "Search Results Page", - "scrape_selectors": { - "search_term": ["input[name='q']", "input[name='search']", ".search-input"], - }, - }, - "login": { - "description": "Track user login", - "params": { - "method": "/* login method */", - }, - "trigger": "Login Success", - }, - "sign_up": { - "description": "Track user registration", - "params": { - "method": "/* signup method */", - }, - "trigger": "Signup Success", - }, - "share": { - "description": "Track social sharing", - "params": { - "method": "/* share platform */", - "content_type": "/* article, product, etc */", - "item_id": "/* content ID */", - }, - "trigger": "Share Button Click", - }, - "outbound_click": { - "description": "Track outbound link clicks", - "params": { - "link_url": "/* destination URL */", - "link_domain": "/* destination domain */", - "outbound": "true", - }, - "trigger": "Outbound Link Click", - }, - } - - def __init__(self, currency="KRW"): - self.currency = currency - self.generated_tags = [] - - def generate_tag(self, event_name, options=None): - """Generate a custom HTML tag for a specific event.""" - options = options or {} - - if event_name not in self.EVENT_TEMPLATES: - return {"error": f"Unknown event: {event_name}. Available: {list(self.EVENT_TEMPLATES.keys())}"} - - template = self.EVENT_TEMPLATES[event_name] - - # Build the tag - tag = { - "event": event_name, - "name": f"cHTML - dataLayer - {event_name}", - "description": template["description"], - "trigger_recommendation": template.get("trigger", "Custom"), - "trigger_selector": template.get("trigger_selector"), - "html": self._generate_html(event_name, template, options), - "scrape_selectors": template.get("scrape_selectors", {}), - } - - self.generated_tags.append(tag) - return tag - - def _generate_html(self, event_name, template, options): - """Generate the custom HTML tag code.""" - use_scraping = options.get("use_scraping", False) - include_comments = options.get("include_comments", True) - - lines = ["") - - return "\n".join(lines) - - def _generate_scraping_code(self, template, event_name): - """Generate code that scrapes values from DOM.""" - lines = [] - selectors = template.get("scrape_selectors", {}) - - lines.append(" // Scrape product/order data from page") - lines.append(" function getText(selectors) {") - lines.append(" for (var i = 0; i < selectors.length; i++) {") - lines.append(" var el = document.querySelector(selectors[i]);") - lines.append(" if (el) return el.textContent.trim();") - lines.append(" }") - lines.append(" return '';") - lines.append(" }") - lines.append("") - lines.append(" function getPrice(selectors) {") - lines.append(" var text = getText(selectors);") - lines.append(" var num = text.replace(/[^0-9]/g, '');") - lines.append(" return num ? parseInt(num, 10) : 0;") - lines.append(" }") - lines.append("") - - # Generate variable assignments - for field, sels in selectors.items(): - sel_str = json.dumps(sels) - if field in ["price", "value"]: - lines.append(f" var {field} = getPrice({sel_str});") - else: - lines.append(f" var {field} = getText({sel_str});") - - lines.append("") - lines.append(" window.dataLayer.push({") - lines.append(f" 'event': '{event_name}',") - lines.append(" 'ecommerce': {") - lines.append(f" 'currency': '{self.currency}',") - - if "value" in selectors or "price" in selectors: - lines.append(" 'value': value || price,") - if "transaction_id" in selectors: - lines.append(" 'transaction_id': transaction_id,") - - lines.append(" 'items': [{") - - items_params = template.get("items_params", []) - for param in items_params: - if param in selectors: - lines.append(f" '{param}': {param},") - elif param == "quantity": - lines.append(f" '{param}': 1,") - elif param == "price": - lines.append(f" '{param}': price,") - - lines.append(" }]") - lines.append(" }") - lines.append(" });") - - return lines - - def _generate_manual_ecommerce_code(self, event_name, template, options): - """Generate ecommerce code with manual value placeholders.""" - lines = [] - - lines.append(" window.dataLayer.push({") - lines.append(f" 'event': '{event_name}',") - lines.append(" 'ecommerce': {") - - for param, default_value in template.get("params", {}).items(): - if param == "currency": - lines.append(f" '{param}': '{self.currency}',") - else: - lines.append(f" '{param}': {default_value},") - - lines.append(" 'items': [{") - - for param in template.get("items_params", []): - lines.append(f" '{param}': /* TODO: set {param} */,") - - lines.append(" }]") - lines.append(" }") - lines.append(" });") - - return lines - - def generate_from_audit(self, audit_report): - """Generate tags based on audit findings.""" - recommendations = [] - - # Check for missing events - datalayer = audit_report.get("datalayer_analysis", {}) - existing_events = [e.get("event") for e in datalayer.get("events", [])] - - # Form analysis - form_analysis = audit_report.get("form_analysis", {}) - if form_analysis.get("forms_found") and "form_submit" not in existing_events: - recommendations.append({ - "event": "form_submit", - "reason": f"Found {len(form_analysis['forms_found'])} form(s) but no form_submit event", - }) - self.generate_tag("form_submit") - - # Checkout analysis - checkout = audit_report.get("checkout_analysis", {}) - elements = checkout.get("elements_found", {}) - - if elements.get("addToCart") and "add_to_cart" not in existing_events: - recommendations.append({ - "event": "add_to_cart", - "reason": "Add to cart elements found but no add_to_cart event", - }) - self.generate_tag("add_to_cart") - - if elements.get("checkout") and "begin_checkout" not in existing_events: - recommendations.append({ - "event": "begin_checkout", - "reason": "Checkout elements found but no begin_checkout event", - }) - self.generate_tag("begin_checkout") - - # Check for missing ecommerce sequence - for event in CHECKOUT_SEQUENCE: - if event not in existing_events: - if event not in [r["event"] for r in recommendations]: - recommendations.append({ - "event": event, - "reason": f"Missing from checkout sequence", - }) - self.generate_tag(event) - - return { - "recommendations": recommendations, - "generated_tags": self.generated_tags, - } - - def generate_all_ecommerce(self): - """Generate all ecommerce-related tags.""" - ecommerce_events = [ - "view_item", "add_to_cart", "remove_from_cart", "view_cart", - "begin_checkout", "add_shipping_info", "add_payment_info", "purchase" - ] - - for event in ecommerce_events: - self.generate_tag(event) - - return self.generated_tags - - def generate_engagement_tags(self): - """Generate engagement tracking tags.""" - engagement_events = [ - "form_submit", "form_start", "scroll", "file_download", - "video_start", "video_progress", "video_complete", - "search", "outbound_click", "share" - ] - - for event in engagement_events: - self.generate_tag(event) - - return self.generated_tags - - def save_tags(self, output_path, format="html"): - """Save generated tags to files.""" - import os - - if not self.generated_tags: - return {"error": "No tags generated yet"} - - os.makedirs(output_path, exist_ok=True) - saved_files = [] - - for tag in self.generated_tags: - event = tag["event"] - - if format == "html": - filename = f"{event}_tag.html" - filepath = os.path.join(output_path, filename) - with open(filepath, "w", encoding="utf-8") as f: - f.write(f"\n") - f.write(f"\n") - f.write(f"\n") - if tag.get("trigger_selector"): - f.write(f"\n") - f.write("\n") - f.write(tag["html"]) - saved_files.append(filepath) - - elif format == "json": - filename = f"{event}_tag.json" - filepath = os.path.join(output_path, filename) - with open(filepath, "w", encoding="utf-8") as f: - json.dump(tag, f, indent=2, ensure_ascii=False) - saved_files.append(filepath) - - # Also save a combined file - combined_path = os.path.join(output_path, f"all_tags.{format}") - if format == "html": - with open(combined_path, "w", encoding="utf-8") as f: - f.write("\n") - f.write("\n\n") - for tag in self.generated_tags: - f.write(f"\n\n") - f.write(f"\n") - f.write(f"\n\n") - f.write(tag["html"]) - f.write("\n\n") - else: - with open(combined_path, "w", encoding="utf-8") as f: - json.dump(self.generated_tags, f, indent=2, ensure_ascii=False) - - saved_files.append(combined_path) - - return {"saved_files": saved_files, "count": len(self.generated_tags)} - - def print_tag(self, tag): - """Print a tag to console.""" - print(f"\n{'='*60}") - print(f"📦 {tag['name']}") - print(f" {tag['description']}") - print(f" Trigger: {tag['trigger_recommendation']}") - if tag.get("trigger_selector"): - print(f" Selector: {tag['trigger_selector']}") - print(f"{'='*60}") - print(tag["html"]) - print() - - -def main(): - parser = argparse.ArgumentParser( - description="GTM Manager - Audit and manage Google Tag Manager implementations", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Audit a website - python gtm_manager.py audit --url https://example.com - - # Generate ecommerce dataLayer tags - python gtm_manager.py inject --preset ecommerce --output ./tags - - # Generate tags from audit report - python gtm_manager.py inject --from-audit gtm_audit_report.json - - # Generate specific event tag - python gtm_manager.py inject --event purchase --event add_to_cart - """ - ) - - subparsers = parser.add_subparsers(dest="command", help="Command to run") - - # Audit subcommand - audit_parser = subparsers.add_parser("audit", help="Audit GTM implementation on a URL") - audit_parser.add_argument("--url", required=True, help="Target URL to audit") - audit_parser.add_argument("--container", help="Expected GTM container ID (e.g., GTM-XXXXXX)") - audit_parser.add_argument("--journey", default="full", - choices=["pageview", "scroll", "click", "form", "checkout", "datalayer", "full"], - help="Journey type to simulate") - audit_parser.add_argument("--output", default="gtm_audit_report.json", help="Output file path") - audit_parser.add_argument("--timeout", type=int, default=30000, help="Page load timeout (ms)") - audit_parser.add_argument("--headless", action="store_true", default=True, help="Run headless") - audit_parser.add_argument("--notion", action="store_true", help="Export results to Notion database") - audit_parser.add_argument("--notion-database", help="Notion database ID") - audit_parser.add_argument("--notion-detailed", action="store_true", help="Add detailed content to Notion page") - audit_parser.add_argument("--generate-tags", action="store_true", help="Generate missing dataLayer tags after audit") - - # Inject subcommand - inject_parser = subparsers.add_parser("inject", help="Generate dataLayer injection tags") - inject_parser.add_argument("--event", action="append", help="Event type(s) to generate (can be repeated)") - inject_parser.add_argument("--preset", choices=["ecommerce", "engagement", "all"], - help="Generate preset group of tags") - inject_parser.add_argument("--from-audit", help="Generate tags based on audit report JSON file") - inject_parser.add_argument("--output", default="./gtm_tags", help="Output directory for generated tags") - inject_parser.add_argument("--format", choices=["html", "json"], default="html", help="Output format") - inject_parser.add_argument("--currency", default="KRW", help="Currency code for ecommerce events") - inject_parser.add_argument("--scrape", action="store_true", help="Generate DOM scraping code for values") - inject_parser.add_argument("--list-events", action="store_true", help="List available event types") - - args = parser.parse_args() - - if args.command == "audit": - run_audit(args) - elif args.command == "inject": - run_inject(args) - else: - parser.print_help() - - -def run_audit(args): - """Run GTM audit.""" - auditor = GTMAuditor( - url=args.url, - container_id=args.container, - timeout=args.timeout, - headless=args.headless, - ) - - report = auditor.run_audit(journey=args.journey) - auditor.save_report(args.output) - auditor.print_summary() - - # Generate missing tags if requested - if args.generate_tags: - print("\n🏷️ Generating missing dataLayer tags...") - injector = DataLayerInjector() - result = injector.generate_from_audit(report) - - if result["recommendations"]: - print(f" Found {len(result['recommendations'])} missing event(s)") - for rec in result["recommendations"]: - print(f" - {rec['event']}: {rec['reason']}") - - output_dir = args.output.replace(".json", "_tags") - save_result = injector.save_tags(output_dir) - print(f" Saved {save_result['count']} tags to {output_dir}/") - else: - print(" No missing events detected") - - # Export to Notion if requested - if args.notion: - try: - exporter = NotionExporter(database_id=args.notion_database) - result = exporter.export_report(report, journey_type=args.journey) - - if args.notion_detailed and result.get("page_id"): - exporter.add_detailed_content(result["page_id"], report) - print(" Added detailed content to Notion page") - except Exception as e: - print(f"❌ Notion export failed: {e}") - - -def run_inject(args): - """Run dataLayer injection tag generation.""" - injector = DataLayerInjector(currency=args.currency) - - # List available events - if args.list_events: - print("\n📋 Available Event Types:") - print("=" * 60) - - categories = { - "Ecommerce": ["view_item", "add_to_cart", "remove_from_cart", "view_cart", - "begin_checkout", "add_shipping_info", "add_payment_info", "purchase"], - "Forms & Leads": ["form_submit", "form_start", "generate_lead"], - "Engagement": ["scroll", "file_download", "search", "outbound_click", "share"], - "Video": ["video_start", "video_progress", "video_complete"], - "User": ["login", "sign_up"], - "Page": ["page_view"], - } - - for category, events in categories.items(): - print(f"\n{category}:") - for event in events: - template = DataLayerInjector.EVENT_TEMPLATES.get(event, {}) - desc = template.get("description", "") - print(f" - {event}: {desc}") - - return - - # Generate from audit report - if args.from_audit: - try: - with open(args.from_audit, "r", encoding="utf-8") as f: - report = json.load(f) - - print(f"📊 Analyzing audit report: {args.from_audit}") - result = injector.generate_from_audit(report) - - if result["recommendations"]: - print(f"\n🔍 Found {len(result['recommendations'])} missing event(s):") - for rec in result["recommendations"]: - print(f" - {rec['event']}: {rec['reason']}") - else: - print(" No missing events detected in audit report") - - except FileNotFoundError: - print(f"❌ Audit report not found: {args.from_audit}") - return - except json.JSONDecodeError: - print(f"❌ Invalid JSON in audit report: {args.from_audit}") - return - - # Generate preset groups - elif args.preset: - print(f"📦 Generating {args.preset} preset tags...") - - if args.preset == "ecommerce": - injector.generate_all_ecommerce() - elif args.preset == "engagement": - injector.generate_engagement_tags() - elif args.preset == "all": - injector.generate_all_ecommerce() - injector.generate_engagement_tags() - for event in ["page_view", "login", "sign_up"]: - injector.generate_tag(event) - - # Generate specific events - elif args.event: - print(f"📦 Generating tags for: {', '.join(args.event)}") - - for event in args.event: - options = {"use_scraping": args.scrape} - result = injector.generate_tag(event, options) - if "error" in result: - print(f" ❌ {result['error']}") - else: - injector.print_tag(result) - - else: - print("❌ Please specify --event, --preset, or --from-audit") - print(" Use --list-events to see available event types") - return - - # Save generated tags - if injector.generated_tags: - save_result = injector.save_tags(args.output, format=args.format) - print(f"\n✅ Saved {save_result['count']} tag(s) to {args.output}/") - for filepath in save_result["saved_files"][:5]: - print(f" - {filepath}") - if len(save_result["saved_files"]) > 5: - print(f" ... and {len(save_result['saved_files']) - 5} more") - - -if __name__ == "__main__": - main() diff --git a/custom-skills/61-gtm-manager/code/scripts/requirements.txt b/custom-skills/61-gtm-manager/code/scripts/requirements.txt deleted file mode 100644 index 4777061..0000000 --- a/custom-skills/61-gtm-manager/code/scripts/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -playwright>=1.40.0 diff --git a/custom-skills/61-gtm-manager/code/scripts/setup.sh b/custom-skills/61-gtm-manager/code/scripts/setup.sh deleted file mode 100644 index 0168057..0000000 --- a/custom-skills/61-gtm-manager/code/scripts/setup.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# GTM Audit Tool Setup Script - -echo "🔧 Setting up GTM Audit Tool..." - -# Check Python -if ! command -v python3 &> /dev/null; then - echo "❌ Python 3 is required but not installed." - exit 1 -fi - -echo "✅ Python 3 found" - -# Install dependencies -echo "📦 Installing Python dependencies..." -pip install -r requirements.txt - -# Install Playwright browsers -echo "🌐 Installing Playwright browsers..." -playwright install chromium - -echo "" -echo "✅ Setup complete!" -echo "" -echo "Usage:" -echo " python gtm_audit.py --url 'https://example.com' --journey full" -echo "" -echo "For Claude Code:" -echo " claude" -echo " > Run a GTM audit on https://example.com" diff --git a/custom-skills/61-gtm-manager/gtm_audit_report.json b/custom-skills/61-gtm-manager/gtm_audit_report.json deleted file mode 100644 index 76d6e5e..0000000 --- a/custom-skills/61-gtm-manager/gtm_audit_report.json +++ /dev/null @@ -1,685 +0,0 @@ -{ - "audit_metadata": { - "url": "https://josunhotel.com/resve/dining/step0.do", - "timestamp": "2025-12-20T23:27:54.071984", - "expected_container": null - }, - "container_status": { - "installed": true, - "containers": [ - "GTM-NP6NJMP" - ], - "position": "head", - "noscript_present": true, - "datalayer_initialized": true, - "datalayer_init_before_gtm": false, - "issues": [] - }, - "datalayer_analysis": { - "events": [ - { - "index": 0, - "event": "gtm.js", - "has_ecommerce": false, - "params": [ - "gtm.start", - "event", - "gtm.uniqueEventId" - ] - }, - { - "index": 1, - "event": "gtm.dom", - "has_ecommerce": false, - "params": [ - "event", - "gtm.uniqueEventId" - ] - }, - { - "index": 2, - "event": "gtm.load", - "has_ecommerce": false, - "params": [ - "event", - "gtm.uniqueEventId" - ] - }, - { - "index": 3, - "event": "gtm.scrollDepth", - "has_ecommerce": false, - "params": [ - "event", - "gtm.scrollThreshold", - "gtm.scrollUnits", - "gtm.scrollDirection", - "gtm.triggers", - "gtm.uniqueEventId" - ] - } - ], - "validation_issues": [], - "sequence_issues": [] - }, - "form_analysis": { - "forms_found": [ - { - "index": 0, - "id": "form", - "name": "form", - "action": "https://josunhotel.com/resve/dining/step0.do", - "method": "get", - "className": null, - "fieldCount": 2, - "fields": [ - { - "type": "hidden", - "name": "searchSysCode", - "id": "selectedSysCode", - "required": false, - "placeholder": null, - "validation": null, - "maxLength": null - }, - { - "type": "hidden", - "name": "diningCode", - "id": "selectedDiningCode", - "required": false, - "placeholder": null, - "validation": null, - "maxLength": null - } - ], - "hasSubmitButton": false - }, - { - "index": 1, - "id": "loginForm", - "name": "loginForm", - "action": "https://josunhotel.com/resve/dining/step0.do", - "method": "get", - "className": null, - "fieldCount": 1, - "fields": [ - { - "type": "hidden", - "name": "nextURL", - "id": "nextURL", - "required": false, - "placeholder": null, - "validation": null, - "maxLength": null - } - ], - "hasSubmitButton": false - }, - { - "index": 2, - "id": "diningMenuPopForm", - "name": "diningMenuPopForm", - "action": "https://josunhotel.com/resve/dining/step0.do", - "method": "get", - "className": null, - "fieldCount": 4, - "fields": [ - { - "type": "hidden", - "name": "lclasCode", - "id": "lclasCode", - "required": false, - "placeholder": null, - "validation": null, - "maxLength": null - }, - { - "type": "hidden", - "name": "searchSysCode", - "id": "menuPopSysCode", - "required": false, - "placeholder": null, - "validation": null, - "maxLength": null - }, - { - "type": "hidden", - "name": "diningCode", - "id": "menuPopDiningCode", - "required": false, - "placeholder": null, - "validation": null, - "maxLength": null - }, - { - "type": "hidden", - "name": "diningNm", - "id": "popDiningNm", - "required": false, - "placeholder": null, - "validation": null, - "maxLength": null - } - ], - "hasSubmitButton": false - } - ], - "tracking_issues": [ - { - "type": "form_no_submit", - "form_index": 0, - "message": "Form has no submit button - may use JS submission", - "recommendation": "Verify form submission triggers dataLayer push" - }, - { - "type": "form_no_submit", - "form_index": 1, - "message": "Form has no submit button - may use JS submission", - "recommendation": "Verify form submission triggers dataLayer push" - }, - { - "type": "form_no_submit", - "form_index": 2, - "message": "Form has no submit button - may use JS submission", - "recommendation": "Verify form submission triggers dataLayer push" - } - ], - "events_status": { - "found": [], - "missing": [ - "form_start", - "form_submit", - "generate_lead" - ], - "recommendation": "Consider implementing: form_start, form_submit, generate_lead" - }, - "interactions": [ - { - "action": "focus", - "field": "searchSysCode", - "timestamp": "2025-12-20T23:28:02.063963" - }, - { - "action": "blur", - "field": "searchSysCode", - "timestamp": "2025-12-20T23:28:02.277645" - }, - { - "action": "focus", - "field": "diningCode", - "timestamp": "2025-12-20T23:28:02.284360" - }, - { - "action": "blur", - "field": "diningCode", - "timestamp": "2025-12-20T23:28:02.496949" - } - ] - }, - "checkout_analysis": { - "elements_found": { - "cart": [], - "checkout": [], - "addToCart": [], - "quantity": [], - "removeItem": [], - "promoCode": [] - }, - "events_status": {}, - "flow_issues": [] - }, - "network_requests": [ - { - "destination": "Meta Pixel", - "url": "https://connect.facebook.net/en_US/fbevents.js", - "method": "GET", - "params": {}, - "timestamp": "2025-12-20T23:27:55.520388" - }, - { - "destination": "Meta Pixel", - "url": "https://connect.facebook.net/signals/config/538814301070436?v=2.9.248&r=stable&domain=josunhotel.com&hme=17590b9a2e1b26755cdc9ecb401f9f46bca979d3ccce95d786db0936167af731&ex_m=94%2C156%2C134%2C20%2C67%", - "method": "GET", - "params": { - "v": "2.9.248", - "r": "stable", - "domain": "josunhotel.com", - "hme": "17590b9a2e1b26755cdc9ecb401f9f46bca979d3ccce95d786db0936167af731", - "ex_m": "94,156,134,20,67,68,127,63,43,128,72,62,10,141,80,15,93,28,122,115,70,73,121,138,102,143,7,3,4,6,5,2,81,91,144,224,167,57,226,227,50,183,27,69,232,231,170,30,56,9,59,87,88,89,95,118,29,26,120,117,116,135,71,137,136,45,55,111,14,140,40,213,215,177,23,24,25,17,18,39,35,37,36,76,82,86,100,126,129,41,101,21,19,107,64,33,131,130,132,123,22,32,54,99,139,65,16,133,104,31,193,163,284,211,154,196,189,164,97,119,75,109,49,44,103,42,108,114,53,60,113,48,51,47,90,142,0,112,13,110,11,1,52,83,58,61,106,79,78,145,146,84,85,8,92,46,124,77,74,66,105,96,38,125,34,98,12,147" - }, - "timestamp": "2025-12-20T23:27:55.567831" - }, - { - "destination": "Meta Pixel", - "url": "https://www.facebook.com/tr/?id=538814301070436&ev=PageView&dl=https%3A%2F%2Fjosunhotel.com%2Fresve%2Fdining%2Fstep0.do&rl=&if=false&ts=1766240875594&sw=1920&sh=1080&v=2.9.248&r=stable&ec=0&o=4126&fbp", - "method": "GET", - "params": { - "id": "538814301070436", - "ev": "PageView", - "dl": "https://josunhotel.com/resve/dining/step0.do", - "if": "false", - "ts": "1766240875594", - "sw": "1920", - "sh": "1080", - "v": "2.9.248", - "r": "stable", - "ec": "0", - "o": "4126", - "fbp": "fb.1.1766240875591.938184183721303390", - "cs_est": "true", - "ler": "empty", - "plt": "403.89999985694885", - "it": "1766240875565", - "coo": "false", - "expv2[0]": "pl0", - "expv2[1]": "el2", - "expv2[2]": "bc1", - "expv2[3]": "mr2", - "expv2[4]": "im1", - "rqm": "GET" - }, - "timestamp": "2025-12-20T23:27:55.597631" - }, - { - "destination": "GA4", - "url": "https://analytics.google.com/g/collect?v=2&tid=G-H05VHCL3NE>m=45je5ca1v899119784z8836519946za20gzb836519946zd836519946&_p=1766240875171&_gaz=1&gcd=13l3l3l3l1l1&npa=0&dma=0&cid=702909924.1766240876&u", - "method": "POST", - "params": { - "v": "2", - "tid": "G-H05VHCL3NE", - "gtm": "45je5ca1v899119784z8836519946za20gzb836519946zd836519946", - "_p": "1766240875171", - "_gaz": "1", - "gcd": "13l3l3l3l1l1", - "npa": "0", - "dma": "0", - "cid": "702909924.1766240876", - "ul": "en-us", - "sr": "1920x1080", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "are": "1", - "frm": "0", - "pscdl": "noapi", - "_s": "1", - "tag_exp": "103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115938466~115938469~116184927~116184929~116251938~116251940", - "dp": "/resve/dining/step0.do", - "sid": "1766240875", - "sct": "1", - "seg": "0", - "dl": "https://josunhotel.com/resve/dining/step0.do", - "en": "page_view", - "_fv": "1", - "_nsi": "1", - "_ss": "1", - "ep.login_status": "비로그인", - "ep.user_lang": "ko", - "ep.contents_group_hotel": "조선호텔앤리조트", - "ep.contents_group_menu": "다이닝 이용", - "ep.contents_group_update": "다이닝 예약 프로세스", - "up.user_lang": "ko", - "up.login_status": "비로그인", - "tfd": "816" - }, - "timestamp": "2025-12-20T23:27:55.700854" - }, - { - "destination": "Google Ads", - "url": "https://googleads.g.doubleclick.net/pagead/viewthroughconversion/10876516957/?random=1766240875706&cv=11&fst=1766240875706&bg=ffffff&guid=ON&async=1&en=gtag.config>m=45be5ca1v887509085z8836519946za2", - "method": "GET", - "params": { - "random": "1766240875706", - "cv": "11", - "fst": "1766240875706", - "bg": "ffffff", - "guid": "ON", - "async": "1", - "en": "gtag.config", - "gtm": "45be5ca1v887509085z8836519946za20gzb836519946zd836519946xec", - "gcd": "13l3l3l3l1l1", - "dma": "0", - "tag_exp": "103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115616986~115938466~115938469~116184927~116184929~116251938~116251940", - "u_w": "1920", - "u_h": "1080", - "url": "https://josunhotel.com/resve/dining/step0.do", - "frm": "0", - "hn": "www.googleadservices.com", - "npa": "0", - "pscdl": "noapi", - "auid": "7778758.1766240876", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "data": "event=gtag.config", - "rfmt": "3", - "fmt": "4" - }, - "timestamp": "2025-12-20T23:27:55.724178" - }, - { - "destination": "Google Ads", - "url": "https://googleads.g.doubleclick.net/pagead/viewthroughconversion/10876516957/?random=1766240875722&cv=11&fst=1766240875722&bg=ffffff&guid=ON&async=1>m=45be5ca1v887509085z8836519946za20gzb836519946zd", - "method": "GET", - "params": { - "random": "1766240875722", - "cv": "11", - "fst": "1766240875722", - "bg": "ffffff", - "guid": "ON", - "async": "1", - "gtm": "45be5ca1v887509085z8836519946za20gzb836519946zd836519946xea", - "gcd": "13l3l3l3l1l1", - "dma": "0", - "tag_exp": "103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115616986~115938466~115938469~116184927~116184929~116251938~116251940", - "u_w": "1920", - "u_h": "1080", - "url": "https://josunhotel.com/resve/dining/step0.do", - "frm": "0", - "hn": "www.googleadservices.com", - "npa": "0", - "pscdl": "noapi", - "auid": "7778758.1766240876", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "_tu": "CA", - "rfmt": "3", - "fmt": "4" - }, - "timestamp": "2025-12-20T23:27:55.724380" - }, - { - "destination": "GA4", - "url": "https://analytics.google.com/g/collect?v=2&tid=G-TRL4XSZ1TD>m=45je5ca1v887683366z8836519946za20gzb836519946zd836519946&_p=1766240875171&em=tv.1~em.3UiOxkIGtQ4owYdw7yAOBB9qepQm5hB4t5octJYCI7g&_gaz=1&", - "method": "POST", - "params": { - "v": "2", - "tid": "G-TRL4XSZ1TD", - "gtm": "45je5ca1v887683366z8836519946za20gzb836519946zd836519946", - "_p": "1766240875171", - "em": "tv.1~em.3UiOxkIGtQ4owYdw7yAOBB9qepQm5hB4t5octJYCI7g", - "_gaz": "1", - "gcd": "13l3l3l3l1l1", - "npa": "0", - "dma": "0", - "cid": "702909924.1766240876", - "ecid": "599478978", - "ul": "en-us", - "sr": "1920x1080", - "ir": "1", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "are": "1", - "frm": "0", - "pscdl": "noapi", - "ec_mode": "a", - "_eu": "EA", - "_s": "1", - "tag_exp": "102015665~103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115616986~115938466~115938469~116184927~116184929~116251938~116251940", - "dp": "/resve/dining/step0.do", - "sid": "1766240875", - "sct": "1", - "seg": "0", - "dl": "https://josunhotel.com/resve/dining/step0.do", - "en": "page_view", - "_fv": "1", - "_ss": "1", - "ep.login_status": "비로그인", - "ep.user_lang": "ko", - "ep.contents_group_hotel": "조선호텔앤리조트", - "ep.contents_group_menu": "다이닝 이용", - "ep.contents_group_update": "다이닝 예약 프로세스", - "up.user_lang": "ko", - "up.login_status": "비로그인", - "tfd": "868" - }, - "timestamp": "2025-12-20T23:27:55.742300" - }, - { - "destination": "Google Ads", - "url": "https://www.google.com/pagead/1p-user-list/10876516957/?random=1766240875722&cv=11&fst=1766239200000&bg=ffffff&guid=ON&async=1>m=45be5ca1v887509085z8836519946za20gzb836519946zd836519946xea&gcd=13l3l", - "method": "GET", - "params": { - "random": [ - "1766240875722", - "1725248437" - ], - "cv": "11", - "fst": "1766239200000", - "bg": "ffffff", - "guid": "ON", - "async": "1", - "gtm": "45be5ca1v887509085z8836519946za20gzb836519946zd836519946xea", - "gcd": "13l3l3l3l1l1", - "dma": "0", - "tag_exp": "103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115616986~115938466~115938469~116184927~116184929~116251938~116251940", - "u_w": "1920", - "u_h": "1080", - "url": "https://josunhotel.com/resve/dining/step0.do", - "frm": "0", - "hn": "www.googleadservices.com", - "npa": "0", - "pscdl": "noapi", - "auid": "7778758.1766240876", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "_tu": "CA", - "rfmt": "3", - "fmt": "3", - "is_vtc": "1", - "cid": "CAQSfwDCSxrROzP7e5XZEQS8k7I9UCNrhQnG5V-tD4ZKQa_7FtJSznqwNhHyDNj7BO_ZAQ0ZmiPlxWqmbyGxMvHSi61fZhkyzwsqEi7B9tTXs6sacAba88_-qgv_8Rgn4jQGuJL6VEJJrTfyPEzvb8DkpSBnvoX3GsuNT06ie6uSVWc", - "rmt_tld": "0", - "ipr": "y" - }, - "timestamp": "2025-12-20T23:27:55.988316" - }, - { - "destination": "Google Ads", - "url": "https://www.google.com/pagead/1p-user-list/10876516957/?random=1766240875706&cv=11&fst=1766239200000&bg=ffffff&guid=ON&async=1&en=gtag.config>m=45be5ca1v887509085z8836519946za20gzb836519946zd8365199", - "method": "GET", - "params": { - "random": [ - "1766240875706", - "47137349" - ], - "cv": "11", - "fst": "1766239200000", - "bg": "ffffff", - "guid": "ON", - "async": "1", - "en": "gtag.config", - "gtm": "45be5ca1v887509085z8836519946za20gzb836519946zd836519946xec", - "gcd": "13l3l3l3l1l1", - "dma": "0", - "tag_exp": "103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115616986~115938466~115938469~116184927~116184929~116251938~116251940", - "u_w": "1920", - "u_h": "1080", - "url": "https://josunhotel.com/resve/dining/step0.do", - "frm": "0", - "hn": "www.googleadservices.com", - "npa": "0", - "pscdl": "noapi", - "auid": "7778758.1766240876", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "data": "event=gtag.config", - "rfmt": "3", - "fmt": "3", - "is_vtc": "1", - "cid": "CAQSfwDCSxrRlXV2juBCOX2pHU5qySObf1Ig-AD_NFppjlwlkvd1cQpXbh1i_ciiVUQiaEEtKY-q8RxtPsLk-6nSWigi7Rpe09oM62SpgggyFvUyqtM2ShCyr_w5AaTOI3665_Bn3q954YNmzAevWsWo3YNoUYN6S0r2iZE7jyFuHWQ", - "rmt_tld": "0", - "ipr": "y" - }, - "timestamp": "2025-12-20T23:27:55.990657" - }, - { - "destination": "GA4", - "url": "https://analytics.google.com/g/collect?v=2&tid=G-H05VHCL3NE>m=45je5ca1v899119784za20gzb836519946zd836519946&_p=1766240875171&gcd=13l3l3l3l1l1&npa=0&dma=0&cid=702909924.1766240876&ul=en-us&sr=1920x10", - "method": "POST", - "params": { - "v": "2", - "tid": "G-H05VHCL3NE", - "gtm": "45je5ca1v899119784za20gzb836519946zd836519946", - "_p": "1766240875171", - "gcd": "13l3l3l3l1l1", - "npa": "0", - "dma": "0", - "cid": "702909924.1766240876", - "ul": "en-us", - "sr": "1920x1080", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "are": "1", - "frm": "0", - "pscdl": "noapi", - "_eu": "AEAAAAQ", - "_s": "2", - "tag_exp": "103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115938466~115938469~116184927~116184929~116251938~116251940", - "dp": "/resve/dining/step0.do", - "sid": "1766240875", - "sct": "1", - "seg": "0", - "dl": "https://josunhotel.com/resve/dining/step0.do", - "_tu": "CA", - "en": "scroll", - "ep.login_status": "비로그인", - "ep.user_lang": "ko", - "ep.contents_group_hotel": "조선호텔앤리조트", - "ep.contents_group_menu": "다이닝 이용", - "ep.contents_group_update": "다이닝 예약 프로세스", - "epn.percent_scrolled": "90", - "_et": "3343", - "tfd": "9162" - }, - "timestamp": "2025-12-20T23:28:04.037363" - }, - { - "destination": "GA4", - "url": "https://analytics.google.com/g/collect?v=2&tid=G-TRL4XSZ1TD>m=45je5ca1v887683366za20gzb836519946zd836519946&_p=1766240875171&gcd=13l3l3l3l1l1&npa=0&dma=0&cid=702909924.1766240876&ecid=599478978&ul=e", - "method": "POST", - "params": { - "v": "2", - "tid": "G-TRL4XSZ1TD", - "gtm": "45je5ca1v887683366za20gzb836519946zd836519946", - "_p": "1766240875171", - "gcd": "13l3l3l3l1l1", - "npa": "0", - "dma": "0", - "cid": "702909924.1766240876", - "ecid": "599478978", - "ul": "en-us", - "sr": "1920x1080", - "ir": "1", - "uaa": "x64", - "uab": "64", - "uafvl": "Chromium;136.0.7103.25|HeadlessChrome;136.0.7103.25|Not.A%2FBrand;99.0.0.0", - "uamb": "0", - "uap": "macOS", - "uapv": "10_15_7", - "uaw": "0", - "are": "1", - "frm": "0", - "pscdl": "noapi", - "_eu": "EEAAAAQ", - "_s": "2", - "tag_exp": "102015665~103116026~103200004~104527906~104528501~104684208~104684211~105391253~115583767~115616986~115938466~115938469~116184927~116184929~116251938~116251940", - "dp": "/resve/dining/step0.do", - "sid": "1766240875", - "sct": "1", - "seg": "0", - "dl": "https://josunhotel.com/resve/dining/step0.do", - "_tu": "CA", - "en": "scroll", - "ep.login_status": "비로그인", - "ep.user_lang": "ko", - "ep.contents_group_hotel": "조선호텔앤리조트", - "ep.contents_group_menu": "다이닝 이용", - "ep.contents_group_update": "다이닝 예약 프로세스", - "epn.percent_scrolled": "90", - "_et": "3298", - "tfd": "9166" - }, - "timestamp": "2025-12-20T23:28:04.040013" - } - ], - "tags_fired": [ - "GA4", - "Google Ads", - "Meta Pixel" - ], - "issues": [ - { - "severity": "warning", - "type": "datalayer_order", - "message": "dataLayer should be initialized before GTM script" - } - ], - "recommendations": [ - { - "priority": "medium", - "action": "Initialize dataLayer before GTM", - "details": "Add 'window.dataLayer = window.dataLayer || [];' before GTM" - }, - { - "priority": "medium", - "action": "Implement form tracking events", - "details": "Missing events: form_start, form_submit, generate_lead" - } - ], - "checklist": { - "container_health": { - "gtm_installed": true, - "correct_container": true, - "no_duplicates": true, - "correct_position": true, - "datalayer_init_order": false - }, - "datalayer_quality": { - "initialized": true, - "events_present": true, - "no_validation_errors": true, - "correct_sequence": true - }, - "form_tracking": { - "forms_identifiable": true, - "form_events_present": false - }, - "tag_firing": { - "ga4_active": true, - "requests_captured": true - } - } -} \ No newline at end of file diff --git a/custom-skills/62-gtm-guardian/README.md b/custom-skills/62-gtm-guardian/README.md deleted file mode 100644 index 0cd5ee2..0000000 --- a/custom-skills/62-gtm-guardian/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# GTM Guardian - -Google Tag Manager 태깅 전략 수립부터 구현, 유지보수까지 7단계 워크플로우를 지원하는 SMART Tool. - -## Overview - -GTM Guardian은 GTM 태깅의 전체 라이프사이클을 체계적으로 관리합니다: - -| Phase | Name | Description | -|-------|------|-------------| -| 1 | Web/App Analysis | DOM 분석, 태깅 가능 요소 식별 | -| 2 | User Behaviour & KPI | 사용자 여정 분석, KPI 정의 | -| 3 | Tagging Plan | 맞춤 이벤트, 파라미터 설계 | -| 4 | Event Taxonomy | 이벤트 택소노미 시트 구조화 | -| 5 | Implementation Guide | 구현 순서, 개발자 가이드 | -| 6 | Progressive Audit | 주기적 진단 및 업데이트 | -| 7 | Lookup App | 이벤트 조회 웹앱 | - -## Dual-Platform Structure - -이 스킬은 Claude Desktop과 Claude Code 양쪽에서 사용 가능합니다: - -``` -62-gtm-guardian/ -├── desktop/ # Claude Desktop 버전 -│ ├── SKILL.md # MCP 기반 워크플로우 (Phase 1-5) -│ ├── references/ # 분석/설계 가이드 -│ └── templates/ # 태깅 계획서, 택소노미 템플릿 -│ -├── code/ # Claude Code 버전 -│ ├── CLAUDE.md # 자동화 및 유지보수 (Phase 6-7) -│ ├── scripts/ # GTM Toolkit 연계 -│ └── references/ # 감사/앱 가이드 -│ -└── README.md -``` - -### Platform 역할 분담 - -| Platform | Phases | Focus | -|----------|--------|-------| -| **Desktop** | 1-5 | 분석, 설계, 문서 작성, Browser 분석, Notion 연동 | -| **Code** | 6-7 | GTM Toolkit 연계, Python 스크립트, API 자동화 | - -## Quick Start - -### Claude Desktop -1. `desktop/SKILL.md`를 Claude Desktop 설정에 추가 -2. "GTM 태깅 계획 작성해줘" 등으로 시작 -3. Phase 1-5 워크플로우 자동 진행 - -### Claude Code -1. `code/CLAUDE.md` 디렉토리에서 작업 -2. D.intel GTM Toolkit 클론 -3. Phase 6-7 자동화 작업 수행 - -## Key Features - -### DataLayer & Events -- GA4 권장 이벤트 기반 스키마 -- GA4, Meta, Kakao, Google Ads 플랫폼 매핑 -- 일관된 네이밍 규칙 - -### Server-side GTM -- GCP Cloud Run, Stape.io, Google Tag Gateway 옵션 -- 1st-party 쿠키, Privacy 강화 - -### Automation -- D.intelligence GTM Toolkit 연계 -- Container 분석, 버전 비교, 미사용 요소 탐지 -- Notion 자동 저장 - -## Site Type Support - -| Site Type | Key Events | -|-----------|------------| -| E-commerce | view_item, add_to_cart, purchase | -| Lead Gen | generate_lead, form_submit | -| SaaS | sign_up, subscription_* | -| Media | article_view, scroll, video_* | -| B2B | demo_request, pricing_view | - -## Related Resources - -- [D.intelligence GTM Toolkit](https://github.com/ourdigital/dintel-gtm-agent) -- [GTM Knowledge Base (Notion)](https://www.notion.so/dintelligence/2cf581e58a1e80c8b358f1625356e931) - -## Triggers - -이 스킬은 다음 키워드에 반응합니다: - -- GTM 태깅 계획, Tagging Plan 작성, 이벤트 설계 -- DataLayer 스키마 설계, dataLayer 구현 가이드 -- 사용자 행동 분석, User Behaviour Modeling, KPI 정의 -- Event Taxonomy, 이벤트 택소노미, 이벤트 명명 규칙 -- GTM 구현 가이드, 태그 설정 체크리스트 -- Server-side GTM, sGTM, Stape -- GTM 감사, 진단, audit diff --git a/custom-skills/62-gtm-guardian/code/CLAUDE.md b/custom-skills/62-gtm-guardian/code/CLAUDE.md deleted file mode 100644 index c0ed512..0000000 --- a/custom-skills/62-gtm-guardian/code/CLAUDE.md +++ /dev/null @@ -1,168 +0,0 @@ -# GTM Guardian (Code) - -GTM 태깅 라이프사이클의 자동화 및 유지보수 단계를 지원하는 Claude Code 스킬. - -**Code 범위**: Phase 6-7 (Progressive Audit, Lookup App), GTM Toolkit 연계 - -> Desktop 스킬에서 Phase 1-5 (분석, 설계, 문서화)를 수행한 후 사용. - -## Quick Commands - -```bash -# D.intelligence GTM Toolkit 클론 -git clone https://github.com/ourdigital/dintel-gtm-agent.git - -# GTM Container 분석 -python analyze_container.py GTM-XXXXXX.json --output report.md - -# 버전 비교 -python diff_versions.py v1.json v2.json --output diff.md - -# 미사용 요소 탐지 -python find_unused.py container.json --type all -``` - -## Workflow Overview - -| Phase | Name | Output | Tools | -|-------|------|--------|-------| -| 6 | Progressive Audit | 진단 리포트, 업데이트 | D.intel GTM Toolkit, Python | -| 7 | Lookup App | 이벤트 조회 앱 | Google Apps Script | - -## Phase 6: Progressive Audit - -### D.intelligence GTM Toolkit Integration - -**Repository**: https://github.com/ourdigital/dintel-gtm-agent - -### Capabilities - -| Feature | Description | Use Case | -|---------|-------------|----------| -| Container Analysis | JSON 파싱, 구조 분석 | 컨테이너 리뷰 | -| Dependency Mapping | 태그↔트리거↔변수 관계 | 영향도 분석 | -| Version Diff | 버전 간 변경사항 비교 | 변경 추적 | -| Tag Validation | 발화 상태 자동 검증 | 정기 감사 | - -### Audit Workflow - -``` -1. GTM Container Export - └── JSON 파일 다운로드 (GTM Admin > Container > Export) - -2. D.intel Toolkit Analysis - ├── python analyze_container.py container.json - ├── Dependency graph 생성 - └── Issue detection - -3. Report Generation - └── Markdown/HTML 리포트 - -4. Notion 업로드 - └── MCP 또는 수동 업로드 -``` - -### Audit Schedule - -| Frequency | Scope | Trigger | -|-----------|-------|---------| -| Weekly | Tag firing validation | 자동 스케줄 | -| Monthly | Full container review | 월간 리포트 | -| Quarterly | Architecture review | 분기 회의 | -| Ad-hoc | Issue investigation | 이슈 발생 시 | - -## Phase 7: Lookup App - -Google Apps Script 기반 Event Taxonomy 조회 앱 배포. - -### Architecture -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Google Sheets │────▶│ Apps Script │────▶│ Web App │ -│ (Taxonomy DB) │ │ (Backend) │ │ (Frontend) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - -### Quick Setup -1. Google Sheets에서 Extensions > Apps Script -2. `references/phase7-lookup-app.md` 코드 참조 -3. Deploy > New deployment > Web app - -## Key References - -- [phase6-audit.md](references/phase6-audit.md) - 진단 워크플로우, 체크리스트 -- [phase7-lookup-app.md](references/phase7-lookup-app.md) - Apps Script 코드, 배포 가이드 - -### Shared References -- [datalayer-schema.md](references/datalayer-schema.md) - DataLayer 스키마 -- [platform-mapping.md](references/platform-mapping.md) - 플랫폼별 이벤트 매핑 -- [naming-conventions.md](references/naming-conventions.md) - 네이밍 규칙 -- [qa-checklist.md](references/qa-checklist.md) - QA 체크리스트 - -## Notion Output - -**Database**: [GTM Knowledge Base](https://www.notion.so/dintelligence/2cf581e58a1e80c8b358f1625356e931) - -### Report Properties - -| Property | Type | Description | -|----------|------|-------------| -| Project | Title | 프로젝트명 | -| Audit Date | Date | 감사 일자 | -| Container ID | Rich Text | GTM-XXXXXX | -| Status | Select | Healthy / Needs Attention / Critical | -| Version | Number | 컨테이너 버전 | -| Issues Count | Number | 발견 이슈 수 | - -### Language Guidelines -- 리포트 내용은 한국어로 작성 -- 기술 용어는 영어 유지 (GTM, dataLayer, GA4 등) -- 코드와 URL은 변경 없이 유지 - -## Common Tasks - -### Container 분석 실행 -```bash -cd dintel-gtm-agent -python analyze_container.py /path/to/GTM-XXXXXX.json \ - --output /path/to/report.md \ - --format markdown -``` - -### 버전 비교 -```bash -python diff_versions.py \ - /path/to/container_v1.json \ - /path/to/container_v2.json \ - --output diff_report.md -``` - -### 미사용 요소 탐지 -```bash -python find_unused.py /path/to/container.json \ - --type all \ - --output unused_elements.md -``` - -## Update Procedures - -### Minor Update (태그 수정) -1. 변경 필요 태그 식별 -2. GTM Preview 모드에서 수정 -3. 테스트 완료 -4. Publish with version note -5. Notion 문서 업데이트 - -### Major Update (신규 태그 추가) -1. Desktop 스킬로 Phase 3-4 문서 업데이트 -2. DataLayer 요구사항 개발팀 전달 -3. GTM 태그/트리거/변수 설정 -4. QA 체크리스트 수행 -5. 단계적 배포 - -## Coding Guidelines - -GTM Toolkit 수정 시: -1. **Tag Destinations**: `TAG_DESTINATIONS` dict에 플랫폼 추가 -2. **Event Validation**: `GA4_EVENT_REQUIREMENTS` dict에 요구사항 추가 -3. **Korean Patterns**: 한국 결제 수단 (카카오페이, 네이버페이, 토스) 지원 diff --git a/custom-skills/62-gtm-guardian/code/references/datalayer-schema.md b/custom-skills/62-gtm-guardian/code/references/datalayer-schema.md deleted file mode 100644 index 2ec93bd..0000000 --- a/custom-skills/62-gtm-guardian/code/references/datalayer-schema.md +++ /dev/null @@ -1,193 +0,0 @@ -# DataLayer Schema Reference - -GA4 권장 이벤트 기반 DataLayer 스키마 정의. - -## Core Principles - -1. GTM 스니펫 이전에 dataLayer 초기화 -2. 이벤트 발생 시점에 push (사전 아님) -3. ecommerce 객체는 push 전 clear 필수 -4. PII 데이터 직접 수집 금지 - -## Base Structure - -### Page Load -```javascript -window.dataLayer = window.dataLayer || []; -dataLayer.push({ - 'event': 'page_data', - 'page': { - 'type': 'home|category|product|cart|checkout|thankyou|content|search', - 'name': 'Page Name', - 'category': 'Category/Subcategory', - 'language': 'ko' - }, - 'user': { - 'status': 'logged_in|logged_out', - 'id': 'hashed_user_id', // SHA256, 로그인 시에만 - 'type': 'new|returning|member|guest', - 'loginMethod': 'email|kakao|naver|google' - } -}); -``` - -## E-commerce Events - -### view_item -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - 'event': 'view_item', - 'ecommerce': { - 'currency': 'KRW', - 'value': 29000, - 'items': [{ - 'item_id': 'SKU_12345', - 'item_name': 'Product Name', - 'item_brand': 'Brand Name', - 'item_category': 'Category', - 'item_variant': 'Blue / Large', - 'price': 29000, - 'quantity': 1 - }] - } -}); -``` - -### add_to_cart -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - 'event': 'add_to_cart', - 'ecommerce': { - 'currency': 'KRW', - 'value': 29000, - 'items': [{ - 'item_id': 'SKU_12345', - 'item_name': 'Product Name', - 'price': 29000, - 'quantity': 1 - }] - } -}); -``` - -### begin_checkout -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - 'event': 'begin_checkout', - 'ecommerce': { - 'currency': 'KRW', - 'value': 58000, - 'coupon': 'WELCOME10', - 'items': [/* cart items */] - } -}); -``` - -### purchase -```javascript -dataLayer.push({ ecommerce: null }); -dataLayer.push({ - 'event': 'purchase', - 'ecommerce': { - 'transaction_id': 'T_20250115_001', - 'affiliation': 'Store Name', - 'value': 53000, - 'tax': 4818, - 'shipping': 3000, - 'currency': 'KRW', - 'coupon': 'WELCOME10', - 'items': [{ - 'item_id': 'SKU_12345', - 'item_name': 'Product Name', - 'price': 29000, - 'quantity': 1 - }] - } -}); -``` - -## Lead Gen Events - -### generate_lead -```javascript -dataLayer.push({ - 'event': 'generate_lead', - 'currency': 'KRW', - 'value': 50000, - 'lead': { - 'source': 'contact_form|landing_page', - 'type': 'inquiry|quote_request' - } -}); -``` - -### form_submit -```javascript -dataLayer.push({ - 'event': 'form_submit', - 'form': { - 'id': 'contact_form', - 'name': 'Contact Us Form', - 'type': 'contact', - 'success': true - } -}); -``` - -## Engagement Events - -### login / sign_up -```javascript -dataLayer.push({ - 'event': 'login', // or 'sign_up' - 'method': 'email|kakao|naver|google' -}); -``` - -### search -```javascript -dataLayer.push({ - 'event': 'search', - 'search_term': 'user search query', - 'search': { - 'results_count': 42, - 'category': 'product|content' - } -}); -``` - -## Item Object Reference - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| item_id | string | Yes | SKU | -| item_name | string | Yes | 상품명 | -| price | number | Yes | 단가 | -| quantity | number | Yes | 수량 | -| item_brand | string | No | 브랜드명 | -| item_category | string | No | 카테고리 | -| item_category2-5 | string | No | 하위 카테고리 | -| item_variant | string | No | 옵션 | -| coupon | string | No | 쿠폰 코드 | -| discount | number | No | 할인 금액 | -| index | number | No | 목록 위치 | - -## Validation Rules - -```javascript -// ✅ Correct -'price': 29000 // number -'quantity': 1 // number -'currency': 'KRW' // uppercase - -// ❌ Wrong -'price': '29000' // string -'currency': 'krw' // lowercase -``` - -### Array Limits -- items[]: 최대 200개 -- 초과 시 분할 전송 diff --git a/custom-skills/62-gtm-guardian/code/references/naming-conventions.md b/custom-skills/62-gtm-guardian/code/references/naming-conventions.md deleted file mode 100644 index 76129d2..0000000 --- a/custom-skills/62-gtm-guardian/code/references/naming-conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# GTM Naming Conventions - -일관된 GTM 컨테이너 관리를 위한 네이밍 규칙. - -## Tag Naming - -### Pattern -``` -[Platform] - [Type] - [Description] -``` - -### Examples -| Platform | Type | Description | Tag Name | -|----------|------|-------------|----------| -| GA4 | Config | Main | GA4 - Config - Main | -| GA4 | Event | purchase | GA4 - Event - purchase | -| GAds | Conversion | Purchase | GAds - Conversion - Purchase | -| Meta | Event | Purchase | Meta - Event - Purchase | -| Kakao | Event | purchase | Kakao - Event - purchase | - -## Trigger Naming - -### Pattern -``` -[Type] - [Condition] - [Context] -``` - -### Type Codes -| Code | Type | Example | -|------|------|---------| -| PV | Page View | PV - All Pages | -| DL | DataLayer Event | DL - purchase | -| Click | Click | Click - CTA Button | -| Form | Form Submit | Form - Contact | -| Scroll | Scroll Depth | Scroll - 75% | -| Vis | Element Visibility | Vis - Video Player | - -### Examples -``` -PV - All Pages -PV - Thank You Page -DL - purchase -DL - add_to_cart -Click - CTA Button - Hero -Form - Contact Form -Scroll - 90% -``` - -## Variable Naming - -### Pattern -``` -[Type] - [Name/Path] -``` - -### Type Codes -| Code | Type | Example | -|------|------|---------| -| DLV | DataLayer Variable | DLV - ecommerce.value | -| CJS | Custom JavaScript | CJS - Item IDs Array | -| Const | Constant | Const - GA4 Measurement ID | -| LT | Lookup Table | LT - Page Type Mapping | -| URL | URL Variable | URL - Path | -| DOM | DOM Element | DOM - Product Title | - -### Examples -``` -DLV - event -DLV - ecommerce.transaction_id -DLV - ecommerce.items -CJS - Items for Meta -CJS - Formatted Price -Const - GA4 Measurement ID -Const - Meta Pixel ID -LT - Payment Method Mapping -``` - -## Folder Structure - -``` -📁 01. Configuration -📁 02. GA4 Events -📁 03. Google Ads -📁 04. Meta Pixel -📁 05. Kakao Pixel -📁 06. Other Platforms -📁 07. Utilities -📁 99. Deprecated -``` - -## Built-in Variables to Enable - -``` -Pages: ✅ Page URL, Path, Hostname, Referrer -Utilities: ✅ Event, Container ID, Random Number -Clicks: ✅ Element, Classes, ID, URL, Text -Forms: ✅ Element, Classes, ID, URL, Text -History: ✅ New/Old History Fragment/State -Videos: ✅ All YouTube variables -Scrolling: ✅ Depth Threshold, Units, Direction -Visibility: ✅ Percent Visible, On-Screen Duration -``` - -## Anti-patterns - -``` -❌ Tag 1, New Tag, Test -❌ GA4 purchase event (일관성 없음) -❌ G4-E-Pch (과도한 약어) -❌ Very Long Tag Name for Purchase Complete Event (너무 김) - -✅ GA4 - Event - purchase -✅ DL - purchase -✅ CJS - Items for Meta -``` - -## Version Notes - -``` -v[Major].[Minor] - [Description] -예: v12.1 - Added form_submit event - -## Changes -- Added: -- Modified: -- Removed: - -## Reason -[변경 이유] -``` diff --git a/custom-skills/62-gtm-guardian/code/references/phase6-audit.md b/custom-skills/62-gtm-guardian/code/references/phase6-audit.md deleted file mode 100644 index b2c4eb2..0000000 --- a/custom-skills/62-gtm-guardian/code/references/phase6-audit.md +++ /dev/null @@ -1,215 +0,0 @@ -# Phase 6: Progressive Audit & Update - -설정된 GTM Container에 대한 주기적 진단, 유효성 검증, 마이너 업데이트 및 태그 추가. - -## Objectives - -1. GTM 컨테이너 건강 상태 점검 -2. 태그 발화 및 데이터 정합성 검증 -3. 최신 요구사항 반영 업데이트 -4. D.intelligence GTM Toolkit 연계 자동화 - -## Audit Schedule - -| Frequency | Scope | Trigger | -|-----------|-------|---------| -| Weekly | Tag firing validation | 자동 스케줄 | -| Monthly | Full container review | 월간 리포트 | -| Quarterly | Architecture review | 분기 회의 | -| Ad-hoc | Issue investigation | 이슈 발생 시 | - -## Audit Checklist - -### 1. Container Health Check -- [ ] 버전 히스토리 확인 (최근 변경사항) -- [ ] 미사용 태그/트리거/변수 식별 -- [ ] 명명 규칙 준수 여부 -- [ ] 폴더 구조 정리 상태 - -### 2. Tag Firing Validation -- [ ] 모든 P1 태그 정상 발화 -- [ ] 트리거 조건 정확성 -- [ ] 변수 값 정확성 -- [ ] 차단 트리거 작동 - -### 3. Data Quality -- [ ] GA4 Realtime 데이터 확인 -- [ ] 전환 데이터 정합성 -- [ ] 파라미터 값 검증 -- [ ] 이상치 탐지 - -### 4. Platform Sync -- [ ] GA4 ↔ GTM 동기화 -- [ ] Google Ads 전환 데이터 -- [ ] Meta Events Manager 상태 -- [ ] Kakao Pixel 상태 - -## D.intelligence GTM Toolkit Integration - -### Repository -``` -https://github.com/ourdigital/dintel-gtm-agent -``` - -### Capabilities - -| Feature | Description | Use Case | -|---------|-------------|----------| -| Container Analysis | JSON 파싱, 구조 분석 | 컨테이너 리뷰 | -| Dependency Mapping | 태그↔트리거↔변수 관계 | 영향도 분석 | -| Version Diff | 버전 간 변경사항 비교 | 변경 추적 | -| Tag Validation | 발화 상태 자동 검증 | 정기 감사 | - -### Integration Workflow - -``` -1. GTM Container Export - └── JSON 파일 다운로드 - -2. D.intel Toolkit Analysis - ├── python analyze_container.py container.json - ├── Dependency graph 생성 - └── Issue detection - -3. Report Generation - └── Markdown/HTML 리포트 - -4. GTM Guardian Update - └── Notion에 리포트 저장 -``` - -### Sample Toolkit Commands - -```bash -# Container 분석 -python analyze_container.py GTM-XXXXXX.json --output report.md - -# 버전 비교 -python diff_versions.py v1.json v2.json --output diff.md - -# 태그 검증 -python validate_tags.py container.json --events events.csv - -# 미사용 요소 탐지 -python find_unused.py container.json --type all -``` - -## Audit Report Template - -```markdown -# GTM Audit Report - -## Summary -- **Container**: GTM-XXXXXX -- **Audit Date**: YYYY-MM-DD -- **Version Reviewed**: XX -- **Overall Status**: ✅ Healthy / ⚠️ Needs Attention / ❌ Critical - -## Findings - -### ✅ Passing -- [항목] - -### ⚠️ Warnings -| Issue | Impact | Recommendation | -|-------|--------|----------------| - -### ❌ Critical -| Issue | Impact | Action Required | -|-------|--------|-----------------| - -## Tag Firing Status - -| Tag | Expected | Actual | Status | -|-----|----------|--------|--------| -| GA4 - Event - purchase | Thank You | Thank You | ✅ | - -## Data Quality Check - -| Metric | Expected | Actual | Variance | -|--------|----------|--------|----------| -| Daily Purchases | ~50 | 48 | -4% | - -## Recommendations -1. -2. - -## Next Steps -- [ ] -- [ ] - -## Appendix -- Container JSON snapshot -- D.intel Toolkit output -``` - -## Update Procedures - -### Minor Update (태그 수정) -``` -1. 변경 필요 태그 식별 -2. Preview 모드에서 수정 -3. 테스트 완료 -4. Publish with version note -5. Notion 문서 업데이트 -``` - -### Major Update (신규 태그 추가) -``` -1. Phase 3-4 문서 업데이트 -2. DataLayer 요구사항 개발팀 전달 -3. GTM 태그/트리거/변수 설정 -4. QA 체크리스트 수행 -5. 단계적 배포 -6. Notion 문서 업데이트 -``` - -### Emergency Fix -``` -1. 이슈 원인 파악 -2. 최소 변경으로 수정 -3. Publish (emergency version note) -4. 근본 원인 분석 -5. 영구 수정 계획 -``` - -## Versioning Best Practices - -### Version Naming -``` -v[Major].[Minor] - [Description] -예: v12.1 - Added form_submit event -``` - -### Version Notes Template -``` -## Changes -- Added: [새로 추가된 항목] -- Modified: [수정된 항목] -- Removed: [삭제된 항목] - -## Reason -[변경 이유] - -## Testing -- [테스트 항목 및 결과] - -## Rollback Plan -[롤백 필요 시 절차] -``` - -## Monitoring Setup - -### GA4 Alerts -- 일일 전환 수 급감 (>30%) -- 세션 수 급감 (>50%) -- 이벤트 수집 오류 - -### Custom Monitoring -- DataLayer 오류 로깅 -- Tag Manager 오류 추적 -- 서버 사이드 태그 상태 (sGTM 시) - -## Next Phase - -주기적 감사 완료 후 → Phase 7: Lookup App (필요 시) diff --git a/custom-skills/62-gtm-guardian/code/references/phase7-lookup-app.md b/custom-skills/62-gtm-guardian/code/references/phase7-lookup-app.md deleted file mode 100644 index 0350783..0000000 --- a/custom-skills/62-gtm-guardian/code/references/phase7-lookup-app.md +++ /dev/null @@ -1,283 +0,0 @@ -# Phase 7: Event Lookup & Parameter Library Web App - -Google Apps Script를 활용한 Event Taxonomy Sheet 기반 업무 지원용 앱 배포. - -## Objectives - -1. Event Taxonomy 데이터 조회 앱 -2. 실시간 파라미터 참조 도구 -3. 비개발자용 Self-service 도구 -4. 팀 간 일관된 이벤트 정보 공유 - -## App Architecture - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Google Sheets │────▶│ Apps Script │────▶│ Web App │ -│ (Taxonomy DB) │ │ (Backend) │ │ (Frontend) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - -## Google Sheets Data Source - -### Required Sheets -- Events Master -- Parameters Reference -- Custom Definitions -- Platform Mapping - -### Column Requirements -→ Phase 4 참조: [phase4-taxonomy.md](phase4-taxonomy.md) - -## Apps Script Setup - -### 1. Project Creation -``` -1. Google Sheets 열기 -2. Extensions > Apps Script -3. 프로젝트 이름 설정 -``` - -### 2. Core Functions - -#### Data Retrieval -```javascript -function getEventsData() { - const sheet = SpreadsheetApp.getActiveSpreadsheet() - .getSheetByName('Events Master'); - const data = sheet.getDataRange().getValues(); - const headers = data[0]; - - return data.slice(1).map(row => { - const obj = {}; - headers.forEach((header, i) => { - obj[header] = row[i]; - }); - return obj; - }); -} - -function getParametersData() { - const sheet = SpreadsheetApp.getActiveSpreadsheet() - .getSheetByName('Parameters Reference'); - const data = sheet.getDataRange().getValues(); - const headers = data[0]; - - return data.slice(1).map(row => { - const obj = {}; - headers.forEach((header, i) => { - obj[header] = row[i]; - }); - return obj; - }); -} -``` - -#### Search Function -```javascript -function searchEvents(query) { - const events = getEventsData(); - const searchTerm = query.toLowerCase(); - - return events.filter(event => - event.event_name.toLowerCase().includes(searchTerm) || - event.event_category.toLowerCase().includes(searchTerm) || - event.notes.toLowerCase().includes(searchTerm) - ); -} - -function getEventDetails(eventName) { - const events = getEventsData(); - const params = getParametersData(); - - const event = events.find(e => e.event_name === eventName); - if (!event) return null; - - const eventParams = event.parameters.split(',').map(p => p.trim()); - const paramDetails = params.filter(p => - eventParams.includes(p.parameter_name) - ); - - return { - event: event, - parameters: paramDetails - }; -} -``` - -### 3. Web App Handler - -```javascript -function doGet(e) { - return HtmlService.createHtmlOutputFromFile('index') - .setTitle('GTM Event Lookup') - .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); -} - -function doPost(e) { - const action = e.parameter.action; - - switch(action) { - case 'search': - return ContentService.createTextOutput( - JSON.stringify(searchEvents(e.parameter.query)) - ).setMimeType(ContentService.MimeType.JSON); - - case 'details': - return ContentService.createTextOutput( - JSON.stringify(getEventDetails(e.parameter.eventName)) - ).setMimeType(ContentService.MimeType.JSON); - - default: - return ContentService.createTextOutput( - JSON.stringify({error: 'Invalid action'}) - ).setMimeType(ContentService.MimeType.JSON); - } -} -``` - -### 4. HTML Frontend (index.html) - -```html - - - -