har-replay

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HAR Replay

HAR 重放

Replay a HAR file as a mock Lightdash backend so the frontend renders with exact production data. No database, warehouse, or authentication needed.
The user must provide a path to a
.har
file. If not provided as
$ARGUMENTS
, ask for it.
将HAR文件作为模拟Lightdash后端重放,让前端使用精准的生产数据进行渲染。无需数据库、数据仓库或身份验证。
用户必须提供
.har
文件的路径。如果未通过
$ARGUMENTS
提供,请向用户索要。

Step 1: Analyze the HAR file

步骤1:分析HAR文件

Run a Python script to extract key information from the HAR:
python
import json, sys, re
from collections import Counter
from urllib.parse import urlparse

har_path = "$HAR_FILE_PATH"
with open(har_path) as f:
    har = json.load(f)

entries = har['log']['entries']
运行Python脚本从HAR中提取关键信息:
python
import json, sys, re
from collections import Counter
from urllib.parse import urlparse

har_path = "$HAR_FILE_PATH"
with open(har_path) as f:
    har = json.load(f)

entries = har['log']['entries']

Extract the page URL from the HAR pages section

Extract the page URL from the HAR pages section

page_path = None pages = har['log'].get('pages', []) for p in pages: title = p.get('title', '') parsed = urlparse(title) if parsed.path and parsed.path != '/': page_path = parsed.path break
page_path = None pages = har['log'].get('pages', []) for p in pages: title = p.get('title', '') parsed = urlparse(title) if parsed.path and parsed.path != '/': page_path = parsed.path break

Fallback: extract from referer headers

Fallback: extract from referer headers

if not page_path: for e in entries: for h in e['request']['headers']: if h['name'].lower() == 'referer': parsed = urlparse(h['value']) if parsed.path and parsed.path != '/': page_path = parsed.path break if page_path: break
if not page_path: for e in entries: for h in e['request']['headers']: if h['name'].lower() == 'referer': parsed = urlparse(h['value']) if parsed.path and parsed.path != '/': page_path = parsed.path break if page_path: break

Find the origin (first API call)

Find the origin (first API call)

origin = None for e in entries: url = urlparse(e['request']['url']) if url.path.startswith('/api/'): origin = f"{url.scheme}://{url.netloc}" break
origin = None for e in entries: url = urlparse(e['request']['url']) if url.path.startswith('/api/'): origin = f"{url.scheme}://{url.netloc}" break

Filter to origin entries only

Filter to origin entries only

api_entries = [e for e in entries if origin and origin in e['request']['url']]
api_entries = [e for e in entries if origin and origin in e['request']['url']]

Find dashboard URL if present

Find dashboard URL if present

dashboard_uuid = None for e in api_entries: path = urlparse(e['request']['url']).path m = re.search(r'/dashboards/([0-9a-f-]{36})', path) if m: dashboard_uuid = m.group(1) break
dashboard_uuid = None for e in api_entries: path = urlparse(e['request']['url']).path m = re.search(r'/dashboards/([0-9a-f-]{36})', path) if m: dashboard_uuid = m.group(1) break

Find project UUID

Find project UUID

project_uuid = None for e in api_entries: path = urlparse(e['request']['url']).path m = re.search(r'/projects/([0-9a-f-]{36})', path) if m: project_uuid = m.group(1) break
project_uuid = None for e in api_entries: path = urlparse(e['request']['url']).path m = re.search(r'/projects/([0-9a-f-]{36})', path) if m: project_uuid = m.group(1) break

Count request types

Count request types

