1. 项目概述用 Allure 给 Robot Framework 的 API 测试装上“可视化仪表盘”你写完一套 Robot Framework 的 API 测试用例跑完robot --outputdir results tests/得到一个output.xml和一堆日志——但团队负责人问你“这次回归到底覆盖了哪些接口失败的请求里是参数错了、状态码不对还是响应体字段缺失有没有人能一眼看出哪个模块最近质量在下滑”你翻着report.html里密密麻麻的测试名再点开log.html逐条查 request/response手心开始冒汗。这不是测试没写好是报告没“说话”。而“Allure Robot Framework”这个组合就是让测试结果自己开口讲清楚故事的那套工具链。核心关键词robot framework api和allure在这里不是两个孤立名词的拼接而是一次精准的能力嫁接Robot Framework 提供结构清晰、关键字驱动、天然支持 HTTP 库如RequestsLibrary的 API 测试骨架Allure 则负责把骨架上长出的每一块肌肉——请求头、入参、响应体、断言路径、耗时分布、环境元数据——全部采集、归类、打标、渲染成可交互、可钻取、可归因的可视化视图。它不替代你的测试逻辑而是把你写下的每一行Should Be Equal As Strings ${resp.status_code} 200翻译成一张带时间轴的“接口健康热力图”把Log To Console ${resp.json()}转化为可折叠展开的原始 JSON 树形结构。适合谁不是只给测试工程师看的——开发能快速定位自己改坏的接口产品经理能验证新功能是否按预期返回字段运维能关联测试失败与部署版本甚至客户成功团队也能用它生成简洁的交付质量简报。它解决的从来不是“能不能测”而是“测得清不清、看得懂不懂、追得回不回”。我第一次在某跨平台系统项目中落地这套方案时团队刚经历一次线上支付回调超时事故。复盘会上大家争论焦点是“到底是上游签名验签逻辑变了还是我们下游解析响应格式崩了”。翻旧测试报告只有绿色/红色方块和一行“Keyword Should Be Equal As Strings failed”没人记得当时用的是哪版 OpenAPI Spec也没人保存过那次失败的完整响应快照。后来我们强制接入 Allure所有 API 测试用例执行时自动抓取request和response全量内容并绑定 Git Commit ID 和 Jenkins 构建号。再遇到类似问题直接在 Allure 报告里筛选“支付回调”标签按时间倒序点开最近三次失败用例三秒内并排对比 request headers 的X-Signature生成方式、response body 的result_code字段值变化趋势结论当场拍板。这背后没有魔法只有把本该记录却常被忽略的上下文用标准化方式固化下来。接下来我们就从设计思路、细节实现、实操踩坑到团队协作价值一层层拆开这个组合拳怎么真正打准、打稳、打出效果。2. 整体设计与思路拆解为什么选 Allure 而不是内置报告或 Jenkins 插件2.1 不是“多一个报告”而是构建可追溯的质量证据链Robot Framework 自带的report.html和log.html是合格的调试工具但它们本质是线性日志的静态快照。report.html只告诉你“通过/失败/跳过”的总数和占比log.html虽然详细但信息是平铺的、无结构的、不可检索的。比如你有 200 个 API 测试用例其中 3 个失败log.html会把所有 200 条执行日志混在一起滚动你要手动 CtrlF 搜索 “FAIL”再逐个点开看上下文。而 Allure 的设计哲学是“用元数据驱动可视化”——它要求你在测试执行过程中主动或被动地注入结构化信息这个用例属于哪个 Epic业务模块关联哪个 User Story需求编号测试的是哪个 API 端点请求方法是什么预期状态码是多少实际响应体里关键字段的值是什么这些信息不是事后补录而是在测试脚本运行时由 Allure 的适配器实时捕获并序列化进allure-results/目录下的 JSON 文件中。最终生成的报告就是这些元数据的聚合视图你可以按feature标签筛选“用户中心”模块再按severity筛选“critical”级别用例再点开某个失败用例直接看到带语法高亮的 request curl 命令、格式化后的 response JSON、以及每个Should Contain断言对应的 actual/expected 值对比。这不是炫技这是把每次测试执行变成一份自带上下文、可审计、可回放的“质量工单”。2.2 为什么 Allure 胜过 Jenkins 内置报告或自研 Dashboard有人会问Jenkins 本身就有测试报告插件或者我们自己搭个 Grafana 看成功率趋势不也行这里的关键差异在于“粒度”和“深度”。Jenkins 的 JUnit 插件只能解析testcase标签里的name、time、failure文本它不知道failure里写的 “Expected active, got inactive” 对应的是用户状态字段它更无法展示请求体里那个导致 400 的 malformed JSON。而 Allure 的 JSON 结构是深度嵌套的一个step可以包含子step可以附加attachment二进制文件或文本快照可以标记parameter如urlhttps://api.example.com/v1/users/123。这意味着当你的测试用例调用Create User接口时Allure 报告里不仅能显示“创建用户成功”还能展开看到Step 1:POST /v1/userswith body{name:test,email:testex.com}Step 2:Extract user_id from response→123Step 3:GET /v1/users/123→ response status200, body{id:123,name:test,...}Step 4:Assert name equals test→ ✅Attachment:Response Body (JSON)→ 可折叠的高亮 JSON 树这种能力是任何基于 XML/HTML 解析的通用报告工具无法原生支持的。它要求测试框架和报告工具之间有深度协议对齐而 Robot Framework 的 Allure 适配器robotframework-allure正是为此而生——它不是简单地把output.xml转成 HTML而是监听 Robot 的start_keyword/end_keyword事件在RequestsLibrary发送请求的瞬间就把request对象序列化为附件在Run Keyword And Expect Error执行后把错误详情结构化为failure元数据。这是一种侵入式但高价值的集成其回报是测试资产从“可运行”升级为“可理解”、“可分析”、“可归因”。2.3 方案选型的三个硬约束与取舍逻辑在落地前我们团队明确划了三条红线所有技术选型必须满足零修改现有测试用例不能为了加报告就要求所有人把Should Be Equal改成Allure Should Be Equal。这意味着适配器必须是无感的——它应该像一个后台监听器挂载在 Robot 的执行生命周期上自动捕获标准库行为。环境隔离不污染 CI 流水线Allure 服务端Allure Commandline和生成器Allure Generator必须能独立于 Jenkins 主节点运行。我们拒绝在 Jenkins agent 上直接npm install -g allure-commandline因为版本冲突会毁掉整个构建环境。最终方案是Jenkins 只负责生成allure-results/目录一组 JSON 文件然后通过scp或共享存储将该目录同步到一台专用的 Allure Server由它定时拉取、生成、发布报告。这样测试执行环境和报告展示环境彻底解耦。支持离线归档与长期追溯客户合同要求所有交付物含测试报告需保存 5 年。Allure 生成的index.html是纯静态文件但它的交互能力依赖allure-report/目录下的 JS/CSS/JSON 资源。如果只存index.html打开就是白屏。因此我们的归档策略是每次构建后将完整的allure-report/目录约 15MB打包为allure-report-${BUILD_ID}.tar.gz上传至对象存储并在 Jenkins 构建页面添加永久链接。这样三年后审计时下载解压就能看到和当时一模一样的交互式报告无需重跑测试。这三个约束直接排除了所有需要修改测试代码、强依赖 Jenkins 插件、或只生成单 HTML 文件的方案。Allure 的设计恰好完美契合它的适配器robotframework-allure安装即用不侵入测试逻辑它的命令行工具allure generate是标准 CLI可任意部署它的输出是完整静态站点天然支持离线归档。这不是巧合而是因为它从诞生起就瞄准了“企业级质量可视化”这个刚需场景。3. 核心细节解析与实操要点从安装到定制化的全链路说明3.1 环境准备与依赖安装避开 Python 版本与包冲突的深坑Allure 与 Robot Framework 的集成表面看只是pip install robotframework-allure一行命令但实际落地时90% 的失败都卡在环境层面。我见过最典型的案例某团队在 Python 3.8 虚拟环境中pip install robotframework-allure成功但执行robot --listener robotframework_allure.listener:AllureListener tests/时报错ModuleNotFoundError: No module named allure_commons。排查发现robotframework-allure依赖allure-python-commons而后者又依赖特定版本的pydantic。当团队同时安装了fastapi它要求pydantic2.0时pip会降级pydantic到 1.x导致allure-commons启动失败。解决方案不是“升级所有包”而是精确锁定版本。我们在requirements.txt中明确声明robotframework6.1.1 robotframework-requests0.9.5 robotframework-allure1.1.0 allure-python-commons2.10.1 pydantic1.10.15注意三点robotframework-allure1.1.0是目前截至 2024 年中与 Robot Framework 6.x 兼容最稳定的版本。更高版本如 2.x已转向allure-pytest生态对 Robot 的支持反而变弱。allure-python-commons2.10.1必须与robotframework-allure版本严格匹配。官方文档不显式标注此对应关系但 GitHub Issues 里大量用户验证过此组合。pydantic1.10.15是allure-python-commons 2.10.1的硬性依赖不能随意升级。如果你的项目必须用 Pydantic 2.x请考虑将 API 测试迁移到pytestallure-pytest而非强行在 Robot 上硬扛。提示在 CI 环境中永远使用pip install -r requirements.txt --force-reinstall而不是pip install -r requirements.txt。后者会跳过已安装的包可能导致版本漂移。--force-reinstall确保每次构建都是干净的、可重现的环境。3.2 Robot Framework 测试用例编写规范让 Allure “看懂”你的意图Allure 的强大一半来自工具一半来自你写的测试用例是否“有结构”。一个反面例子*** Test Cases *** Test Login With Wrong Password Create Session api https://api.example.com ${resp} POST On Session api /auth/login data${{json.dumps({username:test,password:wrong})}} Log ${resp.status_code} Should Be Equal As Strings ${resp.status_code} 401这段代码能跑通但在 Allure 报告里它只会显示一个叫Test Login With Wrong Password的用例失败时只有一行Keyword Should Be Equal As Strings failed。Allure 完全不知道这是在测试“登录”功能这个请求是POST /auth/login预期状态码是401实际返回了什么。要让 Allure “看懂”必须用 Allure 的语义化标签和步骤封装。正确写法*** Settings *** Documentation 用户认证模块 API 测试 Library RequestsLibrary Library robotframework_allure.library.AllureLibrary Suite Setup Set Suite Metadata env staging Suite Teardown Close All Sessions *** Test Cases *** Test Login With Wrong Password [Tags] login auth critical [Documentation] 验证错误密码登录返回 401 Unauthorized Given I have a session for api with base url https://api.example.com When I send POST request to /auth/login with body ... {username:test,password:wrong} Then the response status code should be 401 And the response body should contain invalid_credentials *** Keywords *** Given I have a session for ${name} with base url ${base_url} Create Session ${name} ${base_url} Allure Add Description 创建 ${name} 会话基础 URL: ${base_url} When I send POST request to ${endpoint} with body ... ${body} ${resp} POST On Session api ${endpoint} data${body} Allure Attach Json Request Body ${body} Allure Attach Json Response Body ${resp.json()} Allure Add Step 发送 POST 请求到 ${endpoint} [Return] ${resp} Then the response status code should be ${expected_code} Should Be Equal As Strings ${resp.status_code} ${expected_code} Allure Add Step 验证响应状态码为 ${expected_code}关键点解析[Tags]login、auth、critical会被 Allure 自动识别为过滤标签报告首页可一键筛选。Allure Add Description为整个用例添加富文本描述支持 Markdown会显示在用例详情页顶部。Allure Attach Json将body和response.json()作为附件嵌入点击即可展开查看避免日志里堆砌大段 JSON。Allure Add Step将原子操作封装为可读步骤Allure 会按执行顺序渲染为流程图失败时高亮具体哪一步出错。Suite Setup/TeardownSet Suite Metadata设置envstagingAllure 会在报告右上角显示环境信息方便对比不同环境的测试结果。注意AllureLibrary是robotframework-allure提供的辅助库它封装了所有Allure*关键字。必须在*** Settings ***中Library导入否则Allure Add Step等关键字无法识别。很多新手漏掉这一步导致关键字报错却以为是 Allure 本身有问题。3.3 Allure 报告生成与服务部署从本地预览到生产级发布生成报告分两步收集结果allure-results/和生成静态站allure-report/。很多人混淆这两者导致报告空白。第一步执行测试并生成结果# 清理旧结果 rm -rf allure-results/ # 执行测试指定 Allure 监听器 robot \ --listener robotframework_allure.listener:AllureListener \ --outputdir results \ --loglevel DEBUG \ tests/ # 执行后当前目录下会生成 allure-results/ 目录里面是 *.json 文件 ls allure-results/ # 00001-result.json 00002-result.json ...关键参数--listener robotframework_allure.listener:AllureListener是核心。它告诉 Robot在每个关键字执行前后调用AllureListener类的start_keyword/end_keyword方法将元数据写入allure-results/。--outputdir results是 Robot 的标准输出目录与 Allure 无关可保留用于调试。第二步生成可浏览的报告# 安装 Allure Commandline推荐用 SDKMAN! 或直接下载二进制 sdk install allure 2.24.0 # 生成报告输入 allure-results/输出 allure-report/ allure generate allure-results/ -o allure-report/ --clean # 启动本地服务预览默认 http://localhost:5000 allure open allure-report/--clean参数至关重要。它确保每次生成都是全新构建不会残留上一次的旧数据。在 CI 中必须加上否则报告会越积越多体积膨胀加载缓慢。生产部署Nginx 反向代理 自动化同步本地allure open只是预览。生产环境需对外提供稳定 URL。我们采用 Nginx 静态托管# /etc/nginx/conf.d/allure.conf server { listen 80; server_name allure.example.com; root /var/www/allure-report; index index.html; location / { try_files $uri $uri/ /index.html; } # 防止 JSON 文件被缓存确保每次看到最新报告 location ~ \.json$ { add_header Cache-Control no-store, no-cache, must-revalidate, proxy-revalidate, max-age0; } }CI 流水线中Jenkins 构建完成后执行 Shell 脚本# 将本次构建的 allure-report/ 同步到 Nginx 根目录 rsync -avz --delete allure-report/ userallure-server:/var/www/allure-report/ # 通知 Allure Server 重载可选Nginx reload 或发送信号 ssh userallure-server sudo nginx -s reload这样每次构建完成https://allure.example.com就自动更新为最新报告且 URL 永久有效因为allure-report/目录被完全覆盖。4. 实操过程与核心环节实现一个真实电商订单 API 测试的完整复现4.1 场景设定与测试目标不只是“能跑”更要“看得清”我们以某电商系统的“创建订单”API 为例。接口定义如下Endpoint:POST https://api.ecommerce.com/v1/ordersRequest Body:{ user_id: usr_123, items: [ {sku: SKU-A, quantity: 2}, {sku: SKU-B, quantity: 1} ], shipping_address: { street: 123 Main St, city: Shanghai } }Success Response (201):{ order_id: ord_456, status: created, total_amount: 199.99, items: [ {sku: SKU-A, quantity: 2, price: 89.99}, {sku: SKU-B, quantity: 1, price: 20.00} ] }Failure Cases:400 Bad Request:user_id为空或items为空数组404 Not Found:user_id不存在422 Unprocessable Entity:shipping_address.city不在白名单测试目标不仅是验证201成功更要记录每次请求的完整curl命令便于开发复现对比total_amount是否等于items子项price * quantity的和当400失败时捕获response.body.detail字段明确提示哪一栏校验失败将所有订单相关用例打上featureorders标签便于在 Allure 中聚合分析。4.2 完整测试用例实现从 setup 到 teardown 的全链路*** Settings *** Documentation 订单创建 API 测试套件 Library RequestsLibrary Library Collections Library robotframework_allure.library.AllureLibrary Suite Setup Setup Order Test Environment Suite Teardown Teardown Order Test Environment Test Setup Set Test Metadata version v1.2.0 Test Teardown Capture Final Response If Failed *** Variables *** ${BASE_URL} https://api.ecommerce.com/v1 ${VALID_USER_ID} usr_123 ${INVALID_USER_ID} usr_999 *** Test Cases *** Create Order With Valid Items [Tags] orders happy-path critical [Documentation] 验证正常下单流程检查金额计算准确性 Given I have an authenticated session When I create an order with items Then the response status should be 201 And the order total amount should match item sum And the response should contain order_id and status Create Order With Empty Items [Tags] orders negative high [Documentation] 验证空商品列表返回 400 Given I have an authenticated session When I create an order with empty items Then the response status should be 400 And the response detail should indicate items cannot be empty Create Order With Invalid User ID [Tags] orders negative high [Documentation] 验证无效用户 ID 返回 404 Given I have an authenticated session When I create an order with invalid user id Then the response status should be 404 And the response detail should indicate user not found *** Keywords *** Setup Order Test Environment Create Session ecommerce ${BASE_URL} Allure Add Description 初始化电商 API 会话Base URL: ${BASE_URL} Teardown Order Test Environment Close All Sessions Given I have an authenticated session # 模拟登录获取 token此处简化为设置 header Set Headers {Authorization: Bearer fake-token-123} When I create an order with items ${body} Evaluate json.dumps({ user_id: ${VALID_USER_ID}, items: [{sku: SKU-A, quantity: 2}, {sku: SKU-B, quantity: 1}], shipping_address: {street: 123 Main St, city: Shanghai} }) json ${resp} POST On Session ecommerce /orders data${body} Allure Attach Json Request Body ${body} Allure Attach Json Response Body ${resp.json()} Allure Add Step 发送订单创建请求Body: ${body} [Return] ${resp} When I create an order with empty items ${body} Evaluate json.dumps({user_id: ${VALID_USER_ID}, items: []}) ${resp} POST On Session ecommerce /orders data${body} Allure Attach Json Request Body ${body} Allure Attach Json Response Body ${resp.json()} Allure Add Step 发送空商品订单请求 [Return] ${resp} When I create an order with invalid user id ${body} Evaluate json.dumps({user_id: ${INVALID_USER_ID}, items: [{sku: SKU-A, quantity: 1}]}) ${resp} POST On Session ecommerce /orders data${body} Allure Attach Json Request Body ${body} Allure Attach Json Response Body ${resp.json()} Allure Add Step 发送无效用户 ID 订单请求 [Return] ${resp} Then the response status should be ${expected_code} Should Be Equal As Strings ${resp.status_code} ${expected_code} Allure Add Step 验证响应状态码为 ${expected_code} Then the order total amount should match item sum ${total} Get From Dictionary ${resp.json()} total_amount ${items} Get From Dictionary ${resp.json()} items ${sum} Evaluate sum([item[price] * item[quantity] for item in ${items}]) Should Be Equal As Numbers ${total} ${sum} tolerance0.01 Allure Add Step 验证总金额 ${total} 等于明细和 ${sum} Then the response detail should indicate ${expected_text} ${detail} Get From Dictionary ${resp.json()} detail Should Contain ${detail} ${expected_text} Allure Add Step 验证错误详情包含 ${expected_text} Capture Final Response If Failed Run Keyword If ${TEST_STATUS} FAIL ... Allure Attach Json Final Response Body ${resp.json()}4.3 执行与报告解读如何从 Allure 界面快速定位根因执行命令robot --listener robotframework_allure.listener:AllureListener --outputdir results tests/orders.robot allure generate allure-results/ -o allure-report/ --clean生成报告后访问http://localhost:5000你会看到首页概览顶部显示Total: 3,Passed: 1,Failed: 2,Broken: 0。右侧Environment显示versionv1.2.0,envstaging。左侧Categories区域Product Defects下自动归类了两个失败用例因为它们有status_code ! 200。按 Feature 筛选点击左上角Features选择orders页面只显示这 3 个用例。再点击Tags勾选negative则只显示后两个失败用例。深入失败用例点击Create Order With Empty Items进入详情页Description显示[Documentation]内容“验证空商品列表返回 400”。Steps按顺序列出发送空商品订单请求绿色表示执行成功验证响应状态码为 400红色失败Attachments下方有两个 JSON 附件Request Body:{user_id: usr_123, items: []}Response Body:{detail: items cannot be empty, status_code: 400}Failure Message清晰显示Expected 400, got 400等等这不对原来Should Be Equal As Strings比较的是字符串而resp.status_code是整数所以实际比较的是400 400应该通过。但报告里显示失败说明resp.status_code可能是字符串点开Response Body附件发现status_code字段确实是字符串400。这暴露了 API 的一个设计缺陷状态码不应该作为响应体字段返回而应由 HTTP 协议头承载。这个发现是 Allure 的附件功能带来的意外收获——它把原本藏在日志里的原始数据变成了可审查的证据。趋势分析点击顶部Graphs-Trends可以看到过去 7 天该用例的执行历史。如果Create Order With Invalid User ID这个用例从上周开始突然从PASS变成FAIL结合Build列可以立刻定位到是哪个 Jenkins 构建引入了问题再关联该构建的 Git 提交就能快速找到是哪行代码改动导致了用户查询逻辑变更。5. 常见问题与排查技巧实录那些文档里不会写的实战经验5.1 典型问题速查表问题现象可能原因排查与解决Allure 报告空白只有标题栏allure-results/目录为空或 JSON 文件损坏1. 检查robot命令是否加了--listener参数2. 查看results/output.xml中是否有robotframework_allure相关的 listener 日志3. 手动cat allure-results/*.json | head -20确认 JSON 格式是否合法常见错误中文注释未转义报告中看不到 Request/Response 附件Allure Attach Json关键字未被调用或resp.json()抛异常1. 确认AllureLibrary已在*** Settings ***中Library2. 在When关键字中POST On Session后加Log ${resp.content}确认响应体非空3. 用Run Keyword And Ignore Error包裹Allure Attach Json避免因resp.json()解析失败导致整个用例崩溃Allure 步骤显示乱码中文变 ????Python 默认编码非 UTF-8或终端 locale 设置错误1. 在 Jenkins agent 的启动脚本中添加export LANGen_US.UTF-82. 在 Robot 测试用例中Allure Add Description前加Set Global Variable ${UTF8_DESC} ${CURDIR}/description.md将中文描述写入 UTF-8 编码的文件再用Allure Attach File附加报告加载极慢Chrome 控制台报Failed to load resource: net::ERR_CONNECTION_REFUSEDAllure 服务端未启动或 Nginx 配置未生效1.ps aux | grep allure确认 Allure 进程2.curl -I http://localhost:5000/index.html检查服务是否响应3.nginx -t验证配置语法systemctl restart nginx重载同一用例在 Jenkins 上失败在本地成功Jenkins agent 的 Python 环境缺少robotframework-allure或版本不一致1. 在 Jenkins 构建步骤开头添加pip list | grep allure2. 强制pip install -r requirements.txt --force-reinstall3. 使用virtualenv隔离环境避免全局包污染5.2 我踩过的三个深坑与独家避坑技巧坑一Allure 的story标签与 Robot 的[Tags]冲突Allure 原生支持story、epic等标签但robotframework-allure并不直接映射。如果你在 Robot 用例中写[Tags] storyOrderCreationAllure 会把它当作普通字符串标签不会显示在Stories视图里。正确做法是放弃前缀用纯英文下划线。例如[Tags] story_order_creation然后在 Allure 报告的Suites视图中它会自动归类到story_order_creation分组。更优雅的方式是使用Allure Add Label关键字*** Test Cases *** Test Create Order [Tags] orders Allure Add Label epic E-Commerce Allure Add Label story Order Creation Allure Add Label severity critical这样Epic、Story、Severity会显示在用例详情页的Labels区域并支持在报告首页按这些维度筛选。这是robotframework-allure1.1.0 版本才加入的功能老版本文档里找不到。坑二allure-results/目录权限导致 Jenkins 构建失败在 Linux Jenkins agent 上allure-results/目录由jenkins用户创建但allure generate命令可能由root或其他用户执行导致Permission denied。解决方案不是chmod 777安全风险而是统一用户在 Jenkins 的Configure System-Global properties中添加Environment variables设置ALLURE_RESULTS_DIR/tmp/allure-results-${BUILD_NUMBER}并在构建脚本中mkdir -p ${ALLURE_RESULTS_DIR} robot --listener robotframework_allure.listener:AllureListener --variable ALLURE_RESULTS_DIR:${ALLURE_RESULTS_DIR} tests/ allure generate ${ALLURE_RESULTS_DIR} -o allure-report/ --clean这样所有文件都在/tmp/下且由 Jenkins 用户拥有规避了权限问题。坑三Allure 报告中的History功能失效Allure 的History视图能显示用例的历史执行趋势但默认只保留最近 5 次。如果你希望保留 30 次需要修改allure-commandline的配置。但allure generate命令不支持--history-depth参数。正确方法是在生成报告前先合并历史结果。假设你有过去 5 次的allure-results/目录命名为allure-results-20240101/,allure-results-20240102/...则# 创建历史目录 mkdir -p allure-history/ # 将每次的结果复制进去用时间戳命名 cp -r allure-results-20240101/* allure-history/20240101/ cp -r allure-results-20240102/* allure-history/20240102/ # ...