发布

  • [OPIK-6529] [BE] feat(analytics): runner pairing/disconnect events with runner_type + reason (#6745)

    frostbyte_neo 发布于 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
      "run opik 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 configure when 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 /api suffix 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 project from 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 expectedBaseUrl with URL() 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_link branch as well, so the CLI context card surfaces even
      when the link fragment is malformed.
    • PairingStatusScreen: loosen showWorkspaceContext to fire when any of
      workspace/project/url is present; gate the Workspace row individually.
    • pairing.py: URL-encode the workspace query param the same way as
      url and project, 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 stop to 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 ID and 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 calls disconnect_runner before 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 connect and opik endpoint become Click groups with a hidden _run
      subcommand and a shared RunnerGroup.resolve_command fallback, so the
      legacy opik endpoint --project X -- python script.py invocation form
      still works alongside the new stop subcommand. --project is 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_url so 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.

    • CreateSessionRequest gains a nullable headless flag; 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.activate now returns an ActivationResult(runnerId, runnerType, headless) record so the resource layer can tag analytics
      events without re-reading Redis.
    • opik_connect_started and opik_connect_succeeded carry runner_type
      and headless. opik_connect_failed carries runner_type best-effort
      via a new workspace-scoped peekSessionType lookup (missing when the
      session doesn't exist or is in another workspace).
    • RunnerService.disconnectRunner returns Optional<RunnerType>: present
      on a real disconnect, empty on no-ops (already reaped / not owned).
    • LocalRunnersResource.disconnectRunner emits a new
      opik_runner_disconnected event 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-slack needs;
    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 existing type arg
    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 new reason property: "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

    下载附件