From 5006fa8d9236d7552df78c62dab16ddaa9be990c Mon Sep 17 00:00:00 2001 From: DavidMulder0 Date: Wed, 14 Aug 2024 16:45:30 +0200 Subject: [PATCH 1/7] Fix telemetry commands (#9289) --- packages/core/src/scripts/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/scripts/cli.ts b/packages/core/src/scripts/cli.ts index 45920da7a6b..5af75a4e39c 100644 --- a/packages/core/src/scripts/cli.ts +++ b/packages/core/src/scripts/cli.ts @@ -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]) } From a307d660e6448c14a003935be457dfdf8a7d336f Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:25:56 +1000 Subject: [PATCH 2/7] use active tense for command outcomes --- packages/core/src/scripts/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/scripts/telemetry.ts b/packages/core/src/scripts/telemetry.ts index d4b8b337e9f..71576636f43 100644 --- a/packages/core/src/scripts/telemetry.ts +++ b/packages/core/src/scripts/telemetry.ts @@ -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 From dc983a4e378b0d77f45f57c11eda97d53322107e Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:26:44 +1000 Subject: [PATCH 3/7] add changeset --- .changeset/fix-telemetry-disable.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-telemetry-disable.md diff --git a/.changeset/fix-telemetry-disable.md b/.changeset/fix-telemetry-disable.md new file mode 100644 index 00000000000..e58b20d7127 --- /dev/null +++ b/.changeset/fix-telemetry-disable.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/core": patch +--- + +Fixes the `keystone telemetry disable` command for opting out of telemetry From 446ddcc9e86ca08e46a19120ccedaf454159fd0a Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:50:45 +1000 Subject: [PATCH 4/7] tidy up chalk --- packages/core/src/lib/telemetry.ts | 75 ++++++++++++------------------ 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 6b6957c9592..6410b13de37 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -3,7 +3,12 @@ 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, @@ -147,52 +152,38 @@ 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 () { @@ -202,19 +193,11 @@ function inform () { if (telemetry === false) 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 From 394eaa3d89ec53dbaae196dec612719e4501d7ae Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:51:15 +1000 Subject: [PATCH 5/7] move to a new version 2 telemetry endpoint (failing for now) --- packages/core/src/lib/telemetry.ts | 2 +- packages/core/tests/telemetry.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 6410b13de37..bc582190fe5 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -207,7 +207,7 @@ function inform () { 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', diff --git a/packages/core/tests/telemetry.test.ts b/packages/core/tests/telemetry.test.ts index ab1c22905d5..09ae961e320 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -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', @@ -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', From d237d20a52f619db87d70f67a075230cfac794f2 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:53:40 +1000 Subject: [PATCH 6/7] add version 3 telemetry configuration to re-inform unknown opt-ins --- packages/core/src/lib/telemetry.ts | 61 ++++++++++++++-------------- packages/core/src/types/telemetry.ts | 36 +++++++++++----- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index bc582190fe5..3f4ad67b6ba 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -14,7 +14,8 @@ import { 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' @@ -30,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}`) @@ -51,31 +41,30 @@ function getTelemetryConfig () { const userConfig = new Conf({ projectName: 'keystonejs', projectSuffix: '', - projectVersion: '2.0.0', + projectVersion: '3.0.0', migrations: { - '^2.0.0': (store: Conf) => { - 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, } @@ -83,6 +72,15 @@ function getTelemetryConfig () { 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) + }, }, }) @@ -102,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, } } @@ -189,8 +187,8 @@ export function printTelemetryStatus () { 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 our earlier missed something, do nothing + if (!telemetry) return console.log() // gap to help visiblity console.log(`${bold('Keystone Telemetry')}`) @@ -225,8 +223,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 our earlier missed something, do nothing + if (!telemetry) return const project = telemetry.projects[cwd] ?? { lastSentDate: null } const { lastSentDate } = project @@ -251,8 +249,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 our earlier missed something, do nothing + if (!telemetry) return const { lastSentDate } = telemetry.device if (lastSentDate && lastSentDate >= todaysDate) { @@ -288,7 +286,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() diff --git a/packages/core/src/types/telemetry.ts b/packages/core/src/types/telemetry.ts index ce845955fa2..334b852fe60 100644 --- a/packages/core/src/types/telemetry.ts +++ b/packages/core/src/types/telemetry.ts @@ -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 = { From 8745a398dfaa79821c5dd4878e6c74ae2a90c447 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:56:11 +1000 Subject: [PATCH 7/7] update test titles after review --- packages/core/tests/telemetry.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/tests/telemetry.test.ts b/packages/core/tests/telemetry.test.ts index 09ae961e320..429e005cd59 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -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() @@ -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