feat(tasks): inject agent OTLP telemetry into cloud sandboxes - #71100
Conversation
5097766 to
931e6ba
Compare
🤖 CI report
|
| File | Patch | Uncovered changed lines |
|---|---|---|
products/tasks/backend/temporal/process_task/tests/test_utils.py |
0.0% | 3, 1072–1073, 1077, 1081–1082, 1087, 1089–1091, 1093, 1097, 1101–1102, 1107, 1109, 1111, 1115, 1119–1120, 1125, 1127 |
products/tasks/backend/temporal/process_task/tests/test_provision_sandbox.py |
0.0% | 202–205, 226, 229, 231, 236, 238, 241–245, 247, 252, 254 |
products/tasks/backend/temporal/process_task/activities/provision_sandbox.py |
0.0% | 348–349 |
products/tasks/backend/temporal/process_task/activities/tests/test_get_task_processing_context.py |
0.0% | 42–43, 54–56 |
products/tasks/backend/temporal/process_task/utils.py |
11.1% | 1152–1153, 1155, 1165–1167, 1171–1172 |
products/tasks/backend/temporal/process_task/activities/get_task_processing_context.py |
25.0% | 381–382, 384–386, 388–389, 394, 795 |
products/tasks/backend/feature_flags.py |
72.7% | 46–48 |
products/tasks/backend/temporal/client.py |
88.9% | 124, 152–153 |
products/tasks/backend/logic/services/run_log_mirror.py |
94.9% | 190–192, 198, 208–209, 281 |
🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 30028222937 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
platform_features |
██░░░░░░░░░░░░░░░░░░ 12.1% |
7 / 58 |
batch_exports |
████████░░░░░░░░░░░░ 39.6% |
8,414 / 21,249 |
demo |
███████████░░░░░░░░░ 56.2% |
1,497 / 2,663 |
warehouse_sources_queue |
████████████░░░░░░░░ 59.2% |
148 / 250 |
tasks |
██████████████░░░░░░ 69.4% |
30,000 / 43,235 |
data_tools |
██████████████░░░░░░ 70.0% |
63 / 90 |
ai_gateway |
███████████████░░░░░ 75.0% |
9 / 12 |
signals |
████████████████░░░░ 79.8% |
21,628 / 27,098 |
cdp |
████████████████░░░░ 81.0% |
3,144 / 3,881 |
data_modeling |
█████████████████░░░ 82.7% |
5,547 / 6,708 |
wizard |
█████████████████░░░ 84.8% |
935 / 1,102 |
notebooks |
█████████████████░░░ 85.3% |
7,275 / 8,531 |
agent_platform |
█████████████████░░░ 86.4% |
3,807 / 4,405 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
cohorts |
██████████████████░░ 87.8% |
4,488 / 5,114 |
product_tours |
██████████████████░░ 87.9% |
1,303 / 1,482 |
exports |
██████████████████░░ 88.4% |
6,949 / 7,861 |
data_warehouse |
██████████████████░░ 88.9% |
11,887 / 13,378 |
business_knowledge |
██████████████████░░ 89.0% |
4,391 / 4,936 |
conversations |
██████████████████░░ 89.3% |
16,820 / 18,834 |
engineering_analytics |
██████████████████░░ 89.4% |
6,371 / 7,130 |
dashboards |
██████████████████░░ 89.4% |
5,983 / 6,693 |
visual_review |
██████████████████░░ 89.4% |
5,818 / 6,505 |
error_tracking |
██████████████████░░ 89.7% |
10,174 / 11,342 |
alerts |
██████████████████░░ 89.9% |
4,054 / 4,508 |
early_access_features |
██████████████████░░ 90.1% |
1,031 / 1,144 |
mcp_analytics |
██████████████████░░ 90.1% |
2,763 / 3,065 |
streamlit_apps |
██████████████████░░ 90.4% |
2,501 / 2,767 |
links |
██████████████████░░ 90.6% |
183 / 202 |
slack_app |
██████████████████░░ 90.7% |
9,028 / 9,951 |
marketing_analytics |
██████████████████░░ 91.0% |
11,792 / 12,964 |
stamphog |
██████████████████░░ 91.1% |
4,056 / 4,450 |
mcp_store |
██████████████████░░ 91.9% |
4,257 / 4,634 |
notifications |
███████████████████░ 92.6% |
1,011 / 1,092 |
product_analytics |
███████████████████░ 92.7% |
5,811 / 6,268 |
ai_observability |
███████████████████░ 92.7% |
14,995 / 16,168 |
surveys |
███████████████████░ 93.0% |
5,730 / 6,163 |
managed_migrations |
███████████████████░ 93.1% |
1,424 / 1,530 |
web_analytics |
███████████████████░ 93.1% |
14,398 / 15,461 |
posthog_ai |
███████████████████░ 93.2% |
1,326 / 1,422 |
approvals |
███████████████████░ 93.3% |
3,437 / 3,682 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
workflows |
███████████████████░ 93.6% |
6,390 / 6,826 |
legal_documents |
███████████████████░ 94.1% |
1,568 / 1,667 |
endpoints |
███████████████████░ 94.1% |
8,640 / 9,177 |
messaging |
███████████████████░ 94.3% |
2,682 / 2,845 |
tracing |
███████████████████░ 94.5% |
2,670 / 2,826 |
revenue_analytics |
███████████████████░ 94.6% |
3,651 / 3,858 |
skills |
███████████████████░ 94.6% |
3,035 / 3,207 |
review_hog |
███████████████████░ 94.7% |
6,814 / 7,199 |
logs |
███████████████████░ 95.4% |
9,935 / 10,416 |
experiments |
███████████████████░ 95.7% |
25,036 / 26,159 |
growth |
███████████████████░ 96.1% |
3,245 / 3,376 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
replay_vision |
███████████████████░ 96.2% |
15,094 / 15,691 |
feature_flags |
███████████████████░ 96.5% |
17,110 / 17,734 |
user_interviews |
███████████████████░ 96.5% |
2,638 / 2,734 |
access_control |
███████████████████░ 96.8% |
851 / 879 |
customer_analytics |
███████████████████░ 97.1% |
9,047 / 9,317 |
warehouse_sources |
███████████████████░ 97.1% |
329,165 / 338,906 |
data_catalog |
███████████████████░ 97.4% |
2,367 / 2,429 |
analytics_platform |
████████████████████ 98.0% |
2,153 / 2,197 |
metrics |
████████████████████ 98.2% |
2,491 / 2,536 |
pulse |
████████████████████ 98.4% |
2,017 / 2,049 |
live_debugger |
████████████████████ 99.2% |
613 / 618 |
field_notes |
████████████████████ 99.4% |
158 / 159 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
ℹ️ Docs preview — preview build triggered
Docs from this PR will be published at posthog.com.
| Project | Preview | Updated (UTC) |
|---|---|---|
| posthog.com | Open preview | Jul 23, 2026, 5:10 PM |
The preview should be ready in about 10 minutes. Open the preview at /handbook/engineering/.
|
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 4 · PR risk: 0/10 |
…g it The sandbox delivers POSTHOG_AGENT_OTEL_LOGS_TOKEN via the container environment, which agent-spawned tool subprocesses inherit - so any `env` dump in a persisted transcript or PR body would leak the key (flagged by review bots on PostHog/posthog#71100). bin.ts is the only consumer: parse it into the server config, then delete it from process.env so children never see it. Defense in depth, not a boundary - same-UID processes can still read the container's initial env via /proc; the key itself remains a write-only capture token scoped to a dedicated telemetry project. No test: a one-line env mutation at the CLI entry with no cheap seam; exercising it would mean spawning the CLI end to end. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Companion to PostHog/code#posthog-code/agent-run-otel-telemetry, which adds OTLP export of cloud-run metadata (logs + APM spans) to the agent-server. New optional settings SANDBOX_AGENT_OTEL_LOGS_URL/_TOKEN and SANDBOX_AGENT_OTEL_TRACES_URL are mapped to POSTHOG_AGENT_OTEL_* sandbox env vars by a shared helper called from both env assembly paths (fresh provisioning and snapshot resume). The names are deliberately not standard OTEL_* so OTel SDKs in user code running in the sandbox never auto-export into the telemetry project; the keys are reserved so user sandbox env vars cannot override them. Local dev is covered by adding the URLs to the agentsh debug firewall settings and the Docker localhost rewrite list; prod egress is already allowed via *.posthog.com. Telemetry stays off unless URL + token are both set; the traces URL additionally enables APM spans. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
…t and root-status fixes Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Mirrors the PostHog/code telemetry changes: run errors export provenance only (generic body, error_source/stop_reason attributes - the raw message stays in the session log and the run's error_message), flush/shutdown are best-effort and per-signal independent with a 5s export timeout, a run error marks still-open tool spans as interrupted, and the deprecated OtelTransportConfig stubs are kept for published-package compatibility. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
…minal-point fix Mirrors the PostHog/code change: the task_run root span is now ended and exported at the run's in-process terminal point (background prompt settled, terminal failure, or interactive close) instead of relying on sandbox teardown, which never delivers SIGTERM to the exec'd agent-server process. Documents the interactive hard-teardown limitation. Stacked on posthog-code/agent-run-otel-telemetry (pure fast-forward). Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Incorporates the scout run log mirror from PR #71094 (credit: Andrew Maguire, @andrewm4894) into the agent-run telemetry branch, squashed from the PR's final state so both delivery paths ship together. Persisted task-run log entries are mirrored into the PostHog Logs product, scoped to scout runs. There is no transport of its own: TaskRun.append_log emits one structured stdout line per persisted entry (event=task_run_log) and the per-cluster OTel collector that already tails container stdout ships them into the region's internal project's Logs, parsing JSON keys into queryable attributes and request_id (the run uuid) into a trace id so one run groups as one trace. - run_log_mirror.py translates each ACP JSONL entry: readable bodies for agent messages / tool calls / sandbox output / turn ends (8k char cap, bounded entries per call), severity mapping, and run-identity fields as log attributes - TaskRun.append_log calls the mirror after the S3 write, guarded so any failure is logged and never breaks the run - TASK_RUN_LOGS_MIRROR_ORIGIN_PRODUCTS setting gates which task origins mirror (default signals_scout; empty disables) Complementary to the OTLP run-telemetry export on this branch: the mirror carries full readable bodies for PostHog-authored scout runs into the internal project, while the OTLP path stays metadata-only and covers customer-driven cloud runs. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
…rror Documents the incorporated scout run log mirror (#71094) alongside the OTLP run-telemetry export: architecture, the two pipelines' differing privacy postures and scopes, changed files, and the new TASK_RUN_LOGS_MIRROR_ORIGIN_PRODUCTS setting in the configuration reference. Also applies ruff formatting to test_utils.py after the rebase conflict resolution. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Local testing showed the mirrored scout-run logs (readable agent message bodies, tool calls, sandbox output) never reach the Logs UI in dev: the mirror emits structured stdout lines for the cluster OTel collector to pick up, but locally append_log runs in the host Django process and otel-collector-config.dev.yaml only tails docker-compose container stdout - so the lines land in the phrocs pane and go nowhere. Only the agent-server's own OTLP metadata records were visible in /logs. Add an optional direct delivery leg: when TASK_RUN_LOGS_MIRROR_OTLP_URL and _TOKEN are set, each mirrored batch is also POSTed as OTLP JSON to that logs endpoint (service.name=task-run-log-mirror, run uuid as the trace id - the same request_id -> trace mapping the production collector applies - and the structlog fields as attributes). Both settings default unset, so production keeps the zero-transport stdout path; delivery failures are logged and never raise into the run. Tests: payload shape (severity mapping, trace id, int attribute encoding) and a parameterized guard that no HTTP call happens unless both settings are set - protecting the hot append_log path from accidental sync network calls in production. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
…y leg Drops the mirror-specific TASK_RUN_LOGS_MIRROR_OTLP_URL/_TOKEN settings: in local dev there is one logs destination anyway, and the agent telemetry settings (SANDBOX_AGENT_OTEL_LOGS_URL/_TOKEN) already point at it - so the mirror's direct delivery leg now reuses them, gated on DEBUG. With the agent telemetry configured locally, mirrored scout runs reach /logs with zero extra configuration. The DEBUG gate is what makes the reuse safe: in production those settings point at the customer-facing telemetry project and the collector daemonset already delivers pod stdout to the internal project, so an unconditional reuse would double-deliver scout bodies into the wrong project the moment the telemetry rollout sets them. A test pins that production never posts even with the settings configured. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
The mirror's direct OTLP leg is meant to run in production, not just dev: it delivers scout run transcripts into PostHog's own internal logs project. The dedicated TASK_RUN_LOGS_MIRROR_OTLP_URL/_TOKEN settings are restored (replacing the short-lived DEBUG-gated reuse of the agent telemetry settings) and the DEBUG gate is dropped. The token is the mechanism that keeps customer projects out of it: scout runs execute for customer teams, but capture-logs routes records by the Bearer key, so a centrally configured internal-project key means the mirrored transcripts only ever land in - and bill - our own project, never the customer's. That destination differs from the agent-telemetry project SANDBOX_AGENT_OTEL_LOGS_TOKEN addresses, which is why the settings stay separate. Unset disables the direct leg; the stdout emission for the collector remains either way. Delivery is one POST per persisted batch, scout runs only, 3s timeout, and never raises into the run. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
…d input Review findings from a subagent pass over the mirror module: - The OTLP POST runs synchronously inside append_log, and a scalar requests timeout applies to connect and read independently - a slow connect followed by a stalled read could block the append for ~6s. Use a (1, 3) connect/read tuple to bound the worst case. - acp_method, acp_session_update, and entry_timestamp were forwarded uncapped from the semi-trusted append_log request; an oversized value could push the stdout line past the collector's 100 KB drop threshold, silently discarding the whole record. Cap them at 200 chars. - _extract_text recursed without a depth bound, so an adversarially nested content list would RecursionError and abort mirroring for the entire batch. Bound it at 10 levels. Tests: identifier-cap and deep-nesting regressions, an OTLP-leg privacy test pinning that protocol payloads never reach the payload and that the attribute set is closed, and a negative gating row for the snapshot-resume env path (logs pair missing -> no OTel keys). Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Simplify-pass cleanups, no behavior change: - pass the already-derived session update dict into _body instead of re-parsing the notification per entry - hoist the batch-constant trace fields and event attribute out of the per-record loop in _post_otlp - drop the Z-suffix replace in _time_unix_nano (fromisoformat handles it natively on our Python) Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
REPORT.md was a working artifact for the agent session, not part of the change itself. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
origin_product was API-settable to signals_scout (only image_builder was rejected), so any task:write caller could forge a scout task whose appended run logs would be mirrored into PostHog's internal Logs project once the mirror ships - flagged by the security review bots on the PR. Scout tasks are created exclusively server-side by the signals scout harness via the tasks facade, so treat signals_scout like image_builder: an internal-only origin the public serializer rejects. With creation server-attested, mirroring_enabled() can keep gating on origin_product. The mirror is new in this PR, so no forged tasks can predate the check where it matters. Test: parameterized serializer-level SimpleTestCase over the internal-only origins (verified to fail without the fix); the existing image_builder endpoint test remains the wiring guard. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
154de31 to
82042b7
Compare
…g it The sandbox delivers POSTHOG_AGENT_OTEL_LOGS_TOKEN via the container environment, which agent-spawned tool subprocesses inherit - so any `env` dump in a persisted transcript or PR body would leak the key (flagged by review bots on PostHog/posthog#71100). bin.ts is the only consumer: parse it into the server config, then delete it from process.env so children never see it. Defense in depth, not a boundary - same-UID processes can still read the container's initial env via /proc; the key itself remains a write-only capture token scoped to a dedicated telemetry project. No test: a one-line env mutation at the CLI entry with no cheap seam; exercising it would mean spawning the CLI end to end. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Telemetry was env-gated only (SANDBOX_AGENT_OTEL_* / TASK_RUN_LOGS_MIRROR_ORIGIN_PRODUCTS): all-or-nothing per region, with a deploy needed to change anything - no gradual rollout and no runtime kill switch. Add tasks-agent-run-otel-telemetry, evaluated org-targeted once at dispatch and stamped into run state, following the sandbox-event-ingest pattern (_capture_sandbox_event_ingest_flag is generalized to _capture_run_feature_flags, stamping both flags in one fetch + one atomic mutate). The stamp keeps the decision stable across retries and resumes. The flag gates both legs, AND'ed with the existing env settings: - sandbox OTel env injection - fresh provisions read the new TaskProcessingContext.agent_otel_telemetry_enabled field (state override -> DEBUG -> flag), snapshot resumes pass it into build_sandbox_environment_variables - the scout run-log mirror - reads the state stamp directly, so the append_log hot path never evaluates a flag remotely Fail-closed everywhere: missing stamp, missing flag, or evaluation error means telemetry stays off. DEBUG bypasses the flag (the analytics SDK is disabled locally), so local dev remains purely env-gated. Deploys are inert until the flag is created and ramped. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Two Python code quality (mypy) failures on the branch: - run_log_mirror.mirror_entries typed entries as list[dict], making the defensive non-dict guard "unreachable" under warn_unreachable. The guard is load-bearing (entries arrive from a semi-trusted request payload), so type the parameter list[Any] to match reality. - the flag-gating commit accidentally injected AGENT_OTEL_TELEMETRY_STATE_KEY as the first positional argument of the keep-stream-open feature_enabled call (an over-broad substring replacement), which mypy flagged as a duplicate distinct_id. Restore the call. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Review bots flagged that agent_otel_telemetry_enabled was not in _PROTECTED_RUN_STATE_KEYS, so a same-team task:write caller could PATCH a run's state to force the stamp true and bypass the org rollout flag - injecting the internal OTLP capture token into their sandbox on resume and re-enabling the run-log mirror with the rollout off. Add the shared AGENT_OTEL_TELEMETRY_STATE_KEY constant to the protected set, and sandbox_event_ingest_enabled alongside it - the sibling rollout stamp had the same gap (a caller could flip the event transport branch mid-run). Dispatch (_capture_run_feature_flags) remains the sole writer. Extends the existing protected-state endpoint test: both stamps in the attack payload and assertions, and the telemetry stamp in the state_remove_keys leg. Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Three counters so the cloud-background-agents-runs Grafana dashboard can
verify the telemetry rollout:
- posthog_tasks_agent_otel_telemetry_stamped_total{enabled} - first-time
rollout stamps at dispatch; the enabled=true share tracks the
tasks-agent-run-otel-telemetry flag ramp on new runs
- posthog_tasks_run_log_mirror_entries_total{origin_product} - entries
the run-log mirror actually emitted
- posthog_tasks_run_log_mirror_otlp_batches_total{outcome} - direct-OTLP
mirror delivery outcomes (local-dev leg; stays flat in cloud)
Generated-By: PostHog Code
Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
…annotations Three fixes for the review comment + red CI on the branch: - _is_agent_otel_telemetry_enabled checked the state stamp before DEBUG, but local dev always stamps False (the analytics SDK is disabled there), so the DEBUG bypass was dead and local sandboxes would never get the OTel env even with SANDBOX_AGENT_OTEL_* set - flagged by graphite-app, and inconsistent with the mirror's agent_otel_telemetry_enabled_for_state. DEBUG now wins first at both gates; pinned by a parametrized precedence test. - test_captures_sandbox_event_ingest_flag_before_starting_workflow patched the shared posthoganalytics module attribute and asserted a single feature_enabled call; the dispatch capture now evaluates two flags, so the test asserts both stamps and both calls. - TestAppendLogMirroring needed class-level organization/team annotations for the setUpTestData attributes (mypy attr-defined, the "Python code quality" failure). Generated-By: PostHog Code Task-Id: 0c511836-2180-455a-9b58-45df2a0661ec
Problem
Cloud task agent telemetry needs an opt-in path from the sandbox to PostHog Logs and Tracing. The backend did not have safe configuration plumbing for the OTLP endpoints and project token, and fresh runs and snapshot resumes needed identical behavior.
Changes
Before:
flowchart LR Agent{{Cloud agent}} --> API[Run log API] API --> Session[(Session log)] classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff; classDef phRed fill:#f54e00,stroke:#f54e00,color:#fff; classDef phGray fill:#e5e7eb,stroke:#c7ccd1,color:#000; class Agent phBlue; class API phRed; class Session phGray;After:
flowchart LR Settings[Backend settings] --> Agent{{Cloud agent}} Agent --> API[Run log API] API --> Session[(Session log)] Agent --> Logs[OTLP logs] Agent --> Traces[OTLP traces] classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff; classDef phRed fill:#f54e00,stroke:#f54e00,color:#fff; classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000; classDef phGray fill:#e5e7eb,stroke:#c7ccd1,color:#000; class Agent phBlue; class API,Logs,Traces phRed; class Settings phYellow; class Session phGray;Also carried on this branch: scout run log mirror (#71094)
This branch now incorporates the scout run log mirror from #71094 (credit @andrewm4894), squashed from that PR's final state so both delivery paths ship together:
TaskRun.append_logemits one structured stdout line per persisted entry (event=task_run_log); the per-cluster OTel collector that already ships container stdout delivers them into the region's internal project's Logs, withrequest_id(the run uuid) becoming the trace id.TASK_RUN_LOGS_MIRROR_ORIGIN_PRODUCTS(default:signals_scoutonly; empty disables). Mirror failures are logged and never break the run's log write.TASK_RUN_LOGS_MIRROR_OTLP_URL/_TOKEN, default unset). The token pins the destination to PostHog's own internal logs project — scout runs execute for customer teams, and their mirrored transcripts must never land in (or bill) a customer's project. It is also the only delivery path in local dev, whereappend_logruns in the host Django process whose stdout the dev collector (docker containers only) never tails.If #71094 merges separately first, the duplicated commit here should be dropped on the next rebase.