methods = Counter() api_paths = Counter() has_base64 = False post_endpoints_needing_body_match = []
for e in api_entries: method = e['request']['method'] path = urlparse(e['request']['url']).path methods[method] += 1 normalized = re.sub(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', '{uuid}', path) api_paths[f'{method} {normalized}'] += 1 if e['response']['content'].get('encoding') == 'base64': has_base64 = True
methods = Counter() api_paths = Counter() has_base64 = False post_endpoints_needing_body_match = []
for e in api_entries: method = e['request']['method'] path = urlparse(e['request']['url']).path methods[method] += 1 normalized = re.sub(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', '{uuid}', path) api_paths[f'{method} {normalized}'] += 1 if e['response']['content'].get('encoding') == 'base64': has_base64 = True

Find POST endpoints with multiple entries (need body-based matching)

Find POST endpoints with multiple entries (need body-based matching)

for key, count in api_paths.items(): if key.startswith('POST') and count > 1: post_endpoints_needing_body_match.append((key, count))
print(f"Page path: {page_path}") print(f"Origin: {origin}") print(f"Total API entries: {len(api_entries)}") print(f"Project UUID: {project_uuid}") print(f"Dashboard UUID: {dashboard_uuid}") print(f"Has base64 content: {has_base64}") print(f"Methods: {dict(methods)}") print(f"POST endpoints needing body match: {post_endpoints_needing_body_match}") print() print("API paths:") for p, c in sorted(api_paths.items()): print(f" {p}: {c}")

Report the findings to the user:
- Page path extracted from the HAR (this is the URL the user was viewing when the HAR was captured)
- Origin domain
- Number of API entries
- Notable POST endpoints that need body-based matching (e.g., `dashboard-chart` with multiple chart tiles)
for key, count in api_paths.items(): if key.startswith('POST') and count > 1: post_endpoints_needing_body_match.append((key, count))
print(f"Page path: {page_path}") print(f"Origin: {origin}") print(f"Total API entries: {len(api_entries)}") print(f"Project UUID: {project_uuid}") print(f"Dashboard UUID: {dashboard_uuid}") print(f"Has base64 content: {has_base64}") print(f"Methods: {dict(methods)}") print(f"POST endpoints needing body match: {post_endpoints_needing_body_match}") print() print("API paths:") for p, c in sorted(api_paths.items()): print(f" {p}: {c}")

向用户报告分析结果:
- 从HAR中提取的页面路径(即用户捕获HAR时正在查看的URL)
- 源域名
- API条目数量
- 需要基于请求体匹配的重点POST端点(例如包含多个图表组件的`dashboard-chart`)

Step 2: Write a tailored replay server

步骤2:编写定制化重放服务器

Based on the analysis, write a replay server to
scripts/har-replay-server.ts
. The server must handle these concerns:
基于分析结果,编写重放服务器代码至
scripts/har-replay-server.ts
。服务器必须处理以下事项:

Core structure

核心结构

typescript
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
Use only Node.js built-in modules. Run with
npx tsx scripts/har-replay-server.ts <har-path>
.
typescript
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
仅使用Node.js内置模块。通过
npx tsx scripts/har-replay-server.ts <har-path>
运行。

HAR parsing rules

HAR解析规则

  1. Filter by origin: Only index entries matching the detected origin domain
  2. Base64 decoding: Check
    entry.response.content.encoding === 'base64'
    and decode with
    Buffer.from(text, 'base64').toString('utf-8')
  3. Skip pending poll responses: For
    GET /api/v2/projects/{uuid}/query/{uuid}
    endpoints, parse the response body and skip entries where
    results.status === 'pending'
    — only keep the final
    ready
    response
  4. Normalize 304s to 200: Serve
    304
    responses as
    200
    (they have full body in HAR)
  5. Strip transport headers: Remove
    :pseudo-headers
    ,
    transfer-encoding
    ,
    content-encoding
    ,
    content-length
    from response headers
  1. 按源域名过滤:仅索引与检测到的源域名匹配的条目
  2. Base64解码:检查
    entry.response.content.encoding === 'base64'
    ,并使用
    Buffer.from(text, 'base64').toString('utf-8')
    进行解码
  3. 跳过待处理的轮询响应:对于
    GET /api/v2/projects/{uuid}/query/{uuid}
    端点,解析响应体并跳过
    results.status === 'pending'
    的条目 —— 仅保留最终的
    ready
    响应
  4. 将304响应标准化为200:将
    304
    响应转换为
    200
    返回(HAR中包含完整响应体)
  5. 移除传输相关头信息:从响应头中移除
    :pseudo-headers
    transfer-encoding
    content-encoding
    content-length

Request matching strategy

请求匹配策略

  • GET requests: Match by exact
    "METHOD /path?query"
    . Fall back to path without query string.
  • POST
    dashboard-chart
    : These all hit the same URL but carry different
    chartUuid
    in the request body. Index by
    chartUuid
    from the HAR request body, and match incoming requests by parsing their body.
  • POST
    dashboard-sql-chart
    : Same pattern but with
    savedSqlUuid
    . If there's only one, exact path match works.
  • Other POST requests (e.g.,
    availableFilters
    ): Usually unique paths, exact match works.
  • Any POST endpoint the analysis identified as having multiple entries to the same path: Must use body-based matching on a distinguishing field.
  • GET请求:按精确的
    "METHOD /path?query"
    匹配。如果匹配失败,回退到不带查询字符串的路径匹配。
  • POST
    dashboard-chart
    :这些请求都指向同一URL,但请求体中携带不同的
    chartUuid
    。根据HAR请求体中的
    chartUuid
    建立索引,并通过解析传入请求的体内容进行匹配。
  • POST
    dashboard-sql-chart
    :模式相同,但使用
    savedSqlUuid
    。如果只有一个条目,精确路径匹配即可生效。
  • 其他POST请求(例如
    availableFilters
    ):通常是唯一路径,精确匹配即可生效。
  • 分析中识别出的同一路径存在多个条目的POST端点:必须基于请求体中的区分字段进行匹配。

Server behavior

服务器行为

  • Listen on port 3001
  • Log every request with method, path, and whether it matched
  • Return
    {"status":"error","error":{"message":"HAR replay: no matching entry"}}
    for 404s
  • On startup, print the number of indexed responses and the dashboard URL to navigate to
  • 监听端口3001
  • 记录每个请求的方法、路径以及是否匹配成功
  • 对于404请求,返回
    {"status":"error","error":{"message":"HAR replay: no matching entry"}}
  • 启动时,打印已索引的响应数量以及可访问的仪表盘URL

Step 3: Start the replay server and frontend

步骤3:启动重放服务器与前端

  1. Kill any existing processes on port 3001
  2. Start the replay server in the background:
    bash
    npx tsx scripts/har-replay-server.ts <har-path>
  3. Start the Vite frontend dev server pointing at the replay server:
    bash
    PORT=3001 pnpm -F frontend dev
  4. Wait for both to be ready, then verify with:
    bash
    curl -s http://localhost:3001/api/v1/health
  1. 终止端口3001上的所有现有进程
  2. 在后台启动重放服务器:
    bash
    npx tsx scripts/har-replay-server.ts <har-path>
  3. 启动指向重放服务器的Vite前端开发服务器:
    bash
    PORT=3001 pnpm -F frontend dev
  4. 等待两者启动完成,然后通过以下命令验证:
    bash
    curl -s http://localhost:3001/api/v1/health

Step 4: Provide the URL

步骤4:提供访问URL

Once both servers are confirmed running, tell the user the URL to open in their browser. Construct it from the page path extracted in Step 1:
http://localhost:<vite-port><page-path>
For example, if the HAR was captured on
/projects/abc-123/dashboards/def-456/view
, the URL would be
http://localhost:3002/projects/abc-123/dashboards/def-456/view
.
Note: Vite may pick a port other than 3000 since 3001 is in use. Check the Vite startup output for the actual port.
一旦确认两个服务器都已运行,告知用户在浏览器中打开的URL。使用步骤1中提取的页面路径构建URL:
http://localhost:<vite-port><page-path>
例如,如果HAR是在
/projects/abc-123/dashboards/def-456/view
路径下捕获的,那么URL应为
http://localhost:3002/projects/abc-123/dashboards/def-456/view
注意:由于端口3001已被占用,Vite可能会选择3000以外的端口。请查看Vite启动输出以获取实际端口。

Step 5: Debug any rendering errors

步骤5:调试渲染错误

If the user reports errors:
  1. Check the replay server logs for 404s (missing HAR entries)
  2. Check for response format issues (base64 encoding, unexpected MIME types)
  3. Fix the replay server script and restart
如果用户报告错误:
  1. 检查重放服务器日志中的404记录(缺失的HAR条目)
  2. 检查响应格式问题(Base64编码、意外的MIME类型)
  3. 修改重放服务器脚本并重启

Notes

注意事项

  • The replay server is disposable — it's tailored to the specific HAR file and can be deleted after use
  • HAR files may contain session cookies and sensitive data — do not commit them
  • The frontend will 404 on any navigation away from the captured pages
  • To re-capture: Chrome DevTools > Network tab > right-click > "Save all as HAR with content"
  • 重放服务器是一次性的 —— 它是为特定HAR文件定制的,使用后可删除
  • HAR文件可能包含会话Cookie和敏感数据 —— 请勿提交到代码仓库
  • 前端在导航到捕获页面以外的路径时会出现404
  • 重新捕获HAR的方法:Chrome DevTools > 网络标签页 > 右键 > “另存为HAR并包含内容”