Skip to content

[NextJS] Nextjs app router integration (AI optimistic)#4234

Draft
lierniel wants to merge 4 commits intomainfrom
stephan.koshcheev/nextjs-app-router-integration-ai-optimistic
Draft

[NextJS] Nextjs app router integration (AI optimistic)#4234
lierniel wants to merge 4 commits intomainfrom
stephan.koshcheev/nextjs-app-router-integration-ai-optimistic

Conversation

@lierniel
Copy link

@lierniel lierniel commented Feb 25, 2026

✨ Add Next.js App Router integration for @datadog/browser-rum-react

Motivation

Next.js App Router is a widely adopted routing paradigm (Next.js 13.4+), but RUM view tracking requires manual instrumentation today. This integration provides a <DatadogRumProvider> component that automatically tracks client-side navigations as RUM views with parameterized route names (e.g., /users/:id), following the same sub-package pattern as the existing React Router integrations.

Changes

Plugin configuration (packages/rum-react/src/domain/reactPlugin.ts):

  • Add nextAppRouter?: boolean option to ReactPluginConfiguration
  • Automatically set trackViewsManually = true when nextAppRouter is enabled
  • Warn when both router and nextAppRouter are enabled simultaneously
  • Report nextAppRouter in configuration telemetry

View name computation (packages/rum-react/src/domain/nextjs/computeNextViewName.ts):

  • Segment-aware param replacement: reconstructs parameterized route patterns from pathname + params (e.g., /users/123 + { id: '123' }/users/:id)
  • Handles catch-all segments ([...slug]/:slug), multi-occurrence params, and edge cases (empty/undefined values)
  • Array params processed before string params to avoid partial replacements

Provider component (packages/rum-react/src/domain/nextjs/datadogRumProvider.tsx):

  • Client-side DatadogRumProvider component that reads usePathname() / useParams() from next/navigation
  • Uses ref-based deduplication to prevent duplicate views on re-renders
  • Defers view start via onRumInit subscriber pattern (safe for any init order)
  • Deduplicates by computed view name (not raw pathname), so /users/123/users/456 does not start a new view if both resolve to /users/:id

Sub-package entry point:

  • packages/rum-react/nextjs/package.json — shim pointing to compiled artifacts (same pattern as react-router-v6/, react-router-v7/)
  • packages/rum-react/src/entries/nextjs.ts — entry point exporting DatadogRumProvider

Sample test app (test/apps/nextjs-app/):

  • Simulates Next.js routing via mockNextNavigation.tsx (mock usePathname, useParams, useNavigate backed by React context)
  • Routes: home (/), dynamic param (/user/:id), tracked component (/tracked), error boundary (/error)
  • Uses DatadogRumProvider, ErrorBoundary, UNSTABLE_ReactComponentTracker from the SDK
  • Registered in scripts/build/build-test-apps.ts and test/e2e/lib/framework/sdkBuilds.ts

E2E tests (test/e2e/scenario/reactPlugin.scenario.ts):

  • View name tracking: navigate to /user/42, verify last view name is /user/:id
  • Component render vital: navigate to tracked page, verify TrackedPage vital with duration
  • Error boundary: trigger error, verify error event with framework: 'react' and component stack

Test instructions

  1. Run unit tests: yarn test:unit --spec packages/rum-react/src/domain/nextjs/computeNextViewName.spec.ts and the other spec files in packages/rum-react/src/domain/nextjs/
  2. Run plugin tests: yarn test:unit --spec packages/rum-react/src/domain/reactPlugin.spec.ts
  3. Build test apps: yarn build:apps
  4. Run E2E tests: yarn test:e2e -g "Next.js App Router"
  5. Verify typecheck: yarn typecheck

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

@github-actions
Copy link


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@datadog-official
Copy link

datadog-official bot commented Feb 25, 2026

⚠️ Tests

Fix all issues with BitsAI or with Cursor

⚠️ Warnings

🧪 33 Tests failed

