Skip to content

Commit

Permalink
feat: fetch GitHub latest tag server side and cache in Redis
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrumm committed Jan 22, 2024
1 parent 4865171 commit cda98c6
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 79 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Next.js
NEXT_PUBLIC_API_ROOT="http://localhost:3000/"
VERCEL_GIT_COMMIT_SHA="local"

# Axiom
NEXT_PUBLIC_AXIOM_LOG_LEVEL=debug
Expand Down
6 changes: 4 additions & 2 deletions src/env.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
// server-side environment variables schema ensures the app isn't built with invalid env vars.
Expand All @@ -14,6 +14,7 @@ export const env = createEnv({
'http://localhost:3000/',
'https://free-planning-poker.com/',
]),
VERCEL_GIT_COMMIT_SHA: z.string(),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
NEXT_PUBLIC_SENTRY_DSN: z.string().url(),
Expand Down Expand Up @@ -44,6 +45,7 @@ export const env = createEnv({
// DATABASE_URL: process.env.DATABASE_URL,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_API_ROOT: process.env.NEXT_PUBLIC_API_ROOT,
VERCEL_GIT_COMMIT_SHA: process.env.VERCEL_GIT_COMMIT_SHA,
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
Expand Down
45 changes: 45 additions & 0 deletions src/hooks/config-loader.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect } from 'react';

import { api } from 'fpp/utils/api';

import { useConfigStore } from 'fpp/store/config.store';

import { FeatureFlagType } from 'fpp/server/db/schema';

export const useConfigLoader = () => {
const setFeatureFlags = useConfigStore((state) => state.setFeatureFlags);

const { data: featureFlags, status: statusGetFeatureFlag } =
api.config.getFeatureFlags.useQuery(undefined, {
staleTime: Infinity,
refetchOnMount: false,
refetchOnWindowFocus: false,
retry: false,
});

const setLatestTag = useConfigStore((state) => state.setLatestTag);

const { data: latestTag, status: statusGetLatestTag } =
api.config.getLatestTag.useQuery(undefined, {
staleTime: Infinity,
refetchOnMount: false,
refetchOnWindowFocus: false,
retry: false,
});

useEffect(() => {
if (statusGetFeatureFlag === 'success') {
setFeatureFlags(featureFlags);
} else {
setFeatureFlags(
Object.keys(FeatureFlagType).map((name) => ({
name: name as keyof typeof FeatureFlagType,
enabled: false,
})),
);
}
if (statusGetLatestTag === 'success') {
setLatestTag(latestTag);
}
}, [statusGetFeatureFlag, statusGetLatestTag]);
};
6 changes: 5 additions & 1 deletion src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import 'normalize.css/normalize.css';
import { AxiomWebVitals } from 'next-axiom';

import { api } from 'fpp/utils/api';
import { useConfigLoader } from 'fpp/utils/config-loader.hook';

import { useConfigLoader } from 'fpp/hooks/config-loader.hook';

import Footer from 'fpp/components/layout/footer';

// const theme = createTheme({});

Expand All @@ -39,6 +42,7 @@ const MyApp: AppType = ({ Component, pageProps: { ...pageProps } }) => {
<main className={GeistSans.className}>
<Component {...pageProps} />
</main>
<Footer />
</MantineProvider>
);
};
Expand Down
3 changes: 0 additions & 3 deletions src/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import Document, { Head, Html, Main, NextScript } from 'next/document';

import { createGetInitialProps } from '@mantine/next';

import Footer from 'fpp/components/layout/footer';

const getInitialProps = createGetInitialProps();

