Skip to content

feat(calls): add calls stats and calls export - #26

Merged
purpshell merged 13 commits into
devfrom
t3code/calls-analytics
Sep 23, 2026
Merged

purpshell merged 13 commits into
devfrom
t3code/calls-analytics

Conversation

@purpshell

@purpshell purpshell commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

polymorfa calls stats reads call statistics, including grouped rows and an optional day/hour heatmap. polymorfa calls export streams CSV or NDJSON pages, writes the CSV header once, supports a safe output file and resume cursor, rejects cursor cycles before duplicate output, withholds a resume cursor after partial write failure, and keeps progress/errors on stderr so stdout remains usable in a pipeline. Invalid filters, time ranges, time zones, and output paths fail before a network request.

Both commands use the published typed Client.calls methods from exact @polymorfa/sdk@0.1.0-dev.20260923124417 (SDK #281, merged at ad5e5620). The package-lock integrity is the npm registry tarball sha512-mwcZVuYgFmLJeTSMT12I4YhFFUVKrdXzO+N4LEPAVNIstIxXiC8K8AB4eEmMIzOsShVndcINyj/BZfoTahdPlw==. No raw SDK transport or fetch wrapper remains. Organization keys read team-wide calls by default even when a saved profile has a project owner, or an explicit project when requested; project tokens stay bound to their exact project. Both require sessions:read at the API.

The branch normally includes current CLI dev b782436a and this existing PR targets dev. It is non-draft.

Part Assessment
API Unaffected. The CLI consumes existing GET /platform/calls/stats and /platform/calls/export; no route or wire shape changes. The accepted monorepo Calls analytics contract is on dev.
SDKs Updated upstream. SDK #281 supplies typed stats/list/export and cursor metadata; its automatic dev publication succeeded and the exact downloadable tarball is pinned here.
CLI Updated. New commands, flags, help, output, error exits, project credential handling, pagination, and resume behavior are implemented here.
Docs Updated. README and Calls command research in this repo describe the commands; the monorepo Calls guide includes the API, SDK, and CLI examples. No unreleased Calls changelog entry was added.
Feature releases Unchanged. Calls remains unreleased to customers; this merge does not enable a feature, enroll an audience, deploy, or publish a stable CLI release. API authority still decides access.
Admin Unaffected. These read-only CLI operations do not change Admin controls or contracts.

Full npm run verify passed under Node 22 against the exact published SDK: format, lint, typecheck, 723 tests in 78 files, build, pack dry-run, and production audit with zero vulnerabilities. Command tests exercise a local HTTP server, including success, pagination, cursor and output errors, credential scope, and failure exits. A deployed API and real credential were not exercised.

calls stats summarizes calls from GET /platform/calls/stats: totals, answer
rate, connected time, groups by day, hour, session or outcome, and an
optional day-by-hour heatmap. calls export walks GET /platform/calls/export
through the Polymorfa-Next-Cursor header, writes one CSV header (or NDJSON),
and keeps progress and resume cursors on standard error.

Both accept an organization API key or an exact-project token. The pinned SDK
has no typed methods for these operations yet, so they go through the SDK's
raw transport with a fetch wrapper that reads the cursor header.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Ask an admin to enable usage-based reviews

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Next included review available in 58 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 73 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

The included review limit has been reached and this organization has disabled usage-based review continuation. Wait for reviews to reset or ask a billing admin to change After included review limits.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 9cb9da80-2e31-4c30-af70-bafd1daaee83

📥 Commits

Reviewing files that changed from the base of the PR and between b782436 and e5a29f5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • README.md
  • docs/command-research/calls.md
  • package.json
  • skills/polymorfa/SKILL.md
  • src/base-command.ts
  • src/commands/calls/export.ts
  • src/commands/calls/stats.ts
  • src/context/environment.ts
  • src/developer/flags.ts
  • src/services/call-analytics.ts
  • test/commands/call-analytics.test.ts
  • test/context/environment.test.ts

Comment @coderabbitai help to get the list of available commands.

@purpshell

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@purpshell

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-23T13:15:31.608519Z e5a29f5 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2109df53d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +82 to +85
const format = flags.format as CallExportFormat
const environment = await this.developerEnvironment(flags, {
inferOwner: true,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check output conflicts before resolving project slugs

When --project is a slug, developerEnvironment() resolves it by making a project-list API request before the existing output path is checked. Consequently, calls export --project <slug> --output <existing-file> can contact the service—or fail with a network/authentication error—instead of immediately reporting the promised local overwrite conflict. Move the output-path preflight ahead of environment resolution.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 723f815. calls export refuses an existing --output file (without --force) before developerEnvironment() resolves a project slug. The test 'refuses an existing output file before resolving a project slug' asserts that no request is made.

Comment on lines +107 to +110
: async (chunk: string) => {
file ??= await openOutput(outputPath, flags.force)
await file.write(chunk)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry short file writes

When FileHandle.write() completes with bytesWritten smaller than the encoded chunk, which is permitted for low-level writes, the unwritten suffix is silently discarded while the export advances to the next page and reports all records as written. This can leave a corrupted CSV or NDJSON file under disk pressure or on filesystems that return short writes; use an operation that writes the full chunk or loop until all bytes are persisted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 723f815. File output goes through writeFully, which loops on bytesWritten until the whole encoded chunk is persisted and fails if a write makes no progress.

Comment thread src/services/call-analytics.ts Outdated
outcome: undefined,
session: undefined,
}
const TIME_ZONE = /^(?:UTC|[A-Za-z]+(?:\/[A-Za-z0-9_+-]+){1,2})$/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept valid single-component IANA time zones

For users selecting valid IANA identifiers such as GMT, CET, NZ-CHAT, or W-SU, this regex rejects the value locally solely because it lacks a slash, even though these identifiers are recognized time zones. Validate the identifier with the runtime time-zone database (or otherwise include valid aliases) rather than requiring an Area/Location shape.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 723f815. isTimeZoneName no longer requires Area/Location. It validates the name with Intl.DateTimeFormat (the runtime time zone database), so GMT, CET, NZ-CHAT and W-SU are accepted. Numeric offsets are still refused.

…ccept single-name time zones

- A short FileHandle.write() no longer drops the rest of a chunk: writeFully
  keeps writing the remainder and fails if nothing is written.
- calls export refuses an existing --output file before resolving a project
  slug, so the conflict is reported without contacting the API.
- --timezone accepts any IANA name the runtime knows (CET, GMT, NZ-CHAT) and
  still refuses offsets such as +03:00.
@purpshell

Copy link
Copy Markdown
Member Author

Codex review follow-up (723f815):

  • export.ts:110: fixed. writeFully keeps writing the rest of a short FileHandle.write() and fails if nothing is written. Unit tests cover a writer that persists 3 bytes per call and a zero-byte write.
  • export.ts:85: fixed. The existing --output file is now refused before the command resolves a project slug; a test asserts that no request is sent.
  • call-analytics.ts:54: fixed. --timezone accepts any IANA name the runtime zone database knows (CET, GMT, NZ-CHAT) and still refuses offsets. The API accepts these names once polymorfa/polymorfa#254 lands; until then it returns 400 for them.

@purpshell

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d3b56fe51

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/services/call-analytics.ts Outdated
Comment on lines +336 to +338
if (chunk.length > 0) {
try {
await input.write(chunk)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Truncate empty NDJSON exports

When an NDJSON export successfully returns zero matching records, page.body is empty, so this guard never invokes the lazy file writer. For --output <existing-file> --force, the old file remains untouched while the command reports a successful zero-record export, leaving stale records that can be mistaken for current results; for a new path, no output file is created at all. Ensure a successful empty NDJSON page still opens and truncates or creates the requested destination.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0b6250d. The first page is now always handed to the writer, even when empty, so a zero-record export creates a new --output file or truncates a --force target. The new test covers both cases.

An NDJSON export with no matching records has an empty body, so the lazy
file writer never ran: --force left the old file's records in place and a
new path was never created. The first page is now always written.
# Conflicts:
#	docs/command-research/calls.md
@purpshell

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb7365bcd5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +203 to +205
async function writeStandardOutput(chunk: string): Promise<void> {
// Wait for the pipe to drain so a slow reader bounds memory use.
if (!process.stdout.write(chunk)) await once(process.stdout, 'drain')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop cleanly when the stdout consumer closes

When output is piped to a consumer that exits early, such as calls export | head -n 1, stdout emits EPIPE rather than drain; this wait therefore rejects and the export reports a failed page and exits non-zero, or the error can be unhandled if the initial write returned true. This is especially problematic because the command explicitly supports pipelines. Handle closed-pipe errors and stop pagination normally, as event stream already does in its writeStdout path.

Useful? React with 👍 / 👎.

Comment on lines +90 to +91
if (outputPath !== undefined && !flags.force)
await rejectExisting(outputPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preflight forced output paths before exporting

When --force is present, this condition skips all output-path preflight. For example, calls export --force --output <existing-directory> resolves credentials and performs the export request before openOutput() inevitably fails with EISDIR; a path under a nonexistent parent behaves similarly. Check detectable path errors such as directories and missing parents before environment resolution while still deferring file truncation until the first page arrives.

Useful? React with 👍 / 👎.

@purpshell

Copy link
Copy Markdown
Member Author

@codex review

@purpshell

Copy link
Copy Markdown
Member Author

Current-head review follow-up (9d54c03): export now tracks all previously issued continuation cursors and rejects an A→B→A cycle before writing the repeated page. A write failure may leave partial bytes, so its stopped report no longer advertises that page cursor as append-safe. Two regressions were added; full Node 22 verification passes with 722 tests, build, pack, and production audit zero. Fresh CI and Codex review were requested after this push.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d54c03b3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +187 to +191
if (
maxDays !== undefined &&
input.since !== undefined &&
input.until !== undefined &&
Date.parse(input.until) - Date.parse(input.since) > maxDays * DAY_MS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate statistics limits when only --since is supplied

When --since is supplied without --until, the effective upper bound is the API's default of the current time, but this check is skipped entirely. Thus calls stats --group-by hour --since 2026-01-01T00:00:00Z can exceed the documented 31-day limit and still resolve credentials and contact the service before being rejected there. Compute the effective default upper bound when enforcing the hour and day limits so this invalid range fails during local validation as promised.

Useful? React with 👍 / 👎.

@purpshell

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5a29f5342

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const mode = this.outputMode(flags)
const report = (value: object, human: string): void => {
if (mode === 'json') process.stderr.write(`${JSON.stringify(value)}\n`)
else if (mode === 'human') process.stderr.write(`${human}\n`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the resume cursor in quiet mode

When --quiet is used and a later page fails, this branch suppresses the onStop report along with routine progress. The subsequently rendered API error does not include the failed page's cursor, so a partially written export cannot be safely resumed even though --quiet only promises to suppress successful output and the command promises to report recovery cursors on failure. Emit the stopped/resume information for failures even in quiet mode.

Useful? React with 👍 / 👎.

@purpshell

Copy link
Copy Markdown
Member Author

Merge gate for exact head e5a29f5: Node 22 current-head CI passed; GitHub Codex completed its current-head review with no findings; local codex review --base origin/dev also found no actionable regression. Full local verification passed with 723 tests in 78 files, format/lint/typecheck/build/pack and zero production audit vulnerabilities. CodeRabbit reported Review rate limited; its SUCCESS check is not a clean review. The owner explicitly authorized merging #26 with CI + Codex and recording this missing CodeRabbit review. Calls remains unreleased; this CLI merge does not deploy or enable it.

@purpshell
purpshell merged commit a63a6d8 into dev Sep 23, 2026
3 checks passed
@purpshell
purpshell deleted the t3code/calls-analytics branch October 26, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant