From 032cd5c48f8313b6acf5aec63d835e6ac3d352f6 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:30:40 +1000 Subject: [PATCH 01/19] prefer 'sent now' phrasing' --- packages/core/src/lib/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 475a24c278f..c13bdf67248 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -195,7 +195,7 @@ function inform () { printAbout() 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, but telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`) + console.log(`No telemetry data has been sent now, 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 a42f1043f6cf2f7353c2cd90d4b66f6383057653 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:57:17 +1000 Subject: [PATCH 02/19] add strict object matches for telemetry tests --- packages/core/tests/telemetry.test.ts | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/core/tests/telemetry.test.ts b/packages/core/tests/telemetry.test.ts index 429e005cd59..e353c31bdf2 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -167,10 +167,11 @@ describe('Telemetry tests', () => { await runTelemetry(mockProjectDir, lists, 'sqlite') // inform expect(https.request).toHaveBeenCalledTimes(0) - expect(mockTelemetryConfig).toBeDefined() - expect(mockTelemetryConfig?.device.lastSentDate).toBe(null) - expect(mockTelemetryConfig?.projects).toBeDefined() - expect(Object.keys(mockTelemetryConfig?.projects).length).toBe(0) + expect(mockTelemetryConfig).toStrictEqual({ + informedAt: expect.stringMatching(new RegExp(`^${today}`)), + device: { lastSentDate: null }, + projects: {} + }) }) test('Telemetry is sent after inform', async () => { @@ -179,11 +180,13 @@ describe('Telemetry tests', () => { expectDidSend(null) expect(https.request).toHaveBeenCalledTimes(2) // would be 4 if sent twice - expect(mockTelemetryConfig).toBeDefined() - expect(mockTelemetryConfig?.device.lastSentDate).toBe(today) - expect(mockTelemetryConfig?.projects).toBeDefined() - expect(mockTelemetryConfig?.projects[mockProjectDir]).toBeDefined() - expect(mockTelemetryConfig?.projects[mockProjectDir].lastSentDate).toBe(today) + expect(mockTelemetryConfig).toStrictEqual({ + informedAt: expect.stringMatching(new RegExp(`^${today}`)), + device: { lastSentDate: today }, + projects: { + [mockProjectDir]: { lastSentDate: today } + } + }) }) test('Telemetry is not sent twice in one day', async () => { @@ -195,18 +198,20 @@ describe('Telemetry tests', () => { expect(https.request).toHaveBeenCalledTimes(2) // would be 4 if sent twice }) - test('Telemetry sends a lastSentDate on the third run, second day', async () => { + test('Telemetry sends a lastSentDate on the next run, a different day', async () => { mockTelemetryConfig = mockTelemetryConfigInitialised await runTelemetry(mockProjectDir, lists, 'sqlite') // send, different day expectDidSend(mockYesterday) expect(https.request).toHaveBeenCalledTimes(2) - expect(mockTelemetryConfig).toBeDefined() - expect(mockTelemetryConfig?.device.lastSentDate).toBe(today) - expect(mockTelemetryConfig?.projects).toBeDefined() - expect(mockTelemetryConfig?.projects[mockProjectDir]).toBeDefined() - expect(mockTelemetryConfig?.projects[mockProjectDir].lastSentDate).toBe(today) + expect(mockTelemetryConfig).toStrictEqual({ + informedAt: expect.stringMatching(new RegExp(`^${mockYesterday}`)), + device: { lastSentDate: today }, + projects: { + [mockProjectDir]: { lastSentDate: today } + } + }) }) test(`Telemetry is reset when using "keystone telemetry disable"`, () => { From ce0fce101e37b6aa6101f199c1b5d496e151509a Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:47:10 +1000 Subject: [PATCH 03/19] add stricter telemetry tests and userConfig usage --- packages/core/src/lib/telemetry.ts | 90 +++++++++++++-------------- packages/core/src/types/telemetry.ts | 6 +- packages/core/tests/telemetry.test.ts | 44 ++++++++++--- 3 files changed, 82 insertions(+), 58 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index c13bdf67248..684906da712 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -10,12 +10,11 @@ import { green as g } from 'chalk' import { - type Configuration, type Device, type PackageName, type Project, type TelemetryVersion1, - type TelemetryVersion2and3, + type TelemetryVersion2, } from '../types/telemetry' import { type DatabaseProvider } from '../types' import { type InitialisedList } from './core/initialise-lists' @@ -37,8 +36,14 @@ function log (message: unknown) { } } +type Telemetry = TelemetryVersion2 +type TelemetryOK = Exclude +type Configuration = ReturnType['userConfig'] + function getTelemetryConfig () { - const userConfig = new Conf({ + const userConfig = new Conf<{ + telemetry?: TelemetryVersion2 + }>({ projectName: 'keystonejs', projectSuffix: '', projectVersion: '3.0.0', @@ -47,7 +52,7 @@ function getTelemetryConfig () { const existing = store.get('telemetry') as TelemetryVersion1 if (!existing) return // skip non-configured or known opt-outs - const replacement: TelemetryVersion2and3 = { + const replacement: TelemetryVersion2 = { informedAt: null, // re-inform device: { lastSentDate: existing.device.lastSentDate ?? null, @@ -55,7 +60,7 @@ function getTelemetryConfig () { projects: {}, // see below } - // copy existing project lastSentDate's + // copy existing project.lastSentDate's for (const [projectPath, project] of Object.entries(existing.projects)) { if (projectPath === 'default') continue // informedAt moved to device.lastSentDate @@ -70,16 +75,16 @@ function getTelemetryConfig () { } } - store.set('telemetry', replacement) + store.set('telemetry', replacement satisfies TelemetryVersion2) }, '^3.0.0': (store) => { - const existing = store.get('telemetry') as TelemetryVersion2and3 + const existing = store.get('telemetry') as TelemetryVersion2 if (!existing) return // skip non-configured or known opt-outs store.set('telemetry', { ...existing, informedAt: null, // re-inform - } satisfies TelemetryVersion2and3) + } satisfies Telemetry) }, }, }) @@ -90,9 +95,8 @@ function getTelemetryConfig () { } } -function getDefaultedTelemetryConfig () { +function getDefaulted () { const { telemetry, userConfig } = getTelemetryConfig() - if (telemetry === undefined) { return { telemetry: { @@ -101,8 +105,8 @@ function getDefaultedTelemetryConfig () { lastSentDate: null, }, projects: {}, - } as TelemetryVersion2and3, // help Typescript infer the type - userConfig, + } satisfies Telemetry, // help Typescript infer the type + userConfig } } @@ -155,9 +159,7 @@ function printAbout () { console.log(`For more information, including how to opt-out see https://keystonejs.com/telemetry`) } -export function printTelemetryStatus () { - const { telemetry } = getTelemetryConfig() - +function printTelemetryStatus (telemetry: Telemetry) { if (telemetry === undefined) { console.log(`Keystone telemetry has been reset to ${y`uninitialized`}`) console.log() @@ -184,12 +186,10 @@ export function printTelemetryStatus () { 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 we missed something, do nothing - if (!telemetry) return - +function inform ( + telemetry: TelemetryOK, + userConfig: Configuration +) { console.log() // gap to help visiblity console.log(`${bold('Keystone Telemetry')}`) printAbout() @@ -221,13 +221,10 @@ async function sendEvent (eventType: 'project' | 'device', eventData: Project | async function sendProjectTelemetryEvent ( cwd: string, lists: Record, - dbProviderName: DatabaseProvider + dbProviderName: DatabaseProvider, + telemetry: TelemetryOK, + userConfig: Configuration ) { - const { telemetry, userConfig } = getDefaultedTelemetryConfig() - - // no telemetry? somehow we missed something, do nothing - if (!telemetry) return - const project = telemetry.projects[cwd] ?? { lastSentDate: null } const { lastSentDate } = project if (lastSentDate && lastSentDate >= todaysDate) { @@ -248,12 +245,10 @@ async function sendProjectTelemetryEvent ( userConfig.set('telemetry', telemetry) } -async function sendDeviceTelemetryEvent () { - const { telemetry, userConfig } = getDefaultedTelemetryConfig() - - // no telemetry? somehow we missed something, do nothing - if (!telemetry) return - +async function sendDeviceTelemetryEvent ( + telemetry: TelemetryOK, + userConfig: Configuration +) { const { lastSentDate } = telemetry.device if (lastSentDate && lastSentDate >= todaysDate) { log('device telemetry already sent today') @@ -285,38 +280,43 @@ export async function runTelemetry ( return } - const { telemetry } = getDefaultedTelemetryConfig() + const { telemetry, userConfig } = getDefaulted() // don't run if the user has opted out // or if somehow our defaults are problematic, do nothing - if (!telemetry) return + if (telemetry === false) return // don't send telemetry before we inform the user, allowing opt-out - if (!telemetry.informedAt) return inform() + if (!telemetry.informedAt) return inform(telemetry, userConfig) - await sendProjectTelemetryEvent(cwd, lists, dbProviderName) - await sendDeviceTelemetryEvent() + await sendProjectTelemetryEvent(cwd, lists, dbProviderName, telemetry, userConfig) + await sendDeviceTelemetryEvent(telemetry, userConfig) } catch (err) { log(err) } } +export function statusTelemetry () { + const { telemetry } = getTelemetryConfig() + printTelemetryStatus(telemetry) +} + export function enableTelemetry () { const { telemetry, userConfig } = getTelemetryConfig() - if (telemetry === false) { - userConfig.delete('telemetry') + if (!telemetry) { + userConfig.set('telemetry', getDefaulted().telemetry) } - printTelemetryStatus() + printTelemetryStatus(telemetry) } export function disableTelemetry () { - const { userConfig } = getTelemetryConfig() + const { telemetry, userConfig } = getTelemetryConfig() userConfig.set('telemetry', false) - printTelemetryStatus() + printTelemetryStatus(telemetry) } export function resetTelemetry () { - const { userConfig } = getTelemetryConfig() + const { telemetry, userConfig } = getTelemetryConfig() userConfig.delete('telemetry') - printTelemetryStatus() + printTelemetryStatus(telemetry) } diff --git a/packages/core/src/types/telemetry.ts b/packages/core/src/types/telemetry.ts index 334b852fe60..efc0ba28b79 100644 --- a/packages/core/src/types/telemetry.ts +++ b/packages/core/src/types/telemetry.ts @@ -11,7 +11,7 @@ export type TelemetryVersion1 = } } -export type TelemetryVersion2and3 = +export type TelemetryVersion2 = | undefined | false | { @@ -26,10 +26,6 @@ export type TelemetryVersion2and3 = }> } -export type Configuration = { - telemetry?: undefined | false | TelemetryVersion2and3 -} - export type Device = { previous: string | null // new Date().toISOString().slice(0, 10) os: string // `linux` | `darwin` | `windows` | ... // os.platform() diff --git a/packages/core/tests/telemetry.test.ts b/packages/core/tests/telemetry.test.ts index e353c31bdf2..87698fd555a 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -1,4 +1,5 @@ import https from 'node:https' +import Conf from 'conf' import path from 'path' import type { InitialisedList } from '../src/lib/core/initialise-lists' @@ -43,11 +44,18 @@ jest.mock( ) let mockTelemetryConfig: any = undefined + jest.mock('conf', () => { + const getMockTelemetryConfig = jest.fn(() => { + if (mockTelemetryConfig === 'THROW') throw new Error('JSON.parse error') + return mockTelemetryConfig + }) + return function Conf () { return { - get: () => mockTelemetryConfig, - set: (_name: string, newState: any) => { + get: getMockTelemetryConfig, + set: (key: string, newState: any) => { + if (key !== 'telemetry') throw new Error(`Unexpected conf key ${key}`) mockTelemetryConfig = newState }, delete: () => { @@ -166,6 +174,7 @@ describe('Telemetry tests', () => { test('Telemetry writes out an empty configuration, and sends nothing on first run', async () => { await runTelemetry(mockProjectDir, lists, 'sqlite') // inform + expect(new Conf().get).toHaveBeenCalledTimes(1) expect(https.request).toHaveBeenCalledTimes(0) expect(mockTelemetryConfig).toStrictEqual({ informedAt: expect.stringMatching(new RegExp(`^${today}`)), @@ -179,6 +188,7 @@ describe('Telemetry tests', () => { await runTelemetry(mockProjectDir, lists, 'sqlite') // send expectDidSend(null) + expect(new Conf().get).toHaveBeenCalledTimes(2) expect(https.request).toHaveBeenCalledTimes(2) // would be 4 if sent twice expect(mockTelemetryConfig).toStrictEqual({ informedAt: expect.stringMatching(new RegExp(`^${today}`)), @@ -195,6 +205,7 @@ describe('Telemetry tests', () => { await runTelemetry(mockProjectDir, lists, 'sqlite') // send, same day expectDidSend(null) + expect(new Conf().get).toHaveBeenCalledTimes(3) expect(https.request).toHaveBeenCalledTimes(2) // would be 4 if sent twice }) @@ -204,6 +215,7 @@ describe('Telemetry tests', () => { await runTelemetry(mockProjectDir, lists, 'sqlite') // send, different day expectDidSend(mockYesterday) + expect(new Conf().get).toHaveBeenCalledTimes(1) expect(https.request).toHaveBeenCalledTimes(2) expect(mockTelemetryConfig).toStrictEqual({ informedAt: expect.stringMatching(new RegExp(`^${mockYesterday}`)), @@ -227,10 +239,22 @@ describe('Telemetry tests', () => { await runTelemetry(mockProjectDir, lists, 'sqlite') // send await runTelemetry(mockProjectDir, lists, 'sqlite') // send, same day + expect(new Conf().get).toHaveBeenCalledTimes(3) expect(https.request).toHaveBeenCalledTimes(0) expect(mockTelemetryConfig).toBe(false) }) + test(`Telemetry is unchanged if configuration is malformed`, async () => { + mockTelemetryConfig = 'THROW' + + await runTelemetry(mockProjectDir, lists, 'sqlite') // inform + await runTelemetry(mockProjectDir, lists, 'sqlite') // send + + expect(new Conf().get).toHaveBeenCalledTimes(2) + expect(https.request).toHaveBeenCalledTimes(0) + expect(mockTelemetryConfig).toStrictEqual('THROW') // nothing changes + }) + // easy opt-out tests for (const [key, value] of Object.entries({ NODE_ENV: 'production', @@ -247,24 +271,25 @@ describe('Telemetry tests', () => { process.env[key] = envBefore }) - test(`when initialised, nothing is sent`, async () => { + test(`when telemetry initialised, we do nothing`, async () => { mockTelemetryConfig = mockTelemetryConfigInitialised await runTelemetry(mockProjectDir, lists, 'sqlite') // try send again + expect(new Conf().get).toHaveBeenCalledTimes(0) expect(https.request).toHaveBeenCalledTimes(0) expect(mockTelemetryConfig).toBe(mockTelemetryConfigInitialised) // unchanged }) - test(`if not initialised, we do nothing`, async () => { + test(`when telemetry uninitialised, we do nothing`, async () => { expect(mockTelemetryConfig).toBe(undefined) - expect(https.request).toHaveBeenCalledTimes(0) await runTelemetry(mockProjectDir, lists, 'sqlite') // try inform await runTelemetry(mockProjectDir, lists, 'sqlite') // try send + expect(new Conf().get).toHaveBeenCalledTimes(0) expect(https.request).toHaveBeenCalledTimes(0) - expect(mockTelemetryConfig).toBe(undefined) // nothing changed + expect(mockTelemetryConfig).toBe(undefined) // unchanged }) }) } @@ -272,6 +297,7 @@ describe('Telemetry tests', () => { describe('when something throws internally', () => { let runTelemetryThrows: any beforeEach(() => { + // this is a nightmare, don't touch it jest.resetAllMocks() jest.resetModules() jest.mock('node-fetch', () => { @@ -287,6 +313,7 @@ describe('Telemetry tests', () => { await runTelemetryThrows(mockProjectDir, lists, 'sqlite') // send + // expect(new Conf().get).toHaveBeenCalledTimes(1) // nightmare expect(https.request).toHaveBeenCalledTimes(0) expect(mockTelemetryConfig).toBe(mockTelemetryConfigInitialised) // unchanged }) @@ -309,17 +336,18 @@ describe('Telemetry tests', () => { await runTelemetryCI(mockProjectDir, lists, 'sqlite') // try send again + expect(new Conf().get).toHaveBeenCalledTimes(0) expect(https.request).toHaveBeenCalledTimes(0) expect(mockTelemetryConfig).toBe(mockTelemetryConfigInitialised) // unchanged }) test(`if not initialised, we do nothing`, async () => { - expect(mockTelemetryConfig).toBe(undefined) - expect(https.request).toHaveBeenCalledTimes(0) + mockTelemetryConfig = undefined await runTelemetryCI(mockProjectDir, lists, 'sqlite') // try inform await runTelemetryCI(mockProjectDir, lists, 'sqlite') // try send + expect(new Conf().get).toHaveBeenCalledTimes(0) expect(https.request).toHaveBeenCalledTimes(0) expect(mockTelemetryConfig).toBe(undefined) // nothing changed }) From e3d7e2fe26185b9b8e2445e0225f970cf477b9d9 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:56:57 +1000 Subject: [PATCH 04/19] rename versions to packages --- docs/content/docs/reference/telemetry.md | 8 ++++---- packages/core/src/lib/telemetry.ts | 8 ++++---- packages/core/src/types/telemetry.ts | 2 +- packages/core/tests/telemetry.test.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/content/docs/reference/telemetry.md b/docs/content/docs/reference/telemetry.md index 7a401fe1636..0e6a9538346 100644 --- a/docs/content/docs/reference/telemetry.md +++ b/docs/content/docs/reference/telemetry.md @@ -89,7 +89,7 @@ A device telemetry report is formatted as JSON and currently looks like: The type of information contained within a project telemetry report is currently: - The last date you used `keystone dev` for this project, and -- The resolved versions of any `@keystone-6` packages used by this project, and +- The resolved package versions of any `@keystone-6` packages used by this project, and - The number of lists for this project, and - The name and number of field types that you are using @@ -98,7 +98,7 @@ A project telemetry report is formatted as JSON and currently looks like: ```json { "previous": "2022-11-23", - "versions": { + "packages": { "@keystone-6/auth": "5.0.1", "@keystone-6/core": "3.1.2", "@keystone-6/document-renderer": "1.1.2", @@ -185,8 +185,8 @@ If you wish to see how telemetry is currently configured for your device or proj ## What if I have a complaint or question -If you have any questions or concerns about the information that is gathered please contact us by logging a GitHub Issue [https://github.com/keystonejs/keystone](https://github.com/keystonejs/keystone). +If you have any questions or concerns about the information that is gathered please contact us by logging a GitHub Issue [https://github.com/keystonejs/keystone](https://github.com/keystonejs/keystone). -Alternatively please contact our Privacy Officer by email to [privacy@keystonejs.com](mailto:privacy@keystonejs.com), or by mail to Level 10, 191 Clarence Street, Sydney NSW 2000. +Alternatively please contact our Privacy Officer by email to [privacy@keystonejs.com](mailto:privacy@keystonejs.com), or by mail to Level 10, 191 Clarence Street, Sydney NSW 2000. For further information about Keystone’s security policy please see [https://github.com/keystonejs/keystone/security/policy](https://github.com/keystonejs/keystone/security/policy) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 684906da712..609c98fa2a4 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -137,20 +137,20 @@ function collectFieldCount (lists: Record) { } function collectPackageVersions () { - const versions: Project['versions'] = { + const packages: Project['packages'] = { '@keystone-6/core': '0.0.0', // effectively unknown } for (const packageName of packageNames) { try { const packageJson = require(`${packageName}/package.json`) - versions[packageName] = packageJson.version + packages[packageName] = packageJson.version } catch { // do nothing, most likely because the package is not installed } } - return versions + return packages } function printAbout () { @@ -236,7 +236,7 @@ async function sendProjectTelemetryEvent ( previous: lastSentDate, fields: collectFieldCount(lists), lists: Object.keys(lists).length, - versions: collectPackageVersions(), + packages: collectPackageVersions(), database: dbProviderName, }) diff --git a/packages/core/src/types/telemetry.ts b/packages/core/src/types/telemetry.ts index efc0ba28b79..d7a4b88ba81 100644 --- a/packages/core/src/types/telemetry.ts +++ b/packages/core/src/types/telemetry.ts @@ -49,7 +49,7 @@ export type Project = { // - `@keystone-6` // - `@opensaas` // - ... - versions: Partial> + packages: Partial> lists: number database: DatabaseProvider // uses a new `field.__ksTelemetryFieldTypeName` for the key, defaults to `unknown` diff --git a/packages/core/tests/telemetry.test.ts b/packages/core/tests/telemetry.test.ts index 87698fd555a..77fbcb0e386 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -151,7 +151,7 @@ describe('Telemetry tests', () => { id: 5, }, lists: 2, - versions: mockPackageVersions, + packages: mockPackageVersions, database: 'sqlite', }) ) From 36b97596947ff8e198bf7f97fc2d0d3a497e39ed Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:00:33 +1000 Subject: [PATCH 05/19] add inform command for inform notice, and add last updated date --- .changeset/add-telemetry-inform.md | 5 ++ docs/content/docs/reference/telemetry.md | 1 + packages/core/src/lib/telemetry.ts | 83 ++++++++++++++---------- packages/core/src/scripts/telemetry.ts | 13 ++-- 4 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 .changeset/add-telemetry-inform.md diff --git a/.changeset/add-telemetry-inform.md b/.changeset/add-telemetry-inform.md new file mode 100644 index 00000000000..08dd3fce189 --- /dev/null +++ b/.changeset/add-telemetry-inform.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/core": minor +--- + +Adds `keystone telemetry inform` command to show an informed consent notice diff --git a/docs/content/docs/reference/telemetry.md b/docs/content/docs/reference/telemetry.md index 0e6a9538346..ab63c119f68 100644 --- a/docs/content/docs/reference/telemetry.md +++ b/docs/content/docs/reference/telemetry.md @@ -63,6 +63,7 @@ Keystone collects telemetry information in the form of two different types of da We refer to these two different reports, as “device telemetry” and “project telemetry” respectively. These reports are forwarded to [https://telemetry.keystonejs.com/](https://telemetry.keystonejs.com/), and are reported separately to minimize any correlation between them insofar as the timing and grouping of that data, that an otherwise combined report may have. We are collecting these two reports for different reasons, and thus have no need to associate them. +We may record and differentiate reports using the ` We additionally record a timestamp of the time that the report is received by the server at [https://telemetry.keystonejs.com](https://telemetry.keystonejs.com/). diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 609c98fa2a4..c59f40fc2c9 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -5,9 +5,11 @@ import ci from 'ci-info' import Conf from 'conf' import { bold, + blue as b, yellow as y, red as r, - green as g + green as g, + grey, } from 'chalk' import { type Device, @@ -95,22 +97,15 @@ function getTelemetryConfig () { } } -function getDefaulted () { - const { telemetry, userConfig } = getTelemetryConfig() - if (telemetry === undefined) { - return { - telemetry: { - informedAt: null, - device: { - lastSentDate: null, - }, - projects: {}, - } satisfies Telemetry, // help Typescript infer the type - userConfig - } - } - - return { telemetry, userConfig } +function getDefault (telemetry: Telemetry) { + if (telemetry) return telemetry + return { + informedAt: null, + device: { + lastSentDate: null, + }, + projects: {}, + } satisfies Telemetry // help Typescript infer the type } const todaysDate = new Date().toISOString().slice(0, 10) @@ -156,21 +151,30 @@ function collectPackageVersions () { function printAbout () { 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`) } -function printTelemetryStatus (telemetry: Telemetry) { +function printNext (telemetry: Telemetry) { + if (!telemetry) { + console.log(`Telemetry data will ${r`not`} be sent by this system user`) + return + } + console.log(`Telemetry data will be sent the next time you run ${g`"keystone dev"`}`) +} + +function printTelemetryStatus () { + const { telemetry } = getTelemetryConfig() + if (telemetry === undefined) { console.log(`Keystone telemetry has been reset to ${y`uninitialized`}`) console.log() - console.log(`Telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`) + printNext(telemetry) 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`) + printNext(telemetry) return } @@ -183,7 +187,7 @@ function printTelemetryStatus (telemetry: Telemetry) { } console.log() - console.log(`Telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`) + printNext(telemetry) } function inform ( @@ -195,8 +199,12 @@ function inform ( printAbout() 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 now, but telemetry will be sent the next time you run ${g`"keystone dev"`}, unless you opt-out`) + if (telemetry.informedAt === null) { + console.log(`No telemetry data has been sent as part of this notice`) + } + printNext(telemetry) console.log() // gap to help visiblity + console.log(`For more information, including how to opt-out see ${grey`https://keystonejs.com/telemetry`} (updated ${b`2024-08-15`})`) // update the informedAt telemetry.informedAt = new Date().toJSON() @@ -280,43 +288,48 @@ export async function runTelemetry ( return } - const { telemetry, userConfig } = getDefaulted() + const { telemetry, userConfig } = getTelemetryConfig() // don't run if the user has opted out // or if somehow our defaults are problematic, do nothing if (telemetry === false) return // don't send telemetry before we inform the user, allowing opt-out - if (!telemetry.informedAt) return inform(telemetry, userConfig) + const telemetryDefaulted = getDefault(telemetry) + if (!telemetryDefaulted.informedAt) return inform(telemetryDefaulted, userConfig) - await sendProjectTelemetryEvent(cwd, lists, dbProviderName, telemetry, userConfig) - await sendDeviceTelemetryEvent(telemetry, userConfig) + await sendProjectTelemetryEvent(cwd, lists, dbProviderName, telemetryDefaulted, userConfig) + await sendDeviceTelemetryEvent(telemetryDefaulted, userConfig) } catch (err) { log(err) } } export function statusTelemetry () { - const { telemetry } = getTelemetryConfig() - printTelemetryStatus(telemetry) + printTelemetryStatus() +} + +export function informTelemetry () { + const { userConfig } = getTelemetryConfig() + inform(getDefault(false), userConfig) } export function enableTelemetry () { const { telemetry, userConfig } = getTelemetryConfig() if (!telemetry) { - userConfig.set('telemetry', getDefaulted().telemetry) + userConfig.set('telemetry', getDefault(telemetry)) } - printTelemetryStatus(telemetry) + printTelemetryStatus() } export function disableTelemetry () { - const { telemetry, userConfig } = getTelemetryConfig() + const { userConfig } = getTelemetryConfig() userConfig.set('telemetry', false) - printTelemetryStatus(telemetry) + printTelemetryStatus() } export function resetTelemetry () { - const { telemetry, userConfig } = getTelemetryConfig() + const { userConfig } = getTelemetryConfig() userConfig.delete('telemetry') - printTelemetryStatus(telemetry) + printTelemetryStatus() } diff --git a/packages/core/src/scripts/telemetry.ts b/packages/core/src/scripts/telemetry.ts index 71576636f43..c0922e6355b 100644 --- a/packages/core/src/scripts/telemetry.ts +++ b/packages/core/src/scripts/telemetry.ts @@ -1,9 +1,10 @@ -import chalk from 'chalk' +import { bold } from 'chalk' import { - printTelemetryStatus, - enableTelemetry, disableTelemetry, + enableTelemetry, resetTelemetry, + statusTelemetry, + informTelemetry, } from '../lib/telemetry' export async function telemetry (cwd: string, command?: string) { @@ -15,6 +16,7 @@ export async function telemetry (cwd: string, command?: string) { enable opt-in to telemetry reset resets your telemetry configuration (if any) status show if telemetry is enabled, disabled or uninitialised + inform show an informed consent notice For more details visit: https://keystonejs.com/telemetry ` @@ -22,9 +24,10 @@ For more details visit: https://keystonejs.com/telemetry if (command === 'disable') return disableTelemetry() if (command === 'enable') return enableTelemetry() if (command === 'reset') return resetTelemetry() - if (command === 'status') return printTelemetryStatus() + if (command === 'status') return statusTelemetry() + if (command === 'inform') return informTelemetry() if (command === '--help') { - console.log(`${chalk.bold('Keystone Telemetry')}`) + console.log(`${bold('Keystone Telemetry')}`) console.log(usageText) return } From 7ea48b6fe5f766663635e451e3390880273f7e9f Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:30:48 +1000 Subject: [PATCH 06/19] add differentiation emphasis to notice --- docs/content/docs/reference/telemetry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/reference/telemetry.md b/docs/content/docs/reference/telemetry.md index ab63c119f68..96909dc1e47 100644 --- a/docs/content/docs/reference/telemetry.md +++ b/docs/content/docs/reference/telemetry.md @@ -63,7 +63,7 @@ Keystone collects telemetry information in the form of two different types of da We refer to these two different reports, as “device telemetry” and “project telemetry” respectively. These reports are forwarded to [https://telemetry.keystonejs.com/](https://telemetry.keystonejs.com/), and are reported separately to minimize any correlation between them insofar as the timing and grouping of that data, that an otherwise combined report may have. We are collecting these two reports for different reasons, and thus have no need to associate them. -We may record and differentiate reports using the ` +We differentiate the type and version of reports from the URL used by Keystone. We additionally record a timestamp of the time that the report is received by the server at [https://telemetry.keystonejs.com](https://telemetry.keystonejs.com/). From 639f6196496fa937a2803a08ce5c1a4ae4ffaeb2 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:41:39 +1000 Subject: [PATCH 07/19] use record phrasing for type and version --- docs/content/docs/reference/telemetry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/reference/telemetry.md b/docs/content/docs/reference/telemetry.md index 96909dc1e47..82904020c10 100644 --- a/docs/content/docs/reference/telemetry.md +++ b/docs/content/docs/reference/telemetry.md @@ -63,7 +63,7 @@ Keystone collects telemetry information in the form of two different types of da We refer to these two different reports, as “device telemetry” and “project telemetry” respectively. These reports are forwarded to [https://telemetry.keystonejs.com/](https://telemetry.keystonejs.com/), and are reported separately to minimize any correlation between them insofar as the timing and grouping of that data, that an otherwise combined report may have. We are collecting these two reports for different reasons, and thus have no need to associate them. -We differentiate the type and version of reports from the URL used by Keystone. +We differentiate and record the type and version of reports from the URL used by Keystone. We additionally record a timestamp of the time that the report is received by the server at [https://telemetry.keystonejs.com](https://telemetry.keystonejs.com/). From 77e24a6b767ba032fb2f121ca1830180f21b69c4 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:30:40 +1000 Subject: [PATCH 08/19] dont throw if request has a DNS error --- packages/core/src/lib/telemetry.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index c59f40fc2c9..2d13d7852f0 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -215,14 +215,20 @@ async function sendEvent (eventType: 'project', eventData: Project): Promise async function sendEvent (eventType: 'project' | 'device', eventData: Project | Device) { const endpoint = process.env.KEYSTONE_TELEMETRY_ENDPOINT || defaultTelemetryEndpoint - const req = https.request(`${endpoint}/2/event/${eventType}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + await new Promise((resolve) => { + const req = https.request(`${endpoint}/2/event/${eventType}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, () => { + resolve() + }) + + req.once('error', () => resolve()) + req.end(JSON.stringify(eventData)) }) - req.end(JSON.stringify(eventData)) log(`sent ${eventType} report`) } From d4423337ad428d877a5a194504f0bbf59b775955 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:50:37 +1000 Subject: [PATCH 09/19] rename previous to lastSentData, update policy to show database field is sent --- docs/content/docs/reference/telemetry.md | 16 +++++----- examples/custom-output-paths/keystone.ts | 2 +- packages/core/src/lib/telemetry.ts | 39 +++++++++++------------- packages/core/src/types/telemetry.ts | 18 +++-------- packages/core/tests/telemetry.test.ts | 18 +++++------ 5 files changed, 42 insertions(+), 51 deletions(-) diff --git a/docs/content/docs/reference/telemetry.md b/docs/content/docs/reference/telemetry.md index 82904020c10..c779ea1e6f2 100644 --- a/docs/content/docs/reference/telemetry.md +++ b/docs/content/docs/reference/telemetry.md @@ -79,9 +79,9 @@ A device telemetry report is formatted as JSON and currently looks like: ```json { - "previous": "2022-11-23", + "lastSentDate": "2024-11-23", "os": "darwin", - "node": "18" + "node": "20" } ``` @@ -91,6 +91,7 @@ The type of information contained within a project telemetry report is currently - The last date you used `keystone dev` for this project, and - The resolved package versions of any `@keystone-6` packages used by this project, and +- The database type used by the project, - The number of lists for this project, and - The name and number of field types that you are using @@ -98,19 +99,20 @@ A project telemetry report is formatted as JSON and currently looks like: ```json { - "previous": "2022-11-23", + "lastSentDate": "2024-11-23", "packages": { - "@keystone-6/auth": "5.0.1", - "@keystone-6/core": "3.1.2", + "@keystone-6/auth": "8.0.1", + "@keystone-6/core": "6.1.0", "@keystone-6/document-renderer": "1.1.2", "@keystone-6/fields-document": "5.0.2" }, + "database": "postgresql", "lists": 3, "fields": { "unknown": 1, "@keystone-6/text": 5, - "@keystone-6/image": 1, - "@keystone-6/file": 1 + "@keystone-6/timestamp": 2, + "@keystone-6/checkbox": 1 } } ``` diff --git a/examples/custom-output-paths/keystone.ts b/examples/custom-output-paths/keystone.ts index 5e59148bcb4..e9783b26d2d 100644 --- a/examples/custom-output-paths/keystone.ts +++ b/examples/custom-output-paths/keystone.ts @@ -8,7 +8,7 @@ export default config({ // when working in a monorepo environment you may want to output the prisma client elsewhere // you can use .db.prismaClientPath to configure where that is - prismaClientPath: 'node_modules/.myprisma/client', + prismaClientPath: 'node_modules/myprisma', prismaSchemaPath: 'my-prisma.prisma', }, lists, diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 2d13d7852f0..1da7fac2fa8 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -13,7 +13,6 @@ import { } from 'chalk' import { type Device, - type PackageName, type Project, type TelemetryVersion1, type TelemetryVersion2, @@ -23,15 +22,6 @@ import { type InitialisedList } from './core/initialise-lists' const defaultTelemetryEndpoint = 'https://telemetry.keystonejs.com' -const packageNames: PackageName[] = [ - '@keystone-6/core', - '@keystone-6/auth', - '@keystone-6/fields-document', - '@keystone-6/cloudinary', - '@keystone-6/session-store-redis', - '@opensaas/keystone-nextjs-auth', -] - function log (message: unknown) { if (process.env.KEYSTONE_TELEMETRY_DEBUG === '1') { console.log(`${message}`) @@ -131,17 +121,24 @@ function collectFieldCount (lists: Record) { return fields } -function collectPackageVersions () { +async function collectPackageVersions () { const packages: Project['packages'] = { - '@keystone-6/core': '0.0.0', // effectively unknown + '@keystone-6/core': '0.0.0', // "unknown" } - for (const packageName of packageNames) { + for (const packageName of [ + '@keystone-6/core', + '@keystone-6/auth', + '@keystone-6/fields-document', + '@keystone-6/cloudinary', + '@keystone-6/session-store-redis', + '@opensaas/keystone-nextjs-auth', + ]) { try { - const packageJson = require(`${packageName}/package.json`) + const packageJson = await import(`${packageName}/package.json`) packages[packageName] = packageJson.version } catch { - // do nothing, most likely because the package is not installed + // do nothing, the package is probably not installed } } @@ -216,7 +213,7 @@ async function sendEvent (eventType: 'device', eventData: Device): Promise async function sendEvent (eventType: 'project' | 'device', eventData: Project | Device) { const endpoint = process.env.KEYSTONE_TELEMETRY_ENDPOINT || defaultTelemetryEndpoint await new Promise((resolve) => { - const req = https.request(`${endpoint}/2/event/${eventType}`, { + const req = https.request(`${endpoint}/2/${eventType}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -247,11 +244,11 @@ async function sendProjectTelemetryEvent ( } await sendEvent('project', { - previous: lastSentDate, - fields: collectFieldCount(lists), - lists: Object.keys(lists).length, - packages: collectPackageVersions(), + lastSentDate, + packages: await collectPackageVersions(), database: dbProviderName, + lists: Object.keys(lists).length, + fields: collectFieldCount(lists), }) // update the project lastSentDate @@ -270,7 +267,7 @@ async function sendDeviceTelemetryEvent ( } await sendEvent('device', { - previous: lastSentDate, + lastSentDate, os: platform(), node: process.versions.node.split('.')[0], }) diff --git a/packages/core/src/types/telemetry.ts b/packages/core/src/types/telemetry.ts index d7a4b88ba81..a3de0bd7cb6 100644 --- a/packages/core/src/types/telemetry.ts +++ b/packages/core/src/types/telemetry.ts @@ -27,21 +27,13 @@ export type TelemetryVersion2 = } export type Device = { - previous: string | null // new Date().toISOString().slice(0, 10) + lastSentDate: string | null // new Date().toISOString().slice(0, 10) os: string // `linux` | `darwin` | `windows` | ... // os.platform() node: string // `14` | ... | `18` // process.version.split('.').shift().slice(1) } -export type PackageName = - | '@keystone-6/core' - | '@keystone-6/auth' - | '@keystone-6/fields-document' - | '@keystone-6/cloudinary' - | '@keystone-6/session-store-redis' - | '@opensaas/keystone-nextjs-auth' - export type Project = { - previous: string | null // new Date().toISOString().slice(0, 10) + lastSentDate: string | null // new Date().toISOString().slice(0, 10) // omitted uuid for > - lists: number + packages: Partial> database: DatabaseProvider - // uses a new `field.__ksTelemetryFieldTypeName` for the key, defaults to `unknown` + lists: number fields: { + // uses `field.__ksTelemetryFieldTypeName`, default is `unknown` [key: string]: number } } diff --git a/packages/core/tests/telemetry.test.ts b/packages/core/tests/telemetry.test.ts index 77fbcb0e386..50e0fe788c3 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -8,10 +8,10 @@ import { runTelemetry, disableTelemetry } from '../src/lib/telemetry' const mockProjectRoot = path.resolve(__dirname, '..', '..', '..') const mockProjectDir = path.join(mockProjectRoot, './tests/test-projects/basic') const mockPackageVersions = { - '@keystone-6/core': '3.1.0', - '@keystone-6/auth': '5.0.1', - '@keystone-6/fields-document': '5.0.2', - '@keystone-6/cloudinary': '5.0.1', + '@keystone-6/core': '14.1.0', + '@keystone-6/auth': '9.0.1', + '@keystone-6/fields-document': '18.0.2', + '@keystone-6/cloudinary': '0.0.1', } jest.mock( @@ -145,14 +145,14 @@ describe('Telemetry tests', () => { }) expect((https.request as any).end).toHaveBeenCalledWith( JSON.stringify({ - previous: lastSentDate, + lastSentDate, + packages: mockPackageVersions, + database: 'sqlite', + lists: 2, fields: { unknown: 0, id: 5, }, - lists: 2, - packages: mockPackageVersions, - database: 'sqlite', }) ) @@ -164,7 +164,7 @@ describe('Telemetry tests', () => { }) expect((https.request as any).end).toHaveBeenCalledWith( JSON.stringify({ - previous: lastSentDate, + lastSentDate, os: 'keystone-os', node: process.versions.node.split('.')[0], }) From fb001c384a5759fa0f27989cae3dbcc8d9d2e110 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:52:00 +1000 Subject: [PATCH 10/19] add changeset for missing database field --- .changeset/fix-telemetry-policy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-telemetry-policy.md diff --git a/.changeset/fix-telemetry-policy.md b/.changeset/fix-telemetry-policy.md new file mode 100644 index 00000000000..462775143b0 --- /dev/null +++ b/.changeset/fix-telemetry-policy.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/core": patch +--- + +Update https://keystonejs.com/docs/reference/telemetry to show that `database` type is collected as part of telemetry From ead420bf943a8129aae39921c52fe0f626169133 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:02:29 +1000 Subject: [PATCH 11/19] move myprisma to node_modules/myprisma, no dot --- examples/custom-output-paths/my-prisma.prisma | 2 +- examples/custom-output-paths/my-types.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/custom-output-paths/my-prisma.prisma b/examples/custom-output-paths/my-prisma.prisma index 29f51889e92..ceb462537f5 100644 --- a/examples/custom-output-paths/my-prisma.prisma +++ b/examples/custom-output-paths/my-prisma.prisma @@ -9,7 +9,7 @@ datasource sqlite { generator client { provider = "prisma-client-js" - output = "node_modules/.myprisma/client" + output = "node_modules/myprisma" } model Post { diff --git a/examples/custom-output-paths/my-types.ts b/examples/custom-output-paths/my-types.ts index ec283d8f8fe..63f1b32244e 100644 --- a/examples/custom-output-paths/my-types.ts +++ b/examples/custom-output-paths/my-types.ts @@ -123,22 +123,22 @@ export type KeystoneAdminUISortDirection = | 'DESC' type ResolvedPostCreateInput = { - id?: import('./node_modules/.myprisma/client').Prisma.PostCreateInput['id'] - title?: import('./node_modules/.myprisma/client').Prisma.PostCreateInput['title'] - content?: import('./node_modules/.myprisma/client').Prisma.PostCreateInput['content'] - publishDate?: import('./node_modules/.myprisma/client').Prisma.PostCreateInput['publishDate'] + id?: import('./node_modules/myprisma').Prisma.PostCreateInput['id'] + title?: import('./node_modules/myprisma').Prisma.PostCreateInput['title'] + content?: import('./node_modules/myprisma').Prisma.PostCreateInput['content'] + publishDate?: import('./node_modules/myprisma').Prisma.PostCreateInput['publishDate'] } type ResolvedPostUpdateInput = { id?: undefined - title?: import('./node_modules/.myprisma/client').Prisma.PostUpdateInput['title'] - content?: import('./node_modules/.myprisma/client').Prisma.PostUpdateInput['content'] - publishDate?: import('./node_modules/.myprisma/client').Prisma.PostUpdateInput['publishDate'] + title?: import('./node_modules/myprisma').Prisma.PostUpdateInput['title'] + content?: import('./node_modules/myprisma').Prisma.PostUpdateInput['content'] + publishDate?: import('./node_modules/myprisma').Prisma.PostUpdateInput['publishDate'] } export declare namespace Lists { export type Post = import('@keystone-6/core').ListConfig> namespace Post { - export type Item = import('./node_modules/.myprisma/client').Post + export type Item = import('./node_modules/myprisma').Post export type TypeInfo = { key: 'Post' isSingleton: false @@ -166,8 +166,8 @@ export type TypeInfo = { lists: { readonly Post: Lists.Post.TypeInfo } - prisma: import('./node_modules/.myprisma/client').PrismaClient - prismaTypes: import('./node_modules/.myprisma/client').Prisma + prisma: import('./node_modules/myprisma').PrismaClient + prismaTypes: import('./node_modules/myprisma').Prisma session: Session } From 1e8323a18e7986a11875d0867b2ac6c37188a1d7 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:17:04 +1000 Subject: [PATCH 12/19] dont use gte comparison with date strings --- packages/core/src/lib/telemetry.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 1da7fac2fa8..a5ee06ab470 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -158,9 +158,7 @@ function printNext (telemetry: Telemetry) { console.log(`Telemetry data will be sent the next time you run ${g`"keystone dev"`}`) } -function printTelemetryStatus () { - const { telemetry } = getTelemetryConfig() - +function printTelemetryStatus (telemetry: Telemetry) { if (telemetry === undefined) { console.log(`Keystone telemetry has been reset to ${y`uninitialized`}`) console.log() @@ -217,12 +215,16 @@ async function sendEvent (eventType: 'project' | 'device', eventData: Project | method: 'POST', headers: { 'Content-Type': 'application/json', + 'User-Agent': 'keystonejs' }, }, () => { resolve() }) - req.once('error', () => resolve()) + req.once('error', (err) => { + log(err?.message ?? err) + resolve() + }) req.end(JSON.stringify(eventData)) }) @@ -238,7 +240,7 @@ async function sendProjectTelemetryEvent ( ) { const project = telemetry.projects[cwd] ?? { lastSentDate: null } const { lastSentDate } = project - if (lastSentDate && lastSentDate >= todaysDate) { + if (lastSentDate && lastSentDate === todaysDate) { log('project telemetry already sent today') return } @@ -261,7 +263,7 @@ async function sendDeviceTelemetryEvent ( userConfig: Configuration ) { const { lastSentDate } = telemetry.device - if (lastSentDate && lastSentDate >= todaysDate) { + if (lastSentDate && lastSentDate === todaysDate) { log('device telemetry already sent today') return } @@ -303,13 +305,14 @@ export async function runTelemetry ( await sendProjectTelemetryEvent(cwd, lists, dbProviderName, telemetryDefaulted, userConfig) await sendDeviceTelemetryEvent(telemetryDefaulted, userConfig) - } catch (err) { - log(err) + } catch (err: any) { + log(err?.message ?? err) } } export function statusTelemetry () { - printTelemetryStatus() + const { telemetry } = getTelemetryConfig() + printTelemetryStatus(telemetry) } export function informTelemetry () { @@ -322,17 +325,17 @@ export function enableTelemetry () { if (!telemetry) { userConfig.set('telemetry', getDefault(telemetry)) } - printTelemetryStatus() + statusTelemetry() } export function disableTelemetry () { const { userConfig } = getTelemetryConfig() userConfig.set('telemetry', false) - printTelemetryStatus() + statusTelemetry() } export function resetTelemetry () { const { userConfig } = getTelemetryConfig() userConfig.delete('telemetry') - printTelemetryStatus() + statusTelemetry() } From cccd82c0f310d413f6641bd2fe0e5a6c8ffea960 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:18:09 +1000 Subject: [PATCH 13/19] revert keystonejs User-Agent --- packages/core/src/lib/telemetry.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index a5ee06ab470..11f0403f5b3 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -215,7 +215,6 @@ async function sendEvent (eventType: 'project' | 'device', eventData: Project | method: 'POST', headers: { 'Content-Type': 'application/json', - 'User-Agent': 'keystonejs' }, }, () => { resolve() From 24cac4a631d73cbff66fe2af9a8ded27609e1124 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:19:10 +1000 Subject: [PATCH 14/19] update policy updated date --- packages/core/src/lib/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 11f0403f5b3..2d38cf5aadd 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -199,7 +199,7 @@ function inform ( } printNext(telemetry) console.log() // gap to help visiblity - console.log(`For more information, including how to opt-out see ${grey`https://keystonejs.com/telemetry`} (updated ${b`2024-08-15`})`) + console.log(`For more information, including how to opt-out see ${grey`https://keystonejs.com/telemetry`} (updated ${b`2024-08-20`})`) // update the informedAt telemetry.informedAt = new Date().toJSON() From 31457764bb347e6ece6a345739b6d51c5268ab10 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:21:36 +1000 Subject: [PATCH 15/19] upgrade telemetry endpoint to version 3 --- packages/core/src/lib/telemetry.ts | 4 ++-- packages/core/tests/telemetry.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 2d38cf5aadd..3eb451df041 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -20,7 +20,7 @@ import { import { type DatabaseProvider } from '../types' import { type InitialisedList } from './core/initialise-lists' -const defaultTelemetryEndpoint = 'https://telemetry.keystonejs.com' +const defaultTelemetryEndpoint = 'https://telemetry.keystonejs.com/3/' function log (message: unknown) { if (process.env.KEYSTONE_TELEMETRY_DEBUG === '1') { @@ -211,7 +211,7 @@ async function sendEvent (eventType: 'device', eventData: Device): Promise async function sendEvent (eventType: 'project' | 'device', eventData: Project | Device) { const endpoint = process.env.KEYSTONE_TELEMETRY_ENDPOINT || defaultTelemetryEndpoint await new Promise((resolve) => { - const req = https.request(`${endpoint}/2/${eventType}`, { + const req = https.request(`${endpoint}${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 50e0fe788c3..30cabffe4c9 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -137,7 +137,7 @@ describe('Telemetry tests', () => { } function expectDidSend (lastSentDate: string | null) { - expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/2/event/project`, { + expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/3/project`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -156,7 +156,7 @@ describe('Telemetry tests', () => { }) ) - expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/2/event/device`, { + expect(https.request).toHaveBeenCalledWith(`https://telemetry.keystonejs.com/3/device`, { method: 'POST', headers: { 'Content-Type': 'application/json', From cd18302c02895d2527fb16274dc5250cf7d9222d Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:37:22 +1000 Subject: [PATCH 16/19] fix telemetry tests for jest --- packages/core/src/lib/telemetry.ts | 9 ++++----- packages/core/tests/telemetry.test.ts | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 3eb451df041..08d70ebebc8 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -135,9 +135,10 @@ async function collectPackageVersions () { '@opensaas/keystone-nextjs-auth', ]) { try { - const packageJson = await import(`${packageName}/package.json`) + const packageJson = require(`${packageName}/package.json`) + // const packageJson = await import(`${packageName}/package.json`, { assert: { type: 'json' } }) // TODO: broken in jest packages[packageName] = packageJson.version - } catch { + } catch (err) { // do nothing, the package is probably not installed } } @@ -216,9 +217,7 @@ async function sendEvent (eventType: 'project' | 'device', eventData: Project | headers: { 'Content-Type': 'application/json', }, - }, () => { - resolve() - }) + }, () => resolve()) req.once('error', (err) => { log(err?.message ?? err) diff --git a/packages/core/tests/telemetry.test.ts b/packages/core/tests/telemetry.test.ts index 30cabffe4c9..8b6e7f6dd98 100644 --- a/packages/core/tests/telemetry.test.ts +++ b/packages/core/tests/telemetry.test.ts @@ -66,12 +66,16 @@ jest.mock('conf', () => { }) jest.mock('node:https', () => { + const once = jest.fn() const end = jest.fn() - const request = jest.fn().mockImplementation(() => ({ end })) as any - request.end = end - return { - request - } + const request = jest.fn().mockImplementation((_, __, f) => { + setTimeout(() => f(), 100) + return { once, end } + }) + // added for reach by toHaveBeenCalledWith + ;(request as any).once = once + ;(request as any).end = end + return { request } }) jest.mock('node:os', () => { @@ -86,7 +90,6 @@ jest.mock('ci-info', () => { return { isCI: false } }) -/////////////////////// const lists: Record = { Thing: { fields: { @@ -142,7 +145,7 @@ describe('Telemetry tests', () => { headers: { 'Content-Type': 'application/json', }, - }) + }, expect.any(Function)) expect((https.request as any).end).toHaveBeenCalledWith( JSON.stringify({ lastSentDate, @@ -161,7 +164,7 @@ describe('Telemetry tests', () => { headers: { 'Content-Type': 'application/json', }, - }) + }, expect.any(Function)) expect((https.request as any).end).toHaveBeenCalledWith( JSON.stringify({ lastSentDate, From 28848696b02575e5b9a3ffb03f2361ba3c46d561 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:42:54 +1000 Subject: [PATCH 17/19] add a line berak before printing status as part of inform --- packages/core/src/lib/telemetry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 08d70ebebc8..6b9d61fae36 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -194,10 +194,11 @@ function inform ( console.log(`${bold('Keystone Telemetry')}`) printAbout() console.log(`You can use ${g`"keystone telemetry --help"`} to update your preferences at any time`) - console.log() if (telemetry.informedAt === null) { + console.log() console.log(`No telemetry data has been sent as part of this notice`) } + console.log() printNext(telemetry) console.log() // gap to help visiblity console.log(`For more information, including how to opt-out see ${grey`https://keystonejs.com/telemetry`} (updated ${b`2024-08-20`})`) From 61079d9dbc4c8b8105bc3964cf94a8a0035e71d3 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:45:37 +1000 Subject: [PATCH 18/19] reduce linebreaks when printing inform --- packages/core/src/lib/telemetry.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 6b9d61fae36..58091cb1be3 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -146,11 +146,6 @@ async function collectPackageVersions () { return packages } -function printAbout () { - console.log(`${y`Keystone collects anonymous data when you run`} ${g`"keystone dev"`}`) - console.log() -} - function printNext (telemetry: Telemetry) { if (!telemetry) { console.log(`Telemetry data will ${r`not`} be sent by this system user`) @@ -192,7 +187,7 @@ function inform ( ) { console.log() // gap to help visiblity console.log(`${bold('Keystone Telemetry')}`) - printAbout() + console.log(`${y`Keystone collects anonymous data when you run`} ${g`"keystone dev"`}`) console.log(`You can use ${g`"keystone telemetry --help"`} to update your preferences at any time`) if (telemetry.informedAt === null) { console.log() From 3dff2b2ee630ffdcea48de2273722e0c57d18d1f Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:54:39 +1000 Subject: [PATCH 19/19] vary the auxiliary verb situationally --- packages/core/src/lib/telemetry.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/src/lib/telemetry.ts b/packages/core/src/lib/telemetry.ts index 58091cb1be3..7b8848ae70f 100644 --- a/packages/core/src/lib/telemetry.ts +++ b/packages/core/src/lib/telemetry.ts @@ -154,22 +154,24 @@ function printNext (telemetry: Telemetry) { console.log(`Telemetry data will be sent the next time you run ${g`"keystone dev"`}`) } -function printTelemetryStatus (telemetry: Telemetry) { +function printTelemetryStatus (telemetry: Telemetry, updated = false) { + const auxverb = updated ? 'has been' : 'is' + if (telemetry === undefined) { - console.log(`Keystone telemetry has been reset to ${y`uninitialized`}`) + console.log(`Keystone telemetry ${auxverb} ${y`uninitialized`}`) console.log() printNext(telemetry) return } if (telemetry === false) { - console.log(`Keystone telemetry is ${r`disabled`}`) + console.log(`Keystone telemetry ${auxverb} ${r`disabled`}`) console.log() printNext(telemetry) return } - console.log(`Keystone telemetry is ${g`enabled`}`) + console.log(`Keystone telemetry ${auxverb} ${g`enabled`}`) console.log() console.log(` Device telemetry was last sent on ${telemetry.device.lastSentDate}`) @@ -304,9 +306,9 @@ export async function runTelemetry ( } } -export function statusTelemetry () { +export function statusTelemetry (updated = false) { const { telemetry } = getTelemetryConfig() - printTelemetryStatus(telemetry) + printTelemetryStatus(telemetry, updated) } export function informTelemetry () { @@ -319,17 +321,17 @@ export function enableTelemetry () { if (!telemetry) { userConfig.set('telemetry', getDefault(telemetry)) } - statusTelemetry() + statusTelemetry(true) } export function disableTelemetry () { const { userConfig } = getTelemetryConfig() userConfig.set('telemetry', false) - statusTelemetry() + statusTelemetry(true) } export function resetTelemetry () { const { userConfig } = getTelemetryConfig() userConfig.delete('telemetry') - statusTelemetry() + statusTelemetry(true) }