DatadogRumProvider does not start a new view on re-render with same pathname from Chrome 63.0.3239.84 (Windows 10) (Datadog) (Fix with Cursor)
Error: Should not already be working.
    at performWorkOnRoot (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:16424:15 <- /tmp/_karma_webpack_197022/commons.js:28712:87)
    at performWorkOnRootViaSchedulerTask (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:18957:7 <- /tmp/_karma_webpack_197022/commons.js:29863:9)
    at flushActQueue (webpack:///node_modules/react/cjs/react.development.js:590:34 <- /tmp/_karma_webpack_197022/commons.js:37742:44)
    at exports.act (webpack:///node_modules/react/cjs/react.development.js:884:10 <- /tmp/_karma_webpack_197022/commons.js:37928:37)
    at appendComponent (webpack:///packages/rum-react/test/appendComponent.ts:13:6 <- /tmp/_karma_webpack_197022/commons.js:92365:47)
    at UserContext.it (webpack:///packages/rum-react/src/domain/nextjs/datadogRumProvider.spec.tsx:59:20 <- /tmp/_karma_webpack_197022/commons.js:90920:79)
    at <Jasmine>
DatadogRumProvider does not start a new view when navigating to a different instance of the same route from Chrome 63.0.3239.84 (Windows 10) (Datadog) (Fix with Cursor)
Error: Should not already be working.
    at performWorkOnRoot (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:16424:15 <- /tmp/_karma_webpack_197022/commons.js:28712:87)
    at performWorkOnRootViaSchedulerTask (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:18957:7 <- /tmp/_karma_webpack_197022/commons.js:29863:9)
    at flushActQueue (webpack:///node_modules/react/cjs/react.development.js:590:34 <- /tmp/_karma_webpack_197022/commons.js:37742:44)
    at exports.act (webpack:///node_modules/react/cjs/react.development.js:884:10 <- /tmp/_karma_webpack_197022/commons.js:37928:37)
    at appendComponent (webpack:///packages/rum-react/test/appendComponent.ts:13:6 <- /tmp/_karma_webpack_197022/commons.js:92365:47)
    at UserContext.it (webpack:///packages/rum-react/src/domain/nextjs/datadogRumProvider.spec.tsx:107:20 <- /tmp/_karma_webpack_197022/commons.js:90955:79)
    at <Jasmine>
DatadogRumProvider renders children without wrapper DOM elements from Chrome 63.0.3239.84 (Windows 10) (Datadog) (Fix with Cursor)
SyntaxError: Failed to execute 'measure' on 'Performance': The mark '[object Object]' does not exist.
error properties: Object({ INDEX_SIZE_ERR: 1, DOMSTRING_SIZE_ERR: 2, HIERARCHY_REQUEST_ERR: 3, WRONG_DOCUMENT_ERR: 4, INVALID_CHARACTER_ERR: 5, NO_DATA_ALLOWED_ERR: 6, NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9, INUSE_ATTRIBUTE_ERR: 10, INVALID_STATE_ERR: 11, SYNTAX_ERR: 12, INVALID_MODIFICATION_ERR: 13, NAMESPACE_ERR: 14, INVALID_ACCESS_ERR: 15, VALIDATION_ERR: 16, TYPE_MISMATCH_ERR: 17, SECURITY_ERR: 18, NETWORK_ERR: 19, ABORT_ERR: 20, URL_MISMATCH_ERR: 21, QUOTA_EXCEEDED_ERR: 22, TIMEOUT_ERR: 23, INVALID_NODE_TYPE_ERR: 24, DATA_CLONE_ERR: 25, code: 12 })
Error: Failed to execute 'measure' on 'Performance': The mark '[object Object]' does not exist.
    at logComponentTrigger (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:4091:25 <- /tmp/_karma_webpack_197022/commons.js:22971:400)
    at commitPassiveMountOnFiber (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:15737:15 <- /tmp/_karma_webpack_197022/commons.js:28439:266)
    at recursivelyTraversePassiveMountEffects (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:15439:11 <- /tmp/_karma_webpack_197022/commons.js:28368:13)
    at commitPassiveMountOnFiber (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:15519:11 <- /tmp/_karma_webpack_197022/commons.js:28389:17)
    at flushPassiveEffects (webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:18432:9 <- /tmp/_karma_webpack_197022/commons.js:29608:13)
    at webpack:///node_modules/react-dom/cjs/react-dom-client.development.js:17923:15 <- /tmp/_karma_webpack_197022/commons.js:29357:17
    at flushActQueue (webpack:///node_modules/react/cjs/react.development.js:590:34 <- /tmp/_karma_webpack_197022/commons.js:37742:44)
...
View all

ℹ️ Info

❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 70.27%
Overall Coverage: 77.15% (-0.04%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 5ea0e10 | Docs | Datadog PR Page | Was this helpful? Give us feedback!


let forceUpdate: () => void

function App() {
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I love the idea of doing a component in a unit test. I would prefer to put it in a e2e test and here test behaviour.

return <>{children}</>
}

DatadogRumProvider.displayName = 'DatadogRumProvider'
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: What is displayName this for?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just for the React Browser Extension to be able to display the component name in the React Virtual DOM tree. This is optional but nice to have

* return (
* <html>
* <body>
* <DatadogRumProvider>{children}</DatadogRumProvider>
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am stealing the following question since it was asked to me too:
Question: Why is this a Provider if it's not wrapping anything? Could it be a function call too?

Copy link
Author

@lierniel lierniel Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, indeed, it can be implemented as a hook for example. We can't make it a pure function call because we have to call hooks in it, but hooks approach could work, I agree

"devDependencies": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"next": "15.2.2",
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question:: So we will support next >= 15.2.2. Shouldn't we try to support sooner versions too?
Suggestion:: For example support version >= 13?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's just the version we use inside our integration. As it is defined above in the peerDependencies section, this approach supports all the versions >= 13

import {
reactPlugin,
UNSTABLE_ReactComponentTracker as ReactComponentTracker,
ErrorBoundary,
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The thing about this is that NextJS has a different signature to report errors.
export default function ErrorPage({ error, reset, }:
So the React Error Boundary would not have the whole context. So we would need to make something for it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, tbh I didn't look too much into sample app implementation made by AI, so it probably has a lots of problems which you raised

datadogRum.setGlobalContext(window.RUM_CONTEXT)
}

function Link({
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Isn't this a Next component? Why do we have to re-declare it?

)
}

function App() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not how App Router works for routes. Each file is it's own route. Doc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is important so that we use NextJS specific behaviour like client and server components and even prefetching and test against it.

return useContext(NavigationContext).navigate
}

export function NavigationProvider({
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: What is this component for? Would customer need to create this file themselves? Shouldn't this be handled on the SDK side?

test.describe(`with ${description}`, () => {
createTest('should define a view name with DatadogRumProvider')
.withRum()
.withReactApp(appName)
Copy link
Contributor

@BeltranBulbarellaDD BeltranBulbarellaDD Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why are using ReactApp and not NextApp. IIUC, here you are serving the static html files to the tests, a NextApp needs it's own server to run so as to leverage NextJS features like prefetching, server components, cache, etc.


// Firefox may delay dispatching error events from React error boundaries,
// causing flushEvents() to miss them, this timeout ensures the RUM event is captured.
if (browserName === 'firefox') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not soo sure about this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants