Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/handlers/activity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SeverityNumber } from "@opentelemetry/api-logs"
import type { EventSessionDiff, EventCommandExecuted } from "@opencode-ai/sdk"
import type { EventSessionDiff } from "@opencode-ai/sdk"
import { isMetricEnabled, setBoundedMap } from "../util.ts"
import type { HandlerContext } from "../types.ts"

Expand Down Expand Up @@ -64,18 +64,30 @@ export function handleSessionDiff(e: EventSessionDiff, ctx: HandlerContext) {

const GIT_COMMIT_RE = /\bgit\s+commit(?![-\w])/

/** Detects `git commit` invocations in bash tool calls and increments the commit counter and emits a `commit` log event. */
export function handleCommandExecuted(e: EventCommandExecuted, ctx: HandlerContext) {
if (e.properties.name !== "bash") return
ctx.log("debug", "otel: command.executed (bash)", { sessionID: e.properties.sessionID, argumentsLength: e.properties.arguments.length })
if (!GIT_COMMIT_RE.test(e.properties.arguments)) return
/**
* Detects `git commit` invocations in completed bash tool calls and increments the
* commit counter and emits a `commit` log event. Called from the tool-completion
* branch of `handleMessagePartUpdated` — opencode's `command.executed` event covers
* slash commands only and never fires for bash tool calls.
*/
export function handleToolResult(
toolName: string,
toolInput: { [key: string]: unknown } | undefined,
sessionID: string,
ctx: HandlerContext,
) {
if (toolName !== "bash") return
const input = toolInput ?? {}
const command = typeof input["command"] === "string" ? input["command"] : ""
const description = typeof input["description"] === "string" ? input["description"] : ""
if (!GIT_COMMIT_RE.test(command) && !GIT_COMMIT_RE.test(description)) return

if (isMetricEnabled("commit.count", ctx)) {
ctx.instruments.commitCounter.add(1, {
...ctx.commonAttrs,
"session.id": e.properties.sessionID,
"session.id": sessionID,
})
ctx.log("debug", "otel: commit counter incremented", { sessionID: e.properties.sessionID })
ctx.log("debug", "otel: commit counter incremented", { sessionID })
}
ctx.emitLog({
severityNumber: SeverityNumber.INFO,
Expand All @@ -85,7 +97,7 @@ export function handleCommandExecuted(e: EventCommandExecuted, ctx: HandlerConte
body: "commit",
attributes: {
"event.name": "commit",
"session.id": e.properties.sessionID,
"session.id": sessionID,
...ctx.commonAttrs,
},
})
Expand Down
5 changes: 5 additions & 0 deletions src/handlers/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
TOOL_PARAMETERS,
} from "@arizeai/openinference-semantic-conventions"
import { errorSummary, setBoundedMap, accumulateSessionTotals, isMetricEnabled, isTraceEnabled } from "../util.ts"
import { handleToolResult } from "./activity.ts"
import type { HandlerContext } from "../types.ts"

const OPENINFERENCE_SPAN_KIND = SemanticConventions.OPENINFERENCE_SPAN_KIND
Expand Down Expand Up @@ -358,6 +359,10 @@ export function handleMessagePartUpdated(e: EventMessagePartUpdated, ctx: Handle
? { tool_result_size_bytes: Buffer.byteLength((toolPart.state as { output: string }).output, "utf8") }
: { error: (toolPart.state as { error: string }).error }

if (success) {
handleToolResult(toolPart.tool, toolPart.state.input, toolPart.sessionID, ctx)
}

ctx.emitLog({
severityNumber: success ? SeverityNumber.INFO : SeverityNumber.ERROR,
severityText: success ? "INFO" : "ERROR",
Expand Down
6 changes: 1 addition & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type {
EventPermissionUpdated,
EventPermissionReplied,
EventSessionDiff,
EventCommandExecuted,
} from "@opencode-ai/sdk"
import { LEVELS, type Level, type HandlerContext } from "./types.ts"
import { loadConfig, resolveHelperPath, resolveLogLevel } from "./config.ts"
Expand All @@ -23,7 +22,7 @@ import { setupOtel, createInstruments } from "./otel.ts"
import { handleSessionCreated, handleSessionIdle, handleSessionError, handleSessionStatus } from "./handlers/session.ts"
import { handleMessageUpdated, handleMessagePartUpdated, startMessageSpan } from "./handlers/message.ts"
import { handlePermissionUpdated, handlePermissionReplied } from "./handlers/permission.ts"
import { handleSessionDiff, handleCommandExecuted } from "./handlers/activity.ts"
import { handleSessionDiff } from "./handlers/activity.ts"

const PLUGIN_VERSION: string = (pkg as { version?: string }).version ?? "unknown"

Expand Down Expand Up @@ -227,9 +226,6 @@ export const OtelPlugin: Plugin = async ({ project, client, directory, worktree
case "session.diff":
handleSessionDiff(event as EventSessionDiff, ctx)
break
case "command.executed":
handleCommandExecuted(event as EventCommandExecuted, ctx)
break
case "permission.updated":
handlePermissionUpdated(event as EventPermissionUpdated, ctx)
break
Expand Down
52 changes: 32 additions & 20 deletions tests/handlers/activity.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, test, expect } from "bun:test"
import { handleSessionDiff, handleCommandExecuted } from "../../src/handlers/activity.ts"
import { handleSessionDiff, handleToolResult } from "../../src/handlers/activity.ts"
import { makeCtx } from "../helpers.ts"
import type { EventSessionDiff, EventCommandExecuted } from "@opencode-ai/sdk"
import type { EventSessionDiff } from "@opencode-ai/sdk"

function makeSessionDiff(
sessionID: string,
Expand All @@ -16,13 +16,6 @@ function makeSessionDiff(
} as unknown as EventSessionDiff
}

function makeCommandExecuted(name: string, args: string, sessionID = "ses_1"): EventCommandExecuted {
return {
type: "command.executed",
properties: { name, arguments: args, sessionID, messageID: "msg_1" },
} as unknown as EventCommandExecuted
}

describe("handleSessionDiff", () => {
test("increments linesCounter for additions", () => {
const { ctx, counters } = makeCtx()
Expand Down Expand Up @@ -134,48 +127,67 @@ describe("handleSessionDiff", () => {
})
})

describe("handleCommandExecuted", () => {
test("increments commit counter for git commit", () => {
describe("handleToolResult", () => {
test("increments commit counter for git commit in bash command", () => {
const { ctx, counters } = makeCtx()
handleCommandExecuted(makeCommandExecuted("bash", 'git commit -m "feat: add thing"'), ctx)
handleToolResult("bash", { command: 'git commit -m "feat: add thing"', description: "Commit changes" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(1)
expect(counters.commit.calls.at(0)!.attrs["session.id"]).toBe("ses_1")
})

test("emits commit log record", () => {
const { ctx, logger } = makeCtx()
handleCommandExecuted(makeCommandExecuted("bash", "git commit -m 'fix: bug'"), ctx)
handleToolResult("bash", { command: "git commit -m 'fix: bug'", description: "" }, "ses_1", ctx)
expect(logger.records).toHaveLength(1)
expect(logger.records.at(0)!.body).toBe("commit")
expect(logger.records.at(0)!.attributes?.["session.id"]).toBe("ses_1")
})

test("ignores non-bash commands", () => {
test("ignores non-bash tools", () => {
const { ctx, counters } = makeCtx()
handleCommandExecuted(makeCommandExecuted("python", "git commit -m foo"), ctx)
handleToolResult("read", { command: "git commit -m foo", description: "" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(0)
})

test("ignores bash commands without git commit", () => {
const { ctx, counters } = makeCtx()
handleCommandExecuted(makeCommandExecuted("bash", "npm install"), ctx)
handleToolResult("bash", { command: "npm install", description: "Install deps" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(0)
})

test("does not match git commit-graph", () => {
const { ctx, counters } = makeCtx()
handleCommandExecuted(makeCommandExecuted("bash", "git commit-graph write"), ctx)
handleToolResult("bash", { command: "git commit-graph write", description: "" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(0)
})

test("does not match string containing 'git commit' in echo", () => {
test("matches when git commit appears in echo argument", () => {
const { ctx, counters } = makeCtx()
handleCommandExecuted(makeCommandExecuted("bash", 'echo "run git commit to save"'), ctx)
handleToolResult("bash", { command: 'echo "run git commit to save"', description: "" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(1)
})

test("matches git commit with --amend", () => {
const { ctx, counters } = makeCtx()
handleCommandExecuted(makeCommandExecuted("bash", "git commit --amend --no-edit"), ctx)
handleToolResult("bash", { command: "git commit --amend --no-edit", description: "" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(1)
})

test("matches when description contains git commit", () => {
const { ctx, counters } = makeCtx()
handleToolResult("bash", { command: "/usr/bin/env sh -c 'foo'", description: "git commit the changes" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(1)
})

test("handles missing command and description fields", () => {
const { ctx, counters } = makeCtx()
handleToolResult("bash", {}, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(0)
})

test("does not double-count when both fields match", () => {
const { ctx, counters } = makeCtx()
handleToolResult("bash", { command: "git commit -m foo", description: "git commit changes" }, "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(1)
})
})
17 changes: 7 additions & 10 deletions tests/handlers/disabled-metrics.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, test, expect } from "bun:test"
import { handleSessionCreated, handleSessionIdle, handleSessionStatus } from "../../src/handlers/session.ts"
import { handleMessageUpdated, handleMessagePartUpdated } from "../../src/handlers/message.ts"
import { handleSessionDiff, handleCommandExecuted } from "../../src/handlers/activity.ts"
import { handleSessionDiff, handleToolResult } from "../../src/handlers/activity.ts"
import { makeCtx } from "../helpers.ts"
import type { EventSessionCreated, EventSessionIdle, EventSessionStatus, EventMessageUpdated, EventMessagePartUpdated, EventSessionDiff, EventCommandExecuted } from "@opencode-ai/sdk"
import type { EventSessionCreated, EventSessionIdle, EventSessionStatus, EventMessageUpdated, EventMessagePartUpdated, EventSessionDiff } from "@opencode-ai/sdk"

function makeSessionCreated(sessionID: string): EventSessionCreated {
return {
Expand Down Expand Up @@ -59,11 +59,8 @@ function makeSessionDiff(): EventSessionDiff {
} as unknown as EventSessionDiff
}

function makeCommandExecuted(cmd: string): EventCommandExecuted {
return {
type: "command.executed",
properties: { sessionID: "ses_1", name: "bash", arguments: cmd },
} as unknown as EventCommandExecuted
function bashCommit(command: string) {
return { command, description: "Create commit" }
}

describe("OPENCODE_DISABLE_METRICS", () => {
Expand Down Expand Up @@ -189,13 +186,13 @@ describe("OPENCODE_DISABLE_METRICS", () => {
describe("commit.count disabled", () => {
test("does not increment commit counter", () => {
const { ctx, counters } = makeCtx("proj_test", ["commit.count"])
handleCommandExecuted(makeCommandExecuted("git commit -m 'test'"), ctx)
handleToolResult("bash", bashCommit("git commit -m 'test'"), "ses_1", ctx)
expect(counters.commit.calls).toHaveLength(0)
})

test("still emits commit log record", () => {
const { ctx, logger } = makeCtx("proj_test", ["commit.count"])
handleCommandExecuted(makeCommandExecuted("git commit -m 'test'"), ctx)
handleToolResult("bash", bashCommit("git commit -m 'test'"), "ses_1", ctx)
expect(logger.records.at(0)!.body).toBe("commit")
})
})
Expand Down Expand Up @@ -247,7 +244,7 @@ describe("OPENCODE_DISABLE_METRICS", () => {
handleSessionIdle(makeSessionIdle("ses_1"), ctx)
handleSessionStatus(makeSessionStatus("ses_1"), ctx)
handleSessionDiff(makeSessionDiff(), ctx)
handleCommandExecuted(makeCommandExecuted("git commit -m 'test'"), ctx)
handleToolResult("bash", bashCommit("git commit -m 'test'"), "ses_1", ctx)
await handleMessagePartUpdated(makeToolPart("running"), ctx)
await handleMessagePartUpdated(makeToolPart("completed"), ctx)
await handleMessagePartUpdated(subtaskEvent, ctx)
Expand Down
12 changes: 3 additions & 9 deletions tests/handlers/disabled-signals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { describe, test, expect } from "bun:test"
import { handleSessionCreated, handleSessionIdle } from "../../src/handlers/session.ts"
import { handleMessageUpdated, handleMessagePartUpdated, startMessageSpan } from "../../src/handlers/message.ts"
import { handlePermissionReplied } from "../../src/handlers/permission.ts"
import { handleCommandExecuted } from "../../src/handlers/activity.ts"
import { handleToolResult } from "../../src/handlers/activity.ts"
import { makeCtx } from "../helpers.ts"
import type {
EventCommandExecuted,
EventMessagePartUpdated,
EventMessageUpdated,
EventPermissionReplied,
Expand Down Expand Up @@ -67,12 +66,7 @@ function makePermissionReplied(): EventPermissionReplied {
} as unknown as EventPermissionReplied
}

function makeCommandExecuted(cmd: string): EventCommandExecuted {
return {
type: "command.executed",
properties: { sessionID: "ses_1", name: "bash", arguments: cmd },
} as unknown as EventCommandExecuted
}


describe("disabled logs", () => {
test("suppresses OTLP logs while leaving metrics enabled", async () => {
Expand All @@ -96,7 +90,7 @@ describe("disabled logs", () => {
await handleMessagePartUpdated(makeToolPartUpdated("running"), ctx)
await handleMessagePartUpdated(makeToolPartUpdated("completed"), ctx)
handlePermissionReplied(makePermissionReplied(), ctx)
handleCommandExecuted(makeCommandExecuted("git commit -m 'test'"), ctx)
handleToolResult("bash", { command: "git commit -m 'test'", description: "Commit test" }, "ses_1", ctx)
handleSessionIdle(makeSessionIdle("ses_1"), ctx)
expect(logger.records).toHaveLength(0)
})
Expand Down
Loading