Skip to content

Commit

Permalink
improve telemtry
Browse files Browse the repository at this point in the history
  • Loading branch information
florianbgt committed Dec 12, 2024
1 parent 4e01be7 commit 0484bfa
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 31 deletions.
84 changes: 84 additions & 0 deletions packages/cli/src/config/globalConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import os from "os";
import path from "path";
import fs from "fs";
import { v4 } from "uuid";
import { z } from "zod";

export const globalConfigSchema = z.object({
userId: z.string(),
});

function getConfigPath() {
const appName = "napi";
const homeDir = os.homedir();

if (os.platform() === "win32") {
// Windows: Use %APPDATA%
const appData =
process.env.APPDATA || path.join(homeDir, "AppData", "Roaming");
return path.join(appData, appName, "config.json");
} else if (os.platform() === "darwin") {
// macOS: Use ~/Library/Application Support
return path.join(
homeDir,
"Library",
"Application Support",
appName,
"config.json",
);
} else {
// Linux and others: Use ~/.config
const configDir =
process.env.XDG_CONFIG_HOME || path.join(homeDir, ".config");
return path.join(configDir, appName, "config.json");
}
}

async function createNewConfig(configPath: string) {
const config = {
userId: v4(),
};

try {
const configDir = path.dirname(configPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
} catch (error) {
console.error(`Failed to write config: ${error}`);
}

return config;
}

export async function getOrCreateGlobalConfig() {
const configPath = getConfigPath();

let config: z.infer<typeof globalConfigSchema>;

try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "utf-8");

try {
config = globalConfigSchema.parse(JSON.parse(content));
} catch (error) {
console.debug(`Failed to parse config: ${error}`);
config = await createNewConfig(configPath);
}
} else {
config = await createNewConfig(configPath);
}
} catch (error) {
console.error(`Failed to read or create config: ${error}`);
config = await createNewConfig(configPath);
}

return config;
}

export function updateConfig(newConfig: z.infer<typeof globalConfigSchema>) {
const configPath = getConfigPath();
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
}
33 changes: 33 additions & 0 deletions packages/cli/src/helper/checkNpmVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import localPackageJson from "../../package.json";

export async function checkVersionMiddleware() {
const currentVersion = localPackageJson.version;
let latestVersion: string;

try {
const response = await fetch(
`https://registry.npmjs.org/${localPackageJson.name}/latest`,
);
if (!response.ok) {
console.warn(
"Failed to fetch latest version from npm, ignoring version check",
);
}

const latestPackageJson: { version: string } = await response.json();
latestVersion = latestPackageJson.version;
} catch (err) {
console.warn(
`Failed to fetch latest version from npm, ignoring version check. Error: ${err}`,
);
return;
}

if (currentVersion !== latestVersion) {
console.warn(
`You are using version ${currentVersion} of ${localPackageJson.name}. ` +
`The latest version is ${latestVersion}. Please update to the latest version.`,
);
process.exit(1);
}
}
4 changes: 4 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import splitCommandHandler from "./commands/split";
import { getConfigFromWorkDir, getOpenaiApiKeyFromConfig } from "./config";
import { findAvailablePort, openInBrowser } from "./helper/server";
import { TelemetryEvents, trackEvent } from "./telemetry";
import { checkVersionMiddleware } from "./helper/checkNpmVersion";

// remove all warning.
// We need this because of some depreciation warning we have with 3rd party libraries
Expand All @@ -23,6 +24,9 @@ trackEvent(TelemetryEvents.APP_START, {
});

yargs(hideBin(process.argv))
.middleware(() => {
checkVersionMiddleware();
})
.options({
workdir: {
type: "string",
Expand Down
61 changes: 30 additions & 31 deletions packages/cli/src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import axios from "axios";
import { EventEmitter } from "events";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { v4 as uuidv4 } from "uuid";
import os from "os";
import { getOrCreateGlobalConfig } from "./config/globalConfig";
import packageJson from "../package.json";

export enum TelemetryEvents {
APP_START = "app_start",
Expand All @@ -18,7 +16,9 @@ export enum TelemetryEvents {
}

export interface TelemetryEvent {
sessionId: string;
userId: string;
os: string;
version: string;
eventId: TelemetryEvents;
data: Record<string, unknown>;
timestamp: string;
Expand All @@ -28,52 +28,51 @@ const telemetry = new EventEmitter();
const TELEMETRY_ENDPOINT =
process.env.TELEMETRY_ENDPOINT ||
"https://napi-watchdog-api-gateway-33ge7a49.nw.gateway.dev/telemetryHandler";
const SESSION_FILE_PATH = join(os.tmpdir(), "napi_session_id");

// getSessionId generates a new session ID and cache it in SESSION_FILE_PATH
function getSessionId() {
if (existsSync(SESSION_FILE_PATH)) {
const fileContent = readFileSync(SESSION_FILE_PATH, "utf-8");
const [storedDate, sessionId] = fileContent.split(":");
const today = new Date().toISOString().slice(0, 10);

if (storedDate === today) {
return sessionId;
}
}

const newSessionId = uuidv4();
const today = new Date().toISOString().slice(0, 10);
writeFileSync(SESSION_FILE_PATH, `${today}:${newSessionId}`);
return newSessionId;
}

const SESSION_ID = getSessionId();

telemetry.on("event", (data) => {
sendTelemetryData(data);
});

async function sendTelemetryData(data: TelemetryEvent) {
const controller = new AbortController();
const timeoutSeconds = 15;
const timeoutId = setTimeout(() => controller.abort(), timeoutSeconds * 1000);

try {
await axios.post(TELEMETRY_ENDPOINT, data, {
const response = await fetch(TELEMETRY_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "napi",
},
timeout: 100000,
body: JSON.stringify(data),
signal: controller.signal,
});

clearTimeout(timeoutId);

if (!response.ok) {
console.debug(`Failed to send telemetry data: ${response.statusText}`);
}
} catch (error) {
console.debug(`Failed to send telemetry data: ${error}`);
if (error instanceof Error && error.name === "AbortError") {
console.debug("Request timed out");
} else {
console.debug(`Failed to send telemetry data: ${error}`);
}
}
}

export function trackEvent(
export async function trackEvent(
eventId: TelemetryEvents,
eventData: Record<string, unknown>,
) {
const config = await getOrCreateGlobalConfig();

const telemetryPayload: TelemetryEvent = {
sessionId: SESSION_ID,
userId: config.userId,
os: os.platform(),
version: packageJson.version,
eventId,
data: eventData,
timestamp: new Date().toISOString(),
Expand Down
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"noFallthroughCasesInSwitch": true
}
}

0 comments on commit 0484bfa

Please sign in to comment.