aa-top-movers-watchlist
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTop Movers Watchlist (Adobe Analytics)
异动指标监控清单(Adobe Analytics)
Identify which dimension items had the biggest increases and decreases for a
key metric between two time periods. Surface the top gainers and losers across
pages, channels, campaigns, products, or any other breakout dimension.
识别两个时间段内,哪些维度项的关键指标出现了最大幅度的增长或下降。展示页面、渠道、营销活动、产品或其他拆分维度下的Top涨幅项与跌幅项。
AA MCP Tools Used
使用的AA MCP工具
- — select report suite
findReportSuites - — set session context (reportSuiteId + globalCompanyId)
setSessionDefaults - — load calendar/timezone context
describeAa(REPORT_SUITE_CONTEXT_GUIDE) - — resolve the key metric ID
findMetrics - — discover available breakdown dimensions
findDimensions - — period A and period B for each dimension
runReport - — validate specific items if needed
searchDimensionItems
- — 选择报表套件
findReportSuites - — 设置会话上下文(reportSuiteId + globalCompanyId)
setSessionDefaults - — 加载日历/时区上下文
describeAa(REPORT_SUITE_CONTEXT_GUIDE) - — 解析关键指标ID
findMetrics - — 发现可用的拆分维度
findDimensions - — 获取每个维度在时间段A和时间段B的数据
runReport - — 按需验证特定维度项
searchDimensionItems
Phase 0 — Setup
阶段0 — 准备工作
-
Confirm report suite with/
findReportSuites.setSessionDefaults -
Callto load report suite context. Record:
describeAa(REPORT_SUITE_CONTEXT_GUIDE)- — first-day-of-week from the context guide. If the context guide returns no value, use Monday (ISO 8601) as the explicit deterministic default. This is not a fallback that drifts between runs: every run on the same report suite resolves to the same
WEEK_START_DOW, either from the context guide or from the fixed Monday default.WEEK_START_DOW - — report suite timezone.
TIMEZONE - —
WEEK_START_DOW_SOURCEif the value came from"context guide",describeAaif the context guide was silent and Monday was used. Surface this in the artifact footer so the source is auditable."default"
You will use these values in Phase 1 when defining Period A and Period B.
findReportSuites(globalCompanyId: "<gcid>")
setSessionDefaults(globalCompanyId: "<gcid>", reportSuiteId: "<rsid>")
describeAa(guideType: "REPORT_SUITE_CONTEXT_GUIDE")-
使用/
findReportSuites确认报表套件。setSessionDefaults -
调用加载报表套件上下文,并记录:
describeAa(REPORT_SUITE_CONTEXT_GUIDE)- — 上下文指南中的一周起始日。如果上下文指南未返回值,则明确使用周一(ISO 8601标准)作为确定性默认值。此默认值不会随运行变化:同一报表套件的每次运行都会解析为相同的
WEEK_START_DOW,要么来自上下文指南,要么使用固定的周一默认值。WEEK_START_DOW - — 报表套件时区。
TIMEZONE - — 如果值来自
WEEK_START_DOW_SOURCE则为describeAa,如果上下文指南未提供值而使用了周一默认值则为"context guide"。将此信息显示在报告页脚,以便追溯来源。"default"
这些值将在阶段1定义时间段A和时间段B时使用。
findReportSuites(globalCompanyId: "<gcid>")
setSessionDefaults(globalCompanyId: "<gcid>", reportSuiteId: "<rsid>")
describeAa(guideType: "REPORT_SUITE_CONTEXT_GUIDE")Phase 1 — Confirm Parameters
阶段1 — 确认参数
Ask the user:
-
Metric — "Which metric should I rank movers by? (visits, revenue, conversions, page views, etc.)" If not specified, default to.
metrics/visits -
Dimension — "Which dimension should I break down?
- Pages
- Marketing Channel
- Campaigns
- Products
- Traffic Sources
- Geographic Regions
- Entry Pages" If not specified, offer the above list.
-
Time periods — "What two periods should I compare?" Common defaults:
- This week vs. last week
- This month vs. last month
- Last 7 days vs. prior 7 days
- Last 30 days vs. prior 30 days
Calendar rule (mandatory): Period A and Period B MUST use the samefrom Phase 0 — both periods'WEEK_START_DOWfall on the same day-of-week, both are exactly equal length, and Period B ends immediately before Period A starts. Never mix conventions (e.g., a Mon–Sun Period A with a Sun–Sat Period B) within the same run. For custom date ranges, compute Period B as the equal-length window ending immediately before Period A starts.startDateSanity check before calling: confirmrunReportandperiodA.startDateare the same day-of-week and thatperiodB.startDate. If not, recompute.periodA.startDate - periodB.endDate == 1 day -
Item limit — how many top gainers and losers to show (default: 10 each).
-
Materiality threshold — minimum absolute value in Period A or B to be included (filters out noise from low-volume items). Default: exclude items with fewer than 100 visits (or equivalent) in both periods.
Confirm: "I'll show the top 10 gainers and losers for [metric] broken down
by [dimension], comparing [Period A] vs [Period B]."
询问用户:
-
指标 — “我应该依据哪个指标对异动项进行排名?(访问量、收入、转化量、页面浏览量等)” 如果未指定,默认使用。
metrics/visits -
维度 — “我应该按哪个维度拆分数据?
- 页面
- 营销渠道
- 营销活动
- 产品
- 流量来源
- 地理区域
- 入口页面” 如果未指定,提供上述列表供选择。
-
时间段 — “我应该对比哪两个时间段?” 常见默认选项:
- 本周 vs 上周
- 本月 vs 上月
- 最近7天 vs 之前7天
- 最近30天 vs 之前30天
日历规则(强制要求):时间段A和时间段B必须使用阶段0中确定的相同— 两个时间段的WEEK_START_DOW必须是同一星期几,长度完全相等,且时间段B在时间段A开始前结束。同一运行中绝不能混合使用不同的日期约定(例如,时间段A为周一至周日,而时间段B为周日至周六)。对于自定义日期范围,将时间段B计算为与时间段A长度相等且在其开始前结束的时间窗口。startDate调用前的合理性检查:确认runReport和periodA.startDate是同一星期几,且periodB.startDate。如果不满足,则重新计算。periodA.startDate - periodB.endDate == 1天 -
项数限制 — 显示多少个Top涨幅项和跌幅项(默认:各10个)。
-
重要性阈值 — 时间段A或B中需达到的最小绝对值(过滤低流量项带来的噪音)。默认:排除两个时间段中访问量(或等效指标)均少于100的项。
确认话术:“我将展示[指标]按[维度]拆分后的Top 10涨幅项和跌幅项,对比[时间段A]与[时间段B]。”
Phase 2 — Resolve Components
阶段2 — 解析组件
findMetrics(expansions: "componentType")
findDimensions(page: 1, limit: 100)Note:requires thefindMetricsparameter (useexpansionsor"componentType")."categories"requiresfindDimensionsandpage.limit
Record for the metric and dimension.
idfindMetrics(expansions: "componentType")
findDimensions(page: 1, limit: 100)注意:需要findMetrics参数(使用expansions或"componentType")。"categories"需要findDimensions和page参数。limit
记录指标和维度的。
idPhase 3 — Run Period Reports
阶段3 — 运行时间段报表
Run the metric for the selected dimension in both periods:
runReport(
metricIds: "<metricId>",
dimensionId: "<dimensionId>",
startDate: "<period A start>T00:00",
endDate: "<period A end>T23:59",
limit: 200
)
runReport(
metricIds: "<metricId>",
dimensionId: "<dimensionId>",
startDate: "<period B start>T00:00",
endDate: "<period B end>T23:59",
limit: 200
)Note:usesrunReport(notmetricIds) andmetricId/startDatein ISO 8601 format (endDate), not aYYYY-MM-DDTHH:mmparameter.dateRange
Use to capture enough items for a meaningful mover analysis.
limit: 200为选定维度在两个时间段内运行指标查询:
runReport(
metricIds: "<metricId>",
dimensionId: "<dimensionId>",
startDate: "<period A start>T00:00",
endDate: "<period A end>T23:59",
limit: 200
)
runReport(
metricIds: "<metricId>",
dimensionId: "<dimensionId>",
startDate: "<period B start>T00:00",
endDate: "<period B end>T23:59",
limit: 200
)注意:使用runReport(而非metricIds),且metricId/startDate采用ISO 8601格式(endDate),不使用YYYY-MM-DDTHH:mm参数。dateRange
使用以捕获足够多的项,确保异动分析有意义。
limit: 200Phase 4 — Compute Deltas and Rank
阶段4 — 计算差值并排名
For each dimension item present in either period:
- Period A value (or 0 if not in results)
- Period B value (or 0 if not in results)
- Absolute delta: Period A - Period B
- Percent change: delta / Period B × 100 (or "+100% new" if Period B = 0)
- Apply materiality filter: exclude items where max(Period A, Period B) < materiality threshold
Sort by absolute delta descending for gainers (positive delta).
Sort by absolute delta ascending for losers (negative delta).
Take top N from each list.
对于任一时间段中存在的每个维度项:
- 时间段A的值(如果未在结果中则为0)
- 时间段B的值(如果未在结果中则为0)
- 绝对差值:时间段A - 时间段B
- 变化百分比:差值 / 时间段B × 100(如果时间段B=0则显示"+100% new")
- 应用重要性过滤:排除时间段A和时间段B的最大值均低于重要性阈值的项
按绝对差值降序排列涨幅项(正差值)。
按绝对差值升序排列跌幅项(负差值)。
从每个列表中选取前N项。
Special cases
特殊情况
- New items (present in Period A only): flag as "New — no prior period data." Include if materiality threshold met.
- Disappeared items (present in Period B only, 0 in Period A): flag as "Dropped — no current period data."
- Near-zero items: items with <1% of total metric value — consider excluding from the watchlist unless they show very large percent changes.
- 新增项(仅在时间段A中存在):标记为"新增 — 无前期数据"。如果满足重要性阈值则纳入。
- 消失项(仅在时间段B中存在,时间段A中为0):标记为"已消失 — 无当前数据"。
- 近零项:指标值占总指标值不足1%的项 — 除非其变化百分比极大,否则考虑从监控清单中排除。
Phase 5 — Optional Multi-Dimension Watchlist
阶段5 — 可选的多维度监控清单
If the user wants movers across multiple dimensions (e.g., both pages AND
channels), repeat Phase 3–4 for each additional dimension. Each additional
dimension adds 2 more calls.
runReportrunReport(metricIds: "<metricId>", dimensionId: "variables/marketingchannel",
startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
runReport(metricIds: "<metricId>", dimensionId: "variables/page",
startDate: "<A>T00:00", endDate: "<A>T23:59", ...)Group results into separate tables by dimension in the HTML report.
如果用户希望查看跨多个维度的异动项(例如,同时查看页面和渠道),则为每个额外维度重复阶段3-4的操作。每个额外维度会增加2次调用。
runReportrunReport(metricIds: "<metricId>", dimensionId: "variables/marketingchannel",
startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
runReport(metricIds: "<metricId>", dimensionId: "variables/page",
startDate: "<A>T00:00", endDate: "<A>T23:59", ...)在HTML报告中按维度将结果分组为单独的表格。
Phase 6 — Generate HTML Report
阶段6 — 生成HTML报告
Build the movers report inline and write to
.
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html在线构建异动报告,并写入。
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.htmlRendering rules — apply consistently across runs
渲染规则 — 运行间保持一致
Two runs of this skill on the same report suite + metric/dimension + period
must render identically (modulo the generation timestamp). The rules below
pin the formatting choices that the AI would otherwise drift on.
同一报表套件、指标/维度、时间段下,此技能的两次运行必须生成完全相同的报告(仅生成时间戳可不同)。以下规则固定了AI可能会随意变动的格式选择。
Number formatting
数字格式
- KPI values (the big number in each summary tile, and per-row mover
values) — use full digits with thousands separators (,
8,160,77,584). Do NOT use SI suffixes like1,250,000orK, even for large values. Stakeholders want exact numbers, not abbreviations.M - Percent change (in pills and narrative bullets) — always one decimal
place, rounded half-away-from-zero. For example, displays as
−23.55%, never−23.6%. Compute on full-precision values; round only at display time.−23.5% - Percentage-point change (for already-percentage metrics like Conversion
Rate or Bounce Rate) — same rounding, suffix . Example:
pp.+0.40 pp - Currency — prefix with thousands separators and no decimals for values ≥ $100 (
$); cents only when value < $100 ($1,240,000).$45.20
- KPI值(每个汇总卡片中的大数字,以及每行异动项的值) — 使用带千位分隔符的完整数字(、
8,160、77,584)。即使数值很大,也不要使用1,250,000或K等国际单位制后缀。相关人员需要精确数字,而非缩写。M - 变化百分比(在标签和叙述性项目符号中) — 始终保留一位小数,采用四舍五入到最近整数的方式。例如,显示为
−23.55%,绝不能显示为−23.6%。基于全精度值计算;仅在显示时进行四舍五入。−23.5% - 百分点变化(针对转化率或跳出率等本身为百分比的指标) — 采用相同的四舍五入规则,后缀为。示例:
pp。+0.40 pp - 货币 — 以为前缀,带千位分隔符;数值≥$100时不保留小数(
$);数值<$100时保留分位($1,240,000)。$45.20
Null / missing data handling
空值/缺失数据处理
A KPI tile or mover row must reflect what the report suite actually returned.
The AI must not silently substitute a different metric or hide a tile to
make the report look cleaner.
- Both periods return 0 or NULL for the tracked metric in a summary tile:
render the tile with =
kpi-value, pill classData unavailable, pill textflat, and⚠ N/Atext =prior. The tile stays in the grid; do not omit it.Both periods returned no data — validate instrumentation - One period returns valid data, the other 0 / NULL: render the tile with
the valid value as , pill class
kpi-value, pill textflat, and⚠ N/Atext =prior.Prior {period_noun}: no data - Never substitute a different metric (e.g., switching from Revenue to Orders because Revenue came back $0). The metric being analyzed MUST be the metric rendered.
KPI卡片或异动项行必须如实反映报表套件返回的数据。AI不得擅自替换为其他指标或隐藏卡片来美化报告。
- 汇总卡片中两个时间段的跟踪指标均返回0或NULL:卡片的显示为
kpi-value,标签样式为数据不可用,标签文本为flat,⚠ N/A文本为prior。卡片保留在网格中;不得省略。两个时间段均无数据 — 请验证数据采集 - 一个时间段返回有效数据,另一个返回0/NULL:卡片的显示有效数据,标签样式为
kpi-value,标签文本为flat,⚠ N/A文本为prior。前期{时间段名词}:无数据 - 绝不能替换为其他指标(例如,因收入返回$0而切换为订单量)。分析的指标必须与报告中显示的指标一致。
HTML template
HTML模板
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top Movers — {ORG_NAME} — {METRIC_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: 600px;
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; }
/* === Period strip === */
.period-bar { background: var(--surface); border-radius: 8px;
padding: 14px 22px; margin-bottom: 28px;
font-size: 13px; color: var(--ink-muted);
box-shadow: 0 1px 3px rgba(0,0,0,.04);
border-left: 3px solid var(--accent-red); }
.period-bar strong { color: var(--ink); font-weight: 600; }
/* === Dimension section === */
.dim-section { margin-top: 32px; }
.dim-section > h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 22px; font-weight: 700; color: var(--ink);
margin-bottom: 16px; padding-bottom: 10px;
border-bottom: 1px solid var(--border); }
/* === Gainers / Losers dual panels === */
.movers-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px;
margin-bottom: 32px; }
.movers-panel { background: var(--surface); border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.05); overflow: hidden; }
.movers-panel-header { padding: 18px 24px; border-bottom: 1px solid var(--border); }
.movers-panel-header.gainers { border-top: 3px solid var(--accent-green); }
.movers-panel-header.losers { border-top: 3px solid var(--accent-red); }
.movers-panel-header h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 17px; font-weight: 700; }
.movers-panel-header.gainers h2 { color: var(--accent-green); }
.movers-panel-header.losers h2 { color: var(--accent-red); }
/* === Tables === */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #faf8f4; padding: 10px 18px;
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: 10px 18px; border-bottom: 1px solid #f4f1eb; }
tbody tr:last-child td { border-bottom: none; }
.delta-pos { color: var(--accent-green); font-weight: 700; }
.delta-neg { color: var(--accent-red); font-weight: 700; }
/* === Rank badges (circular) === */
.rank-badge { display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px;
border-radius: 50%;
background: var(--accent-red); color: #fff;
font-size: 11px; font-weight: 700;
margin-right: 8px; vertical-align: middle; }
.rank-badge.gain { background: var(--accent-green); }
.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,.35); }
footer { text-align: center; padding: 32px 24px;
font-size: 12px; color: var(--ink-muted); }
@media (max-width: 700px) { .movers-grid { grid-template-columns: 1fr; } }
/* === Print === */
@media print {
nav { display: none; position: static; }
header { padding: 36px 32px 28px; }
header h1 { font-size: 42px; }
.back-top { display: none; }
.movers-grid { page-break-inside: avoid; }
.movers-panel, .period-bar {
box-shadow: none; border: 1px solid var(--border);
}
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="eyebrow">Top Movers Watchlist</div>
<h1>{ORG_NAME} Top Movers</h1>
<p class="lede">Biggest gainers and decliners for {METRIC_NAME} between {PERIOD_A} and {PERIOD_B}.</p>
<div class="meta">
<span><span class="icon">📅</span> {PERIOD_A} vs {PERIOD_B}</span>
<span><span class="icon">📊</span> {REPORT_SUITE}</span>
<span><span class="icon">🕔</span> Prepared {GENERATED_DATE}</span>
</div>
</div>
</header>
<nav>
<a href="#summary">Summary</a>
<!-- one nav link per dimension -->
</nav>
<div class="container">
<div id="summary" class="period-bar">
<strong>Metric:</strong> {METRIC_NAME} |
<strong>Period A:</strong> {PERIOD_A} |
<strong>Period B:</strong> {PERIOD_B} |
<strong>Materiality:</strong> ≥{THRESHOLD} {METRIC_UNIT}
</div>
<!-- One .dim-section per dimension -->
<div id="dim-{DIM_ID}" class="dim-section">
<h2>{DIMENSION_NAME}</h2>
<div class="movers-grid">
<div class="movers-panel">
<div class="movers-panel-header gainers"><h2>▲ Top Gainers</h2></div>
<table>
<thead><tr>
<th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
<th>Delta</th><th>% Change</th>
</tr></thead>
<tbody><!-- gainer rows: use <span class="rank-badge gain">N</span> --></tbody>
</table>
</div>
<div class="movers-panel">
<div class="movers-panel-header losers"><h2>▼ Top Losers</h2></div>
<table>
<thead><tr>
<th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
<th>Delta</th><th>% Change</th>
</tr></thead>
<tbody><!-- loser rows: use <span class="rank-badge">N</span> --></tbody>
</table>
</div>
</div>
</div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">
⇧
</button>
<footer>Top Movers Watchlist — {ORG_NAME} — Generated {GENERATED_DATE}</footer>
</body>
</html>Section titles — no phase prefix: Section headings in the HTML report must not include
the phase number. Use the plain section name only (e.g., "Top Movers" not "Phase 3 — Top Movers",
"Dimension Breakdown" not "Phase 4 — Dimension Breakdown").
Write to and open:
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.htmlbash
open /tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.htmlhtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top Movers — {ORG_NAME} — {METRIC_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: 600px;
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; }
/* === Period strip === */
.period-bar { background: var(--surface); border-radius: 8px;
padding: 14px 22px; margin-bottom: 28px;
font-size: 13px; color: var(--ink-muted);
box-shadow: 0 1px 3px rgba(0,0,0,.04);
border-left: 3px solid var(--accent-red); }
.period-bar strong { color: var(--ink); font-weight: 600; }
/* === Dimension section === */
.dim-section { margin-top: 32px; }
.dim-section > h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 22px; font-weight: 700; color: var(--ink);
margin-bottom: 16px; padding-bottom: 10px;
border-bottom: 1px solid var(--border); }
/* === Gainers / Losers dual panels === */
.movers-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px;
margin-bottom: 32px; }
.movers-panel { background: var(--surface); border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.05); overflow: hidden; }
.movers-panel-header { padding: 18px 24px; border-bottom: 1px solid var(--border); }
.movers-panel-header.gainers { border-top: 3px solid var(--accent-green); }
.movers-panel-header.losers { border-top: 3px solid var(--accent-red); }
.movers-panel-header h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 17px; font-weight: 700; }
.movers-panel-header.gainers h2 { color: var(--accent-green); }
.movers-panel-header.losers h2 { color: var(--accent-red); }
/* === Tables === */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #faf8f4; padding: 10px 18px;
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: 10px 18px; border-bottom: 1px solid #f4f1eb; }
tbody tr:last-child td { border-bottom: none; }
.delta-pos { color: var(--accent-green); font-weight: 700; }
.delta-neg { color: var(--accent-red); font-weight: 700; }
/* === Rank badges (circular) === */
.rank-badge { display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px;
border-radius: 50%;
background: var(--accent-red); color: #fff;
font-size: 11px; font-weight: 700;
margin-right: 8px; vertical-align: middle; }
.rank-badge.gain { background: var(--accent-green); }
.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,.35); }
footer { text-align: center; padding: 32px 24px;
font-size: 12px; color: var(--ink-muted); }
@media (max-width: 700px) { .movers-grid { grid-template-columns: 1fr; } }
/* === Print === */
@media print {
nav { display: none; position: static; }
header { padding: 36px 32px 28px; }
header h1 { font-size: 42px; }
.back-top { display: none; }
.movers-grid { page-break-inside: avoid; }
.movers-panel, .period-bar {
box-shadow: none; border: 1px solid var(--border);
}
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="eyebrow">Top Movers Watchlist</div>
<h1>{ORG_NAME} Top Movers</h1>
<p class="lede">Biggest gainers and decliners for {METRIC_NAME} between {PERIOD_A} and {PERIOD_B}.</p>
<div class="meta">
<span><span class="icon">📅</span> {PERIOD_A} vs {PERIOD_B}</span>
<span><span class="icon">📊</span> {REPORT_SUITE}</span>
<span><span class="icon">🕔</span> Prepared {GENERATED_DATE}</span>
</div>
</div>
</header>
<nav>
<a href="#summary">Summary</a>
<!-- one nav link per dimension -->
</nav>
<div class="container">
<div id="summary" class="period-bar">
<strong>Metric:</strong> {METRIC_NAME} |
<strong>Period A:</strong> {PERIOD_A} |
<strong>Period B:</strong> {PERIOD_B} |
<strong>Materiality:</strong> ≥{THRESHOLD} {METRIC_UNIT}
</div>
<!-- One .dim-section per dimension -->
<div id="dim-{DIM_ID}" class="dim-section">
<h2>{DIMENSION_NAME}</h2>
<div class="movers-grid">
<div class="movers-panel">
<div class="movers-panel-header gainers"><h2>▲ Top Gainers</h2></div>
<table>
<thead><tr>
<th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
<th>Delta</th><th>% Change</th>
</tr></thead>
<tbody><!-- gainer rows: use <span class="rank-badge gain">N</span> --></tbody>
</table>
</div>
<div class="movers-panel">
<div class="movers-panel-header losers"><h2>▼ Top Losers</h2></div>
<table>
<thead><tr>
<th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
<th>Delta</th><th>% Change</th>
</tr></thead>
<tbody><!-- loser rows: use <span class="rank-badge">N</span> --></tbody>
</table>
</div>
</div>
</div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">
⇧
</button>
<footer>Top Movers Watchlist — {ORG_NAME} — Generated {GENERATED_DATE}</footer>
</body>
</html>章节标题 — 无阶段前缀:HTML报告中的章节标题不得包含阶段编号。仅使用纯章节名称(例如,“异动指标Top榜单”而非“阶段3 — 异动指标Top榜单”,“维度拆分”而非“阶段4 — 维度拆分”)。
写入并打开:
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.htmlbash
open /tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.htmlInline Summary (Always Deliver)
在线摘要(必须提供)
Always follow the HTML report with a brief text callout:
Top Movers: [Metric] — [Period A] vs [Period B]
GAINERS (by [Dimension])
1. [Item A] +12,400 visits (+34%)
2. [Item B] +8,200 visits (+21%)
3. [Item C] +5,100 visits (+18%)
LOSERS
1. [Item X] -9,800 visits (-41%)
2. [Item Y] -4,200 visits (-18%)
3. [Item Z] -2,900 visits (-12%)
Key takeaway: [1 sentence explanation of the story]HTML报告后必须附带简短的文本摘要:
异动指标Top榜单: [指标] — [时间段A] vs [时间段B]
涨幅项(按[维度])
1. [项A] +12,400访问量 (+34%)
2. [项B] +8,200访问量 (+21%)
3. [项C] +5,100访问量 (+18%)
跌幅项
1. [项X] -9,800访问量 (-41%)
2. [项Y] -4,200访问量 (-18%)
3. [项Z] -2,900访问量 (-12%)
关键结论: [1句话解释核心发现]Guardrails
约束规则
- Always apply a materiality threshold to suppress noise from items with negligible volume.
- Flag new items (no prior period) and disappeared items (no current period) explicitly — they often tell a significant story.
- For percent-change ranking, use absolute delta (not percent) as primary sort to avoid surfacing low-volume items with misleading large percentages.
- Cap dimension list at 6 for call efficiency. Offer to run additional dimensions on request.
- 始终应用重要性阈值,以抑制低流量项带来的噪音。
- 明确标记新增项(无前期数据)和消失项(无当前数据) — 这些项往往能反映重要情况。
- 对于变化百分比排名,以绝对差值(而非百分比)作为主要排序依据,避免因低流量项的误导性高百分比而使其被突出显示。
- 为提高调用效率,维度列表最多保留6个。可根据用户请求运行额外维度。
Example Interaction
示例交互
"What pages were up and down the most last week?"
- Confirm report suite.
- Confirm metric: page views. Dimension: page name. Period: last 7 days vs prior 7 days. Limit: top 10 each.
- Run 2 reports for page × visits.
- Compute deltas, apply materiality (min 500 page views in either period).
- Generate HTML report and deliver inline summary.
- "Top gainer: /products/new-feature with +18,200 page views (+312%). Top loser: /promo/expired-campaign with -9,400 page views (-88%)."
“上周哪些页面的流量涨幅和跌幅最大?”
- 确认报表套件。
- 确认指标:页面浏览量。维度:页面名称。时间段:最近7天 vs 之前7天。限制:各Top 10。
- 运行2次页面×访问量的报表查询。
- 计算差值,应用重要性阈值(任一时间段页面浏览量≥500)。
- 生成HTML报告并提供在线摘要。
- “Top涨幅项:/products/new-feature,页面浏览量增加18,200次(+312%)。Top跌幅项:/promo/expired-campaign,页面浏览量减少9,400次(-88%)。”