Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: add dashboard analytics to sdk and public api by magicspon · Pull Request #353 · usesend/useSend · GitHub
Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add dashboard analytics to sdk and public api by magicspon · Pull Request #353 · usesend/useSend · GitHub
Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add dashboard analytics to sdk and public api by magicspon · Pull Request #353 · usesend/useSend · GitHub
Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: add dashboard analytics to sdk and public api by magicspon · Pull Request #353 · usesend/useSend · GitHub
Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add dashboard analytics to sdk and public api by magicspon · Pull Request #353 · usesend/useSend · GitHub
Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add dashboard analytics to sdk and public api by magicspon · Pull Request #353 · usesend/useSend · GitHub
Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: add dashboard analytics to sdk and public api by magicspon · Pull Request #353 · usesend/useSend · GitHub
Skip to content

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

Merged
KMKoushik merged 2 commits into
usesend: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 into
usesend:mainfrom
magicspon:feat-analytics-sdk-and-public-api

Conversation

@magicspon

@magicsponmagicspon commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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

vercelBot commented Feb 10, 2026

Copy link
Copy Markdown

@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

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
constdomain=
team.apiKey.domainId??
(domainIdParam ? Number(domainIdParam) : undefined);
if(domain!==undefined&&Number.isNaN(domain)){
returnc.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.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

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;

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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--) {

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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"),

@cubic-dev-aicubic-dev-aiBotFeb 10, 2026

Copy link
Copy Markdown
Contributor

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
Copy Markdown

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

@greptile-appsgreptile-appsBot 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.

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
Copy Markdown

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!

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
importemailTimeSeriesfrom"./api/analytics/email-time-series";
importreputationMetricsDatafrom"./api/analytics/reputation-metrics-data";
importemailTimeSeriesfrom"~/server/public-api/api/analytics/email-time-series";
importreputationMetricsDatafrom"~/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.

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

LGTM

@KMKoushik
KMKoushik merged commit ce8b780 into usesend:mainMar 4, 2026
6 of 7 checks passed
Sign up for freeto 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

@magicspon@KMKoushik