关于 cja-top-movers-watchlist
这是一项 Customer Journey Analytics 技能,可比较两个时段中某项指标在不同维度上的变化并排列增幅与降幅。它还会识别新增和消失的项目,并生成 HTML 报告,便于集中查看变化。
主要功能
- 01比较两个时段的维度项目
- 02排列增幅最大和降幅最大项
- 03识别新增和消失项目
- 04计算绝对变化和百分比变化
- 05生成 HTML 报告和内联摘要
适用场景
- 01查看周环比变化最大的营销渠道
- 02找出指标变化最大的页面或产品
- 03发现新出现或消失的维度项目
MCP 相关
使用 Customer Journey Analytics MCP 工具解析指标和维度、检查维度值,并获取两个时段的报表。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ adobe/ skills --skill cja-top-movers-watchlist安装后在 Claude Code 中输入/cja-top-movers-watchlist确认已加载。
附带文件evals/evals.json
详细介绍
Top Movers 监控清单(客户旅程分析)
展示任意维度下某项指标在两个时间段内涨幅最大和跌幅最大的项目。输出会明确告知用户哪些项目发生了变化、变化幅度是多少,以及哪些项目是新出现或已完全消失的——这通常是最值得关注的信号。
与完整的异常排查相比,这项技能更快、更有针对性。当用户想要浏览整体变化,而不是深入调查单个异常时,请使用它。
使用的 CJA MCP 工具
describeCja(DATAVIEW_CONTEXT_GUIDE)— 加载数据视图的日历和时区信息findMetrics— 确定要监控的指标findCalculatedMetrics— 如果该指标是自定义 KPIfindDimensions— 确定用于细分的维度runReport— 获取两个时间段的维度指标数据searchDimensionItems— 用户指定名称时,验证维度值
阶段 0 — 设置
- 根据需要调用
findDataViews和setDefaultSessionDataViewId。 - 调用
describeCja("DATAVIEW_CONTEXT_GUIDE")加载数据视图上下文。将一周的第一天记为WEEK_START_DOW,时区记为TIMEZONE。如果上下文指南未返回一周起始日,则默认为星期一(ISO 8601)。你将在阶段 1.3 中使用这两个值。
阶段 1 — 确认输入
1.1 指标
如果用户指定了指标,请解析该指标:
findMetrics(search: "<user's metric name>")
如果未指定,请根据使用情况推荐最常用的 3 个指标:
“你想跟踪哪项指标?根据团队最常使用的指标,我可以推荐:Sessions、Revenue、Orders。”
1.2 维度(细分依据)
常见维度选项及其典型用途:
| 维度 | 用途 |
|---|---|
| Marketing Channel | “哪些渠道发生了变化?” |
| Page Name | “哪些页面正在呈现趋势?” |
| Campaign | “哪些营销活动有所改善?” |
| Product | “哪些产品的吸引力上升或下降了?” |
| Country / Region | “哪些市场发生了变化?” |
| Device Type | “移动设备还是桌面设备的数据发生了变化?” |
| Referring Domain | “哪些引荐来源发生了变化?” |
如果用户未指定,请询问:
“你想按哪个维度细分?例如营销渠道、页面、营销活动、国家/地区或产品。”
调用 findDimensions(search: "<dimension keyword>") 以确定维度 ID。
1.3 时间段
定义时间段 A(当前)和时间段 B(对比)。默认值:
- 时间段 A:本周(或最近 7 天)
- 时间段 B:上周(或此前的 7 天)
如果用户指定“本月与上月对比”或自定义日期范围,请按要求映射。运行报告前务必确认时间段:
“我会比较本周(Mar 13–19)与上周(Mar 6–12)。这样可以吗?”
日历规则(必须遵守):
使用阶段 0 中的 WEEK_START_DOW 来确定“周”的定义。时间段 A 和时间段 B 必须使用相同的一周起始日——也就是说,两个时间段的 startDate 都必须落在同一星期几,长度必须完全相同,并且时间段 B 必须在时间段 A 开始前一天结束。一次运行中不得混用不同的定义(例如,时间段 A 为周一至周日,而时间段 B 为周日至周六)。选定一次边界后,再据此推导两个时间段。对于自定义日期范围,将时间段 B 计算为在时间段 A 开始前一天结束、且长度相同的时间窗口。
**调用 runReport 前的合理性检查:**确认 periodA.startDate 和 periodB.startDate 是同一星期几,并且 periodA.startDate - periodB.endDate == 1 day。如果不满足,请重新计算。
阶段 2 — 获取两个时段的数据
运行两份报告,每个时段一份,并使用相同的维度细分:
runReport(
dimensionIds: "<dimension id>",
metricIds: "<metric id>",
startDate: "<period A start>T00:00:00",
endDate: "<period A end>T23:59:59",
page: 0,
limit: 50
)
runReport(
dimensionIds: "<dimension id>",
metricIds: "<metric id>",
startDate: "<period B start>T00:00:00",
endDate: "<period B end>T23:59:59",
page: 0,
limit: 50
)
使用 limit: 50,以捕获足够多的条目,从而发现有意义的变化。
如果用户的维度有数千个值(例如,页面名称),请按时段 A 的数据量
取前 100 个,以确保比较有意义。
行数据位于 rows 数组中——每行都有 value(维度项名称)
和 data[0](指标值)。维度基数没有限制,
但结果默认按指标降序排序,这正是你需要的顺序。
运行前务必使用 findDimensions(searchQuery: "<name>") 验证维度 ID;
维度 ID 可能与预期不同
(例如,variables/marketing_channel 而不是 variables/marketingchannel)。
阶段 3 — 计算排名
将两组结果按维度值合并为一张统一的表:
对于任一时段中出现的每个维度值:
valueA= 时段 A 的指标值(若不存在则为 0)valueB= 时段 B 的指标值(若不存在则为 0)delta= valueA − valueBpctChange= 若 valueB > 0,则为 (delta / valueB) × 100;否则为“新出现”status:- 在 A 中出现但不在 B 中 → 新出现(本时段出现)
- 在 B 中出现但不在 A 中 → 已消失(本时段不再出现)
- 两者中均出现 → 常规变动项
排序方式:
- 增长最多:按 delta 降序排序(绝对增长最大的优先)
- 下降最多:按 delta 升序排序(绝对下降最大的优先)
- 百分比增长最多:按 pctChange 降序排序
- 百分比下降最多:按 pctChange 升序排序
每个列表最多显示 前 10 项。新出现和已消失的项目应各自单独分组, 不受数量限制(它们始终是值得关注的信号)。
阶段 4 — 生成 HTML 报告
在行内生成变动项报告,并写入
/tmp/cja_top_movers_report_<YYYY-MM-DD_HHMMSS>.html。
渲染规则 — 每次运行均一致应用
在相同的数据视图、指标/维度和时段上运行两次此技能,渲染结果必须 完全一致(生成时间戳除外)。以下规则用于固定格式选择,避免 AI 自行变通。
数字格式
- KPI 值(每个摘要卡片中的大号数字,以及每行变动项的值)——使用带千位分隔符的完整数字(
8,160、77,584、1,250,000)。即使数值很大,也不要使用K或M等 SI 后缀。利益相关者需要精确数字,而不是缩写。 - 百分比变化(徽标和叙述性要点中)——始终保留一位小数,按远离零方向舍入。例如,
−23.55%显示为−23.6%,绝不显示为−23.5%。使用完整精度计算;仅在显示时舍入。 - 百分点变化(用于转化率或跳出率等本身为百分比的指标)——舍入规则相同,后缀为
pp。示例:+0.40 pp。 - 货币——使用
$前缀和千位分隔符;数值 ≥ $100 时不显示小数位($1,240,000);数值 < $100 时显示美分($45.20)。
空值 / 缺失数据处理
KPI 卡片或变动项行必须如实反映数据视图实际返回的内容。 AI 不得悄悄替换其他指标,也不得为了让报告看起来更简洁而隐藏卡片。
- 两个时段的跟踪指标均返回 0 或 NULL:在摘要卡片中将
kpi-value渲染为Data unavailable,徽标类设为flat,徽标文本设为⚠ N/A,prior文本设为Both periods returned no data — validate instrumentation。卡片仍须保留在网格中;不得省略。 - 一个时段返回有效数据,另一个时段返回 0 / NULL:将有效值作为
kpi-value,徽标类设为flat,徽标文本设为⚠ N/A,prior文本设为Prior {period_noun}: no data。 - 绝不替换为其他指标(例如,Revenue 返回 $0 时改用 Orders)。正在分析的指标必须是渲染的指标。
HTML 模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top Movers — {ORG_NAME} — {METRIC_NAME} by {DIMENSION_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">
<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: 34px;
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); }
/* === Dual risers / fallers panels === */
.two-col { display: grid; grid-template-columns: 1fr 1fr;
gap: 20px; margin-bottom: 22px; }
@media (max-width: 760px) { .two-col { grid-template-columns: 1fr; } }
/* === 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; }
/* Top-border accent on risers/fallers tiles */
.section.risers { border-top: 3px solid var(--accent-green); }
.section.fallers { border-top: 3px solid var(--accent-red); }
.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; }
.section.risers .section-header h2 { color: var(--accent-green); }
.section.fallers .section-header h2 { color: var(--accent-red); }
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; }
.delta-up { color: var(--accent-green); font-weight: 600; }
.delta-down { color: var(--accent-red); font-weight: 600; }
.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.blue { background: #eef2f8; color: #2c4a7a; }
.badge.grey { background: #f1efea; color: var(--ink-muted); }
.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, .section {
box-shadow: none; border: 1px solid var(--border);
}
.back-top { display: none; }
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="eyebrow">Top Movers Report</div>
<h1>{ORG_NAME} Top Movers</h1>
<p class="lede">Biggest gainers and decliners for {METRIC_NAME} by {DIMENSION_NAME}, {PERIOD_A_LABEL} vs {PERIOD_B_LABEL}.</p>
<div class="meta">
<span><span class="icon">📅</span> {PERIOD_A_LABEL}</span>
<span><span class="icon">📊</span> {DATA_VIEW}</span>
<span><span class="icon">🕔</span> Prepared {GENERATED_DATE}</span>
</div>
</div>
</header>
<nav>
<a href="#summary">Summary</a>
<a href="#gainers">Gainers</a>
<a href="#decliners">Decliners</a>
<a href="#newdisappeared">New & Gone</a>
<a href="#fulltable">Full Table</a>
</nav>
<div class="container">
<div class="section-label">Watchlist Summary</div>
<div id="summary" class="kpi-row">
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">Total Items Compared</div></div>
<div class="kpi-value">{TOTAL_ITEMS}</div>
<span class="prior">Dimension values in scope</span>
</div>
<div class="kpi-tile up">
<div class="kpi-head"><div class="kpi-label">Biggest Gain</div></div>
<div class="kpi-value">{TOP_GAINER_VALUE}</div>
<span class="pill up">▲ {TOP_GAINER_NAME}</span>
</div>
<div class="kpi-tile down">
<div class="kpi-head"><div class="kpi-label">Biggest Drop</div></div>
<div class="kpi-value">{TOP_DECLINER_VALUE}</div>
<span class="pill down">▼ {TOP_DECLINER_NAME}</span>
</div>
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">New Entries</div></div>
<div class="kpi-value">{NEW_ENTRIES}</div>
<span class="prior">Appeared this period</span>
</div>
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">Disappeared</div></div>
<div class="kpi-value">{DISAPPEARED}</div>
<span class="prior">Dropped out this period</span>
</div>
</div>
<div class="two-col">
<div id="gainers" class="section risers">
<div class="section-header" onclick="toggle('gain-body')">
<h2>▲ Top 10 Gainers</h2>
<span id="gain-body-icon">▾</span>
</div>
<div id="gain-body">
<table>
<thead><tr>
<th>Item</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Delta</th>
<th>% Change</th>
</tr></thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div id="decliners" class="section fallers">
<div class="section-header" onclick="toggle('dec-body')">
<h2>▼ Top 10 Decliners</h2>
<span id="dec-body-icon">▾</span>
</div>
<div id="dec-body">
<table>
<thead><tr>
<th>Item</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Delta</th>
<th>% Change</th>
</tr></thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div id="newdisappeared" class="section">
<div class="section-header" onclick="toggle('nd-body')">
<h2>New Entries & Disappeared Items</h2>
<span id="nd-body-icon">▾</span>
</div>
<div id="nd-body">
<table>
<thead><tr>
<th>Item</th>
<th>Status</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Notes</th>
</tr></thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div id="fulltable" class="section">
<div class="section-header" onclick="toggle('full-body')">
<h2>Full Comparison Table</h2>
<span id="full-body-icon">▾</span>
</div>
<div id="full-body">
<table>
<thead><tr>
<th>Item</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Delta</th>
<th>% Change</th>
<th>Status</th>
</tr></thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">↑</button>
<footer>Top Movers — {ORG_NAME} — Generated {GENERATED_DATE}</footer>
<script>
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>
工作流摘要
- 确认指标、维度和两个时间段。
- 针对时间段 A 运行
runReport,按维度细分(上限为 50)。 - 使用相同参数针对时间段 B 运行
runReport。 - 按维度值进行合并;计算 delta、pctChange 和 status。
- 分类为:涨幅最大的前 10 项、跌幅最大的前 10 项、新增项、消失项。
- 内联生成 HTML 报告,写入
/tmp/cja_top_movers_report_<YYYY-MM-DD_HHMMSS>.html。 - 使用
open /tmp/cja_top_movers_report_<YYYY-MM-DD_HHMMSS>.html打开。 - 提供 3 行内联摘要:"涨幅最大:X(+Y%),跌幅最大:Z(−W%)。 N 个新项目进入前 50;M 个项目消失。"
重要注意事项
- 只读监控。 切勿修改区段、指标或项目定义。
- 运行前确认指标和区段范围。 对于含糊的请求("监控所有内容"),需要进一步明确——询问哪些维度和指标最重要。
- 使用一致的对比时间范围。 "涨跌幅最大的项目"必须对比时长相同的时间段;时间范围不匹配会产生错误信号。
- 区分噪声和信号。 流量极低的维度值(例如,只有 5 次访问的区段)可能出现 500% 的波动——应用最低阈值(例如至少 100 个会话)后再标记为涨跌幅较大的项目。
- 限制监控列表的规模。 默认展示涨跌幅最大的前 10–20 项;向用户展示 100 个维度项会适得其反。
- 结合背景解读变化。 尽可能注明可能解释变化的已知业务事件(营销活动、发布、服务中断)。
交互示例
"上周与前一周相比,哪些营销渠道的变化最大?"
- 指标:会话(默认使用量最高的指标)
- 维度:营销渠道
- 时间段 A:上周;时间段 B:前一周
- 运行两份报告并合并结果
- 涨幅最大:付费社交 +32%,自然搜索 +8%
- 跌幅最大:电子邮件 −18%,直接访问 −5%
- 新增:联盟营销(前一周未进入前 50)
- 生成并打开报告
- 内联摘要:"付费社交涨幅最大(+32%)。电子邮件是 跌幅最大的渠道(−18%)。联盟营销渠道首次进入 排名。"