Skip to content

Instantly share code, notes, and snippets.

@eragon512
Last active June 22, 2026 14:24
Show Gist options
  • Select an option

  • Save eragon512/80c671db1a77127e59be3e2ec64d83d0 to your computer and use it in GitHub Desktop.

Select an option

Save eragon512/80c671db1a77127e59be3e2ec64d83d0 to your computer and use it in GitHub Desktop.
Reads .next/trace and exports as OTLP Traces
/**
* Post-build step: reads .next/trace and exports the
* build spans as OTLP traces.
*
* Run after `next build` completes:
* node scripts/export-build-traces.js
*
* Required env vars:
* OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
* OTEL_EXPORTER_OTLP_HEADERS (comma-separated k=v)
*
* Optional:
* TRACEPARENT - if set, spans are parented under it
*/
const { readFileSync } = require('node:fs');
const SERVICE_NAME = 'oodle-frontend-nextjs-build';
const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
if (!endpoint) {
console.error(
'[export-build-traces] ' + 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is required'
);
process.exit(1);
}
async function main() {
const {
BasicTracerProvider,
BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-node');
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { trace, SpanStatusCode, ROOT_CONTEXT } = require('@opentelemetry/api');
const { W3CTraceContextPropagator } = require('@opentelemetry/core');
const rawHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS || '';
const headers = Object.fromEntries(
rawHeaders
.split(',')
.filter(Boolean)
.map((h) => {
const idx = h.indexOf('=');
return [h.slice(0, idx).trim(), h.slice(idx + 1).trim()];
})
);
const resource = resourceFromAttributes({
'service.name': SERVICE_NAME,
'github.run_id': process.env.GITHUB_RUN_ID || '',
'github.sha': process.env.GITHUB_SHA || '',
});
const provider = new BasicTracerProvider({
resource,
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({
url: endpoint,
headers,
})
),
],
});
const tracer = provider.getTracer(SERVICE_NAME);
let parentCtx = ROOT_CONTEXT;
if (process.env.TRACEPARENT) {
const propagator = new W3CTraceContextPropagator();
parentCtx = propagator.extract(
ROOT_CONTEXT,
{ traceparent: process.env.TRACEPARENT },
{
get: (c, key) => c[key],
keys: (c) => Object.keys(c),
}
);
}
const traceFile = process.argv[2] || '.next/trace';
let traceEntries;
try {
const raw = readFileSync(traceFile, 'utf8');
traceEntries = raw
.trim()
.split('\n')
.filter(Boolean)
.flatMap((line) => {
const parsed = JSON.parse(line);
return Array.isArray(parsed) ? parsed : [parsed];
});
} catch (err) {
console.error(
`[export-build-traces] ` + `Cannot read ${traceFile}:`,
err.message
);
await provider.shutdown();
process.exit(1);
}
if (!traceEntries.length) {
console.warn('[export-build-traces] ' + 'No entries in trace file');
await provider.shutdown();
process.exit(0);
}
const byId = new Set(traceEntries.map((e) => e.id));
const roots = [];
const children = new Map();
for (const entry of traceEntries) {
if (!entry.parentId || !byId.has(entry.parentId)) {
roots.push(entry);
} else {
if (!children.has(entry.parentId)) {
children.set(entry.parentId, []);
}
children.get(entry.parentId).push(entry);
}
}
function createSpans(entries, parentContext) {
for (const entry of entries) {
const startMs = entry.startTime;
const endMs = entry.startTime + entry.duration / 1000;
const span = tracer.startSpan(
entry.name,
{
startTime: startMs,
attributes: entry.tags
? Object.fromEntries(
Object.entries(entry.tags).map(([k, v]) => [
`next.${k}`,
String(v),
])
)
: {},
},
parentContext
);
span.setStatus({
code: SpanStatusCode.OK,
});
span.end(endMs);
const kids = children.get(entry.id);
if (kids) {
createSpans(kids, trace.setSpan(parentContext, span));
}
}
}
const buildFailed =
process.env.BUILD_EXIT_CODE && process.env.BUILD_EXIT_CODE !== '0';
const earliest = Math.min(...traceEntries.map((e) => e.startTime));
const latest = Math.max(
...traceEntries.map((e) => e.startTime + e.duration / 1000)
);
const rootSpan = tracer.startSpan(
'next-build',
{ startTime: earliest },
parentCtx
);
const rootCtx = trace.setSpan(parentCtx, rootSpan);
createSpans(roots, rootCtx);
rootSpan.setStatus(
buildFailed
? {
code: SpanStatusCode.ERROR,
message: `next build exited ${process.env.BUILD_EXIT_CODE}`,
}
: { code: SpanStatusCode.OK }
);
rootSpan.end(latest);
await provider.forceFlush();
await provider.shutdown();
console.log(
`[export-build-traces] Exported ` +
`${traceEntries.length} spans to ${endpoint}`
);
}
main().catch((err) => {
console.error('[export-build-traces] Fatal:', err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment