Set Up Traceway
Analyze the application first, agree on its Traceway project structure with the user, help them create that structure in the dashboard, and only then integrate and verify it.
Operating Contract
- Inspect before asking for credentials or changing code.
- Explain what was detected, what is already tracked, and how each production component should report to Traceway.
- Present a proposed project map and wait for the user to confirm it before implementation.
- There are two classes of credentials, treated differently:
- Setup tokens (prefix
tws_) are org-scoped, expire in 6 hours, and can only propose a setup plan; nothing is created until the user approves the plan on the Traceway website. They are designed to transit chat, and asking the user to paste one is expected.
- Project ingest tokens and upload tokens are long-lived. Never print their values, never ask the user to paste them into chat, and never commit them. When Step 3's apply script (or the
traceway CLI) writes an approved plan's ingest tokens into env files, read only the variable names and statuses from its output, never the values.
- Never print token values found in files or the environment. Report only that a value exists and the variable name that carries it.
- Never commit tokens or filled connection strings.
Invocation Forms
| Invocation carries |
Path |
| A project ingest token + url |
Fast Path for a single component, otherwise Steps 1-2, then integrate against the existing project |
A setup token (tws_) + url |
Steps 1-2, then Step 3 from "Write the plan file" (3b) |
| No token |
Steps 1-2, then Step 3 from the top |
Every backend integrates with OpenTelemetry. There is one backend path, not one per language or web framework: Go, Node, Python, PHP, Java, .NET, Ruby, and everything else export OTLP/HTTP to <instance>/api/otel/*. The Traceway project is created with framework OpenTelemetry, which is the only backend option in the dashboard's framework picker. The native Traceway Go SDK is a deliberate exception used only when the user explicitly asks for it.
Fast Path
When all of the following hold, skip Steps 2 and 3 and go straight to the integration steps:
- the repository has exactly one deployable component, and
- the user supplied (or the environment already carries) an instance URL and a project ingest token (not a
tws_ setup token, which exists to create projects), and
- the token's project already matches that component.
Still run Step 1, because it decides how to instrument. Collapse "Propose and Confirm the Project Map" to a single confirmation line ("This is a Go API; it reports to your existing <name> project over OpenTelemetry") and skip the project-creation step entirely. The full map-and-creation ceremony exists for repositories with more than one deployable component, or where no project exists yet. Do not make a user who handed you a token sit through a project-map review for a single service.
Step 1: Analyze the Architecture
Before changing anything, build a picture of what is deployed and what is already instrumented:
Frameworks and languages: detect them from package.json, go.mod, composer.json, requirements.txt/pyproject.toml, pubspec.yaml, build.gradle(.kts), Package.swift, *.xcodeproj/*.xcworkspace, Podfile, and source extensions. For iOS/Apple targets, note whether the sources are Swift or Objective-C. That choice picks the path in "Frontend and Mobile" (Swift gets the native SDK; Objective-C-only has none and falls back to OTel).
Deployable components and entry points: inventory APIs, SSR servers, browser apps, workers, schedulers, CLIs, and mobile applications. Treat code organization as evidence, not proof that something is deployed. Note the deployment target while you are here: Kubernetes manifests, a Helm chart, Kustomize overlays, Dockerfile plus a compose file, or cloud-init / Ansible / Terraform. It decides the host-metrics path in Step 8.
Existing observability: find OpenTelemetry configuration, Traceway SDKs, Sentry, Datadog, New Relic, Honeycomb, logging exporters, tracing middleware, source map or symbol upload steps, and observability environment-variable names. Explain whether Traceway will replace, coexist with, or extend each integration. Never display discovered credential values.
Production role of JS meta-frameworks (Next.js, SvelteKit, Remix, Nuxt): never assume full-stack.
- Frontend-only signals:
output: 'export' in next.config.* (static export, so there is no server in production); no API routes (app/api/**/route.*, pages/api/*) or only trivial ones; no 'use server' server actions; rewrites/proxy config or a NEXT_PUBLIC_API_URL-style env var pointing the browser at an external API; a separate backend service in this repo, another repo, or another language; static hosting in the deploy config (S3/CloudFront, GitHub Pages, nginx serving out/).
- Server signals: API routes with real logic,
'use server' server actions, database clients imported in server code, SSR reading its own data layer, next start or standalone output in the deploy config.
- Mixed or unclear: ask how the application is deployed and whether the framework server does production work worth tracking.
Frontend-only means integrate ONLY the browser side with the frontend SDK; do NOT add OTel to, or otherwise instrument, the framework's server side. A separate backend is its own component with its own Traceway project.
Background work: find cron jobs, queue consumers, schedulers, CLI commands, and long-running workers. Record whether libraries already emit CONSUMER spans.
AI/LLM usage: check dependencies for openai, @anthropic-ai/sdk, anthropic, langchain / @langchain/*, ai (Vercel AI SDK), litellm, google-generativeai, cohere, openrouter, and agent frameworks (langgraph, crewai, autogen / ag2, pydantic-ai, @openai/agents, @mastra/*, llamaindex / llama-index, semantic-kernel). Then decide whether this is a conversational product (chatbot, assistant, agent) rather than one-shot LLM calls: look for chat or streaming-chat routes (/chat, /completions, SSE or websocket handlers feeding an LLM), message-history persistence (a messages/conversations table or thread ids), tool/function-calling definitions passed to the model, or any agent framework above. If conversational or tool-calling, read ai-agent.md in this skill directory before proposing the AI integration.
Browser build and release flow: identify the bundler, output directory, source-map settings, deploy pipeline, and existing artifact uploads.
Mobile build and release flow: determine whether Flutter is obfuscated, Android uses R8/minification, iOS produces dSYMs, and whether multiple platform directories are one cross-platform product or independently released apps.
Deployment and host metrics: inspect Dockerfiles, Compose, Kubernetes, Helm, Terraform, Ansible, cloud-init, PaaS configs, and CI/CD. Determine whether the backend runs on a host where the Traceway OTel Agent is applicable.
Present the findings before asking questions:
| Component |
Repository evidence |
Production role |
Current tracking |
Proposed Traceway project |
Integration |
Credentials needed |
apps/api |
Go module + Gin routes |
Backend API |
OTel / none / vendor |
Product Backend |
OTel |
Runtime token |
apps/web |
Svelte + Vite |
Browser app |
Existing SDK / none |
Product Web |
Traceway Svelte SDK |
Runtime + upload token |
Use actual paths and findings. Include unresolved deployment assumptions explicitly.
Step 2: Propose and Confirm the Project Map
Explain these default boundaries:
| Application boundary |
Traceway project |
What belongs in it |
| Backend system |
One project with framework OpenTelemetry |
API endpoints, child spans, issues, background tasks, AI traces, logs, application/runtime metrics, and host metrics. APIs, workers, schedulers, and the host agent share the project token; distinguish them with stable service.name values. |
| Browser frontend |
One separate project per deployed browser application, using React, Svelte, Vue.js, or jQuery |
Browser errors, web vitals, session replay, distributed-trace linkage, source maps, and browser release metadata. |
| Mobile app |
One separate project per independently released application, using Flutter, React Native, Android, or iOS |
Mobile errors/crashes, replay where supported, and build symbols or mappings. A single Flutter product targeting Android and iOS normally uses one project; separate native apps use separate projects. |
| Full-stack JS app |
Two projects when its server runs in production |
Server-side work goes to the OpenTelemetry backend project; browser-side work goes to the browser project. |
The dashboard's framework picker offers exactly these nine options: OpenTelemetry for every backend, React / Svelte / Vue.js / jQuery for browsers, and Flutter / React Native / Android / iOS for mobile. There is no Gin, Django, Laravel, Next.js, or Remix entry, and none is needed. A Next.js or Remix browser project selects React, and its server side selects OpenTelemetry like any other backend. The framework-specific setup for a backend lives on the project's Connection page, which asks for the language and web framework after the project exists.
Do not create one project per backend process by default. Keeping the API, workers, AI calls, and server metrics together preserves their operational context and distributed traces. Split backend projects only for a real product, ownership, access-control, compliance, or data-isolation boundary.
Ask the user to confirm only what the repository cannot establish safely:
- Which detected components are deployed and should be tracked?
- Traceway Cloud or self-hosted, and which organization should own the projects?
- Are the proposed names and project boundaries correct?
- Does the backend run on a VM/host, Kubernetes, or serverless/PaaS, and should host metrics be collected?
- Do mobile directories represent one cross-platform product or separate released apps?
Each confirmed row must also settle what Step 3's plan file needs:
- The framework value, one of the nine:
opentelemetry, react, svelte, vuejs, jquery, flutter, react-native, android, ios.
- The env wiring: which untracked env file holds the credential (
envFile) and under which name (envVar), following the credential-naming rules above (public prefixes like VITE_/PUBLIC_/NEXT_PUBLIC_ for browser values). envFormat is token when the code composes the connection string itself or the value feeds an OTLP Authorization header, and connectionString when the SDK init reads the full <token>@<instance>/api/report string straight from the variable.
- The deployment placement, from Step 1's deployment analysis: when the credential cannot be hardcoded into the repository because of how the component is deployed (Vercel/Fly/K8s/CI-injected env, secret managers), add a
deployment block with the platform name and the exact command or UI path, using the literal placeholder <token> for the value. The website substitutes the real value after approval; never put real tokens in the plan. A mobile project whose credential lives in build config usually gets a deployment block and no envFile.
Do not modify code until the user confirms this map.
Step 3: Create the Projects
The default path: submit the confirmed map as a setup plan, the user approves it visually on the Traceway website, and a short curl script writes the resulting ingest tokens into env files. Nothing needs to be installed beyond curl and jq. Existing correctly mapped Traceway projects may be reused (the plan matches projects by name, so listing an existing name reuses it instead of duplicating).
3a. Get a setup token (skip when the invocation carried one)
Branch on account state; ask only if the repository and conversation do not establish it:
- Has a Traceway account: "Open
<instance>/setup (Traceway Cloud: https://cloud.tracewayapp.com/setup). Log in if prompted; the page shows a setup token starting with tws_. Paste it here and keep the page open." The token expires in 6 hours; the page has a Generate New Token button.
- No account yet: "Open
<instance>/register (Traceway Cloud: https://cloud.tracewayapp.com/register). Step 1 creates your account and organization. On step 2, keep 'AI' selected; it shows a setup token starting with tws_. Paste it here and leave that page open: my proposal will appear there for your approval, and the projects show up live once you approve."
Do not have the user create projects in the web UI on this path, and do not proceed without the token.
3b. Write the plan file
Write traceway-setup-plan.json from the confirmed map, one entry per project:
{
"projects": [
{"name": "Product Backend", "framework": "opentelemetry",
"envFile": "backend/.env", "envVar": "TRACEWAY_BACKEND_TOKEN"},
{"name": "Product Web", "framework": "svelte",
"envFile": "frontend/.env", "envVar": "PUBLIC_TRACEWAY_WEB_CONNECTION_STRING",
"envFormat": "connectionString",
"deployment": {"platform": "Vercel",
"instructions": "vercel env add PUBLIC_TRACEWAY_WEB_CONNECTION_STRING production\n# paste: <token>"}}
]
}
Rules: framework is one of the nine values from Step 2; every envFile must be untracked (create it and confirm it is gitignored first); envVar follows the credential-naming rules; deployment.instructions uses the literal <token> placeholder, never a real value.
3c. Submit the plan and wait for approval (curl)
Use curl and jq directly for this step. Do NOT install the traceway CLI for it; nothing gets installed on this path. (Only if a CLI new enough to have setup apply is already present is printf '%s' "$TRACEWAY_SETUP_TOKEN" | traceway setup apply --url "$TRACEWAY_URL" --plan traceway-setup-plan.json --token-stdin an equivalent one-step alternative.) If jq is unavailable and cannot be installed, use the manual fallback (3e).
Export the credentials first; setup tokens may transit chat, so this is fine:
export TRACEWAY_URL="<instance>" # no trailing slash
export TRACEWAY_SETUP_TOKEN="tws_..."
Validate the token and show the organization (this response carries project names only, safe to display):
curl -s -w '\nHTTP %{http_code}\n' -H "Authorization: Bearer $TRACEWAY_SETUP_TOKEN" \
"$TRACEWAY_URL/api/setup/session"
Submit the plan as a draft:
curl -s -w '\nHTTP %{http_code}\n' -X PUT \
-H "Authorization: Bearer $TRACEWAY_SETUP_TOKEN" -H 'Content-Type: application/json' \
--data-binary @traceway-setup-plan.json "$TRACEWAY_URL/api/setup/plan"
200 {"status":"pending"} means the draft is up; tell the user: "Review the proposal on the Traceway page you have open and press Approve Setup." A 422 is a validation message: fix the plan and resubmit. A 401 means the token is invalid or expired: ask the user for a fresh one from <instance>/setup.
Then wait for the decision with the script below. The script exists because the decision response carries the ingest tokens: values must flow API to env file without ever reaching stdout, your context, or the chat. Write it to traceway-setup-wait.sh exactly as given and run sh traceway-setup-wait.sh. Never fetch GET /api/setup/plan directly, and never read the env files it writes.
#!/bin/sh
# Waits for the setup decision, then writes each ingest token into the env
# file named by the plan. Prints statuses and variable names only.
set -eu
PLAN=${1:-traceway-setup-plan.json}
AUTH="Authorization: Bearer ${TRACEWAY_SETUP_TOKEN:?}"
URL=${TRACEWAY_URL:?}
deadline=$(($(date +%s) + 1800))
while :; do
resp=$(curl -s -H "$AUTH" "$URL/api/setup/plan")
status=$(printf '%s' "$resp" | jq -r '.status // "error"')
case "$status" in
approved) break ;;
rejected)
echo "rejected: $(printf '%s' "$resp" | jq -r '.reason // "(no reason given)"')"
exit 2 ;;
pending|none) ;;
*) echo "unexpected response, the setup token may have expired"; exit 1 ;;
esac
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "timed out waiting for approval; rerun when the user is ready"
exit 3
fi
sleep 3
done
i=0
n=$(jq '.projects | length' "$PLAN")
while [ "$i" -lt "$n" ]; do
name=$(jq -r ".projects[$i].name" "$PLAN")
envfile=$(jq -r ".projects[$i].envFile // empty" "$PLAN")
envvar=$(jq -r ".projects[$i].envVar // empty" "$PLAN")
format=$(jq -r ".projects[$i].envFormat // \"token\"" "$PLAN")
pstatus=$(printf '%s' "$resp" | jq -r --arg n "$name" \
'[.projects[] | select(.name == $n)][0].status // "missing"')
if [ "$pstatus" = missing ]; then
echo "$name: missing from the approved plan"
i=$((i + 1)); continue
fi
if [ -n "$envfile" ] && [ -n "$envvar" ]; then
value=$(printf '%s' "$resp" | jq -r --arg n "$name" --arg f "$format" \
'[.projects[] | select(.name == $n)][0]
| if $f == "connectionString"
then .token + "@" + (.backendUrl | sub("/+$"; "")) + "/api/report"
else .token end')
dir=$(dirname "$envfile"); mkdir -p "$dir"
tmp=$(mktemp "$dir/.tmp-env-XXXXXX")
{ [ -f "$envfile" ] && grep -v "^${envvar}=" "$envfile" > "$tmp"; } || true
printf '%s=%s\n' "$envvar" "$value" >> "$tmp"
mv "$tmp" "$envfile"
echo "$name: $pstatus, wrote $envvar to $envfile"
else
echo "$name: $pstatus"
fi
i=$((i + 1))
done
echo "done: deployment credentials, if any, are shown on the Traceway page the user has open"
Env files come out mode 0600 via mktemp. Failure handling:
- Rejected (exit 2): the script prints the user's rejection reason. Revise the map with the user, update the plan file, submit again (a new PUT replaces the pending draft), and rerun the wait script. Already-approved projects are matched by name, never duplicated.
- Expired or invalid token (401 / exit 1): ask the user for a fresh token from
<instance>/setup, re-export it, resubmit, rerun.
- Timeout (exit 3): resubmit and rerun when the user is ready; it is safe.
Delete the plan file and traceway-setup-wait.sh after a successful apply; they hold no secrets but they are setup litter.
3d. Confirm and handle deployment credentials
On approval the Traceway page celebrates and lands the user on the new project's dashboard, which doubles as the confirmation that the projects exist. For every project with a deployment block, the page first shows a Next Steps panel with the exact command and the real credential value; walk the user through completing it there before they continue, because the setup page is not reachable again once the account has projects. Never relay those values through chat; if a value is needed later, each project's Connection page in the dashboard carries its token.
3e. Manual fallback
When the user prefers clicking, curl or jq is unavailable, or apply keeps failing, follow dashboard-project-setup.md: the user creates each project in the dashboard UI and puts the tokens straight into their env files or secret store. On this path the old rule applies in full: ingest tokens never transit chat.
Upload tokens stay manual on every path: the uploaders read fixed variable names. traceway-sourcemaps reads TRACEWAY_SOURCEMAP_TOKEN, and dart run traceway:upload_symbols and the iOS dSYM script read TRACEWAY_UPLOAD_TOKEN (all three also read TRACEWAY_URL). Either name the CI secret exactly that, or keep a component-specific secret and pass it explicitly:
traceway-sourcemaps --url "$TRACEWAY_URL" --token "$TRACEWAY_WEB_UPLOAD_TOKEN" --directory ./dist
Inventing a name like TRACEWAY_WEB_UPLOAD_TOKEN and then calling the uploader with no --token fails at release time, not at setup time, so make the choice explicit in the CI step. Upload tokens are generated per project from Connection -> Source Maps or Symbol Upload in the dashboard.
Integration Paths
Pick the path by project type, and pick the project type from the "Analyze the Architecture" findings, never from the framework name alone (a Next.js repo can be a full-stack app or just the frontend of a separate backend). Per path, this is not negotiable per framework; it is how Traceway is designed to receive data:
| Project type |
Path |
| Backend (any language) |
OpenTelemetry, exporting OTLP/HTTP to <instance>/api/otel/v1/*. Always, including Go. |
| Frontend (browser SPA, or a JS meta-framework running frontend-only in production) |
Traceway @tracewayapp/<framework> SDK + bundler plugin + source map upload (see "Frontend and Mobile" below). |
| Full-stack JS (Next.js, SvelteKit, Remix, actually serving its API/SSR in production per "Analyze the Architecture") |
BOTH sides, each under its own Traceway project: server side via OpenTelemetry AND browser side via the frontend SDK. |
| Mobile (Flutter, React Native, Android, native Swift iOS) |
The Traceway platform SDK. Never OTel. Sole exception: a non-Swift iOS/Apple app has no native SDK, so it uses an OTel library (e.g. Honeycomb) exporting to Traceway like a backend (see "Frontend and Mobile"). |
The rules every backend integration must satisfy, and the table of what each span becomes, are in Step 4 where they are applied.
Step 4: Backend OTel Setup
The same shape in every language:
- Install the language's OpenTelemetry SDK plus the auto-instrumentation for the web framework and database clients.
- Point the OTLP/HTTP exporter at Traceway with the project token as a Bearer header.
- Set
service.name (becomes the Server Name in Traceway) and service.version (enables release comparison) on the resource.
- Verify endpoint grouping before anything else.
Three rules that are not negotiable
- Endpoints MUST arrive parametrized.
http.route must be the route pattern (/api/users/:id), never the concrete URL. Traceway uses the value as-is, but only when it starts with /: a route name like app_user_show is discarded exactly like a missing value. With no usable route it falls back to url.path, and the Endpoints page explodes into one row per unique URL.
- Background work MUST use
SpanKind.CONSUMER. A root span with the default INTERNAL kind and no HTTP attributes is silently dropped (exceptions recorded on it still reach Issues, but the task run itself is lost).
- The exporter MUST use an OpenTelemetry project's token. If OTLP arrives with a browser project's token (framework React, Svelte, Vue.js, jQuery, or React Native), Traceway keeps only the Issues and discards every endpoint, task, child span, and AI trace. The symptom is "errors appear but the Endpoints page is empty", and the cause is almost always two projects whose tokens got swapped.
How Traceway classifies spans
The rules are evaluated in this order and the first match wins.
| # |
Span |
Condition |
Becomes |
| 1 |
Any span |
SpanKind = INTERNAL and carries exception.* attributes |
Issue. Never promoted, even with HTTP or gen_ai.* attributes present. A child span keeps its ordinary Span row, a root span gets only the Issue |
| 2 |
Root span, or a span whose parent is not in the same export batch |
SpanKind = SERVER or INTERNAL with HTTP attributes |
Endpoint |
| 3 |
Any span |
SpanKind = CONSUMER |
Task |
| 4 |
Root span |
SpanKind = INTERNAL with a console.command attribute |
Task (CLI command) |
| 5 |
Any span |
Has any gen_ai.* attribute |
AI Trace |
| 6 |
Non-root span |
Has a parent span id |
Span (child) |
| 7 |
Root span |
Nothing above matched |
Dropped (exceptions recorded on it still become Issues, unlinked) |
Because the order is fixed, a CONSUMER span carrying gen_ai.* is a Task, and a SERVER request span carrying gen_ai.* is an Endpoint. Put the model call on its own child span (Step 6).
Two more rules that do not depend on span kind:
- An
exception event, or exception.* attributes, on any span produces an Issue, whatever the span itself became.
- An endpoint that returns 404 with no real route matched (
http.route missing, or a catch-all like /, /*) is renamed to the literal endpoint UNMATCHED, so bot scans and typo'd URLs collapse into one row. A concrete matched route returning 404 keeps its own name.
For the exact rules, endpoint naming, metric conversion, and the remaining quirks, read data-model.md in this skill directory. It is the authoritative reference.
Exporter configuration
Where the SDK supports the standard env vars, prefer them; they work identically across languages:
OTEL_SERVICE_NAME=my-service
OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3 # there is no OTEL_SERVICE_VERSION variable
OTEL_EXPORTER_OTLP_ENDPOINT=https://<instance>/api/otel
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # required, several SDKs default to gRPC
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <project-token>"
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
Two of those lines are load-bearing in a way that fails silently:
OTEL_EXPORTER_OTLP_PROTOCOL is not optional. Left unset, the Python SDK and the Java agent resolve otlp to gRPC, and Traceway has no gRPC listener. The app starts, serves traffic, exits 0, prints no warning, and every export is lost. (PHP is the one exception to the value: it needs http/json.)
- There is no
OTEL_SERVICE_VERSION. service.version only reaches Traceway through OTEL_RESOURCE_ATTRIBUTES. Without it, every endpoint row comes back with an empty App Version and release comparison does nothing.
SDKs append /v1/traces, /v1/metrics, /v1/logs to the endpoint automatically, so set the base URL only. A full signal path in OTEL_EXPORTER_OTLP_ENDPOINT produces /v1/traces/v1/traces, and the signal-specific variables (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) are used verbatim with nothing appended. When configuring in code instead, the full URLs are https://<instance>/api/otel/v1/traces (and /v1/metrics, /v1/logs) with header Authorization: Bearer <project-token>.
When Step 3's apply script wrote the env files, the plan's variables already hold real values locally, so wire the exporter to exactly those names (e.g. Authorization=Bearer ${TRACEWAY_BACKEND_TOKEN}) and run live verification immediately. The script writes only each project's one plan variable: companion variables the integration reads, like TRACEWAY_URL for the instance URL, are yours to add to the same env file now. On the manual fallback the user populates the variables first.
Constraints: OTLP/HTTP only (protobuf or JSON). OTLP/gRPC is not supported, there is no listener on port 4317. Content-Encoding: gzip is fine. The body is capped at 10 MB after decompression, and an oversized batch is rejected outright with 413 and {"error":"request body exceeds the 10MB limit"}. Nothing is ingested partially. A wrong or missing token answers 401, and any other path answers 404. All three are invisible from the application: most SDKs log exporter failures at debug level only, so when nothing arrives, turn the SDK's own diagnostic logging on first.
Node.js example
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node \
@opentelemetry/instrumentation @opentelemetry/api @opentelemetry/sdk-metrics \
@opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http
Declare @opentelemetry/api yourself even though it also arrives transitively. Relying on hoisting breaks under pnpm's strict node_modules and under Yarn PnP.
Create instrumentation.mjs at the project root and load it before the app: node --import ./instrumentation.mjs server.js. Use the .mjs extension. A .js file holding import syntax fails to load in a CommonJS project, which is what most projects still are.
import { register } from "node:module";
register("@opentelemetry/instrumentation/hook.mjs", import.meta.url);
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
const url = process.env.TRACEWAY_URL;
const headers = { Authorization: `Bearer ${process.env.TRACEWAY_BACKEND_TOKEN}` };
const sdk = new NodeSDK({
serviceName: process.env.OTEL_SERVICE_NAME ?? "my-service",
traceExporter: new OTLPTraceExporter({ url: `${url}/api/otel/v1/traces`, headers }),
metricReaders: [
new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: `${url}/api/otel/v1/metrics`, headers }),
exportIntervalMillis: 30_000,
}),
],
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Three parts of that snippet are load-bearing:
- The two
register lines install the ESM loader hook. Without them an ESM app ("type": "module", import express from "express") still gets HTTP spans but no http.route, so every URL becomes its own endpoint row. A CommonJS app (require("express")) is patched without the hook, and the lines are harmless there, so keep them either way.
serviceName becomes the Server Name in Traceway. Leave it out and every span reports unknown_service:node. For release comparison also set OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3.
metricReaders is a list. The older singular metricReader option is deprecated.
Auto-instrumentation covers Express routes (sets http.route), status codes, errors, and database clients (pg, mysql2, mongodb, ioredis). SQLite and custom business logic need manual tracer.startActiveSpan() child spans.
Next.js server exception
Next.js must not use the generic instrumentation.mjs preload above, node --import, or NODE_OPTIONS. Next.js creates its own incoming request root span with the matched http.route; preloading the generic Node HTTP instrumentation creates a competing root named from the literal URL, so /api/users/1 and /api/users/2 become separate endpoints.
For a Next.js server, create a minimal instrumentation.ts hook and put the SDK in a separate instrumentation.node.ts. Next.js compiles the hook for both Node and Edge; importing OTel packages directly in the hook, even with dynamic import() calls after a runtime guard, can make the development Edge compilation resolve Node built-ins and fail. The wrapper must conditionally import only the Node module:
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./instrumentation.node");
}
}
Keep every Node-only import and the SDK startup in the sibling module:
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { NodeSDK } from "@opentelemetry/sdk-node";
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = undefined;
const instance = process.env.TRACEWAY_URL!.replace(/\/+$/, "");
const base = `${instance}/api/otel`;
const headers = { Authorization: `Bearer ${process.env.TRACEWAY_BACKEND_TOKEN}` };
new NodeSDK({
resource: resourceFromAttributes({
"service.name": process.env.OTEL_SERVICE_NAME ?? "nextjs-server",
"service.version": process.env.APP_VERSION ?? "development",
}),
traceExporter: new OTLPTraceExporter({ url: `${base}/v1/traces`, headers }),
metricReaders: [
new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: `${base}/v1/metrics`, headers }),
exportIntervalMillis: 30_000,
}),
],
logRecordProcessors: [
new BatchLogRecordProcessor({
exporter: new OTLPLogExporter({ url: `${base}/v1/logs`, headers }),
}),
],
instrumentations: [getNodeAutoInstrumentations()],
}).start();
Install the log exporter and SDK packages in addition to the generic Node dependencies: @opentelemetry/exporter-logs-otlp-http, @opentelemetry/sdk-logs, @opentelemetry/api-logs, and @opentelemetry/resources. Next.js versions before 15 also need experimental.instrumentationHook: true. Full setup and verification: https://docs.tracewayapp.com/client/otel/nextjs
Resetting Error.prepareStackTrace in instrumentation.node.ts lets Node apply source maps instead of leaving server Issues pointed at minified Next chunks. Production must also start with NODE_OPTIONS=--enable-source-maps, and the build must emit .next/server/**/*.js.map; verify both rather than assuming readable server stacks. If the webpack build omits them, add experimental: { serverSourceMaps: true } to next.config. Also list @opentelemetry/auto-instrumentations-node in serverExternalPackages so Next does not bundle the instrumentation registry or warn about its optional transports.
Per-language notes
Go: use the framework's OTel middleware. go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin, github.com/riandyrn/otelchi (a community package, and pass otelchi.WithChiRoutes(r) or it reports raw URLs), github.com/gofiber/contrib/otelfiber/v2. All three set http.route from the matched route pattern. Exporter: otlptracehttp.WithEndpointURL(...) + WithHeaders. Two Go-only traps:
Set the propagator. Go's global propagator is a no-op by default, so traceparent is never sent or read and cross-service traces never link. One line, right after otel.SetTracerProvider(tp):
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, propagation.Baggage{},
))
stdlib net/http does not report its route. otelhttp reads the route from r.Pattern, which ServeMux fills in only after it has matched, so the usual top-level otelhttp.NewHandler(mux, "server") records no http.route at all and every URL becomes its own endpoint row. Either wrap each registered handler, which puts the span start inside the mux:
mux.Handle("GET /api/users/{id}", otelhttp.NewHandler(usersHandler, "users"))
or keep the single top-level wrap and stamp the route yourself:
// imports: strings, go.opentelemetry.io/otel/trace,
// semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
func withRoute(mux *http.ServeMux) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, pattern := mux.Handler(r); pattern != "" {
if i := strings.IndexByte(pattern, '/'); i >= 0 {
route := pattern[i:] // drop the "GET " prefix, http.route must start with "/"
trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute(route))
}
}
mux.ServeHTTP(w, r)
})
}
handler := otelhttp.NewHandler(withRoute(mux), "server")
Python: pip install opentelemetry-distro opentelemetry-exporter-otlp, then opentelemetry-bootstrap -a install, then run the app under opentelemetry-instrument with the env vars above (opentelemetry-instrument uvicorn app:app, opentelemetry-instrument gunicorn wsgi:app, opentelemetry-instrument python worker.py). Starting the server without that prefix sends nothing. FastAPI and Flask instrumentation sets http.route correctly with zero application code, in the framework's own syntax (GET /users/{user_id}, GET /orders/<order_id>), and both frameworks already answer 500 on an unhandled exception, so the endpoint status and the Issue line up without extra work. Three Python-only gotchas:
Logs need two more variables. OTEL_LOGS_EXPORTER=otlp attaches the OTel handler to the root logger, whose level defaults to WARNING, so every logger.info(...) is filtered out before the bridge sees it and only WARN and above reach Traceway. Attaching the handler also replaces the root handler list, so the app's own records stop appearing on stdout. Add both:
OTEL_PYTHON_LOG_CORRELATION=true # restores console output, stamps otelTraceID/otelSpanID
OTEL_PYTHON_LOG_LEVEL=info # only read alongside the line above
OTEL_PYTHON_LOG_LEVEL on its own does nothing. Do not set OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true; on current versions the bridge is already on and that variable switches it to the SDK's deprecated handler.
Metrics take a minute. The SDK default for OTEL_METRIC_EXPORT_INTERVAL is 60000 ms, so a correct pipeline looks empty while you are checking it. Set OTEL_METRIC_EXPORT_INTERVAL=10000 during verification.
Django needs two extra things: export DJANGO_SETTINGS_MODULE or opentelemetry-instrument fails to start Django at all, and add the route middleware from the Django guide, because opentelemetry-instrumentation-django reports http.route as Django's own pattern (api/users/<int:user_id>/), which has no leading / and is therefore discarded.
Guides: https://docs.tracewayapp.com/client/otel/python and https://docs.tracewayapp.com/client/otel/django
PHP: Laravel via composer require keepsuit/laravel-opentelemetry open-telemetry/exporter-otlp php-http/guzzle7-adapter; Symfony via composer require traceway/opentelemetry-symfony open-telemetry/exporter-otlp php-http/guzzle7-adapter (the stock Symfony auto-instrumentation sets http.route to the route NAME, which Traceway discards; see data-model.md). PHP does not take OTEL_PHP_AUTOLOAD_ENABLED as a plain env var next to the block above. Laravel needs nothing extra, the keepsuit service provider starts the SDK. Symfony starts the SDK through open_telemetry.sdk.autoload_enabled: true in config/packages/open_telemetry.yaml, or through a real process environment variable set in php-fpm, the Dockerfile, or Apache. Never put OTEL_PHP_AUTOLOAD_ENABLED in .env: Dotenv reads it after Composer autoload, the bundle then skips starting the SDK, and every signal silently becomes a no-op. PHP also prefers OTEL_EXPORTER_OTLP_PROTOCOL=http/json, though http/protobuf works too and is only slower without ext-protobuf.
- Symfony workers: never instrument
messenger:consume. The bundle already excludes it. traces.console.excluded_commands defaults to ['messenger:consume', 'messenger:consume-messages'], and that key is a prototyped array node, so setting it in config/packages/open_telemetry.yaml REPLACES the default instead of adding to it. If you override it for any reason you MUST re-list both entries. A consumer runs in an endless loop, so its command span never ends, never exports, and swallows every child span for the life of the worker. Excluding the command does not silence the work inside it: the Messenger middleware still emits one CONSUMER span per message, and those are what become Tasks in Traceway. Check what actually resolved with `bin/con
…(truncated)
1---2name: traceway-setup3description: Analyze and instrument repositories for Traceway observability. Use when the user wants to plan, add, migrate, or verify Traceway or OpenTelemetry monitoring for backend, browser, full-stack, mobile or iOS, or AI-agent software, including project topology and user-approved setup-plan creation. Backends use OTLP/HTTP; browser frontends and independently released mobile clients use separate Traceway SDK projects. Accept either an existing project token and instance URL or an organization setup token (`tws_...`) and URL; with no token, analyze first and guide setup-token acquisition. Treat any `tws_` token as a setup token.4---56# Set Up Traceway78Analyze the application first, agree on its Traceway project structure with the user, help them create that structure in the dashboard, and only then integrate and verify it.910## Operating Contract1112- Inspect before asking for credentials or changing code.13- Explain what was detected, what is already tracked, and how each production component should report to Traceway.14- Present a proposed project map and wait for the user to confirm it before implementation.15- There are two classes of credentials, treated differently:16 - **Setup tokens** (prefix `tws_`) are org-scoped, expire in 6 hours, and can only propose a setup plan; nothing is created until the user approves the plan on the Traceway website. They are designed to transit chat, and asking the user to paste one is expected.17 - **Project ingest tokens and upload tokens** are long-lived. Never print their values, never ask the user to paste them into chat, and never commit them. When Step 3's apply script (or the `traceway` CLI) writes an approved plan's ingest tokens into env files, read only the variable names and statuses from its output, never the values.18- Never print token values found in files or the environment. Report only that a value exists and the variable name that carries it.19- Never commit tokens or filled connection strings.2021## Invocation Forms2223| Invocation carries | Path |24|---|---|25| A project ingest token + url | Fast Path for a single component, otherwise Steps 1-2, then integrate against the existing project |26| A setup token (`tws_`) + url | Steps 1-2, then Step 3 from "Write the plan file" (3b) |27| No token | Steps 1-2, then Step 3 from the top |2829**Every backend integrates with OpenTelemetry.** There is one backend path, not one per language or web framework: Go, Node, Python, PHP, Java, .NET, Ruby, and everything else export OTLP/HTTP to `<instance>/api/otel/*`. The Traceway project is created with framework **OpenTelemetry**, which is the only backend option in the dashboard's framework picker. The native Traceway Go SDK is a deliberate exception used only when the user explicitly asks for it.3031## Fast Path3233When all of the following hold, skip Steps 2 and 3 and go straight to the integration steps:3435- the repository has exactly one deployable component, and36- the user supplied (or the environment already carries) an instance URL and a project ingest token (not a `tws_` setup token, which exists to create projects), and37- the token's project already matches that component.3839Still run Step 1, because it decides *how* to instrument. Collapse "Propose and Confirm the Project Map" to a single confirmation line ("This is a Go API; it reports to your existing `<name>` project over OpenTelemetry") and skip the project-creation step entirely. The full map-and-creation ceremony exists for repositories with more than one deployable component, or where no project exists yet. Do not make a user who handed you a token sit through a project-map review for a single service.4041## Step 1: Analyze the Architecture4243Before changing anything, build a picture of what is deployed and what is already instrumented:44451. **Frameworks and languages**: detect them from `package.json`, `go.mod`, `composer.json`, `requirements.txt`/`pyproject.toml`, `pubspec.yaml`, `build.gradle`(`.kts`), `Package.swift`, `*.xcodeproj`/`*.xcworkspace`, `Podfile`, and source extensions. For iOS/Apple targets, note whether the sources are Swift or Objective-C. That choice picks the path in "Frontend and Mobile" (Swift gets the native SDK; Objective-C-only has none and falls back to OTel).462. **Deployable components and entry points**: inventory APIs, SSR servers, browser apps, workers, schedulers, CLIs, and mobile applications. Treat code organization as evidence, not proof that something is deployed. Note the deployment target while you are here: Kubernetes manifests, a Helm chart, Kustomize overlays, `Dockerfile` plus a compose file, or cloud-init / Ansible / Terraform. It decides the host-metrics path in Step 8.473. **Existing observability**: find OpenTelemetry configuration, Traceway SDKs, Sentry, Datadog, New Relic, Honeycomb, logging exporters, tracing middleware, source map or symbol upload steps, and observability environment-variable names. Explain whether Traceway will replace, coexist with, or extend each integration. Never display discovered credential values.484. **Production role of JS meta-frameworks** (Next.js, SvelteKit, Remix, Nuxt): never assume full-stack.49 - **Frontend-only signals**: `output: 'export'` in `next.config.*` (static export, so there is no server in production); no API routes (`app/api/**/route.*`, `pages/api/*`) or only trivial ones; no `'use server'` server actions; `rewrites`/proxy config or a `NEXT_PUBLIC_API_URL`-style env var pointing the browser at an external API; a separate backend service in this repo, another repo, or another language; static hosting in the deploy config (S3/CloudFront, GitHub Pages, nginx serving `out/`).50 - **Server signals**: API routes with real logic, `'use server'` server actions, database clients imported in server code, SSR reading its own data layer, `next start` or standalone output in the deploy config.51 - **Mixed or unclear**: ask how the application is deployed and whether the framework server does production work worth tracking.5253 Frontend-only means integrate ONLY the browser side with the frontend SDK; do NOT add OTel to, or otherwise instrument, the framework's server side. A separate backend is its own component with its own Traceway project.545. **Background work**: find cron jobs, queue consumers, schedulers, CLI commands, and long-running workers. Record whether libraries already emit `CONSUMER` spans.556. **AI/LLM usage**: check dependencies for `openai`, `@anthropic-ai/sdk`, `anthropic`, `langchain` / `@langchain/*`, `ai` (Vercel AI SDK), `litellm`, `google-generativeai`, `cohere`, `openrouter`, and agent frameworks (`langgraph`, `crewai`, `autogen` / `ag2`, `pydantic-ai`, `@openai/agents`, `@mastra/*`, `llamaindex` / `llama-index`, `semantic-kernel`). Then decide whether this is a **conversational product** (chatbot, assistant, agent) rather than one-shot LLM calls: look for chat or streaming-chat routes (`/chat`, `/completions`, SSE or websocket handlers feeding an LLM), message-history persistence (a `messages`/`conversations` table or thread ids), tool/function-calling definitions passed to the model, or any agent framework above. If conversational or tool-calling, read `ai-agent.md` in this skill directory before proposing the AI integration.567. **Browser build and release flow**: identify the bundler, output directory, source-map settings, deploy pipeline, and existing artifact uploads.578. **Mobile build and release flow**: determine whether Flutter is obfuscated, Android uses R8/minification, iOS produces dSYMs, and whether multiple platform directories are one cross-platform product or independently released apps.589. **Deployment and host metrics**: inspect Dockerfiles, Compose, Kubernetes, Helm, Terraform, Ansible, cloud-init, PaaS configs, and CI/CD. Determine whether the backend runs on a host where the Traceway OTel Agent is applicable.5960Present the findings before asking questions:6162| Component | Repository evidence | Production role | Current tracking | Proposed Traceway project | Integration | Credentials needed |63|---|---|---|---|---|---|---|64| `apps/api` | Go module + Gin routes | Backend API | OTel / none / vendor | `Product Backend` | OTel | Runtime token |65| `apps/web` | Svelte + Vite | Browser app | Existing SDK / none | `Product Web` | Traceway Svelte SDK | Runtime + upload token |6667Use actual paths and findings. Include unresolved deployment assumptions explicitly.6869## Step 2: Propose and Confirm the Project Map7071Explain these default boundaries:7273| Application boundary | Traceway project | What belongs in it |74|---|---|---|75| **Backend system** | One project with framework **OpenTelemetry** | API endpoints, child spans, issues, background tasks, AI traces, logs, application/runtime metrics, and host metrics. APIs, workers, schedulers, and the host agent share the project token; distinguish them with stable `service.name` values. |76| **Browser frontend** | One separate project per deployed browser application, using **React**, **Svelte**, **Vue.js**, or **jQuery** | Browser errors, web vitals, session replay, distributed-trace linkage, source maps, and browser release metadata. |77| **Mobile app** | One separate project per independently released application, using **Flutter**, **React Native**, **Android**, or **iOS** | Mobile errors/crashes, replay where supported, and build symbols or mappings. A single Flutter product targeting Android and iOS normally uses one project; separate native apps use separate projects. |78| **Full-stack JS app** | Two projects when its server runs in production | Server-side work goes to the OpenTelemetry backend project; browser-side work goes to the browser project. |7980The dashboard's framework picker offers exactly these nine options: OpenTelemetry for every backend, React / Svelte / Vue.js / jQuery for browsers, and Flutter / React Native / Android / iOS for mobile. There is no Gin, Django, Laravel, Next.js, or Remix entry, and none is needed. A Next.js or Remix browser project selects **React**, and its server side selects **OpenTelemetry** like any other backend. The framework-specific setup for a backend lives on the project's Connection page, which asks for the language and web framework after the project exists.8182Do not create one project per backend process by default. Keeping the API, workers, AI calls, and server metrics together preserves their operational context and distributed traces. Split backend projects only for a real product, ownership, access-control, compliance, or data-isolation boundary.8384Ask the user to confirm only what the repository cannot establish safely:85861. Which detected components are deployed and should be tracked?872. Traceway Cloud or self-hosted, and which organization should own the projects?883. Are the proposed names and project boundaries correct?894. Does the backend run on a VM/host, Kubernetes, or serverless/PaaS, and should host metrics be collected?905. Do mobile directories represent one cross-platform product or separate released apps?9192Each confirmed row must also settle what Step 3's plan file needs:9394- The **framework value**, one of the nine: `opentelemetry`, `react`, `svelte`, `vuejs`, `jquery`, `flutter`, `react-native`, `android`, `ios`.95- The **env wiring**: which untracked env file holds the credential (`envFile`) and under which name (`envVar`), following the credential-naming rules above (public prefixes like `VITE_`/`PUBLIC_`/`NEXT_PUBLIC_` for browser values). `envFormat` is `token` when the code composes the connection string itself or the value feeds an OTLP Authorization header, and `connectionString` when the SDK init reads the full `<token>@<instance>/api/report` string straight from the variable.96- The **deployment placement**, from Step 1's deployment analysis: when the credential cannot be hardcoded into the repository because of how the component is deployed (Vercel/Fly/K8s/CI-injected env, secret managers), add a `deployment` block with the platform name and the exact command or UI path, using the literal placeholder `<token>` for the value. The website substitutes the real value after approval; never put real tokens in the plan. A mobile project whose credential lives in build config usually gets a `deployment` block and no `envFile`.9798Do not modify code until the user confirms this map.99100## Step 3: Create the Projects101102The default path: submit the confirmed map as a setup plan, the user approves it visually on the Traceway website, and a short curl script writes the resulting ingest tokens into env files. Nothing needs to be installed beyond `curl` and `jq`. Existing correctly mapped Traceway projects may be reused (the plan matches projects by name, so listing an existing name reuses it instead of duplicating).103104### 3a. Get a setup token (skip when the invocation carried one)105106Branch on account state; ask only if the repository and conversation do not establish it:107108- **Has a Traceway account**: "Open `<instance>/setup` (Traceway Cloud: https://cloud.tracewayapp.com/setup). Log in if prompted; the page shows a setup token starting with `tws_`. Paste it here and keep the page open." The token expires in 6 hours; the page has a Generate New Token button.109- **No account yet**: "Open `<instance>/register` (Traceway Cloud: https://cloud.tracewayapp.com/register). Step 1 creates your account and organization. On step 2, keep 'AI' selected; it shows a setup token starting with `tws_`. Paste it here and leave that page open: my proposal will appear there for your approval, and the projects show up live once you approve."110111Do not have the user create projects in the web UI on this path, and do not proceed without the token.112113### 3b. Write the plan file114115Write `traceway-setup-plan.json` from the confirmed map, one entry per project:116117```json118{119 "projects": [120 {"name": "Product Backend", "framework": "opentelemetry",121 "envFile": "backend/.env", "envVar": "TRACEWAY_BACKEND_TOKEN"},122 {"name": "Product Web", "framework": "svelte",123 "envFile": "frontend/.env", "envVar": "PUBLIC_TRACEWAY_WEB_CONNECTION_STRING",124 "envFormat": "connectionString",125 "deployment": {"platform": "Vercel",126 "instructions": "vercel env add PUBLIC_TRACEWAY_WEB_CONNECTION_STRING production\n# paste: <token>"}}127 ]128}129```130131Rules: `framework` is one of the nine values from Step 2; every `envFile` must be untracked (create it and confirm it is gitignored first); `envVar` follows the credential-naming rules; `deployment.instructions` uses the literal `<token>` placeholder, never a real value.132133### 3c. Submit the plan and wait for approval (curl)134135Use `curl` and `jq` directly for this step. Do NOT install the `traceway` CLI for it; nothing gets installed on this path. (Only if a CLI new enough to have `setup apply` is already present is `printf '%s' "$TRACEWAY_SETUP_TOKEN" | traceway setup apply --url "$TRACEWAY_URL" --plan traceway-setup-plan.json --token-stdin` an equivalent one-step alternative.) If `jq` is unavailable and cannot be installed, use the manual fallback (3e).136137Export the credentials first; setup tokens may transit chat, so this is fine:138139```bash140export TRACEWAY_URL="<instance>" # no trailing slash141export TRACEWAY_SETUP_TOKEN="tws_..."142```143144Validate the token and show the organization (this response carries project names only, safe to display):145146```bash147curl -s -w '\nHTTP %{http_code}\n' -H "Authorization: Bearer $TRACEWAY_SETUP_TOKEN" \148 "$TRACEWAY_URL/api/setup/session"149```150151Submit the plan as a draft:152153```bash154curl -s -w '\nHTTP %{http_code}\n' -X PUT \155 -H "Authorization: Bearer $TRACEWAY_SETUP_TOKEN" -H 'Content-Type: application/json' \156 --data-binary @traceway-setup-plan.json "$TRACEWAY_URL/api/setup/plan"157```158159`200 {"status":"pending"}` means the draft is up; tell the user: "Review the proposal on the Traceway page you have open and press **Approve Setup**." A `422` is a validation message: fix the plan and resubmit. A `401` means the token is invalid or expired: ask the user for a fresh one from `<instance>/setup`.160161Then wait for the decision with the script below. The script exists because the decision response carries the ingest tokens: values must flow API to env file without ever reaching stdout, your context, or the chat. Write it to `traceway-setup-wait.sh` exactly as given and run `sh traceway-setup-wait.sh`. Never fetch `GET /api/setup/plan` directly, and never read the env files it writes.162163```sh164#!/bin/sh165# Waits for the setup decision, then writes each ingest token into the env166# file named by the plan. Prints statuses and variable names only.167set -eu168PLAN=${1:-traceway-setup-plan.json}169AUTH="Authorization: Bearer ${TRACEWAY_SETUP_TOKEN:?}"170URL=${TRACEWAY_URL:?}171deadline=$(($(date +%s) + 1800))172while :; do173 resp=$(curl -s -H "$AUTH" "$URL/api/setup/plan")174 status=$(printf '%s' "$resp" | jq -r '.status // "error"')175 case "$status" in176 approved) break ;;177 rejected)178 echo "rejected: $(printf '%s' "$resp" | jq -r '.reason // "(no reason given)"')"179 exit 2 ;;180 pending|none) ;;181 *) echo "unexpected response, the setup token may have expired"; exit 1 ;;182 esac183 if [ "$(date +%s)" -ge "$deadline" ]; then184 echo "timed out waiting for approval; rerun when the user is ready"185 exit 3186 fi187 sleep 3188done189i=0190n=$(jq '.projects | length' "$PLAN")191while [ "$i" -lt "$n" ]; do192 name=$(jq -r ".projects[$i].name" "$PLAN")193 envfile=$(jq -r ".projects[$i].envFile // empty" "$PLAN")194 envvar=$(jq -r ".projects[$i].envVar // empty" "$PLAN")195 format=$(jq -r ".projects[$i].envFormat // \"token\"" "$PLAN")196 pstatus=$(printf '%s' "$resp" | jq -r --arg n "$name" \197 '[.projects[] | select(.name == $n)][0].status // "missing"')198 if [ "$pstatus" = missing ]; then199 echo "$name: missing from the approved plan"200 i=$((i + 1)); continue201 fi202 if [ -n "$envfile" ] && [ -n "$envvar" ]; then203 value=$(printf '%s' "$resp" | jq -r --arg n "$name" --arg f "$format" \204 '[.projects[] | select(.name == $n)][0]205 | if $f == "connectionString"206 then .token + "@" + (.backendUrl | sub("/+$"; "")) + "/api/report"207 else .token end')208 dir=$(dirname "$envfile"); mkdir -p "$dir"209 tmp=$(mktemp "$dir/.tmp-env-XXXXXX")210 { [ -f "$envfile" ] && grep -v "^${envvar}=" "$envfile" > "$tmp"; } || true211 printf '%s=%s\n' "$envvar" "$value" >> "$tmp"212 mv "$tmp" "$envfile"213 echo "$name: $pstatus, wrote $envvar to $envfile"214 else215 echo "$name: $pstatus"216 fi217 i=$((i + 1))218done219echo "done: deployment credentials, if any, are shown on the Traceway page the user has open"220```221222Env files come out mode 0600 via mktemp. Failure handling:223224- **Rejected (exit 2)**: the script prints the user's rejection reason. Revise the map with the user, update the plan file, submit again (a new PUT replaces the pending draft), and rerun the wait script. Already-approved projects are matched by name, never duplicated.225- **Expired or invalid token (401 / exit 1)**: ask the user for a fresh token from `<instance>/setup`, re-export it, resubmit, rerun.226- **Timeout (exit 3)**: resubmit and rerun when the user is ready; it is safe.227228Delete the plan file and `traceway-setup-wait.sh` after a successful apply; they hold no secrets but they are setup litter.229230### 3d. Confirm and handle deployment credentials231232On approval the Traceway page celebrates and lands the user on the new project's dashboard, which doubles as the confirmation that the projects exist. For every project with a `deployment` block, the page first shows a **Next Steps** panel with the exact command and the real credential value; walk the user through completing it there before they continue, because the setup page is not reachable again once the account has projects. Never relay those values through chat; if a value is needed later, each project's Connection page in the dashboard carries its token.233234### 3e. Manual fallback235236When the user prefers clicking, `curl` or `jq` is unavailable, or apply keeps failing, follow `dashboard-project-setup.md`: the user creates each project in the dashboard UI and puts the tokens straight into their env files or secret store. On this path the old rule applies in full: ingest tokens never transit chat.237238**Upload tokens stay manual on every path: the uploaders read fixed variable names.** `traceway-sourcemaps` reads `TRACEWAY_SOURCEMAP_TOKEN`, and `dart run traceway:upload_symbols` and the iOS dSYM script read `TRACEWAY_UPLOAD_TOKEN` (all three also read `TRACEWAY_URL`). Either name the CI secret exactly that, or keep a component-specific secret and pass it explicitly:239240```bash241traceway-sourcemaps --url "$TRACEWAY_URL" --token "$TRACEWAY_WEB_UPLOAD_TOKEN" --directory ./dist242```243244Inventing a name like `TRACEWAY_WEB_UPLOAD_TOKEN` and then calling the uploader with no `--token` fails at release time, not at setup time, so make the choice explicit in the CI step. Upload tokens are generated per project from **Connection** -> **Source Maps** or **Symbol Upload** in the dashboard.245246## Integration Paths247248Pick the path by project type, and pick the project type from the "Analyze the Architecture" findings, never from the framework name alone (a Next.js repo can be a full-stack app or just the frontend of a separate backend). Per path, this is not negotiable per framework; it is how Traceway is designed to receive data:249250| Project type | Path |251|---|---|252| **Backend** (any language) | OpenTelemetry, exporting OTLP/HTTP to `<instance>/api/otel/v1/*`. Always, including Go. |253| **Frontend** (browser SPA, or a JS meta-framework running frontend-only in production) | Traceway `@tracewayapp/<framework>` SDK + bundler plugin + source map upload (see "Frontend and Mobile" below). |254| **Full-stack JS** (Next.js, SvelteKit, Remix, actually serving its API/SSR in production per "Analyze the Architecture") | BOTH sides, each under its own Traceway project: server side via OpenTelemetry AND browser side via the frontend SDK. |255| **Mobile** (Flutter, React Native, Android, native Swift iOS) | The Traceway platform SDK. Never OTel. Sole exception: a non-Swift iOS/Apple app has no native SDK, so it uses an OTel library (e.g. Honeycomb) exporting to Traceway like a backend (see "Frontend and Mobile"). |256257The rules every backend integration must satisfy, and the table of what each span becomes, are in Step 4 where they are applied.258259## Step 4: Backend OTel Setup260261The same shape in every language:2622631. Install the language's OpenTelemetry SDK plus the auto-instrumentation for the web framework and database clients.2642. Point the OTLP/HTTP exporter at Traceway with the project token as a Bearer header.2653. Set `service.name` (becomes the Server Name in Traceway) and `service.version` (enables release comparison) on the resource.2664. Verify endpoint grouping before anything else.267268### Three rules that are not negotiable2692701. **Endpoints MUST arrive parametrized.** `http.route` must be the route pattern (`/api/users/:id`), never the concrete URL. Traceway uses the value as-is, but only when it starts with `/`: a route *name* like `app_user_show` is discarded exactly like a missing value. With no usable route it falls back to `url.path`, and the Endpoints page explodes into one row per unique URL.2712. **Background work MUST use `SpanKind.CONSUMER`.** A root span with the default `INTERNAL` kind and no HTTP attributes is silently dropped (exceptions recorded on it still reach Issues, but the task run itself is lost).2723. **The exporter MUST use an OpenTelemetry project's token.** If OTLP arrives with a browser project's token (framework React, Svelte, Vue.js, jQuery, or React Native), Traceway keeps only the Issues and discards every endpoint, task, child span, and AI trace. The symptom is "errors appear but the Endpoints page is empty", and the cause is almost always two projects whose tokens got swapped.273274### How Traceway classifies spans275276The rules are evaluated in this order and the first match wins.277278| # | Span | Condition | Becomes |279|---|---|---|---|280| 1 | Any span | `SpanKind = INTERNAL` **and** carries `exception.*` attributes | **Issue.** Never promoted, even with HTTP or `gen_ai.*` attributes present. A child span keeps its ordinary Span row, a root span gets only the Issue |281| 2 | Root span, or a span whose parent is not in the same export batch | `SpanKind = SERVER` or `INTERNAL` with HTTP attributes | **Endpoint** |282| 3 | Any span | `SpanKind = CONSUMER` | **Task** |283| 4 | Root span | `SpanKind = INTERNAL` with a `console.command` attribute | **Task** (CLI command) |284| 5 | Any span | Has any `gen_ai.*` attribute | **AI Trace** |285| 6 | Non-root span | Has a parent span id | **Span** (child) |286| 7 | Root span | Nothing above matched | **Dropped** (exceptions recorded on it still become Issues, unlinked) |287288Because the order is fixed, a `CONSUMER` span carrying `gen_ai.*` is a Task, and a `SERVER` request span carrying `gen_ai.*` is an Endpoint. Put the model call on its own child span (Step 6).289290Two more rules that do not depend on span kind:291292- An `exception` event, or `exception.*` attributes, on any span produces an **Issue**, whatever the span itself became.293- An endpoint that returns **404** with no real route matched (`http.route` missing, or a catch-all like `/`, `/*`) is renamed to the literal endpoint `UNMATCHED`, so bot scans and typo'd URLs collapse into one row. A concrete matched route returning 404 keeps its own name.294295For the exact rules, endpoint naming, metric conversion, and the remaining quirks, read `data-model.md` in this skill directory. It is the authoritative reference.296297### Exporter configuration298299Where the SDK supports the standard env vars, prefer them; they work identically across languages:300301```bash302OTEL_SERVICE_NAME=my-service303OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3 # there is no OTEL_SERVICE_VERSION variable304OTEL_EXPORTER_OTLP_ENDPOINT=https://<instance>/api/otel305OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # required, several SDKs default to gRPC306OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <project-token>"307OTEL_TRACES_EXPORTER=otlp308OTEL_METRICS_EXPORTER=otlp309OTEL_LOGS_EXPORTER=otlp310```311312Two of those lines are load-bearing in a way that fails silently:313314- **`OTEL_EXPORTER_OTLP_PROTOCOL` is not optional.** Left unset, the Python SDK and the Java agent resolve `otlp` to **gRPC**, and Traceway has no gRPC listener. The app starts, serves traffic, exits 0, prints no warning, and every export is lost. (PHP is the one exception to the value: it needs `http/json`.)315- **There is no `OTEL_SERVICE_VERSION`.** `service.version` only reaches Traceway through `OTEL_RESOURCE_ATTRIBUTES`. Without it, every endpoint row comes back with an empty App Version and release comparison does nothing.316317SDKs append `/v1/traces`, `/v1/metrics`, `/v1/logs` to the endpoint automatically, so set the **base** URL only. A full signal path in `OTEL_EXPORTER_OTLP_ENDPOINT` produces `/v1/traces/v1/traces`, and the signal-specific variables (`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) are used verbatim with nothing appended. When configuring in code instead, the full URLs are `https://<instance>/api/otel/v1/traces` (and `/v1/metrics`, `/v1/logs`) with header `Authorization: Bearer <project-token>`.318319When Step 3's apply script wrote the env files, the plan's variables already hold real values locally, so wire the exporter to exactly those names (e.g. `Authorization=Bearer ${TRACEWAY_BACKEND_TOKEN}`) and run live verification immediately. The script writes only each project's one plan variable: companion variables the integration reads, like `TRACEWAY_URL` for the instance URL, are yours to add to the same env file now. On the manual fallback the user populates the variables first.320321Constraints: OTLP/HTTP only (protobuf or JSON). OTLP/gRPC is not supported, there is no listener on port 4317. `Content-Encoding: gzip` is fine. The body is capped at 10 MB after decompression, and an oversized batch is rejected outright with `413` and `{"error":"request body exceeds the 10MB limit"}`. Nothing is ingested partially. A wrong or missing token answers `401`, and any other path answers `404`. All three are invisible from the application: most SDKs log exporter failures at debug level only, so when nothing arrives, turn the SDK's own diagnostic logging on first.322323### Node.js example324325```bash326npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node \327 @opentelemetry/instrumentation @opentelemetry/api @opentelemetry/sdk-metrics \328 @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http329```330331Declare `@opentelemetry/api` yourself even though it also arrives transitively. Relying on hoisting breaks under pnpm's strict `node_modules` and under Yarn PnP.332333Create `instrumentation.mjs` at the project root and load it before the app: `node --import ./instrumentation.mjs server.js`. Use the `.mjs` extension. A `.js` file holding `import` syntax fails to load in a CommonJS project, which is what most projects still are.334335```javascript336import { register } from "node:module";337register("@opentelemetry/instrumentation/hook.mjs", import.meta.url);338339import { NodeSDK } from "@opentelemetry/sdk-node";340import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";341import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";342import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";343import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";344345const url = process.env.TRACEWAY_URL;346const headers = { Authorization: `Bearer ${process.env.TRACEWAY_BACKEND_TOKEN}` };347348const sdk = new NodeSDK({349 serviceName: process.env.OTEL_SERVICE_NAME ?? "my-service",350 traceExporter: new OTLPTraceExporter({ url: `${url}/api/otel/v1/traces`, headers }),351 metricReaders: [352 new PeriodicExportingMetricReader({353 exporter: new OTLPMetricExporter({ url: `${url}/api/otel/v1/metrics`, headers }),354 exportIntervalMillis: 30_000,355 }),356 ],357 instrumentations: [getNodeAutoInstrumentations()],358});359360sdk.start();361```362363Three parts of that snippet are load-bearing:364365- **The two `register` lines install the ESM loader hook.** Without them an ESM app (`"type": "module"`, `import express from "express"`) still gets HTTP spans but no `http.route`, so every URL becomes its own endpoint row. A CommonJS app (`require("express")`) is patched without the hook, and the lines are harmless there, so keep them either way.366- **`serviceName`** becomes the Server Name in Traceway. Leave it out and every span reports `unknown_service:node`. For release comparison also set `OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3`.367- **`metricReaders` is a list.** The older singular `metricReader` option is deprecated.368369Auto-instrumentation covers Express routes (sets `http.route`), status codes, errors, and database clients (`pg`, `mysql2`, `mongodb`, `ioredis`). SQLite and custom business logic need manual `tracer.startActiveSpan()` child spans.370371#### Next.js server exception372373Next.js must not use the generic `instrumentation.mjs` preload above, `node --import`, or `NODE_OPTIONS`. Next.js creates its own incoming request root span with the matched `http.route`; preloading the generic Node HTTP instrumentation creates a competing root named from the literal URL, so `/api/users/1` and `/api/users/2` become separate endpoints.374375For a Next.js server, create a minimal `instrumentation.ts` hook and put the SDK in a separate `instrumentation.node.ts`. Next.js compiles the hook for both Node and Edge; importing OTel packages directly in the hook, even with dynamic `import()` calls after a runtime guard, can make the development Edge compilation resolve Node built-ins and fail. The wrapper must conditionally import only the Node module:376377```typescript378export async function register() {379 if (process.env.NEXT_RUNTIME === "nodejs") {380 await import("./instrumentation.node");381 }382}383```384385Keep every Node-only import and the SDK startup in the sibling module:386387```typescript388import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";389import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";390import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";391import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";392import { resourceFromAttributes } from "@opentelemetry/resources";393import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";394import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";395import { NodeSDK } from "@opentelemetry/sdk-node";396397(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = undefined;398399const instance = process.env.TRACEWAY_URL!.replace(/\/+$/, "");400const base = `${instance}/api/otel`;401const headers = { Authorization: `Bearer ${process.env.TRACEWAY_BACKEND_TOKEN}` };402403new NodeSDK({404 resource: resourceFromAttributes({405 "service.name": process.env.OTEL_SERVICE_NAME ?? "nextjs-server",406 "service.version": process.env.APP_VERSION ?? "development",407 }),408 traceExporter: new OTLPTraceExporter({ url: `${base}/v1/traces`, headers }),409 metricReaders: [410 new PeriodicExportingMetricReader({411 exporter: new OTLPMetricExporter({ url: `${base}/v1/metrics`, headers }),412 exportIntervalMillis: 30_000,413 }),414 ],415 logRecordProcessors: [416 new BatchLogRecordProcessor({417 exporter: new OTLPLogExporter({ url: `${base}/v1/logs`, headers }),418 }),419 ],420 instrumentations: [getNodeAutoInstrumentations()],421}).start();422```423424Install the log exporter and SDK packages in addition to the generic Node dependencies: `@opentelemetry/exporter-logs-otlp-http`, `@opentelemetry/sdk-logs`, `@opentelemetry/api-logs`, and `@opentelemetry/resources`. Next.js versions before 15 also need `experimental.instrumentationHook: true`. Full setup and verification: https://docs.tracewayapp.com/client/otel/nextjs425426Resetting `Error.prepareStackTrace` in `instrumentation.node.ts` lets Node apply source maps instead of leaving server Issues pointed at minified Next chunks. Production must also start with `NODE_OPTIONS=--enable-source-maps`, and the build must emit `.next/server/**/*.js.map`; verify both rather than assuming readable server stacks. If the webpack build omits them, add `experimental: { serverSourceMaps: true }` to `next.config`. Also list `@opentelemetry/auto-instrumentations-node` in `serverExternalPackages` so Next does not bundle the instrumentation registry or warn about its optional transports.427428### Per-language notes429430- **Go**: use the framework's OTel middleware. `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin`, `github.com/riandyrn/otelchi` (a community package, and pass `otelchi.WithChiRoutes(r)` or it reports raw URLs), `github.com/gofiber/contrib/otelfiber/v2`. All three set `http.route` from the matched route pattern. Exporter: `otlptracehttp.WithEndpointURL(...)` + `WithHeaders`. Two Go-only traps:431 - **Set the propagator.** Go's global propagator is a no-op by default, so `traceparent` is never sent or read and cross-service traces never link. One line, right after `otel.SetTracerProvider(tp)`:432433 ```go434 otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(435 propagation.TraceContext{}, propagation.Baggage{},436 ))437 ```438439 - **stdlib `net/http` does not report its route.** `otelhttp` reads the route from `r.Pattern`, which `ServeMux` fills in only after it has matched, so the usual top-level `otelhttp.NewHandler(mux, "server")` records no `http.route` at all and every URL becomes its own endpoint row. Either wrap each registered handler, which puts the span start inside the mux:440441 ```go442 mux.Handle("GET /api/users/{id}", otelhttp.NewHandler(usersHandler, "users"))443 ```444445 or keep the single top-level wrap and stamp the route yourself:446447 ```go448 // imports: strings, go.opentelemetry.io/otel/trace,449 // semconv "go.opentelemetry.io/otel/semconv/v1.26.0"450 func withRoute(mux *http.ServeMux) http.Handler {451 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {452 if _, pattern := mux.Handler(r); pattern != "" {453 if i := strings.IndexByte(pattern, '/'); i >= 0 {454 route := pattern[i:] // drop the "GET " prefix, http.route must start with "/"455 trace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPRoute(route))456 }457 }458 mux.ServeHTTP(w, r)459 })460 }461462 handler := otelhttp.NewHandler(withRoute(mux), "server")463 ```464465- **Python**: `pip install opentelemetry-distro opentelemetry-exporter-otlp`, then `opentelemetry-bootstrap -a install`, then run the app under `opentelemetry-instrument` with the env vars above (`opentelemetry-instrument uvicorn app:app`, `opentelemetry-instrument gunicorn wsgi:app`, `opentelemetry-instrument python worker.py`). Starting the server without that prefix sends nothing. FastAPI and Flask instrumentation sets `http.route` correctly with zero application code, in the framework's own syntax (`GET /users/{user_id}`, `GET /orders/<order_id>`), and both frameworks already answer `500` on an unhandled exception, so the endpoint status and the Issue line up without extra work. Three Python-only gotchas:466 - **Logs need two more variables.** `OTEL_LOGS_EXPORTER=otlp` attaches the OTel handler to the **root** logger, whose level defaults to `WARNING`, so every `logger.info(...)` is filtered out before the bridge sees it and only WARN and above reach Traceway. Attaching the handler also replaces the root handler list, so the app's own records stop appearing on stdout. Add both:467468 ```bash469 OTEL_PYTHON_LOG_CORRELATION=true # restores console output, stamps otelTraceID/otelSpanID470 OTEL_PYTHON_LOG_LEVEL=info # only read alongside the line above471 ```472473 `OTEL_PYTHON_LOG_LEVEL` on its own does nothing. Do **not** set `OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true`; on current versions the bridge is already on and that variable switches it to the SDK's deprecated handler.474 - **Metrics take a minute.** The SDK default for `OTEL_METRIC_EXPORT_INTERVAL` is 60000 ms, so a correct pipeline looks empty while you are checking it. Set `OTEL_METRIC_EXPORT_INTERVAL=10000` during verification.475 - **Django needs two extra things**: export `DJANGO_SETTINGS_MODULE` or `opentelemetry-instrument` fails to start Django at all, and add the route middleware from the Django guide, because `opentelemetry-instrumentation-django` reports `http.route` as Django's own pattern (`api/users/<int:user_id>/`), which has no leading `/` and is therefore discarded.476477 Guides: https://docs.tracewayapp.com/client/otel/python and https://docs.tracewayapp.com/client/otel/django478- **PHP**: Laravel via `composer require keepsuit/laravel-opentelemetry open-telemetry/exporter-otlp php-http/guzzle7-adapter`; Symfony via `composer require traceway/opentelemetry-symfony open-telemetry/exporter-otlp php-http/guzzle7-adapter` (the stock Symfony auto-instrumentation sets `http.route` to the route NAME, which Traceway discards; see `data-model.md`). PHP does not take `OTEL_PHP_AUTOLOAD_ENABLED` as a plain env var next to the block above. Laravel needs nothing extra, the keepsuit service provider starts the SDK. Symfony starts the SDK through `open_telemetry.sdk.autoload_enabled: true` in `config/packages/open_telemetry.yaml`, or through a real process environment variable set in php-fpm, the Dockerfile, or Apache. Never put `OTEL_PHP_AUTOLOAD_ENABLED` in `.env`: Dotenv reads it after Composer autoload, the bundle then skips starting the SDK, and every signal silently becomes a no-op. PHP also prefers `OTEL_EXPORTER_OTLP_PROTOCOL=http/json`, though `http/protobuf` works too and is only slower without `ext-protobuf`.479 - **Symfony workers: never instrument `messenger:consume`.** The bundle already excludes it. `traces.console.excluded_commands` defaults to `['messenger:consume', 'messenger:consume-messages']`, and that key is a prototyped array node, so setting it in `config/packages/open_telemetry.yaml` REPLACES the default instead of adding to it. If you override it for any reason you MUST re-list both entries. A consumer runs in an endless loop, so its command span never ends, never exports, and swallows every child span for the life of the worker. Excluding the command does not silence the work inside it: the Messenger middleware still emits one CONSUMER span per message, and those are what become Tasks in Traceway. Check what actually resolved with `bin/con480481…(truncated)