Skip to content

Commit 4aad06b

Browse files
committed
Merge branch 'trunk' of github.com:Automattic/studio into stu-363-user-settings-return-the-list-of-installed-editors
2 parents afaf875 + 2ff1895 commit 4aad06b

File tree

11 files changed

+212
-99
lines changed

11 files changed

+212
-99
lines changed

Diff for: cli/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { __ } from '@wordpress/i18n';
22
import { Command, Option } from 'commander';
3+
import { StatsGroup, StatsMetric } from 'src/lib/bump-stats/types';
34
import { registerCommand as registerPreviewCreateCommand } from 'cli/commands/preview/create';
45
import { registerCommand as registerPreviewDeleteCommand } from 'cli/commands/preview/delete';
56
import { registerCommand as registerPreviewListCommand } from 'cli/commands/preview/list';
67
import { registerCommand as registerPreviewUpdateCommand } from 'cli/commands/preview/update';
78
import { loadTranslations } from 'cli/lib/i18n';
9+
import { bumpAggregatedUniqueStat } from 'cli/lib/stats';
810
import { version } from 'cli/package.json';
911

1012
async function main() {
@@ -16,6 +18,9 @@ async function main() {
1618
.name( 'studio' )
1719
.description( __( 'Studio by WordPress.com CLI' ) )
1820
.version( version )
21+
.hook( 'preAction', () => {
22+
bumpAggregatedUniqueStat( StatsGroup.STUDIO_CLI_USAGE_UNIQUE, StatsMetric.SUCCESS, 'weekly' );
23+
} )
1924
.addOption(
2025
new Option( '--output-format [format]', __( 'Specify a non-standard output format' ) )
2126
.argParser( ( value: string ) => {

Diff for: cli/lib/appdata.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'fs';
22
import os from 'os';
33
import path from 'path';
44
import { __ } from '@wordpress/i18n';
5+
import { StatsGroup, StatsMetric } from 'src/lib/bump-stats/types';
56
import { z } from 'zod';
67
import { LoggerError } from 'cli/logger';
78

@@ -34,6 +35,9 @@ const userDataSchema = z
3435
} )
3536
.passthrough()
3637
.optional(),
38+
lastBumpStats: z
39+
.record( z.nativeEnum( StatsGroup ), z.record( z.nativeEnum( StatsMetric ), z.number() ) )
40+
.optional(),
3741
} )
3842
.passthrough();
3943

Diff for: cli/lib/stats.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { isSameDay, isSameMonth, isSameWeek } from 'date-fns';
2+
import fetch from 'node-fetch';
3+
import { AggregateInterval, StatsGroup, StatsMetric } from 'src/lib/bump-stats/types';
4+
import { readAppdata, saveAppdata } from './appdata';
5+
6+
// Bumps a stat if it hasn't been bumped within the current aggregate interval.
7+
// This allows us to approximate a 1-count-per-user stat without recording which
8+
// user the event came from (bump stats are anonymous).
9+
//
10+
// We don't want to block the thread to record the stat, so this function doesn't
11+
// await promises before returning.
12+
export async function bumpAggregatedUniqueStat(
13+
group: StatsGroup,
14+
stat: StatsMetric,
15+
aggregateBy: AggregateInterval,
16+
bumpInDev = false
17+
) {
18+
const lastBump = await getLastBump( group, stat );
19+
20+
if ( lastBump === null ) {
21+
bumpStat( group, stat, bumpInDev );
22+
await updateLastBump( group, stat );
23+
return;
24+
}
25+
26+
const now = Date.now();
27+
28+
if ( aggregateBy === 'daily' && isSameDay( lastBump, now ) ) {
29+
return;
30+
}
31+
if ( aggregateBy === 'weekly' && isSameWeek( lastBump, now ) ) {
32+
return;
33+
}
34+
if ( aggregateBy === 'monthly' && isSameMonth( lastBump, now ) ) {
35+
return;
36+
}
37+
38+
const didBump = bumpStat( group, stat, bumpInDev );
39+
if ( didBump ) {
40+
await updateLastBump( group, stat );
41+
}
42+
}
43+
44+
// Returns true if we attempted to bump the stat
45+
export function bumpStat( group: StatsGroup, stat: StatsMetric, bumpInDev = false ) {
46+
if ( process.env.NODE_ENV === 'development' && ! bumpInDev ) {
47+
console.info( `Would have bumped stat: ${ group }=${ stat }` );
48+
return false;
49+
}
50+
51+
// Fire and forget POST request
52+
fetch( 'https://public-api.wordpress.com/wpcom/v2/studio-app/bump-stat', {
53+
method: 'POST',
54+
headers: {
55+
'Content-Type': 'application/json',
56+
},
57+
body: JSON.stringify( { group, stat } ),
58+
} ).catch( () => {
59+
// A failed request typically indicates a network issue, which we don't need to report
60+
} );
61+
62+
return true;
63+
}
64+
65+
// Returns UTC timestamp of the last time the stat was bumped, or null if it has never been bumped.
66+
async function getLastBump( group: StatsGroup, stat: StatsMetric ): Promise< number | null > {
67+
const { lastBumpStats } = await readAppdata();
68+
return lastBumpStats?.[ group ]?.[ stat ] ?? null;
69+
}
70+
71+
// Store this moment as the last time we bumped the state, in UTC time.
72+
async function updateLastBump( group: StatsGroup, stat: StatsMetric ) {
73+
const data = await readAppdata();
74+
data.lastBumpStats ??= {};
75+
data.lastBumpStats[ group ] ??= {};
76+
( data.lastBumpStats[ group ] as Record< StatsMetric, number > )[ stat ] = Date.now();
77+
await saveAppdata( data );
78+
}

Diff for: src/index.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { PROTOCOL_PREFIX } from 'src/constants';
1515
import * as ipcHandlers from 'src/ipc-handlers';
1616
import { hasActiveSyncOperations } from 'src/lib/active-sync-operations';
1717
import { bumpAggregatedUniqueStat, bumpStat } from 'src/lib/bump-stats';
18-
import { getPlatformMetric, StatsGroup } from 'src/lib/bump-stats/types';
18+
import { getPlatformMetric } from 'src/lib/bump-stats/lib';
19+
import { StatsGroup } from 'src/lib/bump-stats/types';
1920
import {
2021
listenCLICommands,
2122
getCLIDataForMainInstance,
@@ -30,9 +31,11 @@ import { getSentryReleaseInfo } from 'src/lib/sentry-release';
3031
import { setupLogging } from 'src/logging';
3132
import { createMainWindow, getMainWindow } from 'src/main-window';
3233
import {
33-
migrateFromWpNowFolder,
3434
needsToMigrateFromWpNowFolder,
35+
migrateFromWpNowFolder,
3536
} from 'src/migrations/migrate-from-wp-now-folder';
37+
import { migrateAllDatabasesInSitu } from 'src/migrations/move-databases-in-situ';
38+
import { removeSitesWithEmptyDirectories } from 'src/migrations/remove-sites-with-empty-dirs';
3639
import { installCLIOnWindows } from 'src/modules/cli/lib/install-windows';
3740
import { setupWPServerFiles, updateWPServerFiles } from 'src/setup-wp-server-files';
3841
import { stopAllServersOnQuit } from 'src/site-server';
@@ -76,6 +79,19 @@ if ( gotTheLock && ! isInInstaller ) {
7679
}
7780
}
7881

82+
async function setupSentryUserId() {
83+
const userData = await loadUserData();
84+
85+
if ( ! userData.sentryUserId ) {
86+
userData.sentryUserId = crypto.randomUUID();
87+
console.log( Date.now(), 'Saving sentry user ID', userData.sentryUserId );
88+
await saveUserData( userData );
89+
}
90+
91+
console.log( 'Setting Sentry user ID:', userData.sentryUserId );
92+
Sentry.setUser( { id: userData.sentryUserId } );
93+
}
94+
7995
async function appBoot() {
8096
app.setName( packageJson.productName );
8197

@@ -255,22 +271,19 @@ async function appBoot() {
255271
await migrateFromWpNowFolder();
256272
}
257273

258-
const userData = await loadUserData();
274+
await setupSentryUserId();
259275

260-
if ( ! userData.sentryUserId ) {
261-
userData.sentryUserId = crypto.randomUUID();
262-
await saveUserData( userData );
263-
}
276+
await removeSitesWithEmptyDirectories();
264277

265-
console.log( 'Setting Sentry user ID:', userData.sentryUserId );
266-
Sentry.setUser( { id: userData.sentryUserId } );
278+
await migrateAllDatabasesInSitu();
267279

268280
createMainWindow();
269281

270282
// Handle CLI commands
271283
listenCLICommands();
272284
executeCLICommand();
273285

286+
const userData = await loadUserData();
274287
// Bump stats for the first time the app runs - this is when no lastBumpStats are available
275288
if ( ! userData.lastBumpStats ) {
276289
bumpStat( StatsGroup.STUDIO_APP_LAUNCH, getPlatformMetric( process.platform ) );

Diff for: src/ipc-handlers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { ARCHIVER_OPTIONS, MAIN_MIN_WIDTH, SIDEBAR_WIDTH } from 'src/constants';
2121
import { sendIpcEventToRendererWithWindow } from 'src/ipc-utils';
2222
import { ACTIVE_SYNC_OPERATIONS } from 'src/lib/active-sync-operations';
2323
import { bumpStat } from 'src/lib/bump-stats';
24-
import { getImporterMetric, StatsGroup, StatsMetric } from 'src/lib/bump-stats/types';
24+
import { getImporterMetric } from 'src/lib/bump-stats/lib';
25+
import { StatsGroup, StatsMetric } from 'src/lib/bump-stats/types';
2526
import { calculateDirectorySize } from 'src/lib/calculate-directory-size';
2627
import { download } from 'src/lib/download';
2728
import { isEmptyDir, pathExists, isWordPressDirectory, sanitizeFolderName } from 'src/lib/fs-utils';

Diff for: src/lib/bump-stats/lib.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { StatsMetric } from 'src/lib/bump-stats/types';
2+
import {
3+
JetpackImporter,
4+
LocalImporter,
5+
PlaygroundImporter,
6+
SQLImporter,
7+
WpressImporter,
8+
} from 'src/lib/import-export/import/importers';
9+
10+
export function getPlatformMetric( platform: typeof process.platform ): StatsMetric {
11+
switch ( platform ) {
12+
case 'darwin':
13+
return StatsMetric.DARWIN;
14+
case 'linux':
15+
return StatsMetric.LINUX;
16+
case 'win32':
17+
return StatsMetric.WINDOWS;
18+
default:
19+
return StatsMetric.UNKNOWN_PLATFORM;
20+
}
21+
}
22+
23+
export function getImporterMetric( importer?: string ): StatsMetric {
24+
switch ( importer ) {
25+
case JetpackImporter.name:
26+
return StatsMetric.JETPACK_IMPORTER;
27+
case LocalImporter.name:
28+
return StatsMetric.LOCAL_IMPORTER;
29+
case SQLImporter.name:
30+
return StatsMetric.SQL_IMPORTER;
31+
case PlaygroundImporter.name:
32+
return StatsMetric.PLAYGROUND_IMPORTER;
33+
case WpressImporter.name:
34+
return StatsMetric.WPRESS_IMPORTER;
35+
default:
36+
return StatsMetric.UNKNOWN_IMPORTER;
37+
}
38+
}

Diff for: src/lib/bump-stats/types.ts

+3-38
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import {
2-
JetpackImporter,
3-
LocalImporter,
4-
PlaygroundImporter,
5-
SQLImporter,
6-
WpressImporter,
7-
} from 'src/lib/import-export/import/importers';
8-
91
export enum StatsGroup {
2+
// Studio app
103
STUDIO_APP_LAUNCH = 'studio-app-launch-first',
114
STUDIO_APP_LAUNCH_TOTAL = 'studio-app-launch-total',
125
STUDIO_APP_LAUNCH_UNIQUE = 'local-environment-launch-uniques',
136
STUDIO_IMPORT = 'studio-app-import',
147
STUDIO_EXPORT = 'studio-app-export',
8+
// Studio CLI
9+
STUDIO_CLI_USAGE_UNIQUE = 'studio-cli-usage-unique',
1510
}
1611

1712
export enum StatsMetric {
@@ -35,33 +30,3 @@ export enum StatsMetric {
3530
}
3631

3732
export type AggregateInterval = 'daily' | 'weekly' | 'monthly';
38-
39-
export function getPlatformMetric( platform: typeof process.platform ): StatsMetric {
40-
switch ( platform ) {
41-
case 'darwin':
42-
return StatsMetric.DARWIN;
43-
case 'linux':
44-
return StatsMetric.LINUX;
45-
case 'win32':
46-
return StatsMetric.WINDOWS;
47-
default:
48-
return StatsMetric.UNKNOWN_PLATFORM;
49-
}
50-
}
51-
52-
export function getImporterMetric( importer?: string ): StatsMetric {
53-
switch ( importer ) {
54-
case JetpackImporter.name:
55-
return StatsMetric.JETPACK_IMPORTER;
56-
case LocalImporter.name:
57-
return StatsMetric.LOCAL_IMPORTER;
58-
case SQLImporter.name:
59-
return StatsMetric.SQL_IMPORTER;
60-
case PlaygroundImporter.name:
61-
return StatsMetric.PLAYGROUND_IMPORTER;
62-
case WpressImporter.name:
63-
return StatsMetric.WPRESS_IMPORTER;
64-
default:
65-
return StatsMetric.UNKNOWN_IMPORTER;
66-
}
67-
}

Diff for: src/main-window.ts

-25
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@ import {
77
WINDOWS_TITLEBAR_HEIGHT,
88
} from 'src/constants';
99
import { sendIpcEventToRendererWithWindow } from 'src/ipc-utils';
10-
import { isEmptyDir } from 'src/lib/fs-utils';
1110
import { portFinder } from 'src/lib/port-finder';
12-
import { keepSqliteIntegrationUpdated } from 'src/lib/sqlite-versions';
1311
import { removeMenu } from 'src/menu';
14-
import { UserData } from 'src/storage/storage-types';
1512
import { loadUserData, saveUserData } from 'src/storage/user-data';
16-
import { moveDatabasesInSitu } from 'vendor/wp-now/src';
1713

1814
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
1915
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
@@ -38,21 +34,6 @@ function initializePortFinder( sites: SiteDetails[] ) {
3834
} );
3935
}
4036

41-
async function removeSitesWithEmptyDirectories( userData: UserData ) {
42-
const sitesWithNonEmptyDirectories: SiteDetails[] = [];
43-
const storedSites = userData.sites || [];
44-
for ( const site of storedSites ) {
45-
if ( ! site.path ) {
46-
continue;
47-
}
48-
const directoryIsEmpty = await isEmptyDir( site.path );
49-
if ( ! directoryIsEmpty ) {
50-
sitesWithNonEmptyDirectories.push( site );
51-
}
52-
}
53-
saveUserData( { ...userData, sites: sitesWithNonEmptyDirectories } );
54-
}
55-
5637
export function createMainWindow(): BrowserWindow {
5738
if ( mainWindow && ! mainWindow.isDestroyed() ) {
5839
return mainWindow;
@@ -78,12 +59,6 @@ export function createMainWindow(): BrowserWindow {
7859
const { devToolsOpen, sites } = userData;
7960
setupDevTools( mainWindow, devToolsOpen );
8061
initializePortFinder( sites );
81-
removeSitesWithEmptyDirectories( userData );
82-
for ( const site of sites ) {
83-
moveDatabasesInSitu( site.path ).then( () => {
84-
keepSqliteIntegrationUpdated( site.path );
85-
} );
86-
}
8762
} );
8863

8964
mainWindow.webContents.on( 'devtools-opened', async () => {

Diff for: src/migrations/move-databases-in-situ.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import path from 'node:path';
2+
import fs from 'fs-extra';
3+
import { keepSqliteIntegrationUpdated } from 'src/lib/sqlite-versions';
4+
import { loadUserData } from 'src/storage/user-data';
5+
import getWpNowConfig from 'vendor/wp-now/src/config';
6+
import { SQLITE_FILENAME } from 'vendor/wp-now/src/constants';
7+
import getSqlitePath from 'vendor/wp-now/src/get-sqlite-path';
8+
9+
async function moveDatabasesInSitu( projectPath: string ) {
10+
const dbPhpPath = path.join( projectPath, 'wp-content', 'db.php' );
11+
const hasDbPhpInSitu = fs.existsSync( dbPhpPath ) && fs.lstatSync( dbPhpPath ).isFile();
12+
13+
const { wpContentPath } = await getWpNowConfig( { path: projectPath } );
14+
if (
15+
wpContentPath &&
16+
fs.existsSync( path.join( wpContentPath, 'database' ) ) &&
17+
! hasDbPhpInSitu
18+
) {
19+
// Do not mount but move the files to projectPath once
20+
const databasePath = path.join( projectPath, 'wp-content', 'database' );
21+
fs.rmdirSync( databasePath );
22+
fs.moveSync( path.join( wpContentPath, 'database' ), databasePath );
23+
24+
const sqlitePath = path.join( projectPath, 'wp-content', 'plugins', SQLITE_FILENAME );
25+
fs.rmdirSync( sqlitePath );
26+
fs.copySync( path.join( getSqlitePath() ), sqlitePath );
27+
28+
fs.rmdirSync( dbPhpPath );
29+
fs.copySync( path.join( getSqlitePath(), 'db.copy' ), dbPhpPath );
30+
fs.rmSync( wpContentPath, { recursive: true, force: true } );
31+
}
32+
}
33+
34+
export async function migrateAllDatabasesInSitu() {
35+
const userData = await loadUserData();
36+
for ( const site of userData.sites ) {
37+
moveDatabasesInSitu( site.path ).then( () => {
38+
keepSqliteIntegrationUpdated( site.path );
39+
} );
40+
}
41+
}

0 commit comments

Comments
 (0)