From 38c1e8754a6a221f2c9bd93945da9048d9140d20 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Wed, 26 Nov 2025 11:19:30 -0500 Subject: [PATCH 1/6] changing how we store the req to reference in page load to consider child spans in the store --- packages/datadog-plugin-next/src/index.js | 32 ++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/datadog-plugin-next/src/index.js b/packages/datadog-plugin-next/src/index.js index d1e80f658df..9bf69557511 100644 --- a/packages/datadog-plugin-next/src/index.js +++ b/packages/datadog-plugin-next/src/index.js @@ -13,7 +13,7 @@ class NextPlugin extends ServerPlugin { constructor (...args) { super(...args) - this._requests = new WeakMap() + this._requestsBySpanId = new Map() this.addSub('apm:next:page:load', message => this.pageLoad(message)) } @@ -35,7 +35,10 @@ class NextPlugin extends ServerPlugin { analyticsSampler.sample(span, this.config.measured, true) - this._requests.set(span, req) + // Store request by span ID to handle cases where child spans are activated + // before pageLoad runs (especially when OpenTelemetry TracerProvider is enabled) + const spanId = span.context().toSpanId() + this._requestsBySpanId.set(spanId, req) return { ...store, span } } @@ -81,6 +84,11 @@ class NextPlugin extends ServerPlugin { this.config.hooks.request(span, req, res) span.finish() + + // Cleanup: Remove request from Map to prevent memory leaks + // since Map (unlike WeakMap) won't auto-garbage collect + const spanId = span.context().toSpanId() + this._requestsBySpanId.delete(spanId) } pageLoad ({ page, isAppPath = false, isStatic = false }) { @@ -89,7 +97,25 @@ class NextPlugin extends ServerPlugin { if (!store) return const span = store.span - const req = this._requests.get(span) + + const spanId = span.context().toSpanId() + + // Convert parent ID from hex to decimal to match storage format. + // IMPORTANT: toSpanId() returns decimal string (e.g., "457018059221062340") + // but _parentId.toString() defaults to hex (e.g., "0657a780e3cf52c4"). + // We must use toString(10) to get decimal format for Map lookup to work. + let parentSpanId = null + if (span.context()._parentId) { + parentSpanId = span.context()._parentId.toString(10) + } + + // Try current span first, then parent span. + // This handles cases where pageLoad runs in a child span context + // (e.g., when OpenTelemetry TracerProvider creates "resolve page components" span) + let req = this._requestsBySpanId.get(spanId) + if (!req && parentSpanId) { + req = this._requestsBySpanId.get(parentSpanId) + } // safeguard against missing req in complicated timeout scenarios if (!req) return From c539a1ba4b51c979b348f071d44fbb0bb4db4b1e Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 8 Dec 2025 13:38:20 -0500 Subject: [PATCH 2/6] adding tests to validate that new behavior of checking for span id works with child spans --- packages/datadog-plugin-next/src/index.js | 4 ---- .../datadog-plugin-next/test/index.spec.js | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/datadog-plugin-next/src/index.js b/packages/datadog-plugin-next/src/index.js index 9bf69557511..451aaa4712e 100644 --- a/packages/datadog-plugin-next/src/index.js +++ b/packages/datadog-plugin-next/src/index.js @@ -36,7 +36,6 @@ class NextPlugin extends ServerPlugin { analyticsSampler.sample(span, this.config.measured, true) // Store request by span ID to handle cases where child spans are activated - // before pageLoad runs (especially when OpenTelemetry TracerProvider is enabled) const spanId = span.context().toSpanId() this._requestsBySpanId.set(spanId, req) @@ -101,9 +100,6 @@ class NextPlugin extends ServerPlugin { const spanId = span.context().toSpanId() // Convert parent ID from hex to decimal to match storage format. - // IMPORTANT: toSpanId() returns decimal string (e.g., "457018059221062340") - // but _parentId.toString() defaults to hex (e.g., "0657a780e3cf52c4"). - // We must use toString(10) to get decimal format for Map lookup to work. let parentSpanId = null if (span.context()._parentId) { parentSpanId = span.context()._parentId.toString(10) diff --git a/packages/datadog-plugin-next/test/index.spec.js b/packages/datadog-plugin-next/test/index.spec.js index b57cdc31492..b096e3b5814 100644 --- a/packages/datadog-plugin-next/test/index.spec.js +++ b/packages/datadog-plugin-next/test/index.spec.js @@ -277,6 +277,30 @@ describe('Plugin', function () { .get(`http://127.0.0.1:${port}/api/hello/world`) .catch(done) }) + + it('should handle child spans and still find the request object', done => { + agent + .assertSomeTraces(traces => { + const spans = traces[0] + const nextRequestSpan = spans.find(span => span.name === 'next.request') + assert.ok(nextRequestSpan, 'next.request span should exist') + + assert.strictEqual(nextRequestSpan.resource, 'GET /api/hello/[name]') + assert.strictEqual(nextRequestSpan.meta['next.page'], '/api/hello/[name]') + assert.strictEqual(nextRequestSpan.meta['http.method'], 'GET') + assert.strictEqual(nextRequestSpan.meta['http.status_code'], '200') + + const webRequestSpan = spans.find(span => span.name === 'web.request') + assert.ok(webRequestSpan, 'web.request span should exist') + assert.strictEqual(webRequestSpan.resource, 'GET /api/hello/[name]') + }) + .then(done) + .catch(done) + + axios + .get(`http://127.0.0.1:${port}/api/hello/world`) + .catch(done) + }) }) describe('for pages', () => { From 266b5356146ffb1cef6b872426ebf1b5f2e66bdd Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 8 Dec 2025 14:14:49 -0500 Subject: [PATCH 3/6] test for change now create a child span to check this works as expected --- packages/datadog-plugin-next/test/index.spec.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/datadog-plugin-next/test/index.spec.js b/packages/datadog-plugin-next/test/index.spec.js index b096e3b5814..1a60e848cb0 100644 --- a/packages/datadog-plugin-next/test/index.spec.js +++ b/packages/datadog-plugin-next/test/index.spec.js @@ -278,27 +278,33 @@ describe('Plugin', function () { .catch(done) }) - it('should handle child spans and still find the request object', done => { + it.only('should handle child spans and still find the request object', done => { agent .assertSomeTraces(traces => { const spans = traces[0] + const nextRequestSpan = spans.find(span => span.name === 'next.request') assert.ok(nextRequestSpan, 'next.request span should exist') - assert.strictEqual(nextRequestSpan.resource, 'GET /api/hello/[name]') - assert.strictEqual(nextRequestSpan.meta['next.page'], '/api/hello/[name]') + assert.strictEqual(nextRequestSpan.resource, 'GET /api/child-span') + assert.strictEqual(nextRequestSpan.meta['next.page'], '/api/child-span') assert.strictEqual(nextRequestSpan.meta['http.method'], 'GET') assert.strictEqual(nextRequestSpan.meta['http.status_code'], '200') const webRequestSpan = spans.find(span => span.name === 'web.request') assert.ok(webRequestSpan, 'web.request span should exist') - assert.strictEqual(webRequestSpan.resource, 'GET /api/hello/[name]') + assert.strictEqual(webRequestSpan.resource, 'GET /api/child-span') + + const childSpan = spans.find(span => span.name === 'child.operation') + if (childSpan) { + assert.strictEqual(childSpan.parent_id.toString(), nextRequestSpan.span_id.toString()) + } }) .then(done) .catch(done) axios - .get(`http://127.0.0.1:${port}/api/hello/world`) + .get(`http://127.0.0.1:${port}/api/child-span`) .catch(done) }) }) From b76351c03fc52b4fac80744c99abc4e1ddc6e60c Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 8 Dec 2025 15:10:03 -0500 Subject: [PATCH 4/6] updating child test --- packages/datadog-plugin-next/test/index.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datadog-plugin-next/test/index.spec.js b/packages/datadog-plugin-next/test/index.spec.js index 1a60e848cb0..3d403b154a8 100644 --- a/packages/datadog-plugin-next/test/index.spec.js +++ b/packages/datadog-plugin-next/test/index.spec.js @@ -278,7 +278,7 @@ describe('Plugin', function () { .catch(done) }) - it.only('should handle child spans and still find the request object', done => { + it('should handle child spans and still find the request object', done => { agent .assertSomeTraces(traces => { const spans = traces[0] From 6579ea754229f646a2eb31bb8a26ee420214e79d Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 8 Dec 2025 15:36:09 -0500 Subject: [PATCH 5/6] updating tests --- packages/datadog-plugin-next/test/index.spec.js | 13 ++++++------- .../test/pages/api/hello/[name].js | 11 +++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/datadog-plugin-next/test/index.spec.js b/packages/datadog-plugin-next/test/index.spec.js index 3d403b154a8..2c5e2c114c4 100644 --- a/packages/datadog-plugin-next/test/index.spec.js +++ b/packages/datadog-plugin-next/test/index.spec.js @@ -286,25 +286,24 @@ describe('Plugin', function () { const nextRequestSpan = spans.find(span => span.name === 'next.request') assert.ok(nextRequestSpan, 'next.request span should exist') - assert.strictEqual(nextRequestSpan.resource, 'GET /api/child-span') - assert.strictEqual(nextRequestSpan.meta['next.page'], '/api/child-span') + assert.strictEqual(nextRequestSpan.resource, 'GET /api/hello/[name]') + assert.strictEqual(nextRequestSpan.meta['next.page'], '/api/hello/[name]') assert.strictEqual(nextRequestSpan.meta['http.method'], 'GET') assert.strictEqual(nextRequestSpan.meta['http.status_code'], '200') const webRequestSpan = spans.find(span => span.name === 'web.request') assert.ok(webRequestSpan, 'web.request span should exist') - assert.strictEqual(webRequestSpan.resource, 'GET /api/child-span') + assert.strictEqual(webRequestSpan.resource, 'GET /api/hello/[name]') const childSpan = spans.find(span => span.name === 'child.operation') - if (childSpan) { - assert.strictEqual(childSpan.parent_id.toString(), nextRequestSpan.span_id.toString()) - } + assert.ok(childSpan, 'child span should exist') + assert.strictEqual(childSpan.parent_id.toString(), nextRequestSpan.span_id.toString()) }) .then(done) .catch(done) axios - .get(`http://127.0.0.1:${port}/api/child-span`) + .get(`http://127.0.0.1:${port}/api/hello/world?createChildSpan=true`) .catch(done) }) }) diff --git a/packages/datadog-plugin-next/test/pages/api/hello/[name].js b/packages/datadog-plugin-next/test/pages/api/hello/[name].js index 729cdfdb0a7..1b43c15c1a7 100644 --- a/packages/datadog-plugin-next/test/pages/api/hello/[name].js +++ b/packages/datadog-plugin-next/test/pages/api/hello/[name].js @@ -2,6 +2,17 @@ export default (req, res) => { const tracer = require('../../../../../dd-trace') + + if (req.query.createChildSpan === 'true') { + const childSpan = tracer.startSpan('child.operation', { + childOf: tracer.scope().active() + }) + + tracer.scope().activate(childSpan, () => { + childSpan.finish() + }) + } + const span = tracer.scope().active() const name = span && span.context()._name From 5035e0639511ffba71fc4b91f60a6ea1bc4a9c96 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Tue, 9 Dec 2025 15:30:49 -0500 Subject: [PATCH 6/6] using a weakmap with the span id object --- packages/datadog-plugin-next/src/index.js | 26 ++++++----------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/packages/datadog-plugin-next/src/index.js b/packages/datadog-plugin-next/src/index.js index 451aaa4712e..3d6bba7d512 100644 --- a/packages/datadog-plugin-next/src/index.js +++ b/packages/datadog-plugin-next/src/index.js @@ -10,10 +10,10 @@ const errorPages = new Set(['/404', '/500', '/_error', '/_not-found', '/_not-fou class NextPlugin extends ServerPlugin { static id = 'next' + #requestsBySpanId = new WeakMap() constructor (...args) { super(...args) - this._requestsBySpanId = new Map() this.addSub('apm:next:page:load', message => this.pageLoad(message)) } @@ -36,8 +36,8 @@ class NextPlugin extends ServerPlugin { analyticsSampler.sample(span, this.config.measured, true) // Store request by span ID to handle cases where child spans are activated - const spanId = span.context().toSpanId() - this._requestsBySpanId.set(spanId, req) + const spanId = span.context()._spanId + this.#requestsBySpanId.set(spanId, req) return { ...store, span } } @@ -83,11 +83,6 @@ class NextPlugin extends ServerPlugin { this.config.hooks.request(span, req, res) span.finish() - - // Cleanup: Remove request from Map to prevent memory leaks - // since Map (unlike WeakMap) won't auto-garbage collect - const spanId = span.context().toSpanId() - this._requestsBySpanId.delete(spanId) } pageLoad ({ page, isAppPath = false, isStatic = false }) { @@ -97,21 +92,12 @@ class NextPlugin extends ServerPlugin { const span = store.span - const spanId = span.context().toSpanId() - - // Convert parent ID from hex to decimal to match storage format. - let parentSpanId = null - if (span.context()._parentId) { - parentSpanId = span.context()._parentId.toString(10) - } + const spanId = span.context()._spanId + const parentSpanId = span.context()._parentId // Try current span first, then parent span. // This handles cases where pageLoad runs in a child span context - // (e.g., when OpenTelemetry TracerProvider creates "resolve page components" span) - let req = this._requestsBySpanId.get(spanId) - if (!req && parentSpanId) { - req = this._requestsBySpanId.get(parentSpanId) - } + const req = this.#requestsBySpanId.get(spanId) ?? this.#requestsBySpanId.get(parentSpanId) // safeguard against missing req in complicated timeout scenarios if (!req) return