Remove the remaining no-op tracing and telemetry-only helpers
The build no longer ships telemetry egress, so the next cleanup pass deletes the remaining tracing compatibility layer and the helper modules whose only job was to shape telemetry payloads. This removes the dead session/beta/perfetto tracing files, drops telemetry-only file-operation and plugin-fetch helpers, and rewires the affected callers to keep only their real product behavior. Constraint: Preserve existing user-visible behavior and feature-gated product logic while removing inert tracing/reporting scaffolding Constraint: Leave GrowthBook in place for now because it functions as the repo's local feature-flag adapter, not a live reporting path Rejected: Delete growthbook.ts in the same pass | Its call surface is wide and now tied to local product behavior rather than telemetry export Rejected: Leave no-op tracing and helper modules in place | They continued to create audit noise and implied behavior that no longer existed Confidence: high Scope-risk: moderate Reversibility: clean Directive: Remaining analytics-named code should be treated as either local compatibility calls or feature-gate infrastructure unless a concrete egress path is reintroduced Tested: bun test src/services/analytics/index.test.ts src/components/FeedbackSurvey/submitTranscriptShare.test.ts Tested: bun run ./scripts/build.ts Not-tested: bun x tsc --noEmit (repository has pre-existing unrelated type errors)
This commit is contained in:
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Telemetry for plugin/marketplace fetches that hit the network.
|
||||
*
|
||||
* Added for inc-5046 (GitHub complained about claude-plugins-official load).
|
||||
* Before this, fetch operations only had logForDebugging — no way to measure
|
||||
* actual network volume. This surfaces what's hitting GitHub vs GCS vs
|
||||
* user-hosted so we can see the GCS migration take effect and catch future
|
||||
* hot-path regressions before GitHub emails us again.
|
||||
*
|
||||
* Volume: these fire at startup (install-counts 24h-TTL)
|
||||
* and on explicit user action (install/update). NOT per-interaction. Similar
|
||||
* envelope to tengu_binary_download_*.
|
||||
*/
|
||||
|
||||
import {
|
||||
logEvent,
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { OFFICIAL_MARKETPLACE_NAME } from './officialMarketplace.js'
|
||||
|
||||
export type PluginFetchSource =
|
||||
| 'install_counts'
|
||||
| 'marketplace_clone'
|
||||
| 'marketplace_pull'
|
||||
| 'marketplace_url'
|
||||
| 'plugin_clone'
|
||||
| 'mcpb'
|
||||
|
||||
export type PluginFetchOutcome = 'success' | 'failure' | 'cache_hit'
|
||||
|
||||
// Allowlist of public hosts we report by name. Anything else (enterprise
|
||||
// git, self-hosted, internal) is bucketed as 'other' — we don't want
|
||||
// internal hostnames (git.mycorp.internal) landing in telemetry. Bounded
|
||||
// cardinality also keeps the dashboard host-breakdown tractable.
|
||||
const KNOWN_PUBLIC_HOSTS = new Set([
|
||||
'github.com',
|
||||
'raw.githubusercontent.com',
|
||||
'objects.githubusercontent.com',
|
||||
'gist.githubusercontent.com',
|
||||
'gitlab.com',
|
||||
'bitbucket.org',
|
||||
'codeberg.org',
|
||||
'dev.azure.com',
|
||||
'ssh.dev.azure.com',
|
||||
'storage.googleapis.com', // GCS — where Dickson's migration points
|
||||
])
|
||||
|
||||
/**
|
||||
* Extract hostname from a URL or git spec and bucket to the allowlist.
|
||||
* Handles `https://host/...`, `git@host:path`, `ssh://host/...`.
|
||||
* Returns a known public host, 'other' (parseable but not allowlisted —
|
||||
* don't leak private hostnames), or 'unknown' (unparseable / local path).
|
||||
*/
|
||||
function extractHost(urlOrSpec: string): string {
|
||||
let host: string
|
||||
const scpMatch = /^[^@/]+@([^:/]+):/.exec(urlOrSpec)
|
||||
if (scpMatch) {
|
||||
host = scpMatch[1]!
|
||||
} else {
|
||||
try {
|
||||
host = new URL(urlOrSpec).hostname
|
||||
} catch {
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
const normalized = host.toLowerCase()
|
||||
return KNOWN_PUBLIC_HOSTS.has(normalized) ? normalized : 'other'
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the URL/spec points at anthropics/claude-plugins-official — the
|
||||
* repo GitHub complained about. Lets the dashboard separate "our problem"
|
||||
* traffic from user-configured marketplaces.
|
||||
*/
|
||||
function isOfficialRepo(urlOrSpec: string): boolean {
|
||||
return urlOrSpec.includes(`anthropics/${OFFICIAL_MARKETPLACE_NAME}`)
|
||||
}
|
||||
|
||||
export function logPluginFetch(
|
||||
source: PluginFetchSource,
|
||||
urlOrSpec: string | undefined,
|
||||
outcome: PluginFetchOutcome,
|
||||
durationMs: number,
|
||||
errorKind?: string,
|
||||
): void {
|
||||
// String values are bounded enums / hostname-only — no code, no paths,
|
||||
// no raw error messages. Same privacy envelope as tengu_web_fetch_host.
|
||||
logEvent('tengu_plugin_remote_fetch', {
|
||||
source: source as SafeString,
|
||||
host: (urlOrSpec ? extractHost(urlOrSpec) : 'unknown') as SafeString,
|
||||
is_official: urlOrSpec ? isOfficialRepo(urlOrSpec) : false,
|
||||
outcome: outcome as SafeString,
|
||||
duration_ms: Math.round(durationMs),
|
||||
...(errorKind && { error_kind: errorKind as SafeString }),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify an error into a stable bucket for the error_kind field. Keeps
|
||||
* cardinality bounded — raw error messages would explode dashboard grouping.
|
||||
*
|
||||
* Handles both axios Error objects (Node.js error codes like ENOTFOUND) and
|
||||
* git stderr strings (human phrases like "Could not resolve host"). DNS
|
||||
* checked BEFORE timeout because gitClone's error enhancement at
|
||||
* marketplaceManager.ts:~950 rewrites DNS failures to include the word
|
||||
* "timeout" — ordering the other way would misclassify git DNS as timeout.
|
||||
*/
|
||||
export function classifyFetchError(error: unknown): string {
|
||||
const msg = String((error as { message?: unknown })?.message ?? error)
|
||||
if (
|
||||
/ENOTFOUND|ECONNREFUSED|EAI_AGAIN|Could not resolve host|Connection refused/i.test(
|
||||
msg,
|
||||
)
|
||||
) {
|
||||
return 'dns_or_refused'
|
||||
}
|
||||
if (/ETIMEDOUT|timed out|timeout/i.test(msg)) return 'timeout'
|
||||
if (
|
||||
/ECONNRESET|socket hang up|Connection reset by peer|remote end hung up/i.test(
|
||||
msg,
|
||||
)
|
||||
) {
|
||||
return 'conn_reset'
|
||||
}
|
||||
if (/403|401|authentication|permission denied/i.test(msg)) return 'auth'
|
||||
if (/404|not found|repository not found/i.test(msg)) return 'not_found'
|
||||
if (/certificate|SSL|TLS|unable to get local issuer/i.test(msg)) return 'tls'
|
||||
// Schema validation throws "Invalid response format" (install_counts) —
|
||||
// distinguish from true unknowns so the dashboard can
|
||||
// see "server sent garbage" separately.
|
||||
if (/Invalid response format|Invalid marketplace schema/i.test(msg)) {
|
||||
return 'invalid_schema'
|
||||
}
|
||||
return 'other'
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import { errorMessage, getErrnoCode } from '../errors.js'
|
||||
import { getFsImplementation } from '../fsOperations.js'
|
||||
import { logError } from '../log.js'
|
||||
import { jsonParse, jsonStringify } from '../slowOperations.js'
|
||||
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
|
||||
import { getPluginsDirectory } from './pluginDirectories.js'
|
||||
|
||||
const INSTALL_COUNTS_CACHE_VERSION = 1
|
||||
@@ -196,21 +195,8 @@ async function fetchInstallCountsFromGitHub(): Promise<
|
||||
throw new Error('Invalid response format from install counts API')
|
||||
}
|
||||
|
||||
logPluginFetch(
|
||||
'install_counts',
|
||||
INSTALL_COUNTS_URL,
|
||||
'success',
|
||||
performance.now() - started,
|
||||
)
|
||||
return response.data.plugins
|
||||
} catch (error) {
|
||||
logPluginFetch(
|
||||
'install_counts',
|
||||
INSTALL_COUNTS_URL,
|
||||
'failure',
|
||||
performance.now() - started,
|
||||
classifyFetchError(error),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -227,7 +213,6 @@ export async function getInstallCounts(): Promise<Map<string, number> | null> {
|
||||
const cache = await loadInstallCountsCache()
|
||||
if (cache) {
|
||||
logForDebugging('Using cached install counts')
|
||||
logPluginFetch('install_counts', INSTALL_COUNTS_URL, 'cache_hit', 0)
|
||||
const map = new Map<string, number>()
|
||||
for (const entry of cache.counts) {
|
||||
map.set(entry.plugin, entry.unique_installs)
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
getAddDirExtraMarketplaces,
|
||||
} from './addDirPluginSettings.js'
|
||||
import { markPluginVersionOrphaned } from './cacheUtils.js'
|
||||
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
|
||||
import { removeAllPluginsForMarketplace } from './installedPluginsManager.js'
|
||||
import {
|
||||
extractHostFromSource,
|
||||
@@ -1110,13 +1109,7 @@ async function cacheMarketplaceFromGit(
|
||||
disableCredentialHelper: options?.disableCredentialHelper,
|
||||
sparsePaths,
|
||||
})
|
||||
logPluginFetch(
|
||||
'marketplace_pull',
|
||||
gitUrl,
|
||||
pullResult.code === 0 ? 'success' : 'failure',
|
||||
performance.now() - pullStarted,
|
||||
pullResult.code === 0 ? undefined : classifyFetchError(pullResult.stderr),
|
||||
)
|
||||
void pullStarted
|
||||
if (pullResult.code === 0) return
|
||||
logForDebugging(`git pull failed, will re-clone: ${pullResult.stderr}`, {
|
||||
level: 'warn',
|
||||
@@ -1156,13 +1149,7 @@ async function cacheMarketplaceFromGit(
|
||||
)
|
||||
const cloneStarted = performance.now()
|
||||
const result = await gitClone(gitUrl, cachePath, ref, sparsePaths)
|
||||
logPluginFetch(
|
||||
'marketplace_clone',
|
||||
gitUrl,
|
||||
result.code === 0 ? 'success' : 'failure',
|
||||
performance.now() - cloneStarted,
|
||||
result.code === 0 ? undefined : classifyFetchError(result.stderr),
|
||||
)
|
||||
void cloneStarted
|
||||
if (result.code !== 0) {
|
||||
// Clean up any partial directory created by the failed clone so the next
|
||||
// attempt starts fresh. Best-effort: if this fails, the stale dir will be
|
||||
@@ -1284,13 +1271,6 @@ async function cacheMarketplaceFromUrl(
|
||||
headers,
|
||||
})
|
||||
} catch (error) {
|
||||
logPluginFetch(
|
||||
'marketplace_url',
|
||||
url,
|
||||
'failure',
|
||||
performance.now() - fetchStarted,
|
||||
classifyFetchError(error),
|
||||
)
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
||||
throw new Error(
|
||||
@@ -1317,25 +1297,13 @@ async function cacheMarketplaceFromUrl(
|
||||
// Validate the response is a valid marketplace
|
||||
const result = PluginMarketplaceSchema().safeParse(response.data)
|
||||
if (!result.success) {
|
||||
logPluginFetch(
|
||||
'marketplace_url',
|
||||
url,
|
||||
'failure',
|
||||
performance.now() - fetchStarted,
|
||||
'invalid_schema',
|
||||
)
|
||||
throw new ConfigParseError(
|
||||
`Invalid marketplace schema from URL: ${result.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
|
||||
redactedUrl,
|
||||
response.data,
|
||||
)
|
||||
}
|
||||
logPluginFetch(
|
||||
'marketplace_url',
|
||||
url,
|
||||
'success',
|
||||
performance.now() - fetchStarted,
|
||||
)
|
||||
void fetchStarted
|
||||
|
||||
safeCallProgress(onProgress, 'Saving marketplace to cache')
|
||||
// Ensure cache directory exists
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from '../settings/settings.js'
|
||||
import { jsonParse, jsonStringify } from '../slowOperations.js'
|
||||
import { getSystemDirectories } from '../systemDirectories.js'
|
||||
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
|
||||
/**
|
||||
* User configuration values for MCPB
|
||||
*/
|
||||
@@ -490,7 +489,6 @@ async function downloadMcpb(
|
||||
}
|
||||
|
||||
const started = performance.now()
|
||||
let fetchTelemetryFired = false
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
timeout: 120000, // 2 minute timeout
|
||||
@@ -507,11 +505,6 @@ async function downloadMcpb(
|
||||
})
|
||||
|
||||
const data = new Uint8Array(response.data)
|
||||
// Fire telemetry before writeFile — the event measures the network
|
||||
// fetch, not disk I/O. A writeFile EACCES would otherwise match
|
||||
// classifyFetchError's /permission denied/ → misreport as auth.
|
||||
logPluginFetch('mcpb', url, 'success', performance.now() - started)
|
||||
fetchTelemetryFired = true
|
||||
|
||||
// Save to disk (binary data)
|
||||
await writeFile(destPath, Buffer.from(data))
|
||||
@@ -523,15 +516,7 @@ async function downloadMcpb(
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
if (!fetchTelemetryFired) {
|
||||
logPluginFetch(
|
||||
'mcpb',
|
||||
url,
|
||||
'failure',
|
||||
performance.now() - started,
|
||||
classifyFetchError(error),
|
||||
)
|
||||
}
|
||||
void started
|
||||
const errorMsg = errorMessage(error)
|
||||
const fullError = new Error(
|
||||
`Failed to download MCPB file from ${url}: ${errorMsg}`,
|
||||
|
||||
@@ -85,7 +85,6 @@ import { SettingsSchema } from '../settings/types.js'
|
||||
import { jsonParse, jsonStringify } from '../slowOperations.js'
|
||||
import { getAddDirEnabledPlugins } from './addDirPluginSettings.js'
|
||||
import { verifyAndDemote } from './dependencyResolver.js'
|
||||
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
|
||||
import { checkGitAvailable } from './gitAvailability.js'
|
||||
import { getInMemoryInstalledPlugins } from './installedPluginsManager.js'
|
||||
import { getManagedPluginNames } from './managedPlugins.js'
|
||||
@@ -563,13 +562,6 @@ export async function gitClone(
|
||||
const cloneResult = await execFileNoThrow(gitExe(), args)
|
||||
|
||||
if (cloneResult.code !== 0) {
|
||||
logPluginFetch(
|
||||
'plugin_clone',
|
||||
gitUrl,
|
||||
'failure',
|
||||
performance.now() - cloneStarted,
|
||||
classifyFetchError(cloneResult.stderr),
|
||||
)
|
||||
throw new Error(`Failed to clone repository: ${cloneResult.stderr}`)
|
||||
}
|
||||
|
||||
@@ -595,13 +587,6 @@ export async function gitClone(
|
||||
)
|
||||
|
||||
if (unshallowResult.code !== 0) {
|
||||
logPluginFetch(
|
||||
'plugin_clone',
|
||||
gitUrl,
|
||||
'failure',
|
||||
performance.now() - cloneStarted,
|
||||
classifyFetchError(unshallowResult.stderr),
|
||||
)
|
||||
throw new Error(
|
||||
`Failed to fetch commit ${sha}: ${unshallowResult.stderr}`,
|
||||
)
|
||||
@@ -616,27 +601,12 @@ export async function gitClone(
|
||||
)
|
||||
|
||||
if (checkoutResult.code !== 0) {
|
||||
logPluginFetch(
|
||||
'plugin_clone',
|
||||
gitUrl,
|
||||
'failure',
|
||||
performance.now() - cloneStarted,
|
||||
classifyFetchError(checkoutResult.stderr),
|
||||
)
|
||||
throw new Error(
|
||||
`Failed to checkout commit ${sha}: ${checkoutResult.stderr}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Fire success only after ALL network ops (clone + optional SHA fetch)
|
||||
// complete — same telemetry-scope discipline as mcpb and marketplace_url.
|
||||
logPluginFetch(
|
||||
'plugin_clone',
|
||||
gitUrl,
|
||||
'success',
|
||||
performance.now() - cloneStarted,
|
||||
)
|
||||
void cloneStarted
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user