diff --git a/packages/next/build/webpack/plugins/telemetry-plugin.ts b/packages/next/build/webpack/plugins/telemetry-plugin.ts index 20cc027d385296f..7e62625b45814ae 100644 --- a/packages/next/build/webpack/plugins/telemetry-plugin.ts +++ b/packages/next/build/webpack/plugins/telemetry-plugin.ts @@ -24,6 +24,8 @@ export type Feature = | 'next/legacy/image' | 'next/script' | 'next/dynamic' + | '@next/font/google' + | '@next/font/local' | 'swcLoader' | 'swcMinify' | 'swcRelay' @@ -64,6 +66,10 @@ const FEATURE_MODULE_MAP: ReadonlyMap = new Map([ ['next/script', '/next/script.js'], ['next/dynamic', '/next/dynamic.js'], ]) +const FEATURE_MODULE_REGEXP_MAP: ReadonlyMap = 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 = [ @@ -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 } } @@ -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 { diff --git a/packages/next/telemetry/events/build.ts b/packages/next/telemetry/events/build.ts index 4321b76bb5937cf..7d4a7b962c4fbd7 100644 --- a/packages/next/telemetry/events/build.ts +++ b/packages/next/telemetry/events/build.ts @@ -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' diff --git a/test/integration/telemetry/test/index.test.js b/test/integration/telemetry/test/index.test.js index 4d26bcad9f4d93e..5b2b9d1da6499e9 100644 --- a/test/integration/telemetry/test/index.test.js +++ b/test/integration/telemetry/test/index.test.js @@ -11,6 +11,7 @@ import { nextBuild, nextLint, check, + findAllTelemetryEvents, } from 'next-test-utils' const appDir = path.join(__dirname, '..') @@ -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, @@ -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, @@ -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, @@ -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([ { @@ -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([ { @@ -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, @@ -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, @@ -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, @@ -917,7 +933,7 @@ describe('Telemetry CLI', () => { path.join(appDir, 'package.swc-plugins') ) - const pluginDetectedEvents = findAllEvents( + const pluginDetectedEvents = findAllTelemetryEvents( stderr, 'NEXT_SWC_PLUGIN_DETECTED' ) @@ -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, @@ -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) -} diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index 13727b9ccd807c8..3cdda7813ab5512 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -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) +} diff --git a/test/production/next-font/google-font-mocked-responses.js b/test/production/next-font/google-font-mocked-responses.js new file mode 100644 index 000000000000000..cd7b8e56b0b0286 --- /dev/null +++ b/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; +} + `, +} diff --git a/test/production/next-font/telemetry.test.ts b/test/production/next-font/telemetry.test.ts new file mode 100644 index 000000000000000..8867c1ec780f890 --- /dev/null +++ b/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, + }) + }) +}) diff --git a/test/production/next-font/telemetry/next.config.js b/test/production/next-font/telemetry/next.config.js new file mode 100644 index 000000000000000..6a94ea94ad86409 --- /dev/null +++ b/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', + }, + ], + }, +} diff --git a/test/production/next-font/telemetry/pages-unused/index.js b/test/production/next-font/telemetry/pages-unused/index.js new file mode 100644 index 000000000000000..a681aa7ce257cbe --- /dev/null +++ b/test/production/next-font/telemetry/pages-unused/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return

Hello world

+} diff --git a/test/production/next-font/telemetry/pages/_app.js b/test/production/next-font/telemetry/pages/_app.js new file mode 100644 index 000000000000000..05702a3dc483443 --- /dev/null +++ b/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 ( +
+ +
+ ) +} + +export default MyApp diff --git a/test/production/next-font/telemetry/pages/index.js b/test/production/next-font/telemetry/pages/index.js new file mode 100644 index 000000000000000..26400dc9cebe7fc --- /dev/null +++ b/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 ( + <> +

Hello world 1

+

Hello world 2

+ + ) +} diff --git a/test/production/next-font/telemetry/pages/my-font.woff2 b/test/production/next-font/telemetry/pages/my-font.woff2 new file mode 100644 index 000000000000000..a6b3c3a9d69faa7 Binary files /dev/null and b/test/production/next-font/telemetry/pages/my-font.woff2 differ