Skip to content

feat: add dashboard analytics to sdk and public api#353

Open
magicspon wants to merge 1 commit intousesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api
Open

feat: add dashboard analytics to sdk and public api#353
magicspon wants to merge 1 commit intousesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon
Copy link
Contributor

@magicspon magicspon commented Feb 10, 2026

Summary by cubic

Expose dashboard analytics via two new public API endpoints and a typed SDK to let developers fetch email time series and reputation metrics programmatically. Also moved analytics logic into a shared service and updated docs.

  • New Features

    • Public API: GET /v1/analytics/email-time-series (days=7|30, domainId) and GET /v1/analytics/reputation-metrics (domainId).
    • SDK: UseSend.analytics with emailTimeSeries() and reputationMetrics() methods, returning typed responses.
    • Docs: Added Analytics section and OpenAPI specs for the new endpoints.
  • Refactors

    • Extracted analytics queries into server/service/dashboard-service.ts and simplified the dashboard router to call the service.
    • Registered the new public API routes and removed inline DB queries from the router.

Written for commit afcdd46. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added analytics endpoints for email time series data and reputation metrics retrieval
    • Introduced Contact Books API for managing contact lists
    • Added Idempotency-Key header support for email operations
    • Extended SDK with analytics functionality for email and reputation data queries
  • Documentation

    • Added API reference documentation for new analytics endpoints

@vercel
Copy link

vercel bot commented Feb 10, 2026

@magicspon is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Walkthrough

