Skip to content

Commit

Permalink
Add telemetry for @next/font (#42579)
Browse files Browse the repository at this point in the history
## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
Hannes Bornö committed Nov 8, 2022
1 parent 90ddc24 commit 81890c8
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 26 deletions.
21 changes: 20 additions & 1 deletion packages/next/build/webpack/plugins/telemetry-plugin.ts
Expand Up @@ -24,6 +24,8 @@ export type Feature =
| 'next/legacy/image'
| 'next/script'
| 'next/dynamic'
| '@next/font/google'
| '@next/font/local'
| 'swcLoader'
| 'swcMinify'
| 'swcRelay'
Expand Down Expand Up @@ -64,6 +66,10 @@ const FEATURE_MODULE_MAP: ReadonlyMap<Feature, string> = new Map([
['next/script', '/next/script.js'],
['next/dynamic', '/next/dynamic.js'],
])
const FEATURE_MODULE_REGEXP_MAP: ReadonlyMap<Feature, RegExp> = new Map([
['@next/font/google', /\/@next\/font\/google\/target.css?.+$/],
['@next/font/local', /\/@next\/font\/local\/target.css?.+$/],
])

// List of build features used in webpack configuration
const BUILD_FEATURES: Array<Feature> = [
Expand Down Expand Up @@ -101,8 +107,14 @@ function findFeatureInModule(module: Module): Feature | undefined {
if (module.type !== 'javascript/auto') {
return
}
const normalizedIdentifier = module.identifier().replace(/\\/g, '/')
for (const [feature, path] of FEATURE_MODULE_MAP) {
if (module.identifier().replace(/\\/g, '/').endsWith(path)) {
if (normalizedIdentifier.endsWith(path)) {
return feature
}
}
for (const [feature, regexp] of FEATURE_MODULE_REGEXP_MAP) {
if (regexp.test(normalizedIdentifier)) {
return feature
}
}
Expand Down Expand Up @@ -152,6 +164,13 @@ export class TelemetryPlugin implements webpack.WebpackPluginInstance {
invocationCount: 0,
})
}

for (const featureName of FEATURE_MODULE_REGEXP_MAP.keys()) {
this.usageTracker.set(featureName, {
featureName,
invocationCount: 0,
})
}
}

apply(compiler: webpack.Compiler): void {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/telemetry/events/build.ts
Expand Up @@ -136,6 +136,8 @@ export type EventBuildFeatureUsage = {
| 'next/future/image'
| 'next/script'
| 'next/dynamic'
| '@next/font/google'
| '@next/font/local'
| 'experimental/optimizeCss'
| 'experimental/nextScriptWorkers'
| 'optimizeFonts'
Expand Down
54 changes: 29 additions & 25 deletions test/integration/telemetry/test/index.test.js
Expand Up @@ -11,6 +11,7 @@ import {
nextBuild,
nextLint,
check,
findAllTelemetryEvents,
} from 'next-test-utils'

const appDir = path.join(__dirname, '..')
Expand Down Expand Up @@ -672,7 +673,10 @@ describe('Telemetry CLI', () => {
expect(event1).toMatch(`"nextRulesEnabled": {`)
expect(event1).toMatch(/"@next\/next\/.+?": "(off|warn|error)"/)

const featureUsageEvents = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const featureUsageEvents = findAllTelemetryEvents(
stderr,
'NEXT_BUILD_FEATURE_USAGE'
)
expect(featureUsageEvents).toContainEqual({
featureName: 'build-lint',
invocationCount: 1,
Expand All @@ -684,7 +688,7 @@ describe('Telemetry CLI', () => {
stderr: true,
env: { NEXT_TELEMETRY_DEBUG: 1 },
})
const events = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const events = findAllTelemetryEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
expect(events).toContainEqual({
featureName: 'build-lint',
invocationCount: 0,
Expand All @@ -703,7 +707,7 @@ describe('Telemetry CLI', () => {
})
await fs.remove(nextConfig)

const events = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const events = findAllTelemetryEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
expect(events).toContainEqual({
featureName: 'build-lint',
invocationCount: 0,
Expand Down Expand Up @@ -742,7 +746,10 @@ describe('Telemetry CLI', () => {
stderr: true,
env: { NEXT_TELEMETRY_DEBUG: 1 },
})
const featureUsageEvents = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const featureUsageEvents = findAllTelemetryEvents(
stderr,
'NEXT_BUILD_FEATURE_USAGE'
)
expect(featureUsageEvents).toEqual(
expect.arrayContaining([
{
Expand Down Expand Up @@ -782,7 +789,10 @@ describe('Telemetry CLI', () => {
})
await fs.remove(path.join(appDir, 'next.config.js'))
await fs.remove(path.join(appDir, 'jsconfig.json'))
const featureUsageEvents = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const featureUsageEvents = findAllTelemetryEvents(
stderr,
'NEXT_BUILD_FEATURE_USAGE'
)
expect(featureUsageEvents).toEqual(
expect.arrayContaining([
{
Expand Down Expand Up @@ -837,7 +847,7 @@ describe('Telemetry CLI', () => {
path.join(appDir, 'next.config.optimize-css')
)

const events = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const events = findAllTelemetryEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
expect(events).toContainEqual({
featureName: 'experimental/optimizeCss',
invocationCount: 1,
Expand All @@ -860,7 +870,10 @@ describe('Telemetry CLI', () => {
path.join(appDir, 'next.config.next-script-workers')
)

const featureUsageEvents = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const featureUsageEvents = findAllTelemetryEvents(
stderr,
'NEXT_BUILD_FEATURE_USAGE'
)
expect(featureUsageEvents).toContainEqual({
featureName: 'experimental/nextScriptWorkers',
invocationCount: 1,
Expand All @@ -880,7 +893,10 @@ describe('Telemetry CLI', () => {

await fs.remove(path.join(appDir, 'middleware.js'))

const buildOptimizedEvents = findAllEvents(stderr, 'NEXT_BUILD_OPTIMIZED')
const buildOptimizedEvents = findAllTelemetryEvents(
stderr,
'NEXT_BUILD_OPTIMIZED'
)
expect(buildOptimizedEvents).toContainEqual(
expect.objectContaining({
middlewareCount: 1,
Expand Down Expand Up @@ -917,7 +933,7 @@ describe('Telemetry CLI', () => {
path.join(appDir, 'package.swc-plugins')
)

const pluginDetectedEvents = findAllEvents(
const pluginDetectedEvents = findAllTelemetryEvents(
stderr,
'NEXT_SWC_PLUGIN_DETECTED'
)
Expand All @@ -941,7 +957,10 @@ describe('Telemetry CLI', () => {
stderr: true,
env: { NEXT_TELEMETRY_DEBUG: 1 },
})
const featureUsageEvents = findAllEvents(stderr, 'NEXT_BUILD_FEATURE_USAGE')
const featureUsageEvents = findAllTelemetryEvents(
stderr,
'NEXT_BUILD_FEATURE_USAGE'
)
expect(featureUsageEvents).toContainEqual({
featureName: 'next/legacy/image',
invocationCount: 1,
Expand All @@ -952,18 +971,3 @@ describe('Telemetry CLI', () => {
})
})
})

/**
* Parse the output and return all entries that match the provided `eventName`
* @param {string} output output of the console
* @param {string} eventName
* @returns {Array<{}>}
*/
function findAllEvents(output, eventName) {
const regex = /\[telemetry\] ({.+?^})/gms
// Pop the last element of each entry to retrieve contents of the capturing group
const events = [...output.matchAll(regex)].map((entry) =>
JSON.parse(entry.pop())
)
return events.filter((e) => e.eventName === eventName).map((e) => e.payload)
}
15 changes: 15 additions & 0 deletions test/lib/next-test-utils.js
Expand Up @@ -852,3 +852,18 @@ export function runDevSuite(suiteName, appDir, options) {
export function runProdSuite(suiteName, appDir, options) {
return runSuite(suiteName, { appDir, env: 'prod' }, options)
}

/**
* Parse the output and return all entries that match the provided `eventName`
* @param {string} output output of the console
* @param {string} eventName
* @returns {Array<{}>}
*/
export function findAllTelemetryEvents(output, eventName) {
const regex = /\[telemetry\] ({.+?^})/gms
// Pop the last element of each entry to retrieve contents of the capturing group
const events = [...output.matchAll(regex)].map((entry) =>
JSON.parse(entry.pop())
)
return events.filter((e) => e.eventName === eventName).map((e) => e.payload)
}
14 changes: 14 additions & 0 deletions test/production/next-font/google-font-mocked-responses.js
@@ -0,0 +1,14 @@
module.exports = {
'https://fonts.googleapis.com/css2?family=Open+Sans:wght@300..800&display=optional': `
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300 800;
font-stretch: 100%;
font-display: optional;
src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
`,
}
84 changes: 84 additions & 0 deletions test/production/next-font/telemetry.test.ts
@@ -0,0 +1,84 @@
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { findAllTelemetryEvents } from 'next-test-utils'
import { join } from 'path'

const mockedGoogleFontResponses = require.resolve(
'./google-font-mocked-responses.js'
)

describe('@next/font used telemetry', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'telemetry/pages')),
'next.config.js': new FileRef(
join(__dirname, 'telemetry/next.config.js')
),
},
dependencies: {
'@next/font': 'canary',
},
env: {
NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses,
NEXT_TELEMETRY_DEBUG: '1',
},
})
})
afterAll(() => next.destroy())

it('should send @next/font/google and @next/font/local usage event', async () => {
const events = findAllTelemetryEvents(
next.cliOutput,
'NEXT_BUILD_FEATURE_USAGE'
)
expect(events).toContainEqual({
featureName: '@next/font/google',
invocationCount: 1,
})
expect(events).toContainEqual({
featureName: '@next/font/local',
invocationCount: 1,
})
})
})

describe('@next/font unused telemetry', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'telemetry/pages-unused')),
'next.config.js': new FileRef(
join(__dirname, 'telemetry/next.config.js')
),
},
dependencies: {
'@next/font': 'canary',
},
env: {
NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses,
NEXT_TELEMETRY_DEBUG: '1',
},
})
})
afterAll(() => next.destroy())

it('should not send @next/font/google and @next/font/local usage event', async () => {
const events = findAllTelemetryEvents(
next.cliOutput,
'NEXT_BUILD_FEATURE_USAGE'
)
expect(events).toContainEqual({
featureName: '@next/font/google',
invocationCount: 0,
})
expect(events).toContainEqual({
featureName: '@next/font/local',
invocationCount: 0,
})
})
})
13 changes: 13 additions & 0 deletions test/production/next-font/telemetry/next.config.js
@@ -0,0 +1,13 @@
module.exports = {
experimental: {
fontLoaders: [
{
loader: '@next/font/google',
options: { subsets: ['latin'] },
},
{
loader: '@next/font/local',
},
],
},
}
3 changes: 3 additions & 0 deletions test/production/next-font/telemetry/pages-unused/index.js
@@ -0,0 +1,3 @@
export default function Page() {
return <p>Hello world</p>
}
13 changes: 13 additions & 0 deletions test/production/next-font/telemetry/pages/_app.js
@@ -0,0 +1,13 @@
import localFont from '@next/font/local'

const myFont = localFont({ src: './my-font.woff2' })

function MyApp({ Component, pageProps }) {
return (
<div className={myFont.className}>
<Component {...pageProps} />
</div>
)
}

export default MyApp
11 changes: 11 additions & 0 deletions test/production/next-font/telemetry/pages/index.js
@@ -0,0 +1,11 @@
import { Open_Sans } from '@next/font/google'
const openSans = Open_Sans({ subsets: ['latin'] })

export default function Page() {
return (
<>
<p>Hello world 1</p>
<p className={openSans.className}>Hello world 2</p>
</>
)
}
Binary file not shown.

0 comments on commit 81890c8

Please sign in to comment.