cja-funnel-health-check

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Funnel Health Check (Customer Journey Analytics)

漏斗健康检查(Customer Journey Analytics)

Turn a plain-English funnel description into a quantified step-by-step conversion analysis. The output identifies the biggest leakage point in the funnel and provides dimension-based breakdowns to show which audience or channel has the worst drop-off.
Funnel health checks are most valuable when a team suspects a specific step is broken but hasn't quantified it. This skill does the quantification in a single conversation turn.

将用自然语言描述的漏斗转化为量化的分步转化分析。输出结果会识别漏斗中泄漏最严重的节点,并提供基于维度的细分数据,展示哪类受众或渠道的用户流失情况最严重。
当团队怀疑某个特定步骤存在问题但尚未量化时,漏斗健康检查的价值最大。此技能可在单次对话中完成量化分析。

CJA MCP Tools Used

使用的CJA MCP工具

  • findDimensions
    — resolve page name, event, or other dimensions for step filtering
  • findMetrics
    — get the base metric to measure (sessions, visitors, events)
  • searchDimensionItems
    — find exact dimension values for step names
  • runReport
    (with
    adhocSegments
    ) — measure visitor counts at each funnel step
  • findSegments
    — use existing segments as funnel step filters if applicable
  • describeSegment
    — understand segment logic before applying as a funnel step

  • findDimensions
    — 解析页面名称、事件或其他维度,用于步骤筛选
  • findMetrics
    — 获取用于衡量的基础指标(会话数、访客数、事件数)
  • searchDimensionItems
    — 查找与步骤名称匹配的精确维度值
  • runReport
    (搭配
    adhocSegments
    ) — 衡量每个漏斗步骤的访客数量
  • findSegments
    — 若适用,使用现有细分作为漏斗步骤筛选条件
  • describeSegment
    — 在将细分作为漏斗步骤应用前,先了解其逻辑

Phase 0 — Setup

阶段0 — 设置

  1. Call
    findDataViews
    to list available data views.
  2. If the user hasn't specified a data view, present the list and ask which to use.
  3. Call
    setDefaultSessionDataViewId
    with the chosen ID.
  4. Ask the user to define the funnel stages if not already specified (e.g., "What are the steps in the funnel you want to analyze?").
  1. 调用
    findDataViews
    列出可用的数据视图。
  2. 如果用户未指定数据视图,列出选项并询问使用哪一个。
  3. 使用选定的ID调用
    setDefaultSessionDataViewId
  4. 如果用户尚未指定漏斗阶段,请询问用户定义这些阶段(例如:“你想要分析的漏斗包含哪些步骤?”)。

Phase 1 — Define Funnel Steps

阶段1 — 定义漏斗步骤

1.1 Parse the user's funnel description

1.1 解析用户的漏斗描述

Extract the step sequence from the user's plain-English description:
  • "Homepage → Product Page → Cart → Purchase"
  • "Registration → Onboarding Step 1 → Onboarding Step 2 → Activated"
  • "Landing Page → Lead Form → Thank You Page"
Each step must resolve to a measurable condition in CJA. A step can be:
  • Page view: user viewed a specific page (resolved via page name dimension)
  • Event/metric: user triggered a specific action (e.g., "Added to Cart")
  • Segment membership: user meets a pre-built segment condition
从用户的自然语言描述中提取步骤序列:
  • “首页 → 产品页 → 购物车 → 购买”
  • “注册 → 新用户引导步骤1 → 新用户引导步骤2 → 激活”
  • “着陆页 → 线索表单 → 感谢页”
每个步骤必须能解析为CJA中可衡量的条件。步骤可以是:
  • 页面浏览:用户浏览了特定页面(通过页面名称维度解析)
  • 事件/指标:用户触发了特定操作(例如“加入购物车”)
  • 细分成员:用户符合预构建的细分条件

1.2 Clarify ambiguous steps

1.2 明确模糊的步骤

If any step is vague (e.g., "checkout" without a page name), ask one question:
"For the 'Checkout' step — should I look at people who visited a page containing 'checkout' in the URL, or those who triggered a specific event like 'Cart Add'?"
Do not ask more than one clarifying question at a time.
如果任何步骤描述模糊(例如未指定页面名称的“结账”),提出一个问题:
“对于‘结账’步骤 — 我应该查看URL中包含‘checkout’的页面访问者,还是触发了‘加入购物车’这类特定事件的用户?”
一次不要提出多个澄清问题。

1.3 Resolve page names to dimension values

1.3 将页面名称解析为维度值

For page-based steps, call
searchDimensionItems
to find the exact dimension values that match the step name. First call
findDimensions
with a semantic search like
"page name url"
to confirm the correct dimension ID — in most CJA data views this is
variables/web.webPageDetails.name
, not
variables/page
:
searchDimensionItems(
  dimensionId: "variables/web.webPageDetails.name",
  searchAnd: "<step page name>",
  startDate: "<period start>",
  endDate: "<period end>",
  page: 0,
  limit: 10
)
Present matches to the user if there are multiple candidates:
"I found these pages matching 'checkout': /checkout/start, /checkout/payment, /checkout/review. Should I use '/checkout/start' as the entry to the checkout step?"

对于基于页面的步骤,调用
searchDimensionItems
查找与步骤名称匹配的精确维度值。首先调用
findDimensions
并使用语义搜索,例如
"page name url"
,确认正确的维度ID — 在大多数CJA数据视图中,这个ID是
variables/web.webPageDetails.name
,而非
variables/page
searchDimensionItems(
  dimensionId: "variables/web.webPageDetails.name",
  searchAnd: "<step page name>",
  startDate: "<period start>",
  endDate: "<period end>",
  page: 0,
  limit: 10
)
如果有多个候选结果,将匹配项展示给用户:
“我找到了这些匹配‘checkout’的页面:/checkout/start, /checkout/payment, /checkout/review。我应该将‘/checkout/start’作为结账步骤的入口吗?”

