Skip to content

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

Merged
KMKoushik merged 2 commits intousesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api
Mar 4, 2026
Merged

feat: add dashboard analytics to sdk and public api#353
KMKoushik merged 2 commits 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. Centralized analytics logic in a shared service and updated docs.

  • New Features

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

    • Moved analytics queries into server/service/dashboard-service.ts; dashboard router now calls the service.
    • Registered the new public API routes and removed inline DB queries from the router.

Written for commit 719fc02. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added analytics endpoints for email time-series and reputation metrics retrieval.
  • SDK

    • Extended SDK with analytics methods to query email time-series and reputation metrics.
  • Documentation

    • Added API reference pages and OpenAPI spec entries documenting the 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

Adds two analytics endpoints: GET /v1/analytics/email-time-series and GET /v1/analytics/reputation-metrics. Changes include OpenAPI spec additions, new MDX docs and docs navigation entries, server public-api route handlers, service-layer implementations (emailTimeSeries and reputationMetricsData), integration into the public-api initializer, and SDK client support (Analytics class and UseSend.analytics). The service functions perform DB aggregation and return typed result shapes; public routes wire requests to those services and expose the new responses.

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 reflects the main objectives of the PR: adding dashboard analytics to both the SDK and public API, which are the primary changes.

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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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!

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/server/public-api/index.ts`:
- Around line 31-32: The two new imports emailTimeSeries and
reputationMetricsData in public-api index.ts use relative paths; update their
import specifiers to use the ~/ alias for modules under apps/web/src (i.e.,
replace "./api/analytics/email-time-series" and
"./api/analytics/reputation-metrics-data" with their corresponding "~/..."
paths) so they follow the apps/web src import convention and resolver.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d746c5f9-a80e-48a2-9cd4-278980d8990d

📥 Commits

Reviewing files that changed from the base of the PR and between afcdd46 and 719fc02.

📒 Files selected for processing (5)
  • apps/docs/api-reference/openapi.json
  • apps/docs/docs.json
  • apps/web/src/server/public-api/index.ts
  • packages/sdk/src/usesend.ts
  • packages/sdk/types/schema.d.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/docs/docs.json
  • apps/docs/api-reference/openapi.json
  • packages/sdk/types/schema.d.ts

Comment on lines +31 to +32
import emailTimeSeries from "./api/analytics/email-time-series";
import reputationMetricsData from "./api/analytics/reputation-metrics-data";
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

Use ~/ aliases for the new analytics imports.

These new imports point to modules under apps/web/src, so they should use the ~/ alias instead of relative paths.

Proposed change
-import emailTimeSeries from "./api/analytics/email-time-series";
-import reputationMetricsData from "./api/analytics/reputation-metrics-data";
+import emailTimeSeries from "~/server/public-api/api/analytics/email-time-series";
+import reputationMetricsData from "~/server/public-api/api/analytics/reputation-metrics-data";

As per coding guidelines apps/web/src/**/*.{ts,tsx}: Use the ~/ alias for imports from src in apps/web (e.g., import { x } from "~/utils/x").

📝 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
import emailTimeSeries from "./api/analytics/email-time-series";
import reputationMetricsData from "./api/analytics/reputation-metrics-data";
import emailTimeSeries from "~/server/public-api/api/analytics/email-time-series";
import reputationMetricsData from "~/server/public-api/api/analytics/reputation-metrics-data";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/server/public-api/index.ts` around lines 31 - 32, The two new
imports emailTimeSeries and reputationMetricsData in public-api index.ts use
relative paths; update their import specifiers to use the ~/ alias for modules
under apps/web/src (i.e., replace "./api/analytics/email-time-series" and
"./api/analytics/reputation-metrics-data" with their corresponding "~/..."
paths) so they follow the apps/web src import convention and resolver.

Copy link
Member

@KMKoushik KMKoushik left a comment

Choose a reason for hiding this comment

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

Reviewed locally. LGTM with minor non-blocking input validation follow-up.

@KMKoushik
Copy link
Member

LGTM

@KMKoushik KMKoushik merged commit ce8b780 into usesend:main Mar 4, 2026
6 of 7 checks passed
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.

2 participants