Skip to content

feat: Phase 6 — DevEx + observability polish#9

Merged
fbmoulin merged 3 commits intomainfrom
feat/phase6-quality-polish
Apr 27, 2026
Merged

feat: Phase 6 — DevEx + observability polish#9
fbmoulin merged 3 commits intomainfrom
feat/phase6-quality-polish

Conversation

@fbmoulin
Copy link
Copy Markdown
Owner

Closes Phase 6 of ROADMAP.md.

Summary

Item Where Notes
Dependabot weekly grouped .github/dependabot.yml pip + github-actions, separate security group
ruff config pyproject.toml [tool.ruff] E/F/W/I/UP/B/C4/SIM rules + per-file ignores
mypy CI job .github/workflows/ci.yml mypy: strict on personalize/, permissive elsewhere; soft gate (|| true)
bandit CI job .github/workflows/ci.yml bandit: HARD gate on HIGH severity (currently 0)
request_id middleware app.py UUID4 + structlog contextvars + X-Request-ID header in/out
KCD_* env-var reference README.md full table for capture + server tunables

Code-quality fixes surfaced by new linters

  • bandit B324: hashlib.md5(url.encode()) in kratos_clone/capture.py was flagged HIGH. Used purely as a content-addressable filename hash (not security). Annotated with usedforsecurity=False per stdlib idiom.
  • bandit B201: app.run(debug=True) in app.py __main__ — only ever runs in dev (production uses gunicorn wsgi:app). Annotated # nosec B201 with comment.
  • ruff SIM105: 3× try/except/pass in app.py:_purge_session + process_download — replaced with contextlib.suppress(Exception).
  • ruff UP045: Optional[Callable[...]] in kratos_clone/capture.pyCallable[...] | None (PEP 604).
  • mypy strict on personalize/: 7 errors fixed (Mapping for invariance, type-ignores for openai SDK call-overload mismatches, explicit return types on json.loads results).

Test plan

  • uv run pytest -q → 183 passed, 2 skipped (live OpenAI gated)
  • uv run ruff check && ruff format --check → all clean
  • uv run mypy personalize/ → no issues found
  • uv run bandit -r personalize/ kratos_clone/ scripts/ app.py --severity-level high → exit 0
  • import app thread-count invariant preserved (P2-7)
  • X-Request-ID propagation verified end-to-end via test client

Out of scope (deferred, see TODO.md)

  • Type hints on app.py and kratos_clone/ (gradual; soft mypy gate stays soft until ready)
  • Bumping bandit gate to MEDIUM
  • Type stubs for OpenAI SDK overload mismatches

Commits

  • 6d43dd5 baseline (Dependabot + ruff/mypy/bandit + auto-fixes)
  • 5126edf request_id middleware
  • (this PR) docs: KCD_* table + Phase 6 SHIPPED

…g + CI jobs

Adds:
- .github/dependabot.yml — weekly grouped pip + github-actions updates
- pyproject.toml [tool.ruff] with E/F/W/I/UP/B/C4/SIM rule set + per-file ignores
- pyproject.toml [tool.mypy] strict on personalize/, permissive on legacy
- CI job 'mypy (personalize/ strict, rest permissive)' — soft gate
- CI job 'bandit (security lint)' — HARD gate on HIGH severity (currently 0)

Fixes surfaced by new linters:
- B324 hashlib.md5 in kratos_clone/capture.py — added usedforsecurity=False
- B201 flask debug=True in app.py __main__ — annotated #nosec B201 (dev-only)
- SIM105 try-except-pass in app.py — replaced with contextlib.suppress(Exception)
- UP045 Optional[Callable] in kratos_clone/capture.py — replaced with X | None
- 7 mypy errors in personalize/ — fixed (Mapping for invariance, type-ignores
  for openai SDK overload mismatches, explicit return type annotations)

Result: ruff clean, mypy clean on personalize/ (8 files), bandit HIGH=0,
178 tests + 2 skipped. Auto-formatted 21 legacy files via ruff format.
- before_request: parse X-Request-ID header (allow-list [A-Za-z0-9_-]{1,64})
  or generate UUID4. Bind to structlog contextvars so every log line in the
  request scope auto-inherits request_id=<id>.
- after_request: emit X-Request-ID in response, clear contextvars.
- _safe_request_id: defense-in-depth regex for header injection (in practice
  Werkzeug rejects \r\n at the parser level — caught in test).
- 5 tests; full suite 183 passing.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@fbmoulin has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 43 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e14e8d82-d1e4-4e0f-9075-793358262afc

📥 Commits

