har-replay
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHAR 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 file. If not provided as , ask for it.
.har$ARGUMENTS将HAR文件作为模拟Lightdash后端重放,让前端使用精准的生产数据进行渲染。无需数据库、数据仓库或身份验证。
用户必须提供文件的路径。如果未通过提供,请向用户索要。
.har$ARGUMENTSStep 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 . The server must handle these concerns:
scripts/har-replay-server.ts基于分析结果,编写重放服务器代码至。服务器必须处理以下事项:
scripts/har-replay-server.tsCore 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解析规则
- Filter by origin: Only index entries matching the detected origin domain
- Base64 decoding: Check and decode with
entry.response.content.encoding === 'base64'Buffer.from(text, 'base64').toString('utf-8') - Skip pending poll responses: For endpoints, parse the response body and skip entries where
GET /api/v2/projects/{uuid}/query/{uuid}— only keep the finalresults.status === 'pending'responseready - Normalize 304s to 200: Serve responses as
304(they have full body in HAR)200 - Strip transport headers: Remove ,
:pseudo-headers,transfer-encoding,content-encodingfrom response headerscontent-length
- 按源域名过滤:仅索引与检测到的源域名匹配的条目
- Base64解码:检查,并使用
entry.response.content.encoding === 'base64'进行解码Buffer.from(text, 'base64').toString('utf-8') - 跳过待处理的轮询响应:对于端点,解析响应体并跳过
GET /api/v2/projects/{uuid}/query/{uuid}的条目 —— 仅保留最终的results.status === 'pending'响应ready - 将304响应标准化为200:将响应转换为
304返回(HAR中包含完整响应体)200 - 移除传输相关头信息:从响应头中移除、
:pseudo-headers、transfer-encoding、content-encodingcontent-length
Request matching strategy
请求匹配策略
- GET requests: Match by exact . Fall back to path without query string.
"METHOD /path?query" - POST : These all hit the same URL but carry different
dashboard-chartin the request body. Index bychartUuidfrom the HAR request body, and match incoming requests by parsing their body.chartUuid - POST : Same pattern but with
dashboard-sql-chart. If there's only one, exact path match works.savedSqlUuid - Other POST requests (e.g., ): Usually unique paths, exact match works.
availableFilters - 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 :这些请求都指向同一URL,但请求体中携带不同的
dashboard-chart。根据HAR请求体中的chartUuid建立索引,并通过解析传入请求的体内容进行匹配。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 for 404s
{"status":"error","error":{"message":"HAR replay: no matching entry"}} - 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:启动重放服务器与前端
- Kill any existing processes on port 3001
- Start the replay server in the background:
bash
npx tsx scripts/har-replay-server.ts <har-path> - Start the Vite frontend dev server pointing at the replay server:
bash
PORT=3001 pnpm -F frontend dev - Wait for both to be ready, then verify with:
bash
curl -s http://localhost:3001/api/v1/health
- 终止端口3001上的所有现有进程
- 在后台启动重放服务器:
bash
npx tsx scripts/har-replay-server.ts <har-path> - 启动指向重放服务器的Vite前端开发服务器:
bash
PORT=3001 pnpm -F frontend dev - 等待两者启动完成,然后通过以下命令验证:
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 , the URL would be .
/projects/abc-123/dashboards/def-456/viewhttp://localhost:3002/projects/abc-123/dashboards/def-456/viewNote: 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是在路径下捕获的,那么URL应为。
/projects/abc-123/dashboards/def-456/viewhttp://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:
- Check the replay server logs for 404s (missing HAR entries)
- Check for response format issues (base64 encoding, unexpected MIME types)
- Fix the replay server script and restart
如果用户报告错误:
- 检查重放服务器日志中的404记录(缺失的HAR条目)
- 检查响应格式问题(Base64编码、意外的MIME类型)
- 修改重放服务器脚本并重启
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并包含内容”