Skip to content
Merged
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
11 changes: 8 additions & 3 deletions actions/setup/js/send_otlp_span.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,10 @@ function isValidSpanId(id) {
* trace ID so that dispatched child workflows share the parent's OTLP trace;
* `context.otel_parent_span_id` is used as the parent span ID so the child's setup span
* is properly nested under the parent's setup span in the trace hierarchy; and
* `context.item_type`, `context.item_number`, and `context.trigger_label` are emitted as
* `gh-aw.trigger.item_type`, `gh-aw.trigger.item_number`, and `gh-aw.trigger.label`
* attributes so every span can be linked back to the GitHub item that triggered the workflow
* `context.item_type`, `context.item_number`, `context.trigger_label`, and `context.comment_id`
* are emitted as `gh-aw.trigger.item_type`, `gh-aw.trigger.item_number`, `gh-aw.trigger.label`,
* and `gh-aw.trigger.comment_id` attributes so every span can be linked back to the GitHub item
* (and specific comment) that triggered the workflow
*
* @param {SendJobSetupSpanOptions} [options]
* @returns {Promise<{ traceId: string, spanId: string }>} The trace and span IDs used.
Expand Down Expand Up @@ -574,6 +575,7 @@ async function sendJobSetupSpan(options = {}) {
const itemType = typeof awInfo.context?.item_type === "string" ? awInfo.context.item_type : "";
const itemNumber = typeof awInfo.context?.item_number === "string" ? awInfo.context.item_number : "";
const triggerLabel = typeof awInfo.context?.trigger_label === "string" ? awInfo.context.trigger_label : "";
const commentId = typeof awInfo.context?.comment_id === "string" ? awInfo.context.comment_id : "";

const traceId = optionsTraceId || inputTraceId || contextTraceId || generateTraceId();

Expand Down Expand Up @@ -633,6 +635,7 @@ async function sendJobSetupSpan(options = {}) {
if (itemType) attributes.push(buildAttr("gh-aw.trigger.item_type", itemType));
if (itemNumber) attributes.push(buildAttr("gh-aw.trigger.item_number", itemNumber));
if (triggerLabel) attributes.push(buildAttr("gh-aw.trigger.label", triggerLabel));
if (commentId) attributes.push(buildAttr("gh-aw.trigger.comment_id", commentId));

// Include experiment assignments so each span can be correlated with the
// A/B variant selected for this run (written by pick_experiment.cjs).
Expand Down Expand Up @@ -832,6 +835,7 @@ async function sendJobConclusionSpan(spanName, options = {}) {
const itemType = typeof awInfo.context?.item_type === "string" ? awInfo.context.item_type : "";
const itemNumber = typeof awInfo.context?.item_number === "string" ? awInfo.context.item_number : "";
const triggerLabel = typeof awInfo.context?.trigger_label === "string" ? awInfo.context.trigger_label : "";
const commentId = typeof awInfo.context?.comment_id === "string" ? awInfo.context.comment_id : "";
const jobName = process.env.INPUT_JOB_NAME || "";
const runId = process.env.GITHUB_RUN_ID || "";
const runAttempt = awInfo.run_attempt || process.env.GITHUB_RUN_ATTEMPT || "1";
Expand Down Expand Up @@ -893,6 +897,7 @@ async function sendJobConclusionSpan(spanName, options = {}) {
if (itemType) attributes.push(buildAttr("gh-aw.trigger.item_type", itemType));
if (itemNumber) attributes.push(buildAttr("gh-aw.trigger.item_number", itemNumber));
if (triggerLabel) attributes.push(buildAttr("gh-aw.trigger.label", triggerLabel));
if (commentId) attributes.push(buildAttr("gh-aw.trigger.comment_id", commentId));
if (!isNaN(effectiveTokens) && effectiveTokens > 0) {
attributes.push(buildAttr("gh-aw.effective_tokens", effectiveTokens));
}
Expand Down
44 changes: 44 additions & 0 deletions actions/setup/js/send_otlp_span.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,7 @@ describe("sendJobSetupSpan", () => {
expect(span.attributes).toContainEqual({ key: "gh-aw.trigger.item_number", value: { stringValue: "42" } });
const keys = span.attributes.map(a => a.key);
expect(keys).not.toContain("gh-aw.trigger.label");
expect(keys).not.toContain("gh-aw.trigger.comment_id");
});

it("emits gh-aw.trigger.label when trigger_label is non-empty", async () => {
Expand All @@ -1620,6 +1621,26 @@ describe("sendJobSetupSpan", () => {
expect(span.attributes).toContainEqual({ key: "gh-aw.trigger.label", value: { stringValue: "copilot" } });
});

it("emits gh-aw.trigger.comment_id when comment_id is non-empty", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://traces.example.com";

readFileSpy.mockImplementation(filePath => {
if (filePath === "/tmp/gh-aw/aw_info.json") {
return JSON.stringify({ context: { item_type: "pull_request", item_number: "99", trigger_label: "copilot", comment_id: "123456789" } });
}
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});

await sendJobSetupSpan();

const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const span = body.resourceSpans[0].scopeSpans[0].spans[0];
expect(span.attributes).toContainEqual({ key: "gh-aw.trigger.comment_id", value: { stringValue: "123456789" } });
});

it("omits trigger attributes when aw_info.json is absent", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);
Expand All @@ -1634,6 +1655,7 @@ describe("sendJobSetupSpan", () => {
expect(keys).not.toContain("gh-aw.trigger.item_type");
expect(keys).not.toContain("gh-aw.trigger.item_number");
expect(keys).not.toContain("gh-aw.trigger.label");
expect(keys).not.toContain("gh-aw.trigger.comment_id");
});
});

Expand Down Expand Up @@ -3484,6 +3506,7 @@ describe("sendJobConclusionSpan", () => {
expect(span.attributes).toContainEqual({ key: "gh-aw.trigger.item_number", value: { stringValue: "7" } });
const keys = span.attributes.map(a => a.key);
expect(keys).not.toContain("gh-aw.trigger.label");
expect(keys).not.toContain("gh-aw.trigger.comment_id");
});

it("emits gh-aw.trigger.label when trigger_label is non-empty", async () => {
Expand All @@ -3508,6 +3531,26 @@ describe("sendJobConclusionSpan", () => {
expect(span.attributes).toContainEqual({ key: "gh-aw.trigger.label", value: { stringValue: "bug" } });
});

it("emits gh-aw.trigger.comment_id when comment_id is non-empty", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://traces.example.com";

readFileSpy.mockImplementation(filePath => {
if (filePath === "/tmp/gh-aw/aw_info.json") {
return JSON.stringify({ context: { item_type: "pull_request", item_number: "456", trigger_label: "bug", comment_id: "987654321" } });
}
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});

await sendJobConclusionSpan("gh-aw.job.conclusion");

const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const span = body.resourceSpans[0].scopeSpans[0].spans[0];
expect(span.attributes).toContainEqual({ key: "gh-aw.trigger.comment_id", value: { stringValue: "987654321" } });
});

it("omits trigger attributes when aw_info.json is absent", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);
Expand All @@ -3522,6 +3565,7 @@ describe("sendJobConclusionSpan", () => {
expect(keys).not.toContain("gh-aw.trigger.item_type");
expect(keys).not.toContain("gh-aw.trigger.item_number");
expect(keys).not.toContain("gh-aw.trigger.label");
expect(keys).not.toContain("gh-aw.trigger.comment_id");
});
});

Expand Down
Loading