Skip to content

Commit

Permalink
Add perf data experiment (#17956)
Browse files Browse the repository at this point in the history
  • Loading branch information
Timer committed Oct 16, 2020
1 parent 6bc6e2c commit 0fe5b23
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 1 deletion.
9 changes: 9 additions & 0 deletions packages/next/build/index.ts
Expand Up @@ -1159,6 +1159,15 @@ export default async function build(
printCustomRoutes({ redirects, rewrites, headers })
}

if (config.experimental.analyticsId) {
console.log(
chalk.bold.green('Next.js Analytics') +
' is enabled for this production build. ' +
"You'll receive a Real Experience Score computed by all of your visitors."
)
console.log('')
}

if (tracer) {
const parsedResults = await tracer.profiler.stopProfiling()
await new Promise((resolve) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -1015,6 +1015,9 @@ export default async function getBaseWebpackConfig(
'process.env.__NEXT_i18n_SUPPORT': JSON.stringify(
!!config.experimental.i18n
),
'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(
config.experimental.analyticsId
),
...(isServer
? {
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
Expand Down
40 changes: 40 additions & 0 deletions packages/next/client/performance-relayer.ts
Expand Up @@ -8,13 +8,53 @@ import {
ReportHandler,
} from 'web-vitals'

const initialHref = location.href
let isRegistered = false
let userReportHandler: ReportHandler | undefined

function onReport(metric: Metric) {
if (userReportHandler) {
userReportHandler(metric)
}

// This code is not shipped, executed, or present in the client-side
// JavaScript bundle unless explicitly enabled in your application.
//
// When this feature is enabled, we'll make it very clear by printing a
// message during the build (`next build`).
if (
process.env.NODE_ENV === 'production' &&
// This field is empty unless you explicitly configure it:
process.env.__NEXT_ANALYTICS_ID
) {
const body: Record<string, string> = {
dsn: process.env.__NEXT_ANALYTICS_ID,
id: metric.id,
page: window.__NEXT_DATA__.page,
href: initialHref,
event_name: metric.name,
value: metric.value.toString(),
speed:
'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in navigator['connection']
? (navigator['connection']['effectiveType'] as string)
: '',
}

const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`:
type: 'application/x-www-form-urlencoded',
})
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals'
;(navigator.sendBeacon && navigator.sendBeacon(vitalsUrl, blob)) ||
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
})
}
}

export default (onPerfEntry?: ReportHandler) => {
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/config.ts
Expand Up @@ -60,6 +60,7 @@ const defaultConfig: { [key: string]: any } = {
optimizeImages: false,
scrollRestoration: false,
i18n: false,
analyticsId: process.env.VERCEL_ANALYTICS_ID || '',
},
future: {
excludeDefaultMomentLocales: false,
Expand Down
15 changes: 15 additions & 0 deletions test/integration/relay-analytics-disabled/pages/_app.js
@@ -0,0 +1,15 @@
/* global localStorage */
/* eslint-disable camelcase */
import App from 'next/app'

export default class MyApp extends App {}

/*
Method is experimental and will eventually be handled in a Next.js plugin
*/
export function reportWebVitals(data) {
localStorage.setItem(
data.name || data.entryType,
data.value !== undefined ? data.value : data.startTime
)
}
25 changes: 25 additions & 0 deletions test/integration/relay-analytics-disabled/pages/index.js
@@ -0,0 +1,25 @@
if (typeof navigator !== 'undefined') {
window.__BEACONS = window.__BEACONS || []

navigator.sendBeacon = async function () {
const args = await Promise.all(
[...arguments].map((v) => {
if (v instanceof Blob) {
return v.text()
}
return v
})
)

window.__BEACONS.push(args)
}
}

export default () => {
return (
<div>
<h1>Foo!</h1>
<h2>bar!</h2>
</div>
)
}
72 changes: 72 additions & 0 deletions test/integration/relay-analytics-disabled/test/index.test.js
@@ -0,0 +1,72 @@
/* eslint-env jest */

import fs from 'fs-extra'
import { findPort, killApp, nextBuild, nextStart } from 'next-test-utils'
import webdriver from 'next-webdriver'
import path, { join } from 'path'

const appDir = join(__dirname, '../')
let appPort
let server
jest.setTimeout(1000 * 60 * 2)

let buildManifest

describe('Analytics relayer (disabled)', () => {
let stdout
beforeAll(async () => {
appPort = await findPort()
;({ stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
buildManifest = require(path.join(
appDir,
'.next/build-manifest.json'
), 'utf8')
server = await nextStart(appDir, appPort)
})
afterAll(() => killApp(server))

it('Does not relay any data', async () => {
const browser = await webdriver(appPort, '/')
await browser.waitForElementByCss('h1')
const h1Text = await browser.elementByCss('h1').text()
const firstContentfulPaint = parseFloat(
await browser.eval('localStorage.getItem("FCP")')
)

expect(h1Text).toMatch(/Foo!/)

expect(firstContentfulPaint).not.toBeNaN()
expect(firstContentfulPaint).toBeGreaterThan(0)

const beacons = (await browser.eval('window.__BEACONS')).map(([, value]) =>
Object.fromEntries(new URLSearchParams(value))
)

expect(beacons.length).toBe(0)

expect(stdout).not.toMatch('Next.js Analytics')

await browser.close()
})

it('Does not include the code', async () => {
const pageFiles = [
...new Set([
...buildManifest.pages['/'].filter((file) => file.endsWith('.js')),
...buildManifest.pages['/_app'].filter((file) => file.endsWith('.js')),
]),
]

expect(pageFiles.length).toBeGreaterThan(1)

for (const pageFile of pageFiles) {
const content = await fs.readFile(
path.join(appDir, '.next', pageFile),
'utf8'
)
expect(content).not.toMatch('vercel-analytics')
}
})
})
17 changes: 17 additions & 0 deletions test/integration/relay-analytics/pages/index.js
@@ -1,3 +1,20 @@
if (typeof navigator !== 'undefined') {
window.__BEACONS = window.__BEACONS || []

navigator.sendBeacon = async function () {
const args = await Promise.all(
[...arguments].map((v) => {
if (v instanceof Blob) {
return v.text()
}
return v
})
)

window.__BEACONS.push(args)
}
}

export default () => {
return (
<div>
Expand Down
35 changes: 34 additions & 1 deletion test/integration/relay-analytics/test/index.test.js
Expand Up @@ -10,9 +10,13 @@ let server
jest.setTimeout(1000 * 60 * 2)

describe('Analytics relayer', () => {
let stdout
beforeAll(async () => {
appPort = await findPort()
await nextBuild(appDir)
;({ stdout } = await nextBuild(appDir, [], {
env: { VERCEL_ANALYTICS_ID: 'test' },
stdout: true,
}))
server = await nextStart(appDir, appPort)
})
afterAll(() => killApp(server))
Expand Down Expand Up @@ -54,6 +58,35 @@ describe('Analytics relayer', () => {
expect(cls).not.toBeNull()
expect(largestContentfulPaint).not.toBeNaN()
expect(largestContentfulPaint).toBeGreaterThan(0)

const beacons = (await browser.eval('window.__BEACONS')).map(([, value]) =>
Object.fromEntries(new URLSearchParams(value))
)

beacons.sort((a, b) => a.event_name.localeCompare(b.event_name))

expect(beacons.length).toBe(2)
expect(beacons[0]).toMatchObject({
dsn: 'test',
event_name: 'FCP',
href: expect.stringMatching('http://'),
id: expect.stringContaining('-'),
page: '/',
speed: '4g',
value: expect.stringContaining('.'),
})
expect(beacons[1]).toMatchObject({
dsn: 'test',
event_name: 'TTFB',
href: expect.stringMatching('http://'),
id: expect.stringContaining('-'),
page: '/',
speed: '4g',
value: expect.stringContaining('.'),
})

expect(stdout).toMatch('Next.js Analytics')

await browser.close()
})
})

0 comments on commit 0fe5b23

Please sign in to comment.