-
[OPIK-6529] [BE] feat(analytics): runner pairing/disconnect events with runner_type + reason (#6745)
发布于
2026-05-25 10:47:19 +00:00 -
[NA] [SDK] refactor: group opik connect/endpoint into cli/local_runner with Rich error UX
-
Move connect.py, endpoint.py, _run.py, pairing.py, error_view.py under
cli/local_runner/ so the pairing flow is one cohesive subpackage. -
Banner now shows Workspace alongside Opik URL and Project, with values
rendered bold so they read before the pairing link. -
Replace the inline error string with a Rich-rendered labelled block
(Reason / Workspace / URL / Config / Fix / Docs / Run) via a new
RichClickError + build_config_error_block factory. Plain text stays on
.message for Sentry/tests; Rich output is used in .show(). -
Detect "no config file at ~/.opik.config" and surface a clear
"runopik configure" call-to-action on every failure path. -
Drop default values on internal kwargs that production callers always
supply (workspace, base_url, config_file_exists, create_if_missing, etc.)
so signatures reflect actual usage; add test helpers to keep call sites
readable.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
-
[NA] [SDK][FE] feat: auto-configure on connect/endpoint + pairing context UX
-
opik connect/endpoint auto-launch
opik configurewhen no config file
exists. Skipped on --non-interactive, --headless, no TTY, or when an
API key is already supplied via --api-key / OPIK_API_KEY. -
Extract preflight helpers (should_create_project, maybe_auto_configure)
into cli/local_runner/preflight.py so tests can target a public surface. -
Pair URL now carries
&url=<ui-url>with the/apisuffix stripped, so
the pairing page can show the user-facing instance address. -
Pairing error screens render a labelled context card under the subtitle:
"Workspace: X / Pairing with Opik at: " — URL is a clickable link
that opens the Opik UI in a new tab. -
scripts/dev-runner.{sh,ps1} export TOGGLE_FORCE_WORKSPACE_VERSION=version_2
by default so a fresh local backend doesn't trip the "Workspace upgrade
required" pairing screen. Override via TOGGLE_FORCE_WORKSPACE_VERSION=disabled.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
-
[NA] [FE] feat(pairing): reword settings card and reorder Opik URL/Workspace
-
Add caption "The CLI tried to pair using these Opik settings:" above
the card so the framing is unambiguous — these are the values the CLI
used, not the user's current session. -
Rename "Pairing with Opik at" → "Opik URL" so the labels match the CLI
banner (Opik URL / Workspace) verbatim. -
Reorder rows: URL first, then Workspace — same order as the banner.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
-
[NA] [SDK][FE] feat(pairing): include project name in pair link and context card
-
CLI build_pairing_link now accepts project_name and appends a URL-encoded
&project=<name>query param. run_pairing forwards it through. -
PairingPage reads
projectfrom the query; PairingStatusScreen renders
a new Project row under Workspace in the error context card. -
Project names with spaces /
/round-trip safely via percent-encoding;
added a regression test covering the encoding shape.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
-
fix lint
-
fix(pairing): scheme allow-list on Opik URL link + URL-encode workspace
Addresses PR #6691 review feedback:
- PairingStatusScreen: validate
expectedBaseUrlwithURL()and only
render it as a clickable<a>when the scheme is http/https; otherwise
render as plain text. Prevents a crafted pair URL like
?url=javascript:alert(1)from producing a clickable script link. - PairingPage: thread expected workspace/project/baseUrl through the
invalid_linkbranch as well, so the CLI context card surfaces even
when the link fragment is malformed. - PairingStatusScreen: loosen
showWorkspaceContextto fire when any of
workspace/project/url is present; gate the Workspace row individually. - pairing.py: URL-encode the
workspacequery param the same way as
urlandproject, so workspace names containing&/=don't split
the query and confuse the FE's URLSearchParams parsing. Added a
regression test covering the encoding.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(tui): use solid bullet for banner mark
The combining-enclosing-circle glyph (⠀⃝) failed to render on
terminals that lack the combining mark, falling back to two red
placeholder boxes before "opik". Switch to U+25CF (●), which renders
consistently as a solid colored dot.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- refactor(preflight): skip redundant project lookup when preflight already 404'd
should_create_project now returns (create_if_missing, known_missing) so
the interactive path — which already observed the 404 before prompting —
can tell resolve_project_id to skip the duplicate lookup and go straight
to _create_project. Headless still leaves known_missing=False so the
resolver can find an already-existing project across re-runs.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
-
[OPIK-6497] [SDK][FE] feat(runner): notify backend on close for instant disconnect
-
Supervisor calls disconnect_runner on shutdown (Ctrl+C, SIGTERM, 410
eviction, clean exit) so the FE flips the runner card off before the
heartbeat TTL expires. Best-effort: server-side cleanup is idempotent
and the heartbeat-TTL reaper picks up anything we miss. -
activate.py restores default signal handlers and raises KeyboardInterrupt
on first signal so a second Ctrl+C force-exits when the child wedges on
its own SIGINT path. Bound supervisor's graceful timeout to 5s. -
Tighten FE poll interval to 1s so the disconnect surfaces in the runner
card immediately. -
Cover supervisor disconnect on shutdown, 410 eviction, ApiError tolerance,
and end-to-end SIGINT/SIGTERM delivery via a subprocess driver.
Implements OPIK-6497.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- revert(activate): drop SIG_DFL + KeyboardInterrupt force-kill path
The two-stage signal handling (raise KeyboardInterrupt on first signal,
restore SIG_DFL so a second signal force-kills) was a safety net for
wedged agents that swallow KeyboardInterrupt. Removing it: the supervisor's
graceful timeout + SIGKILL escalation already covers wedged children, and
keeping the handler purely cooperative avoids changing the SIGINT contract
that frameworks (uvicorn, etc.) may rely on.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- [OPIK-6516] [SDK] feat(runner): add
opik connect/endpoint stopto cleanly terminate local runners
Adds first-class stop subcommands so Ollie (and humans) can tear down a
headless runner without resorting to pkill on the process group, which often
left orphaned children and stale "Endpoint open" state in the Agent Sandbox.opik connect stop --project X | --all | --runner IDand the same for
endpoint. Each Supervisor writes a small JSON lock file under
~/.opik/runners/on startup (pid, runner_id, runner_type, project,
workspace, started_at) and removes it in the shutdown finally; stale entries
are purged opportunistically on the next write.- Stop sends SIGTERM to the supervisor pid, which routes through the
OPIK-6497 handler that callsdisconnect_runnerbefore exit — the FE flips
off on its next poll instead of waiting out the heartbeat TTL. SIGKILL is
reserved for unresponsive supervisors and skips the backend notification
(the reaper picks them up). opik connectandopik endpointbecome Click groups with a hidden_run
subcommand and a sharedRunnerGroup.resolve_commandfallback, so the
legacyopik endpoint --project X -- python script.pyinvocation form
still works alongside the newstopsubcommand.--projectis now
required at the Click layer instead of validated by hand.- Headless endpoint pairing now surfaces a "Paired ✓" panel with a direct
Agent Playground link (connect keeps the generic project URL since Ollie
is the user-facing surface there). Both browser and headless flows share
pairing.post_pairing_urlso the destination logic lives in one place. - Internal helpers (Supervisor, launch_supervisor) drop default values on
parameters that aren't part of the user SDK API — every caller passes
workspace/project_name/runner_type explicitly. - Intra-package imports in
cli/local_runner/switched to module-style
(from . import pairing; pairing.RunnerType) for clearer call-site
attribution and easier patch targets in tests.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- [OPIK-6529] [BE] feat(analytics): tag runner pairing events with runner_type, headless, and emit opik_runner_disconnected
Adds the fields we need to distinguish Ollie traffic from human-driven pairings
in PostHog, and to compare clean SDK-driven shutdowns against heartbeat-TTL
reaps.CreateSessionRequestgains a nullableheadlessflag; PairingService
persists it in the Redis session hash and reads it back on activate. Old
sessions without the field decode as false (browser-pairing default).PairingService.activatenow returns anActivationResult(runnerId, runnerType, headless)record so the resource layer can tag analytics
events without re-reading Redis.opik_connect_startedandopik_connect_succeededcarryrunner_type
andheadless.opik_connect_failedcarriesrunner_typebest-effort
via a new workspace-scopedpeekSessionTypelookup (missing when the
session doesn't exist or is in another workspace).RunnerService.disconnectRunnerreturnsOptional<RunnerType>: present
on a real disconnect, empty on no-ops (already reaped / not owned).LocalRunnersResource.disconnectRunneremits a new
opik_runner_disconnectedevent with runner_id, workspace_id, user_name,
runner_type, date — only when an actual disconnect happened. This makes
the SDK-driven shutdown signal (OPIK-6497) measurable against the
heartbeat-TTL reaper path.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- ci(guardrails): disable guardrails-ai integration tests
guardrails-ai is on PyPI quarantine, so the install step in the lib
integration runner fails before pytest even starts. Comment out the
job and its references in workflow_dispatch and notify-slackneeds;
drop the SUITE_RESULTS entry (a YAML#would survive as literal text
into the JSON payload). Re-enable when the package is back on PyPI.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- style(runner): collapse RunnerGroup fallback call onto one line
ruff format wants the super().resolve_command(...) call on a single
line; restore that to unblock CI lint.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- test(analytics): assert opik_connect_*/runner_disconnected payloads end-to-end
Routes the analytics StatsClient at the existing WireMock instance and adds
a BiEvents nested class to each of PairingResourceTest and LocalRunnersResourceTest
that captures the actual JSON the BE would deliver to stats.comet.com.PairingResourceTest$BiEvents (5):
- connect_started carries runner_type and headless=false for browser pairing
- connect_started carries runner_type=endpoint, headless=true for a headless
SDK pairing (Ollie) - connect_succeeded reads runner_type and headless from the stored Redis session
- connect_failed peeks the stored runner_type on bad HMAC (ForbiddenException)
- connect_failed omits runner_type cleanly when the session doesn't exist
(NotFoundException) — exercises the peek-empty branch without crashing
LocalRunnersResourceTest$BiEvents (2):
- opik_runner_disconnected fires with runner_id, workspace_id, runner_type
on a real SDK-initiated DELETE of an owned runner - the same DELETE against an unknown runner returns 204 and does NOT emit
the event (no-op disconnects do not pollute the clean-shutdown signal)
Also adds a small callDisconnect helper to LocalRunnersResourceClient so the
DELETE call doesn't need to be hand-rolled in tests.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- refactor(analytics): drop headless tagging from pairing events
The SDK can't tell the BE it's running headless — the generated
create_pairing_session client doesn't carry the flag — so the event
property would have been "false" for every request and the Ollie-vs-browser
distinction was unactionable. Pull the field off the wire format, out of
the Redis session hash, and off the connect_started / connect_succeeded
payloads. runner_type tagging stays (it rides on the existingtypearg
the SDK already sends), and opik_runner_disconnected stays — those two
already cover the "how many people ran opik connect / opik stop" question.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- feat(analytics): split opik_runner_disconnected by reason (stopped|reaped)
The clean-shutdown event already fired from LocalRunnersResource on the
SDK-driven DELETE. Add the missing half: emit the same event from the
heartbeat-TTL reaper the first time it observes a runner is dead (gated
on FIELD_DISCONNECTED_AT so the same dead runner doesn't re-emit on
later reaper passes during the purge grace window).Both paths carry runner_id, workspace_id, user_name, runner_type, date,
and a newreasonproperty: "stopped" for SDK-driven clean exits and
"reaped" for heartbeat-TTL deaths. Single event name keeps PostHog
totals clean; the property lets us slice clean-vs-ungraceful and
compute the ratio. Wires AnalyticsService into RunnerServiceImpl;
covered by new tests in LocalRunnerReaperIntegrationTest (fire-exactly-
once and never-on-alive) plus an assertion bump in
LocalRunnersResourceTest$BiEvents.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(analytics): attribute reaped runner event to user identity
reapRunner runs in the Quartz scheduler outside request scope, so the
2-arg trackEvent overload fell back to the installation anonymous ID.
Pass the captured userName via the 3-arg overload, and use HashMap +
conditional put to omit user_name / runner_type when unset (matches the
connect_failed pattern instead of emitting empty strings).Drop verify(analyticsService) tests per the analytics-instrumentation
skill convention — recordsDisconnectedAtOnFirstReap covers the gating
via the disconnected_at field, and the stopped path is exercised
organically by LocalRunnersResourceTest$BiEvents over WireMock.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com
下载附件
-