Skip to content

Commit 34e19b1

Browse files
Merge branch 'canary' into feat/configurable-hostname
2 parents 50449b7 + 3a9e532 commit 34e19b1

File tree

6 files changed

+136
-23
lines changed

6 files changed

+136
-23
lines changed

docs/advanced-features/using-mdx.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,17 @@ The following steps outline how to setup `@next/mdx` in your Next.js project:
5353
// providerImportSource: "@mdx-js/react",
5454
},
5555
})
56-
module.exports = withMDX({
57-
// Append the default value with md extensions
56+
57+
/** @type {import('next').NextConfig} */
58+
const nextConfig = {
59+
// Configure pageExtensions to include md and mdx
5860
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
59-
})
61+
// Optionally, add any other Next.js config below
62+
reactStrictMode: true,
63+
}
64+
65+
// Merge MDX config with Next.js config
66+
module.exports = withMDX(nextConfig)
6067
```
6168

6269
3. Create a new MDX page within the `/pages` directory:

packages/next/src/cli/next-dev.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ const handleSessionStop = async () => {
6060
durationMilliseconds: Date.now() - sessionStarted,
6161
pagesDir,
6262
appDir,
63-
})
63+
}),
64+
true
6465
)
65-
await telemetry.flush()
66+
telemetry.flushDetached('dev', dir)
6667
} catch (err) {
6768
console.error(err)
6869
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import { Telemetry, TelemetryEvent } from './storage'
4+
import loadConfig from '../server/config'
5+
import { getProjectDir } from '../lib/get-project-dir'
6+
import { PHASE_DEVELOPMENT_SERVER } from '../shared/lib/constants'
7+
8+
// this process should be started with following arg order
9+
// 1. mode e.g. dev, export, start
10+
// 2. project dir
11+
;(async () => {
12+
const args = [...process.argv]
13+
let dir = args.pop()
14+
const mode = args.pop()
15+
16+
if (!dir || mode !== 'dev') {
17+
throw new Error(
18+
`Invalid flags should be run as node detached-flush dev ./path-to/project`
19+
)
20+
}
21+
dir = getProjectDir(dir)
22+
23+
const config = await loadConfig(PHASE_DEVELOPMENT_SERVER, dir)
24+
const distDir = path.join(dir, config.distDir || '.next')
25+
const eventsPath = path.join(distDir, '_events.json')
26+
27+
let events: TelemetryEvent[]
28+
try {
29+
events = JSON.parse(fs.readFileSync(eventsPath, 'utf8'))
30+
} catch (err: any) {
31+
if (err.code === 'ENOENT') {
32+
// no events to process we can exit now
33+
process.exit(0)
34+
}
35+
throw err
36+
}
37+
38+
const telemetry = new Telemetry({ distDir })
39+
await telemetry.record(events)
40+
await telemetry.flush()
41+
42+
// finished flushing events clean-up/exit
43+
fs.unlinkSync(eventsPath)
44+
process.exit(0)
45+
})()

packages/next/src/telemetry/post-payload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import retry from 'next/dist/compiled/async-retry'
22
import fetch from 'next/dist/compiled/node-fetch'
33

4-
export function _postPayload(endpoint: string, body: object) {
4+
export function _postPayload(endpoint: string, body: object, signal?: any) {
55
return (
66
retry(
77
() =>
@@ -10,6 +10,7 @@ export function _postPayload(endpoint: string, body: object) {
1010
body: JSON.stringify(body),
1111
headers: { 'content-type': 'application/json' },
1212
timeout: 5000,
13+
signal,
1314
}).then((res) => {
1415
if (!res.ok) {
1516
const err = new Error(res.statusText)

packages/next/src/telemetry/storage.ts

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { getAnonymousMeta } from './anonymous-meta'
88
import * as ciEnvironment from './ci-info'
99
import { _postPayload } from './post-payload'
1010
import { getRawProjectId } from './project-id'
11+
import { AbortController } from 'next/dist/compiled/@edge-runtime/primitives/abort-controller'
12+
import fs from 'fs'
13+
import spawn from 'next/dist/compiled/cross-spawn'
1114

1215
// This is the key that stores whether or not telemetry is enabled or disabled.
1316
const TELEMETRY_KEY_ENABLED = 'telemetry.enabled'
@@ -26,7 +29,7 @@ const TELEMETRY_KEY_ID = `telemetry.anonymousId`
2629
// See the `oneWayHash` function.
2730
const TELEMETRY_KEY_SALT = `telemetry.salt`
2831

29-
type TelemetryEvent = { eventName: string; payload: object }
32+
export type TelemetryEvent = { eventName: string; payload: object }
3033
type EventContext = {
3134
anonymousId: string
3235
projectId: string
@@ -57,6 +60,7 @@ function getStorageDirectory(distDir: string): string | undefined {
5760

5861
export class Telemetry {
5962
private conf: Conf<any> | null
63+
private distDir: string
6064
private sessionId: string
6165
private rawProjectId: string
6266
private NEXT_TELEMETRY_DISABLED: any
@@ -69,6 +73,7 @@ export class Telemetry {
6973
const { NEXT_TELEMETRY_DISABLED, NEXT_TELEMETRY_DEBUG } = process.env
7074
this.NEXT_TELEMETRY_DISABLED = NEXT_TELEMETRY_DISABLED
7175
this.NEXT_TELEMETRY_DEBUG = NEXT_TELEMETRY_DEBUG
76+
this.distDir = distDir
7277
const storageDirectory = getStorageDirectory(distDir)
7378

7479
try {
@@ -177,15 +182,23 @@ export class Telemetry {
177182
}
178183

179184
record = (
180-
_events: TelemetryEvent | TelemetryEvent[]
185+
_events: TelemetryEvent | TelemetryEvent[],
186+
deferred?: boolean
181187
): Promise<RecordObject> => {
182-
const _this = this
183-
// pseudo try-catch
184-
async function wrapper() {
185-
return await _this.submitRecord(_events)
186-
}
187-
188-
const prom = wrapper()
188+
const prom = (
189+
deferred
190+
? // if we know we are going to immediately call
191+
// flushDetached we can skip starting the initial
192+
// submitRecord which will then be cancelled
193+
new Promise((resolve) =>
194+
resolve({
195+
isFulfilled: true,
196+
isRejected: false,
197+
value: _events,
198+
})
199+
)
200+
: this.submitRecord(_events)
201+
)
189202
.then((value) => ({
190203
isFulfilled: true,
191204
isRejected: false,
@@ -203,6 +216,8 @@ export class Telemetry {
203216
return res
204217
})
205218

219+
;(prom as any)._events = Array.isArray(_events) ? _events : [_events]
220+
;(prom as any)._controller = (prom as any)._controller
206221
// Track this `Promise` so we can flush pending events
207222
this.queue.add(prom)
208223

@@ -211,6 +226,35 @@ export class Telemetry {
211226

212227
flush = async () => Promise.all(this.queue).catch(() => null)
213228

229+
// writes current events to disk and spawns separate
230+
// detached process to submit the records without blocking
231+
// the main process from exiting
232+
flushDetached = (mode: 'dev', dir: string) => {
233+
const allEvents: TelemetryEvent[] = []
234+
235+
this.queue.forEach((item: any) => {
236+
try {
237+
item._controller?.abort()
238+
allEvents.push(...item._events)
239+
} catch (_) {
240+
// if we fail to abort ignore this event
241+
}
242+
})
243+
fs.writeFileSync(
244+
path.join(this.distDir, '_events.json'),
245+
JSON.stringify(allEvents)
246+
)
247+
248+
spawn('node', [require.resolve('./deteched-flush'), mode, dir], {
249+
detached: !this.NEXT_TELEMETRY_DEBUG,
250+
...(this.NEXT_TELEMETRY_DEBUG
251+
? {
252+
stdio: 'inherit',
253+
}
254+
: {}),
255+
})
256+
}
257+
214258
private submitRecord = (
215259
_events: TelemetryEvent | TelemetryEvent[]
216260
): Promise<any> => {
@@ -248,13 +292,20 @@ export class Telemetry {
248292
sessionId: this.sessionId,
249293
}
250294
const meta: EventMeta = getAnonymousMeta()
251-
return _postPayload(`https://telemetry.nextjs.org/api/v1/record`, {
252-
context,
253-
meta,
254-
events: events.map(({ eventName, payload }) => ({
255-
eventName,
256-
fields: payload,
257-
})) as Array<EventBatchShape>,
258-
})
295+
const postController = new AbortController()
296+
const res = _postPayload(
297+
`https://telemetry.nextjs.org/api/v1/record`,
298+
{
299+
context,
300+
meta,
301+
events: events.map(({ eventName, payload }) => ({
302+
eventName,
303+
fields: payload,
304+
})) as Array<EventBatchShape>,
305+
},
306+
postController.signal
307+
)
308+
res._controller = postController
309+
return res
259310
}
260311
}

test/integration/telemetry/test/index.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,10 @@ describe('Telemetry CLI', () => {
480480

481481
expect(event1).toMatch(/"pagesDir": true/)
482482
expect(event1).toMatch(/"turboFlag": true/)
483+
484+
expect(await fs.pathExists(path.join(appDir, '.next/_events.json'))).toBe(
485+
false
486+
)
483487
} finally {
484488
await teardown()
485489
}
@@ -516,6 +520,10 @@ describe('Telemetry CLI', () => {
516520
expect(event1).toMatch(/"turboFlag": false/)
517521
expect(event1).toMatch(/"pagesDir": true/)
518522
expect(event1).toMatch(/"appDir": true/)
523+
524+
expect(await fs.pathExists(path.join(appDir, '.next/_events.json'))).toBe(
525+
false
526+
)
519527
} finally {
520528
await teardown()
521529
}

0 commit comments

Comments
 (0)