Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 26 additions & 18 deletions src/appDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import type { Parser } from "./core/parser"
import { findProjectRoot, uriPath } from "./core/pathUtils"
import { buildRouterGraph } from "./core/routerResolver"
import { routerNodeToAppDefinition } from "./core/transformer"
import { collectRoutes, countRouters } from "./core/treeUtils"
import { collectRoutes } from "./core/treeUtils"
import type { AppDefinition } from "./core/types"
import { log } from "./utils/logger"
import { createTimer, trackEntrypointDetected } from "./utils/telemetry"
import { vscodeFileSystem } from "./vscode/vscodeFileSystem"

export type { EntryPoint }

export interface DiscoveryStats {
detection_method_config: number
detection_method_pyproject: number
detection_method_heuristic: number
folders_with_apps: number
}

/**
* Parses an entrypoint string in module:variable notation.
* Supports formats like "my_app.main:app" or "main".
Expand Down Expand Up @@ -137,12 +143,18 @@ async function parsePyprojectForEntryPoint(
*/
export async function discoverFastAPIApps(
parser: Parser,
trackTelemetry = false,
): Promise<AppDefinition[]> {
): Promise<{ apps: AppDefinition[]; stats: DiscoveryStats }> {
const stats: DiscoveryStats = {
detection_method_config: 0,
detection_method_pyproject: 0,
detection_method_heuristic: 0,
folders_with_apps: 0,
}

const workspaceFolders = vscode.workspace.workspaceFolders
if (!workspaceFolders) {
log("No workspace folders found")
return []
return { apps: [], stats }
}

log(
Expand All @@ -152,7 +164,6 @@ export async function discoverFastAPIApps(
const apps: AppDefinition[] = []

for (const folder of workspaceFolders) {
const folderTimer = createTimer()
let detectionMethod: "config" | "pyproject" | "heuristic" = "heuristic"
const folderApps: AppDefinition[] = []
const config = vscode.workspace.getConfiguration("fastapi", folder.uri)
Expand Down Expand Up @@ -222,29 +233,26 @@ export async function discoverFastAPIApps(
}
}

const folderRoutes = collectRoutes(folderApps)

if (folderApps.length > 0) {
const folderRoutes = collectRoutes(folderApps)
log(
`Found ${folderApps.length} FastAPI app(s) with ${folderRoutes.length} route(s) in ${folder.name}`,
)
stats.folders_with_apps++
}

// Track entrypoint detection per workspace folder (initial discovery only)
if (trackTelemetry) {
trackEntrypointDetected({
duration_ms: folderTimer(),
method: detectionMethod,
success: folderApps.length > 0,
routes_count: folderRoutes.length,
routers_count: countRouters(folderApps),
})
if (detectionMethod === "config") {
stats.detection_method_config++
} else if (detectionMethod === "pyproject") {
stats.detection_method_pyproject++
} else {
stats.detection_method_heuristic++
}
}

if (apps.length === 0) {
log("No FastAPI apps found in workspace")
}

return apps
return { apps, stats }
}
3 changes: 2 additions & 1 deletion src/cloud/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ export class CloudAuthenticationProvider
)
}

