2019-08-29 18:43:06 +02:00
|
|
|
import chalk from 'chalk'
|
|
|
|
import ciEnvironment from 'ci-info'
|
|
|
|
import Conf from 'conf'
|
|
|
|
import { BinaryLike, createHash, randomBytes } from 'crypto'
|
|
|
|
import findUp from 'find-up'
|
|
|
|
import isDockerFunction from 'is-docker'
|
|
|
|
import path from 'path'
|
2019-09-07 16:51:09 +02:00
|
|
|
|
|
|
|
import { getAnonymousMeta } from './anonymous-meta'
|
2019-08-29 18:43:06 +02:00
|
|
|
import { _postPayload } from './post-payload'
|
|
|
|
import { getProjectId } from './project-id'
|
|
|
|
|
|
|
|
let config: Conf<any> | undefined
|
|
|
|
let projectId: string | undefined
|
|
|
|
let randomRunId: string | undefined
|
|
|
|
|
|
|
|
// This is the key that stores whether or not telemetry is enabled or disabled.
|
|
|
|
const TELEMETRY_KEY_ENABLED = 'telemetry.enabled'
|
|
|
|
|
|
|
|
// This is the key that specifies when the user was informed about anonymous
|
|
|
|
// telemetry collection.
|
|
|
|
const TELEMETRY_KEY_NOTIFY_DATE = 'telemetry.notifiedAt'
|
|
|
|
|
|
|
|
// This is a quasi-persistent identifier used to dedupe recurring events. It's
|
|
|
|
// generated from random data and completely anonymous.
|
|
|
|
const TELEMETRY_KEY_ID = `telemetry.anonymousId`
|
|
|
|
|
|
|
|
// This is the cryptographic salt that is included within every hashed value.
|
|
|
|
// This salt value is never sent to us, ensuring privacy and the one-way nature
|
|
|
|
// of the hash (prevents dictionary lookups of pre-computed hashes).
|
|
|
|
// See the `computeHash` function.
|
|
|
|
const TELEMETRY_KEY_SALT = `telemetry.salt`
|
|
|
|
|
|
|
|
const { NEXT_TELEMETRY_DISABLED, NEXT_TELEMETRY_DEBUG } = process.env
|
|
|
|
|
2019-09-07 20:11:17 +02:00
|
|
|
let isDisabled: boolean = !!NEXT_TELEMETRY_DISABLED
|
2019-08-29 18:43:06 +02:00
|
|
|
|
|
|
|
function notify() {
|
|
|
|
// No notification needed if telemetry is not enabled
|
|
|
|
if (!config || isDisabled) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// The end-user has already been notified about our telemetry integration. We
|
|
|
|
// don't need to constantly annoy them about it.
|
|
|
|
// We will re-inform users about the telemetry if significant changes are
|
|
|
|
// ever made.
|
|
|
|
if (config.get(TELEMETRY_KEY_NOTIFY_DATE, '')) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
config.set(TELEMETRY_KEY_NOTIFY_DATE, Date.now().toString())
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
`${chalk.magenta.bold(
|
|
|
|
'Attention'
|
|
|
|
)}: Next.js now collects completely anonymous telemetry regarding usage.`
|
|
|
|
)
|
|
|
|
console.log(
|
|
|
|
`This information is used to shape Next.js' roadmap and prioritize features.`
|
|
|
|
)
|
|
|
|
console.log(
|
|
|
|
`You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:`
|
|
|
|
)
|
|
|
|
console.log(chalk.cyan('https://nextjs.org/telemetry'))
|
|
|
|
console.log()
|
|
|
|
}
|
|
|
|
|
|
|
|
function setup() {
|
|
|
|
if (config) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let cwd =
|
|
|
|
ciEnvironment.isCI || isDockerFunction()
|
|
|
|
? // CI environments will normally cache `node_modules/`
|
|
|
|
findUp.sync('node_modules')
|
|
|
|
: undefined
|
|
|
|
if (cwd) cwd = path.join(cwd, '.next')
|
|
|
|
|
|
|
|
config = new Conf({ projectName: 'nextjs', cwd })
|
|
|
|
|
|
|
|
let anonymousId = config.get(TELEMETRY_KEY_ID)
|
|
|
|
if (!anonymousId) {
|
|
|
|
config.set(
|
|
|
|
TELEMETRY_KEY_ID,
|
|
|
|
(anonymousId = randomBytes(32).toString('hex'))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!config.get(TELEMETRY_KEY_SALT)) {
|
|
|
|
config.set(TELEMETRY_KEY_SALT, randomBytes(16).toString('hex'))
|
|
|
|
}
|
|
|
|
|
|
|
|
projectId = getProjectId()
|
|
|
|
randomRunId = randomBytes(8).toString('hex')
|
|
|
|
|
|
|
|
if (config.get(TELEMETRY_KEY_ENABLED, true) === false) {
|
|
|
|
isDisabled = true
|
|
|
|
}
|
|
|
|
|
|
|
|
notify()
|
|
|
|
}
|
|
|
|
|
|
|
|
export function computeHash(payload: BinaryLike): string | null {
|
|
|
|
setup()
|
|
|
|
|
|
|
|
const salt = config!.get(TELEMETRY_KEY_SALT)
|
|
|
|
if (!salt) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
const hash = createHash('sha256')
|
|
|
|
|
|
|
|
// Always prepend the payload value with salt. This ensures the hash is truly
|
|
|
|
// one-way.
|
|
|
|
hash.update(salt)
|
|
|
|
|
|
|
|
// Update is an append operation, not a replacement. The salt from the prior
|
|
|
|
// update is still present!
|
|
|
|
hash.update(payload)
|
|
|
|
return hash.digest('hex')
|
|
|
|
}
|
|
|
|
|
|
|
|
export function setTelemetryEnabled(_enabled: boolean) {
|
|
|
|
setup()
|
|
|
|
|
|
|
|
const enabled = !!_enabled
|
|
|
|
config!.set(TELEMETRY_KEY_ENABLED, enabled)
|
|
|
|
isDisabled = !enabled
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isTelemetryEnabled(): boolean {
|
|
|
|
setup()
|
|
|
|
|
|
|
|
return config!.get(TELEMETRY_KEY_ENABLED, true) !== false
|
|
|
|
}
|
|
|
|
|
|
|
|
type TelemetryEvent = { eventName: string; payload: object }
|
2019-09-05 02:31:35 +02:00
|
|
|
type EventContext = {
|
|
|
|
anonymousId: string
|
|
|
|
projectId: string
|
|
|
|
sessionId: string
|
|
|
|
}
|
2019-09-07 16:51:09 +02:00
|
|
|
type EventMeta = { [key: string]: unknown }
|
2019-09-05 02:31:35 +02:00
|
|
|
type EventBatchShape = {
|
|
|
|
eventName: string
|
|
|
|
fields: object
|
|
|
|
}
|
2019-08-29 18:43:06 +02:00
|
|
|
function _record(_events: TelemetryEvent | TelemetryEvent[]): Promise<any> {
|
|
|
|
let events: TelemetryEvent[]
|
|
|
|
if (Array.isArray(_events)) {
|
|
|
|
events = _events
|
|
|
|
} else {
|
|
|
|
events = [_events]
|
|
|
|
}
|
|
|
|
|
|
|
|
if (events.length < 1) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
|
|
|
setup()
|
|
|
|
|
|
|
|
if (NEXT_TELEMETRY_DEBUG) {
|
|
|
|
// Print to standard error to simplify selecting the output
|
|
|
|
events.forEach(({ eventName, payload }) =>
|
|
|
|
console.error(
|
|
|
|
`[telemetry] ` + JSON.stringify({ eventName, payload }, null, 2)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
// Do not send the telemetry data if debugging. Users may use this feature
|
|
|
|
// to preview what data would be sent.
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip recording telemetry if the feature is disabled
|
|
|
|
if (isDisabled) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
|
|
|
const anonymousId = config!.get(TELEMETRY_KEY_ID)
|
|
|
|
if (!anonymousId) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
2019-09-05 02:31:35 +02:00
|
|
|
const context: EventContext = {
|
|
|
|
anonymousId: anonymousId,
|
|
|
|
projectId: projectId!,
|
|
|
|
sessionId: randomRunId!,
|
|
|
|
}
|
2019-09-07 16:51:09 +02:00
|
|
|
const meta: EventMeta = getAnonymousMeta()
|
2019-08-29 18:43:06 +02:00
|
|
|
return _postPayload(`https://telemetry.nextjs.org/api/v1/record`, {
|
|
|
|
context,
|
2019-09-07 16:51:09 +02:00
|
|
|
meta,
|
2019-08-29 18:43:06 +02:00
|
|
|
events: events.map(({ eventName, payload }) => ({
|
|
|
|
eventName,
|
|
|
|
fields: payload,
|
2019-09-05 02:31:35 +02:00
|
|
|
})) as Array<EventBatchShape>,
|
2019-08-29 18:43:06 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
export function record(
|
|
|
|
_events: TelemetryEvent | TelemetryEvent[]
|
|
|
|
): Promise<{
|
|
|
|
isFulfilled: boolean
|
|
|
|
isRejected: boolean
|
|
|
|
value?: any
|
|
|
|
reason?: any
|
|
|
|
}> {
|
|
|
|
// pseudo try-catch
|
|
|
|
async function wrapper() {
|
|
|
|
return await _record(_events)
|
|
|
|
}
|
|
|
|
|
|
|
|
return wrapper()
|
|
|
|
.then(value => ({
|
|
|
|
isFulfilled: true,
|
|
|
|
isRejected: false,
|
|
|
|
value,
|
|
|
|
}))
|
|
|
|
.catch(reason => ({
|
|
|
|
isFulfilled: false,
|
|
|
|
isRejected: true,
|
|
|
|
reason,
|
|
|
|
}))
|
|
|
|
}
|