Reviewing files that changed from the base of the PR and between 641d857 and 2747ccf.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • README.md
  • ROADMAP.md
  • TODO.md
  • app.py
  • kratos_clone/__main__.py
  • kratos_clone/capture.py
  • kratos_clone/post.py
  • personalize/openai_client.py
  • personalize/patcher.py
  • personalize/sanitize.py
  • pyproject.toml
  • scripts/generate_design_system_v1.py
  • scripts/generate_design_system_v2.py
  • scripts/inventory.py
  • scripts/post_process.py
  • scripts/probe.py
  • scripts/validate.py
  • tests/conftest.py
  • tests/test_capture_helpers.py
  • tests/test_client_errors.py
  • tests/test_generator_helpers.py
  • tests/test_personalize_openai_client.py
  • tests/test_personalize_patcher.py
  • tests/test_post.py
  • tests/test_post_process.py
  • tests/test_probe.py
  • tests/test_request_id.py
  • tests/test_validate.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase6-quality-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@fbmoulin fbmoulin merged commit 55109d4 into main Apr 27, 2026
8 checks passed
@fbmoulin fbmoulin deleted the feat/phase6-quality-polish branch April 27, 2026 18:39
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request completes Phase 6 of the project, focusing on developer experience and observability. Key improvements include the addition of a Dependabot configuration for automated updates, a comprehensive environment variable reference in the README, and the introduction of X-Request-ID middleware for request tracing. The project's tooling is also enhanced with new configurations for ruff, mypy, and bandit. Review feedback identifies a potential context leakage in the Flask middleware that should be resolved by using teardown_request, suggests adding error handling for base64 decoding in the image patcher to improve resilience, and notes redundant lines in the updated roadmap that require cleanup.

Comment thread app.py
Comment on lines +101 to +107
@app.after_request
def _emit_request_id(response: Response) -> Response:
rid = request.environ.get("request_id")
if rid:
response.headers["X-Request-ID"] = rid
structlog.contextvars.clear_contextvars()
return response
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Clearing context variables in after_request is not robust because this hook is skipped if an unhandled exception occurs during request processing. This can lead to context leakage (e.g., an old request_id appearing in logs for a subsequent request on the same worker thread). Using teardown_request ensures the context is cleared regardless of whether the request succeeded or failed.

Suggested change
@app.after_request
def _emit_request_id(response: Response) -> Response:
rid = request.environ.get("request_id")
if rid:
response.headers["X-Request-ID"] = rid
structlog.contextvars.clear_contextvars()
return response
@app.after_request
def _emit_request_id(response: Response) -> Response:
rid = request.environ.get("request_id")
if rid:
response.headers["X-Request-ID"] = rid
return response
@app.teardown_request
def _clear_request_context(exception: Exception | None = None) -> None:
structlog.contextvars.clear_contextvars()

Comment thread personalize/patcher.py
Comment on lines +169 to +171
# Accept either raw bytes (from gpt-image-1 result) or a base64 string
# (from older callers / serialized plans).
raw: bytes = base64.b64decode(payload) if isinstance(payload, str) else payload
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The base64 decoding of the image payload should be wrapped in a try-except block. If the input string is malformed or not valid base64, base64.b64decode will raise an exception, which would crash the entire personalization process for the current request. Adding error handling here makes the patcher more resilient to unexpected input.

Suggested change
# Accept either raw bytes (from gpt-image-1 result) or a base64 string
# (from older callers / serialized plans).
raw: bytes = base64.b64decode(payload) if isinstance(payload, str) else payload
# Accept either raw bytes (from gpt-image-1 result) or a base64 string
# (from older callers / serialized plans).
try:
raw: bytes = base64.b64decode(payload) if isinstance(payload, str) else payload
except Exception:
log.warning("invalid_image_payload", slot_id=slot_id)
continue

Comment thread ROADMAP.md
**Out of scope (deferred to Phase 7 / cleanup sprint):**
- Type hints on `app.py` and full `kratos_clone/` (gradual typing — flip the soft gate when ready)
- Bumping bandit gate to MEDIUM
- Type stubs for the OpenAI SDK overload-mismatch (see `# type: ignore[call-overload]` in `personalize/openai_client.py`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The lines immediately following this section (128-130) appear to be dangling table rows from the previous version of the roadmap. They are redundant with the information now provided in the updated table (lines 117-122) and should be removed to maintain correct Markdown structure.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2747ccfe2c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app.py
Comment on lines +102 to +106
def _emit_request_id(response: Response) -> Response:
rid = request.environ.get("request_id")
if rid:
response.headers["X-Request-ID"] = rid
structlog.contextvars.clear_contextvars()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear request context on teardown, not only after response

The request ID context is only cleared in after_request, but Flask skips after_request when exceptions are propagated (notably in debug/testing modes). Since this app’s __main__ runs with debug=True, an unhandled error can leave structlog contextvars populated and mis-attribute subsequent logs to the previous request. Add a teardown_request (or equivalent finally-path) clear to guarantee cleanup on all request exit paths.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant