📡 OTel Instrumentation Improvement: add deployment.environment to setup span resource attributes
Analysis Date: 2026-04-05
Priority: Medium
Effort: Small (< 2h)
Problem
The sendJobConclusionSpan function sets deployment.environment as a resource attribute (staging or production, derived from awInfo.staged), but sendJobSetupSpan does not — even though it already reads aw_info.json and has the information needed to compute it.
The gap is in actions/setup/js/send_otlp_span.cjs:
- Conclusion span sets
deployment.environment at line 638
- Setup span builds its resource attributes at lines 405–412 but never adds
deployment.environment
This means environment-based filtering in any OTel backend (Grafana, Honeycomb, Datadog, Sentry) silently omits all setup spans. A dashboard query like deployment.environment = "production" returns only conclusion spans, which makes it impossible to filter the beginning of any trace by environment.
Why This Matters (DevOps Perspective)
Every trace starts with the setup span — it establishes the trace ID, captures job metadata, and anchors the parent span for downstream conclusion spans. If setup spans lack deployment.environment, then:
- Dashboards that group or filter by environment show incomplete data; the setup half of every trace is invisible in environment-scoped views.
- Alert rules on
deployment.environment = "staging" will never fire on setup spans, creating a blind spot during the exact job-start window most likely to expose configuration errors.
- Backend attribute indexing (Honeycomb columns, Grafana label matchers, Datadog facets) treats the two span types in the same trace as being in different environments, which breaks cardinality and group-by aggregations.
- JSONL mirror artifacts (
/tmp/gh-aw/otel.jsonl) written for setup spans lack deployment.environment, reducing their utility for local debugging without a live collector.
This is also an internal consistency issue: two spans in the same trace carry different resource schemas, which can confuse trace viewers that expect resource attributes to be uniform across a service's spans.
Current Behavior
// Current: actions/setup/js/send_otlp_span.cjs (lines 363–412)
// sendJobSetupSpan reads awInfo but only uses otel_trace_id from it.
// staged is never extracted, so deployment.environment is never set.
const awInfo = readJSONIfExists("/tmp/gh-aw/aw_info.json") || {};
const rawContextTraceId = typeof awInfo.context?.otel_trace_id === "string"
? awInfo.context.otel_trace_id.trim().toLowerCase()
: "";
const contextTraceId = isValidTraceId(rawContextTraceId) ? rawContextTraceId : "";
// ... (lines 392–412)
const resourceAttributes = [
buildAttr("github.repository", repository),
buildAttr("github.run_id", runId),
];
if (repository && runId) {
const [owner, repo] = repository.split("/");
resourceAttributes.push(buildAttr("github.actions.run_url", buildWorkflowRunUrl({ runId }, { owner, repo })));
}
if (eventName) {
resourceAttributes.push(buildAttr("github.event_name", eventName));
}
// deployment.environment is never added here ← gap
By contrast, sendJobConclusionSpan correctly sets it (line 638):
resourceAttributes.push(buildAttr("deployment.environment", staged ? "staging" : "production"));
Proposed Change
// Proposed addition to actions/setup/js/send_otlp_span.cjs
// In sendJobSetupSpan, after the existing awInfo read at line 363:
const awInfo = readJSONIfExists("/tmp/gh-aw/aw_info.json") || {};
// ... existing otel_trace_id extraction unchanged ...
// Add this line alongside other metadata extraction (around line 385–391):
const staged = awInfo.staged === true;
// Then append to resourceAttributes (after line 411, mirroring the conclusion span):
resourceAttributes.push(buildAttr("deployment.environment", staged ? "staging" : "production"));
The staged flag is compile-time metadata written into aw_info.json before any job runs in a given workflow execution, so it is available when the setup span fires for downstream jobs (safe-outputs, conclusion). For the activation job itself, aw_info.json may not yet exist when setup fires — in that case awInfo is {}, awInfo.staged is undefined, and staged evaluates to false, producing "production" as the default. This matches the conclusion span's behavior under the same condition.
Expected Outcome
After this change:
- In Grafana / Honeycomb / Datadog: filtering by
deployment.environment = "staging" returns both setup and conclusion spans — complete end-to-end traces for staging runs
- In Sentry: environment-scoped issue grouping captures the full trace from job start to conclusion
- In the JSONL mirror: setup span entries in
/tmp/gh-aw/otel.jsonl include deployment.environment, making them useful for local debugging without a live collector
- For on-call engineers: traces no longer have a split resource schema; environment-based alert rules now fire on the span that starts the trace, not just the one that ends it
Implementation Steps
Evidence from Live Sentry Data
Note: No Sentry MCP tooling was available in this analysis run. The gap is confirmed entirely from static code analysis: deployment.environment is added to resourceAttributes in sendJobConclusionSpan (line 638) and absent from the equivalent block in sendJobSetupSpan (lines 405–412). The test file (send_otlp_span.test.cjs) has three deployment.environment test cases for sendJobConclusionSpan (lines 1633, 1651, 1676) and zero for sendJobSetupSpan, which independently confirms the gap.
Related Files
actions/setup/js/send_otlp_span.cjs — primary change site (sendJobSetupSpan, lines 363–428)
actions/setup/js/send_otlp_span.test.cjs — test additions needed (mirror lines 1620–1700)
actions/setup/js/action_setup_otlp.cjs — no change needed (calls sendJobSetupSpan unmodified)
actions/setup/js/action_conclusion_otlp.cjs — reference implementation (already correct)
Generated by the Daily OTel Instrumentation Advisor workflow
Generated by Daily OTel Instrumentation Advisor · ● 141.5K · ◷
📡 OTel Instrumentation Improvement: add
deployment.environmentto setup span resource attributesAnalysis Date: 2026-04-05
Priority: Medium
Effort: Small (< 2h)
Problem
The
sendJobConclusionSpanfunction setsdeployment.environmentas a resource attribute (staging or production, derived fromawInfo.staged), butsendJobSetupSpandoes not — even though it already readsaw_info.jsonand has the information needed to compute it.The gap is in
actions/setup/js/send_otlp_span.cjs:deployment.environmentat line 638deployment.environmentThis means environment-based filtering in any OTel backend (Grafana, Honeycomb, Datadog, Sentry) silently omits all setup spans. A dashboard query like
deployment.environment = "production"returns only conclusion spans, which makes it impossible to filter the beginning of any trace by environment.Why This Matters (DevOps Perspective)
Every trace starts with the setup span — it establishes the trace ID, captures job metadata, and anchors the parent span for downstream conclusion spans. If setup spans lack
deployment.environment, then:deployment.environment = "staging"will never fire on setup spans, creating a blind spot during the exact job-start window most likely to expose configuration errors./tmp/gh-aw/otel.jsonl) written for setup spans lackdeployment.environment, reducing their utility for local debugging without a live collector.This is also an internal consistency issue: two spans in the same trace carry different resource schemas, which can confuse trace viewers that expect resource attributes to be uniform across a service's spans.
Current Behavior
By contrast,
sendJobConclusionSpancorrectly sets it (line 638):Proposed Change
The
stagedflag is compile-time metadata written intoaw_info.jsonbefore any job runs in a given workflow execution, so it is available when the setup span fires for downstream jobs (safe-outputs, conclusion). For the activation job itself,aw_info.jsonmay not yet exist when setup fires — in that caseawInfois{},awInfo.stagedisundefined, andstagedevaluates tofalse, producing"production"as the default. This matches the conclusion span's behavior under the same condition.Expected Outcome
After this change:
deployment.environment = "staging"returns both setup and conclusion spans — complete end-to-end traces for staging runs/tmp/gh-aw/otel.jsonlincludedeployment.environment, making them useful for local debugging without a live collectorImplementation Steps
actions/setup/js/send_otlp_span.cjs, extractconst staged = awInfo.staged === true;insendJobSetupSpan(alongside the existingawInfousage)buildAttr("deployment.environment", staged ? "staging" : "production")to theresourceAttributesarray insendJobSetupSpanactions/setup/js/send_otlp_span.test.cjsunder thesendJobSetupSpandescribe block, mirroring the existingstaged / deployment.environmenttests forsendJobConclusionSpan(lines 1620–1700):deployment.environment=productionwhenaw_info.jsonis absentdeployment.environment=stagingwhenawInfo.staged=truedeployment.environment=productionwhenawInfo.staged=falsecd actions/setup/js && npx vitest runto confirm tests passmake fmtto ensure formattingEvidence from Live Sentry Data
Related Files
actions/setup/js/send_otlp_span.cjs— primary change site (sendJobSetupSpan, lines 363–428)actions/setup/js/send_otlp_span.test.cjs— test additions needed (mirror lines 1620–1700)actions/setup/js/action_setup_otlp.cjs— no change needed (callssendJobSetupSpanunmodified)actions/setup/js/action_conclusion_otlp.cjs— reference implementation (already correct)Generated by the Daily OTel Instrumentation Advisor workflow