Skip to content

Commit 259331d

Browse files
bm1549claude
andcommitted
test(core): add Cloudflare Workers (workerd) acceptance test
Adds a fixture Worker plus an automated test that boots real workerd via wrangler's unstable_dev, requires dd-trace via a relative import to this repo's build, calls tracer.init() inside fetch(), emits a flat span, and asserts the existing FakeAgent's /v1/traces route actually receives the OTLP payload (an HTTP 200 from the Worker alone doesn't prove the span left the isolate). This is the acceptance test for PRs #1-3, which fixed the module-load gaps that let dd-trace load under workerd at all. Also adds a dedicated CI job to run it, and pins wrangler as a devDependency so CI has the CLI/unstable_dev API available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8fa5ef6 commit 259331d

7 files changed

Lines changed: 717 additions & 4 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Cloudflare Workers
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [master]
7+
schedule:
8+
- cron: 0 4 * * *
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
workerd:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
id-token: write
20+
steps:
21+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
22+
- uses: ./.github/actions/node
23+
with:
24+
version: '22'
25+
- uses: ./.github/actions/install
26+
- run: npm run test:integration:cloudflare-workers
27+
- uses: ./.github/actions/upload-junit-artifacts
28+
if: "!cancelled()"
29+
with:
30+
id: ${{ github.job }}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
const fs = require('node:fs')
5+
const path = require('node:path')
6+
7+
const { unstable_dev: unstableDev } = require('wrangler')
8+
9+
const { FakeAgent } = require('../helpers')
10+
11+
const FIXTURES_DIR = path.join(__dirname, 'fixtures')
12+
const CONFIG_TEMPLATE_PATH = path.join(FIXTURES_DIR, 'wrangler.jsonc')
13+
const GENERATED_CONFIG_PATH = path.join(FIXTURES_DIR, 'wrangler.generated.json')
14+
const WORKER_PATH = path.join(FIXTURES_DIR, 'worker.mjs')
15+
16+
/**
17+
* Resolves once the FakeAgent receives an OTLP traces POST, or rejects on timeout.
18+
* The Worker's export is fire-and-forget from workerd's perspective (see worker.mjs),
19+
* so an HTTP 200 from `worker.fetch()` proves nothing by itself — only this event proves
20+
* the span actually left the isolate over OTLP.
21+
*
22+
* @param {import('../helpers/fake-agent')} agent
23+
* @param {number} timeout
24+
* @returns {Promise<{ headers: Record<string, string>, payload: object }>}
25+
*/
26+
function waitForOtlpTraces (agent, timeout) {
27+
return new Promise((resolve, reject) => {
28+
const timer = setTimeout(() => reject(new Error('Timeout waiting for OTLP traces')), timeout)
29+
agent.once('otlp-traces', (msg) => {
30+
clearTimeout(timer)
31+
resolve(msg)
32+
})
33+
})
34+
}
35+
36+
describe('Cloudflare Workers (workerd) acceptance test', function () {
37+
this.timeout(60_000)
38+
39+
let agent
40+
let worker
41+
42+
before(async () => {
43+
agent = await new FakeAgent().start()
44+
45+
// wrangler.jsonc's "vars" are static, but the FakeAgent's port is only known at
46+
// runtime, so template the endpoint into a generated config used just for this run.
47+
const template = fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf8')
48+
const endpoint = `http://127.0.0.1:${agent.port}/v1/traces`
49+
fs.writeFileSync(GENERATED_CONFIG_PATH, template.replaceAll('__OTLP_ENDPOINT__', endpoint))
50+
51+
worker = await unstableDev(WORKER_PATH, {
52+
config: GENERATED_CONFIG_PATH,
53+
experimental: { disableExperimentalWarning: true },
54+
})
55+
})
56+
57+
after(async () => {
58+
await worker?.stop()
59+
await agent?.stop()
60+
fs.rmSync(GENERATED_CONFIG_PATH, { force: true })
61+
})
62+
63+
it('loads, initializes, and exports a span over OTLP from inside real workerd', async () => {
64+
const tracesPromise = waitForOtlpTraces(agent, 15_000)
65+
66+
const response = await worker.fetch('/')
67+
assert.strictEqual(response.status, 200)
68+
69+
const { payload } = await tracesPromise
70+
71+
const resourceSpan = payload.resourceSpans[0]
72+
const serviceNameAttribute = resourceSpan.resource.attributes.find(
73+
(attribute) => attribute.key === 'service.name'
74+
)
75+
assert.deepStrictEqual(serviceNameAttribute.value, { stringValue: 'cf-workers-ci' })
76+
77+
const span = resourceSpan.scopeSpans[0].spans.find((candidate) => candidate.name === 'cf.worker.test')
78+
assert.ok(span, 'expected an OTLP span named "cf.worker.test"')
79+
})
80+
})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
wrangler.generated.json
2+
.wrangler/
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Relative import resolves to this repo's own build (not an npm-published
2+
// version), so the test exercises the local dd-trace loading under workerd.
3+
import tracer from '../../../index.js'
4+
5+
// workerd forbids most I/O (including dd-trace's init-time file reads and
6+
// logging/telemetry pipeline) outside of a request handler, so tracer.init()
7+
// cannot run at module scope here — it must run on first request, inside
8+
// fetch().
9+
let initialized = false
10+
11+
export default {
12+
async fetch (request, env, ctx) {
13+
if (!initialized) {
14+
tracer.init() // reads process.env, populated from wrangler.jsonc "vars"
15+
initialized = true
16+
}
17+
18+
// A flat span: scope.activate()/parenting is not supported in workerd yet.
19+
const span = tracer.startSpan('cf.worker.test')
20+
span.setTag('http.method', request.method)
21+
span.setTag('deploy.target', 'cloudflare-workers')
22+
span.finish()
23+
24+
// span.finish() fires the OTLP export as fire-and-forget async I/O (an
25+
// http.request() call), and dd-trace exposes no awaitable flush yet, so
26+
// hold the isolate open long enough for that request to leave. A localhost
27+
// POST clears in well under this margin; the generous value just guards
28+
// against CI load (the test resolves on receipt, not on this timer).
29+
ctx.waitUntil(new Promise((resolve) => setTimeout(resolve, 8000)))
30+
31+
return new Response('ok\n')
32+
},
33+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
// Template consumed by cloudflare-workers.spec.js: it copies this file and substitutes the
3+
// OTLP traces endpoint placeholder below with the FakeAgent's actual listening address before
4+
// passing the generated config to wrangler. Not used directly by `wrangler dev`.
5+
"name": "dd-trace-cloudflare-workers-test",
6+
"main": "./worker.mjs",
7+
// FinalizationRegistry/WeakRef are gated by compatibility_date in workerd (absent on older
8+
// dates, present on recent ones); dd-trace's opentracing/span.js constructs a
9+
// FinalizationRegistry at module load, so a recent date is required for this fixture to load.
10+
"compatibility_date": "2026-07-18",
11+
"compatibility_flags": ["nodejs_compat", "nodejs_compat_populate_process_env"],
12+
"vars": {
13+
"DD_TRACE_OTEL_ENABLED": "true",
14+
"OTEL_TRACES_EXPORTER": "otlp",
15+
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "__OTLP_ENDPOINT__",
16+
"OTEL_EXPORTER_OTLP_TRACES_HEADERS": "dd-api-key=fake",
17+
"DD_SERVICE": "cf-workers-ci",
18+
"DD_ENV": "ci"
19+
}
20+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"test:integration:appsec:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/appsec/*.spec.js\"",
7777
"test:integration:crashtracking": "mocha --timeout 60000 \"integration-tests/crashtracking/*.spec.js\"",
7878
"test:integration:bun": "mocha --timeout 60000 \"integration-tests/bun/*.spec.js\"",
79+
"test:integration:cloudflare-workers": "mocha --timeout 60000 \"integration-tests/cloudflare-workers/*.spec.js\"",
7980
"test:integration:cucumber": "mocha --timeout 60000 \"integration-tests/cucumber/*.spec.js\"",
8081
"test:integration:cucumber:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/cucumber/*.spec.js\"",
8182
"test:integration:cypress": "mocha --timeout 60000 \"integration-tests/cypress/${SPEC:-cypress-*}.spec.js\"",
@@ -242,6 +243,7 @@
242243
"typescript": "^6.0.3",
243244
"v8-to-istanbul": "^9.0.0",
244245
"workerpool": "^10.0.3",
246+
"wrangler": "^4.112.0",
245247
"yaml": "^2.9.0",
246248
"yarn-deduplicate": "^6.0.2"
247249
}

0 commit comments

Comments
 (0)