Skip to content

Commit

Permalink
Fix telemetry disable command (#9289)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <[email protected]>
  • Loading branch information
DavidMulder0 and dcousens committed Aug 15, 2024
1 parent c25a5c2 commit 830d46d
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 96 deletions.
5 changes: 5 additions & 0 deletions .changeset/fix-telemetry-disable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@keystone-6/core": patch
---

Fixes the `keystone telemetry disable` command for opting out of telemetry
140 changes: 62 additions & 78 deletions packages/core/src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import https from 'node:https'

import ci from 'ci-info'
import Conf from 'conf'
import chalk from 'chalk'
import {
bold,
yellow as y,
red as r,
green as g
} from 'chalk'
import {
type Configuration,
type Device,
type PackageName,
type Project,
type Telemetry,
type TelemetryVersion1,
type TelemetryVersion2and3,
} from '../types/telemetry'
import { type DatabaseProvider } from '../types'
import { type InitialisedList } from './core/initialise-lists'
Expand All @@ -25,17 +31,6 @@ const packageNames: PackageName[] = [
'@opensaas/keystone-nextjs-auth',
]

type TelemetryVersion1 =
| undefined
| false
| {
device: { lastSentDate?: string, informedAt: string }
projects: {
default: { lastSentDate?: string, informedAt: string }
[projectPath: string]: { lastSentDate?: string, informedAt: string }
}
}

function log (message: unknown) {
if (process.env.KEYSTONE_TELEMETRY_DEBUG === '1') {
console.log(`${message}`)
Expand All @@ -46,38 +41,46 @@ function getTelemetryConfig () {
const userConfig = new Conf<Configuration>({
projectName: 'keystonejs',
projectSuffix: '',
projectVersion: '2.0.0',
projectVersion: '3.0.0',
migrations: {
'^2.0.0': (store: Conf<Configuration>) => {
const existing = store.get('telemetry') as unknown as TelemetryVersion1
if (!existing) return
'^2.0.0': (store) => {
const existing = store.get('telemetry') as TelemetryVersion1
if (!existing) return // skip non-configured or known opt-outs

const replacement: Telemetry = {
// every informedAt was a copy of device.informedAt, it was copied everywhere
informedAt: existing.device.informedAt,
const replacement: TelemetryVersion2and3 = {
informedAt: null, // re-inform
device: {
lastSentDate: existing.device.lastSentDate ?? null,
},
projects: {}, // manually copying this below
projects: {}, // see below
}

// copy existing project lastSentDate's
for (const [projectPath, project] of Object.entries(existing.projects)) {
if (projectPath === 'default') continue // informedAt moved to root
if (projectPath === 'default') continue // informedAt moved to device.lastSentDate

// dont copy garbage
if (typeof project !== 'object') continue
if (typeof project.lastSentDate !== 'string') continue
if (new Date(project.lastSentDate).toString() === 'Invalid Date') continue

// only lastSentDate is retained
// retain lastSentDate
replacement.projects[projectPath] = {
lastSentDate: project.lastSentDate,
}
}

store.set('telemetry', replacement)
},
'^3.0.0': (store) => {
const existing = store.get('telemetry') as TelemetryVersion2and3
if (!existing) return // skip non-configured or known opt-outs

store.set('telemetry', {
...existing,
informedAt: null, // re-inform
} satisfies TelemetryVersion2and3)
},
},
})

Expand All @@ -97,8 +100,8 @@ function getDefaultedTelemetryConfig () {
device: {
lastSentDate: null,
},
projects: {} as Telemetry['projects'], // help Typescript infer the type
},
projects: {},
} as TelemetryVersion2and3, // help Typescript infer the type
userConfig,
}
}
Expand Down Expand Up @@ -147,84 +150,64 @@ function collectPackageVersions () {
}

function printAbout () {
console.log(
`${chalk.yellow('Keystone collects anonymous data when you run')} ${chalk.green(
'"keystone dev"'
)}`
)
console.log(`${y`Keystone collects anonymous data when you run`} ${g`"keystone dev"`}`)
console.log()
console.log(
`For more information, including how to opt-out see https://keystonejs.com/telemetry`
)
console.log(`For more information, including how to opt-out see https://keystonejs.com/telemetry`)
}

export function printTelemetryStatus () {
const { telemetry } = getTelemetryConfig()

if (telemetry === undefined) {
console.log(`Keystone telemetry has been reset to ${chalk.yellow('uninitialized')}`)
console.log()
console.log(
`Telemetry will be sent the next time you run ${chalk.green(
'"keystone dev"'
)}, unless you opt-out`
)
} else if (telemetry === false) {
console.log(`Keystone telemetry is ${chalk.red('disabled')}`)
console.log(`Keystone telemetry has been reset to ${y`uninitialized`}`)
console.log()
console.log(`Telemetry will ${chalk.red('not')} be sent by this system user`)
} else if (typeof telemetry === 'object') {
console.log(`Keystone telemetry is ${chalk.green('enabled')}`)
console.log(`Telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`)
return
}

if (telemetry === false) {
console.log(`Keystone telemetry is ${r`disabled`}`)
console.log()
console.log(`Telemetry will ${r`not`} be sent by this system user`)
return
}

console.log(` Device telemetry was last sent on ${telemetry.device.lastSentDate}`)
for (const [projectPath, project] of Object.entries(telemetry.projects)) {
console.log(
` Project telemetry for "${chalk.yellow(projectPath)}" was last sent on ${
project?.lastSentDate
}`
)
}
console.log(`Keystone telemetry is ${g`enabled`}`)
console.log()

console.log()
console.log(
`Telemetry will be sent the next time you run ${chalk.green(
'"keystone dev"'
)}, unless you opt-out`
)
console.log(` Device telemetry was last sent on ${telemetry.device.lastSentDate}`)
for (const [projectPath, project] of Object.entries(telemetry.projects)) {
console.log(` Project telemetry for "${y(projectPath)}" was last sent on ${project?.lastSentDate}`)
}

console.log()
console.log(`Telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`)
}

function inform () {
const { telemetry, userConfig } = getDefaultedTelemetryConfig()

// no telemetry? somehow our earlier checks missed an opt out, do nothing
if (telemetry === false) return
// no telemetry? somehow we missed something, do nothing
if (!telemetry) return

console.log() // gap to help visiblity
console.log(`${chalk.bold('Keystone Telemetry')}`)
console.log(`${bold('Keystone Telemetry')}`)
printAbout()
console.log(
`You can use ${chalk.green(
'"keystone telemetry --help"'
)} to update your preferences at any time`
)
console.log(`You can use ${g`"keystone telemetry --help"`} to update your preferences at any time`)
console.log()
console.log(
`No telemetry data has been sent yet, but telemetry will be sent the next time you run ${chalk.green(
'"keystone dev"'
)}, unless you opt-out`
)
console.log(`No telemetry data has been sent, but telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`)
console.log() // gap to help visiblity

// update the informedAt
telemetry.informedAt = new Date().toJSON()
userConfig.set('telemetry', telemetry)
}

async function sendEvent (eventType: 'project', eventData: Project): Promise<void>
async function sendEvent (eventType: 'device', eventData: Device): Promise<void>
async function sendEvent (eventType: 'project' | 'device', eventData: Project | Device) {
const endpoint = process.env.KEYSTONE_TELEMETRY_ENDPOINT || defaultTelemetryEndpoint
const req = https.request(`${endpoint}/v1/event/${eventType}`, {
const req = https.request(`${endpoint}/2/event/${eventType}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -242,8 +225,8 @@ async function sendProjectTelemetryEvent (
) {
const { telemetry, userConfig } = getDefaultedTelemetryConfig()

// no telemetry? somehow our earlier checks missed an opt out, do nothing
if (telemetry === false) return
// no telemetry? somehow we missed something, do nothing
if (!telemetry) return

const project = telemetry.projects[cwd] ?? { lastSentDate: null }
const { lastSentDate } = project
Expand All @@ -268,8 +251,8 @@ async function sendProjectTelemetryEvent (
async function sendDeviceTelemetryEvent () {
const { telemetry, userConfig } = getDefaultedTelemetryConfig()

// no telemetry? somehow our earlier checks missed an opt out, do nothing
if (telemetry === false) return
// no telemetry? somehow we missed something, do nothing
if (!telemetry) return

const { lastSentDate } = telemetry.device
if (lastSentDate && lastSentDate >= todaysDate) {
Expand Down Expand Up @@ -305,7 +288,8 @@ export async function runTelemetry (
const { telemetry } = getDefaultedTelemetryConfig()

// don't run if the user has opted out
if (telemetry === false) return
// or if somehow our defaults are problematic, do nothing
if (!telemetry) return

// don't send telemetry before we inform the user, allowing opt-out
if (!telemetry.informedAt) return inform()
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function cli (cwd: string, argv: string[]) {
return prisma(cwd, argv.slice(1), Boolean(flags.frozen))
}

if (command === 'telemetry') {
if (command.startsWith('telemetry')) {
return telemetry(cwd, argv[1])
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/scripts/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function telemetry (cwd: string, command?: string) {
Usage
$ keystone telemetry [command]
Commands
disable opt-out of telemetry, disabled for this system user
disable opt-out of telemetry, disabling telemetry for this system user
enable opt-in to telemetry
reset resets your telemetry configuration (if any)
status show if telemetry is enabled, disabled or uninitialised
Expand Down
36 changes: 25 additions & 11 deletions packages/core/src/types/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import type { DatabaseProvider } from './core'

export type Telemetry = {
informedAt: string | null
device: {
lastSentDate: string | null
}
projects: Partial<{
[projectPath: string]: {
lastSentDate: string
export type TelemetryVersion1 =
| undefined
| false
| {
device: { lastSentDate?: string, informedAt: string }
projects: {
default: { lastSentDate?: string, informedAt: string }
[projectPath: string]: { lastSentDate?: string, informedAt: string }
}
}
}>
}

export type TelemetryVersion2and3 =
| undefined
| false
| {
informedAt: string | null
device: {
lastSentDate: string | null
}
projects: Partial<{
[projectPath: string]: {
lastSentDate: string
}
}>
}

export type Configuration = {
telemetry?: undefined | false | Telemetry
telemetry?: undefined | false | TelemetryVersion2and3
}

export type Device = {
Expand Down
10 changes: 5 additions & 5 deletions packages/core/tests/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('Telemetry tests', () => {
}

function expectDidSend (lastSentDate: string | null) {
expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/v1/event/project`, {
expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/2/event/project`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -148,7 +148,7 @@ describe('Telemetry tests', () => {
})
)

expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/v1/event/device`, {
expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/2/event/device`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -173,12 +173,12 @@ describe('Telemetry tests', () => {
expect(Object.keys(mockTelemetryConfig?.projects).length).toBe(0)
})

test('Telemetry is sent on second run', async () => {
test('Telemetry is sent after inform', async () => {
await runTelemetry(mockProjectDir, lists, 'sqlite') // inform
await runTelemetry(mockProjectDir, lists, 'sqlite') // send

expectDidSend(null)
expect(https.request).toHaveBeenCalledTimes(2)
expect(https.request).toHaveBeenCalledTimes(2) // would be 4 if sent twice
expect(mockTelemetryConfig).toBeDefined()
expect(mockTelemetryConfig?.device.lastSentDate).toBe(today)
expect(mockTelemetryConfig?.projects).toBeDefined()
Expand Down Expand Up @@ -215,7 +215,7 @@ describe('Telemetry tests', () => {
expect(mockTelemetryConfig).toBe(false)
})

test(`Telemetry is not sent if telemetry is disabled`, async () => {
test(`Telemetry is not sent if telemetry configuration is disabled`, async () => {
mockTelemetryConfig = false

await runTelemetry(mockProjectDir, lists, 'sqlite') // inform
Expand Down

0 comments on commit 830d46d

Please sign in to comment.