Skip to content

Commit

Permalink
fix: Get API usage by subscription period (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
amaury1093 authored Feb 14, 2022
1 parent 0b7eb60 commit 459165e
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 110 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dependencies": {
"@geist-ui/react": "^2.1.5",
"@geist-ui/react-icons": "^1.0.1",
"@sentry/nextjs": "^6.13.3",
"@sentry/nextjs": "^6.17.7",
"@stripe/stripe-js": "^1.22.0",
"@supabase/supabase-js": "^1.24.0",
"@types/cors": "^2.8.12",
Expand Down
6 changes: 4 additions & 2 deletions src/components/ApiUsage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ export function ApiUsage({ subscription }: ApiUsageProps): React.ReactElement {
return;
}

getApiUsageClient(user).then(setApiCalls).catch(sentryException);
}, [user]);
getApiUsageClient(user, subscription)
.then(setApiCalls)
.catch(sentryException);
}, [user, subscription]);

return (
<section>
Expand Down
19 changes: 14 additions & 5 deletions src/pages/api/v0/check_email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { withSentry } from '@sentry/nextjs';
import { User } from '@supabase/supabase-js';
import axios, { AxiosError } from 'axios';
import Cors from 'cors';
import { addMonths, differenceInMilliseconds } from 'date-fns';
import { NextApiRequest, NextApiResponse } from 'next';
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
import { getClientIp } from 'request-ip';

import { setRateLimitHeaders } from '../../../util/helpers';
import { sentryException } from '../../../util/sentry';
import { subApiMaxCalls } from '../../../util/subs';
import type { SupabaseCall, SupabaseUser } from '../../../util/supabaseClient';
import { SupabaseCall, SupabaseUser } from '../../../util/supabaseClient';
import {
getActiveSubscription,
getApiUsageServer,
Expand Down Expand Up @@ -117,10 +118,8 @@ const checkEmail = async (

// TODO instead of doing another round of network call, we should do a
// join for subscriptions and API calls inside getUserByApiToken.
const [sub, used] = await Promise.all([
getActiveSubscription(authUser),
getApiUsageServer(user),
]);
const sub = await getActiveSubscription(authUser);
const used = await getApiUsageServer(user, sub);

const max = subApiMaxCalls(sub);
if (used > max) {
Expand All @@ -131,6 +130,16 @@ const checkEmail = async (
return;
}

// Set rate limit headers.
const now = new Date();
const nextReset = sub ? sub.current_period_end : addMonths(now, 1);
const msDiff = differenceInMilliseconds(nextReset, now);
setRateLimitHeaders(
res,
new RateLimiterRes(max - used - 1, msDiff, used, undefined), // 1st arg has -1, because we just consumed 1 email.
max
);

return forwardToHeroku(req, res, user);
} catch (err) {
sentryException(err as Error);
Expand Down
30 changes: 26 additions & 4 deletions src/util/supabaseClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { PostgrestFilterBuilder } from '@supabase/postgrest-js';
import { createClient, User } from '@supabase/supabase-js';
import { subMonths } from 'date-fns';

export interface SupabasePrice {
active: boolean;
Expand Down Expand Up @@ -110,18 +111,39 @@ export function updateUserName(

// Get the api calls of a user in the past month. Same as
// `getApiUsageServer`, but for client usage.
export async function getApiUsageClient(user: User): Promise<number> {
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
export async function getApiUsageClient(
user: User,
subscription: SupabaseSubscription | null | undefined
): Promise<number> {
const { count, error } = await supabase
.from<SupabaseCall>('calls')
.select('*', { count: 'exact' })
.eq('user_id', user.id)
.gt('created_at', oneMonthAgo.toUTCString());
.gt('created_at', getUsageStartDate(subscription).toUTCString());

if (error) {
throw error;
}

if (count === null) {
throw new Error(
`Got null count in getApiUsageClient for user ${user.id}.`
);
}

return count || 0;
}

// Returns the start date of the usage metering.
// - If the user has an active subscription, it's the current period's start
// date.
// - If not, then it's 1 month rolling.
export function getUsageStartDate(
subscription: SupabaseSubscription | null | undefined
): Date {
if (!subscription) {
return subMonths(new Date(), 1);
}

return subscription.current_period_start;
}
22 changes: 15 additions & 7 deletions src/util/supabaseServer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createClient, User } from '@supabase/supabase-js';

import {
getUsageStartDate,
SupabaseCall,
SupabaseSubscription,
SupabaseUser,
Expand Down Expand Up @@ -55,18 +56,25 @@ export async function getActiveSubscription(

// Get the api calls of a user in the past month. Same as
// `getApiUsageClient`, but for server usage.
export async function getApiUsageServer(user: SupabaseUser): Promise<number> {
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const { data, error } = await supabaseAdmin
export async function getApiUsageServer(
user: SupabaseUser,
subscription: SupabaseSubscription | null | undefined
): Promise<number> {
const { count, error } = await supabaseAdmin
.from<SupabaseCall>('calls')
.select('*')
.select('*', { count: 'exact' })
.eq('user_id', user.id)
.gt('created_at', oneMonthAgo.toUTCString());
.gt('created_at', getUsageStartDate(subscription).toUTCString());

if (error) {
throw error;
}

return data?.length || 0;
if (count === null) {
throw new Error(
`Got null count in getApiUsageServer for user ${user.id}.`
);
}

return count || 0;
}
Loading

1 comment on commit 459165e

@vercel
Copy link

@vercel vercel bot commented on 459165e Feb 14, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.