diff --git a/.prettierignore b/.prettierignore index cd4dd6cc00..d1ee84bc43 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,4 @@ yarn.lock /docs /developer-extension/.output /developer-extension/.wxt +/test/apps/nextjs-app-router/.next diff --git a/eslint.config.mjs b/eslint.config.mjs index 0c04bbf93a..8ec65cc36a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -35,6 +35,7 @@ export default tseslint.config( 'docs', 'developer-extension/.wxt', 'developer-extension/.output', + 'test/apps/nextjs-app-router', ], }, diff --git a/packages/rum-react/.npmignore b/packages/rum-react/.npmignore index 4049b9b2d8..c239794535 100644 --- a/packages/rum-react/.npmignore +++ b/packages/rum-react/.npmignore @@ -5,3 +5,4 @@ /src/**/*.spec.ts /src/**/*.specHelper.ts !/react-router-v[6-7]/* +!/nextjs/* diff --git a/packages/rum-react/NEXTJS_PLAN.md b/packages/rum-react/NEXTJS_PLAN.md new file mode 100644 index 0000000000..92760d2c7c --- /dev/null +++ b/packages/rum-react/NEXTJS_PLAN.md @@ -0,0 +1,404 @@ +# Next.js App Router Integration Plan + +## Overview + +Create a Next.js App Router integration as a new entry point within `@datadog/browser-rum-react` package (following the react-router-v6/v7 pattern). This integration will provide automatic route tracking, error boundary support, and component performance tracking for Next.js 13+ applications. + +**Package**: `@datadog/browser-rum-react/nextjs` + +## Architecture Decisions + +### 1. Package Structure + +- **Extend rum-react** with `/nextjs` entry point (not a separate package) +- Maximizes code reuse for ErrorBoundary and UNSTABLE_ReactComponentTracker +- Follows established pattern of react-router-v6/v7 entry points + +### 2. Initialization Patterns + +Support **both** patterns to accommodate different use cases: + +- **DatadogRumProvider component** (primary/recommended): For use in app/layout.tsx +- **Instrumentation file approach**: For early initialization using instrumentation-client.ts + +### 3. View Naming Strategy + +- **Automatic pattern detection**: Like React Router, view names use patterns not actual values +- Built-in transformation: `/product/123` → `/product/:id`, `/user/abc-123-def` → `/user/:uuid` +- Works automatically with no configuration required (matches React Router behavior) + +### 4. Plugin Configuration + +- Add `nextjs: boolean` flag to `ReactPluginConfiguration` +- When enabled, sets `trackViewsManually: true` automatically + +## Components & Exports + +### Main Exports (from `@datadog/browser-rum-react/nextjs`) + +**Initialization:** + +- `DatadogRumProvider` - Component wrapper for automatic route tracking +- `initDatadogRum(config)` - Helper for instrumentation file usage + +**Error Tracking:** + +- `ErrorBoundary` - Reused from rum-react +- `addReactError` - Reused from rum-react + +**Performance:** + +- `UNSTABLE_ReactComponentTracker` - Reused from rum-react + +**Manual Control (Advanced):** + +- `usePathnameTracker()` - Hook for custom tracking logic (rare use case) + +**Types:** + +- `NextjsRumConfig` - Configuration interface for instrumentation file +- `DatadogRumProviderProps` - Provider component props + +## Implementation Files + +### New Files Created + +#### Core Integration + +``` +packages/rum-react/src/domain/nextjs/ +├── index.ts # Barrel export +├── types.ts # TypeScript interfaces +├── datadogRumProvider.tsx # Main provider component +├── datadogRumProvider.spec.tsx # Provider tests +├── usePathnameTracker.ts # Route change detection hook +├── usePathnameTracker.spec.ts # Hook tests +├── startNextjsView.ts # View creation logic +├── startNextjsView.spec.ts # View creation tests +├── normalizeViewName.ts # Internal view name normalization +├── normalizeViewName.spec.ts # Normalization tests +└── initDatadogRum.ts # Instrumentation file helper +``` + +#### Entry Point + +``` +packages/rum-react/src/entries/ +└── nextjs.ts # Main entry point + +packages/rum-react/nextjs/ +├── package.json # Points to ../esm/entries/nextjs.js +└── typedoc.json # Documentation config +``` + +### Files Modified + +**packages/rum-react/src/domain/reactPlugin.ts** + +- Add `nextjs?: boolean` to `ReactPluginConfiguration` interface +- Set `trackViewsManually: true` when `nextjs: true` + +**packages/rum-react/package.json** + +- Add `"next": ">=13"` to `peerDependencies` +- Mark as optional in `peerDependenciesMeta` + +**packages/rum-react/README.md** + +- Add Next.js App Router section with usage examples + +## Technical Implementation Details + +### 1. DatadogRumProvider Component + +```typescript +// src/domain/nextjs/datadogRumProvider.tsx +'use client' + +export function DatadogRumProvider({ children }: DatadogRumProviderProps) { + usePathnameTracker() + return <>{children} +} +``` + +**Key features:** + +- Must be client component (`'use client'` directive) +- Uses `usePathnameTracker` internally +- Automatically normalizes dynamic segments (no configuration needed) +- Transparent wrapper (no DOM nodes) +- Always tracks initial page load (matches React Router behavior) + +### 2. usePathnameTracker Hook + +```typescript +// src/domain/nextjs/usePathnameTracker.ts +import { usePathname } from 'next/navigation' +import { useRef, useEffect } from 'react' + +export function usePathnameTracker() { + const pathname = usePathname() + const pathnameRef = useRef(null) + + useEffect(() => { + if (pathnameRef.current !== pathname) { + pathnameRef.current = pathname + startNextjsView(pathname) + } + }, [pathname]) +} +``` + +**Implementation notes:** + +- Uses Next.js `usePathname()` hook +- `useRef` to avoid unnecessary re-renders +- `useEffect` to ensure client-side only execution +- Tracks initial page load (consistent with React Router) +- Automatically normalizes view names (no config needed) + +### 3. startNextjsView Function + +```typescript +// src/domain/nextjs/startNextjsView.ts +export function startNextjsView(pathname: string) { + onRumInit((configuration, rumPublicApi) => { + if (!configuration.nextjs) { + display.warn('`nextjs: true` is missing from the react plugin configuration, ' + 'the view will not be tracked.') + return + } + + const viewName = normalizeViewName(pathname) + rumPublicApi.startView(viewName) + }) +} +``` + +**Key behaviors:** + +- Uses `onRumInit` subscription pattern (from reactPlugin) +- Checks `configuration.nextjs` flag +- Applies automatic view name normalization +- Calls `rumPublicApi.startView()` with normalized name + +### 4. View Name Normalization (Internal) + +```typescript +// src/domain/nextjs/normalizeViewName.ts + +/** + * Internal function that automatically normalizes pathnames to route patterns. + * Mimics React Router behavior where view names use placeholders. + * + * Examples: + * /product/123 -> /product/:id + * /user/abc-123-def-456 -> /user/:uuid + * /orders/456/items/789 -> /orders/:id/items/:id + */ +export function normalizeViewName(pathname: string): string { + return ( + pathname + // Replace UUID segments first (more specific pattern) + .replace(/\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(?=\/|[?#]|$)/gi, '/:uuid') + // Replace numeric segments + .replace(/\/\d+(?=\/|[?#]|$)/g, '/:id') + ) +} +``` + +**Note**: This function is internal and not exported. It automatically applies pattern detection to match React Router's behavior of showing route patterns rather than actual values. + +### 5. Instrumentation File Helper + +```typescript +// src/domain/nextjs/initDatadogRum.ts + +/** + * Helper for Next.js instrumentation-client.ts file. + * Initializes RUM and sets up global error tracking. + */ +export function initDatadogRum(config: NextjsRumConfig, datadogRum: RumPublicApi): void { + if (typeof window === 'undefined') { + // Server-side guard + return + } + + const { datadogConfig, nextjsConfig } = config + const nextjsPlugin = reactPlugin({ nextjs: true }) + const existingPlugins = (datadogConfig.plugins || []) as Array + + datadogRum.init({ + ...datadogConfig, + plugins: [nextjsPlugin].concat(existingPlugins), + }) + + // Optional: Set up early error capture + if (nextjsConfig?.captureEarlyErrors) { + addEventListener({}, window, 'error', (event) => { + datadogRum.addError(event.error) + }) + + addEventListener({}, window, 'unhandledrejection', (event: PromiseRejectionEvent) => { + datadogRum.addError(event.reason) + }) + } +} +``` + +## Usage Patterns + +### Pattern 1: DatadogRumProvider (Recommended) + +```typescript +// app/components/datadog-provider.tsx +'use client' +import { DatadogRumProvider } from '@datadog/browser-rum-react/nextjs' + +export function DatadogProvider({ children }) { + return {children} +} + +// app/layout.tsx +import { datadogRum } from '@datadog/browser-rum' +import { reactPlugin } from '@datadog/browser-rum-react' +import { DatadogProvider } from './components/datadog-provider' + +datadogRum.init({ + applicationId: '', + clientToken: '', + site: 'datadoghq.com', + plugins: [reactPlugin({ nextjs: true })], +}) + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) +} +``` + +### Pattern 2: Instrumentation File (Advanced) + +```typescript +// instrumentation-client.ts +import { initDatadogRum } from '@datadog/browser-rum-react/nextjs' + +export function register() { + initDatadogRum({ + datadogConfig: { + applicationId: '', + clientToken: '', + site: 'datadoghq.com', + }, + nextjsConfig: { + captureEarlyErrors: true, + } + }) +} + +// app/components/router-tracker.tsx +'use client' +import { usePathnameTracker } from '@datadog/browser-rum-react/nextjs' + +export function RouterTracker() { + usePathnameTracker() + return null +} + +// app/layout.tsx +import { RouterTracker } from './components/router-tracker' + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + ) +} +``` + +## Error Tracking Integration + +### With Next.js error.js + +```typescript +// app/error.tsx +'use client' +import { useEffect } from 'react' +import { addReactError } from '@datadog/browser-rum-react/nextjs' + +export default function Error({ error, reset }) { + useEffect(() => { + addReactError(error, { componentStack: '' }) + }, [error]) + + return ( +
+

Something went wrong!

+ +
+ ) +} +``` + +### With Datadog ErrorBoundary + +```typescript +// app/layout.tsx or page-level +import { ErrorBoundary } from '@datadog/browser-rum-react/nextjs' + +function ErrorFallback({ error, resetError }) { + return ( +
+

