-
Notifications
You must be signed in to change notification settings - Fork 447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cli) add telemetry scaffolding #5321
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
7c31555
fix(deps): add telemetry package as cli dependency
bjoerge d6a29d2
feat(cli): add telemetry scaffolding for CLI
bjoerge 5c1775d
fix(cli): flush telemetry store before logging out
bjoerge 520f583
refactor(cli): abstract continuous integration check
juice49 ec652fe
feat(cli): treat telemetry consent as denied when running in CI
juice49 0727413
fix(cli): parse trueish env vars when detecting ci environment
bjoerge 323cf72
fixup! feat(cli): add telemetry scaffolding for CLI
bjoerge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import {SanityClient} from '@sanity/client' | ||
import {ConsentStatus, createBatchedStore, createSessionId, TelemetryEvent} from '@sanity/telemetry' | ||
import {debug as baseDebug} from '../debug' | ||
import {getClientWrapper, getCliToken} from './clientWrapper' | ||
import {isTrueish} from './isTrueish' | ||
import {isCi} from './isCi' | ||
|
||
const debug = baseDebug.extend('telemetry') | ||
|
||
const VALID_STATUSES: ConsentStatus[] = ['granted', 'denied', 'unset'] | ||
function parseConsent(value: unknown): ConsentStatus { | ||
if (typeof value === 'string' && VALID_STATUSES.includes(value.toLowerCase() as any)) { | ||
return value as ConsentStatus | ||
} | ||
throw new Error(`Invalid consent status. Must be one of: ${VALID_STATUSES.join(', ')}`) | ||
} | ||
|
||
function createTelemetryClient(token: string) { | ||
const getClient = getClientWrapper(null, 'sanity.cli.js') | ||
return getClient({requireUser: false, requireProject: false}).config({ | ||
apiVersion: '2023-12-18', | ||
token, | ||
useCdn: false, | ||
useProjectHostname: false, | ||
}) | ||
} | ||
|
||
let _client: SanityClient | null = null | ||
function getCachedClient(token: string) { | ||
if (!_client) { | ||
_client = createTelemetryClient(token) | ||
} | ||
return _client | ||
} | ||
|
||
export function createTelemetryStore(options: {env: {[key: string]: string | undefined}}) { | ||
debug('Initializing telemetry') | ||
const {env} = options | ||
|
||
function fetchConsent(client: SanityClient) { | ||
return client.request({uri: '/intake/telemetry-status'}) | ||
} | ||
|
||
function resolveConsent(): Promise<{status: ConsentStatus}> { | ||
debug('Resolving consent…') | ||
if (isCi) { | ||
debug('CI environment detected, treating telemetry consent as denied') | ||
return Promise.resolve({status: 'denied'}) | ||
} | ||
if (isTrueish(env.DO_NOT_TRACK)) { | ||
debug('DO_NOT_TRACK is set, consent is denied') | ||
return Promise.resolve({status: 'denied'}) | ||
} | ||
const token = getCliToken() | ||
if (!token) { | ||
debug('User is not logged in, consent is undetermined') | ||
return Promise.resolve({status: 'undetermined'}) | ||
} | ||
const client = getCachedClient(token) | ||
return fetchConsent(client) | ||
.then((response) => { | ||
debug('User consent status is %s', response.status) | ||
return {status: parseConsent(response.status)} | ||
}) | ||
.catch((err) => { | ||
debug('Failed to fetch user consent status, treating it as "undetermined": %s', err.stack) | ||
return {status: 'undetermined'} | ||
}) | ||
} | ||
|
||
// Note: if this function throws/rejects the events will be put back on the buffer | ||
async function sendEvents(batch: TelemetryEvent[]) { | ||
const token = getCliToken() | ||
if (!token) { | ||
// Note: since the telemetry store checks for consent before sending events, and this token | ||
// check is also done during consent checking, this would normally never happen | ||
debug('No user token found. Something is not quite right') | ||
return Promise.reject(new Error('User is not logged in')) | ||
} | ||
const client = getCachedClient(token) | ||
debug('Submitting %s telemetry events', batch.length) | ||
try { | ||
return await client.request({ | ||
uri: '/intake/batch', | ||
method: 'POST', | ||
json: true, | ||
body: batch, | ||
}) | ||
} catch (err) { | ||
const statusCode = err.response && err.response.statusCode | ||
debug( | ||
'Failed to send telemetry events%s: %s', | ||
statusCode ? ` (HTTP ${statusCode})` : '', | ||
err.stack, | ||
) | ||
// note: we want to throw - the telemetry store implements error handling already | ||
throw err | ||
} | ||
} | ||
|
||
const sessionId = createSessionId() | ||
debug('session id: %s', sessionId) | ||
|
||
const store = createBatchedStore(sessionId, { | ||
resolveConsent, | ||
sendEvents, | ||
}) | ||
process.once('beforeExit', () => store.flush()) | ||
return store | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/* eslint-disable no-process-env */ | ||
import {isTrueish} from './isTrueish' | ||
|
||
export const isCi = | ||
isTrueish(process.env.CI) || // Travis CI, CircleCI, Gitlab CI, Appveyor, CodeShip | ||
isTrueish(process.env.CONTINUOUS_INTEGRATION) || // Travis CI | ||
process.env.BUILD_NUMBER // Jenkins, TeamCity |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export function isTrueish(value: string | undefined) { | ||
if (value === undefined) return false | ||
if (value.toLowerCase() === 'true') return true | ||
if (value.toLowerCase() === 'false') return false | ||
const number = parseInt(value, 10) | ||
if (isNaN(number)) return false | ||
return number > 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3758,6 +3758,17 @@ | |
dependencies: | ||
"@sanity/uuid" "3.0.2" | ||
|
||
"@sanity/telemetry@^0.7.0": | ||
version "0.7.0" | ||
resolved "https://registry.yarnpkg.com/@sanity/telemetry/-/telemetry-0.7.0.tgz#12df09a6a1b8ae5037685755257c5f7f52c3b276" | ||
integrity sha512-XyNN+bysHRVRxWXRrVkJFrcz7ZBd2jioy8L8xHm4rp5UxX2kv57LQT9JGwt3+TX37yjm/Dz8Z6LgsPi/7bN+aA== | ||
dependencies: | ||
lodash "^4.17.21" | ||
react "^18.2.0" | ||
react-dom "^18.2.0" | ||
rxjs "^7.8.1" | ||
typeid-js "^0.3.0" | ||
|
||
"@sanity/[email protected]": | ||
version "0.0.1-alpha.1" | ||
resolved "https://registry.yarnpkg.com/@sanity/test/-/test-0.0.1-alpha.1.tgz#35b0847b7b10a547f281ffe4615f53a8296e0bee" | ||
|
@@ -15663,6 +15674,13 @@ typedarray@^0.0.6: | |
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" | ||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== | ||
|
||
typeid-js@^0.3.0: | ||
version "0.3.0" | ||
resolved "https://registry.yarnpkg.com/typeid-js/-/typeid-js-0.3.0.tgz#704dedde2382fcb4e5c15c54d285f0209d37f9cf" | ||
integrity sha512-A1EmvIWG6xwYRfHuYUjPltHqteZ1EiDG+HOmbIYXeHUVztmnGrPIfU9KIK1QC30x59ko0r4JsMlwzsALCyiB3Q== | ||
dependencies: | ||
uuidv7 "^0.4.4" | ||
|
||
typescript@^5.1.6, typescript@^5.2.2: | ||
version "5.2.2" | ||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" | ||
|
@@ -15946,6 +15964,11 @@ uuid@^9.0.1: | |
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" | ||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== | ||
|
||
uuidv7@^0.4.4: | ||
version "0.4.4" | ||
resolved "https://registry.yarnpkg.com/uuidv7/-/uuidv7-0.4.4.tgz#e7ffd7981f590c478fb8868eff4bb3bc55fa90e6" | ||
integrity sha512-jjRGChg03uGp9f6wQYSO8qXkweJwRbA5WRuEQE8xLIiehIzIIi23qZSzsyvZPCPoFqkeLtZuz7Plt1LGukAInA== | ||
|
||
v8-compile-cache-lib@^3.0.1: | ||
version "3.0.1" | ||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this here instead of in the
logoutCommand
code?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trying to keep the interface exposed to subcommands minimal, so wanted to avoid passing the
flush
method to subcommands at this point. We might change this later if we see that it's something we want subcommands to be able to do, but for now it made sense to special case this here.