Phase 2 — Measure Each Step

阶段2 — 衡量每个步骤

For each funnel step, construct an ad hoc segment that filters to visitors/sessions that reached that step. Then run a report measuring the base metric (usually Unique Visitors or Sessions) with that segment applied.
对于每个漏斗步骤,构建一个临时细分(ad hoc segment),筛选出到达该步骤的访客/会话。然后运行报告,应用该细分来衡量基础指标(通常是独立访客数或会话数)。

2.1 Construct ad hoc segments for each step

2.1 为每个步骤构建临时细分

A "reached step N" ad hoc segment is:
  • Container: Visit or Person (use Visit for session-level funnels, Person for cross-visit journeys)
  • Condition: Page Name equals "<step page value>" OR Event occurred
“到达步骤N”的临时细分规则:
  • 容器:访问(Visit)或用户(Person)(会话级漏斗使用Visit,跨访问旅程使用Person)
  • 条件:页面名称等于“<步骤页面值>” 或 事件已触发

2.2 Run reports for each step

2.2 为每个步骤运行报告

Run one
runReport
per step with the ad hoc segment applied. The
adhocSegments
parameter takes fully-formed CJA segment definition objects — NOT a simplified shorthand. The correct structure is:
runReport(
  dimensionIds: "variables/web.webPageDetails.name",
  metricIds: "metrics/visitors",
  startDate: "<period start>",
  endDate: "<period end>",
  page: 0,
  limit: 1,
  adhocSegments: [{
    "func": "segment",
    "version": [1, 0, 0],
    "container": {
      "func": "container",
      "context": "visitors",
      "pred": {
        "func": "streq",
        "val": { "func": "attr", "name": "variables/web.webPageDetails.name" },
        "str": "<step page value>"
      }
    }
  }]
)
Use
context: "visitors"
(person-level) for cross-visit funnels. Use
context: "visits"
(session-level) for within-session funnels. The metric value comes from
summaryData.filteredTotals[0]
in the response.
The dimension and value used in the predicate should match what was found via
searchDimensionItems
in Phase 1 — use the exact
value
string returned.
Important: Each step uses a cumulative filter — measure "visitors who EVER reached this step in the period," not "visitors who ONLY visited this page." This produces the classic funnel waterfall.
Capture
stepCount[i]
for each step i from 1 to N.

为每个步骤运行一次
runReport
并应用临时细分。
adhocSegments
参数需要完整的CJA细分定义对象 — 而非简化的简写形式。正确的结构如下:
runReport(
  dimensionIds: "variables/web.webPageDetails.name",
  metricIds: "metrics/visitors",
  startDate: "<period start>",
  endDate: "<period end>",
  page: 0,
  limit: 1,
  adhocSegments: [{
    "func": "segment",
    "version": [1, 0, 0],
    "container": {
      "func": "container",
      "context": "visitors",
      "pred": {
        "func": "streq",
        "val": { "func": "attr", "name": "variables/web.webPageDetails.name" },
        "str": "<step page value>"
      }
    }
  }]
)
跨访问漏斗使用
context: "visitors"
(用户级)。会话内漏斗使用
context: "visits"
(会话级)。指标值来自响应中的
summaryData.filteredTotals[0]
谓词中使用的维度和值应与阶段1中通过
searchDimensionItems
找到的内容匹配 — 使用返回的精确
value
字符串。
重要提示:每个步骤使用累积筛选 — 衡量“在周期内曾到达此步骤的访客”,而非“仅访问此页面的访客”。这会生成经典的漏斗瀑布图。
记录每个步骤i(从1到N)的
stepCount[i]

Phase 3 — Compute Funnel Metrics

阶段3 — 计算漏斗指标

For each step-to-step transition:
  • stepConversionRate[i→i+1]
    = stepCount[i+1] / stepCount[i] × 100
  • dropOff[i→i+1]
    = stepCount[i] − stepCount[i+1]
  • dropOffRate[i→i+1]
    = 100 − stepConversionRate[i→i+1]
Overall funnel:
  • overallConversionRate
    = stepCount[N] / stepCount[1] × 100
  • biggestDropOffStep
    = argmax(dropOff[i→i+1]) — the step with the most visitors lost

对于每个步骤到步骤的转换:
  • stepConversionRate[i→i+1]
    = stepCount[i+1] / stepCount[i] × 100
  • dropOff[i→i+1]
    = stepCount[i] − stepCount[i+1]
  • dropOffRate[i→i+1]
    = 100 − stepConversionRate[i→i+1]
整体漏斗:
  • overallConversionRate
    = stepCount[N] / stepCount[1] × 100
  • biggestDropOffStep
    = argmax(dropOff[i→i+1]) — 流失访客数最多的步骤

Phase 4 — Optional: Segment the Funnel

阶段4 — 可选:细分漏斗

If the user wants to compare funnel performance across audiences or channels, run the same step reports filtered by a dimension or segment:
runReport(
  dimensionIds: "variables/device_type",
  metricIds: "metrics/visitors",
  startDate: "<range start>",
  endDate: "<range end>",
  page: 0,
  limit: 10,
  adhocSegments: [{ /* step N filter — same structure as Phase 2 */ }]
)
Note: use
findDimensions
with
searchQuery: "device type mobile desktop"
to confirm the device dimension ID for the data view (often
variables/device_type
).
This shows step-N visitor counts broken down by device type (or channel, or country). If one segment has a dramatically lower conversion through the worst step, that's the target for optimization.
Common comparisons to suggest:
  • Device type (mobile vs desktop conversion often differs significantly)
  • Marketing channel (paid vs organic users may convert differently)
  • New vs returning visitors

