diff --git a/cli/analytics.ts b/cli/analytics.ts index d674bee12..772b8fa9d 100644 --- a/cli/analytics.ts +++ b/cli/analytics.ts @@ -1,21 +1,46 @@ +import yargs from "yargs"; + import Analytics from "analytics-node"; -import { getConfigSettings, getConfigSettingsPath, upsertConfigSettings } from "df/cli/config"; -import { ynQuestion } from "df/cli/console"; -import { v4 as uuidv4 } from "uuid"; +import {getConfigSettings, getConfigSettingsPath, upsertConfigSettings} from "df/cli/config"; +import {ynQuestion} from "df/cli/console"; +import {INamedOption} from "df/cli/yargswrapper"; +import {v4 as uuidv4} from "uuid"; + +export const trackOption: INamedOption = { + name: "track", + option: { + describe: `Sets analytics tracking without asking the user. Overrides settings.json`, + type: "boolean" + } +}; const analytics = new Analytics("eR24ln3MniE3TKZXkvAkOGkiSN02xXqw"); let currentCommand: string; +let allowAnonymousAnalytics: boolean; +let anonymousUserId: string; -export async function maybeConfigureAnalytics() { +export async function maybeConfigureAnalytics(track?: boolean) { const settings = await getConfigSettings(); + if (track !== undefined) { + allowAnonymousAnalytics = track; + if (track) { + // in the case where the user *wants* tracking and has no settings.json + // assign them a temporary tracking id + anonymousUserId = settings.anonymousUserId || uuidv4(); + } + return; + } // We should only ask if users want to track analytics if they are in an interactive terminal; if (!process.stdout.isTTY) { return; } if (settings.allowAnonymousAnalytics !== undefined) { + allowAnonymousAnalytics = settings.allowAnonymousAnalytics; + anonymousUserId = settings.anonymousUserId; return; } + const optInResponse = ynQuestion( ` To help improve the quality of our products, we collect anonymized usage data and anonymized stacktraces when crashes are encountered. @@ -24,22 +49,24 @@ This can be changed at any point by modifying your settings file: ${getConfigSet Would you like to opt-in to anonymous usage and error tracking?`, false ); + allowAnonymousAnalytics = optInResponse; + anonymousUserId = uuidv4(); + await upsertConfigSettings({ - allowAnonymousAnalytics: optInResponse, - anonymousUserId: uuidv4() + allowAnonymousAnalytics, + anonymousUserId }); } export async function trackCommand(command: string) { - currentCommand = command; - const config = await getConfigSettings(); - if (!config.allowAnonymousAnalytics) { + if (!allowAnonymousAnalytics) { return; } + currentCommand = command; await new Promise(resolve => { analytics.track( { - userId: config.anonymousUserId, + userId: anonymousUserId, event: "event_dataform_cli_command", properties: { command @@ -54,14 +81,13 @@ export async function trackCommand(command: string) { } export async function trackError() { - const config = await getConfigSettings(); - if (!config.allowAnonymousAnalytics) { + if (!allowAnonymousAnalytics) { return; } await new Promise(resolve => { analytics.track( { - userId: config.anonymousUserId, + userId: anonymousUserId, event: "event_dataform_cli_error", properties: { currentCommand diff --git a/cli/index.ts b/cli/index.ts index 1687418fc..4b9a2c31f 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -8,7 +8,7 @@ import { build, compile, credentials, init, install, run, table, test } from "df import { CREDENTIALS_FILENAME } from "df/api/commands/credentials"; import * as dbadapters from "df/api/dbadapters"; import { prettyJsonStringify } from "df/api/utils"; -import { trackError } from "df/cli/analytics"; +import {trackError, trackOption} from "df/cli/analytics"; import { print, printCompiledGraph, @@ -242,6 +242,7 @@ export function runCli() { description: "Create a new dataform project.", positionalOptions: [warehouseOption, projectDirOption], options: [ + trackOption, { name: defaultDatabaseOptionName, option: { @@ -316,7 +317,7 @@ export function runCli() { format: `install [${projectDirMustExistOption.name}]`, description: "Install a project's NPM dependencies.", positionalOptions: [projectDirMustExistOption], - options: [], + options: [trackOption], processFn: async argv => { print("Installing NPM dependencies...\n"); await install(argv[projectDirMustExistOption.name]); @@ -329,6 +330,7 @@ export function runCli() { description: `Create a ${credentials.CREDENTIALS_FILENAME} file for Dataform to use when accessing your warehouse.`, positionalOptions: [warehouseOption, projectDirMustExistOption], options: [ + trackOption, { name: testConnectionOptionName, option: { @@ -414,7 +416,8 @@ export function runCli() { schemaSuffixOverrideOption, jsonOutputOption, varsOption, - timeoutOption + timeoutOption, + trackOption ], processFn: async argv => { const projectDir = argv[projectDirMustExistOption.name]; @@ -503,7 +506,7 @@ export function runCli() { format: `test [${projectDirMustExistOption.name}]`, description: "Run the dataform project's unit tests on the configured data warehouse.", positionalOptions: [projectDirMustExistOption], - options: [credentialsOption, varsOption, timeoutOption], + options: [credentialsOption, varsOption, timeoutOption, trackOption], processFn: async argv => { print("Compiling...\n"); const compiledGraph = await compile({ @@ -574,7 +577,8 @@ export function runCli() { credentialsOption, jsonOutputOption, varsOption, - timeoutOption + timeoutOption, + trackOption ], processFn: async argv => { if (!argv[jsonOutputOption.name]) { @@ -692,7 +696,7 @@ export function runCli() { format: `format [${projectDirMustExistOption.name}]`, description: "Format the dataform project's files.", positionalOptions: [projectDirMustExistOption], - options: [], + options: [trackOption], processFn: async argv => { const filenames = glob.sync("{definitions,includes}/**/*.{js,sqlx}", { cwd: argv[projectDirMustExistOption.name] @@ -722,7 +726,7 @@ export function runCli() { format: `listtables <${warehouseOption.name}>`, description: "List tables on the configured data warehouse.", positionalOptions: [warehouseOption], - options: [credentialsOption], + options: [credentialsOption, trackOption], processFn: async argv => { const readCredentials = credentials.read( argv[warehouseOption.name], @@ -757,7 +761,7 @@ export function runCli() { } } ], - options: [credentialsOption], + options: [credentialsOption, trackOption], processFn: async argv => { const readCredentials = credentials.read( argv[warehouseOption.name], diff --git a/cli/yargswrapper.ts b/cli/yargswrapper.ts index 84872fb8f..6e54440fd 100644 --- a/cli/yargswrapper.ts +++ b/cli/yargswrapper.ts @@ -1,6 +1,6 @@ import yargs from "yargs"; -import { maybeConfigureAnalytics, trackCommand } from "df/cli/analytics"; +import {maybeConfigureAnalytics, trackCommand, trackOption} from "df/cli/analytics"; export interface ICli { commands: ICommand[]; @@ -28,7 +28,7 @@ export function createYargsCli(cli: ICli) { command.description, (yargsChainer: yargs.Argv) => createOptionsChain(yargsChainer, command), async (argv: { [argumentName: string]: any }) => { - await maybeConfigureAnalytics(); + await maybeConfigureAnalytics(argv[trackOption.name]); const analyticsTrack = trackCommand(command.format.split(" ")[0]); const exitCode = await command.processFn(argv); let timer: NodeJS.Timer; diff --git a/version.bzl b/version.bzl index c3b057290..dce7def9c 100644 --- a/version.bzl +++ b/version.bzl @@ -1,3 +1,3 @@ # NOTE: If you change the format of this line, you must change the bash command # in /scripts/publish to extract the version string correctly. -DF_VERSION = "2.4.0" +DF_VERSION = "2.4.1"