-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #30070 from storybookjs/tom/metadata-tweaks
Telemetry: Add metadata distinguishing "apps" from "design systems"
- Loading branch information
Showing
16 changed files
with
307 additions
and
36 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
// empty file only matched on path |
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 @@ | ||
// empty file only matched on path |
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,71 @@ | ||
import type { Transform } from 'node:stream'; | ||
import { PassThrough } from 'node:stream'; | ||
|
||
import { beforeEach, describe, expect, it, vitest } from 'vitest'; | ||
|
||
// eslint-disable-next-line depend/ban-dependencies | ||
import { execaCommand as rawExecaCommand } from 'execa'; | ||
|
||
import { execCommandCountLines } from './exec-command-count-lines'; | ||
|
||
vitest.mock('execa'); | ||
|
||
const execaCommand = vitest.mocked(rawExecaCommand); | ||
beforeEach(() => { | ||
execaCommand.mockReset(); | ||
}); | ||
|
||
type ExecaStreamer = typeof Promise & { | ||
stdout: Transform; | ||
kill: () => void; | ||
}; | ||
|
||
function createExecaStreamer() { | ||
let resolver: () => void; | ||
const promiseLike: ExecaStreamer = new Promise<void>((aResolver, aRejecter) => { | ||
resolver = aResolver; | ||
}) as any; | ||
|
||
promiseLike.stdout = new PassThrough(); | ||
// @ts-expect-error technically it is invalid to use resolver "before" it is assigned (but not really) | ||
promiseLike.kill = resolver; | ||
return promiseLike; | ||
} | ||
|
||
describe('execCommandCountLines', () => { | ||
it('counts lines, many', async () => { | ||
const streamer = createExecaStreamer(); | ||
execaCommand.mockReturnValue(streamer as any); | ||
|
||
const promise = execCommandCountLines('some command'); | ||
|
||
streamer.stdout.write('First line\n'); | ||
streamer.stdout.write('Second line\n'); | ||
streamer.kill(); | ||
|
||
expect(await promise).toEqual(2); | ||
}); | ||
|
||
it('counts lines, one', async () => { | ||
const streamer = createExecaStreamer(); | ||
execaCommand.mockReturnValue(streamer as any); | ||
|
||
const promise = execCommandCountLines('some command'); | ||
|
||
streamer.stdout.write('First line\n'); | ||
streamer.kill(); | ||
|
||
expect(await promise).toEqual(1); | ||
}); | ||
|
||
it('counts lines, none', async () => { | ||
const streamer = createExecaStreamer(); | ||
execaCommand.mockReturnValue(streamer as any); | ||
|
||
const promise = execCommandCountLines('some command'); | ||
|
||
streamer.kill(); | ||
|
||
expect(await promise).toEqual(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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { createInterface } from 'node:readline'; | ||
|
||
// eslint-disable-next-line depend/ban-dependencies | ||
import { execaCommand } from 'execa'; | ||
|
||
/** | ||
* Execute a command in the local terminal and count the lines in the result | ||
* | ||
* @param command The command to execute. | ||
* @param options Execa options | ||
* @returns The number of lines the command returned | ||
*/ | ||
export async function execCommandCountLines( | ||
command: string, | ||
options?: Parameters<typeof execaCommand>[1] | ||
) { | ||
const process = execaCommand(command, { shell: true, buffer: false, ...options }); | ||
if (!process.stdout) { | ||
// eslint-disable-next-line local-rules/no-uncategorized-errors | ||
throw new Error('Unexpected missing stdout'); | ||
} | ||
|
||
let lineCount = 0; | ||
const rl = createInterface(process.stdout); | ||
rl.on('line', () => { | ||
lineCount += 1; | ||
}); | ||
|
||
// If the process errors, this will throw | ||
await process; | ||
|
||
rl.close(); | ||
|
||
return lineCount; | ||
} |
14 changes: 14 additions & 0 deletions
14
code/core/src/telemetry/get-application-file-count.test.ts
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,14 @@ | ||
import { join } from 'node:path'; | ||
|
||
import { describe, expect, it } from 'vitest'; | ||
|
||
import { getApplicationFilesCountUncached } from './get-application-file-count'; | ||
|
||
const mocksDir = join(__dirname, '..', '__mocks__'); | ||
|
||
describe('getApplicationFilesCount', () => { | ||
it('should find files with correct names', async () => { | ||
const files = await getApplicationFilesCountUncached(mocksDir); | ||
expect(files).toMatchInlineSnapshot(`2`); | ||
}); | ||
}); |
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,32 @@ | ||
import { sep } from 'node:path'; | ||
|
||
import { execCommandCountLines } from './exec-command-count-lines'; | ||
import { runTelemetryOperation } from './run-telemetry-operation'; | ||
|
||
// We are looking for files with the word "page" or "screen" somewhere in them with these exts | ||
const nameMatches = ['page', 'screen']; | ||
const extensions = ['js', 'jsx', 'ts', 'tsx']; | ||
|
||
export const getApplicationFilesCountUncached = async (basePath: string) => { | ||
const bothCasesNameMatches = nameMatches.flatMap((match) => [ | ||
match, | ||
[match[0].toUpperCase(), ...match.slice(1)].join(''), | ||
]); | ||
|
||
const globs = bothCasesNameMatches.flatMap((match) => | ||
extensions.map((extension) => `"${basePath}${sep}*${match}*.${extension}"`) | ||
); | ||
|
||
try { | ||
const command = `git ls-files -- ${globs.join(' ')}`; | ||
return await execCommandCountLines(command); | ||
} catch { | ||
return undefined; | ||
} | ||
}; | ||
|
||
export const getApplicationFileCount = async (path: string) => { | ||
return runTelemetryOperation('applicationFiles', async () => | ||
getApplicationFilesCountUncached(path) | ||
); | ||
}; |
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,29 @@ | ||
import { expect, it } from 'vitest'; | ||
|
||
import { getHasRouterPackage } from './get-has-router-package'; | ||
|
||
it('returns true if there is a routing package in package.json', () => { | ||
expect( | ||
getHasRouterPackage({ | ||
dependencies: { | ||
react: '^18', | ||
'react-dom': '^18', | ||
'react-router': '^6', | ||
}, | ||
}) | ||
).toBe(true); | ||
}); | ||
|
||
it('returns false if there is a routing package in package.json dependencies', () => { | ||
expect( | ||
getHasRouterPackage({ | ||
dependencies: { | ||
react: '^18', | ||
'react-dom': '^18', | ||
}, | ||
devDependencies: { | ||
'react-router': '^6', | ||
}, | ||
}) | ||
).toBe(false); | ||
}); |
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,37 @@ | ||
import type { PackageJson } from '../types'; | ||
|
||
const routerPackages = new Set([ | ||
'react-router', | ||
'react-router-dom', | ||
'remix', | ||
'@tanstack/react-router', | ||
'expo-router', | ||
'@reach/router', | ||
'react-easy-router', | ||
'@remix-run/router', | ||
'wouter', | ||
'wouter-preact', | ||
'preact-router', | ||
'vue-router', | ||
'unplugin-vue-router', | ||
'@angular/router', | ||
'@solidjs/router', | ||
|
||
// metaframeworks that imply routing | ||
'next', | ||
'react-scripts', | ||
'gatsby', | ||
'nuxt', | ||
'@sveltejs/kit', | ||
]); | ||
|
||
/** | ||
* @param packageJson The package JSON of the project | ||
* @returns Boolean Does this project use a routing package? | ||
*/ | ||
export function getHasRouterPackage(packageJson: PackageJson) { | ||
// NOTE: we just check real dependencies; if it is in dev dependencies, it may just be an example | ||
return Object.keys(packageJson?.dependencies ?? {}).some((depName) => | ||
routerPackages.has(depName) | ||
); | ||
} |
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 |
---|---|---|
@@ -1,37 +1,18 @@ | ||
// eslint-disable-next-line depend/ban-dependencies | ||
import { execaCommand } from 'execa'; | ||
|
||
import { createFileSystemCache, resolvePathInStorybookCache } from '../common'; | ||
|
||
const cache = createFileSystemCache({ | ||
basePath: resolvePathInStorybookCache('portable-stories'), | ||
ns: 'storybook', | ||
ttl: 24 * 60 * 60 * 1000, // 24h | ||
}); | ||
import { execCommandCountLines } from './exec-command-count-lines'; | ||
import { runTelemetryOperation } from './run-telemetry-operation'; | ||
|
||
export const getPortableStoriesFileCountUncached = async (path?: string) => { | ||
const command = `git grep -l composeStor` + (path ? ` -- ${path}` : ''); | ||
const { stdout } = await execaCommand(command, { | ||
cwd: process.cwd(), | ||
shell: true, | ||
}); | ||
|
||
return stdout.split('\n').filter(Boolean).length; | ||
try { | ||
const command = `git grep -l composeStor` + (path ? ` -- ${path}` : ''); | ||
return await execCommandCountLines(command); | ||
} catch (err: any) { | ||
// exit code 1 if no matches are found | ||
return err.exitCode === 1 ? 0 : undefined; | ||
} | ||
}; | ||
|
||
const CACHE_KEY = 'portableStories'; | ||
export const getPortableStoriesFileCount = async (path?: string) => { | ||
let cached = await cache.get(CACHE_KEY); | ||
if (!cached) { | ||
try { | ||
const count = await getPortableStoriesFileCountUncached(); | ||
cached = { count }; | ||
await cache.set(CACHE_KEY, cached); | ||
} catch (err: any) { | ||
// exit code 1 if no matches are found | ||
const count = err.exitCode === 1 ? 0 : null; | ||
cached = { count }; | ||
} | ||
} | ||
return cached.count; | ||
return runTelemetryOperation('portableStories', async () => | ||
getPortableStoriesFileCountUncached(path) | ||
); | ||
}; |
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,25 @@ | ||
import { createFileSystemCache, resolvePathInStorybookCache } from '../common'; | ||
|
||
const cache = createFileSystemCache({ | ||
basePath: resolvePathInStorybookCache('telemetry'), | ||
ns: 'storybook', | ||
ttl: 24 * 60 * 60 * 1000, // 24h | ||
}); | ||
|
||
/** | ||
* Run an (expensive) operation, caching the result in a FS cache for 24 hours. | ||
* | ||
* NOTE: if the operation returns `undefined` the value will not be cached. Use this to indicate | ||
* that the operation failed. | ||
*/ | ||
export const runTelemetryOperation = async <T>(cacheKey: string, operation: () => Promise<T>) => { | ||
let cached = await cache.get<T>(cacheKey); | ||
if (cached === undefined) { | ||
cached = await operation(); | ||
// Undefined indicates an error, setting isn't really valuable. | ||
if (cached !== undefined) { | ||
await cache.set(cacheKey, cached); | ||
} | ||
} | ||
return cached; | ||
}; |
Oops, something went wrong.