diff --git a/README.md b/README.md index 70f4e82..729af82 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ delete Delete a project by exact name or ID # Analytics all-sites Historical summary across all projects bot-traffic Filtered automated traffic by project or --all -stats Overview: events, users, daily trends +stats Overview: events, users, daily trends, countries live [name] Real-time terminal dashboard across all projects insights Period-over-period comparison with trends breakdown --property path Top pages, referrers, UTM sources, countries @@ -67,7 +67,7 @@ funnel --steps-json Structured funnel analysis with raw-vs-strict d funnel --steps a,b,c Legacy comma-separated funnel steps retention Cohort retention: % of users who return sessions-dist Session duration distribution -events Raw event log +events Raw event log, including each event's country sessions Individual session records query Flexible analytics query (metrics, group_by, filters) properties Discover event names & property keys diff --git a/bin/cli.mjs b/bin/cli.mjs index 2438e72..f020887 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -949,6 +949,15 @@ const cmdStats = withApi(async (api, project, days = 7) => { } } + if (Array.isArray(data.countries) && data.countries.length > 0) { + log(''); + heading('Countries:'); + for (const row of data.countries) { + log(` ${BOLD}${row.country || 'Unknown'}${RESET} ${row.count} events ${DIM}(${row.unique_users} users)${RESET}`); + } + log(` ${DIM}Users can appear in more than one country.${RESET}`); + } + // Monthly usage summary from response headers const monthlyUsage = headers['x-monthly-usage']; if (monthlyUsage) { @@ -987,7 +996,7 @@ const cmdEvents = withApi(async (api, project, opts = {}) => { for (const e of data.events) { const time = new Date(e.timestamp).toLocaleString(); - log(` ${DIM}${time}${RESET} ${BOLD}${e.event}${RESET} ${DIM}${e.user_id || ''}${RESET}`); + log(` ${DIM}${time}${RESET} ${BOLD}${e.event}${RESET} ${DIM}${e.user_id || ''}${RESET} ${DIM}country: ${e.country || 'Unknown'}${RESET}`); if (e.properties) { log(` ${DIM}${JSON.stringify(e.properties)}${RESET}`); } @@ -2344,7 +2353,7 @@ ${BOLD}SETUP${RESET} ${BOLD}ANALYTICS${RESET} ${CYAN}all-sites${RESET} Historical summary across all projects ${CYAN}bot-traffic${RESET} Filtered automated traffic by project or --all - ${CYAN}stats${RESET} Overview: events, users, daily trends + ${CYAN}stats${RESET} Overview: events, users, daily trends, countries ${CYAN}live${RESET} [name] Real-time terminal dashboard across all projects ${CYAN}insights${RESET} Period-over-period comparison with trends ${CYAN}breakdown${RESET} Top pages, referrers, UTM sources, countries diff --git a/package-lock.json b/package-lock.json index a2669ff..970b644 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agent-analytics/cli", - "version": "0.5.34", + "version": "0.5.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-analytics/cli", - "version": "0.5.34", + "version": "0.5.35", "license": "MIT", "dependencies": { "@zowe/secrets-for-zowe-sdk": "^8.32.0" diff --git a/package.json b/package.json index 93e8057..1db990a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agent-analytics/cli", - "version": "0.5.34", + "version": "0.5.35", "description": "Stop juggling dashboards. Let your agent do it. Analytics your AI agent can actually use — track, analyze, experiment, optimize.", "bin": { "agent-analytics": "bin/cli.mjs" diff --git a/test/stats-render.test.mjs b/test/stats-render.test.mjs index 94409cf..e08d4c1 100644 --- a/test/stats-render.test.mjs +++ b/test/stats-render.test.mjs @@ -35,6 +35,36 @@ function runCli(args, env) { } describe('stats command rendering', () => { + it('renders countries, raw event counts, unknown geography and users', async () => { + const { server, url } = await startMockServer({ + totals: { total_events: 7, unique_users: 2 }, + countries: [{ country: 'NL', count: 5, unique_users: 1 }, { country: null, count: 2, unique_users: 1 }], + }); + try { + const result = await runCli(['stats', 'test-project'], { ...agentSessionEnv(), AGENT_ANALYTICS_URL: url }); + const plain = stripAnsi(result.stdout); + assert.equal(result.code, 0); + assert.ok(plain.includes('Countries:')); + assert.ok(plain.includes('NL 5 events')); + assert.ok(plain.includes('Unknown 2 events')); + assert.ok(plain.includes('Users can appear in more than one country.')); + } finally { server.close(); } + }); + + it('shows event country outside the properties JSON', async () => { + const { server, url } = await startMockServer({ events: [ + { event: 'play', country: 'NL', timestamp: 1, properties: { stage: 2 } }, + { event: 'play', country: null, timestamp: 2, properties: null }, + ] }); + try { + const result = await runCli(['events', 'test-project'], { ...agentSessionEnv(), AGENT_ANALYTICS_URL: url }); + const plain = stripAnsi(result.stdout); + assert.equal(result.code, 0); + assert.ok(plain.includes('country: NL')); + assert.ok(plain.includes('country: Unknown')); + assert.ok(plain.includes('"stage":2')); + } finally { server.close(); } + }); it('renders timeSeries as daily chart', async () => { const { server, url } = await startMockServer({ totals: { total_events: 100, unique_users: 10 },