SDK-6983: upload SDK logs and emit kill telemetry on signal termination - #86
SDK-6983: upload SDK logs and emit kill telemetry on signal termination#86kamal-kaur04 wants to merge 7 commits into
Conversation
Signal-terminated runs previously lost their SDK-log tarball (onComplete's
upload never runs on a kill) and emitted no SDKTestSuccessful at all — the
funnelDataSent flag was set by SDKTestAttempted at startup, permanently
disarming the exit-time cleanup resend. Killed builds were undebuggable and
invisible to query-side kill detection.
- capture interrupt signals on BrowserStackConfig (killSignal)
- stamp finishedMetadata {reason: user_killed, signal} on SDKTestSuccessful
- exit-time cleanup rescues the logs.tar.gz upload (--uploadLogs), gated by
a logsUploaded flag so delivered uploads are never repeated
- mark funnelDataSent only for SDKTestSuccessful
- snapshot funnelData credentials before fireFunnelRequest redacts them
in place
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The kill reason previously rode only the analytics funnel; the TestHub
build-stop payload stayed empty on terminated runs. Propagate the caught
signal via env (the detached cleanup child has no in-memory config) and
stamp finished_metadata {reason: user_killed, signal} on the direct-flow
stopBuildUpstream PUT, and exitSignal/exitReason on the CLI StopBinSession
request (the binary maps them to finished_metadata).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🔴 SDK PR Review gate is red. Pending:
It turns green once the latest SDK PR Review Agent run reports GTG on the current head commit. A native reviewer approval is separately required by branch protection before merge. |
Reword the explanatory comments to remove the tracker-ID prefixes while keeping their content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add unit coverage for the interrupt-signal handling: the exit-handler uploadLogs rescue arg, the cleanup log-upload with pre-redaction creds, the funnel finish-reason/kill-signal marking, stopBuildUpstream's finished_metadata, and grpcClient's exitSignal/exitReason on stop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🔴 SDK PR Review gate is red. Pending:
It turns green once the latest SDK PR Review Agent run reports GTG on the current head commit. A native reviewer approval is separately required by branch protection before merge. |
|
🔴 SDK PR Review gate is red. Pending:
It turns green once the latest SDK PR Review Agent run reports GTG on the current head commit. A native reviewer approval is separately required by branch protection before merge. |
07souravkunda
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 4 inline finding(s). Full report in the PR comment below. Verdict: Failed - see PR comment.
| } | ||
| }) | ||
|
|
||
| getInterruptSignals().forEach((sig) => { |
There was a problem hiding this comment.
HIGH — signal handlers never re-exit. These process.on(sig,…) handlers only set the kill flag; attaching a listener suppresses Node's default termination. SIGTERM (the primary CI-cancellation path) and SIGHUP/SIGQUIT/SIGABRT no longer exit the process — it hangs until CI sends SIGKILL, which fires no 'exit' event, so the cleanup child never spawns and logs are not uploaded. After stamping kill metadata, drive a deterministic exit (re-raise via process.kill(process.pid, sig) or process.exit(128+n)) without preempting WDIO's own graceful SIGINT/SIGTERM shutdown.
There was a problem hiding this comment.
Fixed in 9f48766 — valid catch. The listener now arms an unref()'d 5s grace timer after stamping the signal and forces process.exit(128 + n) (SIGTERM→143) if nothing else has terminated the process. WDIO's own graceful shutdown gets first shot (a naturally exiting process is never held open — timer is unref'd); process.exit still fires the 'exit' listener, so the cleanup child spawns and the log/funnel/build-stop rescue runs. 5s stays inside typical CI kill grace (e.g. GitHub Actions ~7.5s before SIGKILL). Unit-tested with fake timers (no exit pre-grace, exit 143 post-grace).
| // identifiable server-side, not only via the analytics funnel. | ||
| const killSignal = process.env[BROWSERSTACK_KILL_SIGNAL] | ||
| if (killSignal) { | ||
| data.finished_metadata = [{ reason: 'user_killed', signal: killSignal }] |
There was a problem hiding this comment.
MEDIUM — finished_metadata shape. Sent here as an array [{reason,signal}], but the funnel path sends an object {reason,signal}. No existing precedent for finished_metadata on the HTTP builds/stop payload in main — confirm the server schema and align the shape, else the kill reason is silently dropped.
There was a problem hiding this comment.
Checked against the server schema — the array is correct for this payload, no change made. The HTTP builds/stop endpoint takes finished_metadata as an array: the node-agent direct flow sends finished_metadata: [{reason, signal, failure_data}] (browserstack-node-agent testhubHandler.js), and the binary's own stopBuild builds finished_metadata: [...] with [] default (browserstack-binary packages/@browserstack/testhub/index.js). The object shape you saw is the EDS funnel event (finishedMetadata inside SDKTestSuccessful event props) — a different destination with its own schema. Both destinations verified end-to-end in the kill-matrix validation (BQ web_events shows the object; build record gets the array via the stop).
| import TestOpsConfig from './testOps/testOpsConfig.js' | ||
| import { BStackLogger } from './bstackLogger.js' | ||
| import { BrowserstackCLI } from './cli/index.js' | ||
| import { BROWSERSTACK_TESTHUB_UUID, BROWSERSTACK_KILL_SIGNAL } from './constants.js' |
There was a problem hiding this comment.
LOW — duplicate import. Second import … from './constants.js' (line 6 is the first). Merge into one (no-duplicate-imports). The v8 port #87 already has this merged.
There was a problem hiding this comment.
Fixed in 9f48766 — merged into a single ./constants.js import.
| if (response) { | ||
| // A delivered upload must not be repeated by the exit-time cleanup | ||
| // rescue; failed/skipped uploads stay eligible for it. | ||
| this.browserStackConfig.logsUploaded = true |
There was a problem hiding this comment.
LOW — logsUploaded set only on a truthy response. If the upload succeeds but returns an empty body, the flag stays false and the detached cleanup child re-uploads. Wasteful, not harmful.
There was a problem hiding this comment.
Fixed in 9f48766 — and your comment surfaced a worse inverse case: a truthy server rejection (response.status !== 'success', which uploadLogs records as a failure) also set logsUploaded = true, disabling the rescue for a build whose upload never landed. Now delivered = !!response && !(response.status && response.status !== 'success') gates the flag. The empty-body-success case can't be distinguished from a transport failure (nodeRequest returns undefined for both on this path), so it stays rescue-eligible — wasteful-but-safe as you noted. Unit-tested (rejection → flag false; success/no-status → true).
Claude Code Review — PR #86SDK-6983: upload SDK logs and emit kill telemetry on signal termination Rescues the SDK-log upload and stamps kill telemetry when a WDIO run is interrupted by a signal. Registers signal handlers that record the kill signal on config + Findings1 · HIGH · Correctness — signal handlers never re-exit — 2 · MEDIUM · Contract — 3 · LOW · Quality — duplicate import from 4 · LOW · Efficiency — Verified clean
Verdict: FAIL — one High-severity correctness finding (#1) that also undermines the PR's own goal. Automated review. Findings verified against the branch head and the proto/API contracts reachable from the repo; finding #2 needs a server-side schema confirmation. |
…log uploads Signal listeners suppress Node's default termination, so a hung shutdown would live until CI's SIGKILL (which fires no 'exit' event, skipping the cleanup rescue). Arm an unref'd 5s grace timer that forces the conventional 128+n exit. Also stop marking logsUploaded when the upload server returns a non-success status, so a rejected upload stays eligible for the rescue. Merge the duplicate constants import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🔴 SDK PR Review gate is red. Pending:
It turns green once the latest SDK PR Review Agent run reports GTG on the current head commit. A native reviewer approval is separately required by branch protection before merge. |
Claude Code Review — PR #86 (re-review)Head Prior findings — all resolved#1 (HIGH) signal handlers never re-exit → ✅ resolved. #2 (MEDIUM) #3 (LOW) duplicate #4 (LOW) Non-blocking note
Verdict: PASS — the High-severity issue is fixed correctly and every prior finding is resolved. |
|
🔴 SDK PR Review gate is red. Pending:
It turns green once the latest SDK PR Review Agent run reports GTG on the current head commit. A native reviewer approval is separately required by branch protection before merge. |
|
🟢 SDK PR Review Agent — GTG on head The prior High-severity signal-handler finding is fixed correctly ( Verdict: PASS. (A native GitHub reviewer approval is still separately required by branch protection before merge.) |
|
🟢 SDK PR Review gate is green — the SDK PR Review Agent has given a GTG for this PR (the A native GitHub reviewer approval is still separately required by branch protection before this PR can merge — this check does not substitute for that. |
1 similar comment
|
🟢 SDK PR Review gate is green — the SDK PR Review Agent has given a GTG for this PR (the A native GitHub reviewer approval is still separately required by branch protection before this PR can merge — this check does not substitute for that. |
What is this about?
Signal-terminated wdio runs (SIGTERM/SIGINT — Ctrl-C, CI cancellation) lose their SDK-log tarball and, on the v9 line, emit no
SDKTestSuccessfulat all — leaving killed builds undebuggable (no log object in S3) and invisible to query-side kill detection. Fleet impact quantified under SDK-6983/SDK-6969 (Bucket E; WDIO-mocha is the largest terminated-run cohort, and killed wdio builds dominate the "logs not found" residue).Kill-matrix validation on production packages showed: event drain and (post-test) stopBuild survive a kill, but the
logs.tar.gzupload only happens inlauncher.onComplete, which a kill either never reaches or cuts short — 3/3 killed cucumber builds and 2/3 killed mocha builds had no S3 log object.Changes:
BrowserStackConfig(killSignal) — the v9 line previously had no signal awareness at all.SDKTestSuccessfulnow carriesfinishedMetadata: { reason: 'user_killed', signal }when the run was signal-terminated (rides the same field the session-linking dashboard already reads forsession_reloaded).cleanup.jsnow takes--uploadLogs <clientBuildUuid>and uploadslogs.tar.gzwhen the launcher's own upload never ran — gated by alogsUploadedflag so delivered uploads are never repeated.funnelDataSentregression fix: the flag was set after ANY funnel event (includingSDKTestAttemptedat startup), permanently disarming the exit-timeSDKTestSuccessfulresend — killed v9 runs sent no finish event at all. Now only the finish event sets it.fireFunnelRequestredactsfunnelDatacredentials in place; cleanup now snapshots them before the funnel send (the log-upload rescue otherwise authenticates as[REDACTED]).Validation (wdio 9, mocha + cucumber, mid-run SIGTERM): killed builds now land their full log tar in S3 (verified via the SDK-logs bucket; e.g. 202KB mocha tar, and a very-early cucumber kill still rescued), funnel carries
user_killed/SIGTERM, exit code 143; baselines unaffected (single upload, no behavior change). Evidence: workspacework/tra-session-linking-instability-split/E/FIX-flush-on-signal-validation.md.Companion PR (node SDK direct flow, same lane): browserstack/browserstack-node-agent#2344.
Note for the v8 line: the same cleanup rescue should be cherry-picked onto the v8 branch when it lands (v8 has signal capture + telemetry already, but the same missing log upload).
Related Jira task/s
Release (mandatory for every PR — required for the
ready-for-reviewlabel)Version bump: (required — tick exactly one)
Release notes type: (optional)
Release notes (customer-facing): (optional but encouraged)
Release notes (internal):
killSignal, emitSDKTestSuccessfulwithfinishedMetadata {reason: user_killed, signal}, and rescue thelogs.tar.gzupload from exit-timecleanup.js(--uploadLogs, gated bylogsUploaded). FixedfunnelDataSentbeing set bySDKTestAttempted(killed runs emitted no finish event) and snapshot funnelData credentials beforefireFunnelRequest's in-place redaction.Checklist
PR Validations
Run Tests: Comment RUN_TESTS to trigger sanity tests.
🤖 Generated with Claude Code