Skip to content

epic-4/story-1: Add Production-Safe Diagnostic Logging - #38

Closed
usmanabbas7 wants to merge 7 commits into
epic-3/story-5-establish-javascript-parity-fixtures-for-state-and-evaluation-behaviorfrom
epic-4/story-1-add-production-safe-diagnostic-logging
Closed

epic-4/story-1: Add Production-Safe Diagnostic Logging#38
usmanabbas7 wants to merge 7 commits into
epic-3/story-5-establish-javascript-parity-fixtures-for-state-and-evaluation-behaviorfrom
epic-4/story-1-add-production-safe-diagnostic-logging

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Story 4.1 — Add Production-Safe Diagnostic Logging

First story of Epic 4. Adds the production-safe diagnostic logging layer and the centralized redaction primitive frozen by qs-08-secret-redaction.

What was built

  • _internal/redaction.py (NEW, L0 leaf, stdlib-only): redact_key (sdk_key_abcdef1234567890sdk_****_7890; short keys fully masked), redact_url (host+path, entire query string elided), SafeContext dataclass (six approved fields), fingerprint_visitor (SHA-256 ref, never raw visitor_id).
  • logging.py (EXTENDED — Story 2.4 helpers preserved): added log_safe() event-oriented wrapper keyed off the LifecycleEvent vocabulary; structural NFR6 defense-in-depth that redacts PII-named kwargs.
  • errors.py: Story 1.2 inline redaction shim repointed onto _internal/redaction.redact_url (one redaction implementation; exception public contracts unchanged — enrichment is Story 4.2).
  • core.py / context.py: additive event-oriented diagnostic emission at the init and orchestration seams — allowlisted fields + hashed visitor ref only. No control-flow / readiness / evaluation / tracking behavior change. Determinism untouched.
  • config.py: additive logger: Optional[logging.Logger] = None.

NFR coverage

  • NFR6 allowlist-only log output (no raw PII), NFR7 credentials fully omitted at all levels, NFR23 stricter query-elision interpretation. Structural redaction at record-construction time (not filter-gated).

Tests

  • 543 → 596 passing (+53; parity suite 123 intact). Zero regressions.
  • redaction (25), logging (14), diagnostic emission (6), config/errors regression (4), integration round-trips (2), layering guards (2).

Beads

Epic ai-driven-product-dev-xk4a; tasks -wmh7, -xgcy, -99y0, -tf6b, -qko1, -2y4i — all closed.

Sprint / review notes

🤖 Generated with Claude Code

@usmanabbas7 usmanabbas7 self-assigned this Jun 8, 2026
@usmanabbas7
usmanabbas7 requested a review from clllaur June 8, 2026 10:25

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

Copy link
Copy Markdown

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 introduces centralized, privacy-safe redaction primitives and integrates them into the SDK's logging and error-handling layers. Key changes include the addition of a centralized redaction module, a safe logging wrapper (log_safe), and updated error classes that leverage these primitives. The feedback highlights opportunities to improve robustness, specifically by handling potential ValueError exceptions in urlsplit, explicitly casting visitor_id to a string to prevent runtime crashes, and quoting log values that contain spaces or special characters to preserve log structure integrity.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +87 to +95
parts = urlsplit(url)
host = parts.netloc or ""
path = parts.path or ""
if host or path:
endpoint = f"{host}{path}"
else:
# Not a recognizable absolute URL; strip any query suffix from the raw
# value rather than emitting it verbatim.
endpoint = url.split("?", 1)[0]

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

Defensive Programming / Robustness: urllib.parse.urlsplit can raise a ValueError if the URL contains invalid/control characters (e.g., under certain Python security updates or malformed inputs). Since redact_url is called during exception formatting (ConfigLoadError, TrackingDeliveryError) and logging (log_safe), any unhandled exception here will crash the error-handling or logging flow, potentially taking down the application or masking the original error. Wrapping urlsplit in a try...except ValueError block with a safe fallback ensures maximum robustness.

    try:
        parts = urlsplit(url)
        host = parts.netloc or ""
        path = parts.path or ""
        if host or path:
            endpoint = f"{host}{path}"
        else:
            endpoint = url.split("?", 1)[0]
    except ValueError:
        endpoint = url.split("?", 1)[0]