Error: {error.message}

+ +
+ ) +} + +export default function Layout({ children }) { + return ( + + {children} + + ) +} +``` + +## Performance Tracking + +```typescript +// app/dashboard/page.tsx +'use client' +import { UNSTABLE_ReactComponentTracker } from '@datadog/browser-rum-react/nextjs' +import { DashboardWidget } from './components/widget' + +export default function DashboardPage() { + return ( + + + + ) +} +``` + +## Success Criteria + +- ✅ View tracking works for Next.js App Router navigation +- ✅ Dynamic routes normalized with pattern detection +- ✅ Error tracking integrates with Next.js error boundaries +- ✅ Component performance tracking works client-side +- ✅ Both initialization patterns supported and documented +- ✅ >90% test coverage for new code +- ✅ Zero TypeScript errors +- ✅ Works with Next.js 13, 14, 15 +- ✅ Clear documentation with multiple examples +- ✅ <5 minute setup time for developers +- ✅ Aligns with React Router behavior (always tracks initial load) diff --git a/packages/rum-react/nextjs/package.json b/packages/rum-react/nextjs/package.json new file mode 100644 index 0000000000..38b6e4229f --- /dev/null +++ b/packages/rum-react/nextjs/package.json @@ -0,0 +1,7 @@ +{ + "name": "@datadog/browser-rum-react/nextjs", + "private": true, + "main": "../cjs/entries/nextjs.js", + "module": "../esm/entries/nextjs.js", + "types": "../cjs/entries/nextjs.d.ts" +} diff --git a/packages/rum-react/package.json b/packages/rum-react/package.json index d188d06206..63c9773c1e 100644 --- a/packages/rum-react/package.json +++ b/packages/rum-react/package.json @@ -14,6 +14,7 @@ "@datadog/browser-rum-core": "6.26.0" }, "peerDependencies": { + "next": ">=13", "react": "18 || 19", "react-router": "6 || 7", "react-router-dom": "6 || 7" @@ -25,6 +26,9 @@ "@datadog/browser-rum-slim": { "optional": true }, + "next": { + "optional": true + }, "react": { "optional": true }, @@ -38,6 +42,7 @@ "devDependencies": { "@types/react": "19.2.8", "@types/react-dom": "19.2.3", + "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3", "react-router": "7.12.0", diff --git a/packages/rum-react/src/domain/nextjs/datadogRumProvider.spec.tsx b/packages/rum-react/src/domain/nextjs/datadogRumProvider.spec.tsx new file mode 100644 index 0000000000..c1a1651300 --- /dev/null +++ b/packages/rum-react/src/domain/nextjs/datadogRumProvider.spec.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { appendComponent } from '../../../test/appendComponent' +import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +import { DatadogRumProvider } from './datadogRumProvider' + +describe('DatadogRumProvider', () => { + let startViewSpy: jasmine.Spy<(name?: string | object) => void> + let usePathnameSpy: jasmine.Spy<() => string> + + beforeEach(() => { + startViewSpy = jasmine.createSpy() + initializeReactPlugin({ + configuration: { + nextjs: true, + }, + publicApi: { + startView: startViewSpy, + }, + }) + + usePathnameSpy = jasmine.createSpy('usePathname').and.returnValue('/') + }) + + it('renders children correctly', () => { + const container = appendComponent( + +
Test Content
+
+ ) + + const child = container.querySelector('[data-testid="test-child"]') + expect(child).not.toBeNull() + expect(child!.textContent).toBe('Test Content') + expect(child!.parentElement).toBe(container) + }) + + it('calls usePathnameTracker', () => { + usePathnameSpy.and.returnValue('/product/123') + + appendComponent( + +
Content
+
+ ) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/product/:id') + }) + + it('renders multiple children', () => { + const container = appendComponent( + +
Child 1
+
Child 2
+
Child 3
+
+ ) + + expect(container.querySelector('[data-testid="child-1"]')!.textContent).toBe('Child 1') + expect(container.querySelector('[data-testid="child-2"]')!.textContent).toBe('Child 2') + expect(container.querySelector('[data-testid="child-3"]')!.textContent).toBe('Child 3') + }) +}) diff --git a/packages/rum-react/src/domain/nextjs/datadogRumProvider.tsx b/packages/rum-react/src/domain/nextjs/datadogRumProvider.tsx new file mode 100644 index 0000000000..a3c3dc52e3 --- /dev/null +++ b/packages/rum-react/src/domain/nextjs/datadogRumProvider.tsx @@ -0,0 +1,43 @@ +'use client' + +import React, { type ReactNode } from 'react' +import { usePathname as nextUsePathname } from 'next/navigation' +import { usePathnameTracker } from './viewTracking' + +export interface DatadogRumProviderProps { + /** + * The children components to render. + */ + + children: ReactNode + + /** + * @internal - For dependency injection in tests. + */ + usePathname?: () => string +} + +/** + * Provider component for Next.js App Router that automatically tracks navigation. + * Wrap your application with this component to enable automatic view tracking. + * + * @example + * ```tsx + * // app/layout.tsx + * import { DatadogRumProvider } from '@datadog/browser-rum-react/nextjs' + * + * export default function RootLayout({ children }) { + * return ( + * + * + * {children} + * + * + * ) + * } + * ``` + */ +export function DatadogRumProvider({ children, usePathname = nextUsePathname }: DatadogRumProviderProps) { + usePathnameTracker(usePathname) + return <>{children} +} diff --git a/packages/rum-react/src/domain/nextjs/index.ts b/packages/rum-react/src/domain/nextjs/index.ts new file mode 100644 index 0000000000..e4106d8112 --- /dev/null +++ b/packages/rum-react/src/domain/nextjs/index.ts @@ -0,0 +1,5 @@ +export { DatadogRumProvider } from './datadogRumProvider' +export type { DatadogRumProviderProps } from './datadogRumProvider' +export { usePathnameTracker, startNextjsView, normalizeViewName } from './viewTracking' +// export { initDatadogRum } from './initDatadogRum' +export type { NextjsRumConfig } from './types' diff --git a/packages/rum-react/src/domain/nextjs/initDatadogRum.ts b/packages/rum-react/src/domain/nextjs/initDatadogRum.ts new file mode 100644 index 0000000000..db1e8ceb82 --- /dev/null +++ b/packages/rum-react/src/domain/nextjs/initDatadogRum.ts @@ -0,0 +1,64 @@ +// Work in progress, not tested yet. + +// import type { RumPublicApi } from '@datadog/browser-rum-core' +// import { addEventListener } from '@datadog/browser-core' +// import { reactPlugin } from '../reactPlugin' +// import type { NextjsRumConfig } from './types' + +// /** +// * Helper for Next.js instrumentation file (instrumentation-client.ts). +// * Initializes RUM with Next.js integration enabled and optional early error capture. +// * +// * @param config - Configuration for Next.js RUM integration +// * @param datadogRum - The datadogRum instance from @datadog/browser-rum +// * @example +// * ```ts +// * // instrumentation-client.ts +// * import { datadogRum } from '@datadog/browser-rum' +// * import { initDatadogRum } from '@datadog/browser-rum-react/nextjs' +// * +// * export function register() { +// * initDatadogRum( +// * { +// * datadogConfig: { +// * applicationId: '', +// * clientToken: '', +// * site: 'datadoghq.com', +// * }, +// * nextjsConfig: { +// * captureEarlyErrors: true, +// * } +// * }, +// * datadogRum +// * ) +// * } +// * ``` +// */ +// export function initDatadogRum(config: NextjsRumConfig, datadogRum: RumPublicApi): void { +// if (typeof window === 'undefined') { +// // Server-side guard - RUM only runs in the browser +// return +// } + +// const { datadogConfig, nextjsConfig } = config + +// // Initialize RUM with the reactPlugin configured for Next.js +// const nextjsPlugin = reactPlugin({ nextjs: true }) +// const existingPlugins = (datadogConfig.plugins || []) as Array + +// datadogRum.init({ +// ...datadogConfig, +// plugins: [nextjsPlugin].concat(existingPlugins), +// }) + +// // Optional: Set up early error capture +// if (nextjsConfig?.captureEarlyErrors) { +// addEventListener({}, window, 'error', (event) => { +// datadogRum.addError(event.error) +// }) + +// addEventListener({}, window, 'unhandledrejection', (event: PromiseRejectionEvent) => { +// datadogRum.addError(event.reason) +// }) +// } +// } diff --git a/packages/rum-react/src/domain/nextjs/types.ts b/packages/rum-react/src/domain/nextjs/types.ts new file mode 100644 index 0000000000..4e4b6fdfa5 --- /dev/null +++ b/packages/rum-react/src/domain/nextjs/types.ts @@ -0,0 +1,25 @@ +import type { RumInitConfiguration } from '@datadog/browser-rum-core' + +/** + * Configuration for Next.js instrumentation file pattern. + */ +export interface NextjsRumConfig { + /** + * Datadog RUM initialization configuration. + */ + datadogConfig: RumInitConfiguration + + /** + * Next.js-specific configuration options. + */ + + nextjsConfig?: { + /** + * Whether to capture early errors that occur before the app fully initializes. + * + * @default false + */ + + captureEarlyErrors?: boolean + } +} diff --git a/packages/rum-react/src/domain/nextjs/viewTracking.spec.tsx b/packages/rum-react/src/domain/nextjs/viewTracking.spec.tsx new file mode 100644 index 0000000000..b7b43dbd07 --- /dev/null +++ b/packages/rum-react/src/domain/nextjs/viewTracking.spec.tsx @@ -0,0 +1,122 @@ +import React, { act, useState } from 'react' +import { display } from '@datadog/browser-core' +import { appendComponent } from '../../../test/appendComponent' +import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +import { startNextjsView, usePathnameTracker } from './viewTracking' + +describe('startNextjsView', () => { + let startViewSpy: jasmine.Spy<(name?: string | object) => void> + + beforeEach(() => { + startViewSpy = jasmine.createSpy() + initializeReactPlugin({ + configuration: { + nextjs: true, + }, + publicApi: { + startView: startViewSpy, + }, + }) + }) + ;[ + ['/product/123', '/product/:id'], + ['/user/abc12345-1234-1234-1234-123456789012', '/user/:uuid'], + ['/about', '/about'], + ['/', '/'], + ].forEach(([pathname, normalizedPathname]) => { + it(`creates a new view with the normalized pathname ${normalizedPathname}`, () => { + startNextjsView(pathname) + + expect(startViewSpy).toHaveBeenCalledOnceWith(normalizedPathname) + }) + }) + + it('warns when nextjs configuration is missing', () => { + const localStartViewSpy = jasmine.createSpy() + const warnSpy = spyOn(display, 'warn') + initializeReactPlugin({ + configuration: {}, + publicApi: { + startView: localStartViewSpy, + }, + }) + + startNextjsView('/product/123') + + expect(warnSpy).toHaveBeenCalledOnceWith( + '`nextjs: true` is missing from the react plugin configuration, the view will not be tracked.' + ) + expect(localStartViewSpy).not.toHaveBeenCalled() + }) + + it('does not create a view when nextjs flag is false', () => { + const localStartViewSpy = jasmine.createSpy() + const warnSpy = spyOn(display, 'warn') + initializeReactPlugin({ + configuration: { + nextjs: false, + }, + publicApi: { + startView: localStartViewSpy, + }, + }) + + startNextjsView('/product/123') + + expect(warnSpy).toHaveBeenCalled() + expect(localStartViewSpy).not.toHaveBeenCalled() + }) +}) + +describe('usePathnameTracker', () => { + let startViewSpy: jasmine.Spy<(name?: string | object) => void> + let usePathnameSpy: jasmine.Spy<() => string> + + beforeEach(() => { + startViewSpy = jasmine.createSpy() + initializeReactPlugin({ + configuration: { + nextjs: true, + }, + publicApi: { + startView: startViewSpy, + }, + }) + + usePathnameSpy = jasmine.createSpy('usePathname').and.returnValue('/') + }) + + function TestComponent() { + usePathnameTracker(usePathnameSpy) + return null + } + + it('calls startNextjsView on mount', () => { + usePathnameSpy.and.returnValue('/product/123') + + appendComponent() + + expect(startViewSpy).toHaveBeenCalledOnceWith('/product/:id') + }) + + it('does not create duplicate views on re-render with same pathname', () => { + usePathnameSpy.and.returnValue('/product/123') + + function ReRenderingComponent() { + const [, setCounter] = useState(0) + usePathnameTracker(usePathnameSpy) + + return + } + + const container = appendComponent() + expect(startViewSpy).toHaveBeenCalledTimes(1) + + const button = container.querySelector('button') as HTMLButtonElement + act(() => { + button.click() + }) + + expect(startViewSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/rum-react/src/domain/nextjs/viewTracking.tsx b/packages/rum-react/src/domain/nextjs/viewTracking.tsx new file mode 100644 index 0000000000..aca0f8215f --- /dev/null +++ b/packages/rum-react/src/domain/nextjs/viewTracking.tsx @@ -0,0 +1,43 @@ +import { useRef, useEffect } from 'react' +import { usePathname as nextUsePathname } from 'next/navigation' +import { display } from '@datadog/browser-core' +import { onRumInit } from '../reactPlugin' + +/** + * Normalizes the pathname to use route patterns (e.g., /product/123 -> /product/:id). + */ +export function normalizeViewName(pathname: string): string { + return pathname + .replace(/\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(?=\/|[?#]|$)/gi, '/:uuid') + .replace(/\/\d+(?=\/|[?#]|$)/g, '/:id') +} + +/** + * Starts a new RUM view. + */ +export function startNextjsView(pathname: string) { + onRumInit((configuration, rumPublicApi) => { + if (!configuration.nextjs) { + display.warn('`nextjs: true` is missing from the react plugin configuration, the view will not be tracked.') + return + } + + const viewName = normalizeViewName(pathname) + rumPublicApi.startView(viewName) + }) +} + +/** + * Tracks navigation changes and starts a new RUM view for each new pathname. + */ +export function usePathnameTracker(usePathname = nextUsePathname) { + const pathname = usePathname() + const pathnameRef = useRef(null) + + useEffect(() => { + if (pathnameRef.current !== pathname) { + pathnameRef.current = pathname + startNextjsView(pathname) + } + }, [pathname]) +} diff --git a/packages/rum-react/src/domain/reactPlugin.spec.ts b/packages/rum-react/src/domain/reactPlugin.spec.ts index 2540c79186..6f0fdd573e 100644 --- a/packages/rum-react/src/domain/reactPlugin.spec.ts +++ b/packages/rum-react/src/domain/reactPlugin.spec.ts @@ -69,6 +69,13 @@ describe('reactPlugin', () => { const pluginConfiguration = { router: true } const plugin = reactPlugin(pluginConfiguration) - expect(plugin.getConfigurationTelemetry()).toEqual({ router: true }) + expect(plugin.getConfigurationTelemetry()).toEqual({ router: true, nextjs: false }) + }) + + it('returns the configuration telemetry when nextjs is true', () => { + const pluginConfiguration = { nextjs: true } + const plugin = reactPlugin(pluginConfiguration) + + expect(plugin.getConfigurationTelemetry()).toEqual({ router: false, nextjs: true }) }) }) diff --git a/packages/rum-react/src/domain/reactPlugin.ts b/packages/rum-react/src/domain/reactPlugin.ts index 946e733ff7..479a896f91 100644 --- a/packages/rum-react/src/domain/reactPlugin.ts +++ b/packages/rum-react/src/domain/reactPlugin.ts @@ -23,6 +23,13 @@ export interface ReactPluginConfiguration { * ``` */ router?: boolean + + /** + * Enable Next.js App Router integration. Make sure to use the DatadogRumProvider from + * {@link @datadog/browser-rum-react/nextjs! | @datadog/browser-rum-react/nextjs} + * to enable automatic view tracking. + */ + nextjs?: boolean } /** @@ -61,7 +68,7 @@ export function reactPlugin(configuration: ReactPluginConfiguration = {}): React for (const subscriber of onRumInitSubscribers) { subscriber(globalConfiguration, globalPublicApi) } - if (configuration.router) { + if (configuration.router || configuration.nextjs) { initConfiguration.trackViewsManually = true } }, @@ -74,7 +81,7 @@ export function reactPlugin(configuration: ReactPluginConfiguration = {}): React } }, getConfigurationTelemetry() { - return { router: !!configuration.router } + return { router: !!configuration.router, nextjs: !!configuration.nextjs } }, } satisfies RumPlugin } diff --git a/packages/rum-react/src/entries/nextjs.ts b/packages/rum-react/src/entries/nextjs.ts new file mode 100644 index 0000000000..af881d84c3 --- /dev/null +++ b/packages/rum-react/src/entries/nextjs.ts @@ -0,0 +1,46 @@ +/** + * Next.js App Router integration. + * + * @packageDocumentation + * @example + * ```tsx + * // app/layout.tsx + * import { datadogRum } from '@datadog/browser-rum' + * import { reactPlugin } from '@datadog/browser-rum-react' + * import { DatadogRumProvider } from '@datadog/browser-rum-react/nextjs' + * + * datadogRum.init({ + * applicationId: '', + * clientToken: '', + * plugins: [reactPlugin({ nextjs: true })], + * // ... + * }) + * + * export default function RootLayout({ children }) { + * return ( + * + * + * {children} + * + * + * ) + * } + * ``` + */ + +// Export Next.js-specific functionality +export { + DatadogRumProvider, + usePathnameTracker, + // initDatadogRum, + startNextjsView, +} from '../domain/nextjs' +export type { DatadogRumProviderProps, NextjsRumConfig } from '../domain/nextjs' + +// Re-export shared functionality from main package +export { ErrorBoundary, addReactError } from '../domain/error' +export type { ErrorBoundaryProps, ErrorBoundaryFallback } from '../domain/error' +export type { ReactPluginConfiguration, ReactPlugin } from '../domain/reactPlugin' +export { reactPlugin } from '../domain/reactPlugin' +// eslint-disable-next-line camelcase +export { UNSTABLE_ReactComponentTracker } from '../domain/performance' diff --git a/test/apps/nextjs-app-router/.gitignore b/test/apps/nextjs-app-router/.gitignore new file mode 100644 index 0000000000..20e9477154 --- /dev/null +++ b/test/apps/nextjs-app-router/.gitignore @@ -0,0 +1,3 @@ +.next +node_modules +.yarn/* \ No newline at end of file diff --git a/test/apps/nextjs-app-router/README.md b/test/apps/nextjs-app-router/README.md new file mode 100644 index 0000000000..f0a5e9d7e6 --- /dev/null +++ b/test/apps/nextjs-app-router/README.md @@ -0,0 +1,53 @@ +# Next.js App Router Test App + +Test application for the `@datadog/browser-rum-react/nextjs` integration. + +## Rebuilding After Changes + +When you make changes to the SDK packages, run these commands: + +### One-liner (from repo root) + +```bash +cd packages/rum-react && yarn build && yarn pack --out package.tgz && cd ../../test/apps/nextjs-app-router && rm -rf node_modules && yarn cache clean --all && yarn install && yarn dev +``` + +### Step by step + +```bash +# 1. Build and pack rum-react (from repo root) +cd packages/rum-react +yarn build +yarn pack --out package.tgz + +# 2. Reinstall in test app +cd ../../test/apps/nextjs-app-router +rm -rf node_modules +yarn cache clean --all +yarn install + +# 3. Start dev server +yarn dev +``` + +App available at http://localhost:3000 + +## Test Routes + +- `/` - Home page +- `/user/42` - Dynamic route (normalizes to `/user/:id`) +- `/tracked` - Component tracking demo +- `/error-test` - Error boundary testing + +## E2E Tests + +E2E tests are in `test/e2e/scenario/nextjs.scenario.ts`. + +### Running E2E Tests + +```bash +# From repo root - starts both dev servers automatically +yarn test:e2e -g "nextjs" +``` + +The Playwright config automatically starts the Next.js dev server on port 3000. diff --git a/test/apps/nextjs-app-router/app/ddprovider.tsx b/test/apps/nextjs-app-router/app/ddprovider.tsx new file mode 100644 index 0000000000..a23c91ab49 --- /dev/null +++ b/test/apps/nextjs-app-router/app/ddprovider.tsx @@ -0,0 +1,22 @@ +'use client' + +import { useEffect, type ReactNode } from 'react' +import { DatadogRumProvider } from '@datadog/browser-rum-react/nextjs' +import { datadogRum } from '@datadog/browser-rum' +import { reactPlugin } from '@datadog/browser-rum-react' + +export function DatadogProvider({ children }: { children: ReactNode }) { + useEffect(() => { + const config = (window as any).RUM_CONFIGURATION + if (!config) { + return + } + + datadogRum.init({ + ...config, + plugins: [reactPlugin({ nextjs: true })], + }) + }, []) + + return {children} +} diff --git a/test/apps/nextjs-app-router/app/error-test/page.tsx b/test/apps/nextjs-app-router/app/error-test/page.tsx new file mode 100644 index 0000000000..d4e877bf09 --- /dev/null +++ b/test/apps/nextjs-app-router/app/error-test/page.tsx @@ -0,0 +1,61 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' +import { ErrorBoundary } from '@datadog/browser-rum-react' + +function ComponentWithErrorButton() { + const [shouldError, setShouldError] = useState(false) + + if (shouldError) { + throw new Error('Error triggered by button click') + } + + return ( +
+

