frappe-code-review
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseQuality Code Review
高质量代码审查
A reviewer's checklist for Frappe applications. Protect correctness, security,
and the future maintainer, in that order of consequence. The sections below run
in that order — spend most attention on §1 and §2. Prefer a root-cause fix over a
workaround, and say why a finding matters (what breaks, for whom).
审查 Frappe 应用的检查清单。按后果严重程度,依次保障正确性、安全性以及未来的维护者。以下各节也按此顺序排列——应将大部分注意力放在第 1 节和第 2 节。优先采用根因修复,而不是变通方案,并说明某项发现为何重要(会造成什么后果、影响哪些人)。
1. Correctness & stability (highest consequence)
1. 正确性与稳定性(后果最严重)
The worst bug is silent stateful corruption — wrong ledgers/stock posted
with no error. Treat stateful and legal/accounting/compliance code as "failure
is not an option" code.
- Fail early and loudly. Use assertions for internal invariants:
. (Assertions are for invariants the code guarantees — not user-facing validation.)
assert total_credit == total_debit - Picture how it breaks. For every change ask: Where can this break? How will someone misuse this? Write fool-proof code. Extensions and overrides should especially consider handling all sorts of failure modes.
- No partial commits. A stray /
frappe.db.commit()mid-transaction ends the transaction and exposes partial state — flag every one. Submitting/saving with validation bypassed (docs posted with no SL/GL entry when validation fails) is a critical bug.db.rollback() - Don't sacrifice atomicity for convenience (e.g. adding to fix bootstrapping makes transactions non-atomic). Autocommit belongs only on schema creation.
autocommit - Preserve invariants over UX. "Compromise UX, but guarantee correctness." Don't degrade working code to accommodate broken code.
- Validate the issue before fixing it. Sometimes the correct fix is "don't fix this". Identify the root causes first.
- Watch for destructive DB APIs with empty/filters.
None/set_value("Site", None, ...)with no filter updates/deletes every row. These must error, not silently operate on the whole table. Flag anydb.delete/set_value/deletewhere the name/filter could beget_valueor empty- or attacker-controlled.None - Check the types in a condition actually match. A comparison between
mismatched types (string vs , string vs int) silently never matches or is always true — cast explicitly (
datetime/cint) at the boundary.flt - Question the return shape. Before indexing a result, ask whether it can be
/
None; watch[None, None](list of dicts) vs scalar confusion.as_dict=1 - Don't silently change long-standing semantics. Behavior callers have relied on for a long time is a contract — altering it is a breaking change in disguise, even when no signature changed.
最糟糕的 bug 是静默的有状态数据损坏——错误的账本/库存被过账,却没有报错。将有状态代码以及法律/会计/合规代码视为**“绝不能失败”**代码。
- 尽早且显式地失败。 对内部不变量使用断言:。(断言用于代码自身保证的不变量,而不是面向用户的验证。)
assert total_credit == total_debit - 设想过会如何出错。 对每项改动都要问:它会在哪里出错?他人会如何误用? 编写难以误用的代码。扩展和覆盖尤其应考虑处理各种失败模式。
- 不允许部分提交。 事务中途意外调用 /
frappe.db.commit()会结束事务并暴露部分状态——每一处都应标记。在绕过验证的情况下提交/保存(验证失败时单据仍被过账且没有 SL/GL 分录)是严重 bug。db.rollback() - 不要为了便利牺牲原子性(例如,为解决引导启动问题而添加 会使事务失去原子性)。
autocommit只应用于创建 schema。autocommit - 优先保障不变量,而非用户体验。 “可以牺牲 UX,但必须保证正确性。” 不要为了迁就有问题的代码而降低原本正常工作的代码质量。
- 修复前先验证问题。 有时正确的修复是“不要修复”。先找出根因。
- 警惕过滤条件为空或为 的破坏性数据库 API。
None/ 不带过滤条件的set_value("Site", None, ...)会更新/删除每一行。这些操作必须报错,而不是悄无声息地作用于整张表。标记所有名称/过滤条件可能为db.delete、为空或可被攻击者控制的None/set_value/delete。get_value - 检查条件中的类型是否真正匹配。 类型不匹配的比较(字符串与 、字符串与整数)会静默地永远不匹配或始终成立——应在边界处显式转换(
datetime/cint)。flt - 质疑返回结构。 在对结果进行索引前,先判断它是否可能为 /
None;注意[None, None](字典列表)与标量之间的混淆。as_dict=1 - 不要静默改变长期以来的语义。 调用方长期依赖的行为就是契约——即使签名未变,改变它也等同于隐藏了破坏性变更。
2. Security
2. 安全性
Avoiding a vulnerability is far easier than fixing one safely. Audit
security-critical code (auth, authorization, permissions, user management)
especially hard.
Injection
- NEVER build SQL by string concatenation/f-strings. Use the ORM or query
builder. If raw SQL is unavoidable, use parameter substitution
() — never interpolate yourself. Better still, avoid introducing new raw SQL at all: beyond injection risk it ties code to one database, and the framework aims to stay DB-agnostic (Postgres support). Prefer
frappe.db.sql("... where name = %s", (user,)).frappe.qb - Type confusion is an injection vector even with the ORM. Frappe accepts
complex types, so a parameter expected to be a string can arrive as a filter
list: passed to
{"key": ["!=", ""]}bypasses a secret-key check. Validate input types at trust boundaries — explicitdb.get_value. Audit everyisinstance(key, str)method for this.@frappe.whitelist - Never /
evalanything yourself.exec/safe_evalonly, in limited volume, and "safe_exec is not magic." Never accept a client-supplied method path to execute.safe_exec
Sandboxing & trust boundaries
- Sandboxed execution (RestrictedPython/) is not reliably safe — assume escapes exist. Security toggles must live at the right trust boundary: server-script enablement is a bench-level config, never site-level (a tenant could enable it and take over the whole server).
safe_exec - Prefer allowlists over blocklists — blocklists are bypassable. Don't expose everything by default.
Access control
- "Think 10 times before " — it is not a shortcut around real authn/authz. Web pages must apply permissions before reading/sharing data. Prefer
allow_guest=True/get_listover hand-rolled queries.get_all - Scope relaxations precisely. Verify a rate-limit/permission exception targets exactly the intended principal — not, say, all non-guest users.
Path traversal / filesystem
- Prefer the File doctype API. If user input enters a path, ensure it can't
traverse () outside the site folder.
/../../
Crypto / secrets
- Never roll your own crypto; reuse existing implementations. Verify authenticity of guest/webhook requests (HMAC).
- Signed/one-time URLs: use a truly secret signing value; expire by both time and first use; validate using the URL alone, not merged form data (Frappe merges URL + form data → replay attacks with one valid signature).
- Store secrets in password fields; never plain text; never leak secrets in logs or error messages.
XSS & the rest of OWASP
- Don't inject user input into the DOM. Treat XSS as critical even when it looks trivial — HTML/JS injection usually leads to account hijack.
- Don't fix XSS by sanitizing and throwing away special characters. Prefer escaping right before injecting values in DOM.
避免引入漏洞远比事后安全修复容易。应特别严格审计安全关键代码(认证、授权、权限、用户管理)。
注入
- 绝不通过字符串拼接/f-string 构造 SQL。 使用 ORM 或 query builder。若不可避免地需要原始 SQL,则使用参数替换()——绝不自行插值。更好的是,完全避免引入新的原始 SQL:除了注入风险外,它还会将代码绑定到单一数据库,而框架的目标是保持数据库无关(支持 Postgres)。优先使用
frappe.db.sql("... where name = %s", (user,))。frappe.qb - 即使使用 ORM,类型混淆也是注入向量。 Frappe 接受复杂类型,因此预期为字符串的参数可能以过滤器列表的形式到达:传给 的
db.get_value可绕过密钥检查。应在信任边界验证输入类型——显式使用{"key": ["!=", ""]}。审计每个isinstance(key, str)方法。@frappe.whitelist - 绝不自行 /
eval任何内容。仅可有限度地使用exec/safe_eval,而且“safe_exec 并不是魔法”。绝不接受由客户端提供的方法执行路径。safe_exec
沙箱与信任边界
- 沙箱执行(RestrictedPython/)无法被可靠地视为安全——应假定存在逃逸手段。安全开关必须位于正确的信任边界:服务器脚本启用权限应是 bench 级配置,绝不能是站点级配置(租户可能借此控制整台服务器)。
safe_exec - 优先使用 allowlist 而不是 blocklist——blocklist 可被绕过。不要默认公开所有内容。
访问控制
- “在设置 前要思考 10 次”——它不是绕过真正 authn/authz 的捷径。网页必须先应用权限,再读取/分享数据。优先使用
allow_guest=True/get_list,而不是手写查询。get_all - 精确限定权限放宽范围。 验证限流/权限例外所针对的主体恰好是预期主体——例如不能面向所有非访客用户。
路径遍历/文件系统
- 优先使用 File doctype API。如果用户输入进入路径,请确保其无法通过路径遍历()逃逸到站点目录之外。
/../../
加密/机密
- 绝不自行实现加密;复用现有实现。验证访客/Webhook 请求的真实性(HMAC)。
- 签名/一次性 URL:使用真正保密的签名值;同时按时间和首次使用限制过期;仅使用 URL 验证,不要与合并后的表单数据一起验证(Frappe 会合并 URL + 表单数据 → 一个有效签名可被重放攻击)。
- 将机密存储在 password 字段中;绝不明文存储;绝不在日志或错误消息中泄露机密。
XSS 与 OWASP 的其他部分
- 不要将用户输入注入 DOM。即使看似轻微,也应将 XSS 视为严重问题——HTML/JS 注入通常会导致账户劫持。
- 不要通过清洗并丢弃特殊字符来修复 XSS。优先在将值注入 DOM 前进行转义。
3. Performance is correctness
3. 性能就是正确性
The cheapest time to fix performance is at review; slow code merged sits
undetected for years. Performance is a feature (Doherty threshold; humans
perceive ~100ms).
- Budgets: common reads < 100ms; reads < 1s; most writes < 5s; never exceed ~10s. P99 of a frequent read-only request should be ~1s. A slow synchronous request blocks a worker (head-of-line blocking).
- Complexity rule: a frequently-called endpoint must do O(1) or O(log N)
work — a large constant factor at worst, never O(N). Counting rows is O(N), not
O(1); over a large/filtered table is expensive. Bound unbounded scans (e.g. last 3 months, a "1000+" sentinel) rather than scanning everything.
COUNT(*) - Indexes are code. Flag any /join/filter on an unindexed column. Indexes (and custom indexes) must be committed in code, not applied ad-hoc — they get lost on migration otherwise. Form loads that pull comments/versions/assignments need all those queries indexed; one unindexed query makes everything sluggish.
WHERE - No DB calls in loops. "Don't write validations that call db in LOOPS." Flag N+1 patterns. Cache stable values (UOM, docstatus, status) instead of re-querying. This is acceptable in background jobs, but never in requests.
- The Remove → Reduce → Reuse ladder for slow code you can't fix: remove it,
invoke it less, or memoize. Pick the right cache scope (
,
@redis_cache,@request_cache— the last balloons memory if overused). DO NOT hand-roll caches in@site_cacheorfrappe.local: "you'll just be creating brand-new cache-invalidation bugs." Don't cache trivially cheap work.frappe.flags - Memory: don't stuff junk into shared module-level files / / class-level state — it stays resident forever. Remove unused module-level imports (move into the function that uses them). Watch for leaks.
__init__.py - Reorder conditionals so the DB call is last. In a boolean expression, put cheap in-memory checks first so short-circuiting can skip the query entirely.
- Aggregate in SQL, not Python. Use /
SUM()in the query instead of fetching all rows to reduce them in memory; push filters into the subquery so they apply before the join.COUNT() - Don't fetch a whole doc for one value. Use /
get_value/get_single_valuefor a single column instead ofset_value; useget_doc().save()instead offrappe.delete_doc(which fetches the doc only to delete it).get_doc().delete() - No MyISAM tables in hot paths. Reading a MyISAM table takes an implicit table-level lock — never touch one in a request path.
- Move long work to a background queue. Long-running work belongs in
, not a synchronous request that blocks a worker.
enqueue(..., queue="long")
修复性能问题的最佳时机是在审查阶段;合并进来的慢代码往往多年无人察觉。性能是一项功能(Doherty 阈值;人类感知的延迟约为 100ms)。
- 预算: 常见读取 < 100ms;读取 < 1s;大多数写入 < 5s;绝不超过约 10s。高频只读请求的 P99 应约为 1s。缓慢的同步请求会阻塞 worker(队头阻塞)。
- 复杂度规则: 高频调用的 endpoint 必须执行 O(1) 或 O(log N) 的工作量——最坏只能有较大的常数因子,绝不能是 O(N)。统计行数是 O(N),不是 O(1);对大型/经过筛选的表执行 代价高昂。应限制无界扫描(例如最近 3 个月,或设置“1000+”哨兵值),而不是扫描全部数据。
COUNT(*) - 索引也是代码。 标记所有在未索引列上的 /join/filter。索引(包括自定义索引)必须通过代码提交,不能临时应用——否则迁移后会丢失。加载表单时,如果会拉取评论/版本/分配信息,则这些查询必须全部建立索引;一个未索引查询就会让整体变慢。
WHERE - 循环中不得调用数据库。 “不要编写会在 LOOP 中调用 db 的验证。”标记 N+1 模式。缓存稳定值(UOM、docstatus、status),而不是重复查询。在后台作业中可以接受这种做法,但请求中绝不可以。
- 对于无法修复的慢代码,遵循移除 → 减少 → 复用阶梯:移除它、减少调用,或对它进行记忆化处理。选择正确的缓存作用域(、
@redis_cache、@request_cache——后者过度使用会显著膨胀内存)。不要在@site_cache或frappe.local中自行实现缓存:“你只会制造全新的缓存失效 bug。”不要缓存成本极低的工作。frappe.flags - 内存: 不要把无用数据塞进共享的模块级文件//类级状态——它们会永久驻留。移除未使用的模块级导入(移入使用它们的函数中)。注意内存泄漏。
__init__.py - 调整条件顺序,让数据库调用放在最后。 在布尔表达式中,先执行开销低的内存检查,这样短路求值就能完全跳过查询。
- 在 SQL 中聚合,而不是在 Python 中。 在查询中使用 /
SUM(),而不是取回所有行后在内存中计算;将过滤条件推入子查询,使其在 join 之前生效。COUNT() - 不要为了一个值加载整份文档。 单列值应使用 /
get_value/get_single_value,而不是set_value;使用get_doc().save(),而不是frappe.delete_doc(后者会先加载整个文档再删除)。get_doc().delete() - 热路径中不要使用 MyISAM 表。 读取 MyISAM 表会获取隐式表级锁——绝不要在请求路径中访问此类表。
- 将耗时工作移至后台队列。 长时间运行的任务应放入 ,而不是放在会阻塞 worker 的同步请求中。
enqueue(..., queue="long")
4. Concurrency
4. 并发
- Check-then-act is a race. — two workers both see "not exists" and both insert. Prefer a DB-level unique constraint; "outsource integrity to the database."
if not frappe.db.exists(...): insert() - Locking footguns: on an unindexed query locks every scanned row (and gaps) — always ensure the filter uses an index, or you lock the whole table. Locking a parent but not its children yields a "mutant" doc.
SELECT ... FOR UPDATE - Global mutable state / class attributes are global in Python — a shared
engine/class attribute leaking query state across concurrent requests produces
garbage. Make query-building stateless. Don't do "weird shit with
" —
frappe.localis for variables, not static state.local
- 先检查再执行存在竞态。 ——两个 worker 都会看到“不存在”并各自插入。优先使用数据库级唯一约束;“把完整性委托给数据库。”
if not frappe.db.exists(...): insert() - 锁使用的陷阱: 对未建立索引的查询执行 会锁住所有被扫描的行(以及间隙)——务必确保过滤条件使用索引,否则会锁住整张表。只锁定父文档却不锁子文档会产生“变异体”文档。
SELECT ... FOR UPDATE - 全局可变状态/类属性在 Python 中就是全局的——共享的 engine/class 属性在并发请求间泄漏查询状态会产生垃圾数据。让查询构建保持无状态。不要用 做“奇怪的事情”——
frappe.local用于变量,而不是静态状态。local
5. Readability & maintainability
5. 可读性与可维护性
~50% of dev time is spent reading code; rotten code eventually forces a rewrite.
- Keep functions pure when they can be pure — easy to read and test.
- Don't pass mutable objects around to be filled in ("assembly" code) — return new values. Passing a mutable to be mutated forces a reader to open two files to understand one thing.
- A function that mutates its input must be named appropriately.
- Prefer the boring construct. While that functional map-reduce one-liner looks beautiful, please just write a 4-line for-loop. Favor debuggable code over clever code.
- Consistency over personal style. A codebase shouldn't be a hodge-podge of 10 styles. Match the surrounding formatting/naming/import conventions; flag a change that breaks them.
- Good taste: restructure so the edge case becomes the common case, removing special-case branches. Ask: can this be simpler? Less code? Is it over-indented if-else soup?
- Prefer extending shared components over copy-paste divergence. 3–4 forked implementations of one thing → slow long-term velocity. Avoid tight coupling across modules; integrate through clear, documented public APIs.
- Document public modules/classes/functions with docstrings; prefer type annotations over describing types in prose ("type hints are 10x better"); type checkers find non-obvious bugs.
- Docstrings should only mention important things. Keep them short and to the point. Don't explain what's trivially understood from function name. Focus on "why".
- Split unrelated changes into separate commits/PRs — keeps review focused
and /reverts clean.
git blame
约 50% 的开发时间都花在阅读代码上;腐化的代码最终会迫使项目重写。
- 在可行时让函数保持纯粹——易于阅读和测试。
- 不要把可变对象传来传去以便就地填充(“装配式”代码)——应返回新的值。传递一个随后会被修改的可变对象,迫使读者同时打开两个文件才能理解一件事。
- 会修改输入的函数,其名称必须恰当。
- 优先选择平凡易懂的构造。 那个函数式 map-reduce 单行表达式虽然看着漂亮,但请老老实实写一个 4 行 for 循环。优先保证代码可调试,而非炫技。
- 一致性优于个人风格。 一个代码库不应成为 10 种风格拼凑的大杂烩。遵循周围的格式/命名/导入约定;标记任何破坏这些约定的改动。
- 良好的代码品味: 重新组织结构,让边缘情况成为常规情况,从而移除特殊分支。问自己:能否更简单?代码能否更少?是否变成了缩进过深的 if-else 汤?
- 优先扩展共享组件,而不是复制粘贴后各自演化。 同一件事出现 3–4 个分叉实现——长期发展速度会变慢。避免模块间紧密耦合;通过清晰、有文档的公共 API 集成。
- 为公共模块/类/函数编写 docstring;优先使用类型注解,而不是用文字描述类型(“类型提示好 10 倍”);类型检查器能发现不易察觉的 bug。
- Docstring 只应提及重要事项。保持简短直接。不要解释从函数名就能轻易理解的内容。专注于“为何”。
- 将不相关的改动拆分为独立的 commit/PR——这样能让审查保持聚焦,并使 /revert 清晰整洁。
git blame
6. API design & backward compatibility
6. API 设计与向后兼容性
- Principle of least astonishment: an API's name + signature should convey ~90% of intent; users shouldn't be surprised by behavior.
- Reject loose/overloaded parameters that accept many disjoint types (string/dict/list/None). Prefer separate single-purpose functions. Beware implicit fallbacks; use explicit variants. "APIs whose correct use depends on tribal knowledge are a liability."
- Build for extension, not override. Provide hooks; never monkey-patch core at runtime ("inexcusably horrible" — breaks future fixes) and never copy a whole core file to change a few lines (fixes won't propagate).
- Backward compatibility is an obligation for mature/public APIs. Follow semver; minor versions = zero breaking changes. Breaking changes include: removing public functions/fields, reordering args, new mandatory args, changed business logic, moved/renamed files (broken imports), bumped shared deps. Renaming without keeping the old name as an alias is an unnecessary break. Every breaking change ships a deprecation warning + docs.
- Watch for schema breaking-change footguns: adding mandatory fields to existing sites, making long-lived fields unique (needs a data patch), changing field types without patches, removing fields.
- Schema changes that silently skip existing sites need a data patch. Single doctypes don't sync new-field defaults to existing sites, and a field-type change (e.g. text→int) doesn't convert existing values — both need an explicit patch, tested against a populated site.
- New parameters go last as keyword args with safe defaults (, not
None) so existing positional callers don't break. When renaming, keep the old name as a shim:"".def old_name(...): return new_name(...) - Patch hygiene. Data patches must be idempotent (safe to re-run), correctly ordered (run after the field/doctype they read exists), and live in the right app (a framework change is patched in the framework, not the downstream app).
- A modified existing test is a red flag. If making a change pass required editing an existing test's assertions, you've likely broken a real workflow — justify it explicitly rather than bending the test.
- 最小意外原则: API 的名称 + 签名应传达约 90% 的意图;用户不应对行为感到意外。
- 拒绝宽松/重载的参数,即允许许多互不相关的类型(字符串/字典/列表/None)。优先提供职责单一的独立函数。警惕隐式回退;使用明确的变体。“正确使用依赖口口相传知识的 API 是一种负担。”
- 为扩展而设计,而非为覆盖而设计。 提供 hook;绝不要在运行时 monkey-patch 核心(“无可辩解地糟糕”——会破坏未来修复),也绝不要复制整个核心文件只为改动几行(这样修复无法传播)。
- 向后兼容是成熟/公共 API 的义务。 遵循 semver;小版本 = 零破坏性变更。破坏性变更包括:移除公共函数/字段、调整参数顺序、新增必填参数、更改业务逻辑、移动/重命名文件(导致导入损坏)、升级共享依赖。重命名却不保留旧名称作为别名属于不必要的破坏。每项破坏性变更都应随附弃用警告 + 文档。
- 注意 schema 破坏性变更的陷阱: 向现有站点添加必填字段、将长期存在的字段设为 unique(需要 data patch)、不通过 patch 就更改字段类型、删除字段。
- 静默跳过现有站点的 schema 变更需要 data patch。 单个 doctype 不会把新字段默认值同步到现有站点,字段类型变更(例如 text→int)也不会转换现有值——两者都需要显式 patch,并针对包含真实数据的站点进行测试。
- 新参数放在最后,作为带安全默认值的 keyword args(,而不是
None),这样现有的位置参数调用方不会中断。重命名时,将旧名称保留为 shim:""。def old_name(...): return new_name(...) - Patch 规范。 Data patch 必须幂等(可安全重复运行)、顺序正确(在其读取的字段/doctype 已存在之后运行),并位于正确的 app 中(框架变更应在框架中修补,而不是在下游 app 中修补)。
- 修改已有测试是一个危险信号。 如果为了让变更通过而不得不编辑已有测试的断言,你很可能破坏了真实业务流程——应明确说明理由,而不是扭曲测试。
7. Testing
7. 测试
- Each PR needs decent test coverage — patch coverage on the diff, not just project coverage. (Frappe target: 85% covered lines in the diff.) Tests should capture the most-used business scenarios.
- Regression test every fix. A bug fix without a test that would have caught it invites the regression back. For extreme-consequence (stateful/compliance) code, go beyond examples — property-based testing (Hypothesis).
- Flag missing migration/data-patch coverage. Schema changes and data patches are the highest-risk, least-tested area; a change that alters fields or migrates data needs a patch tested against a realistic, populated site (empty tables always "migrate" successfully even when the change is invalid).
- Tests must be deterministic and independent. No (flaky); no reliance on state left by other tests (order-dependence); use
randomfor time-dependent logic.freeze_time
- 每个 PR 都需要足够的测试覆盖率——关注 diff 的覆盖率,而不只是项目覆盖率。(Frappe 的目标:diff 中 85% 的代码行被覆盖。)测试应覆盖最常用的业务场景。
- 为每项修复添加回归测试。 没有一个原本能够捕获该 bug 的测试,bug 修复就容易再次回归。对于后果极严重的(有状态/合规)代码,应更进一步——采用基于属性的测试(property-based testing,Hypothesis)。
- 标记缺失的 migration/data-patch 测试。 Schema 变更和 data patch 是风险最高、测试最少的领域;任何修改字段或迁移数据的变更,都需要 patch,并针对逼真的、包含数据的站点进行测试(空表总是能“成功迁移”,即使变更本身无效)。
- 测试必须确定且相互独立。 不使用 (会导致不稳定);不依赖其他测试遗留的状态(顺序依赖);对依赖时间的逻辑使用
random。freeze_time
8. Error messages, logging & observability
8. 错误消息、日志与可观测性
- Error message quality is a legitimate review item. Titles must be specific
and Google-able (never "Message"/"Error"). Reference field names as fields.
State what changed: the row, the field, and before→after values
(). The user must know "qty changed from what to what?"
1 → 2 - Surface failures to the affected party — "a broken email setup is the user's problem only if they know it's broken."
- Log things. Preserve tracebacks/exception context (orders of magnitude easier debugging). Log destructive/admin actions with attributable identity (who, when, from where), persisted outside ephemeral containers.
- 错误消息质量是合理的审查项。 标题必须具体且便于 Google 搜索(绝不能只是“Message”/“Error”)。引用字段时,应明确指出字段名。说明发生了什么变化:具体行、字段以及变化前→变化后的值()。用户必须知道“数量从什么改成了什么?”
1 → 2 - 向受影响方暴露失败——“邮件配置损坏只有在用户知道它已损坏时,才是用户的问题。”
- 记录重要事项。 保留 traceback/异常上下文(会让调试容易几个数量级)。记录破坏性/管理操作,并附上可归因的身份信息(谁、何时、从何处),且将日志持久化在临时容器之外。