This PR introduces two new analytics API endpoints (GET /v1/analytics/email-time-series and GET /v1/analytics/reputation-metrics) with complete supporting infrastructure. The changes include public API route handlers, service layer functions for data aggregation, documentation pages, OpenAPI schema definitions, TypeScript type definitions, and an SDK wrapper class. The endpoints enable retrieval of email metrics over configurable time periods and reputation metrics, with optional domain filtering via domainId query parameter or team apiKey domain association.

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • KMKoushik
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding dashboard analytics capabilities to both the SDK and public API. It is clear, specific, and directly reflects the primary purpose of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/server/public-api/api/analytics/reputation-metrics-data.ts`:
- Around line 38-40: The code assigns domain using Number(domainIdParam) which
can produce NaN and then be passed into the DB query; update the logic around
the domain variable (referencing domain, team.apiKey.domainId, and domainIdParam
in reputation-metrics-data.ts) to validate/coerce the incoming domainIdParam
before using it—either use a Zod coercion schema (e.g.,
z.coerce.number().int().optional()) to parse and validate domainIdParam or add
an explicit guard that calls Number(domainIdParam), checks Number.isFinite(...)
and Number.isInteger(...), and only uses the numeric value when valid; if
invalid, fall back to team.apiKey.domainId or undefined so Prisma never receives
NaN.
🧹 Nitpick comments (4)
apps/web/src/server/public-api/api/analytics/email-time-series.ts (2)

5-49: Consider adding error response schemas (e.g., 401, 500) for completeness.

The route definition only specifies a 200 response. Other endpoints in this codebase (e.g., domain and contact book endpoints in schema.d.ts) define 403 and 404 error responses. While unauthenticated requests may be handled by middleware, documenting error responses in the OpenAPI schema improves the developer experience for API consumers.


52-65: Use c.req.valid("query") instead of c.req.query() to leverage validated and typed query params.

Lines 54–55 bypass the Zod-validated query by using c.req.query("days") and c.req.query("domainId") directly. With @hono/zod-openapi, the validated query object is available via c.req.valid("query"), which provides proper typing and ensures you're working with the already-validated values. This pattern is demonstrated in apps/web/src/server/public-api/api/emails/list-emails.ts:109.

♻️ Proposed refactor
 function emailTimeSeries(app: PublicAPIApp) {
   app.openapi(route, async (c) => {
     const team = c.var.team;
-    const daysParam = c.req.query("days");
-    const domainIdParam = c.req.query("domainId");
+    const { days: daysParam, domainId: domainIdParam } = c.req.valid("query");
 
     const days = daysParam ? Number(daysParam) : undefined;
     const domain =
       team.apiKey.domainId ??
       (domainIdParam ? Number(domainIdParam) : undefined);
 
     const data = await emailTimeSeriesService({ days, domain, team });
 
     return c.json(data);
   });
 }
packages/sdk/src/analytics.ts (1)

27-30: Redundant assignment in constructor.

The private readonly usesend parameter property already assigns this.usesend. The explicit this.usesend = usesend on Line 29 is unnecessary.

Proposed fix
 export class Analytics {
-  constructor(private readonly usesend: UseSend) {
-    this.usesend = usesend;
-  }
+  constructor(private readonly usesend: UseSend) {}
apps/web/src/server/service/dashboard-service.ts (1)

11-12: The days coercion logic silently maps all non-7 values to 30.

input.days !== 7 ? 30 : 7 means values like 14 or 1 are silently treated as 30. The TRPC input schema (z.number().optional()) allows any number, while the public API restricts to "7" | "30". Consider constraining the TRPC schema to match, or at least accepting 30 explicitly:

-const days = input.days !== 7 ? 30 : 7;
+const days = input.days === 7 ? 7 : 30;

The behavior is identical, but the current expression reads as if 7 is the special case being checked. More importantly, consider adding validation in the TRPC input schema:

days: z.union([z.literal(7), z.literal(30)]).optional(),

This would align both API surfaces and prevent silent coercion of unexpected values.

Comment on lines +38 to +40
const domain =
team.apiKey.domainId ??
(domainIdParam ? Number(domainIdParam) : undefined);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Number(domainIdParam) can produce NaN, which will be passed to the database query.

If a caller passes a non-numeric domainId (e.g., ?domainId=abc), Number("abc") yields NaN, which will be forwarded to Prisma's findMany as domainId: NaN. This will likely cause an unexpected query error or silently return no results.

Consider validating/coercing with the Zod schema (e.g., z.coerce.number().int().optional()) or adding a guard:

Proposed fix
     const domain =
       team.apiKey.domainId ??
-      (domainIdParam ? Number(domainIdParam) : undefined);
+      (domainIdParam ? Number(domainIdParam) : undefined);
+
+    if (domain !== undefined && Number.isNaN(domain)) {
+      return c.json({ error: "Invalid domainId" }, 400);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const domain =
team.apiKey.domainId ??
(domainIdParam ? Number(domainIdParam) : undefined);
const domain =
team.apiKey.domainId ??
(domainIdParam ? Number(domainIdParam) : undefined);
if (domain !== undefined && Number.isNaN(domain)) {
return c.json({ error: "Invalid domainId" }, 400);
}
🤖 Prompt for AI Agents
In `@apps/web/src/server/public-api/api/analytics/reputation-metrics-data.ts`
around lines 38 - 40, The code assigns domain using Number(domainIdParam) which
can produce NaN and then be passed into the DB query; update the logic around
the domain variable (referencing domain, team.apiKey.domainId, and domainIdParam
in reputation-metrics-data.ts) to validate/coerce the incoming domainIdParam
before using it—either use a Zod coercion schema (e.g.,
z.coerce.number().int().optional()) to parse and validate domainIdParam or add
an explicit guard that calls Number(domainIdParam), checks Number.isFinite(...)
and Number.isInteger(...), and only uses the numeric value when valid; if
invalid, fall back to team.apiKey.domainId or undefined so Prisma never receives
NaN.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 12 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/src/server/public-api/api/analytics/email-time-series.ts">

<violation number="1" location="apps/web/src/server/public-api/api/analytics/email-time-series.ts:57">
P2: domainId is validated only as a string, but the handler converts raw query params with Number(), which can yield NaN and pass it to the service. Use validated query data and/or coerce domainId to a number in the schema to prevent NaN inputs.</violation>
</file>

<file name="apps/web/src/server/service/dashboard-service.ts">

<violation number="1" location="apps/web/src/server/service/dashboard-service.ts:49">
P2: The time series returns `days + 1` entries (loop runs from days to 0 inclusive), so a 7‑day request yields 8 days of data. This conflicts with the API’s “number of days” contract and likely produces an extra day.</violation>

<violation number="2" location="apps/web/src/server/service/dashboard-service.ts:58">
P2: `format` is given a YYYY-MM-DD string, which date-fns parses via `new Date` (UTC) and can shift the day in non-UTC timezones, producing mislabeled chart dates. Use a Date object (e.g., parseISO or the existing Date) instead of the string.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const daysParam = c.req.query("days");
const domainIdParam = c.req.query("domainId");

const days = daysParam ? Number(daysParam) : undefined;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 10, 2026

Choose a reason for hiding this comment

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

P2: domainId is validated only as a string, but the handler converts raw query params with Number(), which can yield NaN and pass it to the service. Use validated query data and/or coerce domainId to a number in the schema to prevent NaN inputs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/server/public-api/api/analytics/email-time-series.ts, line 57:

<comment>domainId is validated only as a string, but the handler converts raw query params with Number(), which can yield NaN and pass it to the service. Use validated query data and/or coerce domainId to a number in the schema to prevent NaN inputs.</comment>

<file context>
@@ -0,0 +1,68 @@
+    const daysParam = c.req.query("days");
+    const domainIdParam = c.req.query("domainId");
+
+    const days = daysParam ? Number(daysParam) : undefined;
+    const domain =
+      team.apiKey.domainId ??
</file context>
Fix with Cubic

const filledResult: DailyEmailUsage[] = [];
const endDateObj = new Date();

for (let i = days; i > -1; i--) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 10, 2026

Choose a reason for hiding this comment

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

P2: The time series returns days + 1 entries (loop runs from days to 0 inclusive), so a 7‑day request yields 8 days of data. This conflicts with the API’s “number of days” contract and likely produces an extra day.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/server/service/dashboard-service.ts, line 49:

<comment>The time series returns `days + 1` entries (loop runs from days to 0 inclusive), so a 7‑day request yields 8 days of data. This conflicts with the API’s “number of days” contract and likely produces an extra day.</comment>

<file context>
@@ -0,0 +1,133 @@
+  const filledResult: DailyEmailUsage[] = [];
+  const endDateObj = new Date();
+
+  for (let i = days; i > -1; i--) {
+    const dateStr = subDays(endDateObj, i)
+      .toISOString()
</file context>
Fix with Cubic

if (existingData) {
filledResult.push({
...existingData,
date: format(dateStr, "MMM dd"),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 10, 2026

Choose a reason for hiding this comment

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

P2: format is given a YYYY-MM-DD string, which date-fns parses via new Date (UTC) and can shift the day in non-UTC timezones, producing mislabeled chart dates. Use a Date object (e.g., parseISO or the existing Date) instead of the string.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/server/service/dashboard-service.ts, line 58:

<comment>`format` is given a YYYY-MM-DD string, which date-fns parses via `new Date` (UTC) and can shift the day in non-UTC timezones, producing mislabeled chart dates. Use a Date object (e.g., parseISO or the existing Date) instead of the string.</comment>

<file context>
@@ -0,0 +1,133 @@
+    if (existingData) {
+      filledResult.push({
+        ...existingData,
+        date: format(dateStr, "MMM dd"),
+      });
+    } else {
</file context>
Fix with Cubic

@greptile-apps
Copy link

greptile-apps bot commented Feb 10, 2026

Greptile Overview

Greptile Summary

This PR adds analytics endpoints for dashboard-style metrics (email time series and reputation metrics) to both the internal dashboard tRPC router and the public API, and exposes matching client methods via the SDK (usesend.analytics.*). The data fetching logic is centralized into a new apps/web/src/server/service/dashboard-service.ts module that is reused by the tRPC router and the Hono public API routes, while the OpenAPI schema + docs are updated so the SDK types can reference the new endpoints.

Confidence Score: 5/5

  • This PR is safe to merge once formatting is aligned with repo lint/prettier rules.
  • Changes are mostly additive (new endpoints + SDK wrapper) and reuse existing query logic via a shared service module. The only clear issue found is inconsistent formatting/missing semicolons in the new service file, which is likely to fail formatting/lint checks if enforced in CI.
  • apps/web/src/server/service/dashboard-service.ts

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +5 to +8
type EmailTimeSeries = {
days?: number;
domain?: number
team: Team
Copy link

Choose a reason for hiding this comment

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

Inconsistent formatting likely breaks lint

This new service file mixes tabs/spaces and omits semicolons (e.g., domain?: number / team: Team in EmailTimeSeries and ReputationMetricsData, and const { domain, team } = input at line 13/103). If this repo enforces @typescript-eslint/semi / no-tabs / prettier defaults, CI will fail. Please run formatting (prettier) and make indentation consistent (2 spaces) + add missing semicolons.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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