Skip to content

Commit

Permalink
Add experimental Web Vitals support
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Yeats committed May 14, 2020
1 parent 394f35b commit c672d53
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 67 deletions.
64 changes: 64 additions & 0 deletions packages/nextjs/src/experimental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import url from "url"
import { Appsignal, Metrics } from "@appsignal/nodejs"

import { Request, Response, NextFunction, RequestHandler } from "express"

function handleWebVital(
metric: { name: string; value: number },
meter: Metrics
) {
meter.addDistributionValue(
`nextjs_webvital_${metric.name.toLowerCase()}`,
metric.value
)
}

function handleNextMetric(
metric: { name: string; value: number },
meter: Metrics
) {
switch (metric.name) {
case "Next.js-hydration":
meter.addDistributionValue(`nextjs_hydration_time`, metric.value)
break
case "Next.js-route-change-to-render":
meter.addDistributionValue(
`nextjs_route_change_to_render_time`,
metric.value
)
break
case "Next.js-render":
meter.addDistributionValue(`nextjs_render_time`, metric.value)
break
default:
break
}
}

export function webVitalsMiddleware(appsignal: Appsignal): RequestHandler {
return function (req: Request, res: Response, next: NextFunction) {
const meter = appsignal.metrics()
const { pathname } = url.parse(req.url, true)

if (pathname !== "/__appsignal-web-vitals") {
return next()
}

const chunks: any[] = []

req.on("data", chunk => chunks.push(chunk))

req.on("end", () => {
const data = Buffer.concat(chunks)
const metric = JSON.parse(data.toString())

if (metric.label === "web-vital") {
handleWebVital(metric, meter)
} else if (metric.label === "custom") {
handleNextMetric(metric, meter)
}
})

return res.sendStatus(200)
}
}
63 changes: 63 additions & 0 deletions packages/nextjs/src/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import url from "url"
import { Appsignal } from "@appsignal/nodejs"
import { ServerResponse, IncomingMessage } from "http"

interface NextServer {
router: {
dynamicRoutes?: {
page: string
match: (
pathname: string | null | undefined
) => false | { [param: string]: any }
}[]
}
getRequestHandler(): (
req: IncomingMessage,
res: ServerResponse,
...rest: any[]
) => Promise<void>
}

/**
* Wraps Next.js' `app.getRequestHandler()` method in order to augment the current
* span with data.
*/
export function getRequestHandler<T extends NextServer>(
appsignal: Appsignal,
app: T
) {
const handler = app.getRequestHandler()

return function (req: IncomingMessage, res: ServerResponse, ...rest: any[]) {
const span = appsignal.tracer().currentSpan()
const { pathname } = url.parse(req.url || "/", true)

if (span) {
const routes = app.router.dynamicRoutes || []
const matched = routes.filter(el => el.match(pathname))[0]

// passing { debug: true } to the `Appsignal` constructor will log
// data about the current route to the console. don't rely on this
// working in future!
if (appsignal.config.debug) {
console.log("[APPSIGNAL]: Next.js debug data", {
routes,
matched,
pathname
})
}

if (matched) {
// matched to a dynamic route
span.setName(`${req.method} ${matched.page}`)
} else if (!matched && pathname === "/") {
// the root
span.setName(`${req.method} ${pathname}`)
} else {
span.setName(`${req.method} [unknown route]`)
}
}

return handler(req, res, ...rest)
}
}
69 changes: 3 additions & 66 deletions packages/nextjs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,3 @@
import url from "url"
import { Appsignal } from "@appsignal/nodejs"
import { ServerResponse, IncomingMessage } from "http"

interface NextServer {
router: {
dynamicRoutes?: {
page: string
match: (
pathname: string | null | undefined
) => false | { [param: string]: any }
}[]
}
getRequestHandler(): (
req: IncomingMessage,
res: ServerResponse,
...rest: any[]
) => Promise<void>
}

/**
* Wraps Next.js' `app.getRequestHandler()` method in order to augment the current
* span with data.
*/
export function getRequestHandler<T extends NextServer>(
appsignal: Appsignal,
app: T
) {
const handler = app.getRequestHandler()

return function (req: IncomingMessage, res: ServerResponse, ...rest: any[]) {
const span = appsignal.tracer().currentSpan()
const { pathname } = url.parse(req.url || "/", true)

if (span) {
const routes = app.router.dynamicRoutes || []
const matched = routes.filter(el => el.match(pathname))[0]

// identifies the span in the stacked graphs
span.setCategory("process_request.nextjs")

// passing { debug: true } to the `Appsignal` constructor will log
// data about the current route to the console. don't rely on this
// working in future!
if (appsignal.config.debug) {
console.log("[APPSIGNAL]: Next.js debug data", {
routes,
matched,
pathname
})
}

if (matched) {
// matched to a dynamic route
span.setName(`${req.method} ${matched.page}`)
} else if (!matched && pathname === "/") {
// the root
span.setName(`${req.method} ${pathname}`)
} else {
span.setName(`${req.method} [unknown route]`)
}
}

return handler(req, res, ...rest)
}
}
export * from "./handler"
import * as experimental from "./experimental"
export const EXPERIMENTAL = experimental
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const DEFAULT_IGNORED_URLS = [
// next.js hot reloading
/(\/_next\/webpack-hmr)/i,
// gatsby hot reloading
/(\/__webpack_hmr)/i
/(\/__webpack_hmr)/i,
// next.js integration web vitals endpoint
/(\/__appsignal-web-vitals)$/i
]

function incomingRequest(
Expand Down

0 comments on commit c672d53

Please sign in to comment.