diff --git a/src/lib/amplitude-sample-data.ts b/src/lib/amplitude-sample-data.ts index a4be09b5..9a10c9a3 100644 --- a/src/lib/amplitude-sample-data.ts +++ b/src/lib/amplitude-sample-data.ts @@ -71,31 +71,6 @@ const CHART_SPECS: Record = { "wsl2-virtioproxy", ], }, - commands: { - title: "DDEV Commands", - base: 900000, - decay: 0.72, - labels: [ - "exec", - "mysql", - "describe", - "wp", - "custom-command", - "status", - "composer", - "php", - "start", - "artisan", - "xdebug", - "typo3", - "npm", - "restart", - "stop", - "ssh", - "craft", - "magento", - ], - }, cmsProjectTypes: { title: "CMS Project Types", base: 5000, @@ -224,6 +199,34 @@ const CHART_SPECS: Record = { type PropertySpec = { labels: string[]; base: number; decay: number } +// Sample fallback for getCommandUsage(): documented ddev commands in roughly +// their real popularity order (Totals). Not a saved chart, so it lives outside +// CHART_SPECS. +const COMMAND_USAGE_SPEC: PropertySpec = { + base: 900000, + decay: 0.72, + labels: [ + "exec", + "drush", + "mysql", + "describe", + "wp", + "composer", + "php", + "start", + "artisan", + "xdebug", + "typo3", + "npm", + "restart", + "stop", + "ssh", + "craft", + "magento", + "import-db", + ], +} + // Keyed by the property names passed to getProjectPropertyBreakdown(). const PROPERTY_SPECS: Record = { "PHP Version": { @@ -267,6 +270,12 @@ export function getSampleChart(key: string): AmplitudeChart | null { } } +// Sample breakdown of command usage (see getCommandUsage). +export function getSampleCommandUsage(): PropertyBreakdown { + const spec = COMMAND_USAGE_SPEC + return toBreakdown(decayingCounts(spec.labels, spec.base, spec.decay)) +} + // Sample breakdown for a "Project" property name, or null if unknown. export function getSampleProjectProperty( propertyName: string diff --git a/src/lib/amplitude.ts b/src/lib/amplitude.ts index e6fda030..e241123a 100644 --- a/src/lib/amplitude.ts +++ b/src/lib/amplitude.ts @@ -6,6 +6,7 @@ import dotenv from "dotenv" import { getCache, putCache } from "./api" import { getSampleChart, + getSampleCommandUsage, getSampleMonthlyUserHistory, getSampleProjectProperty, } from "./amplitude-sample-data" @@ -15,7 +16,6 @@ dotenv.config() // Saved chart IDs behind each usage-stats section (see https://app.amplitude.com/analytics/ddev/home) export const AMPLITUDE_CHARTS = { environment: "vkdc4w6s", - commands: "bzk6dz2u", cmsProjectTypes: "jz0wqx1w", macosDockerProviders: "2orxojlh", wsl2DockerProviders: "5o615zus", @@ -35,8 +35,6 @@ export const AMPLITUDE_SHARE_LINKS: Partial< > = { environment: "https://app.amplitude.com/analytics/share/1238aca77448459aac7473f2fedbe109", - commands: - "https://app.amplitude.com/analytics/share/f4e48d8678134561ae034f9a020faab4", cmsProjectTypes: "https://app.amplitude.com/analytics/share/0619ab47f9cd433cb14bdea4b4aab3e2", macosDockerProviders: @@ -496,11 +494,164 @@ async function getProjectPropertyBreakdown( const breakdown: PropertyBreakdown = { rows: counts - .map((row) => ({ + .map((row: { label: string; count: number }) => ({ ...row, percent: grandTotal ? (row.count / grandTotal) * 100 : 0, })) - .sort((a, b) => b.count - a.count), + .sort((a: PercentageRow, b: PercentageRow) => b.count - a.count), + } + + putCache(cacheFilename, JSON.stringify(breakdown)) + + return breakdown +} + +// Built-in ddev commands documented at +// https://docs.ddev.com/en/stable/users/usage/commands/. The command-usage +// breakdown filters Amplitude's raw "Command Name" values to this set so it +// shows only real ddev subcommands. Everything else Amplitude records under +// that property is deliberately dropped: the "custom-command" aggregate and +// project-specific custom command names, second-level sub-args tracked as +// their own name (e.g. "pantheon"/"acquia" from `ddev pull pantheon`), and +// undocumented/legacy names. Keep sorted for easy scanning. +export const DDEV_COMMANDS: readonly string[] = [ + "add-on", + "aliases", + "artisan", + "asterios", + "auth", + "blackfire", + "cake", + "clean", + "composer", + "config", + "console", + "craft", + "dbeaver", + "delete", + "describe", + "dotenv", + "dr", + "drush", + "exec", + "export-db", + "heidisql", + "help", + "hostname", + "import-db", + "import-files", + "launch", + "list", + "logs", + "magento", + "mailpit", + "mariadb", + "mutagen", + "mysql", + "npm", + "npx", + "php", + "poweroff", + "psql", + "pull", + "push", + "querious", + "restart", + "sake", + "self-upgrade", + "sequelace", + "share", + "snapshot", + "spark", + "ssh", + "start", + "stop", + "tableplus", + "tui", + "typo3", + "utility", + "version", + "wp", + "xdebug", + "xhgui", + "xhprof", + "yarn", +] + +/** + * Gets total invocations per ddev command over the last 7 days, via an ad hoc + * Events Segmentation query on the "Command" event grouped by "Command Name", + * then filtered to DDEV_COMMANDS. Done in code (rather than an Amplitude saved + * chart's allow-list) so the filter is version-controlled and reviewable, and + * stays a single source of truth. Percentages are relative to the displayed + * (documented) commands only. + */ +export async function getCommandUsage(): Promise { + const cacheFilename = "amplitude-command-usage.json" + const cachedData = getCache(cacheFilename) + + if (cachedData) { + return cachedData + } + + if (!amplitudeCredentialsSet) { + return useSampleData ? getSampleCommandUsage() : null + } + + const end = new Date() + const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000) + const toYyyymmdd = (date: Date) => + date.toISOString().slice(0, 10).replace(/-/g, "") + + const params = new URLSearchParams({ + e: JSON.stringify({ + event_type: "Command", + group_by: [{ type: "event", value: "Command Name" }], + }), + start: toYyyymmdd(start), + end: toYyyymmdd(end), + i: "1", + m: "totals", + limit: "1000", + }) + + const response = await fetch( + `https://amplitude.com/api/2/events/segmentation?${params.toString()}`, + { + headers: { Authorization: authHeader() }, + } + ) + + if (!response.ok) { + throw new Error( + `Amplitude segmentation request failed: HTTP ${response.status}` + ) + } + + const { data } = await response.json() + const allowed = new Set(DDEV_COMMANDS) + // Inline group_by returns seriesLabels as [groupIndex, label] pairs. + const counts = data.seriesLabels + .map((entry: string | [number, string], index: number) => ({ + label: Array.isArray(entry) ? entry[1] : entry, + count: data.seriesCollapsed[index]?.[0]?.value ?? 0, + })) + .filter( + (row: { label: string; count: number }) => + allowed.has(row.label) && row.count > 0 + ) + const grandTotal = counts.reduce( + (sum: number, row: { count: number }) => sum + row.count, + 0 + ) + + const breakdown: PropertyBreakdown = { + rows: counts + .map((row: { label: string; count: number }) => ({ + ...row, + percent: grandTotal ? (row.count / grandTotal) * 100 : 0, + })) + .sort((a: PercentageRow, b: PercentageRow) => b.count - a.count), } putCache(cacheFilename, JSON.stringify(breakdown)) diff --git a/src/pages/usage-stats.astro b/src/pages/usage-stats.astro index 1c125bd6..5ab7f6b8 100644 --- a/src/pages/usage-stats.astro +++ b/src/pages/usage-stats.astro @@ -10,6 +10,7 @@ import { AMPLITUDE_CHARTS, AMPLITUDE_SHARE_LINKS, getAmplitudeChart, + getCommandUsage, getDatabaseTypes, getMonthlyUserHistory, getPhpVersions, @@ -44,7 +45,7 @@ const linkClass = "underline dark:text-white" // for properties/filters no saved chart covers correctly (see each // function's own comment in lib/amplitude.ts for why). const environment = await getAmplitudeChart(AMPLITUDE_CHARTS.environment) -const commands = await getAmplitudeChart(AMPLITUDE_CHARTS.commands) +const commandUsage = await getCommandUsage() const cmsProjectTypes = await getAmplitudeChart( AMPLITUDE_CHARTS.cmsProjectTypes ) @@ -181,30 +182,47 @@ const sponsorshipGoal = sponsorshipData?.current_goal } { - commands && ( + commandUsage && (

- The most popular ddev subcommands, excluding - drush, which dwarfs everything else and is likely - run in automation. + Total invocations of built-in ddev commands over the + past week. Counts for commands like exec and{" "} + drush are inflated by scripting and automated + workflows. Expand below for the full list of documented commands + with usage.

- {AMPLITUDE_SHARE_LINKS.commands && ( -

- - Live values for DDEV commands - -

- )} +
+ + Show all {commandUsage.rows.length} documented commands with + usage + +
+ + + + + + + + + {commandUsage.rows.map((row) => ( + + + + + ))} + +
CommandInvocations
{row.label} + {row.count.toLocaleString("en-US")} +
+
+
) }