@mastra/core
Version:
286 lines (285 loc) • 10.1 kB
JavaScript
import { createHash } from "crypto";
import os from "os";
import { PostHog } from "posthog-node";
import { mkdirSync, readFileSync, writeFileSync } from "fs";
import path from "path";
//#region src/telemetry/posthog.ts
const POSTHOG_API_KEY = "phc_SBLpZVAB6jmHOct9CABq3PF0Yn5FU3G2FgT4xUr2XrT";
const POSTHOG_HOST = "https://us.posthog.com";
const TRUTHY_DISABLED_VALUES = /* @__PURE__ */ new Set([
"1",
"true",
"yes"
]);
let client = null;
function isTelemetryEnabled() {
const value = process.env["MASTRA_TELEMETRY_DISABLED"];
if (!value) return true;
return !TRUTHY_DISABLED_VALUES.has(value.trim().toLowerCase());
}
function hashTelemetryValue(value) {
return createHash("sha256").update(value).digest("hex");
}
function getHashedHostname() {
return hashTelemetryValue(os.hostname() || "unknown-host").slice(0, 16);
}
function getEETelemetryFallbackDistinctId() {
return `mastra-${getHashedHostname()}`;
}
function getClient() {
if (!isTelemetryEnabled()) return null;
if (!client) client = new PostHog(POSTHOG_API_KEY, {
host: POSTHOG_HOST,
flushAt: 1,
flushInterval: 0,
disableGeoip: false
});
return client;
}
function getSystemProperties() {
return {
os: process.platform,
os_version: os.release(),
node_version: process.version,
platform: process.arch,
machine_id: getHashedHostname(),
mastra_version: process.env["npm_package_version"] || "unknown"
};
}
function captureTelemetryEvent(event, distinctId, properties) {
try {
const posthog = getClient();
if (!posthog) return;
posthog.capture({
distinctId: distinctId || getEETelemetryFallbackDistinctId(),
event,
properties: {
...getSystemProperties(),
...properties
}
});
} catch {}
}
//#endregion
//#region src/telemetry/context.ts
function getServerTelemetryContext() {
return {
projectId: hashTelemetryValue(process.env.MASTRA_PROJECT_ROOT || process.cwd()).slice(0, 16),
distinctId: process.env.MASTRA_CLI_DISTINCT_ID || void 0,
command: process.env.MASTRA_TELEMETRY_COMMAND || "server",
nodeEnv: process.env.NODE_ENV || "development"
};
}
//#endregion
//#region src/telemetry/feature-telemetry.ts
const FEATURE_USAGE_EVENT = "mastra_feature_usage";
function countCollection(collection) {
if (!collection || typeof collection !== "object") return 0;
if (collection instanceof Map || collection instanceof Set) return collection.size;
return Object.keys(collection).length;
}
function countAgentsWithMemory(agents) {
if (!agents || typeof agents !== "object") return 0;
return Object.values(agents).filter((agent) => {
try {
return agent?.hasOwnMemory?.() === true;
} catch {
return false;
}
}).length;
}
function bucketStorageType(storage) {
if (!storage) return null;
const constructorName = storage.constructor?.name?.toLowerCase();
if (!constructorName) return "unknown";
if (constructorName.includes("memory")) return "in-memory";
if (constructorName.includes("libsql")) return "libsql";
if (constructorName.includes("postgres")) return "postgres";
if (constructorName.includes("mongo")) return "mongo";
if (constructorName.includes("upstash")) return "upstash";
if (constructorName.includes("dynamodb")) return "dynamodb";
if (constructorName.includes("redis")) return "redis";
return "unknown";
}
function trackFeatureUsage(name, metadata) {
try {
if (!isTelemetryEnabled()) return;
const { projectId, distinctId, command, nodeEnv } = getServerTelemetryContext();
captureTelemetryEvent(FEATURE_USAGE_EVENT, distinctId, {
feature_name: name,
project_id: projectId,
command,
node_env: nodeEnv,
...metadata
});
} catch {}
}
function syncFeatureUsageTelemetry(mastra) {
try {
if (!isTelemetryEnabled()) return;
const agents = mastra.listAgents?.();
const agentsWithMemoryCount = countAgentsWithMemory(agents);
const memory = mastra.listMemory?.();
const workspaces = mastra.listWorkspaces?.();
const tts = mastra.getTTS?.();
const promptBlocks = mastra.listPromptBlocks?.();
const processorConfigurations = mastra.listProcessorConfigurations?.();
const agentChannels = mastra.getChannels?.();
const storage = mastra.getStorage?.();
trackFeatureUsage("mastra_instance_summary", {
agent_count: countCollection(agents),
agents_with_memory_count: agentsWithMemoryCount,
agent_controller_count: countCollection(mastra.listAgentControllers?.()),
workflow_count: countCollection(mastra.listWorkflows?.()),
tool_count: countCollection(mastra.listTools?.()),
processor_count: countCollection(mastra.listProcessors?.()),
processor_configuration_count: countCollection(processorConfigurations),
memory_count: countCollection(memory),
vector_count: countCollection(mastra.listVectors?.()),
scorer_count: countCollection(mastra.listScorers?.()),
workspace_count: countCollection(workspaces),
mcp_server_count: countCollection(mastra.listMCPServers?.()),
gateway_count: countCollection(mastra.listGateways?.()),
channel_count: countCollection(mastra.getChannelProviders?.()),
agent_channel_count: countCollection(agentChannels),
tts_count: countCollection(tts),
prompt_block_count: countCollection(promptBlocks),
editor_enabled: !!mastra.getEditor?.(),
studio_enabled: !!mastra.getStudio?.(),
server_middleware_enabled: !!mastra.getServerMiddleware?.(),
storage_type: bucketStorageType(storage),
observability_enabled: !!storage?.stores?.observability
});
} catch {}
}
//#endregion
//#region src/telemetry/usage-telemetry.ts
const INPUT_TOKENS_METRIC = "mastra_model_total_input_tokens";
const OUTPUT_TOKENS_METRIC = "mastra_model_total_output_tokens";
const BREAKDOWN_LIMIT = 200;
const USAGE_TELEMETRY_EVENT = "mastra_model_token_usage";
function getDefaultCursorPath() {
return path.join(os.homedir(), ".mastra", "usage-telemetry.json");
}
function readCursors(cursorPath) {
try {
const parsed = JSON.parse(readFileSync(cursorPath, "utf-8"));
if (parsed && typeof parsed === "object" && parsed.projects && typeof parsed.projects === "object") return { projects: parsed.projects };
} catch {}
return { projects: {} };
}
function readCursor(cursorPath, projectId) {
const value = readCursors(cursorPath).projects[projectId];
if (!value) return;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? void 0 : date;
}
function writeCursor(cursorPath, projectId, syncedAt) {
const cursors = readCursors(cursorPath);
cursors.projects[projectId] = syncedAt.toISOString();
mkdirSync(path.dirname(cursorPath), { recursive: true });
writeFileSync(cursorPath, JSON.stringify(cursors));
}
function applyBreakdown(rows, response, field) {
for (const group of response.groups) {
const provider = group.dimensions.provider ?? null;
const model = group.dimensions.model ?? null;
const key = `${provider}\u0000${model}`;
let row = rows.get(key);
if (!row) {
row = {
provider,
model,
inputTokens: 0,
outputTokens: 0,
totalInputTokens: 0,
totalOutputTokens: 0
};
rows.set(key, row);
}
row[field] += group.value ?? 0;
}
}
/**
* Sends aggregated model token usage (input/output tokens per provider+model) to
* Mastra's anonymous telemetry when the project has observability metrics enabled.
*
* Sync strategy: deltas since the last successful sync, tracked per project via a
* cursor file in `~/.mastra/usage-telemetry.json`. Each event also carries lifetime
* totals so consumers can read either incremental or cumulative usage. Runs once at
* server startup; respects `MASTRA_TELEMETRY_DISABLED`. Never throws.
*/
async function syncUsageTelemetry(mastra, options = {}) {
try {
if (!isTelemetryEnabled()) return;
const observability = mastra.getStorage()?.stores?.observability;
if (!observability || typeof observability.getMetricBreakdown !== "function") return;
const { projectId, distinctId, command, nodeEnv } = getServerTelemetryContext();
const cursorPath = options.cursorPath ?? getDefaultCursorPath();
const lastSyncedAt = readCursor(cursorPath, projectId);
const now = options.now ?? /* @__PURE__ */ new Date();
if (lastSyncedAt && lastSyncedAt.getTime() >= now.getTime()) return;
const baseArgs = {
groupBy: ["provider", "model"],
aggregation: "sum",
limit: BREAKDOWN_LIMIT
};
const deltaFilters = { timestamp: {
...lastSyncedAt ? {
start: lastSyncedAt,
startExclusive: true
} : {},
end: now
} };
const totalFilters = { timestamp: { end: now } };
const [inputDelta, outputDelta, inputTotal, outputTotal] = await Promise.all([
observability.getMetricBreakdown({
...baseArgs,
name: [INPUT_TOKENS_METRIC],
filters: deltaFilters
}),
observability.getMetricBreakdown({
...baseArgs,
name: [OUTPUT_TOKENS_METRIC],
filters: deltaFilters
}),
observability.getMetricBreakdown({
...baseArgs,
name: [INPUT_TOKENS_METRIC],
filters: totalFilters
}),
observability.getMetricBreakdown({
...baseArgs,
name: [OUTPUT_TOKENS_METRIC],
filters: totalFilters
})
]);
const rows = /* @__PURE__ */ new Map();
applyBreakdown(rows, inputDelta, "inputTokens");
applyBreakdown(rows, outputDelta, "outputTokens");
applyBreakdown(rows, inputTotal, "totalInputTokens");
applyBreakdown(rows, outputTotal, "totalOutputTokens");
const isFirstSync = !lastSyncedAt;
for (const row of rows.values()) {
if (row.inputTokens <= 0 && row.outputTokens <= 0) continue;
captureTelemetryEvent(USAGE_TELEMETRY_EVENT, distinctId, {
provider: row.provider,
model: row.model,
input_tokens: row.inputTokens,
output_tokens: row.outputTokens,
total_input_tokens: row.totalInputTokens,
total_output_tokens: row.totalOutputTokens,
command,
node_env: nodeEnv,
project_id: projectId,
is_first_sync: isFirstSync,
window_start: lastSyncedAt?.toISOString() ?? null,
window_end: now.toISOString()
});
}
writeCursor(cursorPath, projectId, now);
} catch {}
}
//#endregion
export { FEATURE_USAGE_EVENT, USAGE_TELEMETRY_EVENT, syncFeatureUsageTelemetry, syncUsageTelemetry, trackFeatureUsage };
//# sourceMappingURL=index.js.map