如果用户想要比较不同受众或渠道的漏斗表现,运行相同的步骤报告并按维度或细分筛选:
runReport(
  dimensionIds: "variables/device_type",
  metricIds: "metrics/visitors",
  startDate: "<range start>",
  endDate: "<range end>",
  page: 0,
  limit: 10,
  adhocSegments: [{ /* step N filter — same structure as Phase 2 */ }]
)
注意:调用
findDimensions
并使用
searchQuery: "device type mobile desktop"
确认数据视图中的设备维度ID(通常是
variables/device_type
)。
这会展示按设备类型(或渠道、国家)细分的步骤N访客数。如果某个细分群体在最糟糕的步骤中的转化率明显更低,那就是优化的目标。
建议的常见对比:
  • 设备类型(移动端与桌面端的转化率通常差异显著)
  • 营销渠道(付费用户与自然流量用户的转化率可能不同)
  • 新访客 vs 回访客

Phase 5 — Generate HTML Funnel Report

阶段5 — 生成HTML漏斗报告

Generate the funnel report inline and write to
/tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html
.
在线生成漏斗报告,并写入
/tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html

HTML Template

HTML模板

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Funnel Health Check &mdash; {ORG_NAME} &mdash; {FUNNEL_NAME}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #f5f4f1;
    --surface: #ffffff;
    --ink: #1a1a1a;
    --ink-muted: #6b6b6b;
    --border: #e5e2dc;
    --header-bg: #0e0e10;
    --header-warm: #3a1010;
    --accent-red: #c8312f;
    --accent-red-bright: #ff6b68;
    --accent-red-soft: #fdecea;
    --accent-green: #1f7a4d;
    --accent-yellow: #d4a017;
  }
  body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
         background: var(--bg); color: var(--ink); line-height: 1.5;
         -webkit-font-smoothing: antialiased; }

  /* === Header === */
  header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
           color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
  header::after { content: ""; position: absolute; right: -140px; top: -140px;
                  width: 460px; height: 460px;
                  background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
                  pointer-events: none; }
  .header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
  .eyebrow { display: inline-flex; align-items: center; gap: 8px;
             padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
             border-radius: 999px; color: var(--accent-red-bright);
             font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
             text-transform: uppercase; margin-bottom: 24px;
             background: rgba(200,49,47,.10); }
  .eyebrow::before { content: ""; width: 6px; height: 6px;
                     background: var(--accent-red-bright); border-radius: 50%; }
  header h1 { font-family: "Playfair Display", Georgia, serif;
              font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
              line-height: 1.05; margin-bottom: 14px; color: #fff; }
  header .lede { font-size: 16px; max-width: 560px;
                 color: rgba(255,255,255,.80); margin-bottom: 24px;
                 line-height: 1.55; }
  header .meta { display: flex; flex-wrap: wrap; gap: 22px;
                 font-size: 13px; color: rgba(255,255,255,.60); }
  header .meta span { display: inline-flex; align-items: center; gap: 6px; }
  header .meta .icon { opacity: .8; }

  /* === Tabs === */
  nav { background: var(--surface); border-bottom: 1px solid var(--border);
        padding: 0 56px; display: flex; gap: 28px;
        position: sticky; top: 0; z-index: 50; }
  nav a { display: block; padding: 16px 0; font-size: 14px;
          color: var(--ink); text-decoration: none;
          border-bottom: 2px solid transparent;
          transition: border-color .15s ease; }
  nav a:hover { border-bottom-color: var(--accent-red); }

  /* === Container === */
  .container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }

  /* === Section label === */
  .section-label { font-size: 11px; font-weight: 700;
                   text-transform: uppercase; letter-spacing: 1.4px;
                   color: var(--ink-muted); margin-bottom: 14px;
                   padding-bottom: 10px; border-bottom: 1px solid var(--border); }

  /* === KPI grid === */
  .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
             gap: 14px; margin-bottom: 36px; }
  .kpi-tile { background: var(--surface); border-radius: 8px;
              padding: 22px 22px 20px;
              border-top: 3px solid #b9b6ae;
              box-shadow: 0 1px 3px rgba(0,0,0,.05); }
  .kpi-tile.down { border-top-color: var(--accent-red); }
  .kpi-tile.up   { border-top-color: var(--accent-green); }
  .kpi-tile.flat { border-top-color: #b9b6ae; }
  .kpi-head { display: flex; justify-content: space-between;
              align-items: center; margin-bottom: 10px; }
  .kpi-label { font-size: 11px; font-weight: 700;
               text-transform: uppercase;
               color: var(--ink-muted); letter-spacing: 1px; }
  .kpi-value { font-family: "Playfair Display", Georgia, serif;
               font-weight: 700; font-size: 38px;
               line-height: 1; color: var(--ink);
               margin-bottom: 12px; }
  .pill { display: inline-flex; align-items: center; gap: 4px;
          padding: 3px 9px; border-radius: 4px;
          font-size: 12px; font-weight: 600; line-height: 1.4; }
  .pill.down { background: var(--accent-red-soft); color: var(--accent-red); }
  .pill.up   { background: #ebf5ef; color: var(--accent-green); }
  .pill.flat { background: #f1efea; color: var(--ink-muted); }
  .prior { display: block; margin-top: 10px;
           font-size: 12px; color: var(--ink-muted); }

  /* === Funnel chart wrap === */
  .chart-wrap { background: var(--surface); border-radius: 8px;
                padding: 28px 32px;
                box-shadow: 0 1px 3px rgba(0,0,0,.04);
                margin-bottom: 22px; }
  .chart-wrap h2 { font-family: "Playfair Display", Georgia, serif;
                   font-size: 18px; font-weight: 700;
                   margin-bottom: 18px;
                   padding-bottom: 12px;
                   border-bottom: 1px solid var(--border); }

  /* === Sections (collapsible tables) === */
  .section { background: var(--surface); border-radius: 8px;
             box-shadow: 0 1px 3px rgba(0,0,0,.04);
             margin-bottom: 22px; overflow: hidden; }
  .section-header { padding: 18px 28px; border-bottom: 1px solid var(--border);
                    display: flex; justify-content: space-between;
                    align-items: center; cursor: pointer; }
  .section-header h2 { font-family: "Playfair Display", Georgia, serif;
                       font-size: 18px; font-weight: 700; }
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
  thead th { background: #faf8f4; padding: 12px 22px;
             text-align: left; font-weight: 600;
             text-transform: uppercase; letter-spacing: .6px;
             font-size: 11px; color: var(--ink-muted);
             border-bottom: 1px solid var(--border); }
  tbody td { padding: 12px 22px; border-bottom: 1px solid #f4f1eb; }
  tbody tr:last-child td { border-bottom: none; }

  .badge { display: inline-block; padding: 3px 9px; border-radius: 4px;
           font-size: 11px; font-weight: 600; }
  .badge.green  { background: #ebf5ef; color: var(--accent-green); }
  .badge.red    { background: var(--accent-red-soft); color: var(--accent-red); }
  .badge.yellow { background: #fef6e3; color: #b67a08; }
  .badge.grey   { background: #f1efea; color: var(--ink-muted); }

  .highlight-row td { background: var(--accent-red-soft) !important;
                      font-weight: 700; }
  .progress-bar-wrap { background: #f1efea; border-radius: 4px;
                       height: 8px; overflow: hidden;
                       width: 100%; min-width: 80px; }
  .progress-bar { height: 100%; border-radius: 4px;
                  background: linear-gradient(90deg, var(--accent-red), #8a1d1c); }

  .rec-item { display: flex; gap: 12px; align-items: flex-start;
              padding: 14px 28px; border-bottom: 1px solid #f1eeea;
              font-size: 14px; line-height: 1.55; }
  .rec-item:last-child { border-bottom: none; }

  .back-top { position: fixed; bottom: 24px; right: 24px;
              background: var(--accent-red); color: #fff;
              width: 44px; height: 44px; border-radius: 50%;
              border: none; font-size: 20px; cursor: pointer;
              box-shadow: 0 4px 12px rgba(200,49,47,0.30); }
  footer { text-align: center; padding: 32px 24px;
           font-size: 12px; color: var(--ink-muted); }

  /* === Print === */
  @media print {
    nav { display: none; position: static; }
    header { padding: 36px 32px 28px; }
    header h1 { font-size: 42px; }
    .section-header { cursor: default; }
    .kpi-row { page-break-inside: avoid; }
    .kpi-tile, .chart-wrap, .section {
      box-shadow: none; border: 1px solid var(--border);
    }
    .back-top { display: none; }
  }
</style>
</head>
<body>

<header>
  <div class="header-inner">
    <div class="eyebrow">Funnel Health Report</div>
    <h1>{ORG_NAME} Funnel Health</h1>
    <p class="lede">Step-by-step conversion for the {FUNNEL_NAME} journey across {DATE_RANGE}, with the biggest leakage point surfaced.</p>
    <div class="meta">
      <span><span class="icon">&#128197;</span> {DATE_RANGE}</span>
      <span><span class="icon">&#128202;</span> {DATA_VIEW}</span>
      <span><span class="icon">&#128340;</span> Prepared {GENERATED_DATE}</span>
    </div>
  </div>
</header>

<nav>
  <a href="#overview">Overview</a>
  <a href="#chart">Funnel Chart</a>
  <a href="#steps">Step Detail</a>
  <a href="#segments">Segment Breakdown</a>
  <a href="#recs">Recommendations</a>
</nav>

<div class="container">

  <!-- Summary KPI Tiles -->
  <div class="section-label">Funnel Summary</div>
  <div id="overview" class="kpi-row">
    <div class="kpi-tile flat">
      <div class="kpi-head"><div class="kpi-label">Entered Funnel</div></div>
      <div class="kpi-value">{STEP_1_COUNT}</div>
      <span class="prior">Step 1 visitors</span>
    </div>
    <div class="kpi-tile flat">
      <div class="kpi-head"><div class="kpi-label">Completed Funnel</div></div>
      <div class="kpi-value">{STEP_N_COUNT}</div>
      <span class="prior">Reached final step</span>
    </div>
    <div class="kpi-tile flat">
      <div class="kpi-head"><div class="kpi-label">Overall Conversion</div></div>
      <div class="kpi-value">{OVERALL_CVR}%</div>
      <span class="prior">End-to-end rate</span>
    </div>
    <div class="kpi-tile down">
      <div class="kpi-head"><div class="kpi-label">Biggest Drop-Off Step</div></div>
      <div class="kpi-value" style="font-size:22px;">{WORST_STEP_NAME}</div>
      <span class="pill down">&#9660; Worst leakage</span>
    </div>
    <div class="kpi-tile down">
      <div class="kpi-head"><div class="kpi-label">Worst Step Drop-Off</div></div>
      <div class="kpi-value">{WORST_STEP_DROPOFF}%</div>
      <span class="prior">Of visitors lost at this step</span>
    </div>
  </div>

  <!-- Funnel Bar Chart -->
  <div id="chart" class="chart-wrap">
    <h2>Funnel Visualization</h2>
    <canvas id="funnelChart" height="100"></canvas>
  </div>

  <!-- Step-by-Step Table -->
  <div id="steps" class="section">
    <div class="section-header" onclick="toggle('steps-body')">
      <h2>Step-by-Step Analysis</h2>
      <span id="steps-body-icon">&#9662;</span>
    </div>
    <div id="steps-body">
      <table>
        <thead><tr>
          <th>Step</th>
          <th>Visitors</th>
          <th>% of Step 1</th>
          <th>Step Conversion</th>
          <th>Drop-Off</th>
          <th>Drop-Off Rate</th>
          <th>Visual</th>
        </tr></thead>
        <tbody>
          <!-- For each step i:
          <tr class="{highlight-row if worst step}">
            <td>{STEP_NAME}</td>
            <td>{STEP_COUNT}</td>
            <td>{PCT_OF_STEP_1}%</td>
            <td>{STEP_CVR}% {badge}</td>
            <td>{DROPOFF}</td>
            <td>{DROPOFF_RATE}%</td>
            <td>
              <div class="progress-bar-wrap">
                <div class="progress-bar" style="width:{PCT_OF_STEP_1}%"></div>
              </div>
            </td>
          </tr>
          -->
        </tbody>
      </table>
    </div>
  </div>

  <!-- Segment Breakdown (optional) -->
  <div id="segments" class="section">
    <div class="section-header" onclick="toggle('seg-body')">
      <h2>Segment Breakdown at Worst Step</h2>
      <span id="seg-body-icon">&#9662;</span>
    </div>
    <div id="seg-body">
      <table>
        <thead><tr>
          <th>Segment / Dimension</th>
          <th>Entered Worst Step</th>
          <th>Passed Worst Step</th>
          <th>Conversion</th>
          <th>vs. Average</th>
        </tr></thead>
        <tbody>
          <!-- Fill with dimension breakdown at the worst step -->
        </tbody>
      </table>
    </div>
  </div>

  <!-- Recommendations -->
  <div id="recs" class="section">
    <div class="section-header" onclick="toggle('rec-body')">
      <h2>Optimization Recommendations</h2>
      <span id="rec-body-icon">&#9662;</span>
    </div>
    <div id="rec-body">
      <!-- 2-3 recommendation items as .rec-item -->
    </div>
  </div>

</div>

<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">&uarr;</button>
<footer>Funnel Health Check &mdash; {ORG_NAME} &mdash; Generated {GENERATED_DATE}</footer>

<script>
// Funnel bar chart — recolor via design tokens.
// Stage colors fade from accent-red (worst leakage upstream) to a deeper red downstream.
const ctx = document.getElementById('funnelChart').getContext('2d');
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: {STEP_NAMES_JSON},
    datasets: [{
      label: 'Visitors',
      data: {STEP_COUNTS_JSON},
      backgroundColor: [
        'rgba(200,49,47,0.90)',
        'rgba(200,49,47,0.72)',
        'rgba(200,49,47,0.54)',
        'rgba(200,49,47,0.38)',
        'rgba(200,49,47,0.22)'
      ],
      borderRadius: 6
    }]
  },
  options: {
    responsive: true,
    plugins: { legend: { display: false } },
    scales: {
      y: { beginAtZero: true, grid: { color: '#eee9df' },
           ticks: { color: '#6b6b6b' } },
      x: { grid: { display: false },
           ticks: { color: '#1a1a1a', font: { weight: '600' } } }
    }
  }
});

function toggle(id) {
  var el = document.getElementById(id);
  var ic = document.getElementById(id + '-icon');
  if (el.style.display === 'none') { el.style.display=''; ic.textContent='\u25be'; }
  else { el.style.display='none'; ic.textContent='\u25b8'; }
}
</script>
</body></html>

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Funnel Health Check &mdash; {ORG_NAME} &mdash; {FUNNEL_NAME}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #f5f4f1;
    --surface: #ffffff;
    --ink: #1a1a1a;
    --ink-muted: #6b6b6b;
    --border: #e5e2dc;
    --header-bg: #0e0e10;
    --header-warm: #3a1010;
    --accent-red: #c8312f;
    --accent-red-bright: #ff6b68;
    --accent-red-soft: #fdecea;
    --accent-green: #1f7a4d;
    --accent-yellow: #d4a017;
  }
  body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
         background: var(--bg); color: var(--ink); line-height: 1.5;
         -webkit-font-smoothing: antialiased; }

  /* === Header === */
  header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
           color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
  header::after { content: ""; position: absolute; right: -140px; top: -140px;
                  width: 460px; height: 460px;
                  background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
                  pointer-events: none; }
  .header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
  .eyebrow { display: inline-flex; align-items: center; gap: 8px;
             padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
             border-radius: 999px; color: var(--accent-red-bright);
             font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
             text-transform: uppercase; margin-bottom: 24px;
             background: rgba(200,49,47,.10); }
  .eyebrow::before { content: ""; width: 6px; height: 6px;
                     background: var(--accent-red-bright); border-radius: 50%; }
  header h1 { font-family: "Playfair Display", Georgia, serif;
              font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
              line-height: 1.05; margin-bottom: 14px; color: #fff; }
  header .lede { font-size: 16px; max-width: 560px;
                 color: rgba(255,255,255,.80); margin-bottom: 24px;
                 line-height: 1.55; }
  header .meta { display: flex; flex-wrap: wrap; gap: 22px;
                 font-size: 13px; color: rgba(255,255,255,.60); }
  header .meta span { display: inline-flex; align-items: center; gap: 6px; }
  header .meta .icon { opacity: .8; }

  /* === Tabs === */
  nav { background: var(--surface); border-bottom: 1px solid var(--border);
        padding: 0 56px; display: flex; gap: 28px;
        position: sticky; top: 0; z-index: 50; }
  nav a { display: block; padding: 16px 0; font-size: 14px;
          color: var(--ink); text-decoration: none;
          border-bottom: 2px solid transparent;
          transition: border-color .15s ease; }
  nav a:hover { border-bottom-color: var(--accent-red); }

  /* === Container === */
  .container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }

  /* === Section label === */
  .section-label { font-size: 11px; font-weight: 700;
                   text-transform: uppercase; letter-spacing: 1.4px;
                   color: var(--ink-muted); margin-bottom: 14px;
                   padding-bottom: 10px; border-bottom: 1px solid var(--border); }

  /* === KPI grid === */
  .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
             gap: 14px; margin-bottom: 36px; }
  .kpi-tile { background: var(--surface); border-radius: 8px;
              padding: 22px 22px 20px;
              border-top: 3px solid #b9b6ae;
              box-shadow: 0 1px 3px rgba(0,0,0,.05); }
  .kpi-tile.down { border-top-color: var(--accent-red); }
  .kpi-tile.up   { border-top-color: var(--accent-green); }
  .kpi-tile.flat { border-top-color: #b9b6ae; }
  .kpi-head { display: flex; justify-content: space-between;
              align-items: center; margin-bottom: 10px; }
  .kpi-label { font-size: 11px; font-weight: 700;
               text-transform: uppercase;
               color: var(--ink-muted); letter-spacing: 1px; }
  .kpi-value { font-family: "Playfair Display", Georgia, serif;
               font-weight: 700; font-size: 38px;
               line-height: 1; color: var(--ink);
               margin-bottom: 12px; }
  .pill { display: inline-flex; align-items: center; gap: 4px;
          padding: 3px 9px; border-radius: 4px;
          font-size: 12px; font-weight: 600; line-height: 1.4; }
  .pill.down { background: var(--accent-red-soft); color: var(--accent-red); }
  .pill.up   { background: #ebf5ef; color: var(--accent-green); }
  .pill.flat { background: #f1efea; color: var(--ink-muted); }
  .prior { display: block; margin-top: 10px;
           font-size: 12px; color: var(--ink-muted); }

  /* === Funnel chart wrap === */
  .chart-wrap { background: var(--surface); border-radius: 8px;
                padding: 28px 32px;
                box-shadow: 0 1px 3px rgba(0,0,0,.04);
                margin-bottom: 22px; }
  .chart-wrap h2 { font-family: "Playfair Display", Georgia, serif;
                   font-size: 18px; font-weight: 700;
                   margin-bottom: 18px;
                   padding-bottom: 12px;
                   border-bottom: 1px solid var(--border); }

  /* === Sections (collapsible tables) === */
  .section { background: var(--surface); border-radius: 8px;
             box-shadow: 0 1px 3px rgba(0,0,0,.04);
             margin-bottom: 22px; overflow: hidden; }
  .section-header { padding: 18px 28px; border-bottom: 1px solid var(--border);
                    display: flex; justify-content: space-between;
                    align-items: center; cursor: pointer; }
  .section-header h2 { font-family: "Playfair Display", Georgia, serif;
                       font-size: 18px; font-weight: 700; }
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
  thead th { background: #faf8f4; padding: 12px 22px;
             text-align: left; font-weight: 600;
             text-transform: uppercase; letter-spacing: .6px;
             font-size: 11px; color: var(--ink-muted);
             border-bottom: 1px solid var(--border); }
  tbody td { padding: 12px 22px; border-bottom: 1px solid #f4f1eb; }
  tbody tr:last-child td { border-bottom: none; }

  .badge { display: inline-block; padding: 3px 9px; border-radius: 4px;
           font-size: 11px; font-weight: 600; }
  .badge.green  { background: #ebf5ef; color: var(--accent-green); }
  .badge.red    { background: var(--accent-red-soft); color: var(--accent-red); }
  .badge.yellow { background: #fef6e3; color: #b67a08; }
  .badge.grey   { background: #f1efea; color: var(--ink-muted); }

  .highlight-row td { background: var(--accent-red-soft) !important;
                      font-weight: 700; }
  .progress-bar-wrap { background: #f1efea; border-radius: 4px;
                       height: 8px; overflow: hidden;
                       width: 100%; min-width: 80px; }
  .progress-bar { height: 100%; border-radius: 4px;
                  background: linear-gradient(90deg, var(--accent-red), #8a1d1c); }

  .rec-item { display: flex; gap: 12px; align-items: flex-start;
              padding: 14px 28px; border-bottom: 1px solid #f1eeea;
              font-size: 14px; line-height: 1.55; }
  .rec-item:last-child { border-bottom: none; }

  .back-top { position: fixed; bottom: 24px; right: 24px;
              background: var(--accent-red); color: #fff;
              width: 44px; height: 44px; border-radius: 50%;
              border: none; font-size: 20px; cursor: pointer;
              box-shadow: 0 4px 12px rgba(200,49,47,0.30); }
  footer { text-align: center; padding: 32px 24px;
           font-size: 12px; color: var(--ink-muted); }

  /* === Print === */
  @media print {
    nav { display: none; position: static; }
    header { padding: 36px 32px 28px; }
    header h1 { font-size: 42px; }
    .section-header { cursor: default; }
    .kpi-row { page-break-inside: avoid; }
    .kpi-tile, .chart-wrap, .section {
      box-shadow: none; border: 1px solid var(--border);
    }
    .back-top { display: none; }
  }
</style>
</head>
<body>

<header>
  <div class="header-inner">
    <div class="eyebrow">Funnel Health Report</div>
    <h1>{ORG_NAME} Funnel Health</h1>
    <p class="lede">Step-by-step conversion for the {FUNNEL_NAME} journey across {DATE_RANGE}, with the biggest leakage point surfaced.</p>
    <div class="meta">
      <span><span class="icon">&#128197;</span> {DATE_RANGE}</span>
      <span><span class="icon">&#128202;</span> {DATA_VIEW}</span>
      <span><span class="icon">&#128340;</span> Prepared {GENERATED_DATE}</span>
    </div>
  </div>
</header>

<nav>
  <a href="#overview">Overview</a>
  <a href="#chart">Funnel Chart</a>
  <a href="#steps">Step Detail</a>
  <a href="#segments">Segment Breakdown</a>
  <a href="#recs">Recommendations</a>
</nav>

<div class="container">

  <!-- Summary KPI Tiles -->
  <div class="section-label">Funnel Summary</div>
  <div id="overview" class="kpi-row">
    <div class="kpi-tile flat">
      <div class="kpi-head"><div class="kpi-label">Entered Funnel</div></div>
      <div class="kpi-value">{STEP_1_COUNT}</div>
      <span class="prior">Step 1 visitors</span>
    </div>
    <div class="kpi-tile flat">
      <div class="kpi-head"><div class="kpi-label">Completed Funnel</div></div>
      <div class="kpi-value">{STEP_N_COUNT}</div>
      <span class="prior">Reached final step</span>
    </div>
    <div class="kpi-tile flat">
      <div class="kpi-head"><div class="kpi-label">Overall Conversion</div></div>
      <div class="kpi-value">{OVERALL_CVR}%</div>
      <span class="prior">End-to-end rate</span>
    </div>
    <div class="kpi-tile down">
      <div class="kpi-head"><div class="kpi-label">Biggest Drop-Off Step</div></div>
      <div class="kpi-value" style="font-size:22px;">{WORST_STEP_NAME}</div>
      <span class="pill down">&#9660; Worst leakage</span>
    </div>
    <div class="kpi-tile down">
      <div class="kpi-head"><div class="kpi-label">Worst Step Drop-Off</div></div>
      <div class="kpi-value">{WORST_STEP_DROPOFF}%</div>
      <span class="prior">Of visitors lost at this step</span>
    </div>
  </div>

  <!-- Funnel Bar Chart -->
  <div id="chart" class="chart-wrap">
    <h2>Funnel Visualization</h2>
    <canvas id="funnelChart" height="100"></canvas>
  </div>

  <!-- Step-by-Step Table -->
  <div id="steps" class="section">
    <div class="section-header" onclick="toggle('steps-body')">
      <h2>Step-by-Step Analysis</h2>
      <span id="steps-body-icon">&#9662;</span>
    </div>
    <div id="steps-body">
      <table>
        <thead><tr>
          <th>Step</th>
          <th>Visitors</th>
          <th>% of Step 1</th>
          <th>Step Conversion</th>
          <th>Drop-Off</th>
          <th>Drop-Off Rate</th>
          <th>Visual</th>
        </tr></thead>
        <tbody>
          <!-- For each step i:
          <tr class="{highlight-row if worst step}">
            <td>{STEP_NAME}</td>
            <td>{STEP_COUNT}</td>
            <td>{PCT_OF_STEP_1}%</td>
            <td>{STEP_CVR}% {badge}</td>
            <td>{DROPOFF}</td>
            <td>{DROPOFF_RATE}%</td>
            <td>
              <div class="progress-bar-wrap">
                <div class="progress-bar" style="width:{PCT_OF_STEP_1}%"></div>
              </div>
            </td>
          </tr>
          -->
        </tbody>
      </table>
    </div>
  </div>

  <!-- Segment Breakdown (optional) -->
  <div id="segments" class="section">
    <div class="section-header" onclick="toggle('seg-body')">
      <h2>Segment Breakdown at Worst Step</h2>
      <span id="seg-body-icon">&#9662;</span>
    </div>
    <div id="seg-body">
      <table>
        <thead><tr>
          <th>Segment / Dimension</th>
          <th>Entered Worst Step</th>
          <th>Passed Worst Step</th>
          <th>Conversion</th>
          <th>vs. Average</th>
        </tr></thead>
        <tbody>
          <!-- Fill with dimension breakdown at the worst step -->
        </tbody>
      </table>
    </div>
  </div>

  <!-- Recommendations -->
  <div id="recs" class="section">
    <div class="section-header" onclick="toggle('rec-body')">
      <h2>Optimization Recommendations</h2>
      <span id="rec-body-icon">&#9662;</span>
    </div>
    <div id="rec-body">
      <!-- 2-3 recommendation items as .rec-item -->
    </div>
  </div>

</div>

<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">&uarr;</button>
<footer>Funnel Health Check &mdash; {ORG_NAME} &mdash; Generated {GENERATED_DATE}</footer>

<script>
// Funnel bar chart — recolor via design tokens.
// Stage colors fade from accent-red (worst leakage upstream) to a deeper red downstream.
const ctx = document.getElementById('funnelChart').getContext('2d');
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: {STEP_NAMES_JSON},
    datasets: [{
      label: 'Visitors',
      data: {STEP_COUNTS_JSON},
      backgroundColor: [
        'rgba(200,49,47,0.90)',
        'rgba(200,49,47,0.72)',
        'rgba(200,49,47,0.54)',
        'rgba(200,49,47,0.38)',
        'rgba(200,49,47,0.22)'
      ],
      borderRadius: 6
    }]
  },
  options: {
    responsive: true,
    plugins: { legend: { display: false } },
    scales: {
      y: { beginAtZero: true, grid: { color: '#eee9df' },
           ticks: { color: '#6b6b6b' } },
      x: { grid: { display: false },
           ticks: { color: '#1a1a1a', font: { weight: '600' } } }
    }
  }
});

function toggle(id) {
  var el = document.getElementById(id);
  var ic = document.getElementById(id + '-icon');
  if (el.style.display === 'none') { el.style.display=''; ic.textContent='\u25be'; }
  else { el.style.display='none'; ic.textContent='\u25b8'; }
}
</script>
</body></html>

Workflow Summary

工作流程总结

  1. Parse funnel steps from user description.
  2. Clarify any ambiguous steps (one question at a time).
  3. Resolve page names with
    searchDimensionItems
    .
  4. Run one
    runReport
    per step with ad hoc segment filter; capture visitor counts.
  5. Compute step conversion rates, drop-off counts, and rates.
  6. Identify the biggest drop-off step.
  7. Optionally run dimension breakdown at the worst step.
  8. Generate HTML report with Chart.js funnel visualization.
  9. Write to
    /tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html
    .
  10. Open with
    open /tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html
    .
  11. Summarize inline: "Overall conversion: X%. Biggest drop-off at [Step N]: Y% of users abandon. Mobile users drop off at a 2× higher rate than desktop."

  1. 从用户描述中解析漏斗步骤。
  2. 明确任何模糊的步骤(一次一个问题)。
  3. 使用
    searchDimensionItems
    解析页面名称。
  4. 为每个步骤运行一次带临时细分筛选的
    runReport
    ;捕获访客数量。
  5. 计算步骤转化率、流失数量和流失率。
  6. 识别流失最严重的步骤。
  7. 可选:在最糟糕的步骤运行维度细分分析。
  8. 生成带Chart.js漏斗可视化的HTML报告。
  9. 写入
    /tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html
  10. 使用
    open /tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html
    打开报告。
  11. 在线总结:“整体转化率:X%。最大流失点在[步骤N]:Y%的用户放弃。移动端用户的流失率是桌面端的2倍。”

Important Guardrails

重要约束

  • Read-only analysis. Never modify segments, calculated metrics, or project definitions automatically.
  • Confirm funnel stages before running. Ambiguous stage definitions produce misleading results — clarify with the user first.
  • Note attribution model. Funnel conversion rates depend on the attribution model in the data view; mention it in the report.
  • Flag incomplete data. If any stage returns zero or suspiciously low counts, note possible tracking gaps before drawing conclusions.
  • Cap date range. Funnel analysis over very long date ranges (>90 days) can be slow; suggest 30-day windows as default.
  • Never assume stage order. Confirm with the user that the stages are sequential and mutually exclusive before calculating drop-off rates.
  • 只读分析。切勿自动修改细分、计算指标或项目定义。
  • 运行前确认漏斗阶段。模糊的阶段定义会产生误导性结果 — 先与用户确认。
  • 注意归因模型。漏斗转化率取决于数据视图中的归因模型;在报告中提及这一点。
  • 标记不完整数据。如果任何阶段返回零或异常低的数量,在得出结论前先指出可能的跟踪缺口。
  • 限制日期范围。过长日期范围(>90天)的漏斗分析可能很慢;建议默认使用30天窗口。
  • 切勿假设阶段顺序。在计算流失率前,与用户确认阶段是连续且互斥的。

Example Interaction

示例交互

"Check the health of our checkout funnel — I want to see where people are dropping off."
  1. Setup: Call
    findDataViews
    , user selects their e-commerce data view. Call
    setDefaultSessionDataViewId
    .
  2. Define funnel: Ask "What are the checkout stages?" User replies: "Product View → Add to Cart → Checkout Start → Purchase."
  3. Analysis: Run
    runReport
    for each stage transition over the last 30 days. Calculate drop-off rates: Product View→Cart 22%, Cart→Checkout 58%, Checkout→Purchase 71%.
  4. Findings: Identify the Cart→Checkout step as the highest drop-off (78% fall off). Segment by device type to find mobile conversion is 40% lower than desktop.
  5. Report: Present a funnel visualization with drop-off rates per stage, top exit segments, and 3 prioritized recommendations.
“检查我们结账漏斗的健康状况 — 我想看看人们在哪里流失。”
  1. 设置:调用
    findDataViews
    ,用户选择其电商数据视图。调用
    setDefaultSessionDataViewId
  2. 定义漏斗:询问“结账阶段有哪些?”用户回复:“产品浏览 → 加入购物车 → 开始结账 → 购买。”
  3. 分析:在过去30天内运行每个阶段转换的
    runReport
    。计算流失率:产品浏览→购物车22%,购物车→结账58%,结账→购买71%。
  4. 发现:识别购物车→结账步骤为最高流失点(78%的用户脱落)。按设备类型细分发现移动端转化率比桌面端低40%。
  5. 报告:展示带各阶段流失率的漏斗可视化、顶级流失细分群体,以及3个优先优化建议。

Recommendations Logic

建议逻辑

  • Worst step drop-off > 60%: "Critical leakage — this step is broken or the user expectation is misaligned. Prioritize UX investigation."
  • Mobile drop-off > 2× desktop: "Mobile UX at this step needs attention — consider a dedicated mobile flow or simplified form."
  • Specific channel drop-off > 40% worse than average: "Users from this channel may have mismatched intent — review landing page alignment."
  • Step 1 count < 1,000: "Funnel entry volume is too low for statistical confidence. Check the step definition or expand the date range."
  • 最糟糕步骤流失率 > 60%:“严重泄漏 — 此步骤存在问题或用户预期不符。优先开展UX调查。”
  • 移动端流失率 > 桌面端2倍:“此步骤的移动端UX需要关注 — 考虑专用移动端流程或简化表单。”
  • 特定渠道流失率比平均水平高 >40%:“该渠道的用户可能存在意图不匹配 — 检查着陆页的一致性。”
  • 步骤1数量 < 1000:“漏斗入口量过低,统计置信度不足。检查步骤定义或扩大日期范围。”