Click button to trigger error

+ +
+ ) +} + +export default function ErrorTestPage() { + const throwAsyncError = async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + throw new Error('Test asynchronous error') + } + + const throwUnhandledRejection = () => { + Promise.reject(new Error('Test unhandled promise rejection')) + } + + return ( +
+ ← Back to Home +

Error Testing

+ +
+

Error Boundary (Datadog RUM)

+ ( +
+

Something went wrong

+

{error.message}

+ +
+ )} + > + +
+
+ +
+

Other Error Types

+ + +
+
+ ) +} diff --git a/test/apps/nextjs-app-router/app/layout.tsx b/test/apps/nextjs-app-router/app/layout.tsx new file mode 100644 index 0000000000..62d9de480f --- /dev/null +++ b/test/apps/nextjs-app-router/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next' +import { DatadogProvider } from './ddprovider' + +export const metadata: Metadata = { + title: 'Next.js App Router Test', + description: 'Test app for Datadog RUM Next.js integration', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + +
{children}
+
+ + + ) +} diff --git a/test/apps/nextjs-app-router/app/page.tsx b/test/apps/nextjs-app-router/app/page.tsx new file mode 100644 index 0000000000..22699faf36 --- /dev/null +++ b/test/apps/nextjs-app-router/app/page.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link' + +export default function HomePage() { + return ( +
+

