Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
80 changes: 74 additions & 6 deletions scripts/collect-metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const path = require('path');

const AIBTC_API = 'https://aibtc.com/api';
const DATA_DIR = path.join(__dirname, '..', 'data');
const COHORT_FILE = path.join(DATA_DIR, 'sample-cohort.json');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve maintainability, it's a good practice to define the sample size as a constant here, instead of hardcoding the value 20 in the main function. You can then use this constant in sampleInboxMetrics and writeSampleCohort calls.

Suggested change
const COHORT_FILE = path.join(DATA_DIR, 'sample-cohort.json');
const COHORT_FILE = path.join(DATA_DIR, 'sample-cohort.json');
const SAMPLE_SIZE = 20;


async function fetchAllAgents() {
console.log('Fetching agents...');
Expand All @@ -32,10 +33,58 @@ async function fetchAllAgents() {
return agents;
}

async function sampleInboxMetrics(agents, sampleSize = 20) {
function selectStableSample(agents, sampleSize = 20, cohortAddresses = []) {
const agentsByAddress = new Map(
agents
.filter(agent => agent.btcAddress)
.map(agent => [agent.btcAddress, agent])
);
const selectedAddresses = [];
const seen = new Set();

for (const address of cohortAddresses) {
if (agentsByAddress.has(address) && !seen.has(address)) {
selectedAddresses.push(address);
seen.add(address);
}
if (selectedAddresses.length >= sampleSize) break;
}

const stableAddresses = [...agentsByAddress.keys()]
.filter(address => !seen.has(address))
.sort();

for (const address of stableAddresses) {
selectedAddresses.push(address);
if (selectedAddresses.length >= sampleSize) break;
}

return selectedAddresses
.map(address => agentsByAddress.get(address))
.filter(Boolean);
}

function readSampleCohort() {
if (!fs.existsSync(COHORT_FILE)) {
return [];
}

const cohort = JSON.parse(fs.readFileSync(COHORT_FILE, 'utf8'));
return Array.isArray(cohort.btcAddresses) ? cohort.btcAddresses : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The readSampleCohort function could be more robust. If sample-cohort.json exists but is empty or contains invalid JSON, JSON.parse will throw an error, causing the script to exit. It would be better to handle this case gracefully by catching the error and returning an empty array. This would make the script self-healing in case of a corrupted cohort file, allowing it to generate a new one on the next run.

  try {
    const fileContent = fs.readFileSync(COHORT_FILE, 'utf8');
    if (!fileContent) {
      return [];
    }
    const cohort = JSON.parse(fileContent);
    return Array.isArray(cohort.btcAddresses) ? cohort.btcAddresses : [];
  } catch (error) {
    console.error(`Error reading or parsing ${COHORT_FILE}: ${error.message}`);
    return [];
  }

}

function writeSampleCohort(sample, sampleSize) {
fs.writeFileSync(COHORT_FILE, JSON.stringify({
sampleSize,
updated_at: new Date().toISOString(),
btcAddresses: sample.map(agent => agent.btcAddress)
}, null, 2));
}

async function sampleInboxMetrics(agents, sampleSize = 20, cohortAddresses = []) {
console.log(`Sampling ${sampleSize} agent inboxes...`);
const sample = agents.slice(0, sampleSize);
const sample = selectStableSample(agents, sampleSize, cohortAddresses);

let totalMessages = 0;
let totalSatsReceived = 0;
let totalSatsSent = 0;
Expand All @@ -53,13 +102,23 @@ async function sampleInboxMetrics(agents, sampleSize = 20) {
console.error(`Failed to fetch inbox for ${agent.btcAddress}:`, err.message);
}
}


if (!sample.length) {
return {
totalMessages: 0,
totalSatsReceived: 0,
totalSatsSent: 0,
sample
};
}

// Extrapolate to full network
const scaleFactor = agents.length / sample.length;
return {
totalMessages: Math.round(totalMessages * scaleFactor),
totalSatsReceived: Math.round(totalSatsReceived * scaleFactor),
totalSatsSent: Math.round(totalSatsSent * scaleFactor),
sample
};
}

Expand All @@ -75,7 +134,8 @@ async function main() {
const registeredCount = agents.filter(a => a.level === 1).length;

// Sample inbox metrics
const inboxMetrics = await sampleInboxMetrics(agents);
const sampleCohort = readSampleCohort();
const inboxMetrics = await sampleInboxMetrics(agents, 20, sampleCohort);

// Count new agents today
const newAgentsCount = agents.filter(a => {
Expand Down Expand Up @@ -125,6 +185,7 @@ async function main() {
// Write latest snapshot for quick access
const latestFile = path.join(DATA_DIR, 'latest.json');
fs.writeFileSync(latestFile, JSON.stringify(todayMetrics, null, 2));
writeSampleCohort(inboxMetrics.sample, 20);

// Write agent list
const agentsFile = path.join(DATA_DIR, 'agents.json');
Expand Down Expand Up @@ -152,4 +213,11 @@ async function main() {
}
}

main();
if (require.main === module) {
main();
}

module.exports = {
selectStableSample,
sampleInboxMetrics
};
46 changes: 46 additions & 0 deletions scripts/collect-metrics.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { selectStableSample } = require('./collect-metrics');

function agent(btcAddress) {
return { btcAddress };
}

test('selectStableSample is stable when API result ordering changes', () => {
const agents = [
agent('btc-z'),
agent('btc-c'),
agent('btc-a'),
agent('btc-b')
];
const reorderedAgents = [
agent('btc-b'),
agent('btc-z'),
agent('btc-a'),
agent('btc-c')
];

assert.deepEqual(
selectStableSample(agents, 3).map(item => item.btcAddress),
['btc-a', 'btc-b', 'btc-c']
);
assert.deepEqual(
selectStableSample(reorderedAgents, 3).map(item => item.btcAddress),
['btc-a', 'btc-b', 'btc-c']
);
});

test('selectStableSample preserves an existing cohort and backfills missing addresses', () => {
const agents = [
agent('btc-a'),
agent('btc-b'),
agent('btc-c'),
agent('btc-d')
];
const cohort = ['btc-c', 'btc-missing', 'btc-a'];

assert.deepEqual(
selectStableSample(agents, 3, cohort).map(item => item.btcAddress),
['btc-c', 'btc-a', 'btc-b']
);
});