wgc
Version:
The official CLI tool to manage the GraphQL Federation Platform Cosmo
192 lines • 6.55 kB
JavaScript
import crypto from 'node:crypto';
import os from 'node:os';
import { PostHog } from 'posthog-node';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { isAuthenticated } from '../commands/auth/utils.js';
import { config, getBaseHeaders } from './config.js';
import { CreateClient } from './client/client.js';
// Environment variables to allow opting out of telemetry
// Support for COSMO_TELEMETRY_DISABLED and Console Do Not Track standard
const TELEMETRY_DISABLED = process.env.COSMO_TELEMETRY_DISABLED === 'true' || process.env.DO_NOT_TRACK === '1';
const UNAUTHENTICATED_CONTEXT_METADATA = Object.freeze({ organizationId: 'anonymous' });
let client = null;
let apiClient = null;
const buildPostHogOkResponse = () => ({
status: 200,
text: () => Promise.resolve(''),
json: () => Promise.resolve({}),
});
// PostHog logs flush failures directly; treat network issues as no-ops for CLI UX.
// This will also make the retry mechanism ineffective.
async function safePostHogFetch(url, options) {
try {
const response = await fetch(url, options);
if (response.status < 200 || response.status >= 400) {
if (process.env.DEBUG) {
console.error(`PostHog request failed with status ${response.status}.`);
}
return buildPostHogOkResponse();
}
return response;
}
catch (err) {
if (process.env.DEBUG) {
console.error('PostHog request failed.', err);
}
return buildPostHogOkResponse();
}
}
// Detect if running in a CI environment
const isCI = () => {
return Boolean(process.env.CI || // Travis CI, CircleCI, GitLab CI, GitHub Actions, etc.
process.env.CONTINUOUS_INTEGRATION ||
process.env.BUILD_NUMBER || // Jenkins
process.env.TEAMCITY_VERSION || // TeamCity
process.env.GITLAB_CI ||
process.env.GITHUB_ACTIONS ||
process.env.BUILDKITE);
};
/**
* Check if the CLI is talking to Cosmo Cloud or a self-hosted instance
*/
const isTalkingToCosmoCloud = () => {
const cloudUrl = 'https://cosmo-cp.wundergraph.com';
return config.baseURL.startsWith(cloudUrl);
};
/**
* Initialize PostHog client
* This should be called once at the start of the CLI
*/
export const initTelemetry = () => {
if (TELEMETRY_DISABLED) {
return;
}
const posthogApiKey = process.env.POSTHOG_API_KEY || 'phc_CEnvoyw3KcTuC5E1seDPrgvAamgGRDLfzPi1e7RU1G1';
const posthogHost = process.env.POSTHOG_HOST || 'https://eu.i.posthog.com';
client = new PostHog(posthogApiKey, {
host: posthogHost,
flushAt: 1, // For CLI, we want to send events immediately
flushInterval: 0, // Don't wait to flush events
disableGeoip: false,
fetch: safePostHogFetch,
});
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
apiClient = CreateClient({
baseUrl: config.baseURL,
apiKey: config.apiKey,
proxyUrl,
});
// Handle errors silently to not interrupt CLI operations
client.on('error', (err) => {
if (process.env.DEBUG) {
console.error('Telemetry error:', err);
}
});
};
/**
* Generate a consistent distinct ID
* Uses the platform API to get the organization slug if available
*/
const getIdentity = async () => {
var _a;
try {
if (!apiClient) {
return UNAUTHENTICATED_CONTEXT_METADATA;
}
if (!(await isAuthenticated())) {
return UNAUTHENTICATED_CONTEXT_METADATA;
}
const resp = await apiClient.platform.whoAmI({}, {
headers: getBaseHeaders(),
});
if (((_a = resp.response) === null || _a === void 0 ? void 0 : _a.code) === EnumStatusCode.OK) {
return {
organizationId: resp.organizationId || 'anonymous',
userEmail: resp.userEmail || undefined,
};
}
}
catch (err) {
// skip catch, returning anonymous identity if any error occurs (e.g. network issues, not logged in, etc.)
if (process.env.DEBUG) {
console.debug('Failed to get identity for telemetry, using anonymous.', err);
}
}
return UNAUTHENTICATED_CONTEXT_METADATA;
};
/**
* Capture a usage event
*/
export const capture = async (eventName, properties = {}) => {
var _a, _b;
if (TELEMETRY_DISABLED || !client) {
return;
}
try {
const identity = await getIdentity();
const metadata = getMetadata();
client.capture({
distinctId: (_a = identity.userEmail) !== null && _a !== void 0 ? _a : identity.organizationId,
groups: {
cosmo_organization: (_b = identity.organizationId) !== null && _b !== void 0 ? _b : '',
},
event: eventName,
properties: {
...metadata,
...properties,
},
});
}
catch (err) {
// Silently fail to not disrupt CLI operations
if (process.env.DEBUG) {
console.error('Failed to capture telemetry event:', err);
}
}
};
/**
* Capture a command failure event with error details
*/
export const captureCommandFailure = async (command, error) => {
const errorMessage = error instanceof Error ? error.message : error;
const errorStack = error instanceof Error ? error.stack : undefined;
await capture('command_failure', {
command,
error_message: errorMessage,
error_stack: errorStack,
});
};
/**
* Get CLI metadata to include with all events
*/
const getMetadata = () => {
var _a;
const machineId = crypto.hash('sha256', os.hostname(), 'hex');
return {
cli_version: config.version,
node_version: process.version,
os_name: process.platform,
os_version: ((_a = process.release) === null || _a === void 0 ? void 0 : _a.name) || '',
platform: process.arch,
machine_id: machineId,
is_ci: isCI(),
is_cosmo_cloud: isTalkingToCosmoCloud(),
};
};
/**
* Shutdown PostHog client - should be called before CLI exits
*/
export const shutdownTelemetry = async () => {
if (client) {
try {
await client.shutdown();
}
catch (err) {
// Silently fail
if (process.env.DEBUG) {
console.error('Failed to shutdown telemetry:', err);
}
}
}
};
//# sourceMappingURL=telemetry.js.map