deterministic (same input → same fingerprint) so diagnostic records for one
visitor can be correlated without exposing identity.
"""
digest = hashlib.sha256((visitor_id or "").encode("utf-8")).hexdigest()

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

Defensive Programming: If a user passes a non-string value (like an integer) as the visitor_id (which is common in Python despite type hints), (visitor_id or "") will evaluate to that non-string object, and calling .encode("utf-8") on it will raise an AttributeError. Converting the value to a string explicitly using str(visitor_id or "") prevents runtime crashes.

Suggested change
digest = hashlib.sha256((visitor_id or "").encode("utf-8")).hexdigest()
val = str(visitor_id or "")
digest = hashlib.sha256(val.encode("utf-8")).hexdigest()

Comment on lines +101 to +107
parts = [f"event={event.value}"]
if context is not None:
for k, v in context.as_log_fields().items():
parts.append(f"{k}={v}")
for name, value in fields.items():
parts.append(f"{name}={_redact_field(name, value)}")
log.log(level, " ".join(parts))

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

Robustness / Log Structure Integrity: The log message is constructed by joining key-value pairs with spaces (' '.join(parts)). If any value (such as an experience key, variation key, or custom attribute) contains spaces, tabs, or newlines, it will corrupt the space-separated structure of the log line, making it extremely difficult or impossible for log parsers (like Splunk, Datadog, or ELK) to parse the fields reliably. Quoting values that contain spaces or special characters using json.dumps preserves the integrity of the log structure.

    def _quote_value(val: Any) -> str:
        import json
        s = str(val)
        if any(c in s for c in ' \t\n"\\'):
            return json.dumps(s)
        return s

    parts = [f"event={event.value}"]
    if context is not None:
        for k, v in context.as_log_fields().items():
            parts.append(f"{k}={_quote_value(v)}")
    for name, value in fields.items():
        parts.append(f"{name}={_quote_value(_redact_field(name, value))}")
    log.log(level, " ".join(parts))

usmanabbas7 and others added 7 commits June 15, 2026 16:28
…tion.py)

Beads: ai-driven-product-dev-wmh7
redact_key/redact_url/SafeContext/fingerprint_visitor; L0 leaf, stdlib only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-tf6b
Optional[logging.Logger]=None; default resolves convert_sdk namespace logger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…redaction.py

Beads: ai-driven-product-dev-99y0
Single redaction implementation; ConfigLoadError/TrackingDeliveryError contracts unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-xgcy
Event-oriented LifecycleEvent-keyed records; structural redaction at all levels;
SDKConfig.logger honored; library-logging discipline preserved. Story 2.4 helpers untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + context.py

Beads: ai-driven-product-dev-qko1
Additive log_safe emission at init/config-load (core) and bucketing/conversion
(context) seams; hashed visitor ref, allowlisted fields only; no outcome change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-2y4i
Extend integration suite (direct-config + sdk_key qs-06 flows leak nothing) and
test_layering.py (_internal/redaction stdlib-only, logging.py no higher-layer imports).
Full suite 595 (parity 123 intact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…safe kwargs

Structurally replace values under visitor_id/attributes/email/name field names
with [REDACTED] so a raw PII value can never leak through log_safe even on
future misuse. Adds regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@usmanabbas7
usmanabbas7 force-pushed the epic-4/story-1-add-production-safe-diagnostic-logging branch from a0d368d to 2091406 Compare June 15, 2026 11:28
@usmanabbas7

Copy link
Copy Markdown
Collaborator Author

F-066 propagation (rebase onto remediated 3-3)

Rebased onto the remediated stack (3-3 f2d8916 → … → this branch). Does not modify evaluation/segments.py; the latch fix is inherited cleanly (byte-identical to remediated 3-3), no conflicts.

  • uv run pytest606 passed
  • CI gate: no .github/workflows/ on this branch → no-ci.

@abbaseya

Copy link
Copy Markdown
Collaborator

Superseded — all commits already in main (bc76b64). Closing without merge as part of post-sprint cleanup.

@abbaseya abbaseya closed this Jun 18, 2026
@abbaseya
abbaseya deleted the epic-4/story-1-add-production-safe-diagnostic-logging branch June 18, 2026 16:31
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.

2 participants