Home

+
    +
  • + Go to User 42 +
  • +
  • + Go to User 123 +
  • +
  • + Go to Tracked Component +
  • +
  • + Go to Error Test +
  • +
+
+ ) +} diff --git a/test/apps/nextjs-app-router/app/tracked/page.tsx b/test/apps/nextjs-app-router/app/tracked/page.tsx new file mode 100644 index 0000000000..12401a8b48 --- /dev/null +++ b/test/apps/nextjs-app-router/app/tracked/page.tsx @@ -0,0 +1,16 @@ +'use client' + +import Link from 'next/link' +import { UNSTABLE_ReactComponentTracker as ReactComponentTracker } from '@datadog/browser-rum-react' + +export default function TrackedPage() { + return ( +
+ ← Back to Home + +

Component Tracker

+

This component is tracked for performance metrics.

+
+
+ ) +} diff --git a/test/apps/nextjs-app-router/app/user/[id]/page.tsx b/test/apps/nextjs-app-router/app/user/[id]/page.tsx new file mode 100644 index 0000000000..5a496c9ba4 --- /dev/null +++ b/test/apps/nextjs-app-router/app/user/[id]/page.tsx @@ -0,0 +1,13 @@ +import Link from 'next/link' + +export default async function UserPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + + return ( +
+ ← Back to Home +

User {id}

+

This is a dynamic route testing view name normalization.

+
+ ) +} diff --git a/test/apps/nextjs-app-router/next-env.d.ts b/test/apps/nextjs-app-router/next-env.d.ts new file mode 100644 index 0000000000..c4b7818fbb --- /dev/null +++ b/test/apps/nextjs-app-router/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/test/apps/nextjs-app-router/next.config.js b/test/apps/nextjs-app-router/next.config.js new file mode 100644 index 0000000000..767719fc4f --- /dev/null +++ b/test/apps/nextjs-app-router/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/apps/nextjs-app-router/package.json b/test/apps/nextjs-app-router/package.json new file mode 100644 index 0000000000..b342c95c88 --- /dev/null +++ b/test/apps/nextjs-app-router/package.json @@ -0,0 +1,34 @@ +{ + "name": "nextjs-app-router", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "next start --port 3000" + }, + "dependencies": { + "@datadog/browser-rum": "file:../../../packages/rum/package.tgz", + "@datadog/browser-rum-react": "file:../../../packages/rum-react/package.tgz", + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "resolutions": { + "@datadog/browser-rum-core": "file:../../../packages/rum-core/package.tgz", + "@datadog/browser-core": "file:../../../packages/core/package.tgz", + "@datadog/browser-rum": "file:../../../packages/rum/package.tgz", + "@datadog/browser-rum-react": "file:../../../packages/rum-react/package.tgz", + "@datadog/browser-rum-slim": "file:../../../packages/rum-slim/package.tgz", + "@datadog/browser-worker": "file:../../../packages/worker/package.tgz" + }, + "devDependencies": { + "@types/node": "22.16.0", + "@types/react": "19.2.8", + "@types/react-dom": "19.2.3", + "typescript": "5.9.3" + }, + "volta": { + "extends": "../../../package.json" + } +} diff --git a/test/apps/nextjs-app-router/tsconfig.json b/test/apps/nextjs-app-router/tsconfig.json new file mode 100644 index 0000000000..f22e40d712 --- /dev/null +++ b/test/apps/nextjs-app-router/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + }, + "target": "ES2017" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/test/apps/nextjs-app-router/yarn.lock b/test/apps/nextjs-app-router/yarn.lock new file mode 100644 index 0000000000..f1ea97bef1 --- /dev/null +++ b/test/apps/nextjs-app-router/yarn.lock @@ -0,0 +1,715 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@datadog/browser-core@file:../../../packages/core/package.tgz::locator=nextjs-app-router%40workspace%3A.": + version: 6.26.0 + resolution: "@datadog/browser-core@file:../../../packages/core/package.tgz#../../../packages/core/package.tgz::hash=187b55&locator=nextjs-app-router%40workspace%3A." + checksum: 10c0/19ac48eba81c641b6c6598e2a3d9d0533ff1f422d26f563ed984888c8ab488e73127f2877942fc0816eec3fb2e91eca0fadf97f8ecaf9af914b7ea0591a96c68 + languageName: node + linkType: hard + +"@datadog/browser-rum-core@file:../../../packages/rum-core/package.tgz::locator=nextjs-app-router%40workspace%3A.": + version: 6.26.0 + resolution: "@datadog/browser-rum-core@file:../../../packages/rum-core/package.tgz#../../../packages/rum-core/package.tgz::hash=ab8181&locator=nextjs-app-router%40workspace%3A." + dependencies: + "@datadog/browser-core": "npm:6.26.0" + checksum: 10c0/24d0d5207c6df687bf9caae795a8598136c3788daa96bb4c321b30de9838b126dfec418df3cdbbbcf3fa2608b47e552581dfe3d0c1134811c98c306891b343d7 + languageName: node + linkType: hard + +"@datadog/browser-rum-react@file:../../../packages/rum-react/package.tgz::locator=nextjs-app-router%40workspace%3A.": + version: 6.26.0 + resolution: "@datadog/browser-rum-react@file:../../../packages/rum-react/package.tgz#../../../packages/rum-react/package.tgz::hash=f71519&locator=nextjs-app-router%40workspace%3A." + dependencies: + "@datadog/browser-core": "npm:6.26.0" + "@datadog/browser-rum-core": "npm:6.26.0" + peerDependencies: + next: ">=13" + react: 18 || 19 + react-router: 6 || 7 + react-router-dom: 6 || 7 + peerDependenciesMeta: + "@datadog/browser-rum": + optional: true + "@datadog/browser-rum-slim": + optional: true + next: + optional: true + react: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + checksum: 10c0/c94598ac8926ba88d9f6a06bb347ee42b8e2615372a54c6a8405dfa9c8bb93d77ded7d51433bd804882d9814c357c4f5003dabf4a022eddf6e91bc39d7723a57 + languageName: node + linkType: hard + +"@datadog/browser-rum@file:../../../packages/rum/package.tgz::locator=nextjs-app-router%40workspace%3A.": + version: 6.26.0 + resolution: "@datadog/browser-rum@file:../../../packages/rum/package.tgz#../../../packages/rum/package.tgz::hash=8b8895&locator=nextjs-app-router%40workspace%3A." + dependencies: + "@datadog/browser-core": "npm:6.26.0" + "@datadog/browser-rum-core": "npm:6.26.0" + peerDependencies: + "@datadog/browser-logs": 6.26.0 + peerDependenciesMeta: + "@datadog/browser-logs": + optional: true + checksum: 10c0/2f8f6fa898bb2d9a6ea3efe35025c09b9e865ecf3fe7bab983f0e8df6d296778662fb4da8df0d396b37e01b723cf38af03443dd3982323bef12d3658b9e8be42 + languageName: node + linkType: hard + +"@emnapi/runtime@npm:^1.7.0": + version: 1.8.1 + resolution: "@emnapi/runtime@npm:1.8.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/f4929d75e37aafb24da77d2f58816761fe3f826aad2e37fa6d4421dac9060cbd5098eea1ac3c9ecc4526b89deb58153852fa432f87021dc57863f2ff726d713f + languageName: node + linkType: hard + +"@img/colour@npm:^1.0.0": + version: 1.0.0 + resolution: "@img/colour@npm:1.0.0" + checksum: 10c0/02261719c1e0d7aa5a2d585981954f2ac126f0c432400aa1a01b925aa2c41417b7695da8544ee04fd29eba7ecea8eaf9b8bef06f19dc8faba78f94eeac40667d + languageName: node + linkType: hard + +"@img/sharp-darwin-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm@npm:1.2.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-ppc64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-riscv64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-riscv64@npm:1.2.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-ppc64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-ppc64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-ppc64": + optional: true + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-riscv64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-riscv64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-riscv64": + optional: true + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-s390x@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-wasm32@npm:0.34.5" + dependencies: + "@emnapi/runtime": "npm:^1.7.0" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-arm64@npm:0.34.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-ia32@npm:0.34.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-x64@npm:0.34.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@next/env@npm:16.1.6": + version: 16.1.6 + resolution: "@next/env@npm:16.1.6" + checksum: 10c0/ed7023edb94b9b2e5da3f9c99d08b614da9757c1edd0ecec792fce4d336b4f0c64db1a84955e07cfbd848b9e61c4118fff28f4098cd7b0a7f97814a90565ebe6 + languageName: node + linkType: hard + +"@next/swc-darwin-arm64@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-darwin-arm64@npm:16.1.6" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-darwin-x64@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-darwin-x64@npm:16.1.6" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@next/swc-linux-arm64-gnu@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-arm64-gnu@npm:16.1.6" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-arm64-musl@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-arm64-musl@npm:16.1.6" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-linux-x64-gnu@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-x64-gnu@npm:16.1.6" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-x64-musl@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-x64-musl@npm:16.1.6" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-win32-arm64-msvc@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-win32-arm64-msvc@npm:16.1.6" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-win32-x64-msvc@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-win32-x64-msvc@npm:16.1.6" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/helpers@npm:0.5.15": + version: 0.5.15 + resolution: "@swc/helpers@npm:0.5.15" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10c0/33002f74f6f885f04c132960835fdfc474186983ea567606db62e86acd0680ca82f34647e8e610f4e1e422d1c16fce729dde22cd3b797ab1fd9061a825dabca4 + languageName: node + linkType: hard + +"@types/node@npm:22.16.0": + version: 22.16.0 + resolution: "@types/node@npm:22.16.0" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10c0/6219b521062f6c38d4d85ebd25807bd7f2bc703a5acba24e2c6716938d9d6cefd6fafd7b5156f61580eb58a0d82e8921751b778655675389631d813e5f261c03 + languageName: node + linkType: hard + +"@types/react-dom@npm:19.2.3": + version: 19.2.3 + resolution: "@types/react-dom@npm:19.2.3" + peerDependencies: + "@types/react": ^19.2.0 + checksum: 10c0/b486ebe0f4e2fb35e2e108df1d8fc0927ca5d6002d5771e8a739de11239fe62d0e207c50886185253c99eb9dedfeeb956ea7429e5ba17f6693c7acb4c02f8cd1 + languageName: node + linkType: hard + +"@types/react@npm:19.2.8": + version: 19.2.8 + resolution: "@types/react@npm:19.2.8" + dependencies: + csstype: "npm:^3.2.2" + checksum: 10c0/832834998c4ee971fca72ecf1eb95dc924ad3931a2112c687a4dae498aabd115c5fa4db09186853e34a646226b0223808c8f867df03d17601168f9cf119448de + languageName: node + linkType: hard + +"baseline-browser-mapping@npm:^2.8.3": + version: 2.9.19 + resolution: "baseline-browser-mapping@npm:2.9.19" + bin: + baseline-browser-mapping: dist/cli.js + checksum: 10c0/569928db78bcd081953d7db79e4243a59a579a34b4ae1806b9b42d3b7f84e5bc40e6e82ae4fa06e7bef8291bf747b33b3f9ef5d3c6e1e420cb129d9295536129 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001579": + version: 1.0.30001768 + resolution: "caniuse-lite@npm:1.0.30001768" + checksum: 10c0/16808cb39f9563098deab6d45bcd0642a79fc5ace8dbcea8106b008b179820353e3ec089ed7e54f1f3c8bb84f2c2835b451f308212d8f36c2b7942f879e91955 + languageName: node + linkType: hard + +"client-only@npm:0.0.1": + version: 0.0.1 + resolution: "client-only@npm:0.0.1" + checksum: 10c0/9d6cfd0c19e1c96a434605added99dff48482152af791ec4172fb912a71cff9027ff174efd8cdb2160cc7f377543e0537ffc462d4f279bc4701de3f2a3c4b358 + languageName: node + linkType: hard + +"csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce + languageName: node + linkType: hard + +"detect-libc@npm:^2.1.2": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + +"nanoid@npm:^3.3.6": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" + bin: + nanoid: bin/nanoid.cjs + checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b + languageName: node + linkType: hard + +"next@npm:16.1.6": + version: 16.1.6 + resolution: "next@npm:16.1.6" + dependencies: + "@next/env": "npm:16.1.6" + "@next/swc-darwin-arm64": "npm:16.1.6" + "@next/swc-darwin-x64": "npm:16.1.6" + "@next/swc-linux-arm64-gnu": "npm:16.1.6" + "@next/swc-linux-arm64-musl": "npm:16.1.6" + "@next/swc-linux-x64-gnu": "npm:16.1.6" + "@next/swc-linux-x64-musl": "npm:16.1.6" + "@next/swc-win32-arm64-msvc": "npm:16.1.6" + "@next/swc-win32-x64-msvc": "npm:16.1.6" + "@swc/helpers": "npm:0.5.15" + baseline-browser-mapping: "npm:^2.8.3" + caniuse-lite: "npm:^1.0.30001579" + postcss: "npm:8.4.31" + sharp: "npm:^0.34.4" + styled-jsx: "npm:5.1.6" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.51.1 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + sharp: + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 10c0/543766bf879bb5a5d454dc18cb302953270a92efba1d01dd028ea83c64b69573ce7d6e6c3759ecbaabec0a84131b0237263c24d1ccd7c8a97205e776dcd34e0b + languageName: node + linkType: hard + +"nextjs-app-router@workspace:.": + version: 0.0.0-use.local + resolution: "nextjs-app-router@workspace:." + dependencies: + "@datadog/browser-rum": "file:../../../packages/rum/package.tgz" + "@datadog/browser-rum-react": "file:../../../packages/rum-react/package.tgz" + "@types/node": "npm:22.16.0" + "@types/react": "npm:19.2.8" + "@types/react-dom": "npm:19.2.3" + next: "npm:16.1.6" + react: "npm:19.2.3" + react-dom: "npm:19.2.3" + typescript: "npm:5.9.3" + languageName: unknown + linkType: soft + +"picocolors@npm:^1.0.0": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 + languageName: node + linkType: hard + +"postcss@npm:8.4.31": + version: 8.4.31 + resolution: "postcss@npm:8.4.31" + dependencies: + nanoid: "npm:^3.3.6" + picocolors: "npm:^1.0.0" + source-map-js: "npm:^1.0.2" + checksum: 10c0/748b82e6e5fc34034dcf2ae88ea3d11fd09f69b6c50ecdd3b4a875cfc7cdca435c958b211e2cb52355422ab6fccb7d8f2f2923161d7a1b281029e4a913d59acf + languageName: node + linkType: hard + +"react-dom@npm:19.2.3": + version: 19.2.3 + resolution: "react-dom@npm:19.2.3" + dependencies: + scheduler: "npm:^0.27.0" + peerDependencies: + react: ^19.2.3 + checksum: 10c0/dc43f7ede06f46f3acc16ee83107c925530de9b91d1d0b3824583814746ff4c498ea64fd65cd83aba363205268adff52e2827c582634ae7b15069deaeabc4892 + languageName: node + linkType: hard + +"react@npm:19.2.3": + version: 19.2.3 + resolution: "react@npm:19.2.3" + checksum: 10c0/094220b3ba3a76c1b668f972ace1dd15509b157aead1b40391d1c8e657e720c201d9719537375eff08f5e0514748c0319063392a6f000e31303aafc4471f1436 + languageName: node + linkType: hard + +"scheduler@npm:^0.27.0": + version: 0.27.0 + resolution: "scheduler@npm:0.27.0" + checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 + languageName: node + linkType: hard + +"semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e + languageName: node + linkType: hard + +"sharp@npm:^0.34.4": + version: 0.34.5 + resolution: "sharp@npm:0.34.5" + dependencies: + "@img/colour": "npm:^1.0.0" + "@img/sharp-darwin-arm64": "npm:0.34.5" + "@img/sharp-darwin-x64": "npm:0.34.5" + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + "@img/sharp-linux-arm": "npm:0.34.5" + "@img/sharp-linux-arm64": "npm:0.34.5" + "@img/sharp-linux-ppc64": "npm:0.34.5" + "@img/sharp-linux-riscv64": "npm:0.34.5" + "@img/sharp-linux-s390x": "npm:0.34.5" + "@img/sharp-linux-x64": "npm:0.34.5" + "@img/sharp-linuxmusl-arm64": "npm:0.34.5" + "@img/sharp-linuxmusl-x64": "npm:0.34.5" + "@img/sharp-wasm32": "npm:0.34.5" + "@img/sharp-win32-arm64": "npm:0.34.5" + "@img/sharp-win32-ia32": "npm:0.34.5" + "@img/sharp-win32-x64": "npm:0.34.5" + detect-libc: "npm:^2.1.2" + semver: "npm:^7.7.3" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-ppc64": + optional: true + "@img/sharp-libvips-linux-riscv64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-ppc64": + optional: true + "@img/sharp-linux-riscv64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-arm64": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/fd79e29df0597a7d5704b8461c51f944ead91a5243691697be6e8243b966402beda53ddc6f0a53b96ea3cb8221f0b244aa588114d3ebf8734fb4aefd41ab802f + languageName: node + linkType: hard + +"source-map-js@npm:^1.0.2": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf + languageName: node + linkType: hard + +"styled-jsx@npm:5.1.6": + version: 5.1.6 + resolution: "styled-jsx@npm:5.1.6" + dependencies: + client-only: "npm:0.0.1" + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 10c0/ace50e7ea5ae5ae6a3b65a50994c51fca6ae7df9c7ecfd0104c36be0b4b3a9c5c1a2374d16e2a11e256d0b20be6d47256d768ecb4f91ab390f60752a075780f5 + languageName: node + linkType: hard + +"tslib@npm:^2.4.0, tslib@npm:^2.8.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + +"typescript@npm:5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A5.9.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 + languageName: node + linkType: hard + +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 + languageName: node + linkType: hard diff --git a/test/e2e/lib/framework/createTest.ts b/test/e2e/lib/framework/createTest.ts index 25b32dc831..5041cdc6f8 100644 --- a/test/e2e/lib/framework/createTest.ts +++ b/test/e2e/lib/framework/createTest.ts @@ -9,6 +9,7 @@ import { BrowserLogsManager, deleteAllCookies, getBrowserName, sendXhr } from '. import { DEFAULT_LOGS_CONFIGURATION, DEFAULT_RUM_CONFIGURATION } from '../helpers/configuration' import { validateRumFormat } from '../helpers/validation' import type { BrowserConfiguration } from '../../../browsers.conf' +import { NEXTJS_APP_URL } from '../helpers/playwright' import { IntakeRegistry } from './intakeRegistry' import { flushEvents } from './flushEvents' import type { Servers } from './httpServers' @@ -43,6 +44,7 @@ interface TestContext { deleteAllCookies: () => Promise sendXhr: (url: string, headers?: string[][]) => Promise interactWithWorker: (cb: (worker: ServiceWorker) => void) => Promise + isNextjsApp: boolean } type TestRunner = (testContext: TestContext) => Promise | void @@ -64,6 +66,7 @@ class TestBuilder { } = {} private useServiceWorker: boolean = false private hostName?: string + private nextjsApp = false constructor(private title: string) {} @@ -112,6 +115,12 @@ class TestBuilder { return this } + withNextjsApp() { + this.nextjsApp = true + this.setups = [{ factory: () => '' }] + return this + } + withBasePath(newBasePath: string) { this.basePath = newBasePath return this @@ -194,6 +203,7 @@ class TestBuilder { testFixture: this.testFixture, extension: this.extension, hostName: this.hostName, + nextjsApp: this.nextjsApp, } if (this.alsoRunWithRumSlim) { @@ -269,11 +279,14 @@ function declareTest(title: string, setupOptions: SetupOptions, factory: SetupFa const testContext = createTestContext(servers, page, context, browserLogs, browserName, setupOptions) servers.intake.bindServerApp(createIntakeServerApp(testContext.intakeRegistry)) - const setup = factory(setupOptions, servers) - servers.base.bindServerApp(createMockServerApp(servers, setup, setupOptions.remoteConfiguration)) - servers.crossOrigin.bindServerApp(createMockServerApp(servers, setup)) + // Next.js runs on its own server, only set up mock server for other test apps + if (!setupOptions.nextjsApp) { + const setup = factory(setupOptions, servers) + servers.base.bindServerApp(createMockServerApp(servers, setup, setupOptions.remoteConfiguration)) + servers.crossOrigin.bindServerApp(createMockServerApp(servers, setup)) + } - await setUpTest(browserLogs, testContext) + await setUpTest(browserLogs, testContext, setupOptions, servers) try { await runner(testContext) @@ -290,12 +303,17 @@ function createTestContext( browserContext: BrowserContext, browserLogsManager: BrowserLogsManager, browserName: TestContext['browserName'], - { basePath, hostName }: SetupOptions + { basePath, hostName, nextjsApp }: SetupOptions ): TestContext { - const baseUrl = new URL(basePath, servers.base.origin) + let baseUrl: URL - if (hostName) { - baseUrl.hostname = hostName + if (nextjsApp) { + baseUrl = new URL(basePath, NEXTJS_APP_URL) + } else { + baseUrl = new URL(basePath, servers.base.origin) + if (hostName) { + baseUrl.hostname = hostName + } } return { @@ -305,6 +323,7 @@ function createTestContext( page, browserContext, browserName, + isNextjsApp: !!nextjsApp, withBrowserLogs: (cb: (logs: BrowserLog[]) => void) => { try { cb(browserLogsManager.get()) @@ -316,7 +335,7 @@ function createTestContext( await page.evaluate(`(${cb.toString()})(window.myServiceWorker.active)`) }, flushBrowserLogs: () => browserLogsManager.clear(), - flushEvents: () => flushEvents(page), + flushEvents: () => flushEvents(page, nextjsApp ? NEXTJS_APP_URL : undefined), deleteAllCookies: () => deleteAllCookies(browserContext), sendXhr: (url: string, headers?: string[][]) => sendXhr(page, url, headers), getExtensionId: async () => { @@ -331,7 +350,12 @@ function createTestContext( } } -async function setUpTest(browserLogsManager: BrowserLogsManager, { baseUrl, page, browserContext }: TestContext) { +async function setUpTest( + browserLogsManager: BrowserLogsManager, + { baseUrl, page, browserContext }: TestContext, + setupOptions: SetupOptions, + servers: Servers +) { browserContext.on('console', (msg) => { browserLogsManager.add({ level: msg.type() as BrowserLog['level'], @@ -350,6 +374,20 @@ async function setUpTest(browserLogsManager: BrowserLogsManager, { baseUrl, page }) }) + // For Next.js apps, inject RUM configuration before navigation + if (setupOptions.nextjsApp && setupOptions.rum) { + const rumConfig = { + ...setupOptions.rum, + proxy: servers.intake.origin, + } + await page.addInitScript( + ({ config }) => { + ;(window as any).RUM_CONFIGURATION = config + }, + { config: rumConfig } + ) + } + await page.goto(baseUrl) await waitForServersIdle() } diff --git a/test/e2e/lib/framework/flushEvents.ts b/test/e2e/lib/framework/flushEvents.ts index 7567d42116..1a30c26e4c 100644 --- a/test/e2e/lib/framework/flushEvents.ts +++ b/test/e2e/lib/framework/flushEvents.ts @@ -2,7 +2,7 @@ import type { Page } from '@playwright/test' import { getTestServers, waitForServersIdle } from './httpServers' import { waitForRequests } from './waitForRequests' -export async function flushEvents(page: Page) { +export async function flushEvents(page: Page, gotoUrl?: string) { await waitForRequests(page) const servers = await getTestServers() @@ -21,6 +21,6 @@ export async function flushEvents(page: Page) { // The issue mainly occurs with local e2e tests (not browserstack), because the network latency is // very low (same machine), so the request resolves very quickly. In real life conditions, this // issue is mitigated, because requests will likely take a few milliseconds to reach the server. - await page.goto(`${servers.base.origin}/ok?duration=200`) + await page.goto(gotoUrl ?? `${servers.base.origin}/ok?duration=200`) await waitForServersIdle() } diff --git a/test/e2e/lib/framework/pageSetups.ts b/test/e2e/lib/framework/pageSetups.ts index 3a47a8ac51..074d3b32a3 100644 --- a/test/e2e/lib/framework/pageSetups.ts +++ b/test/e2e/lib/framework/pageSetups.ts @@ -27,6 +27,7 @@ export interface SetupOptions { logsConfiguration?: LogsInitConfiguration } hostName?: string + nextjsApp?: boolean } export interface WorkerOptions { diff --git a/test/e2e/lib/helpers/playwright.ts b/test/e2e/lib/helpers/playwright.ts index a6affa5973..657eabdabf 100644 --- a/test/e2e/lib/helpers/playwright.ts +++ b/test/e2e/lib/helpers/playwright.ts @@ -4,6 +4,7 @@ import { getBuildInfos } from '../../../envUtils.ts' import packageJson from '../../../../package.json' with { type: 'json' } export const DEV_SERVER_BASE_URL = 'http://localhost:8080' +export const NEXTJS_APP_URL = 'http://localhost:3000' export function getPlaywrightConfigBrowserName(name: string): PlaywrightWorkerOptions['browserName'] { if (name.includes('firefox')) { diff --git a/test/e2e/playwright.base.config.ts b/test/e2e/playwright.base.config.ts index 9bc8fb1a0d..e0743b58cf 100644 --- a/test/e2e/playwright.base.config.ts +++ b/test/e2e/playwright.base.config.ts @@ -1,7 +1,7 @@ import path from 'path' import type { ReporterDescription, Config } from '@playwright/test' import { getTestReportDirectory } from '../envUtils' -import { DEV_SERVER_BASE_URL } from './lib/helpers/playwright' +import { DEV_SERVER_BASE_URL, NEXTJS_APP_URL } from './lib/helpers/playwright' const isCi = !!process.env.CI const isLocal = !isCi @@ -32,12 +32,21 @@ export const config: Config = { }, webServer: isLocal - ? { - stdout: 'pipe', - cwd: path.join(__dirname, '../..'), - command: 'yarn dev', - url: DEV_SERVER_BASE_URL, - reuseExistingServer: true, - } + ? [ + { + stdout: 'pipe', + cwd: path.join(__dirname, '../..'), + command: 'yarn dev', + url: DEV_SERVER_BASE_URL, + reuseExistingServer: true, + }, + { + stdout: 'pipe', + cwd: path.join(__dirname, '../apps/nextjs-app-router'), + command: 'yarn dev', + url: NEXTJS_APP_URL, + reuseExistingServer: true, + }, + ] : undefined, } diff --git a/test/e2e/scenario/nextjs.scenario.ts b/test/e2e/scenario/nextjs.scenario.ts new file mode 100644 index 0000000000..b788c3ea42 --- /dev/null +++ b/test/e2e/scenario/nextjs.scenario.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test' +import { createTest } from '../lib/framework' + +test.describe('nextjs app router', () => { + createTest('should normalize dynamic route to /user/:id') + .withRum() + .withNextjsApp() + .run(async ({ page, flushEvents, intakeRegistry }) => { + await page.click('text=Go to User 42') + await page.waitForURL('**/user/42') + + await page.click('text=Back to Home') + await page.waitForURL('**/localhost:3000/') + + await flushEvents() + + const viewEvents = intakeRegistry.rumViewEvents + expect(viewEvents.length).toBeGreaterThanOrEqual(2) + + const homeView = viewEvents.find((e) => e.view.name === '/') + expect(homeView).toBeDefined() + + const userView = viewEvents.find((e) => e.view.name === '/user/:id') + expect(userView).toBeDefined() + expect(userView?.view.loading_type).toBe('route_change') + }) + + createTest('should send a react component render vital event') + .withRum() + .withNextjsApp() + .run(async ({ page, flushEvents, intakeRegistry }) => { + await page.click('text=Go to Tracked Component') + await page.waitForURL('**/tracked') + + await page.click('text=Back to Home') + await page.waitForURL('**/localhost:3000/') + + await flushEvents() + + const vitalEvents = intakeRegistry.rumVitalEvents + expect(vitalEvents.length).toBeGreaterThan(0) + + const trackedVital = vitalEvents.find((e) => e.vital.description === 'TrackedPage') + expect(trackedVital).toBeDefined() + expect(trackedVital?.vital.duration).toEqual(expect.any(Number)) + }) + + createTest('should capture react error from error boundary') + .withRum() + .withNextjsApp() + .run(async ({ page, flushEvents, intakeRegistry, withBrowserLogs }) => { + await page.click('text=Go to Error Test') + await page.waitForURL('**/error-test') + await page.click('#error-button') + + await page.click('text=Back to Home') + await page.waitForURL('**/localhost:3000/') + + await flushEvents() + + const errorEvents = intakeRegistry.rumErrorEvents + expect(errorEvents.length).toBeGreaterThan(0) + + const boundaryError = errorEvents.find((e) => e.error.message?.includes('Error triggered by button')) + expect(boundaryError).toBeDefined() + expect(boundaryError?.error.source).toBe('custom') + expect(boundaryError?.context?.framework).toBe('react') + expect(boundaryError?.error.component_stack).toBeDefined() + + withBrowserLogs((browserLogs) => { + expect(browserLogs.length).toBeGreaterThan(0) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 5fb112aef6..abe7bfe03b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -330,12 +330,14 @@ __metadata: "@datadog/browser-rum-core": "npm:6.26.0" "@types/react": "npm:19.2.8" "@types/react-dom": "npm:19.2.3" + next: "npm:16.1.6" react: "npm:19.2.3" react-dom: "npm:19.2.3" react-router: "npm:7.12.0" react-router-dom: "npm:7.12.0" react-router-dom-6: "npm:react-router-dom@6.30.3" peerDependencies: + next: ">=13" react: 18 || 19 react-router: 6 || 7 react-router-dom: 6 || 7 @@ -344,6 +346,8 @@ __metadata: optional: true "@datadog/browser-rum-slim": optional: true + next: + optional: true react: optional: true react-router: @@ -471,6 +475,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.7.0": + version: 1.8.1 + resolution: "@emnapi/runtime@npm:1.8.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/f4929d75e37aafb24da77d2f58816761fe3f826aad2e37fa6d4421dac9060cbd5098eea1ac3c9ecc4526b89deb58153852fa432f87021dc57863f2ff726d713f + languageName: node + linkType: hard + "@emnapi/wasi-threads@npm:1.1.0": version: 1.1.0 resolution: "@emnapi/wasi-threads@npm:1.1.0" @@ -873,6 +886,233 @@ __metadata: languageName: node linkType: hard +"@img/colour@npm:^1.0.0": + version: 1.0.0 + resolution: "@img/colour@npm:1.0.0" + checksum: 10c0/02261719c1e0d7aa5a2d585981954f2ac126f0c432400aa1a01b925aa2c41417b7695da8544ee04fd29eba7ecea8eaf9b8bef06f19dc8faba78f94eeac40667d + languageName: node + linkType: hard + +"@img/sharp-darwin-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm@npm:1.2.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-ppc64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-riscv64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-riscv64@npm:1.2.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-ppc64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-ppc64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-ppc64": + optional: true + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-riscv64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-riscv64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-riscv64": + optional: true + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-s390x@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-wasm32@npm:0.34.5" + dependencies: + "@emnapi/runtime": "npm:^1.7.0" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-arm64@npm:0.34.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-ia32@npm:0.34.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-x64@npm:0.34.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@inquirer/ansi@npm:^1.0.0, @inquirer/ansi@npm:^1.0.2": version: 1.0.2 resolution: "@inquirer/ansi@npm:1.0.2" @@ -1463,6 +1703,69 @@ __metadata: languageName: node linkType: hard +"@next/env@npm:16.1.6": + version: 16.1.6 + resolution: "@next/env@npm:16.1.6" + checksum: 10c0/ed7023edb94b9b2e5da3f9c99d08b614da9757c1edd0ecec792fce4d336b4f0c64db1a84955e07cfbd848b9e61c4118fff28f4098cd7b0a7f97814a90565ebe6 + languageName: node + linkType: hard + +"@next/swc-darwin-arm64@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-darwin-arm64@npm:16.1.6" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-darwin-x64@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-darwin-x64@npm:16.1.6" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@next/swc-linux-arm64-gnu@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-arm64-gnu@npm:16.1.6" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-arm64-musl@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-arm64-musl@npm:16.1.6" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-linux-x64-gnu@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-x64-gnu@npm:16.1.6" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-x64-musl@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-linux-x64-musl@npm:16.1.6" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-win32-arm64-msvc@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-win32-arm64-msvc@npm:16.1.6" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-win32-x64-msvc@npm:16.1.6": + version: 16.1.6 + resolution: "@next/swc-win32-x64-msvc@npm:16.1.6" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -2488,6 +2791,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:0.5.15": + version: 0.5.15 + resolution: "@swc/helpers@npm:0.5.15" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10c0/33002f74f6f885f04c132960835fdfc474186983ea567606db62e86acd0680ca82f34647e8e610f4e1e422d1c16fce729dde22cd3b797ab1fd9061a825dabca4 + languageName: node + linkType: hard + "@swc/types@npm:^0.1.25": version: 0.1.25 resolution: "@swc/types@npm:0.1.25" @@ -4077,6 +4389,15 @@ __metadata: languageName: node linkType: hard +"baseline-browser-mapping@npm:^2.8.3": + version: 2.9.19 + resolution: "baseline-browser-mapping@npm:2.9.19" + bin: + baseline-browser-mapping: dist/cli.js + checksum: 10c0/569928db78bcd081953d7db79e4243a59a579a34b4ae1806b9b42d3b7f84e5bc40e6e82ae4fa06e7bef8291bf747b33b3f9ef5d3c6e1e420cb129d9295536129 + languageName: node + linkType: hard + "baseline-browser-mapping@npm:^2.9.0": version: 2.9.7 resolution: "baseline-browser-mapping@npm:2.9.7" @@ -4556,6 +4877,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001579": + version: 1.0.30001767 + resolution: "caniuse-lite@npm:1.0.30001767" + checksum: 10c0/37067c6d2b26623f81494a1f206adbff2b80baed3318ba430684e428bd7d81be889f39db8ef081501d1db5f7dd5d15972892f173eb59c9f3bb780e0b38e6610a + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001759": version: 1.0.30001760 resolution: "caniuse-lite@npm:1.0.30001760" @@ -4802,6 +5130,13 @@ __metadata: languageName: node linkType: hard +"client-only@npm:0.0.1": + version: 0.0.1 + resolution: "client-only@npm:0.0.1" + checksum: 10c0/9d6cfd0c19e1c96a434605added99dff48482152af791ec4172fb912a71cff9027ff174efd8cdb2160cc7f377543e0537ffc462d4f279bc4701de3f2a3c4b358 + languageName: node + linkType: hard + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -5632,6 +5967,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.1.2": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + "detect-node-es@npm:^1.1.0": version: 1.1.0 resolution: "detect-node-es@npm:1.1.0" @@ -10408,7 +10750,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.11": +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6": version: 3.3.11 resolution: "nanoid@npm:3.3.11" bin: @@ -10461,6 +10803,66 @@ __metadata: languageName: node linkType: hard +"next@npm:16.1.6": + version: 16.1.6 + resolution: "next@npm:16.1.6" + dependencies: + "@next/env": "npm:16.1.6" + "@next/swc-darwin-arm64": "npm:16.1.6" + "@next/swc-darwin-x64": "npm:16.1.6" + "@next/swc-linux-arm64-gnu": "npm:16.1.6" + "@next/swc-linux-arm64-musl": "npm:16.1.6" + "@next/swc-linux-x64-gnu": "npm:16.1.6" + "@next/swc-linux-x64-musl": "npm:16.1.6" + "@next/swc-win32-arm64-msvc": "npm:16.1.6" + "@next/swc-win32-x64-msvc": "npm:16.1.6" + "@swc/helpers": "npm:0.5.15" + baseline-browser-mapping: "npm:^2.8.3" + caniuse-lite: "npm:^1.0.30001579" + postcss: "npm:8.4.31" + sharp: "npm:^0.34.4" + styled-jsx: "npm:5.1.6" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.51.1 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + sharp: + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 10c0/543766bf879bb5a5d454dc18cb302953270a92efba1d01dd028ea83c64b69573ce7d6e6c3759ecbaabec0a84131b0237263c24d1ccd7c8a97205e776dcd34e0b + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -11597,7 +11999,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.1.1": +"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -11772,6 +12174,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:8.4.31": + version: 8.4.31 + resolution: "postcss@npm:8.4.31" + dependencies: + nanoid: "npm:^3.3.6" + picocolors: "npm:^1.0.0" + source-map-js: "npm:^1.0.2" + checksum: 10c0/748b82e6e5fc34034dcf2ae88ea3d11fd09f69b6c50ecdd3b4a875cfc7cdca435c958b211e2cb52355422ab6fccb7d8f2f2923161d7a1b281029e4a913d59acf + languageName: node + linkType: hard + "postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -13148,6 +13561,90 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.34.4": + version: 0.34.5 + resolution: "sharp@npm:0.34.5" + dependencies: + "@img/colour": "npm:^1.0.0" + "@img/sharp-darwin-arm64": "npm:0.34.5" + "@img/sharp-darwin-x64": "npm:0.34.5" + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + "@img/sharp-linux-arm": "npm:0.34.5" + "@img/sharp-linux-arm64": "npm:0.34.5" + "@img/sharp-linux-ppc64": "npm:0.34.5" + "@img/sharp-linux-riscv64": "npm:0.34.5" + "@img/sharp-linux-s390x": "npm:0.34.5" + "@img/sharp-linux-x64": "npm:0.34.5" + "@img/sharp-linuxmusl-arm64": "npm:0.34.5" + "@img/sharp-linuxmusl-x64": "npm:0.34.5" + "@img/sharp-wasm32": "npm:0.34.5" + "@img/sharp-win32-arm64": "npm:0.34.5" + "@img/sharp-win32-ia32": "npm:0.34.5" + "@img/sharp-win32-x64": "npm:0.34.5" + detect-libc: "npm:^2.1.2" + semver: "npm:^7.7.3" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-ppc64": + optional: true + "@img/sharp-libvips-linux-riscv64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-ppc64": + optional: true + "@img/sharp-linux-riscv64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-arm64": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/fd79e29df0597a7d5704b8461c51f944ead91a5243691697be6e8243b966402beda53ddc6f0a53b96ea3cb8221f0b244aa588114d3ebf8734fb4aefd41ab802f + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -13369,7 +13866,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf @@ -13786,6 +14283,22 @@ __metadata: languageName: node linkType: hard +"styled-jsx@npm:5.1.6": + version: 5.1.6 + resolution: "styled-jsx@npm:5.1.6" + dependencies: + client-only: "npm:0.0.1" + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 10c0/ace50e7ea5ae5ae6a3b65a50994c51fca6ae7df9c7ecfd0104c36be0b4b3a9c5c1a2374d16e2a11e256d0b20be6d47256d768ecb4f91ab390f60752a075780f5 + languageName: node + linkType: hard + "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -14166,7 +14679,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62