export default class _Document extends Document {
Expand All @@ -19,7 +17,6 @@ export default class _Document extends Document {
<Main />
<NextScript />
</body>
<Footer />
</Html>
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { configRouter } from 'fpp/server/api/routers/config.router';
import { contactRouter } from 'fpp/server/api/routers/contact.router';
import { featureFlagRouter } from 'fpp/server/api/routers/feature-flag.router';
import { roadmapRouter } from 'fpp/server/api/routers/roadmap.router';
import { roomRouter } from 'fpp/server/api/routers/room.router';
import { trackingRouter } from 'fpp/server/api/routers/tracking.router';
Expand All @@ -11,7 +11,7 @@ import { roomStateRouter } from 'fpp/server/room-state/room-state.router';
export const appRouter = createTRPCRouter({
roomState: roomStateRouter,
contact: contactRouter,
featureFlag: featureFlagRouter,
config: configRouter,
room: roomRouter,
tracking: trackingRouter,
vote: voteRouter,
Expand Down
97 changes: 97 additions & 0 deletions src/server/api/routers/config.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { env } from 'fpp/env.mjs';

import * as Sentry from '@sentry/nextjs';
import { Redis } from '@upstash/redis';
import { sql } from 'drizzle-orm';

import { logEndpoint } from 'fpp/constants/logging.constant';

import { createTRPCRouter, publicProcedure } from 'fpp/server/api/trpc';
import { FeatureFlagType, featureFlags } from 'fpp/server/db/schema';

const redis = new Redis({
url: env.UPSTASH_REDIS_REST_URL_ROOM_STATE,
token: env.UPSTASH_REDIS_REST_TOKEN_ROOM_STATE,
});

export const configRouter = createTRPCRouter({
getFeatureFlags: publicProcedure.query(async ({ ctx: { db } }) => {
const activeFeatureFlags = (
await db
.select({ name: featureFlags.name })
.from(featureFlags)
.where(sql`${featureFlags.enabled} = 1`)
).map((row) => row.name);

return Object.keys(FeatureFlagType).map((name) => ({
name: name as keyof typeof FeatureFlagType,
enabled: activeFeatureFlags.includes(name),
}));
}),
getLatestTag: publicProcedure.query(async () => {
let latestTag: string | null = null;

// TODO: improve by combining the two redis calls into one

const redisLatestCommitSha = await redis.get(
`${env.NEXT_PUBLIC_NODE_ENV}:latestCommitSha`,
);

// Validate if the latest redis commit sha is the same as the current one to not fetch the latest tag from GitHub
if (
redisLatestCommitSha &&
redisLatestCommitSha === env.VERCEL_GIT_COMMIT_SHA
) {
latestTag = await redis.get('latestTag');
}

// Fetch the latest tag from GitHub if the redis commit sha is not the same as the current one or latestTag is null
if (!latestTag) {
await fetch(
'https://api.github.com/repos/jkrumm/free-planning-poker/tags',
).then(async (res) =>
res
.json()
.then(async (res: { name: string }[]) => {
console.log('res', res);

latestTag = res[0]!.name;

if (!latestTag) {
throw new Error('no latest tag found');
}

// Persist the latest tag and commit sha in redis
await redis.set('latestTag', latestTag);
await redis.set(
`${env.NEXT_PUBLIC_NODE_ENV}:latestCommitSha`,
env.VERCEL_GIT_COMMIT_SHA,
);

console.warn('fetched latest tag', {
redisLatestCommitSha,
commitSha: env.VERCEL_GIT_COMMIT_SHA as string,
nodeEnv: env.NEXT_PUBLIC_NODE_ENV,
latestTag,
});
})
.catch((e) => {
console.error('failed to fetch latest tag', e);
Sentry.captureException(e, {
tags: {
endpoint: logEndpoint.GET_LATEST_TAG,
},
extra: {
redisLatestCommitSha,
commitSha: env.VERCEL_GIT_COMMIT_SHA,
nodeEnv: env.NEXT_PUBLIC_NODE_ENV,
},
});
}),
);
}

// Fallback to 2.0.0 if latestTag is still null
return latestTag ?? '2.0.0';
}),
});
20 changes: 0 additions & 20 deletions src/server/api/routers/feature-flag.router.ts

This file was deleted.

47 changes: 0 additions & 47 deletions src/utils/config-loader.hook.ts

This file was deleted.

Loading

0 comments on commit cda98c6

Please sign in to comment.