@wular/pnext 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
// OpenTelemetry span emission (COMPAT).
|
|
2
|
+
//
|
|
3
|
+
// Next's span taxonomy (~12 span types with exact next.span_type / next.span_name / attribute shapes).
|
|
4
|
+
// The root `BaseServer.handleRequest` span is opened in a request interceptor, its OTel Context stashed
|
|
5
|
+
// on the request WorkUnit, and closed in a responseFinalizer once the status is known. Child spans
|
|
6
|
+
// re-enter the stashed root context so they nest under the root - pnext threads parenting through its
|
|
7
|
+
// OWN work-unit ALS rather than relying on the app's ContextManager spanning our async boundaries.
|
|
8
|
+
//
|
|
9
|
+
// Everything is inert unless @opentelemetry/api resolves from the app AND a work unit is active.
|
|
10
|
+
|
|
11
|
+
import { getWorkUnit, queueAfterTask, type WorkUnit } from '../../request/context';
|
|
12
|
+
import { getOtelApi, type OtelApi, type OtelContext, type OtelSpan } from './api';
|
|
13
|
+
|
|
14
|
+
// next.span_type taxonomy (exact strings Next emits; asserted verbatim).
|
|
15
|
+
export const SPAN_TYPE = {
|
|
16
|
+
handleRequest: 'BaseServer.handleRequest',
|
|
17
|
+
getBodyResult: 'AppRender.getBodyResult',
|
|
18
|
+
fetch: 'AppRender.fetch',
|
|
19
|
+
runHandler: 'AppRouteRouteHandlers.runHandler',
|
|
20
|
+
generateMetadata: 'ResolveMetadata.generateMetadata',
|
|
21
|
+
createComponentTree: 'NextNodeServer.createComponentTree',
|
|
22
|
+
getLayoutOrPageModule: 'NextNodeServer.getLayoutOrPageModule',
|
|
23
|
+
findPageComponents: 'NextNodeServer.findPageComponents',
|
|
24
|
+
startResponse: 'NextNodeServer.startResponse',
|
|
25
|
+
clientComponentLoading: 'NextNodeServer.clientComponentLoading',
|
|
26
|
+
middleware: 'Middleware.execute',
|
|
27
|
+
getRequestHandler: 'NextServer.getRequestHandler',
|
|
28
|
+
nodeRunHandler: 'Node.runHandler',
|
|
29
|
+
renderDocument: 'Render.renderDocument',
|
|
30
|
+
getServerSideProps: 'Render.getServerSideProps',
|
|
31
|
+
getStaticProps: 'Render.getStaticProps',
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
// The runtime label the app's exporter reads at export time. Next's test exporter serializes
|
|
35
|
+
// `process.env.NEXT_RUNTIME` per span; for a route declared `runtime: 'edge'` Next runs the render in
|
|
36
|
+
// the edge sandbox, so its spans say 'edge' while node-server-side spans stay 'nodejs'. pnext runs
|
|
37
|
+
// everything in one Bun process, so the tracer mirrors the per-span runtime split by setting
|
|
38
|
+
// NEXT_RUNTIME around each span.end() - SimpleSpanProcessor exports synchronously inside end().
|
|
39
|
+
type SpanRuntime = 'edge' | 'nodejs';
|
|
40
|
+
|
|
41
|
+
// Span types Next emits from INSIDE the edge sandbox when the route is edge.
|
|
42
|
+
const EDGE_SANDBOX_SPAN_TYPES: ReadonlySet<string> = new Set([
|
|
43
|
+
SPAN_TYPE.getBodyResult,
|
|
44
|
+
SPAN_TYPE.createComponentTree,
|
|
45
|
+
SPAN_TYPE.getLayoutOrPageModule,
|
|
46
|
+
SPAN_TYPE.generateMetadata,
|
|
47
|
+
SPAN_TYPE.fetch,
|
|
48
|
+
SPAN_TYPE.renderDocument,
|
|
49
|
+
SPAN_TYPE.getServerSideProps,
|
|
50
|
+
SPAN_TYPE.getStaticProps,
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// Per-WorkUnit otel state (stashed on the compat scratch map).
|
|
54
|
+
const OTEL_STATE = Symbol.for('pnext.compat.otel.state');
|
|
55
|
+
|
|
56
|
+
interface OtelState {
|
|
57
|
+
api: OtelApi;
|
|
58
|
+
/** Context with the root span active — children re-enter this to nest. */
|
|
59
|
+
rootContext: OtelContext;
|
|
60
|
+
/** The root BaseServer.handleRequest span (closed by the finalizer). */
|
|
61
|
+
rootSpan: OtelSpan;
|
|
62
|
+
/** Guard so the root span is ended exactly once. */
|
|
63
|
+
rootEnded: boolean;
|
|
64
|
+
/** Request method, kept for the pattern-rename span name. */
|
|
65
|
+
method: string;
|
|
66
|
+
/** Whether this was an RSC request, kept for the pattern-rename span name. */
|
|
67
|
+
rsc: boolean;
|
|
68
|
+
/** The raw request target (`pathname + search`) the root span opened with. */
|
|
69
|
+
target: string;
|
|
70
|
+
/**
|
|
71
|
+
* The extracted INCOMING context (traceparent applied, no pnext span active). Top-level spans parent
|
|
72
|
+
* here, mirroring Next's spans that sit beside - not under - handleRequest.
|
|
73
|
+
*/
|
|
74
|
+
incomingContext: OtelContext;
|
|
75
|
+
/**
|
|
76
|
+
* Edge emulation mode for the matched route. 'page': the whole render tree
|
|
77
|
+
* exports as runtime 'edge' with the params-query target (Next renders edge
|
|
78
|
+
* pages inside the sandbox). 'handler': only the runHandler span is 'edge'
|
|
79
|
+
* and it parents on the incoming context (Next's node server keeps its own
|
|
80
|
+
* nodejs spans; the sandbox emits a stand-alone runHandler).
|
|
81
|
+
*/
|
|
82
|
+
edgeMode?: 'page' | 'handler';
|
|
83
|
+
/** NEXT_OTEL_VERBOSE=1 wrapper span (NextServer.getRequestHandler). */
|
|
84
|
+
verboseSpan?: OtelSpan;
|
|
85
|
+
/**
|
|
86
|
+
* The context a NEW child span parents under. Starts as rootContext; each
|
|
87
|
+
* withChildSpan pushes its own child context for the duration of `fn` so a
|
|
88
|
+
* nested withChildSpan (e.g. `resolve segment modules` inside `build component
|
|
89
|
+
* tree`) nests under its lexical parent, not the root. Restored on settle.
|
|
90
|
+
*/
|
|
91
|
+
currentContext: OtelContext;
|
|
92
|
+
/** The stamped display route pattern (`/pages/[param]/getServerSideProps`). */
|
|
93
|
+
route?: string;
|
|
94
|
+
/**
|
|
95
|
+
* A pages-router data fetcher errored or returned notFound and the fallback span set replaced the
|
|
96
|
+
* render span. Post-fallback render-internal leaves must not surface as root children - Next does not
|
|
97
|
+
* emit them there.
|
|
98
|
+
*/
|
|
99
|
+
pagesFallback?: boolean;
|
|
100
|
+
/** A materialized pages-API handler ran (suppresses the start-response leaf). */
|
|
101
|
+
pagesApi?: boolean;
|
|
102
|
+
renderSpan?: {
|
|
103
|
+
span: OtelSpan;
|
|
104
|
+
parentContext: OtelContext;
|
|
105
|
+
spanContext: OtelContext;
|
|
106
|
+
ended: boolean;
|
|
107
|
+
/** markRenderSpanError already set an ERROR status; finish must keep it. */
|
|
108
|
+
errored?: boolean;
|
|
109
|
+
/** `next.span_type` the span opened with (runtime label at end). */
|
|
110
|
+
spanType: string;
|
|
111
|
+
/**
|
|
112
|
+
* Pages-router notFound/error fallback: Next replaces the route's render
|
|
113
|
+
* span with the fallback page's own spans (`/_error`, `/_not-found`). A
|
|
114
|
+
* discarded span is NEVER ended, so SimpleSpanProcessor never exports it
|
|
115
|
+
* (its children become orphans the suite's tree-builder filters out).
|
|
116
|
+
*/
|
|
117
|
+
discarded?: boolean;
|
|
118
|
+
};
|
|
119
|
+
componentTreeSpan?: {
|
|
120
|
+
span: OtelSpan;
|
|
121
|
+
parentContext: OtelContext;
|
|
122
|
+
spanContext: OtelContext;
|
|
123
|
+
ended: boolean;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The runtime label for a child span of the given next.span_type. */
|
|
128
|
+
function childRuntime(state: OtelState, spanType: string): SpanRuntime {
|
|
129
|
+
return state.edgeMode === 'page' && EDGE_SANDBOX_SPAN_TYPES.has(spanType) ? 'edge' : 'nodejs';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let cachedAppRoot: string | undefined;
|
|
133
|
+
let spanEndDepth = 0;
|
|
134
|
+
|
|
135
|
+
/** Called once at init so the tracer can resolve @opentelemetry/api from the app. */
|
|
136
|
+
export function setOtelAppRoot(root: string): void {
|
|
137
|
+
cachedAppRoot = root;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function api(): OtelApi | null {
|
|
141
|
+
return cachedAppRoot ? getOtelApi(cachedAppRoot) : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stateOf(unit: WorkUnit | undefined): OtelState | undefined {
|
|
145
|
+
return unit?.compat?.[OTEL_STATE] as OtelState | undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Open the root `BaseServer.handleRequest` span for the active request and stash
|
|
150
|
+
* it (+ its active context) on the work unit. Extracts an incoming `traceparent`
|
|
151
|
+
* so the root span inherits the caller's trace/parent. No-op when otel-api is
|
|
152
|
+
* unavailable, no work unit is active, or a root is already open.
|
|
153
|
+
*/
|
|
154
|
+
export function startRootSpan(input: {
|
|
155
|
+
method: string;
|
|
156
|
+
target: string;
|
|
157
|
+
rsc: boolean;
|
|
158
|
+
headers: Headers;
|
|
159
|
+
}): void {
|
|
160
|
+
const a = api();
|
|
161
|
+
if (!a) return;
|
|
162
|
+
const unit = getWorkUnit();
|
|
163
|
+
if (!unit) return;
|
|
164
|
+
unit.compat ??= {};
|
|
165
|
+
if (unit.compat[OTEL_STATE]) return;
|
|
166
|
+
|
|
167
|
+
// Extract incoming context (traceparent + any custom propagators, e.g. the
|
|
168
|
+
// test's x-custom sampler entry). The carrier is a plain header record.
|
|
169
|
+
const carrier: Record<string, string> = {};
|
|
170
|
+
input.headers.forEach((value, key) => {
|
|
171
|
+
carrier[key] = value;
|
|
172
|
+
});
|
|
173
|
+
const incomingContext = a.propagation.extract(a.context.active(), carrier, headerGetter);
|
|
174
|
+
|
|
175
|
+
const spanName = `${input.rsc ? 'RSC ' : ''}${input.method} ${input.target}`;
|
|
176
|
+
const tracer = a.trace.getTracer('next.js', '0.0.1');
|
|
177
|
+
// NEXT_OTEL_VERBOSE=1: Next additionally emits the NextServer.getRequestHandler
|
|
178
|
+
// wrapper span between the incoming context and handleRequest. The verbose
|
|
179
|
+
// suite asserts its span_type + that it parents directly on the incoming
|
|
180
|
+
// traceparent while everything else nests below.
|
|
181
|
+
let verboseSpan: OtelSpan | undefined;
|
|
182
|
+
let rootParentCtx = incomingContext;
|
|
183
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
184
|
+
if (process.env.NEXT_OTEL_VERBOSE === '1') {
|
|
185
|
+
verboseSpan = tracer.startSpan(
|
|
186
|
+
SPAN_TYPE.getRequestHandler,
|
|
187
|
+
{
|
|
188
|
+
kind: a.SpanKind.INTERNAL,
|
|
189
|
+
attributes: {
|
|
190
|
+
'next.span_name': SPAN_TYPE.getRequestHandler,
|
|
191
|
+
'next.span_type': SPAN_TYPE.getRequestHandler,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
incomingContext,
|
|
195
|
+
);
|
|
196
|
+
rootParentCtx = a.trace.setSpan(incomingContext, verboseSpan);
|
|
197
|
+
}
|
|
198
|
+
const rootSpan = tracer.startSpan(
|
|
199
|
+
spanName,
|
|
200
|
+
{
|
|
201
|
+
kind: a.SpanKind.SERVER,
|
|
202
|
+
attributes: {
|
|
203
|
+
'http.method': input.method,
|
|
204
|
+
'http.target': input.target,
|
|
205
|
+
'next.rsc': input.rsc,
|
|
206
|
+
'next.span_name': spanName,
|
|
207
|
+
'next.span_type': SPAN_TYPE.handleRequest,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
rootParentCtx,
|
|
211
|
+
);
|
|
212
|
+
const rootContext = a.trace.setSpan(rootParentCtx, rootSpan);
|
|
213
|
+
const state: OtelState = {
|
|
214
|
+
api: a,
|
|
215
|
+
rootContext,
|
|
216
|
+
rootSpan,
|
|
217
|
+
rootEnded: false,
|
|
218
|
+
method: input.method,
|
|
219
|
+
rsc: input.rsc,
|
|
220
|
+
target: input.target,
|
|
221
|
+
incomingContext,
|
|
222
|
+
...(verboseSpan ? { verboseSpan } : {}),
|
|
223
|
+
currentContext: rootContext,
|
|
224
|
+
};
|
|
225
|
+
unit.compat[OTEL_STATE] = state;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Set the route name on the root span once the route is matched. `next.route` /
|
|
230
|
+
* `http.route` are the route PATTERN (`/app/[param]/rsc-fetch`); the span name is
|
|
231
|
+
* `[RSC ]<METHOD> <route>` (Next renames from the raw target to the pattern).
|
|
232
|
+
*/
|
|
233
|
+
export function setRootSpanRoute(
|
|
234
|
+
route: string,
|
|
235
|
+
options: {
|
|
236
|
+
/** The matched route declared `runtime: 'edge'`. */
|
|
237
|
+
edge?: boolean;
|
|
238
|
+
/** 'handler' for a route handler, 'page' otherwise. */
|
|
239
|
+
kind?: 'page' | 'handler';
|
|
240
|
+
/** Resolved dynamic params (edge target emulation). */
|
|
241
|
+
params?: Record<string, string | string[]>;
|
|
242
|
+
} = {},
|
|
243
|
+
): void {
|
|
244
|
+
const state = stateOf(getWorkUnit());
|
|
245
|
+
if (!state) return;
|
|
246
|
+
state.route = route;
|
|
247
|
+
const { rootSpan } = state;
|
|
248
|
+
const name = `${state.rsc ? 'RSC ' : ''}${state.method} ${route}`;
|
|
249
|
+
rootSpan.setAttribute('next.route', route);
|
|
250
|
+
rootSpan.setAttribute('http.route', route);
|
|
251
|
+
rootSpan.setAttribute('next.span_name', name);
|
|
252
|
+
rootSpan.updateName(name);
|
|
253
|
+
if (!options.edge) return;
|
|
254
|
+
state.edgeMode = options.kind === 'handler' ? 'handler' : 'page';
|
|
255
|
+
if (state.edgeMode !== 'page') return;
|
|
256
|
+
// A production start invokes an edge page through the sandbox with the resolved dynamic params
|
|
257
|
+
// appended as query params, so the sandbox-side handleRequest span reports them in its route. Mirror
|
|
258
|
+
// that. Always rebuilt from the ORIGINAL request target - the route is stamped twice (early
|
|
259
|
+
// pre-rewrite and late post-rewrite interceptors) and the params must not accumulate.
|
|
260
|
+
const [pathname = '', search = ''] = splitTarget(state.target);
|
|
261
|
+
const query = new URLSearchParams(search);
|
|
262
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
263
|
+
if (Array.isArray(value)) query.set(key, value.join('/'));
|
|
264
|
+
else query.set(key, value);
|
|
265
|
+
}
|
|
266
|
+
const queryText = query.toString();
|
|
267
|
+
if (queryText) {
|
|
268
|
+
rootSpan.setAttribute('http.target', `${pathname}?${queryText}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function splitTarget(target: string): [string, string] {
|
|
273
|
+
const index = target.indexOf('?');
|
|
274
|
+
return index === -1 ? [target, ''] : [target.slice(0, index), target.slice(index + 1)];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Close the root span with the final HTTP status. `code:2` (ERROR) + `error.type`
|
|
279
|
+
* for >=500 (synchronous failures); streaming errors keep status 200 and are
|
|
280
|
+
* marked on the render span instead. Idempotent.
|
|
281
|
+
*/
|
|
282
|
+
export function endRootSpan(status: number): void {
|
|
283
|
+
const state = stateOf(getWorkUnit());
|
|
284
|
+
if (!state || state.rootEnded) return;
|
|
285
|
+
state.rootEnded = true;
|
|
286
|
+
const { api: a, rootSpan } = state;
|
|
287
|
+
rootSpan.setAttribute('http.status_code', status);
|
|
288
|
+
if (status >= 500) {
|
|
289
|
+
rootSpan.setAttribute('error.type', String(status));
|
|
290
|
+
rootSpan.setStatus({ code: a.SpanStatusCode.ERROR });
|
|
291
|
+
} else {
|
|
292
|
+
rootSpan.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
293
|
+
}
|
|
294
|
+
endSpan(rootSpan, state.edgeMode === 'page' ? 'edge' : 'nodejs');
|
|
295
|
+
if (state.verboseSpan) {
|
|
296
|
+
state.verboseSpan.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
297
|
+
endSpan(state.verboseSpan, 'nodejs');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Fallback close for the error path: core's synchronous-500 catch returns a bare Response WITHOUT
|
|
303
|
+
* running responseFinalizers, so endRootSpan(status) never fires there. The error funnel, which DOES run
|
|
304
|
+
* on that path, calls this to end the root span with the fallback status. Idempotent: on the streaming
|
|
305
|
+
* path the finalizer already ended the span, so this no-ops - the funnel must NOT downgrade an
|
|
306
|
+
* already-ended 200 span to 500.
|
|
307
|
+
*/
|
|
308
|
+
export function endRootSpanIfUnclosed(fallbackStatus: number): void {
|
|
309
|
+
const state = stateOf(getWorkUnit());
|
|
310
|
+
if (!state || state.rootEnded) return;
|
|
311
|
+
endRootSpan(fallbackStatus);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The active root span's spanContext. */
|
|
315
|
+
export function rootSpanContext(): { traceId: string; spanId: string } | undefined {
|
|
316
|
+
const state = stateOf(getWorkUnit());
|
|
317
|
+
if (!state) return undefined;
|
|
318
|
+
const sc = state.rootSpan.spanContext();
|
|
319
|
+
return { traceId: sc.traceId, spanId: sc.spanId };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The stashed root OTel context (root span active). */
|
|
323
|
+
export function rootOtelContext(): { api: OtelApi; context: OtelContext } | undefined {
|
|
324
|
+
const state = stateOf(getWorkUnit());
|
|
325
|
+
if (!state) return undefined;
|
|
326
|
+
return { api: state.api, context: state.rootContext };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The active render context, for client trace propagation. */
|
|
330
|
+
export function activeOtelContext(): { api: OtelApi; context: OtelContext } | undefined {
|
|
331
|
+
const state = stateOf(getWorkUnit());
|
|
332
|
+
if (!state) return undefined;
|
|
333
|
+
return { api: state.api, context: state.api.context.active() };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Run `fn` inside a child span nested under the request's root span. The child becomes the active parent
|
|
338
|
+
* for any spans `fn` opens. Returns `fn`'s result. When otel is inert or no root is open, runs `fn`
|
|
339
|
+
* untouched. `onError` lets callers mark render-body (streaming) errors with a message without failing
|
|
340
|
+
* the request.
|
|
341
|
+
*/
|
|
342
|
+
export function withChildSpan<T>(
|
|
343
|
+
name: string,
|
|
344
|
+
spanType: string,
|
|
345
|
+
attributes: Record<string, unknown>,
|
|
346
|
+
fn: () => T,
|
|
347
|
+
options: { kind?: number; onError?: (span: OtelSpan, error: unknown) => void } = {},
|
|
348
|
+
): T {
|
|
349
|
+
const state = stateOf(getWorkUnit());
|
|
350
|
+
if (!state) return fn();
|
|
351
|
+
const { api: a } = state;
|
|
352
|
+
// Parent under the lexically-enclosing span (currentContext), not always the
|
|
353
|
+
// root, so nested withChildSpan calls form the right tree.
|
|
354
|
+
const parentContext = state.currentContext;
|
|
355
|
+
const tracer = a.trace.getTracer('next.js', '0.0.1');
|
|
356
|
+
const span = tracer.startSpan(
|
|
357
|
+
name,
|
|
358
|
+
{
|
|
359
|
+
kind: options.kind ?? a.SpanKind.INTERNAL,
|
|
360
|
+
attributes: {
|
|
361
|
+
...attributes,
|
|
362
|
+
'next.span_name': name,
|
|
363
|
+
'next.span_type': spanType,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
parentContext,
|
|
367
|
+
);
|
|
368
|
+
const childContext = a.trace.setSpan(parentContext, span);
|
|
369
|
+
state.currentContext = childContext;
|
|
370
|
+
const settle = (error?: unknown) => {
|
|
371
|
+
// Restore the enclosing context so siblings parent correctly.
|
|
372
|
+
state.currentContext = parentContext;
|
|
373
|
+
if (error !== undefined) {
|
|
374
|
+
if (options.onError) options.onError(span, error);
|
|
375
|
+
else {
|
|
376
|
+
span.recordException(error);
|
|
377
|
+
span.setStatus({ code: a.SpanStatusCode.ERROR, message: errorMessage(error) });
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
span.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
381
|
+
}
|
|
382
|
+
endSpan(span, childRuntime(state, spanType));
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
const result = a.context.with(childContext, fn);
|
|
386
|
+
if (isThenable(result)) {
|
|
387
|
+
return (result as PromiseLike<unknown>).then(
|
|
388
|
+
value => {
|
|
389
|
+
settle();
|
|
390
|
+
return value;
|
|
391
|
+
},
|
|
392
|
+
error => {
|
|
393
|
+
settle(error);
|
|
394
|
+
throw error;
|
|
395
|
+
},
|
|
396
|
+
) as T;
|
|
397
|
+
}
|
|
398
|
+
settle();
|
|
399
|
+
return result;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
settle(error);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function withRenderBodySpan<T>(
|
|
407
|
+
name: string,
|
|
408
|
+
route: string,
|
|
409
|
+
fn: () => T,
|
|
410
|
+
spanType: string = SPAN_TYPE.getBodyResult,
|
|
411
|
+
): T {
|
|
412
|
+
const state = stateOf(getWorkUnit());
|
|
413
|
+
if (!state) return fn();
|
|
414
|
+
const { api: a } = state;
|
|
415
|
+
const parentContext = state.currentContext;
|
|
416
|
+
const tracer = a.trace.getTracer('next.js', '0.0.1');
|
|
417
|
+
const span = tracer.startSpan(
|
|
418
|
+
name,
|
|
419
|
+
{
|
|
420
|
+
kind: a.SpanKind.INTERNAL,
|
|
421
|
+
attributes: {
|
|
422
|
+
'next.route': route,
|
|
423
|
+
'next.span_name': name,
|
|
424
|
+
'next.span_type': spanType,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
parentContext,
|
|
428
|
+
);
|
|
429
|
+
const spanContext = a.trace.setSpan(parentContext, span);
|
|
430
|
+
const previousRenderSpan = state.renderSpan;
|
|
431
|
+
state.renderSpan = { span, parentContext, spanContext, ended: false, spanType };
|
|
432
|
+
state.currentContext = spanContext;
|
|
433
|
+
|
|
434
|
+
const finish = (error?: unknown) => {
|
|
435
|
+
const current = state.renderSpan;
|
|
436
|
+
if (current?.span !== span || current.ended) return;
|
|
437
|
+
current.ended = true;
|
|
438
|
+
endComponentTreeSpan(state);
|
|
439
|
+
if (current.discarded) {
|
|
440
|
+
// Pages fallback replaced this span — leave it un-ended (never exported).
|
|
441
|
+
state.renderSpan = previousRenderSpan;
|
|
442
|
+
if (state.currentContext === spanContext) state.currentContext = parentContext;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (error !== undefined) markSpanError(a, span, error);
|
|
446
|
+
// A streaming (Suspense-boundary) error already stamped ERROR through
|
|
447
|
+
// markRenderSpanError — never downgrade it back to UNSET here.
|
|
448
|
+
else if (!current.errored && span.isRecording()) {
|
|
449
|
+
span.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
450
|
+
}
|
|
451
|
+
endSpan(span, childRuntime(state, spanType));
|
|
452
|
+
state.renderSpan = previousRenderSpan;
|
|
453
|
+
if (state.currentContext === spanContext) state.currentContext = parentContext;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const queued = queueAfterTask(() => finish());
|
|
457
|
+
try {
|
|
458
|
+
const result = a.context.with(spanContext, fn);
|
|
459
|
+
if (isThenable(result)) {
|
|
460
|
+
return (result as PromiseLike<unknown>).then(
|
|
461
|
+
value => {
|
|
462
|
+
if (!queued) finish();
|
|
463
|
+
return value;
|
|
464
|
+
},
|
|
465
|
+
error => {
|
|
466
|
+
finish(error);
|
|
467
|
+
throw error;
|
|
468
|
+
},
|
|
469
|
+
) as T;
|
|
470
|
+
}
|
|
471
|
+
if (!queued) finish();
|
|
472
|
+
return result;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
finish(error);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function markRenderSpanError(error: unknown): void {
|
|
480
|
+
const state = stateOf(getWorkUnit());
|
|
481
|
+
const render = state?.renderSpan;
|
|
482
|
+
if (!state || !render || render.ended) return;
|
|
483
|
+
render.errored = true;
|
|
484
|
+
markSpanError(state.api, render.span, error);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Run a route-handler invocation inside the `executing api route (app) <route>` span. Nodejs handlers
|
|
489
|
+
* nest under the request root span; edge handlers become a TOP-LEVEL span on the incoming context
|
|
490
|
+
* exported as runtime 'edge' - Next's sandbox emits it stand-alone, since a production start does not
|
|
491
|
+
* thread the node server's span into the sandbox.
|
|
492
|
+
*/
|
|
493
|
+
export function withRouteHandlerSpan<T>(route: string, fn: () => T): T {
|
|
494
|
+
return runHandlerSpan(
|
|
495
|
+
`executing api route (app) ${route}`,
|
|
496
|
+
SPAN_TYPE.runHandler,
|
|
497
|
+
{ 'next.route': route },
|
|
498
|
+
fn,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function runHandlerSpan<T>(
|
|
503
|
+
name: string,
|
|
504
|
+
spanType: string,
|
|
505
|
+
attributes: Record<string, unknown>,
|
|
506
|
+
fn: () => T,
|
|
507
|
+
): T {
|
|
508
|
+
const state = stateOf(getWorkUnit());
|
|
509
|
+
if (!state) return fn();
|
|
510
|
+
const { api: a } = state;
|
|
511
|
+
const edge = state.edgeMode === 'handler';
|
|
512
|
+
const parentContext = edge ? state.incomingContext : state.currentContext;
|
|
513
|
+
const tracer = a.trace.getTracer('next.js', '0.0.1');
|
|
514
|
+
const span = tracer.startSpan(
|
|
515
|
+
name,
|
|
516
|
+
{
|
|
517
|
+
kind: a.SpanKind.INTERNAL,
|
|
518
|
+
attributes: {
|
|
519
|
+
...attributes,
|
|
520
|
+
'next.span_name': name,
|
|
521
|
+
'next.span_type': spanType,
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
parentContext,
|
|
525
|
+
);
|
|
526
|
+
const spanContext = a.trace.setSpan(parentContext, span);
|
|
527
|
+
const previousContext = state.currentContext;
|
|
528
|
+
state.currentContext = spanContext;
|
|
529
|
+
const settle = (error?: unknown) => {
|
|
530
|
+
state.currentContext = previousContext;
|
|
531
|
+
if (error !== undefined) {
|
|
532
|
+
span.recordException(error);
|
|
533
|
+
span.setStatus({ code: a.SpanStatusCode.ERROR, message: errorMessage(error) });
|
|
534
|
+
} else {
|
|
535
|
+
span.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
536
|
+
}
|
|
537
|
+
endSpan(span, edge ? 'edge' : 'nodejs');
|
|
538
|
+
};
|
|
539
|
+
try {
|
|
540
|
+
const result = a.context.with(spanContext, fn);
|
|
541
|
+
if (isThenable(result)) {
|
|
542
|
+
return (result as PromiseLike<unknown>).then(
|
|
543
|
+
value => {
|
|
544
|
+
settle();
|
|
545
|
+
return value;
|
|
546
|
+
},
|
|
547
|
+
error => {
|
|
548
|
+
settle(error);
|
|
549
|
+
throw error;
|
|
550
|
+
},
|
|
551
|
+
) as T;
|
|
552
|
+
}
|
|
553
|
+
settle();
|
|
554
|
+
return result;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
settle(error);
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Run a `pages/api` handler invocation inside the `executing api route (pages) <route>` span (no
|
|
563
|
+
* `next.route` attribute - Next omits it here). Nodejs handlers nest under the request root span; edge
|
|
564
|
+
* handlers become a top-level span on the incoming context, the same split as the app handler. Next's
|
|
565
|
+
* pages-api dispatch emits neither `resolve page components` nor `start response`, so the finalizer's
|
|
566
|
+
* start-response leaf is suppressed for the rest of the request.
|
|
567
|
+
*/
|
|
568
|
+
export function withPagesApiHandlerSpan<T>(route: string, fn: () => T): T {
|
|
569
|
+
const state = stateOf(getWorkUnit());
|
|
570
|
+
if (state) state.pagesApi = true;
|
|
571
|
+
const name = `executing api route (pages) ${route}`;
|
|
572
|
+
return runHandlerSpan(name, SPAN_TYPE.nodeRunHandler, {}, fn);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Run a materialized pages-router data fetcher inside its `Render.<kind>` span. Unlike withChildSpan
|
|
577
|
+
* this parents on the request ROOT span - Next runs data fetching beside, not inside, the document
|
|
578
|
+
* render, so the suite asserts it as a SIBLING of the render span.
|
|
579
|
+
*
|
|
580
|
+
* Fallback emulation on the result: a THROW marks this span ERROR and swaps the open render span for
|
|
581
|
+
* Next's `/_error` render tree; `{ notFound: true }` keeps this span clean (Next reports code 0) and
|
|
582
|
+
* swaps the render span for the `/_not-found` resolution. The route's own render span is DISCARDED in
|
|
583
|
+
* both cases - never ended, never exported - matching Next.
|
|
584
|
+
*/
|
|
585
|
+
export function withPagesDataSpan<T>(
|
|
586
|
+
kind: 'getServerSideProps' | 'getStaticProps',
|
|
587
|
+
fn: () => T,
|
|
588
|
+
): T {
|
|
589
|
+
const state = stateOf(getWorkUnit());
|
|
590
|
+
const route = state?.route;
|
|
591
|
+
if (!state || !route) return fn();
|
|
592
|
+
const { api: a } = state;
|
|
593
|
+
const spanType =
|
|
594
|
+
kind === 'getServerSideProps' ? SPAN_TYPE.getServerSideProps : SPAN_TYPE.getStaticProps;
|
|
595
|
+
const name = `${kind} ${route}`;
|
|
596
|
+
const span = a.trace.getTracer('next.js', '0.0.1').startSpan(
|
|
597
|
+
name,
|
|
598
|
+
{
|
|
599
|
+
kind: a.SpanKind.INTERNAL,
|
|
600
|
+
attributes: {
|
|
601
|
+
'next.route': route,
|
|
602
|
+
'next.span_name': name,
|
|
603
|
+
'next.span_type': spanType,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
state.rootContext,
|
|
607
|
+
);
|
|
608
|
+
const settle = (error: unknown, result: unknown) => {
|
|
609
|
+
if (error !== undefined) {
|
|
610
|
+
markSpanError(a, span, error);
|
|
611
|
+
emitPagesErrorFallbackSpans(state, route);
|
|
612
|
+
} else {
|
|
613
|
+
span.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
614
|
+
if ((result as { notFound?: boolean } | null | undefined)?.notFound) {
|
|
615
|
+
emitPagesNotFoundFallbackSpans(state, route);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
endSpan(span, childRuntime(state, spanType));
|
|
619
|
+
};
|
|
620
|
+
try {
|
|
621
|
+
const result = fn();
|
|
622
|
+
if (isThenable(result)) {
|
|
623
|
+
return (result as PromiseLike<unknown>).then(
|
|
624
|
+
value => {
|
|
625
|
+
settle(undefined, value);
|
|
626
|
+
return value;
|
|
627
|
+
},
|
|
628
|
+
(error: unknown) => {
|
|
629
|
+
settle(error ?? new Error('getServerSideProps error'), undefined);
|
|
630
|
+
throw error;
|
|
631
|
+
},
|
|
632
|
+
) as T;
|
|
633
|
+
}
|
|
634
|
+
settle(undefined, result);
|
|
635
|
+
return result;
|
|
636
|
+
} catch (error) {
|
|
637
|
+
settle(error ?? new Error('getServerSideProps error'), undefined);
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** A pre-ended leaf span parented directly on the request root span. */
|
|
643
|
+
function emitRootLeafSpan(
|
|
644
|
+
state: OtelState,
|
|
645
|
+
name: string,
|
|
646
|
+
spanType: string,
|
|
647
|
+
attributes: Record<string, unknown>,
|
|
648
|
+
): void {
|
|
649
|
+
const { api: a } = state;
|
|
650
|
+
const span = a.trace.getTracer('next.js', '0.0.1').startSpan(
|
|
651
|
+
name,
|
|
652
|
+
{
|
|
653
|
+
kind: a.SpanKind.INTERNAL,
|
|
654
|
+
attributes: {
|
|
655
|
+
...attributes,
|
|
656
|
+
'next.span_name': name,
|
|
657
|
+
'next.span_type': spanType,
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
state.rootContext,
|
|
661
|
+
);
|
|
662
|
+
span.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
663
|
+
endSpan(span, 'nodejs');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Discard the route's open render span (see withPagesDataSpan). */
|
|
667
|
+
function discardRenderSpan(state: OtelState): void {
|
|
668
|
+
state.pagesFallback = true;
|
|
669
|
+
if (state.renderSpan && !state.renderSpan.ended) state.renderSpan.discarded = true;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// gSSP threw: Next renders `/_error` (its own renderDocument span), resolves
|
|
673
|
+
// `/_error`, probes `/500` twice (prod), and the route resolve already fired.
|
|
674
|
+
function emitPagesErrorFallbackSpans(state: OtelState, _route: string): void {
|
|
675
|
+
discardRenderSpan(state);
|
|
676
|
+
emitRootLeafSpan(state, 'render route (pages) /_error', SPAN_TYPE.renderDocument, {
|
|
677
|
+
'next.route': '/_error',
|
|
678
|
+
});
|
|
679
|
+
emitRootLeafSpan(state, 'resolve page components', SPAN_TYPE.findPageComponents, {
|
|
680
|
+
'next.route': '/_error',
|
|
681
|
+
});
|
|
682
|
+
for (let i = 0; i < 2; i += 1) {
|
|
683
|
+
emitRootLeafSpan(state, 'resolve page components', SPAN_TYPE.findPageComponents, {
|
|
684
|
+
'next.route': '/500',
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// gSSP returned { notFound: true }: Next serves the prerendered `/_not-found`
|
|
690
|
+
// (prod: a findPageComponents probe only, no render span).
|
|
691
|
+
function emitPagesNotFoundFallbackSpans(state: OtelState, _route: string): void {
|
|
692
|
+
discardRenderSpan(state);
|
|
693
|
+
emitRootLeafSpan(state, 'resolve page components', SPAN_TYPE.findPageComponents, {
|
|
694
|
+
'next.route': '/_not-found',
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Leaf `start response` span, which Next emits when the response starts piping. pnext's otel response
|
|
700
|
+
* finalizer calls this right before it closes the root span. Parents under the render span when one is
|
|
701
|
+
* open (page renders), else the root (route handlers). Always exports as runtime 'nodejs', since Next
|
|
702
|
+
* pipes from the node server even for edge routes.
|
|
703
|
+
*/
|
|
704
|
+
export function emitStartResponseSpan(): void {
|
|
705
|
+
const state = stateOf(getWorkUnit());
|
|
706
|
+
if (state?.pagesFallback || state?.pagesApi) return;
|
|
707
|
+
emitNodeLeafSpan('start response', SPAN_TYPE.startResponse, {});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Leaf `clientComponentLoading` span with the number of client component modules loaded during the
|
|
712
|
+
* render. pnext SSRs islands only, so the count reflects pnext's own client-module loads - Next's number
|
|
713
|
+
* additionally counts its internal client components, an internals delta not emulated here. Only
|
|
714
|
+
* emitted for page renders, matching Next.
|
|
715
|
+
*/
|
|
716
|
+
export function emitClientComponentLoadingSpan(count: number): void {
|
|
717
|
+
const state = stateOf(getWorkUnit());
|
|
718
|
+
if (!state?.renderSpan || state.renderSpan.ended) return;
|
|
719
|
+
emitNodeLeafSpan(SPAN_TYPE.clientComponentLoading, SPAN_TYPE.clientComponentLoading, {
|
|
720
|
+
'next.clientComponentLoadCount': count,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function emitNodeLeafSpan(
|
|
725
|
+
name: string,
|
|
726
|
+
spanType: string,
|
|
727
|
+
attributes: Record<string, unknown>,
|
|
728
|
+
): void {
|
|
729
|
+
const state = stateOf(getWorkUnit());
|
|
730
|
+
if (!state) return;
|
|
731
|
+
const { api: a } = state;
|
|
732
|
+
const render = state.renderSpan;
|
|
733
|
+
const parentContext = render && !render.ended ? render.spanContext : state.rootContext;
|
|
734
|
+
const span = a.trace.getTracer('next.js', '0.0.1').startSpan(
|
|
735
|
+
name,
|
|
736
|
+
{
|
|
737
|
+
kind: a.SpanKind.INTERNAL,
|
|
738
|
+
attributes: {
|
|
739
|
+
...attributes,
|
|
740
|
+
'next.span_name': name,
|
|
741
|
+
'next.span_type': spanType,
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
parentContext,
|
|
745
|
+
);
|
|
746
|
+
span.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
747
|
+
endSpan(span, 'nodejs');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Run `fn` inside a `middleware <METHOD>` span (`Middleware.execute`). Unlike
|
|
752
|
+
* withChildSpan this does NOT nest under the request root span: Next's
|
|
753
|
+
* middleware span is a top-level trace parented under the INCOMING traceparent
|
|
754
|
+
* (it runs before BaseServer.handleRequest). We extract the carrier from the
|
|
755
|
+
* request headers so the span inherits the caller's trace/parent, matching
|
|
756
|
+
* `parentId: rootParentId`. Inert (runs `fn` untouched) when otel-api is
|
|
757
|
+
* unavailable. Does not require an open work-unit root span.
|
|
758
|
+
*/
|
|
759
|
+
export function withMiddlewareSpan<T>(request: Request, fn: () => T): T {
|
|
760
|
+
const a = api();
|
|
761
|
+
if (!a) return fn();
|
|
762
|
+
const method = request.method.toUpperCase();
|
|
763
|
+
let target = '';
|
|
764
|
+
try {
|
|
765
|
+
const url = new URL(request.url);
|
|
766
|
+
target = url.pathname + url.search;
|
|
767
|
+
} catch {
|
|
768
|
+
target = request.url;
|
|
769
|
+
}
|
|
770
|
+
const carrier: Record<string, string> = {};
|
|
771
|
+
request.headers.forEach((value, key) => {
|
|
772
|
+
carrier[key] = value;
|
|
773
|
+
});
|
|
774
|
+
const parentCtx = a.propagation.extract(a.context.active(), carrier, headerGetter);
|
|
775
|
+
const name = `middleware ${method}`;
|
|
776
|
+
const tracer = a.trace.getTracer('next.js', '0.0.1');
|
|
777
|
+
const span = tracer.startSpan(
|
|
778
|
+
name,
|
|
779
|
+
{
|
|
780
|
+
attributes: {
|
|
781
|
+
'http.method': method,
|
|
782
|
+
'http.target': target,
|
|
783
|
+
'next.span_name': name,
|
|
784
|
+
'next.span_type': SPAN_TYPE.middleware,
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
parentCtx,
|
|
788
|
+
);
|
|
789
|
+
const spanCtx = a.trace.setSpan(parentCtx, span);
|
|
790
|
+
const settle = (error?: unknown) => {
|
|
791
|
+
if (error !== undefined) {
|
|
792
|
+
span.recordException(error);
|
|
793
|
+
span.setStatus({ code: a.SpanStatusCode.ERROR, message: errorMessage(error) });
|
|
794
|
+
} else {
|
|
795
|
+
span.setStatus({ code: a.SpanStatusCode.UNSET });
|
|
796
|
+
}
|
|
797
|
+
// Middleware always runs in the edge runtime in Next; pnext's proxy runner
|
|
798
|
+
// already wraps the handler in withRouteRuntime('edge') — the explicit tag
|
|
799
|
+
// makes the exported runtime deterministic regardless of settle timing.
|
|
800
|
+
endSpan(span, 'edge');
|
|
801
|
+
};
|
|
802
|
+
try {
|
|
803
|
+
const result = a.context.with(spanCtx, fn);
|
|
804
|
+
if (isThenable(result)) {
|
|
805
|
+
return (result as PromiseLike<unknown>).then(
|
|
806
|
+
value => {
|
|
807
|
+
settle();
|
|
808
|
+
return value;
|
|
809
|
+
},
|
|
810
|
+
error => {
|
|
811
|
+
settle(error);
|
|
812
|
+
throw error;
|
|
813
|
+
},
|
|
814
|
+
) as T;
|
|
815
|
+
}
|
|
816
|
+
settle();
|
|
817
|
+
return result;
|
|
818
|
+
} catch (error) {
|
|
819
|
+
settle(error);
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// A leaf span with no work inside (e.g. resolve-segment-modules markers).
|
|
825
|
+
export function emitLeafSpan(
|
|
826
|
+
name: string,
|
|
827
|
+
spanType: string,
|
|
828
|
+
attributes: Record<string, unknown>,
|
|
829
|
+
kind?: number,
|
|
830
|
+
): void {
|
|
831
|
+
if (spanType === SPAN_TYPE.getLayoutOrPageModule) {
|
|
832
|
+
emitSegmentSpan(name, spanType, attributes, kind);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
withChildSpan(name, spanType, attributes, () => undefined, kind !== undefined ? { kind } : {});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function emitSegmentSpan(
|
|
839
|
+
name: string,
|
|
840
|
+
spanType: string,
|
|
841
|
+
attributes: Record<string, unknown>,
|
|
842
|
+
kind?: number,
|
|
843
|
+
): void {
|
|
844
|
+
const state = stateOf(getWorkUnit());
|
|
845
|
+
if (state?.pagesFallback) return;
|
|
846
|
+
if (!state?.renderSpan || state.renderSpan.ended) {
|
|
847
|
+
withChildSpan(name, spanType, attributes, () => undefined, kind !== undefined ? { kind } : {});
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const tree = ensureComponentTreeSpan(state);
|
|
851
|
+
const previous = state.currentContext;
|
|
852
|
+
state.currentContext = tree.spanContext;
|
|
853
|
+
try {
|
|
854
|
+
withChildSpan(name, spanType, attributes, () => undefined, kind !== undefined ? { kind } : {});
|
|
855
|
+
} finally {
|
|
856
|
+
state.currentContext = previous;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function ensureComponentTreeSpan(state: OtelState) {
|
|
861
|
+
const current = state.componentTreeSpan;
|
|
862
|
+
if (current && !current.ended) return current;
|
|
863
|
+
const { api: a } = state;
|
|
864
|
+
const parentContext = state.renderSpan?.spanContext ?? state.currentContext;
|
|
865
|
+
const name = 'build component tree';
|
|
866
|
+
const span = a.trace.getTracer('next.js', '0.0.1').startSpan(
|
|
867
|
+
name,
|
|
868
|
+
{
|
|
869
|
+
kind: a.SpanKind.INTERNAL,
|
|
870
|
+
attributes: {
|
|
871
|
+
'next.span_name': name,
|
|
872
|
+
'next.span_type': SPAN_TYPE.createComponentTree,
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
parentContext,
|
|
876
|
+
);
|
|
877
|
+
const spanContext = a.trace.setSpan(parentContext, span);
|
|
878
|
+
const next = { span, parentContext, spanContext, ended: false };
|
|
879
|
+
state.componentTreeSpan = next;
|
|
880
|
+
return next;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function endComponentTreeSpan(state: OtelState): void {
|
|
884
|
+
const tree = state.componentTreeSpan;
|
|
885
|
+
if (!tree || tree.ended) return;
|
|
886
|
+
tree.ended = true;
|
|
887
|
+
tree.span.setStatus({ code: state.api.SpanStatusCode.UNSET });
|
|
888
|
+
endSpan(tree.span, childRuntime(state, SPAN_TYPE.createComponentTree));
|
|
889
|
+
if (state.currentContext === tree.spanContext) state.currentContext = tree.parentContext;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const headerGetter = {
|
|
893
|
+
keys(carrier: Record<string, string>): string[] {
|
|
894
|
+
return Object.keys(carrier);
|
|
895
|
+
},
|
|
896
|
+
get(carrier: Record<string, string>, key: string): string | undefined {
|
|
897
|
+
return carrier[key.toLowerCase()] ?? carrier[key];
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
/** Setter for propagation.inject into a plain record carrier. */
|
|
902
|
+
export const carrierSetter = {
|
|
903
|
+
set(carrier: Record<string, string>, key: string, value: string): void {
|
|
904
|
+
carrier[key] = value;
|
|
905
|
+
},
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
function errorMessage(error: unknown): string {
|
|
909
|
+
if (error instanceof Error) return error.message;
|
|
910
|
+
return String(error);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function markSpanError(api: OtelApi, span: OtelSpan, error: unknown): void {
|
|
914
|
+
span.setAttribute('error.type', error instanceof Error ? error.name : typeof error);
|
|
915
|
+
span.recordException(error);
|
|
916
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: errorMessage(error) });
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export function otelExportInProgress(): boolean {
|
|
920
|
+
return spanEndDepth > 0;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function endSpan(span: OtelSpan, runtime?: SpanRuntime): void {
|
|
924
|
+
spanEndDepth++;
|
|
925
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
926
|
+
const previousRuntime = process.env.NEXT_RUNTIME;
|
|
927
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
928
|
+
if (runtime) process.env.NEXT_RUNTIME = runtime;
|
|
929
|
+
try {
|
|
930
|
+
span.end();
|
|
931
|
+
} finally {
|
|
932
|
+
if (runtime) {
|
|
933
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
934
|
+
if (previousRuntime === undefined) delete process.env.NEXT_RUNTIME;
|
|
935
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
936
|
+
else process.env.NEXT_RUNTIME = previousRuntime;
|
|
937
|
+
}
|
|
938
|
+
spanEndDepth--;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
943
|
+
return typeof (value as { then?: unknown } | null | undefined)?.then === 'function';
|
|
944
|
+
}
|