2019-08-29 18:43:06 +02:00
|
|
|
import chalk from 'chalk'
|
|
|
|
import Conf from 'conf'
|
|
|
|
import { BinaryLike, createHash, randomBytes } from 'crypto'
|
|
|
|
import isDockerFunction from 'is-docker'
|
|
|
|
import path from 'path'
|
2019-09-07 16:51:09 +02:00
|
|
|
|
|
|
|
import { getAnonymousMeta } from './anonymous-meta'
|
2019-09-26 16:59:24 +02:00
|
|
|
import * as ciEnvironment from './ci-info'
|
2019-08-29 18:43:06 +02:00
|
|
|
import { _postPayload } from './post-payload'
|
2019-10-10 19:18:07 +02:00
|
|
|
import { getRawProjectId } from './project-id'
|
2019-08-29 18:43:06 +02:00
|
|
|
|
|
|
|
// 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).
|
2019-10-10 19:18:07 +02:00
|
|
|
// See the `oneWayHash` function.
|
2019-08-29 18:43:06 +02:00
|
|
|
const TELEMETRY_KEY_SALT = `telemetry.salt`
|
|
|
|
|
|
|
|
const { NEXT_TELEMETRY_DISABLED, NEXT_TELEMETRY_DEBUG } = process.env
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
type TelemetryEvent = { eventName: string; payload: object }
|
|
|
|
type EventContext = {
|
|
|
|
anonymousId: string
|
|
|
|
projectId: string
|
|
|
|
sessionId: string
|
|
|
|
}
|
|
|
|
type EventMeta = { [key: string]: unknown }
|
|
|
|
type EventBatchShape = {
|
|
|
|
eventName: string
|
|
|
|
fields: object
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
export class Telemetry {
|
|
|
|
private conf: Conf<any>
|
|
|
|
private sessionId: string
|
|
|
|
private rawProjectId: string
|
2019-08-29 18:43:06 +02:00
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
constructor({ distDir }: { distDir: string }) {
|
|
|
|
const storageDirectory = getStorageDirectory(distDir)
|
2019-08-29 18:43:06 +02:00
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
this.conf = new Conf({ projectName: 'nextjs', cwd: storageDirectory })
|
|
|
|
this.sessionId = randomBytes(32).toString('hex')
|
|
|
|
this.rawProjectId = getRawProjectId()
|
2019-08-29 18:43:06 +02:00
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
this.notify()
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
private notify = () => {
|
|
|
|
if (this.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 (this.conf.get(TELEMETRY_KEY_NOTIFY_DATE, '')) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.conf.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()
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
get anonymousId(): string {
|
|
|
|
const val = this.conf.get(TELEMETRY_KEY_ID)
|
|
|
|
if (val) {
|
|
|
|
return val
|
|
|
|
}
|
2019-08-29 18:43:06 +02:00
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
const generated = randomBytes(32).toString('hex')
|
|
|
|
this.conf.set(TELEMETRY_KEY_ID, generated)
|
|
|
|
return generated
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
get salt(): string {
|
|
|
|
const val = this.conf.get(TELEMETRY_KEY_SALT)
|
|
|
|
if (val) {
|
|
|
|
return val
|
|
|
|
}
|
2019-08-29 18:43:06 +02:00
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
const generated = randomBytes(16).toString('hex')
|
|
|
|
this.conf.set(TELEMETRY_KEY_SALT, generated)
|
|
|
|
return generated
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
private get isDisabled(): boolean {
|
|
|
|
if (!!NEXT_TELEMETRY_DISABLED) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return this.conf.get(TELEMETRY_KEY_ENABLED, true) === false
|
|
|
|
}
|
2019-08-29 18:43:06 +02:00
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
setEnabled = (_enabled: boolean) => {
|
|
|
|
const enabled = !!_enabled
|
|
|
|
this.conf.set(TELEMETRY_KEY_ENABLED, enabled)
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
get isEnabled(): boolean {
|
|
|
|
return this.conf.get(TELEMETRY_KEY_ENABLED, true) !== false
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
oneWayHash = (payload: BinaryLike): string => {
|
|
|
|
const hash = createHash('sha256')
|
2019-08-29 18:43:06 +02:00
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
// Always prepend the payload value with salt. This ensures the hash is truly
|
|
|
|
// one-way.
|
|
|
|
hash.update(this.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')
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
private get projectId(): string {
|
|
|
|
return this.oneWayHash(this.rawProjectId)
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
record = (
|
|
|
|
_events: TelemetryEvent | TelemetryEvent[]
|
|
|
|
): Promise<{
|
|
|
|
isFulfilled: boolean
|
|
|
|
isRejected: boolean
|
|
|
|
value?: any
|
|
|
|
reason?: any
|
|
|
|
}> => {
|
|
|
|
const _this = this
|
|
|
|
// pseudo try-catch
|
|
|
|
async function wrapper() {
|
|
|
|
return await _this.submitRecord(_events)
|
|
|
|
}
|
|
|
|
|
|
|
|
return wrapper()
|
|
|
|
.then(value => ({
|
|
|
|
isFulfilled: true,
|
|
|
|
isRejected: false,
|
|
|
|
value,
|
|
|
|
}))
|
|
|
|
.catch(reason => ({
|
|
|
|
isFulfilled: false,
|
|
|
|
isRejected: true,
|
|
|
|
reason,
|
|
|
|
}))
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
private submitRecord = (
|
|
|
|
_events: TelemetryEvent | TelemetryEvent[]
|
|
|
|
): Promise<any> => {
|
|
|
|
let events: TelemetryEvent[]
|
|
|
|
if (Array.isArray(_events)) {
|
|
|
|
events = _events
|
|
|
|
} else {
|
|
|
|
events = [_events]
|
|
|
|
}
|
|
|
|
|
|
|
|
if (events.length < 1) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
|
|
|
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 (this.isDisabled) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
|
|
|
const context: EventContext = {
|
|
|
|
anonymousId: this.anonymousId,
|
|
|
|
projectId: this.projectId,
|
|
|
|
sessionId: this.sessionId,
|
|
|
|
}
|
|
|
|
const meta: EventMeta = getAnonymousMeta()
|
|
|
|
return _postPayload(`https://telemetry.nextjs.org/api/v1/record`, {
|
|
|
|
context,
|
|
|
|
meta,
|
|
|
|
events: events.map(({ eventName, payload }) => ({
|
|
|
|
eventName,
|
|
|
|
fields: payload,
|
|
|
|
})) as Array<EventBatchShape>,
|
|
|
|
})
|
2019-09-05 02:31:35 +02:00
|
|
|
}
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
function getStorageDirectory(distDir: string): string | undefined {
|
|
|
|
const isLikelyEphemeral = ciEnvironment.isCI || isDockerFunction()
|
|
|
|
|
|
|
|
if (isLikelyEphemeral) {
|
|
|
|
return path.join(distDir, 'cache')
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|
|
|
|
|
2019-10-10 19:18:07 +02:00
|
|
|
return undefined
|
2019-08-29 18:43:06 +02:00
|
|
|
}
|