Skip to content

Commit 830d46d

Browse files
Fix telemetry disable command (#9289)
Co-authored-by: Daniel Cousens <[email protected]>
1 parent c25a5c2 commit 830d46d

File tree

6 files changed

+99
-96
lines changed

6 files changed

+99
-96
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@keystone-6/core": patch
3+
---
4+
5+
Fixes the `keystone telemetry disable` command for opting out of telemetry

packages/core/src/lib/telemetry.ts

Lines changed: 62 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ import https from 'node:https'
33

44
import ci from 'ci-info'
55
import Conf from 'conf'
6-
import chalk from 'chalk'
6+
import {
7+
bold,
8+
yellow as y,
9+
red as r,
10+
green as g
11+
} from 'chalk'
712
import {
813
type Configuration,
914
type Device,
1015
type PackageName,
1116
type Project,
12-
type Telemetry,
17+
type TelemetryVersion1,
18+
type TelemetryVersion2and3,
1319
} from '../types/telemetry'
1420
import { type DatabaseProvider } from '../types'
1521
import { type InitialisedList } from './core/initialise-lists'
@@ -25,17 +31,6 @@ const packageNames: PackageName[] = [
2531
'@opensaas/keystone-nextjs-auth',
2632
]
2733

28-
type TelemetryVersion1 =
29-
| undefined
30-
| false
31-
| {
32-
device: { lastSentDate?: string, informedAt: string }
33-
projects: {
34-
default: { lastSentDate?: string, informedAt: string }
35-
[projectPath: string]: { lastSentDate?: string, informedAt: string }
36-
}
37-
}
38-
3934
function log (message: unknown) {
4035
if (process.env.KEYSTONE_TELEMETRY_DEBUG === '1') {
4136
console.log(`${message}`)
@@ -46,38 +41,46 @@ function getTelemetryConfig () {
4641
const userConfig = new Conf<Configuration>({
4742
projectName: 'keystonejs',
4843
projectSuffix: '',
49-
projectVersion: '2.0.0',
44+
projectVersion: '3.0.0',
5045
migrations: {
51-
'^2.0.0': (store: Conf<Configuration>) => {
52-
const existing = store.get('telemetry') as unknown as TelemetryVersion1
53-
if (!existing) return
46+
'^2.0.0': (store) => {
47+
const existing = store.get('telemetry') as TelemetryVersion1
48+
if (!existing) return // skip non-configured or known opt-outs
5449

55-
const replacement: Telemetry = {
56-
// every informedAt was a copy of device.informedAt, it was copied everywhere
57-
informedAt: existing.device.informedAt,
50+
const replacement: TelemetryVersion2and3 = {
51+
informedAt: null, // re-inform
5852
device: {
5953
lastSentDate: existing.device.lastSentDate ?? null,
6054
},
61-
projects: {}, // manually copying this below
55+
projects: {}, // see below
6256
}
6357

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

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

73-
// only lastSentDate is retained
67+
// retain lastSentDate
7468
replacement.projects[projectPath] = {
7569
lastSentDate: project.lastSentDate,
7670
}
7771
}
7872

7973
store.set('telemetry', replacement)
8074
},
75+
'^3.0.0': (store) => {
76+
const existing = store.get('telemetry') as TelemetryVersion2and3
77+
if (!existing) return // skip non-configured or known opt-outs
78+
79+
store.set('telemetry', {
80+
...existing,
81+
informedAt: null, // re-inform
82+
} satisfies TelemetryVersion2and3)
83+
},
8184
},
8285
})
8386

@@ -97,8 +100,8 @@ function getDefaultedTelemetryConfig () {
97100
device: {
98101
lastSentDate: null,
99102
},
100-
projects: {} as Telemetry['projects'], // help Typescript infer the type
101-
},
103+
projects: {},
104+
} as TelemetryVersion2and3, // help Typescript infer the type
102105
userConfig,
103106
}
104107
}
@@ -147,84 +150,64 @@ function collectPackageVersions () {
147150
}
148151

149152
function printAbout () {
150-
console.log(
151-
`${chalk.yellow('Keystone collects anonymous data when you run')} ${chalk.green(
152-
'"keystone dev"'
153-
)}`
154-
)
153+
console.log(`${y`Keystone collects anonymous data when you run`} ${g`"keystone dev"`}`)
155154
console.log()
156-
console.log(
157-
`For more information, including how to opt-out see https://keystonejs.com/telemetry`
158-
)
155+
console.log(`For more information, including how to opt-out see https://keystonejs.com/telemetry`)
159156
}
160157

161158
export function printTelemetryStatus () {
162159
const { telemetry } = getTelemetryConfig()
163160

164161
if (telemetry === undefined) {
165-
console.log(`Keystone telemetry has been reset to ${chalk.yellow('uninitialized')}`)
166-
console.log()
167-
console.log(
168-
`Telemetry will be sent the next time you run ${chalk.green(
169-
'"keystone dev"'
170-
)}, unless you opt-out`
171-
)
172-
} else if (telemetry === false) {
173-
console.log(`Keystone telemetry is ${chalk.red('disabled')}`)
162+
console.log(`Keystone telemetry has been reset to ${y`uninitialized`}`)
174163
console.log()
175-
console.log(`Telemetry will ${chalk.red('not')} be sent by this system user`)
176-
} else if (typeof telemetry === 'object') {
177-
console.log(`Keystone telemetry is ${chalk.green('enabled')}`)
164+
console.log(`Telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`)
165+
return
166+
}
167+
168+
if (telemetry === false) {
169+
console.log(`Keystone telemetry is ${r`disabled`}`)
178170
console.log()
171+
console.log(`Telemetry will ${r`not`} be sent by this system user`)
172+
return
173+
}
179174

180-
console.log(` Device telemetry was last sent on ${telemetry.device.lastSentDate}`)
181-
for (const [projectPath, project] of Object.entries(telemetry.projects)) {
182-
console.log(
183-
` Project telemetry for "${chalk.yellow(projectPath)}" was last sent on ${
184-
project?.lastSentDate
185-
}`
186-
)
187-
}
175+
console.log(`Keystone telemetry is ${g`enabled`}`)
176+
console.log()
188177

189-
console.log()
190-
console.log(
191-
`Telemetry will be sent the next time you run ${chalk.green(
192-
'"keystone dev"'
193-
)}, unless you opt-out`
194-
)
178+
console.log(` Device telemetry was last sent on ${telemetry.device.lastSentDate}`)
179+
for (const [projectPath, project] of Object.entries(telemetry.projects)) {
180+
console.log(` Project telemetry for "${y(projectPath)}" was last sent on ${project?.lastSentDate}`)
195181
}
182+
183+
console.log()
184+
console.log(`Telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`)
196185
}
197186

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

201-
// no telemetry? somehow our earlier checks missed an opt out, do nothing
202-
if (telemetry === false) return
190+
// no telemetry? somehow we missed something, do nothing
191+
if (!telemetry) return
203192

204193
console.log() // gap to help visiblity
205-
console.log(`${chalk.bold('Keystone Telemetry')}`)
194+
console.log(`${bold('Keystone Telemetry')}`)
206195
printAbout()
207-
console.log(
208-
`You can use ${chalk.green(
209-
'"keystone telemetry --help"'
210-
)} to update your preferences at any time`
211-
)
196+
console.log(`You can use ${g`"keystone telemetry --help"`} to update your preferences at any time`)
212197
console.log()
213-
console.log(
214-
`No telemetry data has been sent yet, but telemetry will be sent the next time you run ${chalk.green(
215-
'"keystone dev"'
216-
)}, unless you opt-out`
217-
)
198+
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`)
218199
console.log() // gap to help visiblity
219200

220201
// update the informedAt
221202
telemetry.informedAt = new Date().toJSON()
222203
userConfig.set('telemetry', telemetry)
223204
}
224205

206+
async function sendEvent (eventType: 'project', eventData: Project): Promise<void>
207+
async function sendEvent (eventType: 'device', eventData: Device): Promise<void>
225208
async function sendEvent (eventType: 'project' | 'device', eventData: Project | Device) {
226209
const endpoint = process.env.KEYSTONE_TELEMETRY_ENDPOINT || defaultTelemetryEndpoint
227-
const req = https.request(`${endpoint}/v1/event/${eventType}`, {
210+
const req = https.request(`${endpoint}/2/event/${eventType}`, {
228211
method: 'POST',
229212
headers: {
230213
'Content-Type': 'application/json',
@@ -242,8 +225,8 @@ async function sendProjectTelemetryEvent (
242225
) {
243226
const { telemetry, userConfig } = getDefaultedTelemetryConfig()
244227

245-
// no telemetry? somehow our earlier checks missed an opt out, do nothing
246-
if (telemetry === false) return
228+
// no telemetry? somehow we missed something, do nothing
229+
if (!telemetry) return
247230

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

271-
// no telemetry? somehow our earlier checks missed an opt out, do nothing
272-
if (telemetry === false) return
254+
// no telemetry? somehow we missed something, do nothing
255+
if (!telemetry) return
273256

274257
const { lastSentDate } = telemetry.device
275258
if (lastSentDate && lastSentDate >= todaysDate) {
@@ -305,7 +288,8 @@ export async function runTelemetry (
305288
const { telemetry } = getDefaultedTelemetryConfig()
306289

307290
// don't run if the user has opted out
308-
if (telemetry === false) return
291+
// or if somehow our defaults are problematic, do nothing
292+
if (!telemetry) return
309293

310294
// don't send telemetry before we inform the user, allowing opt-out
311295
if (!telemetry.informedAt) return inform()

packages/core/src/scripts/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export async function cli (cwd: string, argv: string[]) {
110110
return prisma(cwd, argv.slice(1), Boolean(flags.frozen))
111111
}
112112

113-
if (command === 'telemetry') {
113+
if (command.startsWith('telemetry')) {
114114
return telemetry(cwd, argv[1])
115115
}
116116

packages/core/src/scripts/telemetry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export async function telemetry (cwd: string, command?: string) {
1111
Usage
1212
$ keystone telemetry [command]
1313
Commands
14-
disable opt-out of telemetry, disabled for this system user
14+
disable opt-out of telemetry, disabling telemetry for this system user
1515
enable opt-in to telemetry
1616
reset resets your telemetry configuration (if any)
1717
status show if telemetry is enabled, disabled or uninitialised

packages/core/src/types/telemetry.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
import type { DatabaseProvider } from './core'
22

3-
export type Telemetry = {
4-
informedAt: string | null
5-
device: {
6-
lastSentDate: string | null
7-
}
8-
projects: Partial<{
9-
[projectPath: string]: {
10-
lastSentDate: string
3+
export type TelemetryVersion1 =
4+
| undefined
5+
| false
6+
| {
7+
device: { lastSentDate?: string, informedAt: string }
8+
projects: {
9+
default: { lastSentDate?: string, informedAt: string }
10+
[projectPath: string]: { lastSentDate?: string, informedAt: string }
11+
}
1112
}
12-
}>
13-
}
13+
14+
export type TelemetryVersion2and3 =
15+
| undefined
16+
| false
17+
| {
18+
informedAt: string | null
19+
device: {
20+
lastSentDate: string | null
21+
}
22+
projects: Partial<{
23+
[projectPath: string]: {
24+
lastSentDate: string
25+
}
26+
}>
27+
}
1428

1529
export type Configuration = {
16-
telemetry?: undefined | false | Telemetry
30+
telemetry?: undefined | false | TelemetryVersion2and3
1731
}
1832

1933
export type Device = {

packages/core/tests/telemetry.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ describe('Telemetry tests', () => {
129129
}
130130

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

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

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

180180
expectDidSend(null)
181-
expect(https.request).toHaveBeenCalledTimes(2)
181+
expect(https.request).toHaveBeenCalledTimes(2) // would be 4 if sent twice
182182
expect(mockTelemetryConfig).toBeDefined()
183183
expect(mockTelemetryConfig?.device.lastSentDate).toBe(today)
184184
expect(mockTelemetryConfig?.projects).toBeDefined()
@@ -215,7 +215,7 @@ describe('Telemetry tests', () => {
215215
expect(mockTelemetryConfig).toBe(false)
216216
})
217217

218-
test(`Telemetry is not sent if telemetry is disabled`, async () => {
218+
test(`Telemetry is not sent if telemetry configuration is disabled`, async () => {
219219
mockTelemetryConfig = false
220220

221221
await runTelemetry(mockProjectDir, lists, 'sqlite') // inform

0 commit comments

Comments
 (0)