startWatching() {
async startWatching() {
// Poll for auth changes since we can't use fs.watch in browser
// and VS Code's file watcher doesn't work for files outside workspace
this.lastAuthState = await this.hasValidToken()
this.pollingInterval = setInterval(
() => this.checkAndFireAuthState(),
AUTH_POLL_INTERVAL_MS,
Expand Down
21 changes: 15 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import * as vscode from "vscode"
import { discoverFastAPIApps } from "./appDiscovery"
import { type DiscoveryStats, discoverFastAPIApps } from "./appDiscovery"
import { ApiService } from "./cloud/api"
import { AUTH_PROVIDER_ID, CloudAuthenticationProvider } from "./cloud/auth"
import { LOGS_VIEW_ID, LogsViewProvider } from "./cloud/commands/logs"
Expand Down Expand Up @@ -70,7 +70,13 @@ export async function activate(context: vscode.ExtensionContext) {
// Initialize telemetry
await initVSCodeTelemetry(context)

let apps: Awaited<ReturnType<typeof discoverFastAPIApps>> = []
let apps: AppDefinition[] = []
let stats: DiscoveryStats = {
detection_method_config: 0,
detection_method_pyproject: 0,
detection_method_heuristic: 0,
folders_with_apps: 0,
}
let success = true

try {
Expand Down Expand Up @@ -108,7 +114,9 @@ export async function activate(context: vscode.ExtensionContext) {

try {
// Discover apps and create providers
apps = await discoverFastAPIApps(parserService, true)
const result = await discoverFastAPIApps(parserService)
apps = result.apps
stats = result.stats
} catch (error) {
success = false
trackActivationFailed(error, "discovery")
Expand All @@ -128,6 +136,7 @@ export async function activate(context: vscode.ExtensionContext) {
routers_count: countRouters(apps),
apps_count: apps.length,
workspace_folder_count: vscode.workspace.workspaceFolders?.length ?? 0,
...stats,
})

// Create grouping function that groups by workspace folder if there are multiple folders
Expand Down Expand Up @@ -173,7 +182,7 @@ export async function activate(context: vscode.ExtensionContext) {
if (refreshTimeout) clearTimeout(refreshTimeout)
refreshTimeout = setTimeout(async () => {
if (!parserService) return
const newApps = await discoverFastAPIApps(parserService)
const { apps: newApps } = await discoverFastAPIApps(parserService)

if (uri) {
await testIndex.invalidateFile(uri.toString())
Expand Down Expand Up @@ -236,7 +245,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Auth provider must be registered regardless of workspace,
// so sign-in works from command palette and Accounts menu in vscode.dev
const authProvider = new CloudAuthenticationProvider(context, apiService)
authProvider.startWatching()
await authProvider.startWatching()

context.subscriptions.push(
{ dispose: () => authProvider.dispose() },
Expand Down Expand Up @@ -425,7 +434,7 @@ function registerCommands(
async () => {
if (!parserService) return
clearImportCache()
const newApps = await discoverFastAPIApps(parserService)
const { apps: newApps } = await discoverFastAPIApps(parserService)
pathOperationProvider.setApps(newApps, groupApps(newApps))
testToRouteProvider.setApps(newApps)
},
Expand Down
2 changes: 1 addition & 1 deletion src/test/cloud/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ suite("cloud/auth", () => {

fsStub.fake.readFile.rejects(new Error("File not found"))

provider.startWatching()
await provider.startWatching()

await clock.tickAsync(3100)
const callCount = fsStub.fake.readFile.callCount
Expand Down
26 changes: 1 addition & 25 deletions src/utils/telemetry/events.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { client } from "./client"
import type {
ActivationEventProps,
EntrypointDetectedEventProps,
} from "./types"
import type { ActivationEventProps } from "./types"

export function createTimer(): () => number {
const start = performance.now()
Expand All @@ -13,8 +10,6 @@ export const Events = {
ACTIVATED: "extension_activated",
ACTIVATION_FAILED: "extension_activation_failed",
DEACTIVATED: "extension_deactivated",
ENTRYPOINT_DETECTED: "extension_entrypoint_detected",
CODELENS_PROVIDED: "extension_codelens_provided",
CODELENS_CLICKED: "extension_codelens_clicked",
TREE_VIEW_VISIBLE: "extension_tree_view_visible",
SEARCH_EXECUTED: "extension_search_executed",
Expand Down Expand Up @@ -125,12 +120,6 @@ export function trackActivationFailed(
})
}

export function trackEntrypointDetected(
props: EntrypointDetectedEventProps,
): void {
client.capture(Events.ENTRYPOINT_DETECTED, { ...props })
}

export function trackTreeViewVisible(): void {
client.capture(Events.TREE_VIEW_VISIBLE)
}
Expand All @@ -145,19 +134,6 @@ export function trackSearchExecuted(
})
}

export function trackCodeLensProvided(
testCallsCount: number,
matchedCount: number,
type: "test" | "route" = "test",
): void {
client.capture(Events.CODELENS_PROVIDED, {
type,
test_calls_count: testCallsCount,
matched_count: matchedCount,
match_rate: testCallsCount > 0 ? matchedCount / testCallsCount : 0,
})
}

export function trackDeactivation(): void {
const duration = client.getSessionDuration()
if (duration !== null) {
Expand Down
3 changes: 0 additions & 3 deletions src/utils/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@ export {
trackCloudProjectUnlinked,
trackCloudSignIn,
trackCloudSignOut,
trackCodeLensProvided,
trackDeactivation,
trackEntrypointDetected,
trackSearchExecuted,
trackTreeViewVisible,
} from "./events"
export type {
ActivationEventProps,
ClientInfo,
EntrypointDetectedEventProps,
TelemetryConfig,
} from "./types"
export {
Expand Down
12 changes: 4 additions & 8 deletions src/utils/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,8 @@ export interface ActivationEventProps {
routers_count: number
apps_count: number
workspace_folder_count: number
}

export interface EntrypointDetectedEventProps {
duration_ms: number
method: "config" | "pyproject" | "heuristic"
success: boolean
routes_count: number
routers_count: number
detection_method_config: number
detection_method_pyproject: number
detection_method_heuristic: number
folders_with_apps: number
}
8 changes: 0 additions & 8 deletions src/vscode/routeToTestCodeLensProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ import {

import { type AppDefinition, collectRoutes } from "../core"
import type { RouteDefinition } from "../core/types"
import { trackCodeLensProvided } from "../utils/telemetry"
import type { TestCallIndex } from "./testIndex"

export class RouteToTestCodeLensProvider implements CodeLensProvider {
private cachedRoutes: RouteDefinition[] = []
private testIndex: TestCallIndex
private indexListener: Disposable
private trackedFiles = new Set<string>()

private _onDidChangeCodeLenses = new EventEmitter<void>()
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event
Expand All @@ -34,7 +32,6 @@ export class RouteToTestCodeLensProvider implements CodeLensProvider {

setApps(apps: AppDefinition[]): void {
this.cachedRoutes = collectRoutes(apps)
this.trackedFiles.clear()
this._onDidChangeCodeLenses.fire()
}

Expand Down Expand Up @@ -77,11 +74,6 @@ export class RouteToTestCodeLensProvider implements CodeLensProvider {
)
}

if (routes.length > 0 && !this.trackedFiles.has(currentFile)) {
this.trackedFiles.add(currentFile)
trackCodeLensProvided(routes.length, codeLenses.length, "route")
}

return codeLenses
}

Expand Down
11 changes: 0 additions & 11 deletions src/vscode/testToRouteCodeLensProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
} from "../core/pathUtils"
import { collectRoutes } from "../core/treeUtils"
import type { AppDefinition, SourceLocation } from "../core/types"
import { trackCodeLensProvided } from "../utils/telemetry"

export class TestToRouteCodeLensProvider implements CodeLensProvider {
private apps: AppDefinition[] = []
Expand All @@ -30,16 +29,13 @@ export class TestToRouteCodeLensProvider implements CodeLensProvider {
private _onDidChangeCodeLenses = new EventEmitter<void>()
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event

private trackedFiles = new Set<string>()

constructor(parser: Parser, apps: AppDefinition[]) {
this.parser = parser
this.apps = apps
}

setApps(apps: AppDefinition[]): void {
this.apps = apps
this.trackedFiles.clear()
this._onDidChangeCodeLenses.fire()
}

Expand Down Expand Up @@ -85,13 +81,6 @@ export class TestToRouteCodeLensProvider implements CodeLensProvider {
}
}

// Track once per file per session (first open only, edits won't update the count)
const fileKey = document.uri.toString()
if (testClientCalls.length > 0 && !this.trackedFiles.has(fileKey)) {
this.trackedFiles.add(fileKey)
trackCodeLensProvided(testClientCalls.length, codeLenses.length)
}

return codeLenses
}

Expand Down
Loading