A sample app that uses React v18 and node >= 24.13.0 to show an interactive UI and send telemetry and errors to datadog.
The rest of this doc was generated by an LLM.
bin/dev/start # installs deps + seeds .env if needed, then runs both serversVite serves the app on :5173 and the BFF listens on :8787. The script
resolves Node through mise so the pinned 24.20.0 is used regardless of
what is on PATH.
bin/dev/start --web # Vite only
bin/dev/start --bff # BFF only
bin/dev/start --agent # also `docker compose up -d` the Datadog agent first
bin/dev/stop # both servers and the agent container
bin/dev/stop --servers # leave the container running
bin/dev/stop --agent # only the container
bin/dev/test # typecheck, then vitest with coverage, then Playwright
bin/dev/test --unit # or --e2e / --types / --watchstop only touches processes whose working directory is this project,
so an unrelated program holding :5173 or :8787 is reported and left
alone. It is safe to run when nothing is up.
All three take --help. The underlying npm scripts (npm run dev,
npm test, …) still work if you prefer them.
Pick Mock in the UI and start checking items — captured DogStatsD
lines appear in the inspector at the bottom of the page. The selected
mode is remembered across reloads, so app.started is observable too.
Browsers can't open UDP sockets, so nc -u -l 8125 and dd-trace can
only work server-side. Telemetry therefore has two paths, and the radio
buttons pick which (if either) is used:
| Mode | Browser | BFF (:8787) |
Agent |
|---|---|---|---|
| Off | no-op transport | — | — |
| Mock | captured in memory, rendered as DogStatsD lines, mirrored to console.debug |
— | — |
| Agent | Datadog RUM (sessions + error stacks) | POST /api/telemetry → hot-shots |
DogStatsD UDP :8125 + APM :8126 |
src/telemetry/ holds the client and its transports; server/ holds
the BFF. Both render metrics under one ddog.* namespace so Mock output
and real agent traffic are directly comparable.
| Event | Fires when | Notable tags |
|---|---|---|
ddog.app.started |
app mounts | — |
ddog.tab.changed |
tab switches | from, to |
ddog.item.checked / .unchecked |
checkbox toggles | item_name, category, price, cart_total |
ddog.cart.limit_exceeded |
cart total passes $25 | cart_total, limit, item_count |
The over-$25 case throws a real CartLimitExceededError, which is
reported and then caught so the cart stays usable — un-check something
and the banner clears.
Mock mode is in-page, but the BFF's real UDP output can be sniffed directly:
npm run dd:sniff # nc -u -l 8125
npm run dev:bff # in another shell, then use Agent modeddog.item.checked:1|c|#service:ddog,env:development,category:Fruit,price:5
ddog.cart.limit_exceeded:1|c|#service:ddog,env:development,limit:25
ddog.errors:1|c|#service:ddog,env:development,error_type:CartLimitExceededError
DD_TRACE_DEBUG=true dumps dd-trace spans to the console — the
server-side equivalent of Mock mode.
Agent mode has two independent halves. They need different credentials, and one of them works today.
.env already holds the RUM application ID and client token, so RUM
ships straight from the browser to browser-intake-datadoghq.com. No
agent, no API key, and no Docker required for this half. Just:
npm run dev # pick "Agent" in the UIVerified against the live intake — a session sends custom actions and the error with full context:
[action] tab.changed ctx {"from":"Fruit","to":"Veggies","value":1}
[action] item.checked ctx {"item_name":"Cherries","price":"10","cart_total":"10"}
[error] Cart total $29 exceeds the $25 limit
ctx {"limit":"25","cart_total":"29","item_count":"3","metric":"cart.limit_exceeded"}
tagged service:ddog-web, version:0.1.0 (injected from
package.json), application.id:28da096a-…. The error appears in
Error Tracking; the actions in RUM → Explorer.
@datadog/browser-rum-react's reactPlugin({ router: false }) is
registered so errors are attributed to React components. router: false
because this app has no router.
Selecting Agent reveals a "View in Datadog" panel with deep links,
built from .env so they carry the right site and application id. It is
the counterpart to Mock mode's inspector: Mock shows what was captured
locally, these show where the data actually went.
| Link | Shows |
|---|---|
| RUM Explorer | @type:action — tab changes, checks, un-checks |
| Error Tracking | @error.type:CartLimitExceededError |
| Session Replay | recorded sessions, 20% sampled |
| Metrics | metric summary filtered to ddog. |
| APM Service | latency and errors for the ddog BFF |
| APM Traces | individual POST /api/telemetry spans |
src/telemetry/dd-links.ts maps the intake site to the matching UI host
— datadoghq.com/.eu take an app. prefix, while regional sites
(us3., us5., ap1.) are already UI hosts.
DD_API_KEY is set in .env, so the agent forwards ddog.* metrics
and APM traces to the account.
npm run dd:up # docker compose up -d — agent on :8125/udp and :8126
npm run dev # BFF reads .env via node --env-file-if-exists
bin/dev/stop # stop the servers and the agent when finishedVerified against the live agent. agent dogstatsd-stats after one
session:
ddog.tab.changed | env:development service:ddog from:Ice_Cream to:Fruit | 1
ddog.item.checked | env:development service:ddog item_name:Cherries price:10 cart_total:10 | 1
ddog.cart.limit_exceeded | env:development service:ddog limit:25 cart_total:29 item_count:3 | 1
ddog.errors | env:development service:ddog error_type:CartLimitExceededError | 1
The forwarder reported 18 series_v3 submissions with 0 dropped, 0
retried and 0 HTTP errors, and the trace agent received spans from
dd-trace for the POST /api/telemetry request.
DD_DOGSTATSD_METRICS_STATS_ENABLE is on in docker-compose.yml — that
is what makes docker exec ddog-agent agent dogstatsd-stats show the
per-metric table above, which is the fastest way to confirm a metric
actually arrived.
bin/dev/test # all three, in order; stops at the first failure
bin/dev/test --unit # 120 unit/component tests across 11 files
bin/dev/test --e2e # 6 Playwright specs
bin/dev/test --types # tsc --noEmit
bin/dev/test --watch # vitest in watch mode--e2e installs the Chromium build Playwright expects if it is missing,
so a fresh checkout works without a separate setup step.
Coverage is 99.7% of statements and 92.9% of branches, above the 90%
floor set in vite.config.ts. Everything was built red-green-refactor.
- Node is pinned to 24.20.0 via
mise.toml; the repo's system Node was 24.11.1, below the>= 24.13.0requirement inpackage.json'sengines. - The BFF runs
.tsdirectly on Node's native type stripping — no build step. - npm 11 gates install scripts;
esbuildand thedd-tracenative modules are allow-listed inpackage.jsonunderallowScripts.