Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clientTraceMetadata experimental option to propagate tracing data to the client #64256

Merged
merged 13 commits into from
May 15, 2024
1 change: 1 addition & 0 deletions packages/next-swc/crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ pub struct ExperimentalConfig {
gzip_size: Option<bool>,

instrumentation_hook: Option<bool>,
client_trace_metadata: Option<Vec<String>>,
large_page_data_bytes: Option<f64>,
logging: Option<serde_json::Value>,
memory_based_workers_count: Option<bool>,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2057,6 +2057,7 @@ export default async function getBaseWebpackConfig(
emotion: config.compiler?.emotion,
modularizeImports: config.modularizeImports,
imageLoaderFile: config.images.loaderFile,
clientTraceMetadata: config.experimental.clientTraceMetadata,
})

const cache: any = {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ export async function exportAppImpl(
deploymentId: nextConfig.deploymentId,
experimental: {
isAppPPREnabled: checkIsAppPPREnabled(nextConfig.experimental.ppr),
clientTraceMetadata: nextConfig.experimental.clientTraceMetadata,
swrDelta: nextConfig.experimental.swrDelta,
},
}
Expand Down
12 changes: 11 additions & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ import { appendMutableCookies } from '../web/spec-extension/adapters/request-coo
import { createServerInsertedHTML } from './server-inserted-html'
import { getRequiredScripts } from './required-scripts'
import { addPathPrefix } from '../../shared/lib/router/utils/add-path-prefix'
import { makeGetServerInsertedHTML } from './make-get-server-inserted-html'
import {
getTracedMetadata,
makeGetServerInsertedHTML,
} from './make-get-server-inserted-html'
import { walkTreeWithFlightRouterState } from './walk-tree-with-flight-router-state'
import { createComponentTree } from './create-component-tree'
import { getAssetQueryString } from './get-asset-query-string'
Expand Down Expand Up @@ -912,6 +915,11 @@ async function renderToHTMLOrFlightImpl(
tree,
formState,
}: RenderToStreamOptions): Promise<RenderToStreamResult> => {
const tracingMetadata = getTracedMetadata(
getTracer().getTracePropagationData(),
renderOpts.experimental.clientTraceMetadata
)

const polyfills: JSX.IntrinsicElements['script'][] =
buildManifest.polyfillFiles
.filter(
Expand Down Expand Up @@ -995,6 +1003,7 @@ async function renderToHTMLOrFlightImpl(
renderServerInsertedHTML,
serverCapturedErrors: allCapturedErrors,
basePath: renderOpts.basePath,
tracingMetadata: tracingMetadata,
})

const renderer = createStaticRenderer({
Expand Down Expand Up @@ -1319,6 +1328,7 @@ async function renderToHTMLOrFlightImpl(
renderServerInsertedHTML,
serverCapturedErrors: [],
basePath: renderOpts.basePath,
tracingMetadata: tracingMetadata,
}),
serverInsertedHTMLToHead: true,
validateRootLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,26 @@ import { renderToReadableStream } from 'react-dom/server.edge'
import { streamToString } from '../stream-utils/node-web-streams-helper'
import { RedirectStatusCode } from '../../client/components/redirect-status-code'
import { addPathPrefix } from '../../shared/lib/router/utils/add-path-prefix'
import type { ClientTraceDataEntry } from '../lib/trace/tracer'

export function getTracedMetadata(
traceData: ClientTraceDataEntry[],
clientTraceMetadata: string[] | undefined
): ClientTraceDataEntry[] | undefined {
if (!clientTraceMetadata) return undefined
return traceData.filter(({ key }) => clientTraceMetadata.includes(key))
}

export function makeGetServerInsertedHTML({
polyfills,
renderServerInsertedHTML,
serverCapturedErrors,
tracingMetadata,
basePath,
}: {
polyfills: JSX.IntrinsicElements['script'][]
renderServerInsertedHTML: () => React.ReactNode
tracingMetadata: ClientTraceDataEntry[] | undefined
serverCapturedErrors: Error[]
basePath: string
}) {
Expand Down Expand Up @@ -82,6 +93,17 @@ export function makeGetServerInsertedHTML({
})
}
{serverInsertedHTML}
{tracingMetadata
? tracingMetadata.map(({ key, value }) => {
return (
<meta
key={`next-trace-data-${key}:${value}`}
name={key}
content={value}
/>
)
})
: null}
{errorMetaTags}
</>,
{
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export interface RenderOptsPartial {
*/
isRoutePPREnabled?: boolean
swrDelta: SwrDelta | undefined
clientTraceMetadata: string[] | undefined
}
postponed?: string
/**
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ export default abstract class Server<
experimental: {
isAppPPREnabled,
swrDelta: this.nextConfig.experimental.swrDelta,
clientTraceMetadata: this.nextConfig.experimental.clientTraceMetadata,
},
}

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
optimizePackageImports: z.array(z.string()).optional(),
optimizeServerReact: z.boolean().optional(),
instrumentationHook: z.boolean().optional(),
clientTraceMetadata: z.array(z.string()).optional(),
turbotrace: z
.object({
logLevel: z
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ export interface ExperimentalConfig {
*/
instrumentationHook?: boolean

/**
* The array of the meta tags to the client injected by tracing propagation data.
*/
clientTraceMetadata?: string[]

/**
* Using this feature will enable the `react@experimental` for the `app` directory.
*/
Expand Down Expand Up @@ -919,6 +924,7 @@ export const defaultConfig: NextConfig = {
turbotrace: undefined,
typedRoutes: false,
instrumentationHook: false,
clientTraceMetadata: undefined,
parallelServerCompiles: false,
parallelServerBuildTraces: false,
ppr:
Expand Down
28 changes: 28 additions & 0 deletions packages/next/src/server/lib/trace/tracer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FetchEventResult } from '../../web/types'
import type { TextMapSetter } from '@opentelemetry/api'
import type { SpanTypes } from './constants'
import { LogSpanAllowList, NextVanillaSpanAllowlist } from './constants'

Expand Down Expand Up @@ -149,6 +150,12 @@ interface NextTracer {
* Returns undefined otherwise.
*/
getActiveScopeSpan(): Span | undefined

/**
* Returns trace propagation data for the currently active context. The format is equal to data provided
* through the OpenTelemetry propagator API.
*/
getTracePropagationData(): ClientTraceDataEntry[]
}

type NextAttributeNames =
Expand All @@ -171,6 +178,20 @@ const rootSpanIdKey = api.createContextKey('next.rootSpanId')
let lastSpanId = 0
const getSpanId = () => lastSpanId++

export interface ClientTraceDataEntry {
key: string
value: string
}

const clientTraceDataSetter: TextMapSetter<ClientTraceDataEntry[]> = {
set(carrier, key, value) {
carrier.push({
key,
value,
})
},
}

class NextTracerImpl implements NextTracer {
/**
* Returns an instance to the trace with configured name.
Expand All @@ -185,6 +206,13 @@ class NextTracerImpl implements NextTracer {
return context
}

public getTracePropagationData(): ClientTraceDataEntry[] {
const activeContext = context.active()
Copy link
Member

Choose a reason for hiding this comment

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

TBD, but you could check if the context is sampled and output nothing if it's not. The traceparent and similar have "sampled" flag. But usually non-sampled trace context is not helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In general, it is beneficial to propagate sampled and un-sampled contexts because telemetry systems often do traced-based sampling, where a tracing decision for a particular trace (usually decided on the first service) is propagated across the entire system, and the individual services of the system only report spans, if the incoming tracing decision is positive.

For example in the traceparent header there is a flag indicating whether the trace is sampled or not: https://www.w3.org/TR/trace-context/#sampled-flag If we didn't propagate the data when the trace is not sampled, the flag would not be needed in the first place.

Does that make sense? All in all this is good because it allows us to have less logic here. :)

Copy link
Member

Choose a reason for hiding this comment

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

That's what I meant - the absence of the trace can be used to mean the traceflags are 00. But I agree with the overall reasoning. You can keep this.

const entries: ClientTraceDataEntry[] = []
propagation.inject(activeContext, entries, clientTraceDataSetter)
return entries
}

public getActiveScopeSpan(): Span | undefined {
return trace.getSpan(context?.active())
}
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/opentelemetry/client-trace-metadata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# files generated by next.js
node_modules/
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const dynamic = 'force-dynamic'

export default function DynamicPage() {
return <h1 id="dynamic-page-header">Dynamic Page</h1>
}
7 changes: 7 additions & 0 deletions test/e2e/opentelemetry/client-trace-metadata/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function StaticPage() {
return <h1 id="static-page-2-header">Static Page 2</h1>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from 'next/link'

export default function StaticPage() {
return (
<>
<h1 id="static-page-header">Static Page</h1>
<Link href="/dynamic-page" id="go-to-dynamic-page">
Go to dynamic page
</Link>
<Link href="/static-page-2" id="go-to-static-page">
Go to static page
</Link>
</>
)
}
Loading
Loading